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 { Product } from "@medusajs/medusa"
|
||||
import { Button, Input } from "@medusajs/ui"
|
||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||
import { Button, Heading, Input, Switch } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
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 {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { castNumber } from "../../../../../lib/cast-number"
|
||||
import { optionalInt } from "../../../../../lib/validation"
|
||||
import { useCreateProductVariant } from "../../../../../hooks/api/products"
|
||||
|
||||
type EditProductVariantsFormProps = {
|
||||
type CreateProductVariantFormProps = {
|
||||
product: Product
|
||||
isStockAndInventoryEnabled?: boolean
|
||||
}
|
||||
|
||||
const CreateProductVariantSchema = z.object({
|
||||
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 = ({
|
||||
product,
|
||||
}: EditProductVariantsFormProps) => {
|
||||
isStockAndInventoryEnabled = false,
|
||||
}: CreateProductVariantFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof CreateProductVariantSchema>>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
values: [],
|
||||
inventory_quantity: 0,
|
||||
manage_inventory: true,
|
||||
allow_backorder: false,
|
||||
options: {},
|
||||
},
|
||||
resolver: zodResolver(CreateProductVariantSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useCreateProductVariant(product.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
mutateAsync(values, {
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const parseNumber = (value?: string | number) => {
|
||||
if (typeof value === "undefined" || value === "") {
|
||||
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 (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form
|
||||
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">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
<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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<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 (
|
||||
<Form.Item>
|
||||
<Form.Label>title</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
<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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="values"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>value</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
value={(value ?? []).join(",")}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
onChange(val.split(",").map((v) => v.trim()))
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<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.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
|
||||
+21
-83
@@ -1,11 +1,11 @@
|
||||
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 { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Fragment, useState } 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"
|
||||
@@ -41,24 +41,24 @@ const ProductEditVariantSchema = z.object({
|
||||
mid_code: z.string().optional(),
|
||||
hs_code: z.string().optional(),
|
||||
origin_country: z.string().optional(),
|
||||
options: z.record(
|
||||
z.object({
|
||||
value: z.string().min(1),
|
||||
})
|
||||
),
|
||||
options: z.record(z.string()),
|
||||
})
|
||||
|
||||
// TODO: Either pass option ID or make the backend handle options constraints differently to handle the lack of IDs
|
||||
export const ProductEditVariantForm = ({
|
||||
product,
|
||||
variant,
|
||||
isStockAndInventoryEnabled = false,
|
||||
}: ProductEditVariantFormProps) => {
|
||||
const [optionValues, setOptionValues] = useState<Record<string, string[]>>(
|
||||
initOptionValues(product)
|
||||
)
|
||||
|
||||
const { t } = useTranslation()
|
||||
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>>({
|
||||
defaultValues: {
|
||||
@@ -78,7 +78,7 @@ export const ProductEditVariantForm = ({
|
||||
mid_code: variant.mid_code || "",
|
||||
hs_code: variant.hs_code || "",
|
||||
origin_country: variant.origin_country || "",
|
||||
options: getDefaultOptionValues(product, variant),
|
||||
options: defaultOptions,
|
||||
},
|
||||
resolver: zodResolver(ProductEditVariantSchema),
|
||||
})
|
||||
@@ -113,7 +113,6 @@ export const ProductEditVariantForm = ({
|
||||
ean,
|
||||
upc,
|
||||
barcode,
|
||||
options,
|
||||
...rest
|
||||
} = 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(
|
||||
{
|
||||
variant_id: variant.id,
|
||||
id: variant.id,
|
||||
weight: parseNumber(weight),
|
||||
height: parseNumber(height),
|
||||
width: parseNumber(width),
|
||||
length: parseNumber(length),
|
||||
options: optionsPayload,
|
||||
...conditionalPayload,
|
||||
...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 (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form
|
||||
@@ -210,30 +189,27 @@ export const ProductEditVariantForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{product.options.map((option) => {
|
||||
{product.options.map((option: any) => {
|
||||
return (
|
||||
<Form.Field
|
||||
key={option.id}
|
||||
control={form.control}
|
||||
name={`options.${option.id}`}
|
||||
name={`options.${option.title}`}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
const options = optionValues[option.id].map((value) => ({
|
||||
label: value,
|
||||
value,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{option.title}</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value.value}
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange({ value: v })
|
||||
onChange(v)
|
||||
}}
|
||||
onCreateOption={handleCreateOption(option.id)}
|
||||
{...field}
|
||||
options={options}
|
||||
options={option.values.map((v: any) => ({
|
||||
label: v.value,
|
||||
value: v.value,
|
||||
}))}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
@@ -530,41 +506,3 @@ export const ProductEditVariantForm = ({
|
||||
</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"
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
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", () => {
|
||||
it("should soft delete a variant and its relations", async () => {
|
||||
const beforeDeletedVariants = await service.listVariants(
|
||||
|
||||
@@ -385,7 +385,7 @@ export default class ProductModuleService<
|
||||
new Set(variantsWithProductId.map((v) => v.product_id!))
|
||||
),
|
||||
},
|
||||
{ take: null },
|
||||
{ take: null, relations: ["values"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -1204,7 +1204,7 @@ export default class ProductModuleService<
|
||||
if (product.variants?.length) {
|
||||
allOptions = await this.productOptionService_.list(
|
||||
{ product_id: upsertedProduct.id },
|
||||
{ take: null },
|
||||
{ take: null, relations: ["values"] },
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user