feat(dashboard): rework create variant flow (#9132)
**What** - new Create variant flow which allows for pricing and inventory creation as well --- https://github.com/user-attachments/assets/75ddcf5a-0f73-40ca-b474-2189c5e2e297 --- CLOSES CC-345
This commit is contained in:
@@ -536,8 +536,9 @@
|
||||
"header": "Edit Variant"
|
||||
},
|
||||
"create": {
|
||||
"header": "Create Variant"
|
||||
"header": "Variant details"
|
||||
},
|
||||
"deleteWarning": "Are you sure you want to delete this variant?",
|
||||
"pricesPagination": "1 - {{current}} of {{total}} prices",
|
||||
"tableItemAvailable": "{{availableCount}} available",
|
||||
"tableItem_one": "{{availableCount}} available at {{locationCount}} location",
|
||||
@@ -551,6 +552,8 @@
|
||||
"inventoryItems": "Go to inventory item",
|
||||
"inventoryKit": "Show inventory items"
|
||||
},
|
||||
"inventoryKit": "Inventory Kit",
|
||||
"inventoryKitHint": "Does this variant consist of several inventory items?",
|
||||
"validation": {
|
||||
"itemId": "Please select inventory item.",
|
||||
"quantity": "Quantity is required. Please input a positive number."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import i18next from "i18next"
|
||||
import { z } from "zod"
|
||||
import { castNumber } from "./cast-number"
|
||||
import { FieldPath, FieldValues, UseFormReturn } from "react-hook-form"
|
||||
|
||||
/**
|
||||
* Validates that an optional value is an integer.
|
||||
@@ -45,3 +46,37 @@ export const metadataFormSchema = z.array(
|
||||
isIgnored: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate subset of form fields
|
||||
* @param form
|
||||
* @param fields
|
||||
* @param schema
|
||||
*/
|
||||
export function partialFormValidation<TForm extends FieldValues>(
|
||||
form: UseFormReturn<TForm>,
|
||||
fields: FieldPath<any>[],
|
||||
schema: z.ZodSchema<any>
|
||||
) {
|
||||
form.clearErrors(fields as any)
|
||||
|
||||
const values = fields.reduce((acc, key) => {
|
||||
acc[key] = form.getValues(key as any)
|
||||
return acc
|
||||
}, {} as Record<string, unknown>)
|
||||
|
||||
const validationResult = schema.safeParse(values)
|
||||
|
||||
if (!validationResult.success) {
|
||||
validationResult.error.errors.forEach(({ path, message, code }) => {
|
||||
form.setError(path.join(".") as any, {
|
||||
type: code,
|
||||
message,
|
||||
})
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { z } from "zod"
|
||||
import * as zod from "zod"
|
||||
import { optionalInt } from "../../../../../lib/validation"
|
||||
|
||||
export const CreateProductVariantSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
sku: z.string().optional(),
|
||||
manage_inventory: z.boolean().optional(),
|
||||
allow_backorder: z.boolean().optional(),
|
||||
inventory_kit: z.boolean().optional(),
|
||||
options: z.record(z.string()),
|
||||
prices: zod
|
||||
.record(zod.string(), zod.string().or(zod.number()).optional())
|
||||
.optional(),
|
||||
inventory: z
|
||||
.array(
|
||||
z.object({
|
||||
inventory_item_id: z.string(),
|
||||
required_quantity: optionalInt,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const CreateVariantDetailsSchema = CreateProductVariantSchema.pick({
|
||||
title: true,
|
||||
sku: true,
|
||||
manage_inventory: true,
|
||||
allow_backorder: true,
|
||||
inventory_kit: true,
|
||||
options: true,
|
||||
})
|
||||
|
||||
export const CreateVariantDetailsFields = Object.keys(
|
||||
CreateVariantDetailsSchema.shape
|
||||
) as (keyof typeof CreateVariantDetailsSchema.shape)[]
|
||||
|
||||
export const CreateVariantPriceSchema = CreateProductVariantSchema.pick({
|
||||
prices: true,
|
||||
})
|
||||
|
||||
export const CreateVariantPriceFields = Object.keys(
|
||||
CreateVariantPriceSchema.shape
|
||||
) as (keyof typeof CreateVariantPriceSchema.shape)[]
|
||||
+330
-412
@@ -1,116 +1,246 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Heading, Input, Switch } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Button, ProgressStatus, ProgressTabs } from "@medusajs/ui"
|
||||
import { useFieldArray, useForm, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { z } from "zod"
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Fragment } from "react"
|
||||
import { Divider } from "../../../../../components/common/divider"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||
import { AdminCreateProductVariantPrice, HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
RouteDrawer,
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
import { useCreateProductVariant } from "../../../../../hooks/api/products"
|
||||
import {
|
||||
CreateProductVariantSchema,
|
||||
CreateVariantDetailsFields,
|
||||
CreateVariantDetailsSchema,
|
||||
CreateVariantPriceFields,
|
||||
CreateVariantPriceSchema,
|
||||
} from "./constants"
|
||||
import DetailsTab from "./details-tab"
|
||||
import PricingTab from "./pricing-tab"
|
||||
import InventoryKitTab from "./inventory-kit-tab"
|
||||
import { castNumber } from "../../../../../lib/cast-number"
|
||||
import { optionalInt } from "../../../../../lib/validation"
|
||||
import { useRegions } from "../../../../../hooks/api"
|
||||
import { partialFormValidation } from "../../../../../lib/validation"
|
||||
|
||||
enum Tab {
|
||||
DETAIL = "detail",
|
||||
PRICE = "price",
|
||||
INVENTORY = "inventory",
|
||||
}
|
||||
|
||||
type TabState = Record<Tab, ProgressStatus>
|
||||
|
||||
const initialTabState: TabState = {
|
||||
[Tab.DETAIL]: "in-progress",
|
||||
[Tab.PRICE]: "not-started",
|
||||
[Tab.INVENTORY]: "not-started",
|
||||
}
|
||||
|
||||
type CreateProductVariantFormProps = {
|
||||
product: HttpTypes.AdminProduct
|
||||
isStockAndInventoryEnabled?: boolean
|
||||
}
|
||||
|
||||
const CreateProductVariantSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
material: z.string().optional(),
|
||||
sku: z.string().optional(),
|
||||
ean: z.string().optional(),
|
||||
upc: z.string().optional(),
|
||||
barcode: z.string().optional(),
|
||||
manage_inventory: z.boolean(),
|
||||
allow_backorder: z.boolean(),
|
||||
weight: optionalInt,
|
||||
height: optionalInt,
|
||||
width: optionalInt,
|
||||
length: optionalInt,
|
||||
mid_code: z.string().optional(),
|
||||
hs_code: z.string().optional(),
|
||||
origin_country: z.string().optional(),
|
||||
options: z.record(z.string()),
|
||||
})
|
||||
|
||||
export const CreateProductVariantForm = ({
|
||||
product,
|
||||
isStockAndInventoryEnabled = false,
|
||||
}: CreateProductVariantFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const [tab, setTab] = useState<Tab>(Tab.DETAIL)
|
||||
const [tabState, setTabState] = useState<TabState>(initialTabState)
|
||||
|
||||
const form = useForm<z.infer<typeof CreateProductVariantSchema>>({
|
||||
defaultValues: {
|
||||
manage_inventory: true,
|
||||
sku: "",
|
||||
title: "",
|
||||
manage_inventory: false,
|
||||
allow_backorder: false,
|
||||
inventory_kit: false,
|
||||
options: {},
|
||||
},
|
||||
resolver: zodResolver(CreateProductVariantSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending: isLoading } = useCreateProductVariant(
|
||||
product.id
|
||||
)
|
||||
const { mutateAsync, isPending } = useCreateProductVariant(product.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const parseNumber = (value?: string | number) => {
|
||||
if (typeof value === "undefined" || value === "") {
|
||||
return undefined
|
||||
}
|
||||
const { regions } = useRegions({ limit: 9999 })
|
||||
|
||||
if (typeof value === "string") {
|
||||
return castNumber(value)
|
||||
}
|
||||
|
||||
return value
|
||||
const regionsCurrencyMap = useMemo(() => {
|
||||
if (!regions?.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const {
|
||||
weight,
|
||||
height,
|
||||
width,
|
||||
length,
|
||||
allow_backorder,
|
||||
manage_inventory,
|
||||
sku,
|
||||
ean,
|
||||
upc,
|
||||
barcode,
|
||||
...rest
|
||||
} = data
|
||||
return regions.reduce((acc, reg) => {
|
||||
acc[reg.id] = reg.currency_code
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
}, [regions])
|
||||
|
||||
/**
|
||||
* If stock and inventory is not enabled, we need to send the inventory and
|
||||
* stock related fields to the API. If it is enabled, it should be handled
|
||||
* in the separate stock and inventory form.
|
||||
*/
|
||||
const conditionalPayload = !isStockAndInventoryEnabled
|
||||
? {
|
||||
sku,
|
||||
ean,
|
||||
upc,
|
||||
barcode,
|
||||
allow_backorder,
|
||||
manage_inventory,
|
||||
const isManageInventoryEnabled = useWatch({
|
||||
control: form.control,
|
||||
name: "manage_inventory",
|
||||
})
|
||||
|
||||
const isInventoryKitEnabled = useWatch({
|
||||
control: form.control,
|
||||
name: "inventory_kit",
|
||||
})
|
||||
|
||||
const inventoryField = useFieldArray({
|
||||
control: form.control,
|
||||
name: `inventory`,
|
||||
})
|
||||
|
||||
const inventoryTabEnabled = isManageInventoryEnabled && isInventoryKitEnabled
|
||||
|
||||
const tabOrder = useMemo(() => {
|
||||
if (inventoryTabEnabled) {
|
||||
return [Tab.DETAIL, Tab.PRICE, Tab.INVENTORY] as const
|
||||
}
|
||||
|
||||
return [Tab.DETAIL, Tab.PRICE] as const
|
||||
}, [inventoryTabEnabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (isInventoryKitEnabled && inventoryField.fields.length === 0) {
|
||||
inventoryField.append({
|
||||
inventory_item_id: "",
|
||||
required_quantity: undefined,
|
||||
})
|
||||
}
|
||||
}, [isInventoryKitEnabled])
|
||||
|
||||
const handleChangeTab = (update: Tab) => {
|
||||
if (tab === update) {
|
||||
return
|
||||
}
|
||||
|
||||
if (tabOrder.indexOf(update) < tabOrder.indexOf(tab)) {
|
||||
const isCurrentTabDirty = false // isTabDirty(tab) TODO
|
||||
|
||||
setTabState((prev) => ({
|
||||
...prev,
|
||||
[tab]: isCurrentTabDirty ? prev[tab] : "not-started",
|
||||
[update]: "in-progress",
|
||||
}))
|
||||
|
||||
setTab(update)
|
||||
return
|
||||
}
|
||||
|
||||
// get the tabs from the current tab to the update tab including the current tab
|
||||
const tabs = tabOrder.slice(0, tabOrder.indexOf(update))
|
||||
|
||||
// validate all the tabs from the current tab to the update tab if it fails on any of tabs then set that tab as current tab
|
||||
for (const tab of tabs) {
|
||||
if (tab === Tab.DETAIL) {
|
||||
if (
|
||||
!partialFormValidation<z.infer<typeof CreateProductVariantSchema>>(
|
||||
form,
|
||||
CreateVariantDetailsFields,
|
||||
CreateVariantDetailsSchema
|
||||
)
|
||||
) {
|
||||
setTabState((prev) => ({
|
||||
...prev,
|
||||
[tab]: "in-progress",
|
||||
}))
|
||||
setTab(tab)
|
||||
return
|
||||
}
|
||||
: {}
|
||||
|
||||
setTabState((prev) => ({
|
||||
...prev,
|
||||
[tab]: "completed",
|
||||
}))
|
||||
} else if (tab === Tab.PRICE) {
|
||||
if (
|
||||
!partialFormValidation<z.infer<typeof CreateProductVariantSchema>>(
|
||||
form,
|
||||
CreateVariantPriceFields,
|
||||
CreateVariantPriceSchema
|
||||
)
|
||||
) {
|
||||
setTabState((prev) => ({
|
||||
...prev,
|
||||
[tab]: "in-progress",
|
||||
}))
|
||||
setTab(tab)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setTabState((prev) => ({
|
||||
...prev,
|
||||
[tab]: "completed",
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
setTabState((prev) => ({
|
||||
...prev,
|
||||
[tab]: "completed",
|
||||
[update]: "in-progress",
|
||||
}))
|
||||
setTab(update)
|
||||
}
|
||||
|
||||
const handleNextTab = (tab: Tab) => {
|
||||
if (tabOrder.indexOf(tab) + 1 >= tabOrder.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextTab = tabOrder[tabOrder.indexOf(tab) + 1]
|
||||
handleChangeTab(nextTab)
|
||||
}
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const { allow_backorder, manage_inventory, sku, title } = data
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
weight: parseNumber(weight),
|
||||
height: parseNumber(height),
|
||||
width: parseNumber(width),
|
||||
length: parseNumber(length),
|
||||
prices: [],
|
||||
...conditionalPayload,
|
||||
...rest,
|
||||
title,
|
||||
sku,
|
||||
allow_backorder,
|
||||
manage_inventory,
|
||||
options: data.options,
|
||||
prices: Object.entries(data.prices ?? {})
|
||||
.map(([currencyOrRegion, value]) => {
|
||||
const ret: AdminCreateProductVariantPrice = {}
|
||||
const amount = castNumber(value)
|
||||
|
||||
if (isNaN(amount) || value === "") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (currencyOrRegion.startsWith("reg_")) {
|
||||
ret.rules = { region_id: currencyOrRegion }
|
||||
ret.currency_code = regionsCurrencyMap[currencyOrRegion]
|
||||
} else {
|
||||
ret.currency_code = currencyOrRegion
|
||||
}
|
||||
|
||||
ret.amount = amount
|
||||
|
||||
return ret
|
||||
})
|
||||
.filter(Boolean),
|
||||
inventory_items: (data.inventory || [])
|
||||
.map((i) => {
|
||||
if (!i.required_quantity || !i.inventory_item_id) {
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
...i,
|
||||
required_quantity: castNumber(i.required_quantity),
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -121,340 +251,128 @@ export const CreateProductVariantForm = ({
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex size-full flex-col overflow-hidden"
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<ProgressTabs
|
||||
value={tab}
|
||||
onValueChange={(tab) => handleChangeTab(tab as Tab)}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteDrawer.Body className="flex size-full flex-col gap-y-8 overflow-auto">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{product.options.map((option: any) => {
|
||||
return (
|
||||
<Form.Field
|
||||
key={option.id}
|
||||
control={form.control}
|
||||
name={`options.${option.title}`}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{option.title}</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v)
|
||||
}}
|
||||
{...field}
|
||||
options={option.values.map((v: any) => ({
|
||||
label: v.value,
|
||||
value: v.value,
|
||||
}))}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<Divider />
|
||||
{!isStockAndInventoryEnabled && (
|
||||
<Fragment>
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Heading level="h2">
|
||||
{t("products.variant.inventory.header")}
|
||||
</Heading>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="sku"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.sku")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="ean"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.ean")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="upc"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.upc")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="barcode"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("fields.barcode")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="manage_inventory"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>
|
||||
{t(
|
||||
"products.variant.inventory.manageInventoryLabel"
|
||||
)}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange(!!checked)
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
{t(
|
||||
"products.variant.inventory.manageInventoryHint"
|
||||
)}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="allow_backorder"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>
|
||||
{t(
|
||||
"products.variant.inventory.allowBackordersLabel"
|
||||
)}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange(!!checked)
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
{t(
|
||||
"products.variant.inventory.allowBackordersHint"
|
||||
)}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex w-full items-center justify-between gap-x-4">
|
||||
<div className="-my-2 w-full max-w-[600px] border-l">
|
||||
<ProgressTabs.List className="grid w-full grid-cols-3">
|
||||
<ProgressTabs.Trigger
|
||||
status={tabState.detail}
|
||||
value={Tab.DETAIL}
|
||||
>
|
||||
{t("priceLists.create.tabs.details")}
|
||||
</ProgressTabs.Trigger>
|
||||
<ProgressTabs.Trigger
|
||||
status={tabState.price}
|
||||
value={Tab.PRICE}
|
||||
>
|
||||
{t("priceLists.create.tabs.prices")}
|
||||
</ProgressTabs.Trigger>
|
||||
{!!inventoryTabEnabled && (
|
||||
<ProgressTabs.Trigger
|
||||
status={tabState.inventory}
|
||||
value={Tab.INVENTORY}
|
||||
>
|
||||
{t("products.create.tabs.inventory")}
|
||||
</ProgressTabs.Trigger>
|
||||
)}
|
||||
</ProgressTabs.List>
|
||||
</div>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
)}
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Heading level="h2">{t("products.attributes")}</Heading>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="material"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.material")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="weight"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.weight")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="width"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.width")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="length"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.length")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="height"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.height")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="mid_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.midCode")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="hs_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.hsCode")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="origin_country"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("fields.countryOfOrigin")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<CountrySelect {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||
<ProgressTabs.Content
|
||||
className="size-full overflow-y-auto"
|
||||
value={Tab.DETAIL}
|
||||
>
|
||||
<DetailsTab form={form} product={product} />
|
||||
</ProgressTabs.Content>
|
||||
<ProgressTabs.Content
|
||||
className="size-full overflow-y-auto"
|
||||
value={Tab.PRICE}
|
||||
>
|
||||
<PricingTab form={form} />
|
||||
</ProgressTabs.Content>
|
||||
{!!inventoryTabEnabled && (
|
||||
<ProgressTabs.Content
|
||||
className="size-full overflow-hidden"
|
||||
value={Tab.INVENTORY}
|
||||
>
|
||||
<InventoryKitTab form={form} />
|
||||
</ProgressTabs.Content>
|
||||
)}
|
||||
</RouteFocusModal.Body>
|
||||
<RouteFocusModal.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<PrimaryButton
|
||||
tab={tab}
|
||||
next={handleNextTab}
|
||||
isLoading={isPending}
|
||||
inventoryTabEnabled={!!inventoryTabEnabled}
|
||||
/>
|
||||
</div>
|
||||
</RouteFocusModal.Footer>
|
||||
</form>
|
||||
</ProgressTabs>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
type PrimaryButtonProps = {
|
||||
tab: Tab
|
||||
next: (tab: Tab) => void
|
||||
isLoading?: boolean
|
||||
inventoryTabEnabled: boolean
|
||||
}
|
||||
|
||||
const PrimaryButton = ({
|
||||
tab,
|
||||
next,
|
||||
isLoading,
|
||||
inventoryTabEnabled,
|
||||
}: PrimaryButtonProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (
|
||||
(inventoryTabEnabled && tab === Tab.INVENTORY) ||
|
||||
(!inventoryTabEnabled && tab === Tab.PRICE)
|
||||
) {
|
||||
return (
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key="next-button"
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={() => next(tab)}
|
||||
>
|
||||
{t("actions.continue")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import React from "react"
|
||||
import { Heading, Input, Switch } from "@medusajs/ui"
|
||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { CreateProductVariantSchema } from "./constants"
|
||||
|
||||
type DetailsTabProps = {
|
||||
product: HttpTypes.AdminProduct
|
||||
form: UseFormReturn<z.infer<typeof CreateProductVariantSchema>>
|
||||
}
|
||||
|
||||
function DetailsTab({ form, product }: DetailsTabProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const manageInventoryEnabled = useWatch({
|
||||
control: form.control,
|
||||
name: "manage_inventory",
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-8 py-16">
|
||||
<Heading level="h1">{t("products.variant.create.header")}</Heading>
|
||||
|
||||
<div className="my-8 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="sku"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.sku")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{product.options.map((option: any) => (
|
||||
<Form.Field
|
||||
key={option.id}
|
||||
control={form.control}
|
||||
name={`options.${option.title}`}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{option.title}</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v)
|
||||
}}
|
||||
{...field}
|
||||
options={option.values.map((v: any) => ({
|
||||
label: v.value,
|
||||
value: v.value,
|
||||
}))}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="manage_inventory"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="bg-ui-bg-component shadow-elevation-card-rest flex gap-x-3 rounded-lg p-4">
|
||||
<Form.Control>
|
||||
<Switch
|
||||
className="mt-[2px]"
|
||||
checked={value}
|
||||
onCheckedChange={(checked) => onChange(!!checked)}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Form.Label>
|
||||
{t("products.variant.inventory.manageInventoryLabel")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("products.variant.inventory.manageInventoryHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="allow_backorder"
|
||||
disabled={!manageInventoryEnabled}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="bg-ui-bg-component shadow-elevation-card-rest flex gap-x-3 rounded-lg p-4">
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(checked) => onChange(!!checked)}
|
||||
{...field}
|
||||
disabled={!manageInventoryEnabled}
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="flex flex-col">
|
||||
<Form.Label>
|
||||
{t("products.variant.inventory.allowBackordersLabel")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("products.variant.inventory.allowBackordersHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="inventory_kit"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="bg-ui-bg-component shadow-elevation-card-rest flex gap-x-3 rounded-lg p-4">
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(checked) => onChange(!!checked)}
|
||||
{...field}
|
||||
disabled={!manageInventoryEnabled}
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="flex flex-col">
|
||||
<Form.Label>
|
||||
{t("products.variant.inventory.inventoryKit")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("products.variant.inventory.inventoryKitHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DetailsTab
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import React from "react"
|
||||
import { z } from "zod"
|
||||
import { useFieldArray, UseFormReturn } from "react-hook-form"
|
||||
import { Button, Heading, IconButton, Input, Label } from "@medusajs/ui"
|
||||
|
||||
import { CreateProductVariantSchema } from "./constants"
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
|
||||
type InventoryKitTabProps = {
|
||||
form: UseFormReturn<z.infer<typeof CreateProductVariantSchema>>
|
||||
}
|
||||
|
||||
function InventoryKitTab({ form }: InventoryKitTabProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const inventory = useFieldArray({
|
||||
control: form.control,
|
||||
name: `inventory`,
|
||||
})
|
||||
|
||||
const inventoryFormData = inventory.fields
|
||||
|
||||
const items = useComboboxData({
|
||||
queryKey: ["inventory_items"],
|
||||
queryFn: (params) => sdk.admin.inventoryItem.list(params),
|
||||
getOptions: (data) =>
|
||||
data.inventory_items.map((item) => ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
})),
|
||||
})
|
||||
|
||||
/**
|
||||
* Will mark an option as disabled if another input already selected that option
|
||||
* @param option
|
||||
* @param inventoryIndex
|
||||
*/
|
||||
const isItemOptionDisabled = (
|
||||
option: (typeof items.options)[0],
|
||||
inventoryIndex: number
|
||||
) => {
|
||||
return inventoryFormData?.some(
|
||||
(i, index) =>
|
||||
index != inventoryIndex && i.inventory_item_id === option.value
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div id="organize" className="flex flex-col gap-y-8">
|
||||
<Heading>{t("products.create.inventory.heading")}</Heading>
|
||||
|
||||
<div className="grid gap-y-4">
|
||||
<div className="flex items-start justify-between gap-x-4">
|
||||
<div className="flex flex-col">
|
||||
<Form.Label>{form.getValues("title")}</Form.Label>
|
||||
<Form.Hint>{t("products.create.inventory.label")}</Form.Hint>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
inventory.append({
|
||||
inventory_item_id: "",
|
||||
required_quantity: "",
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t("actions.add")}
|
||||
</Button>
|
||||
</div>
|
||||
{inventory.fields.map((inventoryItem, inventoryIndex) => (
|
||||
<li
|
||||
key={inventoryItem.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={`inventory.${inventoryIndex}.inventory_item_id`}
|
||||
>
|
||||
{t("fields.item")}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`inventory.${inventoryIndex}.inventory_item_id`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
options={items.options.map((o) => ({
|
||||
...o,
|
||||
disabled: isItemOptionDisabled(
|
||||
o,
|
||||
inventoryIndex
|
||||
),
|
||||
}))}
|
||||
searchValue={items.searchValue}
|
||||
onSearchValueChange={items.onSearchValueChange}
|
||||
fetchNextPage={items.fetchNextPage}
|
||||
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
|
||||
placeholder={t(
|
||||
"products.create.inventory.itemPlaceholder"
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center px-2 py-1.5">
|
||||
<Label
|
||||
size="xsmall"
|
||||
weight="plus"
|
||||
className="text-ui-fg-subtle"
|
||||
htmlFor={`inventory.${inventoryIndex}.required_quantity`}
|
||||
>
|
||||
{t("fields.quantity")}
|
||||
</Label>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`inventory.${inventoryIndex}.required_quantity`}
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-ui-bg-field-component"
|
||||
min={0}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
|
||||
if (value === "") {
|
||||
onChange(null)
|
||||
} else {
|
||||
onChange(Number(value))
|
||||
}
|
||||
}}
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"products.create.inventory.quantityPlaceholder"
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
size="small"
|
||||
variant="transparent"
|
||||
className="text-ui-fg-muted"
|
||||
onClick={() => inventory.remove(inventoryIndex)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</IconButton>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InventoryKitTab
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
import { CreateProductVariantSchema } from "./constants"
|
||||
import { useRegions, useStore } from "../../../../../hooks/api"
|
||||
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
||||
import { useRouteModal } from "../../../../../components/modals"
|
||||
import {
|
||||
createDataGridHelper,
|
||||
createDataGridPriceColumns,
|
||||
DataGrid,
|
||||
} from "../../../../../components/data-grid"
|
||||
|
||||
type PricingTabProps = {
|
||||
form: UseFormReturn<z.infer<typeof CreateProductVariantSchema>>
|
||||
}
|
||||
|
||||
function PricingTab({ form }: PricingTabProps) {
|
||||
const { store } = useStore()
|
||||
const { regions } = useRegions({ limit: 9999 })
|
||||
const { price_preferences: pricePreferences } = usePricePreferences({})
|
||||
|
||||
const { setCloseOnEscape } = useRouteModal()
|
||||
|
||||
const columns = useVariantPriceGridColumns({
|
||||
currencies: store?.supported_currencies,
|
||||
regions,
|
||||
pricePreferences,
|
||||
})
|
||||
|
||||
const variant = useWatch({
|
||||
control: form.control,
|
||||
}) as any
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={[variant]}
|
||||
state={form}
|
||||
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createDataGridHelper<
|
||||
HttpTypes.AdminProductVariant,
|
||||
z.infer<typeof CreateProductVariantSchema>
|
||||
>()
|
||||
|
||||
const useVariantPriceGridColumns = ({
|
||||
currencies = [],
|
||||
regions = [],
|
||||
pricePreferences = [],
|
||||
}: {
|
||||
currencies?: HttpTypes.AdminStore["supported_currencies"]
|
||||
regions?: HttpTypes.AdminRegion[]
|
||||
pricePreferences?: HttpTypes.AdminPricePreference[]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
columnHelper.column({
|
||||
id: t("fields.title"),
|
||||
header: t("fields.title"),
|
||||
cell: (context) => {
|
||||
const entity = context.row.original
|
||||
return (
|
||||
<DataGrid.ReadonlyCell context={context}>
|
||||
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||
<span className="truncate">{entity.title}</span>
|
||||
</div>
|
||||
</DataGrid.ReadonlyCell>
|
||||
)
|
||||
},
|
||||
disableHiding: true,
|
||||
}),
|
||||
...createDataGridPriceColumns<
|
||||
HttpTypes.AdminProductVariant,
|
||||
z.infer<typeof CreateProductVariantSchema>
|
||||
>({
|
||||
currencies: currencies.map((c) => c.currency_code),
|
||||
regions,
|
||||
pricePreferences,
|
||||
getFieldName: (context, value) => {
|
||||
if (context.column.id?.startsWith("currency_prices")) {
|
||||
return `prices.${value}`
|
||||
}
|
||||
return `prices.${value}`
|
||||
},
|
||||
t,
|
||||
}),
|
||||
]
|
||||
}, [t, currencies, regions, pricePreferences])
|
||||
}
|
||||
|
||||
export default PricingTab
|
||||
+3
-9
@@ -1,13 +1,10 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { CreateProductVariantForm } from "./components/create-product-variant-form"
|
||||
|
||||
export const ProductCreateVariant = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { product, isLoading, isError, error } = useProduct(id!)
|
||||
|
||||
@@ -16,11 +13,8 @@ export const ProductCreateVariant = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("products.variant.create.header")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
<RouteFocusModal>
|
||||
{!isLoading && product && <CreateProductVariantForm product={product} />}
|
||||
</RouteDrawer>
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,11 @@ export interface AdminCreateProductVariantPrice {
|
||||
rules?: { region_id: string } | null
|
||||
}
|
||||
|
||||
export interface AdminCreateProductVariantInventoryKit {
|
||||
inventory_item_id: string
|
||||
required_quantity?: number
|
||||
}
|
||||
|
||||
export interface AdminCreateProductVariant {
|
||||
title: string
|
||||
sku?: string
|
||||
@@ -49,6 +54,7 @@ export interface AdminCreateProductVariant {
|
||||
material?: string
|
||||
metadata?: Record<string, unknown>
|
||||
prices: AdminCreateProductVariantPrice[]
|
||||
inventory_items?: AdminCreateProductVariantInventoryKit[]
|
||||
options?: Record<string, string>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user