feat: Add variant creation and editing in the products UI (#6997)
This commit is contained in:
+424
-49
@@ -1,93 +1,468 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||||
import { Button, Input } 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 { Combobox } from "../../../../../components/common/combobox"
|
||||||
|
import { CountrySelect } from "../../../../../components/common/country-select"
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
|
import { optionalInt } from "../../../../../lib/validation"
|
||||||
import { useCreateProductVariant } from "../../../../../hooks/api/products"
|
import { useCreateProductVariant } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type EditProductVariantsFormProps = {
|
type CreateProductVariantFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
|
isStockAndInventoryEnabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateProductVariantSchema = z.object({
|
const CreateProductVariantSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
values: z.array(z.string()).optional(),
|
material: z.string().optional(),
|
||||||
|
sku: z.string().optional(),
|
||||||
|
ean: z.string().optional(),
|
||||||
|
upc: z.string().optional(),
|
||||||
|
barcode: z.string().optional(),
|
||||||
|
inventory_quantity: optionalInt,
|
||||||
|
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 = ({
|
export const CreateProductVariantForm = ({
|
||||||
product,
|
product,
|
||||||
}: EditProductVariantsFormProps) => {
|
isStockAndInventoryEnabled = false,
|
||||||
|
}: CreateProductVariantFormProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof CreateProductVariantSchema>>({
|
const form = useForm<z.infer<typeof CreateProductVariantSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
inventory_quantity: 0,
|
||||||
values: [],
|
manage_inventory: true,
|
||||||
|
allow_backorder: false,
|
||||||
|
options: {},
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateProductVariantSchema),
|
resolver: zodResolver(CreateProductVariantSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useCreateProductVariant(product.id)
|
const { mutateAsync, isLoading } = useCreateProductVariant(product.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
mutateAsync(values, {
|
const parseNumber = (value?: string | number) => {
|
||||||
onSuccess: () => {
|
if (typeof value === "undefined" || value === "") {
|
||||||
handleSuccess()
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return castNumber(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
weight,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
length,
|
||||||
|
inventory_quantity,
|
||||||
|
allow_backorder,
|
||||||
|
manage_inventory,
|
||||||
|
sku,
|
||||||
|
ean,
|
||||||
|
upc,
|
||||||
|
barcode,
|
||||||
|
...rest
|
||||||
|
} = data
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
inventory_quantity: parseNumber(inventory_quantity),
|
||||||
|
allow_backorder,
|
||||||
|
manage_inventory,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
weight: parseNumber(weight),
|
||||||
|
height: parseNumber(height),
|
||||||
|
width: parseNumber(width),
|
||||||
|
length: parseNumber(length),
|
||||||
|
prices: [],
|
||||||
|
...conditionalPayload,
|
||||||
|
...rest,
|
||||||
},
|
},
|
||||||
})
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer.Form form={form}>
|
<RouteDrawer.Form form={form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="flex flex-1 flex-col overflow-hidden"
|
className="flex size-full flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
|
<RouteDrawer.Body className="flex size-full flex-col gap-y-8 overflow-auto">
|
||||||
<Form.Field
|
<div className="flex flex-col gap-y-4">
|
||||||
control={form.control}
|
<Form.Field
|
||||||
name="title"
|
control={form.control}
|
||||||
render={({ field }) => {
|
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="material"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.material")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{product.options.map((option: any) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Field
|
||||||
<Form.Label>title</Form.Label>
|
key={option.id}
|
||||||
<Form.Control>
|
control={form.control}
|
||||||
<Input {...field} />
|
name={`options.${option.title}`}
|
||||||
</Form.Control>
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
<Form.ErrorMessage />
|
return (
|
||||||
</Form.Item>
|
<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>
|
||||||
<Form.Field
|
<Divider />
|
||||||
control={form.control}
|
{!isStockAndInventoryEnabled && (
|
||||||
name="values"
|
<Fragment>
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
<div className="flex flex-col gap-y-8">
|
||||||
return (
|
<div className="flex flex-col gap-y-4">
|
||||||
<Form.Item>
|
<Heading level="h2">
|
||||||
<Form.Label>value</Form.Label>
|
{t("products.variant.inventory.header")}
|
||||||
<Form.Control>
|
</Heading>
|
||||||
<Input
|
<Form.Field
|
||||||
{...field}
|
control={form.control}
|
||||||
value={(value ?? []).join(",")}
|
name="sku"
|
||||||
onChange={(e) => {
|
render={({ field }) => {
|
||||||
const val = e.target.value
|
return (
|
||||||
onChange(val.split(",").map((v) => v.trim()))
|
<Form.Item>
|
||||||
}}
|
<Form.Label optional>{t("fields.sku")}</Form.Label>
|
||||||
/>
|
<Form.Control>
|
||||||
</Form.Control>
|
<Input {...field} />
|
||||||
<Form.ErrorMessage />
|
</Form.Control>
|
||||||
</Form.Item>
|
<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>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="inventory_quantity"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("fields.inventoryQuantity")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input type="number" {...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>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Heading level="h2">{t("products.attributes")}</Heading>
|
||||||
|
<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.Body>
|
||||||
<RouteDrawer.Footer>
|
<RouteDrawer.Footer>
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
|||||||
+21
-83
@@ -1,11 +1,11 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product, ProductOption, ProductVariant } from "@medusajs/medusa"
|
import { Product, ProductVariant } 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, useState } from "react"
|
import { Fragment } from "react"
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
import { Combobox } from "../../../../../components/common/combobox"
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
import { CountrySelect } from "../../../../../components/common/country-select"
|
||||||
import { Divider } from "../../../../../components/common/divider"
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
@@ -41,24 +41,24 @@ const ProductEditVariantSchema = z.object({
|
|||||||
mid_code: z.string().optional(),
|
mid_code: z.string().optional(),
|
||||||
hs_code: z.string().optional(),
|
hs_code: z.string().optional(),
|
||||||
origin_country: z.string().optional(),
|
origin_country: z.string().optional(),
|
||||||
options: z.record(
|
options: z.record(z.string()),
|
||||||
z.object({
|
|
||||||
value: z.string().min(1),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: Either pass option ID or make the backend handle options constraints differently to handle the lack of IDs
|
||||||
export const ProductEditVariantForm = ({
|
export const ProductEditVariantForm = ({
|
||||||
product,
|
product,
|
||||||
variant,
|
variant,
|
||||||
isStockAndInventoryEnabled = false,
|
isStockAndInventoryEnabled = false,
|
||||||
}: ProductEditVariantFormProps) => {
|
}: ProductEditVariantFormProps) => {
|
||||||
const [optionValues, setOptionValues] = useState<Record<string, string[]>>(
|
|
||||||
initOptionValues(product)
|
|
||||||
)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
const defaultOptions = product.options.reduce((acc: any, option: any) => {
|
||||||
|
const varOpt = variant.options.find(
|
||||||
|
(o: any) => o.option_value.option_id === option.id
|
||||||
|
)
|
||||||
|
acc[option.title] = varOpt?.option_value?.value
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof ProductEditVariantSchema>>({
|
const form = useForm<z.infer<typeof ProductEditVariantSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -78,7 +78,7 @@ export const ProductEditVariantForm = ({
|
|||||||
mid_code: variant.mid_code || "",
|
mid_code: variant.mid_code || "",
|
||||||
hs_code: variant.hs_code || "",
|
hs_code: variant.hs_code || "",
|
||||||
origin_country: variant.origin_country || "",
|
origin_country: variant.origin_country || "",
|
||||||
options: getDefaultOptionValues(product, variant),
|
options: defaultOptions,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ProductEditVariantSchema),
|
resolver: zodResolver(ProductEditVariantSchema),
|
||||||
})
|
})
|
||||||
@@ -113,7 +113,6 @@ export const ProductEditVariantForm = ({
|
|||||||
ean,
|
ean,
|
||||||
upc,
|
upc,
|
||||||
barcode,
|
barcode,
|
||||||
options,
|
|
||||||
...rest
|
...rest
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
@@ -134,21 +133,13 @@ export const ProductEditVariantForm = ({
|
|||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const optionsPayload = Object.entries(options).map(([key, value]) => {
|
|
||||||
return {
|
|
||||||
option_id: key,
|
|
||||||
value: value.value,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
variant_id: variant.id,
|
id: variant.id,
|
||||||
weight: parseNumber(weight),
|
weight: parseNumber(weight),
|
||||||
height: parseNumber(height),
|
height: parseNumber(height),
|
||||||
width: parseNumber(width),
|
width: parseNumber(width),
|
||||||
length: parseNumber(length),
|
length: parseNumber(length),
|
||||||
options: optionsPayload,
|
|
||||||
...conditionalPayload,
|
...conditionalPayload,
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
@@ -160,18 +151,6 @@ export const ProductEditVariantForm = ({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCreateOption = (optionId: string) => {
|
|
||||||
return (value: string) => {
|
|
||||||
setOptionValues((prev) => {
|
|
||||||
const values = prev[optionId] || []
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[optionId]: [...values, value],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer.Form form={form}>
|
<RouteDrawer.Form form={form}>
|
||||||
<form
|
<form
|
||||||
@@ -210,30 +189,27 @@ export const ProductEditVariantForm = ({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{product.options.map((option) => {
|
{product.options.map((option: any) => {
|
||||||
return (
|
return (
|
||||||
<Form.Field
|
<Form.Field
|
||||||
key={option.id}
|
key={option.id}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`options.${option.id}`}
|
name={`options.${option.title}`}
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
const options = optionValues[option.id].map((value) => ({
|
|
||||||
label: value,
|
|
||||||
value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{option.title}</Form.Label>
|
<Form.Label>{option.title}</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Combobox
|
<Combobox
|
||||||
value={value.value}
|
value={value}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
onChange({ value: v })
|
onChange(v)
|
||||||
}}
|
}}
|
||||||
onCreateOption={handleCreateOption(option.id)}
|
|
||||||
{...field}
|
{...field}
|
||||||
options={options}
|
options={option.values.map((v: any) => ({
|
||||||
|
label: v.value,
|
||||||
|
value: v.value,
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -530,41 +506,3 @@ export const ProductEditVariantForm = ({
|
|||||||
</RouteDrawer.Form>
|
</RouteDrawer.Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
|
||||||
const getDefaultOptionValues = (product: Product, variant: ProductVariant) => {
|
|
||||||
const opts = variant.options
|
|
||||||
|
|
||||||
return product.options.reduce(
|
|
||||||
(acc, option) => {
|
|
||||||
const variantOption = opts.find((o) => o.option_id === option.id)
|
|
||||||
|
|
||||||
acc[option.id] = {
|
|
||||||
value: variantOption?.value || "",
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, { value: string }>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOptionValues = (option: ProductOption) => {
|
|
||||||
const values = option.values.map((value) => value.value)
|
|
||||||
|
|
||||||
const filteredValues = values.filter((v, i) => values.indexOf(v) === i)
|
|
||||||
|
|
||||||
return filteredValues.map((value) => value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initOptionValues = (product: Product) => {
|
|
||||||
return product.options.reduce(
|
|
||||||
(acc, option) => {
|
|
||||||
const values = getOptionValues(option)
|
|
||||||
|
|
||||||
acc[option.id] = values
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, string[]>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/* eslint-enable prettier/prettier */
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { editProductVariantLoader as loader } from "./loader"
|
|
||||||
export { ProductEditVariant as Component } from "./product-edit-variant"
|
export { ProductEditVariant as Component } from "./product-edit-variant"
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
|
||||||
|
|
||||||
import { medusa, queryClient } from "../../../lib/medusa"
|
|
||||||
import { productsQueryKeys } from "../../../hooks/api/products"
|
|
||||||
|
|
||||||
const queryKey = (id: string) => {
|
|
||||||
return [productsQueryKeys.detail(id)]
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryFn = async (id: string) => {
|
|
||||||
const productRes = await medusa.admin.products.retrieve(id)
|
|
||||||
|
|
||||||
const storeRes = await medusa.admin.store.retrieve()
|
|
||||||
|
|
||||||
const isStockAndInventoryEnabled = storeRes.store.modules.some(
|
|
||||||
(m) => m.module === "inventoryService" || "stockLocationService"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialData: productRes,
|
|
||||||
isStockAndInventoryEnabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editProductVariantQuery = (id: string) => ({
|
|
||||||
queryKey: queryKey(id),
|
|
||||||
queryFn: async () => queryFn(id),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const editProductVariantLoader = async ({
|
|
||||||
params,
|
|
||||||
}: LoaderFunctionArgs) => {
|
|
||||||
const id = params.id
|
|
||||||
const query = editProductVariantQuery(id!)
|
|
||||||
|
|
||||||
return (
|
|
||||||
queryClient.getQueryData<ReturnType<typeof queryFn>>(query.queryKey) ??
|
|
||||||
(await queryClient.fetchQuery(query))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+49
-1
@@ -26,7 +26,7 @@ moduleIntegrationTestRunner({
|
|||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
title: "size",
|
title: "size",
|
||||||
values: ["large"],
|
values: ["large", "small"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -169,6 +169,54 @@ moduleIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("updateVariants", () => {
|
||||||
|
it("should update the title of the variant successfully", async () => {
|
||||||
|
await service.upsertVariants([
|
||||||
|
{
|
||||||
|
id: variantOne.id,
|
||||||
|
title: "new test",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const productVariant = await service.retrieveVariant(variantOne.id)
|
||||||
|
expect(productVariant.title).toEqual("new test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update the options of a variant successfully", async () => {
|
||||||
|
await service.upsertVariants([
|
||||||
|
{
|
||||||
|
id: variantOne.id,
|
||||||
|
options: { size: "small" },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const productVariant = await service.retrieveVariant(variantOne.id, {
|
||||||
|
relations: ["options", "options.option_value", "options.variant"],
|
||||||
|
})
|
||||||
|
expect(productVariant.options).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
option_value: expect.objectContaining({ value: "small" }),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when an id does not exist", async () => {
|
||||||
|
let error
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.updateVariants("does-not-exist", {})
|
||||||
|
} catch (e) {
|
||||||
|
error = e
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.message).toEqual(
|
||||||
|
`Cannot update non-existing variants with ids: does-not-exist`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("softDelete variant", () => {
|
describe("softDelete variant", () => {
|
||||||
it("should soft delete a variant and its relations", async () => {
|
it("should soft delete a variant and its relations", async () => {
|
||||||
const beforeDeletedVariants = await service.listVariants(
|
const beforeDeletedVariants = await service.listVariants(
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ export default class ProductModuleService<
|
|||||||
new Set(variantsWithProductId.map((v) => v.product_id!))
|
new Set(variantsWithProductId.map((v) => v.product_id!))
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ take: null },
|
{ take: null, relations: ["values"] },
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1204,7 +1204,7 @@ export default class ProductModuleService<
|
|||||||
if (product.variants?.length) {
|
if (product.variants?.length) {
|
||||||
allOptions = await this.productOptionService_.list(
|
allOptions = await this.productOptionService_.list(
|
||||||
{ product_id: upsertedProduct.id },
|
{ product_id: upsertedProduct.id },
|
||||||
{ take: null },
|
{ take: null, relations: ["values"] },
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user