From 26700821a22dc7197c5a41e42fa94aa39078a9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 17 Sep 2024 19:46:23 +0200 Subject: [PATCH] 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 --- .../dashboard/src/i18n/translations/en.json | 5 +- .../admin/dashboard/src/lib/validation.ts | 35 + .../create-product-variant-form/constants.ts | 44 ++ .../create-product-variant-form.tsx | 742 ++++++++---------- .../details-tab.tsx | 188 +++++ .../inventory-kit-tab.tsx | 188 +++++ .../pricing-tab.tsx | 100 +++ .../product-create-variant.tsx | 12 +- .../types/src/http/product/admin/payloads.ts | 6 + 9 files changed, 898 insertions(+), 422 deletions(-) create mode 100644 packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/constants.ts create mode 100644 packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/details-tab.tsx create mode 100644 packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/inventory-kit-tab.tsx create mode 100644 packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/pricing-tab.tsx diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 9338e9d574..8bad727101 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -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." diff --git a/packages/admin/dashboard/src/lib/validation.ts b/packages/admin/dashboard/src/lib/validation.ts index b0972576bc..4c94566927 100644 --- a/packages/admin/dashboard/src/lib/validation.ts +++ b/packages/admin/dashboard/src/lib/validation.ts @@ -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( + form: UseFormReturn, + fields: FieldPath[], + schema: z.ZodSchema +) { + form.clearErrors(fields as any) + + const values = fields.reduce((acc, key) => { + acc[key] = form.getValues(key as any) + return acc + }, {} as Record) + + 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 +} diff --git a/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/constants.ts b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/constants.ts new file mode 100644 index 0000000000..f5bb2351ab --- /dev/null +++ b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/constants.ts @@ -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)[] diff --git a/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/create-product-variant-form.tsx b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/create-product-variant-form.tsx index ad54f565ec..7dcd1cb1ae 100644 --- a/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/create-product-variant-form.tsx +++ b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/create-product-variant-form.tsx @@ -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 + +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.DETAIL) + const [tabState, setTabState] = useState(initialTabState) + const form = useForm>({ 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) + }, [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>( + form, + CreateVariantDetailsFields, + CreateVariantDetailsSchema + ) + ) { + setTabState((prev) => ({ + ...prev, + [tab]: "in-progress", + })) + setTab(tab) + return } - : {} + + setTabState((prev) => ({ + ...prev, + [tab]: "completed", + })) + } else if (tab === Tab.PRICE) { + if ( + !partialFormValidation>( + 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 ( - -
+ handleChangeTab(tab as Tab)} + className="flex h-full flex-col overflow-hidden" > - -
- { - return ( - - {t("fields.title")} - - - - - - ) - }} - /> - {product.options.map((option: any) => { - return ( - { - return ( - - {option.title} - - { - onChange(v) - }} - {...field} - options={option.values.map((v: any) => ({ - label: v.value, - value: v.value, - }))} - /> - - - ) - }} - /> - ) - })} -
- - {!isStockAndInventoryEnabled && ( - -
-
- - {t("products.variant.inventory.header")} - - { - return ( - - {t("fields.sku")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.ean")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.upc")} - - - - - - ) - }} - /> - { - return ( - - - {t("fields.barcode")} - - - - - - - ) - }} - /> -
- { - return ( - -
-
- - {t( - "products.variant.inventory.manageInventoryLabel" - )} - - - - onChange(!!checked) - } - {...field} - /> - -
- - {t( - "products.variant.inventory.manageInventoryHint" - )} - -
- -
- ) - }} - /> - { - return ( - -
-
- - {t( - "products.variant.inventory.allowBackordersLabel" - )} - - - - onChange(!!checked) - } - {...field} - /> - -
- - {t( - "products.variant.inventory.allowBackordersHint" - )} - -
- -
- ) - }} - /> + + +
+
+ + + {t("priceLists.create.tabs.details")} + + + {t("priceLists.create.tabs.prices")} + + {!!inventoryTabEnabled && ( + + {t("products.create.tabs.inventory")} + + )} +
- - - )} -
- {t("products.attributes")} - { - return ( - - {t("fields.material")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.weight")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.width")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.length")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.height")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.midCode")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.hsCode")} - - - - - - ) - }} - /> - { - return ( - - - {t("fields.countryOfOrigin")} - - - - - - - ) - }} - /> -
- - -
- - - - -
-
- - +
+
+ + + + + + + + {!!inventoryTabEnabled && ( + + + + )} + + +
+ + + + +
+
+ + + + ) +} + +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 ( + + ) + } + + return ( + ) } diff --git a/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/details-tab.tsx b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/details-tab.tsx new file mode 100644 index 0000000000..6062b2008e --- /dev/null +++ b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/details-tab.tsx @@ -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> +} + +function DetailsTab({ form, product }: DetailsTabProps) { + const { t } = useTranslation() + + const manageInventoryEnabled = useWatch({ + control: form.control, + name: "manage_inventory", + }) + + return ( +
+
+ {t("products.variant.create.header")} + +
+ { + return ( + + {t("fields.title")} + + + + + + ) + }} + /> + + { + return ( + + {t("fields.sku")} + + + + + + ) + }} + /> + + {product.options.map((option: any) => ( + { + return ( + + {option.title} + + { + onChange(v) + }} + {...field} + options={option.values.map((v: any) => ({ + label: v.value, + value: v.value, + }))} + /> + + + ) + }} + /> + ))} +
+
+ { + return ( + +
+ + onChange(!!checked)} + {...field} + /> + + +
+ + {t("products.variant.inventory.manageInventoryLabel")} + + + {t("products.variant.inventory.manageInventoryHint")} + +
+
+ +
+ ) + }} + /> + { + return ( + +
+ + onChange(!!checked)} + {...field} + disabled={!manageInventoryEnabled} + /> + +
+ + {t("products.variant.inventory.allowBackordersLabel")} + + + {t("products.variant.inventory.allowBackordersHint")} + +
+
+ +
+ ) + }} + /> + { + return ( + +
+ + onChange(!!checked)} + {...field} + disabled={!manageInventoryEnabled} + /> + +
+ + {t("products.variant.inventory.inventoryKit")} + + + {t("products.variant.inventory.inventoryKitHint")} + +
+
+ +
+ ) + }} + /> +
+
+
+ ) +} + +export default DetailsTab diff --git a/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/inventory-kit-tab.tsx b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/inventory-kit-tab.tsx new file mode 100644 index 0000000000..0b69abaf3d --- /dev/null +++ b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/inventory-kit-tab.tsx @@ -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> +} + +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 ( +
+
+
+ {t("products.create.inventory.heading")} + +
+
+
+ {form.getValues("title")} + {t("products.create.inventory.label")} +
+ +
+ {inventory.fields.map((inventoryItem, inventoryIndex) => ( +
  • +
    +
    + +
    + + { + return ( + + + ({ + ...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" + )} + /> + + + ) + }} + /> + +
    + +
    + { + return ( + + + { + const value = e.target.value + + if (value === "") { + onChange(null) + } else { + onChange(Number(value)) + } + }} + {...field} + placeholder={t( + "products.create.inventory.quantityPlaceholder" + )} + /> + + + + ) + }} + /> +
    + inventory.remove(inventoryIndex)} + > + + +
  • + ))} +
    +
    +
    +
    + ) +} + +export default InventoryKitTab diff --git a/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/pricing-tab.tsx b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/pricing-tab.tsx new file mode 100644 index 0000000000..213819e7fb --- /dev/null +++ b/packages/admin/dashboard/src/routes/products/product-create-variant/components/create-product-variant-form/pricing-tab.tsx @@ -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> +} + +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 ( + setCloseOnEscape(!editing)} + /> + ) +} + +const columnHelper = createDataGridHelper< + HttpTypes.AdminProductVariant, + z.infer +>() + +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 ( + +
    + {entity.title} +
    +
    + ) + }, + disableHiding: true, + }), + ...createDataGridPriceColumns< + HttpTypes.AdminProductVariant, + z.infer + >({ + 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 diff --git a/packages/admin/dashboard/src/routes/products/product-create-variant/product-create-variant.tsx b/packages/admin/dashboard/src/routes/products/product-create-variant/product-create-variant.tsx index 7d598d2f49..d37b61060c 100644 --- a/packages/admin/dashboard/src/routes/products/product-create-variant/product-create-variant.tsx +++ b/packages/admin/dashboard/src/routes/products/product-create-variant/product-create-variant.tsx @@ -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 ( - - - {t("products.variant.create.header")} - + {!isLoading && product && } - + ) } diff --git a/packages/core/types/src/http/product/admin/payloads.ts b/packages/core/types/src/http/product/admin/payloads.ts index 588cde8095..678e1c2ed8 100644 --- a/packages/core/types/src/http/product/admin/payloads.ts +++ b/packages/core/types/src/http/product/admin/payloads.ts @@ -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 prices: AdminCreateProductVariantPrice[] + inventory_items?: AdminCreateProductVariantInventoryKit[] options?: Record }