feat(dashboard): Product create from - details (#7121)
**What** - First part of the product creation form. - New components: - ChipInput - Allows users to input chips into a input field. Chips are created by hitting the `,` or `Enter / Return` keys. Deleting a chip is done by hitting `Backspace` when the cursor is next to chip, or clicking the `X` button in the chip. Used for inputting option values. - SortableList - A sortable drag-n-drop list that allows the user to re-arrange the order of items. Used for re-arranging the ranking of variants. - ChipGroup - New re-usable component that is used to render a group of values as Chips. This should be used for SplitView form items. - CategoryCombobox - (WIP) Nested Combobox component for selecting multiple categories a product should be associated with. - New hooks: - useComboboxData - Hook for easily managing the state of comboboxes. - useDebouncedSearch - Hook for managing debounced search queries.
This commit is contained in:
1 parent
e42308557e
commit
fdee748eed
84 files changed
+2807
-1558
No files matched your search
@@ -18,6 +18,8 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ariakit/react": "^0.4.1",
|
"@ariakit/react": "^0.4.1",
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@hookform/resolvers": "3.3.2",
|
"@hookform/resolvers": "3.3.2",
|
||||||
"@medusajs/icons": "workspace:^",
|
"@medusajs/icons": "workspace:^",
|
||||||
"@medusajs/ui": "workspace:^",
|
"@medusajs/ui": "workspace:^",
|
||||||
@@ -36,10 +38,10 @@
|
|||||||
"match-sorter": "^6.3.4",
|
"match-sorter": "^6.3.4",
|
||||||
"medusa-react": "workspace:^",
|
"medusa-react": "workspace:^",
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"react": "18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
"react-currency-input-field": "^3.6.11",
|
"react-currency-input-field": "^3.6.11",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "7.49.1",
|
"react-hook-form": "7.49.1",
|
||||||
"react-i18next": "13.5.0",
|
"react-i18next": "13.5.0",
|
||||||
"react-jwt": "^1.2.0",
|
"react-jwt": "^1.2.0",
|
||||||
@@ -53,8 +55,8 @@
|
|||||||
"@medusajs/ui-preset": "workspace:^",
|
"@medusajs/ui-preset": "workspace:^",
|
||||||
"@medusajs/vite-plugin-extension": "workspace:^",
|
"@medusajs/vite-plugin-extension": "workspace:^",
|
||||||
"@types/node": "^20.11.15",
|
"@types/node": "^20.11.15",
|
||||||
"@types/react": "18.2.43",
|
"@types/react": "^18.2.79",
|
||||||
"@types/react-dom": "18.2.17",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@vitejs/plugin-react": "4.2.1",
|
"@vitejs/plugin-react": "4.2.1",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
|
|||||||
@@ -41,14 +41,13 @@
|
|||||||
"typeToConfirm": "Please type {val} to confirm:",
|
"typeToConfirm": "Please type {val} to confirm:",
|
||||||
"noResultsTitle": "No results",
|
"noResultsTitle": "No results",
|
||||||
"noResultsMessage": "Try changing the filters or search query",
|
"noResultsMessage": "Try changing the filters or search query",
|
||||||
|
"noSearchResults": "No search results",
|
||||||
|
"noSearchResultsFor": "No search results for <0>'{{query}}'</0>",
|
||||||
"noRecordsTitle": "No records",
|
"noRecordsTitle": "No records",
|
||||||
"noRecordsMessage": "There are no records to show",
|
"noRecordsMessage": "There are no records to show",
|
||||||
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
||||||
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.",
|
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.",
|
||||||
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved.",
|
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved."
|
||||||
"timeline": "Timeline",
|
|
||||||
"success": "Success",
|
|
||||||
"error": "Error"
|
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"mustBeInt": "The value must be a whole number.",
|
"mustBeInt": "The value must be a whole number.",
|
||||||
@@ -162,8 +161,27 @@
|
|||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"domain": "Products",
|
"domain": "Products",
|
||||||
"createProductTitle": "Create Product",
|
"create": {
|
||||||
"createProductHint": "Create a new product to sell in your store.",
|
"header": "Create Product",
|
||||||
|
"hint": "Create a new product to sell in your store.",
|
||||||
|
"tabs": {
|
||||||
|
"details": "Details",
|
||||||
|
"variants": "Variants"
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"header": "Variants",
|
||||||
|
"productVariants": {
|
||||||
|
"label": "Product variants",
|
||||||
|
"hint": "Variants left unchecked won't be created. This ranking will affect how the variants are ranked in your frontend.",
|
||||||
|
"alert": "Add options to create variants."
|
||||||
|
},
|
||||||
|
"productOptions": {
|
||||||
|
"label": "Product options",
|
||||||
|
"hint": "Define the options for the product, e.g. color, size, etc."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"successToast": "Product {{title}} was successfully created."
|
||||||
|
},
|
||||||
"deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.",
|
"deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.",
|
||||||
"variants": "Variants",
|
"variants": "Variants",
|
||||||
"attributes": "Attributes",
|
"attributes": "Attributes",
|
||||||
@@ -296,10 +314,12 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"header": "Options",
|
"header": "Options",
|
||||||
"edit": {
|
"edit": {
|
||||||
"header": "Edit Option"
|
"header": "Edit Option",
|
||||||
|
"successToast": "Option {{title}} was successfully updated."
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"header": "Create Option"
|
"header": "Create Option",
|
||||||
|
"successToast": "Option {{title}} was successfully created."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
@@ -1579,7 +1599,8 @@
|
|||||||
"unitPrice": "Unit price",
|
"unitPrice": "Unit price",
|
||||||
"startDate": "Start date",
|
"startDate": "Start date",
|
||||||
"endDate": "End date",
|
"endDate": "End date",
|
||||||
"draft": "Draft"
|
"draft": "Draft",
|
||||||
|
"values": "Values"
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"warnings": {
|
"warnings": {
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
|
import { Button, clx } from "@medusajs/ui"
|
||||||
|
import { Children, PropsWithChildren, createContext, useContext } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type ChipGroupVariant = "base" | "component"
|
||||||
|
|
||||||
|
type ChipGroupProps = PropsWithChildren<{
|
||||||
|
onClearAll?: () => void
|
||||||
|
onRemove?: (index: number) => void
|
||||||
|
variant?: ChipGroupVariant
|
||||||
|
className?: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
type GroupContextValue = {
|
||||||
|
onRemove?: (index: number) => void
|
||||||
|
variant: ChipGroupVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupContext = createContext<GroupContextValue | null>(null)
|
||||||
|
|
||||||
|
const useGroupContext = () => {
|
||||||
|
const context = useContext(GroupContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useGroupContext must be used within a ChipGroup component")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const Group = ({
|
||||||
|
onClearAll,
|
||||||
|
onRemove,
|
||||||
|
variant = "component",
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: ChipGroupProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const showClearAll = !!onClearAll && Children.count(children) > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GroupContext.Provider value={{ onRemove, variant }}>
|
||||||
|
<ul
|
||||||
|
role="application"
|
||||||
|
className={clx("flex flex-wrap items-center gap-2", className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showClearAll && (
|
||||||
|
<li>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
type="button"
|
||||||
|
onClick={onClearAll}
|
||||||
|
className="text-ui-fg-muted active:text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
{t("actions.clearAll")}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</GroupContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChipProps = PropsWithChildren<{
|
||||||
|
index: number
|
||||||
|
className?: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
const Chip = ({ index, className, children }: ChipProps) => {
|
||||||
|
const { onRemove, variant } = useGroupContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-component shadow-borders-base flex items-center divide-x overflow-hidden rounded-md",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-component": variant === "component",
|
||||||
|
"bg-ui-bg-base-": variant === "base",
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="txt-compact-small-plus flex items-center justify-center px-2 py-1">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
{!!onRemove && (
|
||||||
|
<button
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
type="button"
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-muted active:text-ui-fg-subtle transition-fg flex items-center justify-center p-1",
|
||||||
|
{
|
||||||
|
"hover:bg-ui-bg-component-hover active:bg-ui-bg-component-pressed":
|
||||||
|
variant === "component",
|
||||||
|
"hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed":
|
||||||
|
variant === "base",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<XMarkMini />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChipGroup = Object.assign(Group, { Chip })
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./chip-group"
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./keypair"
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import { Plus, Trash } from "@medusajs/icons"
|
|
||||||
import { Button, Input, Table } from "@medusajs/ui"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
interface KeyPair {
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KeypairProps {
|
|
||||||
labels: {
|
|
||||||
add: string
|
|
||||||
key?: string
|
|
||||||
value?: string
|
|
||||||
}
|
|
||||||
value: KeyPair[]
|
|
||||||
onChange: (value: KeyPair[]) => void
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Keypair = ({ labels, onChange, value }: KeypairProps) => {
|
|
||||||
const addKeyPair = () => {
|
|
||||||
onChange([...value, { key: ``, value: `` }])
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteKeyPair = (index: number) => {
|
|
||||||
return () => {
|
|
||||||
onChange(value.filter((_, i) => i !== index))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyChange = (index: number) => {
|
|
||||||
return (key: string) => {
|
|
||||||
const newArr = value.map((pair, i) => {
|
|
||||||
if (i === index) {
|
|
||||||
return { key, value: pair.value }
|
|
||||||
}
|
|
||||||
return pair
|
|
||||||
})
|
|
||||||
|
|
||||||
onChange(newArr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onValueChange = (index: number) => {
|
|
||||||
return (val: string) => {
|
|
||||||
const newArr = value.map((pair, i) => {
|
|
||||||
if (i === index) {
|
|
||||||
return { key: pair.key, value: val }
|
|
||||||
}
|
|
||||||
return pair
|
|
||||||
})
|
|
||||||
|
|
||||||
onChange(newArr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Table className="w-full">
|
|
||||||
<Table.Header className="border-t-0">
|
|
||||||
<Table.Row>
|
|
||||||
<Table.HeaderCell>{labels.key}</Table.HeaderCell>
|
|
||||||
<Table.HeaderCell>{labels.value}</Table.HeaderCell>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{value.map((pair, index) => {
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
labels={labels}
|
|
||||||
field={pair}
|
|
||||||
updateKey={onKeyChange(index)}
|
|
||||||
updateValue={onValueChange(index)}
|
|
||||||
onDelete={deleteKeyPair(index)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="small"
|
|
||||||
type="button"
|
|
||||||
className="w-full mt-4"
|
|
||||||
onClick={addKeyPair}
|
|
||||||
>
|
|
||||||
<Plus />
|
|
||||||
{labels.add}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FieldProps = {
|
|
||||||
field: KeyPair
|
|
||||||
labels: {
|
|
||||||
key?: string
|
|
||||||
value?: string
|
|
||||||
}
|
|
||||||
updateKey: (key: string) => void
|
|
||||||
updateValue: (value: string) => void
|
|
||||||
onDelete: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Field: React.FC<FieldProps> = ({
|
|
||||||
field,
|
|
||||||
updateKey,
|
|
||||||
updateValue,
|
|
||||||
onDelete,
|
|
||||||
}) => {
|
|
||||||
const [key, setKey] = useState(field.key)
|
|
||||||
const [value, setValue] = useState(field.value)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell className="!p-0 h-0">
|
|
||||||
<Input
|
|
||||||
className="rounded-none bg-transparent"
|
|
||||||
onBlur={() => updateKey(key)}
|
|
||||||
value={key}
|
|
||||||
onChange={(e) => {
|
|
||||||
setKey(e.currentTarget.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="!p-0 h-0">
|
|
||||||
<Input
|
|
||||||
className="rounded-none bg-transparent"
|
|
||||||
onBlur={() => updateValue(value)}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => {
|
|
||||||
setValue(e.currentTarget.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="!p-0 h-0 border-r">
|
|
||||||
<Button
|
|
||||||
variant="transparent"
|
|
||||||
size="small"
|
|
||||||
type="button"
|
|
||||||
onClick={onDelete}
|
|
||||||
>
|
|
||||||
<Trash />
|
|
||||||
</Button>
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./list"
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Checkbox, Text } from "@medusajs/ui"
|
|
||||||
|
|
||||||
export interface ListProps<T> {
|
|
||||||
options: { title: string; value: T }[]
|
|
||||||
value?: T[]
|
|
||||||
onChange?: (value: T[]) => void
|
|
||||||
compare?: (a: T, b: T) => boolean
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const List = <T extends any>({
|
|
||||||
options,
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
compare,
|
|
||||||
disabled,
|
|
||||||
}: ListProps<T>) => {
|
|
||||||
if (options.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-row justify-center border divide-y rounded-lg">
|
|
||||||
{options.map((option) => {
|
|
||||||
return (
|
|
||||||
<div className="flex p-4 gap-x-4">
|
|
||||||
{onChange && value !== undefined && (
|
|
||||||
<Checkbox
|
|
||||||
disabled={disabled}
|
|
||||||
checked={value.some(
|
|
||||||
(v) => compare?.(v, option.value) ?? v === option.value
|
|
||||||
)}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (checked) {
|
|
||||||
onChange([...value, option.value])
|
|
||||||
} else {
|
|
||||||
onChange(
|
|
||||||
value.filter(
|
|
||||||
(v) =>
|
|
||||||
!(compare?.(v, option.value) ?? v === option.value)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text key={option.title}>{option.title}</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./product-table-cells"
|
|
||||||
-130
@@ -1,130 +0,0 @@
|
|||||||
import { SalesChannel } from "@medusajs/medusa"
|
|
||||||
import {
|
|
||||||
ProductCollectionDTO,
|
|
||||||
ProductDTO,
|
|
||||||
ProductVariantDTO,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
import { StatusBadge, Text } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Thumbnail } from "../thumbnail"
|
|
||||||
|
|
||||||
export const ProductVariantCell = ({
|
|
||||||
variants,
|
|
||||||
}: {
|
|
||||||
variants: ProductVariantDTO[] | null
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
if (!variants || !variants.length) {
|
|
||||||
return (
|
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
|
||||||
-
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text size="small" className="text-ui-fg-base">
|
|
||||||
{t("products.variantCount", {
|
|
||||||
count: variants.length,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProductStatusCell = ({
|
|
||||||
status,
|
|
||||||
}: {
|
|
||||||
status: ProductDTO["status"]
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const color = {
|
|
||||||
draft: "grey",
|
|
||||||
published: "green",
|
|
||||||
rejected: "red",
|
|
||||||
proposed: "blue",
|
|
||||||
}[status] as "grey" | "green" | "red" | "blue"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatusBadge color={color}>
|
|
||||||
{t(`products.productStatus.${status}`)}
|
|
||||||
</StatusBadge>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProductAvailabilityCell = ({
|
|
||||||
salesChannels,
|
|
||||||
}: {
|
|
||||||
salesChannels: SalesChannel[] | null
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
if (!salesChannels || salesChannels.length === 0) {
|
|
||||||
return (
|
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
|
||||||
-
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (salesChannels.length < 3) {
|
|
||||||
return (
|
|
||||||
<Text size="small" className="text-ui-fg-base">
|
|
||||||
{salesChannels.map((sc) => sc.name).join(", ")}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<Text size="small" className="text-ui-fg-base">
|
|
||||||
<span>
|
|
||||||
{salesChannels
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((sc) => sc.name)
|
|
||||||
.join(", ")}
|
|
||||||
</span>{" "}
|
|
||||||
<span>
|
|
||||||
{t("general.plusCountMore", {
|
|
||||||
count: salesChannels.length - 2,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProductTitleCell = ({ product }: { product: ProductDTO }) => {
|
|
||||||
const thumbnail = product.thumbnail
|
|
||||||
const title = product.title
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-x-3">
|
|
||||||
<Thumbnail src={thumbnail} alt={`Thumbnail image of ${title}`} />
|
|
||||||
<Text size="small" className="text-ui-fg-base">
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProductCollectionCell = ({
|
|
||||||
collection,
|
|
||||||
}: {
|
|
||||||
collection: ProductCollectionDTO | null
|
|
||||||
}) => {
|
|
||||||
if (!collection) {
|
|
||||||
return (
|
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
|
||||||
-
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text size="small" className="text-ui-fg-base">
|
|
||||||
{collection.title}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./sortable-list"
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import {
|
||||||
|
Active,
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragOverlay,
|
||||||
|
DragStartEvent,
|
||||||
|
DraggableSyntheticListeners,
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
defaultDropAnimationSideEffects,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
type DropAnimation,
|
||||||
|
type UniqueIdentifier,
|
||||||
|
} from "@dnd-kit/core"
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
arrayMove,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
useSortable,
|
||||||
|
} from "@dnd-kit/sortable"
|
||||||
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
|
import { DotsSix } from "@medusajs/icons"
|
||||||
|
import { IconButton, clx } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
Fragment,
|
||||||
|
PropsWithChildren,
|
||||||
|
ReactNode,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
|
||||||
|
type SortableBaseItem = {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortableListProps<TItem extends SortableBaseItem> {
|
||||||
|
items: TItem[]
|
||||||
|
onChange: (items: TItem[]) => void
|
||||||
|
renderItem: (item: TItem, index: number) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const List = <TItem extends SortableBaseItem>({
|
||||||
|
items,
|
||||||
|
onChange,
|
||||||
|
renderItem,
|
||||||
|
}: SortableListProps<TItem>) => {
|
||||||
|
const [active, setActive] = useState<Active | null>(null)
|
||||||
|
|
||||||
|
const [activeItem, activeIndex] = useMemo(() => {
|
||||||
|
if (active === null) {
|
||||||
|
return [null, null]
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = items.findIndex(({ id }) => id === active.id)
|
||||||
|
|
||||||
|
return [items[index], index]
|
||||||
|
}, [active, items])
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||||
|
setActive(active)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = ({ active, over }: DragEndEvent) => {
|
||||||
|
if (over && active.id !== over.id) {
|
||||||
|
const activeIndex = items.findIndex(({ id }) => id === active.id)
|
||||||
|
const overIndex = items.findIndex(({ id }) => id === over.id)
|
||||||
|
|
||||||
|
onChange(arrayMove(items, activeIndex, overIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
setActive(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragCancel = () => {
|
||||||
|
setActive(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
|
>
|
||||||
|
<Overlay>
|
||||||
|
{activeItem && activeIndex !== null
|
||||||
|
? renderItem(activeItem, activeIndex)
|
||||||
|
: null}
|
||||||
|
</Overlay>
|
||||||
|
<SortableContext items={items}>
|
||||||
|
<ul
|
||||||
|
role="application"
|
||||||
|
className="flex list-inside list-none list-image-none flex-col p-0"
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Fragment key={item.id}>{renderItem(item, index)}</Fragment>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropAnimationConfig: DropAnimation = {
|
||||||
|
sideEffects: defaultDropAnimationSideEffects({
|
||||||
|
styles: {
|
||||||
|
active: {
|
||||||
|
opacity: "0.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortableOverlayProps = PropsWithChildren
|
||||||
|
|
||||||
|
const Overlay = ({ children }: SortableOverlayProps) => {
|
||||||
|
return (
|
||||||
|
<DragOverlay
|
||||||
|
className="shadow-elevation-card-hover overflow-hidden rounded-md [&>li]:border-b-0"
|
||||||
|
dropAnimation={dropAnimationConfig}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DragOverlay>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortableItemProps<TItem extends SortableBaseItem> = PropsWithChildren<{
|
||||||
|
id: TItem["id"]
|
||||||
|
className?: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
type SortableItemContextValue = {
|
||||||
|
attributes: Record<string, any>
|
||||||
|
listeners: DraggableSyntheticListeners
|
||||||
|
ref: (node: HTMLElement | null) => void
|
||||||
|
isDragging: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableItemContext = createContext<SortableItemContextValue | null>(null)
|
||||||
|
|
||||||
|
const useSortableItemContext = () => {
|
||||||
|
const context = useContext(SortableItemContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useSortableItemContext must be used within a SortableItemContext"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item = <TItem extends SortableBaseItem>({
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: SortableItemProps<TItem>) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
isDragging,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
setActivatorNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
} = useSortable({ id })
|
||||||
|
|
||||||
|
const context = useMemo(
|
||||||
|
() => ({
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
ref: setActivatorNodeRef,
|
||||||
|
isDragging,
|
||||||
|
}),
|
||||||
|
[attributes, listeners, setActivatorNodeRef, isDragging]
|
||||||
|
)
|
||||||
|
|
||||||
|
const style: CSSProperties = {
|
||||||
|
opacity: isDragging ? 0.4 : undefined,
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableItemContext.Provider value={context}>
|
||||||
|
<li
|
||||||
|
className={clx("transition-fg flex flex-1 list-none", className)}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
</SortableItemContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragHandle = () => {
|
||||||
|
const { attributes, listeners, ref } = useSortableItemContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
variant="transparent"
|
||||||
|
size="small"
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
ref={ref}
|
||||||
|
className="cursor-grab touch-none active:cursor-grabbing"
|
||||||
|
>
|
||||||
|
<DotsSix className="text-ui-fg-muted" />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SortableList = Object.assign(List, {
|
||||||
|
Item,
|
||||||
|
DragHandle,
|
||||||
|
})
|
||||||
@@ -5,8 +5,8 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { Control } from "react-hook-form"
|
import { Control } from "react-hook-form"
|
||||||
import { AddressSchema } from "../../../lib/schemas"
|
import { AddressSchema } from "../../../lib/schemas"
|
||||||
import { CountrySelect } from "../../common/country-select"
|
|
||||||
import { Form } from "../../common/form"
|
import { Form } from "../../common/form"
|
||||||
|
import { CountrySelect } from "../../inputs/country-select"
|
||||||
|
|
||||||
type AddressFieldValues = z.infer<typeof AddressSchema>
|
type AddressFieldValues = z.infer<typeof AddressSchema>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,9 +16,9 @@ import {
|
|||||||
getOrderPaymentStatus,
|
getOrderPaymentStatus,
|
||||||
} from "../../../lib/order-helpers"
|
} from "../../../lib/order-helpers"
|
||||||
import { TransferOwnershipSchema } from "../../../lib/schemas"
|
import { TransferOwnershipSchema } from "../../../lib/schemas"
|
||||||
import { Combobox } from "../../common/combobox"
|
|
||||||
import { Form } from "../../common/form"
|
import { Form } from "../../common/form"
|
||||||
import { Skeleton } from "../../common/skeleton"
|
import { Skeleton } from "../../common/skeleton"
|
||||||
|
import { Combobox } from "../../inputs/combobox"
|
||||||
|
|
||||||
type TransferOwnerShipFieldValues = z.infer<typeof TransferOwnershipSchema>
|
type TransferOwnerShipFieldValues = z.infer<typeof TransferOwnershipSchema>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
|
import { Badge, clx } from "@medusajs/ui"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
import {
|
||||||
|
FocusEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
|
||||||
|
type ChipInputProps = {
|
||||||
|
value?: string[]
|
||||||
|
onChange?: (value: string[]) => void
|
||||||
|
onBlur?: () => void
|
||||||
|
name?: string
|
||||||
|
disabled?: boolean
|
||||||
|
allowDuplicates?: boolean
|
||||||
|
showRemove?: boolean
|
||||||
|
variant?: "base" | "contrast"
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChipInput = forwardRef<HTMLInputElement, ChipInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
disabled,
|
||||||
|
name,
|
||||||
|
showRemove = true,
|
||||||
|
variant = "base",
|
||||||
|
allowDuplicates = false,
|
||||||
|
className,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const innerRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const isControlledRef = useRef(typeof value !== "undefined")
|
||||||
|
const isControlled = isControlledRef.current
|
||||||
|
|
||||||
|
const [uncontrolledValue, setUncontrolledValue] = useState<string[]>([])
|
||||||
|
|
||||||
|
useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
|
||||||
|
ref,
|
||||||
|
() => innerRef.current
|
||||||
|
)
|
||||||
|
|
||||||
|
const [duplicateIndex, setDuplicateIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const chips = isControlled ? (value as string[]) : uncontrolledValue
|
||||||
|
|
||||||
|
const handleAddChip = (chip: string) => {
|
||||||
|
const cleanValue = chip.trim()
|
||||||
|
|
||||||
|
if (!cleanValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowDuplicates && chips.includes(cleanValue)) {
|
||||||
|
setDuplicateIndex(chips.indexOf(cleanValue))
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setDuplicateIndex(null)
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange?.([...chips, cleanValue])
|
||||||
|
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledValue([...chips, cleanValue])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveChip = (chip: string) => {
|
||||||
|
onChange?.(chips.filter((v) => v !== chip))
|
||||||
|
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledValue(chips.filter((v) => v !== chip))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
|
||||||
|
onBlur?.()
|
||||||
|
|
||||||
|
if (e.target.value) {
|
||||||
|
handleAddChip(e.target.value)
|
||||||
|
e.target.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter" || e.key === ",") {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!innerRef.current?.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddChip(innerRef.current?.value ?? "")
|
||||||
|
innerRef.current.value = ""
|
||||||
|
innerRef.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Backspace" && innerRef.current?.value === "") {
|
||||||
|
handleRemoveChip(chips[chips.length - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a shake animation using framer motion
|
||||||
|
const shake = {
|
||||||
|
x: [0, -2, 2, -2, 2, 0],
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"shadow-borders-base flex min-h-8 flex-wrap items-center gap-1 rounded-md px-2 py-1.5",
|
||||||
|
"transition-fg focus-within:shadow-borders-interactive-with-active",
|
||||||
|
"has-[input:disabled]:bg-ui-bg-disabled has-[input:disabled]:text-ui-fg-disabled has-[input:disabled]:cursor-not-allowed",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover":
|
||||||
|
variant === "contrast",
|
||||||
|
"bg-ui-bg-field hover:bg-ui-bg-field-hover": variant === "base",
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => innerRef.current?.focus()}
|
||||||
|
>
|
||||||
|
{chips.map((v, index) => {
|
||||||
|
return (
|
||||||
|
<AnimatePresence key={`${v}-${index}`}>
|
||||||
|
<Badge
|
||||||
|
size="2xsmall"
|
||||||
|
className={clx("gap-x-0.5 pl-1.5 pr-1.5", {
|
||||||
|
"transition-fg pr-1": showRemove,
|
||||||
|
"shadow-borders-focus": index === duplicateIndex,
|
||||||
|
})}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={index === duplicateIndex ? shake : undefined}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
{showRemove && (
|
||||||
|
<button
|
||||||
|
tabIndex={-1}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveChip(v)}
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-subtle transition-fg outline-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<XMarkMini />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</Badge>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<input
|
||||||
|
className={clx(
|
||||||
|
"caret-ui-fg-base text-ui-fg-base txt-compact-small flex-1 appearance-none bg-transparent",
|
||||||
|
"disabled:text-ui-fg-disabled disabled:cursor-not-allowed",
|
||||||
|
"focus:outline-none",
|
||||||
|
"placeholder:text-ui-fg-muted"
|
||||||
|
)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
ref={innerRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ChipInput.displayName = "ChipInput"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./chip-input"
|
||||||
+1
-1
@@ -30,7 +30,7 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { genericForwardRef } from "../generic-forward-ref"
|
import { genericForwardRef } from "../../common/generic-forward-ref"
|
||||||
|
|
||||||
type ComboboxOption = {
|
type ComboboxOption = {
|
||||||
value: string
|
value: string
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+5
@@ -183,6 +183,8 @@ export const DataTableRoot = <TData,>({
|
|||||||
const to = navigateTo ? navigateTo(row) : undefined
|
const to = navigateTo ? navigateTo(row) : undefined
|
||||||
const isRowDisabled = hasSelect && !row.getCanSelect()
|
const isRowDisabled = hasSelect && !row.getCanSelect()
|
||||||
|
|
||||||
|
const isOdd = row.depth % 2 !== 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Row
|
<Table.Row
|
||||||
key={row.id}
|
key={row.id}
|
||||||
@@ -190,6 +192,7 @@ export const DataTableRoot = <TData,>({
|
|||||||
className={clx(
|
className={clx(
|
||||||
"transition-fg group/row [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
"transition-fg group/row [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||||
{
|
{
|
||||||
|
"bg-ui-bg-subtle hover:bg-ui-bg-subtle-hover": isOdd,
|
||||||
"cursor-pointer": !!to,
|
"cursor-pointer": !!to,
|
||||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
row.getIsSelected(),
|
row.getIsSelected(),
|
||||||
@@ -228,6 +231,8 @@ export const DataTableRoot = <TData,>({
|
|||||||
className={clx({
|
className={clx({
|
||||||
"bg-ui-bg-base group-data-[selected=true]/row:bg-ui-bg-highlight group-data-[selected=true]/row:group-hover/row:bg-ui-bg-highlight-hover group-hover/row:bg-ui-bg-base-hover transition-fg sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
|
"bg-ui-bg-base group-data-[selected=true]/row:bg-ui-bg-highlight group-data-[selected=true]/row:group-hover/row:bg-ui-bg-highlight-hover group-hover/row:bg-ui-bg-base-hover transition-fg sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
|
||||||
isStickyCell,
|
isStickyCell,
|
||||||
|
"bg-ui-bg-subtle group-hover/row:bg-ui-bg-subtle-hover":
|
||||||
|
isOdd && isStickyCell,
|
||||||
"left-[68px]":
|
"left-[68px]":
|
||||||
isStickyCell && hasSelect && !isSelectCell,
|
isStickyCell && hasSelect && !isSelectCell,
|
||||||
"after:bg-ui-border-base":
|
"after:bg-ui-border-base":
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { TriangleRightMini } from "@medusajs/icons"
|
||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { IconButton, Text, clx } from "@medusajs/ui"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { StatusCell } from "../../../components/table/table-cells/common/status-cell"
|
||||||
|
import {
|
||||||
|
TextCell,
|
||||||
|
TextHeader,
|
||||||
|
} from "../../../components/table/table-cells/common/text-cell"
|
||||||
|
import {
|
||||||
|
getCategoryPath,
|
||||||
|
getIsActiveProps,
|
||||||
|
getIsInternalProps,
|
||||||
|
} from "../../../v2-routes/categories/common/utils"
|
||||||
|
|
||||||
|
const columnHelper =
|
||||||
|
createColumnHelper<AdminProductCategoryResponse["product_category"]>()
|
||||||
|
|
||||||
|
export const useCategoryTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: () => <TextHeader text={t("fields.name")} />,
|
||||||
|
cell: ({ getValue, row }) => {
|
||||||
|
const expandHandler = row.getToggleExpandedHandler()
|
||||||
|
|
||||||
|
if (row.original.parent_category !== undefined) {
|
||||||
|
const path = getCategoryPath(row.original)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center gap-1 overflow-hidden">
|
||||||
|
{path.map((chip, index) => (
|
||||||
|
<div
|
||||||
|
key={chip.id}
|
||||||
|
className={clx("overflow-hidden", {
|
||||||
|
"text-ui-fg-muted flex items-center gap-x-1":
|
||||||
|
index !== path.length - 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Text size="small" leading="compact" className="truncate">
|
||||||
|
{chip.name}
|
||||||
|
</Text>
|
||||||
|
{index !== path.length - 1 && (
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
/
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center gap-x-3 overflow-hidden">
|
||||||
|
<div className="flex size-7 items-center justify-center">
|
||||||
|
{row.getCanExpand() ? (
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
expandHandler()
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
<TriangleRightMini
|
||||||
|
className={clx({
|
||||||
|
"rotate-90 transition-transform will-change-transform":
|
||||||
|
row.getIsExpanded(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<span className="truncate">{getValue()}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("handle", {
|
||||||
|
header: () => <TextHeader text={t("fields.handle")} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
return <TextCell text={`/${getValue()}`} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("is_active", {
|
||||||
|
header: () => <TextHeader text={t("fields.status")} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const { color, label } = getIsActiveProps(getValue(), t)
|
||||||
|
|
||||||
|
return <StatusCell color={color}>{label}</StatusCell>
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("is_internal", {
|
||||||
|
header: () => <TextHeader text={t("categories.fields.visibility")} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const { color, label } = getIsInternalProps(getValue(), t)
|
||||||
|
|
||||||
|
return <StatusCell color={color}>{label}</StatusCell>
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,7 +30,6 @@ export const useProductTableFilters = (
|
|||||||
{
|
{
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
fields: "id,name",
|
fields: "id,name",
|
||||||
expand: "",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !isSalesChannelExcluded,
|
enabled: !isSalesChannelExcluded,
|
||||||
|
|||||||
@@ -1,71 +1,91 @@
|
|||||||
import { QueryKey, useInfiniteQuery } from "@tanstack/react-query"
|
import {
|
||||||
import debounce from "lodash/debounce"
|
QueryKey,
|
||||||
import { useCallback, useEffect, useState } from "react"
|
keepPreviousData,
|
||||||
|
useInfiniteQuery,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
|
import { useDebouncedSearch } from "./use-debounced-search"
|
||||||
|
|
||||||
type Params = {
|
type ComboboxExternalData = {
|
||||||
q: string
|
|
||||||
limit: number
|
|
||||||
offset: number
|
offset: number
|
||||||
}
|
limit: number
|
||||||
|
|
||||||
type Page = {
|
|
||||||
count: number
|
count: number
|
||||||
offset: number
|
|
||||||
limit: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UseComboboxDataProps<TParams extends Params, TRes extends Page> = {
|
type ComboboxQueryParams = {
|
||||||
fetcher: (params: TParams) => Promise<TRes>
|
q?: string
|
||||||
params?: Omit<TParams, "q" | "limit" | "offset">
|
offset?: number
|
||||||
queryKey: QueryKey
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const useComboboxData = <
|
||||||
* Hook for fetching infinite data for a combobox.
|
TResponse extends ComboboxExternalData,
|
||||||
*/
|
TParams extends ComboboxQueryParams
|
||||||
export const useComboboxData = <TParams extends Params, TRes extends Page>({
|
>({
|
||||||
fetcher,
|
|
||||||
params,
|
|
||||||
queryKey,
|
queryKey,
|
||||||
}: UseComboboxDataProps<TParams, TRes>) => {
|
queryFn,
|
||||||
const [query, setQuery] = useState("")
|
getOptions,
|
||||||
const [debouncedQuery, setDebouncedQuery] = useState("")
|
defaultValue,
|
||||||
|
defaultValueKey,
|
||||||
|
pageSize = 10,
|
||||||
|
}: {
|
||||||
|
queryKey: QueryKey
|
||||||
|
queryFn: (params: TParams) => Promise<TResponse>
|
||||||
|
getOptions: (data: TResponse) => { label: string; value: string }[]
|
||||||
|
defaultValueKey?: keyof TParams
|
||||||
|
defaultValue?: string | string[]
|
||||||
|
pageSize?: number
|
||||||
|
}) => {
|
||||||
|
const { searchValue, onSearchValueChange, query } = useDebouncedSearch()
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const queryIntialDataBy = defaultValueKey || "id"
|
||||||
const debouncedUpdate = useCallback(
|
const { data: initialData } = useQuery({
|
||||||
debounce((query) => setDebouncedQuery(query), 300),
|
queryKey: queryKey,
|
||||||
[]
|
queryFn: async () => {
|
||||||
)
|
return queryFn({
|
||||||
|
[queryIntialDataBy]: defaultValue,
|
||||||
useEffect(() => {
|
limit: Array.isArray(defaultValue) ? defaultValue.length : 1,
|
||||||
debouncedUpdate(query)
|
|
||||||
|
|
||||||
return () => debouncedUpdate.cancel()
|
|
||||||
}, [query, debouncedUpdate])
|
|
||||||
|
|
||||||
const data = useInfiniteQuery(
|
|
||||||
[...queryKey, debouncedQuery],
|
|
||||||
async ({ pageParam = 0 }) => {
|
|
||||||
const res = await fetcher({
|
|
||||||
q: debouncedQuery,
|
|
||||||
limit: 10,
|
|
||||||
offset: pageParam,
|
|
||||||
...params,
|
|
||||||
} as TParams)
|
} as TParams)
|
||||||
return res
|
|
||||||
},
|
},
|
||||||
{
|
enabled: !!defaultValue,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data, ...rest } = useInfiniteQuery({
|
||||||
|
queryKey: [...queryKey, query],
|
||||||
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
|
return queryFn({
|
||||||
|
q: query,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: pageParam,
|
||||||
|
} as TParams)
|
||||||
|
},
|
||||||
|
initialPageParam: 0,
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
const morePages = lastPage.count > lastPage.offset + lastPage.limit
|
const moreItemsExist = lastPage.count > lastPage.offset + lastPage.limit
|
||||||
return morePages ? lastPage.offset + lastPage.limit : undefined
|
return moreItemsExist ? lastPage.offset + lastPage.limit : undefined
|
||||||
},
|
},
|
||||||
keepPreviousData: true,
|
placeholderData: keepPreviousData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = data?.pages.flatMap((page) => getOptions(page)) ?? []
|
||||||
|
const defaultOptions = initialData ? getOptions(initialData) : []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are no options and the query is empty, then the combobox should be disabled,
|
||||||
|
* as there is no data to search for.
|
||||||
|
*/
|
||||||
|
const disabled = !rest.isPending && !options.length && !searchValue
|
||||||
|
|
||||||
|
// // make sure that the default value is included in the option, if its not in options already
|
||||||
|
if (defaultValue && !options.find((o) => o.value === defaultValue)) {
|
||||||
|
options.unshift(defaultOptions[0])
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
options,
|
||||||
query,
|
searchValue,
|
||||||
setQuery,
|
onSearchValueChange,
|
||||||
|
disabled,
|
||||||
|
...rest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import debounce from "lodash/debounce"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for debouncing search input
|
||||||
|
* @returns searchValue, onSearchValueChange, query
|
||||||
|
*/
|
||||||
|
export const useDebouncedSearch = () => {
|
||||||
|
const [searchValue, onSearchValueChange] = useState("")
|
||||||
|
const [debouncedQuery, setDebouncedQuery] = useState("")
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const debouncedUpdate = useCallback(
|
||||||
|
debounce((query: string) => setDebouncedQuery(query), 300),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedUpdate(searchValue)
|
||||||
|
|
||||||
|
return () => debouncedUpdate.cancel()
|
||||||
|
}, [searchValue, debouncedUpdate])
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchValue,
|
||||||
|
onSearchValueChange,
|
||||||
|
query: debouncedQuery || undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ async function makeRequest<
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
|
|
||||||
// Temp: Add a better error type
|
// Temp: Add a better error type
|
||||||
throw new Error(`API error ${response.status}: ${errorData.message}`)
|
throw new Error(`API error ${response.status}: ${errorData.message}`)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -15,9 +15,9 @@ import { useEffect, useMemo } from "react"
|
|||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { useWatch } from "react-hook-form"
|
import { useWatch } from "react-hook-form"
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
import { getCurrencySymbol } from "../../../../../lib/currencies"
|
||||||
import { CreateDiscountFormReturn } from "./create-discount-form"
|
import { CreateDiscountFormReturn } from "./create-discount-form"
|
||||||
import { DiscountRuleType } from "./types"
|
import { DiscountRuleType } from "./types"
|
||||||
|
|||||||
+2
-2
@@ -13,9 +13,9 @@ import { useForm } from "react-hook-form"
|
|||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
|
|||||||
+1
-1
@@ -7,8 +7,8 @@ import { useCallback, useEffect, useState } from "react"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { json } from "react-router-dom"
|
import { json } from "react-router-dom"
|
||||||
|
|
||||||
import { Combobox } from "../../../../../../components/common/combobox"
|
|
||||||
import { Form } from "../../../../../../components/common/form"
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../../components/inputs/combobox"
|
||||||
import { useCreateDraftOrder } from "../hooks"
|
import { useCreateDraftOrder } from "../hooks"
|
||||||
|
|
||||||
export const CreateDraftOrderCustomerDetails = () => {
|
export const CreateDraftOrderCustomerDetails = () => {
|
||||||
|
|||||||
+1
-1
@@ -7,9 +7,9 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { ShippingOption } from "@medusajs/medusa"
|
import { ShippingOption } from "@medusajs/medusa"
|
||||||
import { CurrencyInput, Heading, Input } from "@medusajs/ui"
|
import { CurrencyInput, Heading, Input } from "@medusajs/ui"
|
||||||
import { json } from "react-router-dom"
|
import { json } from "react-router-dom"
|
||||||
import { Combobox } from "../../../../../../components/common/combobox"
|
|
||||||
import { ConditionalTooltip } from "../../../../../../components/common/conditional-tooltip"
|
import { ConditionalTooltip } from "../../../../../../components/common/conditional-tooltip"
|
||||||
import { Form } from "../../../../../../components/common/form"
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../../components/inputs/combobox"
|
||||||
import { getLocaleAmount } from "../../../../../../lib/money-amount-helpers"
|
import { getLocaleAmount } from "../../../../../../lib/money-amount-helpers"
|
||||||
import { useCreateDraftOrder } from "../hooks"
|
import { useCreateDraftOrder } from "../hooks"
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -1,18 +1,18 @@
|
|||||||
import { PricedVariant } from "@medusajs/client-types"
|
import { PricedVariant } from "@medusajs/client-types"
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { useAdminVariants } from "medusa-react"
|
|
||||||
import { Button } from "@medusajs/ui"
|
|
||||||
import { Order } from "@medusajs/medusa"
|
import { Order } from "@medusajs/medusa"
|
||||||
|
import { Button } from "@medusajs/ui"
|
||||||
|
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||||
|
import { useAdminVariants } from "medusa-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table.tsx"
|
import { useDataTable } from "../../../../../hooks/use-data-table.tsx"
|
||||||
import { SplitView } from "../../../../../components/layout/split-view"
|
|
||||||
|
|
||||||
import { useVariantTableQuery } from "./use-variant-table-query"
|
|
||||||
import { useVariantTableColumns } from "./use-variant-table-columns"
|
import { useVariantTableColumns } from "./use-variant-table-columns"
|
||||||
import { useVariantTableFilters } from "./use-variant-table-filters"
|
import { useVariantTableFilters } from "./use-variant-table-filters"
|
||||||
|
import { useVariantTableQuery } from "./use-variant-table-query"
|
||||||
|
|
||||||
const PAGE_SIZE = 50
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
|||||||
+9
-88
@@ -1,25 +1,17 @@
|
|||||||
import { PencilSquare, TriangleRightMini } from "@medusajs/icons"
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
import { AdminProductCategoryResponse } from "@medusajs/types"
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
import { Container, Heading, IconButton, Text, clx } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
|
||||||
import {
|
|
||||||
TextCell,
|
|
||||||
TextHeader,
|
|
||||||
} from "../../../../../components/table/table-cells/common/text-cell"
|
|
||||||
import { useCategories } from "../../../../../hooks/api/categories"
|
import { useCategories } from "../../../../../hooks/api/categories"
|
||||||
|
import { useCategoryTableColumns } from "../../../../../hooks/table/columns/use-category-table-columns"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useCategoryTableQuery } from "../../../common/hooks/use-category-table-query"
|
import { useCategoryTableQuery } from "../../../common/hooks/use-category-table-query"
|
||||||
import {
|
|
||||||
getCategoryPath,
|
|
||||||
getIsActiveProps,
|
|
||||||
getIsInternalProps,
|
|
||||||
} from "../../../common/utils"
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -51,7 +43,7 @@ export const CategoryListTable = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const columns = useCategoryTableColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
data: product_categories || [],
|
data: product_categories || [],
|
||||||
@@ -114,83 +106,12 @@ const CategoryRowActions = ({
|
|||||||
const columnHelper =
|
const columnHelper =
|
||||||
createColumnHelper<AdminProductCategoryResponse["product_category"]>()
|
createColumnHelper<AdminProductCategoryResponse["product_category"]>()
|
||||||
|
|
||||||
const useCategoryTableColumns = () => {
|
const useColumns = () => {
|
||||||
const { t } = useTranslation()
|
const base = useCategoryTableColumns()
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.accessor("name", {
|
...base,
|
||||||
header: () => <TextHeader text={t("fields.name")} />,
|
|
||||||
cell: ({ getValue, row }) => {
|
|
||||||
const expandHandler = row.getToggleExpandedHandler()
|
|
||||||
|
|
||||||
console.log(row.original)
|
|
||||||
|
|
||||||
if (row.original.parent_category !== undefined) {
|
|
||||||
const path = getCategoryPath(row.original)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex size-full items-center">
|
|
||||||
{path.map((chip) => (
|
|
||||||
<div key={chip.id}>
|
|
||||||
<Text>{chip.name}</Text>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex size-full items-center gap-x-3 overflow-hidden">
|
|
||||||
<div className="flex size-7 items-center justify-center">
|
|
||||||
{row.getCanExpand() ? (
|
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
expandHandler()
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
variant="transparent"
|
|
||||||
>
|
|
||||||
<TriangleRightMini
|
|
||||||
className={clx({
|
|
||||||
"rotate-90 transition-transform will-change-transform":
|
|
||||||
row.getIsExpanded(),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<span className="truncate">{getValue()}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("handle", {
|
|
||||||
header: () => <TextHeader text={t("fields.handle")} />,
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
return <TextCell text={`/${getValue()}`} />
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("is_active", {
|
|
||||||
header: () => <TextHeader text={t("fields.status")} />,
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
const { color, label } = getIsActiveProps(getValue(), t)
|
|
||||||
|
|
||||||
return <StatusCell color={color}>{label}</StatusCell>
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("is_internal", {
|
|
||||||
header: () => <TextHeader text={t("categories.fields.visibility")} />,
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
const { color, label } = getIsInternalProps(getValue(), t)
|
|
||||||
|
|
||||||
return <StatusCell color={color}>{label}</StatusCell>
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -198,6 +119,6 @@ const useCategoryTableColumns = () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[t]
|
[base]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"
|
|||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { HandleInput } from "../../../../../components/common/handle-input"
|
import { HandleInput } from "../../../../../components/inputs/handle-input"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
|
|||||||
+4
-4
@@ -6,14 +6,14 @@ import {
|
|||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../../components/route-modal"
|
} from "../../../../../../components/route-modal"
|
||||||
|
|
||||||
import { CountrySelect } from "../../../../../../components/common/country-select"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Form } from "../../../../../../components/common/form"
|
|
||||||
import { InventoryNext } from "@medusajs/types"
|
import { InventoryNext } from "@medusajs/types"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useUpdateInventoryItem } from "../../../../../../hooks/api/inventory"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { CountrySelect } from "../../../../../../components/inputs/country-select"
|
||||||
|
import { useUpdateInventoryItem } from "../../../../../../hooks/api/inventory"
|
||||||
|
|
||||||
type EditInventoryItemAttributeFormProps = {
|
type EditInventoryItemAttributeFormProps = {
|
||||||
item: InventoryNext.InventoryItemDTO
|
item: InventoryNext.InventoryItemDTO
|
||||||
|
|||||||
+380
@@ -0,0 +1,380 @@
|
|||||||
|
import {
|
||||||
|
ArrowUturnLeft,
|
||||||
|
CheckMini,
|
||||||
|
TriangleRightMini,
|
||||||
|
TrianglesMini,
|
||||||
|
XMarkMini,
|
||||||
|
} from "@medusajs/icons"
|
||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { Text, clx } from "@medusajs/ui"
|
||||||
|
import * as Popover from "@radix-ui/react-popover"
|
||||||
|
import {
|
||||||
|
ComponentPropsWithoutRef,
|
||||||
|
Fragment,
|
||||||
|
MouseEvent,
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
|
import { TextSkeleton } from "../../../../../components/common/skeleton"
|
||||||
|
import { useCategories } from "../../../../../hooks/api/categories"
|
||||||
|
import { useDebouncedSearch } from "../../../../../hooks/use-debounced-search"
|
||||||
|
|
||||||
|
interface CategoryComboboxProps
|
||||||
|
extends Omit<
|
||||||
|
ComponentPropsWithoutRef<"input">,
|
||||||
|
"value" | "defaultValue" | "onChange"
|
||||||
|
> {
|
||||||
|
value: string[]
|
||||||
|
onChange: (value: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type Level = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategoryCombobox = forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
CategoryComboboxProps
|
||||||
|
>(({ value, onChange, className, ...props }, ref) => {
|
||||||
|
const innerRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
|
||||||
|
ref,
|
||||||
|
() => innerRef.current,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const { i18n, t } = useTranslation()
|
||||||
|
|
||||||
|
const [level, setLevel] = useState<Level[]>([])
|
||||||
|
const { searchValue, onSearchValueChange, query } = useDebouncedSearch()
|
||||||
|
|
||||||
|
const { product_categories, isPending, isError, error } = useCategories(
|
||||||
|
{
|
||||||
|
q: query,
|
||||||
|
parent_category_id: !searchValue ? getParentId(level) : undefined,
|
||||||
|
include_descendants_tree: !searchValue ? true : false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: open,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const [showLoading, setShowLoading] = useState(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We add a small artificial delay to the end of the loading state,
|
||||||
|
* this is done to prevent the popover from flickering too much when
|
||||||
|
* navigating between levels or searching.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
setShowLoading(true)
|
||||||
|
} else {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
setShowLoading(false)
|
||||||
|
}, 150)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
}, [isPending])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchValue) {
|
||||||
|
setLevel([])
|
||||||
|
}
|
||||||
|
}, [searchValue])
|
||||||
|
|
||||||
|
function handleLevelUp(e: MouseEvent<HTMLButtonElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
setLevel(level.slice(0, level.length - 1))
|
||||||
|
|
||||||
|
innerRef.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLevelDown(option: ProductCategoryOption) {
|
||||||
|
return (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
setLevel([...level, { id: option.value, label: option.label }])
|
||||||
|
|
||||||
|
innerRef.current?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(option: ProductCategoryOption) {
|
||||||
|
return (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if (isSelected(value, option.value)) {
|
||||||
|
onChange(value.filter((v) => v !== option.value))
|
||||||
|
} else {
|
||||||
|
onChange([...value, option.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
innerRef.current?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenChange(open: boolean) {
|
||||||
|
if (!open) {
|
||||||
|
onSearchValueChange("")
|
||||||
|
setLevel([])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
innerRef.current?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = getOptions(product_categories || [])
|
||||||
|
|
||||||
|
const showTag = value.length > 0 && !open
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"relative flex cursor-pointer items-center gap-x-2 overflow-hidden",
|
||||||
|
"h-8 w-full rounded-md px-2 py-0.5",
|
||||||
|
"bg-ui-bg-field transition-fg shadow-borders-base",
|
||||||
|
"hover:bg-ui-bg-field-hover",
|
||||||
|
"has-[input:focus]:shadow-borders-interactive-with-active",
|
||||||
|
"has-[:invalid]:shadow-borders-error has-[[aria-invalid=true]]:shadow-borders-error",
|
||||||
|
"has-[:disabled]:bg-ui-bg-disabled has-[:disabled]:text-ui-fg-disabled has-[:disabled]:cursor-not-allowed",
|
||||||
|
{
|
||||||
|
// Fake the focus state as long as the popover is open,
|
||||||
|
// this prevents the styling from flickering when navigating
|
||||||
|
// between levels.
|
||||||
|
"shadow-borders-interactive-with-active": open,
|
||||||
|
"pl-0.5": showTag,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<input
|
||||||
|
ref={innerRef}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => onSearchValueChange(e.target.value)}
|
||||||
|
className={clx(
|
||||||
|
"txt-compact-small w-full appearance-none bg-transparent outline-none",
|
||||||
|
"placeholder:text-ui-fg-muted"
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
) : showTag ? (
|
||||||
|
<div className="flex w-full items-center gap-x-2">
|
||||||
|
<div className="flex w-fit items-center gap-x-1">
|
||||||
|
<div className="bg-ui-bg-base txt-compact-small-plus text-ui-fg-subtle focus-within:border-ui-fg-interactive relative flex h-[28px] items-center rounded-[4px] border py-[3px] pl-1.5 pr-1">
|
||||||
|
<span>{value.length}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="size-fit outline-none"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onChange([])
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMarkMini className="text-ui-fg-muted" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{t("general.selected")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full"></div>
|
||||||
|
)}
|
||||||
|
<div className="flex size-5 items-center justify-center">
|
||||||
|
<TrianglesMini className="text-ui-fg-muted" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Content
|
||||||
|
sideOffset={8}
|
||||||
|
role="listbox"
|
||||||
|
className={clx(
|
||||||
|
"shadow-elevation-flyout bg-ui-bg-base -left-2 z-50 w-[var(--radix-popper-anchor-width)] rounded-[8px]",
|
||||||
|
"max-h-[200px] overflow-y-auto",
|
||||||
|
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||||
|
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
||||||
|
)}
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!searchValue && level.length > 0 && (
|
||||||
|
<Fragment>
|
||||||
|
<div className="p-1">
|
||||||
|
<button
|
||||||
|
className={clx(
|
||||||
|
"transition-fg grid w-full appearance-none grid-cols-[20px_1fr] items-center justify-center gap-2 rounded-md px-2 py-1.5 text-left outline-none",
|
||||||
|
"hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed"
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
onClick={handleLevelUp}
|
||||||
|
>
|
||||||
|
<ArrowUturnLeft className="text-ui-fg-muted" />
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{getParentLabel(level)}
|
||||||
|
</Text>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<div className="p-1">
|
||||||
|
{options.length > 0 &&
|
||||||
|
!showLoading &&
|
||||||
|
options.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className={clx(
|
||||||
|
"transition-fg bg-ui-bg-base grid cursor-pointer grid-cols-1 items-center gap-2 overflow-hidden",
|
||||||
|
{
|
||||||
|
"grid-cols-[1fr_32px]":
|
||||||
|
option.has_children && !searchValue,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
className={clx(
|
||||||
|
"grid h-full w-full appearance-none grid-cols-[20px_1fr] items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-left",
|
||||||
|
"hover:bg-ui-bg-base-hover"
|
||||||
|
)}
|
||||||
|
onClick={handleSelect(option)}
|
||||||
|
>
|
||||||
|
<div className="flex size-5 items-center justify-center">
|
||||||
|
{isSelected(value, option.value) && <CheckMini />}
|
||||||
|
</div>
|
||||||
|
<Text
|
||||||
|
as="span"
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className="w-full truncate"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
</button>
|
||||||
|
{option.has_children && !searchValue && (
|
||||||
|
<button
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-muted flex size-8 appearance-none items-center justify-center rounded-md outline-none",
|
||||||
|
"hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed"
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
onClick={handleLevelDown(option)}
|
||||||
|
>
|
||||||
|
<TriangleRightMini />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{showLoading &&
|
||||||
|
Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="grid grid-cols-[20px_1fr_20px] gap-2 px-2 py-1.5"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
<TextSkeleton size="small" leading="compact" />
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{options.length === 0 && !showLoading && (
|
||||||
|
<div className="px-2 py-1.5">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{query ? (
|
||||||
|
<Trans
|
||||||
|
i18n={i18n}
|
||||||
|
i18nKey={"general.noSearchResultsFor"}
|
||||||
|
tOptions={{
|
||||||
|
query: query,
|
||||||
|
}}
|
||||||
|
components={[
|
||||||
|
<span className="font-medium" key="query" />,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
t("general.noRecordsFound")
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
CategoryCombobox.displayName = "CategoryCombobox"
|
||||||
|
|
||||||
|
type ProductCategoryOption = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
has_children: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentId(level: Level[]): string {
|
||||||
|
if (!level.length) {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
return level[level.length - 1].id
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentLabel(level: Level[]): string | null {
|
||||||
|
if (!level.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return level[level.length - 1].label
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions(
|
||||||
|
categories: AdminProductCategoryResponse["product_category"][]
|
||||||
|
): ProductCategoryOption[] {
|
||||||
|
return categories.map((cat) => {
|
||||||
|
return {
|
||||||
|
value: cat.id,
|
||||||
|
label: cat.name,
|
||||||
|
has_children: cat.category_children?.length > 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(values: string[], value: string): boolean {
|
||||||
|
return values.includes(value)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./category-combobox"
|
||||||
+9
-9
@@ -1,18 +1,18 @@
|
|||||||
|
import { CurrencyDTO, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
import { CreateProductSchemaType } from "../product-create/schema"
|
import { useTranslation } from "react-i18next"
|
||||||
import { DataGrid } from "../../../components/grid/data-grid"
|
import { DataGrid } from "../../../components/grid/data-grid"
|
||||||
|
import { CurrencyCell } from "../../../components/grid/grid-cells/common/currency-cell"
|
||||||
|
import { ReadonlyCell } from "../../../components/grid/grid-cells/common/readonly-cell"
|
||||||
|
import { DataGridMeta } from "../../../components/grid/types"
|
||||||
import { useCurrencies } from "../../../hooks/api/currencies"
|
import { useCurrencies } from "../../../hooks/api/currencies"
|
||||||
import { useStore } from "../../../hooks/api/store"
|
import { useStore } from "../../../hooks/api/store"
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
import { ProductCreateSchemaType } from "../product-create/schema"
|
||||||
import { CurrencyDTO, ProductVariantDTO } from "@medusajs/types"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { ReadonlyCell } from "../../../components/grid/grid-cells/common/readonly-cell"
|
|
||||||
import { CurrencyCell } from "../../../components/grid/grid-cells/common/currency-cell"
|
|
||||||
import { DataGridMeta } from "../../../components/grid/types"
|
|
||||||
|
|
||||||
type VariantPricingFormProps = {
|
type VariantPricingFormProps = {
|
||||||
form: UseFormReturn<CreateProductSchemaType>
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
||||||
|
|||||||
+1
-1
@@ -4,8 +4,8 @@ import { Button, Input } from "@medusajs/ui"
|
|||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
|
|||||||
+20
-13
@@ -1,10 +1,12 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Button, Input } from "@medusajs/ui"
|
import { Button, Input, toast } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { ChipInput } from "../../../../../components/inputs/chip-input"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
@@ -34,13 +36,25 @@ export const CreateProductOptionForm = ({
|
|||||||
resolver: zodResolver(CreateProductOptionSchema),
|
resolver: zodResolver(CreateProductOptionSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useCreateProductOption(product.id)
|
const { mutateAsync, isPending } = useCreateProductOption(product.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
mutateAsync(values, {
|
mutateAsync(values, {
|
||||||
onSuccess: () => {
|
onSuccess: ({ option }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("products.options.create.successToast", {
|
||||||
|
title: option.title,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
},
|
},
|
||||||
|
onError: async (err) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: err.message,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -71,21 +85,14 @@ export const CreateProductOptionForm = ({
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="values"
|
name="values"
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>
|
<Form.Label>
|
||||||
{t("products.fields.options.variations")}
|
{t("products.fields.options.variations")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input
|
<ChipInput {...field} />
|
||||||
{...field}
|
|
||||||
value={(value ?? []).join(",")}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value
|
|
||||||
onChange(val.split(",").map((v) => v.trim()))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -100,7 +107,7 @@ export const CreateProductOptionForm = ({
|
|||||||
{t("actions.cancel")}
|
{t("actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</RouteDrawer.Close>
|
</RouteDrawer.Close>
|
||||||
<Button type="submit" size="small" isLoading={isLoading}>
|
<Button type="submit" size="small" isLoading={isPending}>
|
||||||
{t("actions.save")}
|
{t("actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+4
-4
@@ -1,22 +1,22 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Button, Heading, Input, Switch } from "@medusajs/ui"
|
import { Button, Heading, Input, Switch } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { Fragment } from "react"
|
import { Fragment } from "react"
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
|
||||||
import { Divider } from "../../../../../components/common/divider"
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
|
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useCreateProductVariant } from "../../../../../hooks/api/products"
|
||||||
import { castNumber } from "../../../../../lib/cast-number"
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
import { optionalInt } from "../../../../../lib/validation"
|
import { optionalInt } from "../../../../../lib/validation"
|
||||||
import { useCreateProductVariant } from "../../../../../hooks/api/products"
|
|
||||||
|
|
||||||
type CreateProductVariantFormProps = {
|
type CreateProductVariantFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
|
|||||||
-801
@@ -1,801 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Heading,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
Text,
|
|
||||||
Textarea,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
|
||||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
|
|
||||||
|
|
||||||
import { SalesChannel } from "@medusajs/medusa"
|
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { Fragment, useMemo, useState } from "react"
|
|
||||||
import { CountrySelect } from "../../../../components/common/country-select"
|
|
||||||
import { Form } from "../../../../components/common/form"
|
|
||||||
import { HandleInput } from "../../../../components/common/handle-input"
|
|
||||||
import { DataTable } from "../../../../components/table/data-table"
|
|
||||||
import { useSalesChannelTableColumns } from "../../../../hooks/table/columns/use-sales-channel-table-columns"
|
|
||||||
import { useSalesChannelTableFilters } from "../../../../hooks/table/filters/use-sales-channel-table-filters"
|
|
||||||
import { useSalesChannelTableQuery } from "../../../../hooks/table/query/use-sales-channel-table-query"
|
|
||||||
import { useDataTable } from "../../../../hooks/use-data-table"
|
|
||||||
import { Combobox } from "../../../../components/common/combobox"
|
|
||||||
import { FileUpload } from "../../../../components/common/file-upload"
|
|
||||||
import { List } from "../../../../components/common/list"
|
|
||||||
import { useProductTypes } from "../../../../hooks/api/product-types"
|
|
||||||
import { useCollections } from "../../../../hooks/api/collections"
|
|
||||||
import { useSalesChannels } from "../../../../hooks/api/sales-channels"
|
|
||||||
import { useCategories } from "../../../../hooks/api/categories"
|
|
||||||
import { useTags } from "../../../../hooks/api/tags"
|
|
||||||
import { Keypair } from "../../../../components/common/keypair"
|
|
||||||
import { UseFormReturn } from "react-hook-form"
|
|
||||||
import { CreateProductSchemaType } from "../schema"
|
|
||||||
|
|
||||||
type ProductAttributesProps = {
|
|
||||||
form: UseFormReturn<CreateProductSchemaType>
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUPPORTED_FORMATS = [
|
|
||||||
"image/jpeg",
|
|
||||||
"image/png",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp",
|
|
||||||
"image/heic",
|
|
||||||
"image/svg+xml",
|
|
||||||
]
|
|
||||||
|
|
||||||
const permutations = (
|
|
||||||
data: { title: string; values: string[] }[]
|
|
||||||
): { [key: string]: string }[] => {
|
|
||||||
if (data.length === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length === 1) {
|
|
||||||
return data[0].values.map((value) => ({ [data[0].title]: value }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const toProcess = data[0]
|
|
||||||
const rest = data.slice(1)
|
|
||||||
|
|
||||||
return toProcess.values.flatMap((value) => {
|
|
||||||
return permutations(rest).map((permutation) => {
|
|
||||||
return {
|
|
||||||
[toProcess.title]: value,
|
|
||||||
...permutation,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateNameFromPermutation = (permutation: {
|
|
||||||
[key: string]: string
|
|
||||||
}) => {
|
|
||||||
return Object.values(permutation).join(" / ")
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProductAttributesForm = ({ form }: ProductAttributesProps) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [open, onOpenChange] = useState(false)
|
|
||||||
const { product_types, isLoading: isLoadingTypes } = useProductTypes()
|
|
||||||
const { product_tags, isLoading: isLoadingTags } = useTags()
|
|
||||||
const { collections, isLoading: isLoadingCollections } = useCollections()
|
|
||||||
const { sales_channels, isLoading: isLoadingSalesChannels } =
|
|
||||||
useSalesChannels()
|
|
||||||
const { product_categories, isLoading: isLoadingCategories } = useCategories()
|
|
||||||
|
|
||||||
const options = form.watch("options")
|
|
||||||
const optionPermutations = permutations(options ?? [])
|
|
||||||
|
|
||||||
// const { append } = useFieldArray({
|
|
||||||
// name: "images",
|
|
||||||
// control: form.control,
|
|
||||||
// // keyName: "field_id",
|
|
||||||
// })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PanelGroup
|
|
||||||
direction="horizontal"
|
|
||||||
className="flex h-full justify-center overflow-hidden"
|
|
||||||
>
|
|
||||||
<Panel
|
|
||||||
className="flex h-full w-full flex-col items-center"
|
|
||||||
minSize={open ? 50 : 100}
|
|
||||||
>
|
|
||||||
<div className="flex size-full flex-col items-center overflow-auto p-16">
|
|
||||||
<div className="flex w-full max-w-[736px] flex-col justify-center px-2 pb-2">
|
|
||||||
<div className="flex flex-col gap-y-1">
|
|
||||||
<Heading>{t("products.createProductTitle")}</Heading>
|
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
|
||||||
{t("products.createProductHint")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-y-8 divide-y [&>div]:pt-8">
|
|
||||||
<div id="general" className="flex flex-col gap-y-8">
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div className="grid grid-cols-2 gap-x-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="title"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>
|
|
||||||
{t("products.fields.title.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="subtitle"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.subtitle.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Form.Hint>
|
|
||||||
<Trans
|
|
||||||
i18nKey="products.fields.title.hint"
|
|
||||||
t={t}
|
|
||||||
components={[<br key="break" />]}
|
|
||||||
/>
|
|
||||||
</Form.Hint>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-x-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="handle"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label
|
|
||||||
tooltip={t("products.fields.handle.tooltip")}
|
|
||||||
optional
|
|
||||||
>
|
|
||||||
{t("fields.handle")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<HandleInput {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="description"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.description.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Textarea {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
<Form.Hint>
|
|
||||||
<Trans
|
|
||||||
i18nKey={"products.fields.description.hint"}
|
|
||||||
components={[<br key="break" />]}
|
|
||||||
/>
|
|
||||||
</Form.Hint>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="organize" className="flex flex-col gap-y-8">
|
|
||||||
<Heading level="h2">{t("products.organization")}</Heading>
|
|
||||||
<div className="grid grid-cols-1 gap-x-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="discountable"
|
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.discountable.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
checked={!!value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Hint>
|
|
||||||
<Trans i18nKey={"products.fields.discountable.hint"} />
|
|
||||||
</Form.Hint>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-x-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="type_id"
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.type.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select
|
|
||||||
disabled={isLoadingTypes}
|
|
||||||
{...field}
|
|
||||||
onValueChange={onChange}
|
|
||||||
>
|
|
||||||
<Select.Trigger ref={field.ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{(product_types ?? []).map((type) => (
|
|
||||||
<Select.Item key={type.id} value={type.id}>
|
|
||||||
{type.value}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="collection_id"
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.collection.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Select
|
|
||||||
disabled={isLoadingCollections}
|
|
||||||
{...field}
|
|
||||||
onValueChange={onChange}
|
|
||||||
>
|
|
||||||
<Select.Trigger ref={field.ref}>
|
|
||||||
<Select.Value />
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{(collections ?? []).map((collection) => (
|
|
||||||
<Select.Item
|
|
||||||
key={collection.id}
|
|
||||||
value={collection.id}
|
|
||||||
>
|
|
||||||
{collection.title}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-x-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="category_ids"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.categories.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Combobox
|
|
||||||
disabled={isLoadingCategories}
|
|
||||||
options={(product_categories ?? []).map(
|
|
||||||
(category) => ({
|
|
||||||
label: category.name,
|
|
||||||
value: category.id,
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="tags"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.tags.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Combobox
|
|
||||||
disabled={isLoadingTags}
|
|
||||||
options={(product_tags ?? []).map((tag) => ({
|
|
||||||
label: tag.value,
|
|
||||||
value: tag.id,
|
|
||||||
}))}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* TODO: Align to match designs */}
|
|
||||||
<div className="grid grid-cols-1 gap-x-4">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="sales_channels"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.sales_channels.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Hint>
|
|
||||||
<Trans
|
|
||||||
i18nKey={"products.fields.sales_channels.hint"}
|
|
||||||
/>
|
|
||||||
</Form.Hint>
|
|
||||||
<Form.Control>
|
|
||||||
<Combobox
|
|
||||||
disabled={isLoadingSalesChannels}
|
|
||||||
options={(sales_channels ?? []).map(
|
|
||||||
(salesChannel) => ({
|
|
||||||
label: salesChannel.name,
|
|
||||||
value: salesChannel.id,
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* <Button
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => onOpenChange(!open)}
|
|
||||||
>
|
|
||||||
{t("actions.edit")}
|
|
||||||
</Button> */}
|
|
||||||
</div>
|
|
||||||
<div id="variants" className="flex flex-col gap-y-8">
|
|
||||||
<Heading level="h2">{t("products.variants")}</Heading>
|
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="options"
|
|
||||||
render={({ field: { onChange, value } }) => {
|
|
||||||
const normalizedValue = (value ?? []).map((v) => {
|
|
||||||
return {
|
|
||||||
key: v.title,
|
|
||||||
value: v.values.join(","),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.options.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Hint>
|
|
||||||
{t("products.fields.options.hint")}
|
|
||||||
</Form.Hint>
|
|
||||||
<Form.Control>
|
|
||||||
<Keypair
|
|
||||||
labels={{
|
|
||||||
add: t("products.fields.options.add"),
|
|
||||||
key: t("products.fields.options.optionTitle"),
|
|
||||||
value: t("products.fields.options.variations"),
|
|
||||||
}}
|
|
||||||
value={normalizedValue}
|
|
||||||
onChange={(newVal) =>
|
|
||||||
onChange(
|
|
||||||
newVal.map((v) => {
|
|
||||||
return {
|
|
||||||
title: v.key,
|
|
||||||
values: v.value
|
|
||||||
.split(",")
|
|
||||||
.map((v) => v.trim()),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="variants"
|
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
|
||||||
const selectedOptions = (value ?? []).map(
|
|
||||||
(v) => v.options
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.variants.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Hint>
|
|
||||||
{t("products.fields.variants.hint")}
|
|
||||||
</Form.Hint>
|
|
||||||
<Form.Control>
|
|
||||||
<List
|
|
||||||
{...field}
|
|
||||||
value={selectedOptions}
|
|
||||||
onChange={(v) => {
|
|
||||||
onChange(
|
|
||||||
v.map((options, i) => {
|
|
||||||
return {
|
|
||||||
title:
|
|
||||||
generateNameFromPermutation(options),
|
|
||||||
variant_rank: i,
|
|
||||||
options,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
compare={(a, b) => {
|
|
||||||
return (
|
|
||||||
generateNameFromPermutation(a) ===
|
|
||||||
generateNameFromPermutation(b)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
options={optionPermutations.map((opt) => {
|
|
||||||
return {
|
|
||||||
title: generateNameFromPermutation(opt),
|
|
||||||
value: opt,
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="attributes" className="flex flex-col gap-y-8">
|
|
||||||
<Heading level="h2">{t("products.attributes")}</Heading>
|
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="origin_country"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.countryOrigin.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<CountrySelect {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="material"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.material.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="width"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.width.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} type="number" min={0} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="length"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.length.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} type="number" min={0} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="height"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.height.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} type="number" min={0} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="weight"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.weight.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} type="number" min={0} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="mid_code"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.mid_code.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="hs_code"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.fields.hs_code.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="media" className="flex flex-col gap-y-8">
|
|
||||||
<Heading level="h2">{t("products.media.label")}</Heading>
|
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="images"
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<div className="flex flex-col gap-y-4">
|
|
||||||
<div className="flex flex-col gap-y-1">
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("products.media.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Hint>
|
|
||||||
{t("products.media.editHint")}
|
|
||||||
</Form.Hint>
|
|
||||||
</div>
|
|
||||||
<Form.Control>
|
|
||||||
<FileUpload
|
|
||||||
label={t("products.media.uploadImagesLabel")}
|
|
||||||
hint={t("products.media.uploadImagesHint")}
|
|
||||||
hasError={!!form.formState.errors.images}
|
|
||||||
formats={SUPPORTED_FORMATS}
|
|
||||||
onUploaded={() => {
|
|
||||||
form.clearErrors("images")
|
|
||||||
// if (hasInvalidFiles(files)) {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// files.forEach((f) => append(f))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<Fragment>
|
|
||||||
<PanelResizeHandle className="bg-ui-bg-subtle group flex items-center justify-center border-x px-[4.5px] outline-none">
|
|
||||||
{/* Is this meant to be resizable? And if so we need some kind of focus state for the handle cc: @ludvig18 */}
|
|
||||||
<div className="bg-ui-fg-disabled group-focus-visible:bg-ui-fg-muted transition-fg h-6 w-[3px] rounded-full" />
|
|
||||||
</PanelResizeHandle>
|
|
||||||
<Panel defaultSize={50} maxSize={50} minSize={40}>
|
|
||||||
<AddSalesChannelsDrawer onCancel={() => onOpenChange(false)} />
|
|
||||||
</Panel>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</PanelGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
|
||||||
|
|
||||||
const AddSalesChannelsDrawer = ({ onCancel }: { onCancel: () => void }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [selection, setSelection] = useState<RowSelectionState>({})
|
|
||||||
|
|
||||||
const { searchParams, raw } = useSalesChannelTableQuery({
|
|
||||||
pageSize: PAGE_SIZE,
|
|
||||||
})
|
|
||||||
const { sales_channels, count, isLoading, isError, error } =
|
|
||||||
useAdminSalesChannels({
|
|
||||||
...searchParams,
|
|
||||||
})
|
|
||||||
|
|
||||||
const filters = useSalesChannelTableFilters()
|
|
||||||
const columns = useColumns()
|
|
||||||
|
|
||||||
const { table } = useDataTable({
|
|
||||||
data: sales_channels ?? [],
|
|
||||||
columns,
|
|
||||||
count: sales_channels?.length ?? 0,
|
|
||||||
enablePagination: true,
|
|
||||||
enableRowSelection: true,
|
|
||||||
getRowId: (row) => row.id,
|
|
||||||
pageSize: PAGE_SIZE,
|
|
||||||
rowSelection: {
|
|
||||||
state: selection,
|
|
||||||
updater: setSelection,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
|
||||||
<div className="flex-1">
|
|
||||||
<DataTable
|
|
||||||
table={table}
|
|
||||||
columns={columns}
|
|
||||||
pageSize={PAGE_SIZE}
|
|
||||||
filters={filters}
|
|
||||||
isLoading={isLoading}
|
|
||||||
layout="fill"
|
|
||||||
orderBy={["name", "created_at", "updated_at"]}
|
|
||||||
queryObject={raw}
|
|
||||||
search
|
|
||||||
pagination
|
|
||||||
count={count}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end gap-x-2 border-t px-6 py-4">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={onCancel}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{t("actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button size="small" onClick={() => {}} type="button">
|
|
||||||
{t("actions.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<SalesChannel>()
|
|
||||||
|
|
||||||
const useColumns = () => {
|
|
||||||
const base = useSalesChannelTableColumns()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.display({
|
|
||||||
id: "select",
|
|
||||||
header: ({ table }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
table.getIsSomePageRowsSelected()
|
|
||||||
? "indeterminate"
|
|
||||||
: table.getIsAllPageRowsSelected()
|
|
||||||
}
|
|
||||||
onCheckedChange={(value) =>
|
|
||||||
table.toggleAllPageRowsSelected(!!value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...base,
|
|
||||||
],
|
|
||||||
[base]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-details-attribute-section"
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
import { Heading, Input } from "@medusajs/ui"
|
||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../../../components/common/form"
|
||||||
|
import { CountrySelect } from "../../../../../../../components/inputs/country-select"
|
||||||
|
import { ProductCreateSchemaType } from "../../../../types"
|
||||||
|
|
||||||
|
type ProductCreateAttributeSectionProps = {
|
||||||
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCreateAttributeSection = ({
|
||||||
|
form,
|
||||||
|
}: ProductCreateAttributeSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="attributes" className="flex flex-col gap-y-8">
|
||||||
|
<Heading level="h2">{t("products.attributes")}</Heading>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="origin_country"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.countryOrigin.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CountrySelect {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="material"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.material.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="width"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.width.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} type="number" min={0} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="length"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.length.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} type="number" min={0} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="height"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.height.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} type="number" min={0} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="weight"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.weight.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} type="number" min={0} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="mid_code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.mid_code.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="hs_code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.hs_code.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./product-create-details-context"
|
||||||
|
export * from "./use-product-create-details-context"
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createContext } from "react"
|
||||||
|
|
||||||
|
type ProductCreateDetailsContextValue = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCreateDetailsContext =
|
||||||
|
createContext<ProductCreateDetailsContextValue | null>(null)
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { useContext } from "react"
|
||||||
|
import { ProductCreateDetailsContext } from "./product-create-details-context"
|
||||||
|
|
||||||
|
export const useProductCreateDetailsContext = () => {
|
||||||
|
const context = useContext(ProductCreateDetailsContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useProductCreateDetailsContext must be used within a ProductCreateDetailsContextProvider"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-general-section"
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
import { Input, Textarea } from "@medusajs/ui"
|
||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../../../components/common/form"
|
||||||
|
import { HandleInput } from "../../../../../../../components/inputs/handle-input"
|
||||||
|
import { ProductCreateSchemaType } from "../../../../types"
|
||||||
|
|
||||||
|
type ProductCreateGeneralSectionProps = {
|
||||||
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCreateGeneralSection = ({
|
||||||
|
form,
|
||||||
|
}: ProductCreateGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="general" className="flex flex-col gap-y-8">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("products.fields.title.label")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="subtitle"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.subtitle.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Form.Hint>
|
||||||
|
<Trans
|
||||||
|
i18nKey="products.fields.title.hint"
|
||||||
|
t={t}
|
||||||
|
components={[<br key="break" />]}
|
||||||
|
/>
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="handle"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
tooltip={t("products.fields.handle.tooltip")}
|
||||||
|
optional
|
||||||
|
>
|
||||||
|
{t("fields.handle")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<HandleInput {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.description.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Textarea {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Hint>
|
||||||
|
<Trans
|
||||||
|
i18nKey={"products.fields.description.hint"}
|
||||||
|
components={[<br key="break" />]}
|
||||||
|
/>
|
||||||
|
</Form.Hint>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-details-media-section"
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { FileUpload } from "../../../../../../../components/common/file-upload"
|
||||||
|
import { Form } from "../../../../../../../components/common/form"
|
||||||
|
import { ProductCreateSchemaType } from "../../../../types"
|
||||||
|
|
||||||
|
type ProductCreateMediaSectionProps = {
|
||||||
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_FORMATS = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"image/heic",
|
||||||
|
"image/svg+xml",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ProductCreateMediaSection = ({
|
||||||
|
form,
|
||||||
|
}: ProductCreateMediaSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="media" className="flex flex-col gap-y-8">
|
||||||
|
<Heading level="h2">{t("products.media.label")}</Heading>
|
||||||
|
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="images"
|
||||||
|
render={() => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.media.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint>{t("products.media.editHint")}</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control>
|
||||||
|
<FileUpload
|
||||||
|
label={t("products.media.uploadImagesLabel")}
|
||||||
|
hint={t("products.media.uploadImagesHint")}
|
||||||
|
hasError={!!form.formState.errors.images}
|
||||||
|
formats={SUPPORTED_FORMATS}
|
||||||
|
onUploaded={(files) => {
|
||||||
|
form.clearErrors("images")
|
||||||
|
// if (hasInvalidFiles(files)) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// files.forEach((f) => append(f))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-details-organize-section"
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
import { Button, Heading, Switch } from "@medusajs/ui"
|
||||||
|
import { UseFormReturn, useFieldArray } from "react-hook-form"
|
||||||
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { ChipGroup } from "../../../../../../../components/common/chip-group"
|
||||||
|
import { Form } from "../../../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../../../components/inputs/combobox"
|
||||||
|
import { useComboboxData } from "../../../../../../../hooks/use-combobox-data"
|
||||||
|
import { client } from "../../../../../../../lib/client"
|
||||||
|
import { CategoryCombobox } from "../../../../../common/components/category-combobox"
|
||||||
|
import { ProductCreateSchemaType } from "../../../../types"
|
||||||
|
import { useProductCreateDetailsContext } from "../product-create-details-context"
|
||||||
|
|
||||||
|
type ProductCreateOrganizationSectionProps = {
|
||||||
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCreateOrganizationSection = ({
|
||||||
|
form,
|
||||||
|
}: ProductCreateOrganizationSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { onOpenChange } = useProductCreateDetailsContext()
|
||||||
|
|
||||||
|
const collections = useComboboxData({
|
||||||
|
queryKey: ["product_collections"],
|
||||||
|
queryFn: client.collections.list,
|
||||||
|
getOptions: (data) =>
|
||||||
|
data.collections.map((collection) => ({
|
||||||
|
label: collection.title,
|
||||||
|
value: collection.id,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const types = useComboboxData({
|
||||||
|
queryKey: ["product_types"],
|
||||||
|
queryFn: client.productTypes.list,
|
||||||
|
getOptions: (data) =>
|
||||||
|
data.product_types.map((type) => ({
|
||||||
|
label: type.value,
|
||||||
|
value: type.id,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fields, remove, replace } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "sales_channels",
|
||||||
|
keyName: "key",
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClearAllSalesChannels = () => {
|
||||||
|
replace([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="organize" className="flex flex-col gap-y-8">
|
||||||
|
<Heading level="h2">{t("products.organization")}</Heading>
|
||||||
|
<div className="grid grid-cols-1 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="discountable"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.discountable.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={!!value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Hint>
|
||||||
|
<Trans i18nKey={"products.fields.discountable.hint"} />
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="type_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.type.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
options={types.options}
|
||||||
|
searchValue={types.searchValue}
|
||||||
|
onSearchValueChange={types.onSearchValueChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="collection_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.collection.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
options={collections.options}
|
||||||
|
searchValue={collections.searchValue}
|
||||||
|
onSearchValueChange={collections.onSearchValueChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="categories"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.categories.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CategoryCombobox {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="sales_channels"
|
||||||
|
render={() => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex items-start justify-between gap-x-4">
|
||||||
|
<div>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.sales_channels.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint>
|
||||||
|
<Trans i18nKey={"products.fields.sales_channels.hint"} />
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenChange(true)}
|
||||||
|
>
|
||||||
|
{t("actions.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Form.Control className="mt-0">
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<ChipGroup
|
||||||
|
onClearAll={handleClearAllSalesChannels}
|
||||||
|
onRemove={remove}
|
||||||
|
className="py-4"
|
||||||
|
>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<ChipGroup.Chip key={field.key} index={index}>
|
||||||
|
{field.name}
|
||||||
|
</ChipGroup.Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
)}
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-details-variant-section"
|
||||||
+428
@@ -0,0 +1,428 @@
|
|||||||
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Heading,
|
||||||
|
Hint,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Text,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldArrayWithId,
|
||||||
|
UseFormReturn,
|
||||||
|
useFieldArray,
|
||||||
|
useWatch,
|
||||||
|
} from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../../../components/common/form"
|
||||||
|
import { SortableList } from "../../../../../../../components/common/sortable-list"
|
||||||
|
import { ChipInput } from "../../../../../../../components/inputs/chip-input"
|
||||||
|
import { ProductCreateSchemaType } from "../../../../types"
|
||||||
|
|
||||||
|
type ProductCreateVariantsSectionProps = {
|
||||||
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPermutations = (
|
||||||
|
data: { title: string; values: string[] }[]
|
||||||
|
): { [key: string]: string }[] => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 1) {
|
||||||
|
return data[0].values.map((value) => ({ [data[0].title]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const toProcess = data[0]
|
||||||
|
const rest = data.slice(1)
|
||||||
|
|
||||||
|
return toProcess.values.flatMap((value) => {
|
||||||
|
return getPermutations(rest).map((permutation) => {
|
||||||
|
return {
|
||||||
|
[toProcess.title]: value,
|
||||||
|
...permutation,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVariantName = (options: Record<string, string>) => {
|
||||||
|
return Object.values(options).join(" / ")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCreateVariantsSection = ({
|
||||||
|
form,
|
||||||
|
}: ProductCreateVariantsSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const options = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "options",
|
||||||
|
})
|
||||||
|
|
||||||
|
const variants = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "variants",
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchedOptions = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "options",
|
||||||
|
defaultValue: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchedVariants = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "variants",
|
||||||
|
defaultValue: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOptionValueUpdate = (index: number, value: string[]) => {
|
||||||
|
const newOptions = [...watchedOptions]
|
||||||
|
newOptions[index].values = value
|
||||||
|
|
||||||
|
const permutations = getPermutations(newOptions)
|
||||||
|
const oldVariants = [...watchedVariants]
|
||||||
|
|
||||||
|
const findMatchingPermutation = (options: Record<string, string>) => {
|
||||||
|
return permutations.find((permutation) =>
|
||||||
|
Object.keys(options).every((key) => options[key] === permutation[key])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVariants = oldVariants.reduce((variants, variant) => {
|
||||||
|
const match = findMatchingPermutation(variant.options)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
variants.push({
|
||||||
|
...variant,
|
||||||
|
title: getVariantName(match),
|
||||||
|
options: match,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return variants
|
||||||
|
}, [] as typeof oldVariants)
|
||||||
|
|
||||||
|
const usedPermutations = new Set(
|
||||||
|
newVariants.map((variant) => variant.options)
|
||||||
|
)
|
||||||
|
const unusedPermutations = permutations.filter(
|
||||||
|
(permutation) => !usedPermutations.has(permutation)
|
||||||
|
)
|
||||||
|
|
||||||
|
unusedPermutations.forEach((permutation) => {
|
||||||
|
newVariants.push({
|
||||||
|
title: getVariantName(permutation),
|
||||||
|
options: permutation,
|
||||||
|
should_create: false,
|
||||||
|
variant_rank: newVariants.length,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
form.setValue("variants", newVariants)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveOption = (index: number) => {
|
||||||
|
options.remove(index)
|
||||||
|
|
||||||
|
const newOptions = [...watchedOptions]
|
||||||
|
newOptions.splice(index, 1)
|
||||||
|
|
||||||
|
const permutations = getPermutations(newOptions)
|
||||||
|
const oldVariants = [...watchedVariants]
|
||||||
|
|
||||||
|
const findMatchingPermutation = (options: Record<string, string>) => {
|
||||||
|
return permutations.find((permutation) =>
|
||||||
|
Object.keys(options).every((key) => options[key] === permutation[key])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVariants = oldVariants.reduce((variants, variant) => {
|
||||||
|
const match = findMatchingPermutation(variant.options)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
variants.push({
|
||||||
|
...variant,
|
||||||
|
title: getVariantName(match),
|
||||||
|
options: match,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return variants
|
||||||
|
}, [] as typeof oldVariants)
|
||||||
|
|
||||||
|
const usedPermutations = new Set(
|
||||||
|
newVariants.map((variant) => variant.options)
|
||||||
|
)
|
||||||
|
const unusedPermutations = permutations.filter(
|
||||||
|
(permutation) => !usedPermutations.has(permutation)
|
||||||
|
)
|
||||||
|
|
||||||
|
unusedPermutations.forEach((permutation) => {
|
||||||
|
newVariants.push({
|
||||||
|
title: getVariantName(permutation),
|
||||||
|
options: permutation,
|
||||||
|
should_create: false,
|
||||||
|
variant_rank: newVariants.length,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
form.setValue("variants", newVariants)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRankChange = (
|
||||||
|
items: FieldArrayWithId<ProductCreateSchemaType, "variants">[]
|
||||||
|
) => {
|
||||||
|
// Items in the SortableList are momorized, so we need to find the current
|
||||||
|
// value to preserve any changes that have been made to `should_create`.
|
||||||
|
const update = items.map((item, index) => {
|
||||||
|
const variant = watchedVariants.find((v) => v.title === item.title)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
...(variant || item),
|
||||||
|
variant_rank: index,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
variants.replace(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCheckboxState = (variants: ProductCreateSchemaType["variants"]) => {
|
||||||
|
if (variants.every((variant) => variant.should_create)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variants.some((variant) => variant.should_create)) {
|
||||||
|
return "indeterminate"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCheckboxChange = (value: boolean | "indeterminate") => {
|
||||||
|
switch (value) {
|
||||||
|
case true: {
|
||||||
|
const update = watchedVariants.map((variant) => {
|
||||||
|
return {
|
||||||
|
...variant,
|
||||||
|
should_create: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
form.setValue("variants", update)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case false: {
|
||||||
|
const update = watchedVariants.map((variant) => {
|
||||||
|
return {
|
||||||
|
...variant,
|
||||||
|
should_create: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
form.setValue("variants", update)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "indeterminate":
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="variants" className="flex flex-col gap-y-8">
|
||||||
|
<Heading level="h2">{t("products.create.variants.header")}</Heading>
|
||||||
|
<div className="flex flex-col gap-y-6">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="options"
|
||||||
|
render={() => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex items-start justify-between gap-x-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Form.Label>
|
||||||
|
{t("products.create.variants.productOptions.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint>
|
||||||
|
{t("products.create.variants.productOptions.hint")}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
options.append({
|
||||||
|
title: "",
|
||||||
|
values: [],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("actions.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ul className="flex flex-col gap-y-4">
|
||||||
|
{options.fields.map((option, index) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={option.id}
|
||||||
|
className="bg-ui-bg-component shadow-elevation-card-rest grid grid-cols-[1fr_28px] items-center gap-1.5 rounded-xl p-1.5"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[min-content,1fr] items-center gap-1.5">
|
||||||
|
<div className="flex items-center px-2 py-1.5">
|
||||||
|
<Label
|
||||||
|
size="xsmall"
|
||||||
|
weight="plus"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
htmlFor={`options.${index}.title`}
|
||||||
|
>
|
||||||
|
{t("fields.title")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
|
||||||
|
{...form.register(
|
||||||
|
`options.${index}.title` as const
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center px-2 py-1.5">
|
||||||
|
<Label
|
||||||
|
size="xsmall"
|
||||||
|
weight="plus"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
htmlFor={`options.${index}.values`}
|
||||||
|
>
|
||||||
|
{t("fields.values")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name={`options.${index}.values` as const}
|
||||||
|
render={({ field: { onChange, ...field } }) => {
|
||||||
|
const handleValueChange = (value: string[]) => {
|
||||||
|
handleOptionValueUpdate(index, value)
|
||||||
|
onChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChipInput
|
||||||
|
{...field}
|
||||||
|
variant="contrast"
|
||||||
|
onChange={handleValueChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="text-ui-fg-muted"
|
||||||
|
onClick={() => handleRemoveOption(index)}
|
||||||
|
>
|
||||||
|
<XMarkMini />
|
||||||
|
</IconButton>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-x-4 gap-y-4">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<Label weight="plus">
|
||||||
|
{t("products.create.variants.productVariants.label")}
|
||||||
|
</Label>
|
||||||
|
<Hint>{t("products.create.variants.productVariants.hint")}</Hint>
|
||||||
|
</div>
|
||||||
|
{variants.fields.length > 0 ? (
|
||||||
|
<div className="overflow-hidden rounded-xl border">
|
||||||
|
<div
|
||||||
|
className="bg-ui-bg-component text-ui-fg-subtle grid items-center gap-3 border-b px-6 py-3.5"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `20px 28px repeat(${watchedOptions.length}, 1fr)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
checked={getCheckboxState(watchedVariants)}
|
||||||
|
onCheckedChange={onCheckboxChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
{watchedOptions.map((option, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{option.title}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<SortableList
|
||||||
|
items={variants.fields}
|
||||||
|
onChange={handleRankChange}
|
||||||
|
renderItem={(item, index) => {
|
||||||
|
return (
|
||||||
|
<SortableList.Item
|
||||||
|
id={item.id}
|
||||||
|
className={clx("bg-ui-bg-base border-b", {
|
||||||
|
"border-b-0": index === variants.fields.length - 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="text-ui-fg-subtle grid w-full items-center gap-3 px-6 py-3.5"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `20px 28px repeat(${watchedOptions.length}, 1fr)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name={`variants.${index}.should_create` as const}
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Control>
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SortableList.DragHandle />
|
||||||
|
{Object.values(item.options).map((value, index) => (
|
||||||
|
<Text key={index} size="small" leading="compact">
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableList.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Alert>{t("products.create.variants.productVariants.alert")}</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-sales-channel-drawer"
|
||||||
+198
@@ -0,0 +1,198 @@
|
|||||||
|
import { AdminSalesChannelResponse } from "@medusajs/types"
|
||||||
|
import { Button, Checkbox } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
OnChangeFn,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { SplitView } from "../../../../../../../components/layout/split-view"
|
||||||
|
import { DataTable } from "../../../../../../../components/table/data-table"
|
||||||
|
import { useSalesChannels } from "../../../../../../../hooks/api/sales-channels"
|
||||||
|
import { useSalesChannelTableColumns } from "../../../../../../../hooks/table/columns/use-sales-channel-table-columns"
|
||||||
|
import { useSalesChannelTableFilters } from "../../../../../../../hooks/table/filters/use-sales-channel-table-filters"
|
||||||
|
import { useSalesChannelTableQuery } from "../../../../../../../hooks/table/query/use-sales-channel-table-query"
|
||||||
|
import { useDataTable } from "../../../../../../../hooks/use-data-table"
|
||||||
|
import { ProductCreateSchemaType } from "../../../../types"
|
||||||
|
import { useProductCreateDetailsContext } from "../product-create-details-context"
|
||||||
|
|
||||||
|
type ProductCreateSalesChannelDrawerProps = {
|
||||||
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
const PREFIX = "sc"
|
||||||
|
|
||||||
|
export const ProductCreateSalesChannelDrawer = ({
|
||||||
|
form,
|
||||||
|
}: ProductCreateSalesChannelDrawerProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { open, onOpenChange } = useProductCreateDetailsContext()
|
||||||
|
const { getValues, setValue } = form
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<RowSelectionState>({})
|
||||||
|
const [state, setState] = useState<{ id: string; name: string }[]>([])
|
||||||
|
|
||||||
|
const { searchParams, raw } = useSalesChannelTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
const { sales_channels, count, isLoading, isError, error } = useSalesChannels(
|
||||||
|
{
|
||||||
|
...searchParams,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const salesChannels = getValues("sales_channels")
|
||||||
|
|
||||||
|
if (salesChannels) {
|
||||||
|
setState(
|
||||||
|
salesChannels.map((channel) => ({
|
||||||
|
id: channel.id,
|
||||||
|
name: channel.name,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
setSelection(
|
||||||
|
salesChannels.reduce(
|
||||||
|
(acc, channel) => ({
|
||||||
|
...acc,
|
||||||
|
[channel.id]: true,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [open, getValues])
|
||||||
|
|
||||||
|
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||||
|
const value = typeof fn === "function" ? fn(selection) : fn
|
||||||
|
const ids = Object.keys(value)
|
||||||
|
|
||||||
|
const addedIdsSet = new Set(ids.filter((id) => value[id] && !selection[id]))
|
||||||
|
|
||||||
|
let addedSalesChannels: { id: string; name: string }[] = []
|
||||||
|
|
||||||
|
if (addedIdsSet.size > 0) {
|
||||||
|
addedSalesChannels =
|
||||||
|
sales_channels?.filter((channel) => addedIdsSet.has(channel.id)) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prev) => {
|
||||||
|
const filteredPrev = prev.filter((channel) => value[channel.id])
|
||||||
|
return Array.from(new Set([...filteredPrev, ...addedSalesChannels]))
|
||||||
|
})
|
||||||
|
setSelection(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setValue("sales_channels", state, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = useSalesChannelTableFilters()
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: sales_channels ?? [],
|
||||||
|
columns,
|
||||||
|
count: sales_channels?.length ?? 0,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
rowSelection: {
|
||||||
|
state: selection,
|
||||||
|
updater,
|
||||||
|
},
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
||||||
|
<div className="flex-1">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
filters={filters}
|
||||||
|
isLoading={isLoading}
|
||||||
|
layout="fill"
|
||||||
|
orderBy={["name", "created_at", "updated_at"]}
|
||||||
|
queryObject={raw}
|
||||||
|
search
|
||||||
|
pagination
|
||||||
|
count={count}
|
||||||
|
prefix={PREFIX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-x-2 border-t px-6 py-4">
|
||||||
|
<SplitView.Close asChild>
|
||||||
|
<Button size="small" variant="secondary" type="button">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</SplitView.Close>
|
||||||
|
<Button size="small" onClick={handleAdd} type="button">
|
||||||
|
{t("actions.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper =
|
||||||
|
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useSalesChannelTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-details-form"
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import { Heading, Text } from "@medusajs/ui"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import { ProductCreateSchemaType } from "../../types"
|
||||||
|
import { ProductCreateAttributeSection } from "./components/product-create-details-attribute-section"
|
||||||
|
import { ProductCreateDetailsContext } from "./components/product-create-details-context"
|
||||||
|
import { ProductCreateGeneralSection } from "./components/product-create-details-general-section"
|
||||||
|
import { ProductCreateOrganizationSection } from "./components/product-create-details-organize-section"
|
||||||
|
import { ProductCreateVariantsSection } from "./components/product-create-details-variant-section"
|
||||||
|
import { ProductCreateSalesChannelDrawer } from "./components/product-create-sales-channel-drawer"
|
||||||
|
|
||||||
|
type ProductAttributesProps = {
|
||||||
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCreateDetailsForm = ({ form }: ProductAttributesProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProductCreateDetailsContext.Provider
|
||||||
|
value={{ open, onOpenChange: setOpen }}
|
||||||
|
>
|
||||||
|
<SplitView open={open} onOpenChange={setOpen}>
|
||||||
|
<SplitView.Content>
|
||||||
|
<div className="flex flex-col items-center p-16">
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
|
<Header />
|
||||||
|
<ProductCreateGeneralSection form={form} />
|
||||||
|
<Divider />
|
||||||
|
<ProductCreateVariantsSection form={form} />
|
||||||
|
<Divider />
|
||||||
|
<ProductCreateOrganizationSection form={form} />
|
||||||
|
<Divider />
|
||||||
|
<ProductCreateAttributeSection form={form} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Content>
|
||||||
|
<SplitView.Drawer>
|
||||||
|
<ProductCreateSalesChannelDrawer form={form} />
|
||||||
|
</SplitView.Drawer>
|
||||||
|
</SplitView>
|
||||||
|
</ProductCreateDetailsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Heading>{t("products.create.header")}</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("products.create.hint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-create-form"
|
||||||
+50
-36
@@ -1,53 +1,71 @@
|
|||||||
import { Button, ProgressStatus, ProgressTabs } from "@medusajs/ui"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, ProgressStatus, ProgressTabs, toast } from "@medusajs/ui"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { useState } from "react"
|
import { useCreateProduct } from "../../../../../hooks/api/products"
|
||||||
import { useTranslation } from "react-i18next"
|
import { VariantPricingForm } from "../../../common/variant-pricing-form"
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import {
|
import {
|
||||||
CreateProductSchema,
|
PRODUCT_CREATE_FORM_DEFAULTS,
|
||||||
CreateProductSchemaType,
|
ProductCreateSchema,
|
||||||
defaults,
|
} from "../../constants"
|
||||||
normalize,
|
import { ProductCreateSchemaType } from "../../types"
|
||||||
} from "../schema"
|
import { normalizeProductFormValues } from "../../utils"
|
||||||
import { useCreateProduct } from "../../../../hooks/api/products"
|
import { ProductCreateDetailsForm } from "../product-create-details-form"
|
||||||
import { ProductAttributesForm } from "./product-attributes-form"
|
|
||||||
import { VariantPricingForm } from "../../common/variant-pricing-form"
|
|
||||||
|
|
||||||
enum Tab {
|
enum Tab {
|
||||||
PRODUCT = "product",
|
PRODUCT = "product",
|
||||||
PRICE = "price",
|
PRICE = "price",
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabState = Record<Tab, ProgressStatus>
|
type TabState = Record<Tab, ProgressStatus>
|
||||||
|
|
||||||
export const CreateProductPage = () => {
|
const SAVE_DRAFT_BUTTON = "save-draft-button"
|
||||||
const { t } = useTranslation()
|
|
||||||
|
export const ProductCreateForm = () => {
|
||||||
const [tab, setTab] = useState<Tab>(Tab.PRODUCT)
|
const [tab, setTab] = useState<Tab>(Tab.PRODUCT)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const form = useForm<CreateProductSchemaType>({
|
|
||||||
defaultValues: defaults,
|
const form = useForm<ProductCreateSchemaType>({
|
||||||
resolver: zodResolver(CreateProductSchema),
|
defaultValues: PRODUCT_CREATE_FORM_DEFAULTS,
|
||||||
|
resolver: zodResolver(ProductCreateSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useCreateProduct()
|
const { mutateAsync, isPending } = useCreateProduct()
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(
|
const handleSubmit = form.handleSubmit(
|
||||||
async (values, e) => {
|
async (values, e) => {
|
||||||
if (!(e?.nativeEvent instanceof SubmitEvent)) return
|
if (!(e?.nativeEvent instanceof SubmitEvent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const submitter = e?.nativeEvent?.submitter as HTMLButtonElement
|
const submitter = e?.nativeEvent?.submitter as HTMLButtonElement
|
||||||
if (!(submitter instanceof HTMLButtonElement)) return
|
|
||||||
const isDraftSubmission = submitter.dataset.name === "save-draft-button"
|
if (!(submitter instanceof HTMLButtonElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDraftSubmission = submitter.dataset.name === SAVE_DRAFT_BUTTON
|
||||||
|
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
normalize({
|
normalizeProductFormValues({
|
||||||
...values,
|
...values,
|
||||||
status: (isDraftSubmission ? "draft" : "published") as any,
|
status: (isDraftSubmission ? "draft" : "published") as any,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
onSuccess: ({ product }) => {
|
onSuccess: ({ product }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
description: t("products.create.successToast", {
|
||||||
|
title: product.title,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
handleSuccess(`../${product.id}`)
|
handleSuccess(`../${product.id}`)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -79,13 +97,13 @@ export const CreateProductPage = () => {
|
|||||||
status={tabState.product}
|
status={tabState.product}
|
||||||
value={Tab.PRODUCT}
|
value={Tab.PRODUCT}
|
||||||
>
|
>
|
||||||
Products
|
{t("products.create.tabs.details")}
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
status={tabState.price}
|
status={tabState.price}
|
||||||
value={Tab.PRICE}
|
value={Tab.PRICE}
|
||||||
>
|
>
|
||||||
Prices
|
{t("products.create.tabs.variants")}
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
</ProgressTabs.List>
|
</ProgressTabs.List>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,32 +114,28 @@ export const CreateProductPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</RouteFocusModal.Close>
|
</RouteFocusModal.Close>
|
||||||
<Button
|
<Button
|
||||||
className="whitespace-nowrap"
|
data-name={SAVE_DRAFT_BUTTON}
|
||||||
data-name="save-draft-button"
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
size="small"
|
||||||
key="submit-button"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{t("actions.saveAsDraft")}
|
{t("actions.saveAsDraft")}
|
||||||
</Button>
|
</Button>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
tab={tab}
|
tab={tab}
|
||||||
next={() => setTab(Tab.PRICE)}
|
next={() => setTab(Tab.PRICE)}
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
<RouteFocusModal.Body className="size-full overflow-hidden">
|
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||||
<ProgressTabs.Content
|
<ProgressTabs.Content
|
||||||
className="size-full overflow-y-auto"
|
className="size-full overflow-hidden"
|
||||||
value={Tab.PRODUCT}
|
value={Tab.PRODUCT}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full">
|
<ProductCreateDetailsForm form={form} />
|
||||||
<ProductAttributesForm form={form} />
|
|
||||||
</div>
|
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
<ProgressTabs.Content
|
<ProgressTabs.Content
|
||||||
className="size-full overflow-y-auto"
|
className="size-full overflow-y-auto"
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const ProductCreateSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string().optional(),
|
||||||
|
handle: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
discountable: z.boolean(),
|
||||||
|
type_id: z.string().optional(),
|
||||||
|
collection_id: z.string().optional(),
|
||||||
|
categories: z.array(z.string()),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
sales_channels: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
origin_country: z.string().optional(),
|
||||||
|
material: z.string().optional(),
|
||||||
|
width: z.string().optional(),
|
||||||
|
length: z.string().optional(),
|
||||||
|
height: z.string().optional(),
|
||||||
|
weight: z.string().optional(),
|
||||||
|
mid_code: z.string().optional(),
|
||||||
|
hs_code: z.string().optional(),
|
||||||
|
options: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
values: z.array(z.string()),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
variants: z.array(
|
||||||
|
z.object({
|
||||||
|
should_create: z.boolean(),
|
||||||
|
title: z.string(),
|
||||||
|
options: z.record(z.string(), z.string()),
|
||||||
|
variant_rank: z.number(),
|
||||||
|
prices: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
images: z.array(z.string()).optional(),
|
||||||
|
thumbnail: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PRODUCT_CREATE_FORM_DEFAULTS: Partial<
|
||||||
|
z.infer<typeof ProductCreateSchema>
|
||||||
|
> = {
|
||||||
|
discountable: true,
|
||||||
|
tags: [],
|
||||||
|
sales_channels: [],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [],
|
||||||
|
images: [],
|
||||||
|
thumbnail: "",
|
||||||
|
categories: [],
|
||||||
|
collection_id: "",
|
||||||
|
description: "",
|
||||||
|
handle: "",
|
||||||
|
height: "",
|
||||||
|
hs_code: "",
|
||||||
|
length: "",
|
||||||
|
material: "",
|
||||||
|
mid_code: "",
|
||||||
|
origin_country: "",
|
||||||
|
subtitle: "",
|
||||||
|
title: "",
|
||||||
|
type_id: "",
|
||||||
|
weight: "",
|
||||||
|
width: "",
|
||||||
|
}
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { CreateProductPage } from "./components/create-product"
|
import { ProductCreateForm } from "./components/product-create-form/product-create-form"
|
||||||
|
|
||||||
export const ProductCreate = () => {
|
export const ProductCreate = () => {
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
<CreateProductPage />
|
<ProductCreateForm />
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { CreateProductDTO, CreateProductVariantDTO } from "@medusajs/types"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
export const CreateProductSchema = zod.object({
|
|
||||||
title: zod.string(),
|
|
||||||
subtitle: zod.string().optional(),
|
|
||||||
handle: zod.string().optional(),
|
|
||||||
description: zod.string().optional(),
|
|
||||||
discountable: zod.boolean(),
|
|
||||||
type_id: zod.string().optional(),
|
|
||||||
collection_id: zod.string().optional(),
|
|
||||||
category_ids: zod.array(zod.string()).optional(),
|
|
||||||
tags: zod.array(zod.string()).optional(),
|
|
||||||
sales_channels: zod.array(zod.string()).optional(),
|
|
||||||
origin_country: zod.string().optional(),
|
|
||||||
material: zod.string().optional(),
|
|
||||||
width: zod.string().optional(),
|
|
||||||
length: zod.string().optional(),
|
|
||||||
height: zod.string().optional(),
|
|
||||||
weight: zod.string().optional(),
|
|
||||||
mid_code: zod.string().optional(),
|
|
||||||
hs_code: zod.string().optional(),
|
|
||||||
options: zod.array(
|
|
||||||
zod.object({
|
|
||||||
title: zod.string(),
|
|
||||||
values: zod.array(zod.string()),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
variants: zod.array(
|
|
||||||
zod.object({
|
|
||||||
title: zod.string(),
|
|
||||||
options: zod.record(zod.string(), zod.string()),
|
|
||||||
variant_rank: zod.number(),
|
|
||||||
prices: zod.record(zod.string(), zod.string()).optional(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
images: zod.array(zod.string()).optional(),
|
|
||||||
thumbnail: zod.string().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const defaults = {
|
|
||||||
discountable: true,
|
|
||||||
tags: [],
|
|
||||||
sales_channels: [],
|
|
||||||
options: [],
|
|
||||||
variants: [],
|
|
||||||
images: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const normalize = (
|
|
||||||
values: CreateProductSchemaType & { status: CreateProductDTO["status"] }
|
|
||||||
) => {
|
|
||||||
const reqData = {
|
|
||||||
...values,
|
|
||||||
is_giftcard: false,
|
|
||||||
tags: values.tags?.map((tag) => ({ value: tag })),
|
|
||||||
sales_channels: values.sales_channels?.map((sc) => ({ id: sc })),
|
|
||||||
width: values.width ? parseFloat(values.width) : undefined,
|
|
||||||
length: values.length ? parseFloat(values.length) : undefined,
|
|
||||||
height: values.height ? parseFloat(values.height) : undefined,
|
|
||||||
weight: values.weight ? parseFloat(values.weight) : undefined,
|
|
||||||
variants: normalizeVariants(values.variants as any),
|
|
||||||
} as any
|
|
||||||
|
|
||||||
return reqData
|
|
||||||
}
|
|
||||||
|
|
||||||
export const normalizeVariants = (
|
|
||||||
variants: (Partial<CreateProductVariantDTO> & {
|
|
||||||
prices?: Record<string, string>
|
|
||||||
})[]
|
|
||||||
) => {
|
|
||||||
return variants.map((variant) => ({
|
|
||||||
...variant,
|
|
||||||
prices: Object.entries(variant.prices || {}).map(([key, value]: any) => ({
|
|
||||||
currency_code: key,
|
|
||||||
amount: value ? parseFloat(value) : 0,
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateProductSchemaType = zod.infer<typeof CreateProductSchema>
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { ProductCreateSchema } from "./constants"
|
||||||
|
|
||||||
|
export type ProductCreateSchemaType = z.infer<typeof ProductCreateSchema>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { CreateProductDTO } from "@medusajs/types"
|
||||||
|
import { ProductCreateSchemaType } from "./types"
|
||||||
|
|
||||||
|
export const normalizeProductFormValues = (
|
||||||
|
values: ProductCreateSchemaType & { status: CreateProductDTO["status"] }
|
||||||
|
) => {
|
||||||
|
const reqData = {
|
||||||
|
...values,
|
||||||
|
is_giftcard: false,
|
||||||
|
tags: values?.tags?.length
|
||||||
|
? values.tags?.map((tag) => ({ value: tag }))
|
||||||
|
: undefined,
|
||||||
|
sales_channels: values?.sales_channels?.length
|
||||||
|
? values.sales_channels?.map((sc) => ({ id: sc.id }))
|
||||||
|
: undefined,
|
||||||
|
images: values.images?.length ? values.images : undefined,
|
||||||
|
collection_id: values.collection_id || undefined,
|
||||||
|
categories: values.categories.map((id) => ({ id })),
|
||||||
|
type_id: values.type_id || undefined,
|
||||||
|
handle: values.handle || undefined,
|
||||||
|
origin_country: values.origin_country || undefined,
|
||||||
|
material: values.material || undefined,
|
||||||
|
mid_code: values.mid_code || undefined,
|
||||||
|
hs_code: values.hs_code || undefined,
|
||||||
|
thumbnail: values.thumbnail || undefined,
|
||||||
|
subtitle: values.subtitle || undefined,
|
||||||
|
description: values.description || undefined,
|
||||||
|
width: values.width ? parseFloat(values.width) : undefined,
|
||||||
|
length: values.length ? parseFloat(values.length) : undefined,
|
||||||
|
height: values.height ? parseFloat(values.height) : undefined,
|
||||||
|
weight: values.weight ? parseFloat(values.weight) : undefined,
|
||||||
|
variants: normalizeVariants(values.variants),
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqData
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeVariants = (
|
||||||
|
variants: ProductCreateSchemaType["variants"]
|
||||||
|
) => {
|
||||||
|
return variants
|
||||||
|
.filter((variant) => variant.should_create)
|
||||||
|
.map((variant) => ({
|
||||||
|
title: Object.values(variant.options || {}).join(" / "),
|
||||||
|
options: variant.options,
|
||||||
|
prices: Object.entries(variant.prices || {}).map(([key, value]: any) => ({
|
||||||
|
currency_code: key,
|
||||||
|
amount: value ? parseFloat(value) : 0,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
}
|
||||||
+3
-3
@@ -6,17 +6,17 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { Fragment } from "react"
|
import { Fragment } from "react"
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
|
||||||
import { Divider } from "../../../../../components/common/divider"
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
|
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateProductVariant } from "../../../../../hooks/api/products"
|
||||||
import { castNumber } from "../../../../../lib/cast-number"
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
import { optionalInt } from "../../../../../lib/validation"
|
import { optionalInt } from "../../../../../lib/validation"
|
||||||
import { useUpdateProductVariant } from "../../../../../hooks/api/products"
|
|
||||||
|
|
||||||
type ProductEditVariantFormProps = {
|
type ProductEditVariantFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
|
|||||||
+7
-8
@@ -1,21 +1,20 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Button, Input, Select } from "@medusajs/ui"
|
import { Button, Select } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
import { useProductTypes } from "../../../../../hooks/api/product-types"
|
|
||||||
import { useTags } from "../../../../../hooks/api/tags"
|
|
||||||
import { useCollections } from "../../../../../hooks/api/collections"
|
|
||||||
import { useCategories } from "../../../../../hooks/api/categories"
|
import { useCategories } from "../../../../../hooks/api/categories"
|
||||||
|
import { useCollections } from "../../../../../hooks/api/collections"
|
||||||
|
import { useProductTypes } from "../../../../../hooks/api/product-types"
|
||||||
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
|
import { useTags } from "../../../../../hooks/api/tags"
|
||||||
|
|
||||||
type ProductOrganizationFormProps = {
|
type ProductOrganizationFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -58,7 +57,7 @@ export const ProductOrganizationForm = ({
|
|||||||
category_ids: data.category_ids || undefined,
|
category_ids: data.category_ids || undefined,
|
||||||
tags:
|
tags:
|
||||||
data.tags?.map((t) => {
|
data.tags?.map((t) => {
|
||||||
id: t
|
t
|
||||||
}) || undefined,
|
}) || undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-8
@@ -1,13 +1,13 @@
|
|||||||
import { useUpdateProductVariant } from "../../../hooks/api/products"
|
|
||||||
import { RouteFocusModal, useRouteModal } from "../../../components/route-modal"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { ExtendedProductDTO } from "../../../types/api-responses"
|
|
||||||
import { VariantPricingForm } from "../common/variant-pricing-form"
|
|
||||||
import { normalizeVariants } from "../product-create/schema"
|
|
||||||
import { Button } from "@medusajs/ui"
|
import { Button } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
import { RouteFocusModal, useRouteModal } from "../../../components/route-modal"
|
||||||
|
import { useUpdateProductVariant } from "../../../hooks/api/products"
|
||||||
|
import { ExtendedProductDTO } from "../../../types/api-responses"
|
||||||
|
import { VariantPricingForm } from "../common/variant-pricing-form"
|
||||||
|
import { normalizeVariants } from "../product-create/utils"
|
||||||
|
|
||||||
export const UpdateVariantPricesSchema = zod.object({
|
export const UpdateVariantPricesSchema = zod.object({
|
||||||
variants: zod.array(
|
variants: zod.array(
|
||||||
@@ -38,7 +38,7 @@ export const PricingEdit = ({ product }: { product: ExtendedProductDTO }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Add batch update method here
|
// TODO: Add batch update method here
|
||||||
const { mutateAsync, isLoading } = useUpdateProductVariant(product.id, "")
|
const { mutateAsync, isPending } = useUpdateProductVariant(product.id, "")
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(
|
const handleSubmit = form.handleSubmit(
|
||||||
async (values) => {
|
async (values) => {
|
||||||
@@ -69,7 +69,7 @@ export const PricingEdit = ({ product }: { product: ExtendedProductDTO }) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
>
|
>
|
||||||
{t("actions.save")}
|
{t("actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+4
-4
@@ -7,8 +7,8 @@ import { Fragment, useState } from "react"
|
|||||||
import { useFieldArray, useForm } from "react-hook-form"
|
import { useFieldArray, useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { Combobox } from "../../../../../../components/common/combobox"
|
|
||||||
import { Form } from "../../../../../../components/common/form"
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../../components/inputs/combobox"
|
||||||
import { RouteDrawer } from "../../../../../../components/route-modal"
|
import { RouteDrawer } from "../../../../../../components/route-modal"
|
||||||
import { usePromotionRuleValues } from "../../../../../../hooks/api/promotions"
|
import { usePromotionRuleValues } from "../../../../../../hooks/api/promotions"
|
||||||
import { RuleTypeValues } from "../../edit-rules"
|
import { RuleTypeValues } from "../../edit-rules"
|
||||||
@@ -162,7 +162,7 @@ export const RulesFormField = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${fieldRule.id}.${index}`}>
|
<Fragment key={`${fieldRule.id}.${index}`}>
|
||||||
<div className="flex flex-row gap-2 bg-ui-bg-subtle py-2 px-2 rounded-xl border border-ui-border-base">
|
<div className="bg-ui-bg-subtle border-ui-border-base flex flex-row gap-2 rounded-xl border px-2 py-2">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
key={`${identifier}.${scope}.${attributeFields.name}`}
|
key={`${identifier}.${scope}.${attributeFields.name}`}
|
||||||
@@ -299,7 +299,7 @@ export const RulesFormField = ({
|
|||||||
|
|
||||||
{index < fields.length - 1 && (
|
{index < fields.length - 1 && (
|
||||||
<div className="relative px-6 py-3">
|
<div className="relative px-6 py-3">
|
||||||
<div className="absolute top-0 bottom-0 left-[40px] z-[-1] border-ui-border-strong w-px bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-repeat-y"></div>
|
<div className="border-ui-border-strong absolute bottom-0 left-[40px] top-0 z-[-1] w-px bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-repeat-y"></div>
|
||||||
|
|
||||||
<Badge size="2xsmall" className=" text-xs">
|
<Badge size="2xsmall" className=" text-xs">
|
||||||
{t("promotions.form.and")}
|
{t("promotions.form.and")}
|
||||||
@@ -330,7 +330,7 @@ export const RulesFormField = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
className="inline-block text-ui-fg-muted hover:text-ui-fg-subtle ml-2"
|
className="text-ui-fg-muted hover:text-ui-fg-subtle ml-2 inline-block"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const indicesToRemove = fields
|
const indicesToRemove = fields
|
||||||
.map((field, index) => (field.required ? null : index))
|
.map((field, index) => (field.required ? null : index))
|
||||||
|
|||||||
+12
-12
@@ -21,7 +21,7 @@ import {
|
|||||||
RuleOperatorOptionsResponse,
|
RuleOperatorOptionsResponse,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
@@ -358,7 +358,7 @@ export const CreatePromotionForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
|
|
||||||
<RouteFocusModal.Body className="w-[800px] mx-auto my-20">
|
<RouteFocusModal.Body className="mx-auto my-20 w-[800px]">
|
||||||
<ProgressTabs.Content value={Tab.TYPE}>
|
<ProgressTabs.Content value={Tab.TYPE}>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -382,7 +382,7 @@ export const CreatePromotionForm = ({
|
|||||||
label={template.title}
|
label={template.title}
|
||||||
description={template.description}
|
description={template.description}
|
||||||
className={clx("", {
|
className={clx("", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
template.id === field.value,
|
template.id === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -399,7 +399,7 @@ export const CreatePromotionForm = ({
|
|||||||
|
|
||||||
<ProgressTabs.Content
|
<ProgressTabs.Content
|
||||||
value={Tab.PROMOTION}
|
value={Tab.PROMOTION}
|
||||||
className="flex flex-col gap-10 flex-1"
|
className="flex flex-1 flex-col gap-10"
|
||||||
>
|
>
|
||||||
{form.formState.errors.root && (
|
{form.formState.errors.root && (
|
||||||
<Alert
|
<Alert
|
||||||
@@ -432,7 +432,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.method.code.description"
|
"promotions.form.method.code.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"false" === field.value,
|
"false" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -443,7 +443,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.method.automatic.description"
|
"promotions.form.method.automatic.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"true" === field.value,
|
"true" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -509,7 +509,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.value_type.fixed.description"
|
"promotions.form.value_type.fixed.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"fixed" === field.value,
|
"fixed" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -523,7 +523,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.value_type.percentage.description"
|
"promotions.form.value_type.percentage.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"percentage" === field.value,
|
"percentage" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -604,7 +604,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.type.standard.description"
|
"promotions.form.type.standard.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"standard" === field.value,
|
"standard" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -616,7 +616,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.type.buyget.description"
|
"promotions.form.type.buyget.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"buyget" === field.value,
|
"buyget" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -652,7 +652,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.allocation.each.description"
|
"promotions.form.allocation.each.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"each" === field.value,
|
"each" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -666,7 +666,7 @@ export const CreatePromotionForm = ({
|
|||||||
"promotions.form.allocation.across.description"
|
"promotions.form.allocation.across.description"
|
||||||
)}
|
)}
|
||||||
className={clx("basis-1/2", {
|
className={clx("basis-1/2", {
|
||||||
"border-2 border-ui-border-interactive":
|
"border-ui-border-interactive border-2":
|
||||||
"across" === field.value,
|
"across" === field.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import { Trans, useTranslation } from "react-i18next"
|
|||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
|
|||||||
+1
-1
@@ -19,8 +19,8 @@ import * as zod from "zod"
|
|||||||
|
|
||||||
import { PaymentProviderDTO, RegionCountryDTO } from "@medusajs/types"
|
import { PaymentProviderDTO, RegionCountryDTO } from "@medusajs/types"
|
||||||
|
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
import { SplitView } from "../../../../../components/layout/split-view"
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
|
|||||||
+4
-4
@@ -1,18 +1,18 @@
|
|||||||
|
import { PaymentProviderDTO, RegionDTO } from "@medusajs/types"
|
||||||
import { Button, Input, Select, Text, toast } from "@medusajs/ui"
|
import { Button, Input, Select, Text, toast } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { PaymentProviderDTO, RegionDTO } from "@medusajs/types"
|
|
||||||
|
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox/index.ts"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { formatProvider } from "../../../../../lib/format-provider"
|
|
||||||
import { CurrencyInfo } from "../../../../../lib/currencies"
|
|
||||||
import { useUpdateRegion } from "../../../../../hooks/api/regions.tsx"
|
import { useUpdateRegion } from "../../../../../hooks/api/regions.tsx"
|
||||||
|
import { CurrencyInfo } from "../../../../../lib/currencies"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
|
||||||
type EditRegionFormProps = {
|
type EditRegionFormProps = {
|
||||||
region: RegionDTO
|
region: RegionDTO
|
||||||
|
|||||||
+7
-7
@@ -6,17 +6,17 @@ import {
|
|||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../../components/route-modal"
|
} from "../../../../../../components/route-modal"
|
||||||
|
|
||||||
import { Combobox } from "../../../../../../components/common/combobox"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Form } from "../../../../../../components/common/form"
|
|
||||||
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
|
||||||
import { InventoryNext } from "@medusajs/types"
|
import { InventoryNext } from "@medusajs/types"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useCreateReservationItem } from "../../../../../../hooks/api/reservations"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useInventoryItems } from "../../../../../../hooks/api/inventory"
|
|
||||||
import { useStockLocations } from "../../../../../../hooks/api/stock-locations"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { Combobox } from "../../../../../../components/inputs/combobox"
|
||||||
|
import { useInventoryItems } from "../../../../../../hooks/api/inventory"
|
||||||
|
import { useCreateReservationItem } from "../../../../../../hooks/api/reservations"
|
||||||
|
import { useStockLocations } from "../../../../../../hooks/api/stock-locations"
|
||||||
|
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
||||||
|
|
||||||
export const CreateReservationSchema = zod.object({
|
export const CreateReservationSchema = zod.object({
|
||||||
inventory_item_id: zod.string().min(1),
|
inventory_item_id: zod.string().min(1),
|
||||||
|
|||||||
+1
-1
@@ -3,8 +3,8 @@ import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
|
|||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
|
|||||||
+1
-1
@@ -4,8 +4,8 @@ import { useForm } from "react-hook-form"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import * as zod from "zod"
|
|||||||
|
|
||||||
import { TaxRegionResponse } from "@medusajs/types"
|
import { TaxRegionResponse } from "@medusajs/types"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ import { TaxRegionResponse } from "@medusajs/types"
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { useSearchParams } from "react-router-dom"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import { SplitView } from "../../../../../components/layout/split-view"
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import { TaxRateResponse, TaxRegionResponse } from "@medusajs/types"
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { useSearchParams } from "react-router-dom"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { PercentageInput } from "../../../../../components/common/percentage-input"
|
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||||
import { SplitView } from "../../../../../components/layout/split-view"
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
|
|||||||
@@ -4340,6 +4340,55 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/accessibility@npm:^3.1.0":
|
||||||
|
version: 3.1.0
|
||||||
|
resolution: "@dnd-kit/accessibility@npm:3.1.0"
|
||||||
|
dependencies:
|
||||||
|
tslib: ^2.0.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
checksum: 4f9d24e801d66d4fbb551ec389ed90424dd4c5bbdf527000a618e9abb9833cbd84d9a79e362f470ccbccfbd6d00217a9212c92f3cef66e01c951c7f79625b9d7
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/core@npm:^6.1.0":
|
||||||
|
version: 6.1.0
|
||||||
|
resolution: "@dnd-kit/core@npm:6.1.0"
|
||||||
|
dependencies:
|
||||||
|
"@dnd-kit/accessibility": ^3.1.0
|
||||||
|
"@dnd-kit/utilities": ^3.2.2
|
||||||
|
tslib: ^2.0.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
react-dom: ">=16.8.0"
|
||||||
|
checksum: c793eb97cb59285ca8937ebcdfcd27cff09d750ae06722e36ca5ed07925e41abc36a38cff98f9f6056f7a07810878d76909826142a2968330e7e22060e6be584
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/sortable@npm:^8.0.0":
|
||||||
|
version: 8.0.0
|
||||||
|
resolution: "@dnd-kit/sortable@npm:8.0.0"
|
||||||
|
dependencies:
|
||||||
|
"@dnd-kit/utilities": ^3.2.2
|
||||||
|
tslib: ^2.0.0
|
||||||
|
peerDependencies:
|
||||||
|
"@dnd-kit/core": ^6.1.0
|
||||||
|
react: ">=16.8.0"
|
||||||
|
checksum: a6066c652b892c6a11320c7d8f5c18fdf723e721e8eea37f4ab657dee1ac5e7ca710ac32ce0712a57fe968bc07c13bcea5d5599d90dfdd95619e162befd4d2fb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@dnd-kit/utilities@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "@dnd-kit/utilities@npm:3.2.2"
|
||||||
|
dependencies:
|
||||||
|
tslib: ^2.0.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
checksum: 9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@emotion/is-prop-valid@npm:1.2.1":
|
"@emotion/is-prop-valid@npm:1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "@emotion/is-prop-valid@npm:1.2.1"
|
resolution: "@emotion/is-prop-valid@npm:1.2.1"
|
||||||
@@ -6828,6 +6877,8 @@ __metadata:
|
|||||||
resolution: "@medusajs/dashboard@workspace:packages/admin-next/dashboard"
|
resolution: "@medusajs/dashboard@workspace:packages/admin-next/dashboard"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ariakit/react": ^0.4.1
|
"@ariakit/react": ^0.4.1
|
||||||
|
"@dnd-kit/core": ^6.1.0
|
||||||
|
"@dnd-kit/sortable": ^8.0.0
|
||||||
"@hookform/resolvers": 3.3.2
|
"@hookform/resolvers": 3.3.2
|
||||||
"@medusajs/icons": "workspace:^"
|
"@medusajs/icons": "workspace:^"
|
||||||
"@medusajs/medusa": "workspace:^"
|
"@medusajs/medusa": "workspace:^"
|
||||||
@@ -6841,8 +6892,8 @@ __metadata:
|
|||||||
"@tanstack/react-table": 8.10.7
|
"@tanstack/react-table": 8.10.7
|
||||||
"@tanstack/react-virtual": ^3.0.4
|
"@tanstack/react-virtual": ^3.0.4
|
||||||
"@types/node": ^20.11.15
|
"@types/node": ^20.11.15
|
||||||
"@types/react": 18.2.43
|
"@types/react": ^18.2.79
|
||||||
"@types/react-dom": 18.2.17
|
"@types/react-dom": ^18.2.25
|
||||||
"@uiw/react-json-view": ^2.0.0-alpha.17
|
"@uiw/react-json-view": ^2.0.0-alpha.17
|
||||||
"@vitejs/plugin-react": 4.2.1
|
"@vitejs/plugin-react": 4.2.1
|
||||||
autoprefixer: ^10.4.17
|
autoprefixer: ^10.4.17
|
||||||
@@ -6857,10 +6908,10 @@ __metadata:
|
|||||||
postcss: ^8.4.33
|
postcss: ^8.4.33
|
||||||
prettier: ^3.1.1
|
prettier: ^3.1.1
|
||||||
qs: ^6.12.0
|
qs: ^6.12.0
|
||||||
react: 18.2.0
|
react: ^18.2.0
|
||||||
react-country-flag: ^3.1.0
|
react-country-flag: ^3.1.0
|
||||||
react-currency-input-field: ^3.6.11
|
react-currency-input-field: ^3.6.11
|
||||||
react-dom: 18.2.0
|
react-dom: ^18.2.0
|
||||||
react-hook-form: 7.49.1
|
react-hook-form: 7.49.1
|
||||||
react-i18next: 13.5.0
|
react-i18next: 13.5.0
|
||||||
react-jwt: ^1.2.0
|
react-jwt: ^1.2.0
|
||||||
@@ -14791,15 +14842,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-dom@npm:18.2.17":
|
|
||||||
version: 18.2.17
|
|
||||||
resolution: "@types/react-dom@npm:18.2.17"
|
|
||||||
dependencies:
|
|
||||||
"@types/react": "*"
|
|
||||||
checksum: 33b53078ed7e9e0cfc4dc691e938f7db1cc06353bc345947b41b581c3efe2b980c9e4eb6460dbf5ddc521dd91959194c970221a2bd4bfad9d23ebce338e12938
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/react-dom@npm:^17.0.18":
|
"@types/react-dom@npm:^17.0.18":
|
||||||
version: 17.0.20
|
version: 17.0.20
|
||||||
resolution: "@types/react-dom@npm:17.0.20"
|
resolution: "@types/react-dom@npm:17.0.20"
|
||||||
@@ -14827,6 +14869,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/react-dom@npm:^18.2.25":
|
||||||
|
version: 18.3.0
|
||||||
|
resolution: "@types/react-dom@npm:18.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
checksum: 6c90d2ed72c5a0e440d2c75d99287e4b5df3e7b011838cdc03ae5cd518ab52164d86990e73246b9d812eaf02ec351d74e3b4f5bd325bf341e13bf980392fd53b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/react@npm:*":
|
"@types/react@npm:*":
|
||||||
version: 18.2.14
|
version: 18.2.14
|
||||||
resolution: "@types/react@npm:18.2.14"
|
resolution: "@types/react@npm:18.2.14"
|
||||||
@@ -14838,17 +14889,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react@npm:18.2.43":
|
|
||||||
version: 18.2.43
|
|
||||||
resolution: "@types/react@npm:18.2.43"
|
|
||||||
dependencies:
|
|
||||||
"@types/prop-types": "*"
|
|
||||||
"@types/scheduler": "*"
|
|
||||||
csstype: ^3.0.2
|
|
||||||
checksum: 10477a50fbd3c0cc5b8a2ade679f442717f68fb27c8460b2aa1d3256cd18c48f742bbe5b9ee37a8c4c5f832ffa37b3a23c09fd96dd880a8e3182d8929c05e803
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/react@npm:>=16, @types/react@npm:^18.2.0, @types/react@npm:^18.2.14":
|
"@types/react@npm:>=16, @types/react@npm:^18.2.0, @types/react@npm:^18.2.14":
|
||||||
version: 18.2.31
|
version: 18.2.31
|
||||||
resolution: "@types/react@npm:18.2.31"
|
resolution: "@types/react@npm:18.2.31"
|
||||||
@@ -14871,6 +14911,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/react@npm:^18.2.79":
|
||||||
|
version: 18.2.79
|
||||||
|
resolution: "@types/react@npm:18.2.79"
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types": "*"
|
||||||
|
csstype: ^3.0.2
|
||||||
|
checksum: c8a8a005d8830a48cc1ef93c3510c4935a2a03e5557dbecaa8f1038450cbfcb18eb206fa7fba7077d54b8da21faeb25577e897a333392770a7797f625b62c78a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/resolve@npm:1.17.1":
|
"@types/resolve@npm:1.17.1":
|
||||||
version: 1.17.1
|
version: 1.17.1
|
||||||
resolution: "@types/resolve@npm:1.17.1"
|
resolution: "@types/resolve@npm:1.17.1"
|
||||||
@@ -35579,18 +35629,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-dom@npm:18.2.0, react-dom@npm:^18.2.0":
|
|
||||||
version: 18.2.0
|
|
||||||
resolution: "react-dom@npm:18.2.0"
|
|
||||||
dependencies:
|
|
||||||
loose-envify: ^1.1.0
|
|
||||||
scheduler: ^0.23.0
|
|
||||||
peerDependencies:
|
|
||||||
react: ^18.2.0
|
|
||||||
checksum: 66dfc5f93e13d0674e78ef41f92ed21dfb80f9c4ac4ac25a4b51046d41d4d2186abc915b897f69d3d0ebbffe6184e7c5876f2af26bfa956f179225d921be713a
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-dom@npm:^17.0.1, react-dom@npm:^17.0.2":
|
"react-dom@npm:^17.0.1, react-dom@npm:^17.0.2":
|
||||||
version: 17.0.2
|
version: 17.0.2
|
||||||
resolution: "react-dom@npm:17.0.2"
|
resolution: "react-dom@npm:17.0.2"
|
||||||
@@ -35604,6 +35642,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-dom@npm:^18.2.0":
|
||||||
|
version: 18.2.0
|
||||||
|
resolution: "react-dom@npm:18.2.0"
|
||||||
|
dependencies:
|
||||||
|
loose-envify: ^1.1.0
|
||||||
|
scheduler: ^0.23.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.2.0
|
||||||
|
checksum: 66dfc5f93e13d0674e78ef41f92ed21dfb80f9c4ac4ac25a4b51046d41d4d2186abc915b897f69d3d0ebbffe6184e7c5876f2af26bfa956f179225d921be713a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-element-to-jsx-string@npm:^14.3.4":
|
"react-element-to-jsx-string@npm:^14.3.4":
|
||||||
version: 14.3.4
|
version: 14.3.4
|
||||||
resolution: "react-element-to-jsx-string@npm:14.3.4"
|
resolution: "react-element-to-jsx-string@npm:14.3.4"
|
||||||
@@ -35900,15 +35950,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react@npm:18.2.0, react@npm:^18.2.0":
|
|
||||||
version: 18.2.0
|
|
||||||
resolution: "react@npm:18.2.0"
|
|
||||||
dependencies:
|
|
||||||
loose-envify: ^1.1.0
|
|
||||||
checksum: b562d9b569b0cb315e44b48099f7712283d93df36b19a39a67c254c6686479d3980b7f013dc931f4a5a3ae7645eae6386b4aa5eea933baa54ecd0f9acb0902b8
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react@npm:^17.0.1, react@npm:^17.0.2":
|
"react@npm:^17.0.1, react@npm:^17.0.2":
|
||||||
version: 17.0.2
|
version: 17.0.2
|
||||||
resolution: "react@npm:17.0.2"
|
resolution: "react@npm:17.0.2"
|
||||||
@@ -35919,6 +35960,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react@npm:^18.2.0":
|
||||||
|
version: 18.2.0
|
||||||
|
resolution: "react@npm:18.2.0"
|
||||||
|
dependencies:
|
||||||
|
loose-envify: ^1.1.0
|
||||||
|
checksum: b562d9b569b0cb315e44b48099f7712283d93df36b19a39a67c254c6686479d3980b7f013dc931f4a5a3ae7645eae6386b4aa5eea933baa54ecd0f9acb0902b8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"read-cache@npm:^1.0.0":
|
"read-cache@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "read-cache@npm:1.0.0"
|
resolution: "read-cache@npm:1.0.0"
|
||||||
|
|||||||
Reference in new issue
Block a user