feat(dashboard): Variant edit form (#6870)
**What** - Adds form to edit variant details. - If stock and inventory modules are installed we omit inputs for stock and inventory as they should be managed in a separate form in that case. (Still a bit unsure about this, but its what we have in the current admin)
This commit is contained in:
1 parent
8363dbec4f
commit
b2763647f7
10 files changed
+632
-8
No files matched your search
@@ -43,6 +43,10 @@
|
|||||||
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved.",
|
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved.",
|
||||||
"timeline": "Timeline"
|
"timeline": "Timeline"
|
||||||
},
|
},
|
||||||
|
"validation": {
|
||||||
|
"mustBeInt": "The value must be a whole number.",
|
||||||
|
"mustBePositive": "The value must be a positive number."
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"developer": "Developer",
|
"developer": "Developer",
|
||||||
@@ -181,6 +185,18 @@
|
|||||||
"published": "Published",
|
"published": "Published",
|
||||||
"proposed": "Proposed",
|
"proposed": "Proposed",
|
||||||
"rejected": "Rejected"
|
"rejected": "Rejected"
|
||||||
|
},
|
||||||
|
"variant": {
|
||||||
|
"edit": {
|
||||||
|
"header": "Edit Variant"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"header": "Stock & Inventory",
|
||||||
|
"manageInventoryLabel": "Manage inventory",
|
||||||
|
"manageInventoryHint": "When enabled the inventory level will be regulated when orders and returns are created.",
|
||||||
|
"allowBackordersLabel": "Allow backorders",
|
||||||
|
"allowBackordersHint": "When enabled the variant can be sold even if the inventory level is below zero."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"collections": {
|
"collections": {
|
||||||
@@ -940,9 +956,13 @@
|
|||||||
"width": "Width",
|
"width": "Width",
|
||||||
"length": "Length",
|
"length": "Length",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
"midCode": "MID Code",
|
"midCode": "MID code",
|
||||||
"hsCode": "HS Code",
|
"hsCode": "HS code",
|
||||||
"countryOfOrigin": "Country of Origin",
|
"ean": "EAN",
|
||||||
|
"upc": "UPC",
|
||||||
|
"inventoryQuantity": "Inventory quantity",
|
||||||
|
"barcode": "Barcode",
|
||||||
|
"countryOfOrigin": "Country of origin",
|
||||||
"material": "Material",
|
"material": "Material",
|
||||||
"thumbnail": "Thumbnail",
|
"thumbnail": "Thumbnail",
|
||||||
"sku": "SKU",
|
"sku": "SKU",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import i18next from "i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { castNumber } from "./cast-number"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that an optional value is an integer.
|
||||||
|
*/
|
||||||
|
export const optionalInt = z
|
||||||
|
.union([z.string(), z.number()])
|
||||||
|
.optional()
|
||||||
|
.refine(
|
||||||
|
(value) => {
|
||||||
|
if (value === "" || value === undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number.isInteger(castNumber(value))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: i18next.t("validation.mustBeInt"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(value) => {
|
||||||
|
if (value === "" || value === undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return castNumber(value) >= 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: i18next.t("validation.mustBePositive"),
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -239,6 +239,11 @@ export const v1Routes: RouteObject[] = [
|
|||||||
path: "media",
|
path: "media",
|
||||||
lazy: () => import("../../routes/products/product-media"),
|
lazy: () => import("../../routes/products/product-media"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "variants/:variant_id/edit",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/products/product-edit-variant"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { Plus } from "@medusajs/icons"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Container, Heading } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
import { useAdminProductVariants } from "medusa-react"
|
import { useAdminProductVariants } from "medusa-react"
|
||||||
@@ -64,9 +64,9 @@ export const ProductVariantSection = ({
|
|||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: t("actions.edit"),
|
label: t("actions.create"),
|
||||||
to: "variants",
|
to: `variants/create`,
|
||||||
icon: <PencilSquare />,
|
icon: <Plus />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ const VariantActions = ({
|
|||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
to: `variants/${variant.id}`,
|
to: `variants/${variant.id}/edit`,
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-edit-variant-form"
|
||||||
+465
@@ -0,0 +1,465 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||||
|
import { Button, Heading, Input, Switch } from "@medusajs/ui"
|
||||||
|
import { useAdminUpdateVariant } from "medusa-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Fragment } from "react"
|
||||||
|
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"
|
||||||
|
|
||||||
|
type ProductEditVariantFormProps = {
|
||||||
|
product: Product
|
||||||
|
variant: ProductVariant
|
||||||
|
isStockAndInventoryEnabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductEditVariantSchema = 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(),
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ProductEditVariantForm = ({
|
||||||
|
product,
|
||||||
|
variant,
|
||||||
|
isStockAndInventoryEnabled = false,
|
||||||
|
}: ProductEditVariantFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof ProductEditVariantSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
title: variant.title,
|
||||||
|
material: variant.material || "",
|
||||||
|
sku: variant.sku || "",
|
||||||
|
ean: variant.ean || "",
|
||||||
|
upc: variant.upc || "",
|
||||||
|
barcode: variant.barcode || "",
|
||||||
|
inventory_quantity: variant.inventory_quantity || "",
|
||||||
|
manage_inventory: variant.manage_inventory,
|
||||||
|
allow_backorder: variant.allow_backorder,
|
||||||
|
weight: variant.weight || "",
|
||||||
|
height: variant.height || "",
|
||||||
|
width: variant.width || "",
|
||||||
|
length: variant.length || "",
|
||||||
|
mid_code: variant.mid_code || "",
|
||||||
|
hs_code: variant.hs_code || "",
|
||||||
|
origin_country: variant.origin_country || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ProductEditVariantSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdateVariant(product.id)
|
||||||
|
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
variant_id: variant.id,
|
||||||
|
weight: parseNumber(weight),
|
||||||
|
height: parseNumber(height),
|
||||||
|
width: parseNumber(width),
|
||||||
|
length: parseNumber(length),
|
||||||
|
...conditionalPayload,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex size-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>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { editProductVariantLoader as loader } from "./loader"
|
||||||
|
export { ProductEditVariant as Component } from "./product-edit-variant"
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { adminProductKeys, adminStoreKeys } from "medusa-react"
|
||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { medusa, queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const queryKey = (id: string) => {
|
||||||
|
return [adminProductKeys.detail(id), adminStoreKeys.details()]
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { ProductVariant } from "@medusajs/medusa"
|
||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminProduct } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { json, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { ProductEditVariantForm } from "./components/product-edit-variant-form"
|
||||||
|
import { editProductVariantLoader } from "./loader"
|
||||||
|
|
||||||
|
export const ProductEditVariant = () => {
|
||||||
|
const loaderData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof editProductVariantLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { id, variant_id } = useParams()
|
||||||
|
|
||||||
|
const { product, isLoading, isError, error } = useAdminProduct(
|
||||||
|
id!,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
initialData: loaderData?.initialData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const variant = product?.variants.find(
|
||||||
|
(v: ProductVariant) => v.id === variant_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isLoading && !variant) {
|
||||||
|
throw json({
|
||||||
|
status: 404,
|
||||||
|
message: `Variant with ID ${variant_id} was not found.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready = !isLoading && !!product && !!variant
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("products.variant.edit.header")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && (
|
||||||
|
<ProductEditVariantForm
|
||||||
|
product={product}
|
||||||
|
variant={variant as unknown as ProductVariant}
|
||||||
|
isStockAndInventoryEnabled={loaderData?.isStockAndInventoryEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user