feat(dashboard): variant details page (#7767)
* wip: setup * feat: finish inventory, prices section * feat: finish prices section pagination * fix: move edit variants to variants details, fix loader * fix: suggestion * feat: price editor flow
This commit is contained in:
@@ -174,6 +174,7 @@ export const useUpdateProductVariantsBatch = (
|
|||||||
}),
|
}),
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() })
|
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.details() })
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: productsQueryKeys.detail(productId),
|
queryKey: productsQueryKeys.detail(productId),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"viewDetails": "View details",
|
"viewDetails": "View details",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
"showMore": "Show more",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
@@ -325,6 +326,7 @@
|
|||||||
"create": {
|
"create": {
|
||||||
"header": "Create Variant"
|
"header": "Create Variant"
|
||||||
},
|
},
|
||||||
|
"pricesPagination": "1 - {{current}} of {{total}} prices",
|
||||||
"tableItemAvailable": "{{availableCount}} available",
|
"tableItemAvailable": "{{availableCount}} available",
|
||||||
"tableItem_one": "{{availableCount}} available at {{locationCount}} location",
|
"tableItem_one": "{{availableCount}} available at {{locationCount}} location",
|
||||||
"tableItem_other": "{{availableCount}} available at {{locationCount}} locations",
|
"tableItem_other": "{{availableCount}} available at {{locationCount}} locations",
|
||||||
@@ -1677,6 +1679,10 @@
|
|||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled"
|
"disabled": "Disabled"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"productVariant": "Product Variant",
|
||||||
|
"prices": "Prices"
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"refundAmount": "Refund amount",
|
"refundAmount": "Refund amount",
|
||||||
@@ -1688,6 +1694,8 @@
|
|||||||
"customTitle": "Custom title",
|
"customTitle": "Custom title",
|
||||||
"manageInventory": "Manage inventory",
|
"manageInventory": "Manage inventory",
|
||||||
"inventoryKit": "Inventory kit",
|
"inventoryKit": "Inventory kit",
|
||||||
|
"inventoryItems": "Inventory items",
|
||||||
|
"requiredQuantity": "Required quantity",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
|||||||
@@ -110,10 +110,25 @@ export const RouteMap: RouteObject[] = [
|
|||||||
lazy: () =>
|
lazy: () =>
|
||||||
import("../../routes/products/product-create-variant"),
|
import("../../routes/products/product-create-variant"),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id/variants/:variant_id",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/product-variants/product-variant-detail"
|
||||||
|
),
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
path: "variants/:variant_id/edit",
|
path: "edit",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import("../../routes/products/product-edit-variant"),
|
import(
|
||||||
|
"../../routes/product-variants/product-variant-edit"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "prices",
|
||||||
|
lazy: () => import("../../routes/products/product-prices"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./variant-general-section"
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
import { ProductVariantDTO } from "@medusajs/types"
|
||||||
|
import { Badge, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
|
import { Component, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { SectionRow } from "../../../../../components/common/section"
|
||||||
|
import { useDeleteVariant } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
|
type VariantGeneralSectionProps = {
|
||||||
|
variant: ProductVariantDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariantGeneralSection({ variant }: VariantGeneralSectionProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const hasInventoryKit = variant.inventory?.length > 1
|
||||||
|
|
||||||
|
const { mutateAsync } = useDeleteVariant(variant.product_id, variant.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("products.variant.deleteWarning", {
|
||||||
|
title: variant.title,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate("..", { replace: true })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Heading>{variant.title}</Heading>
|
||||||
|
{hasInventoryKit && (
|
||||||
|
<span className="text-ui-fg-muted font-normal">
|
||||||
|
<Component />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-ui-fg-subtle txt-small mt-2">
|
||||||
|
{t("labels.productVariant")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: "edit",
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
icon: <Trash />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SectionRow title={t("fields.sku")} value={variant.sku} />
|
||||||
|
{variant.options.map((o) => (
|
||||||
|
<SectionRow
|
||||||
|
key={o.id}
|
||||||
|
title={o.option?.title}
|
||||||
|
value={<Badge size="2xsmall">{o.value}</Badge>}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./variant-inventory-section"
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Buildings } from "@medusajs/icons"
|
||||||
|
import { InventoryItemDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
|
||||||
|
export const InventoryActions = ({ item }: { item: InventoryItemDTO }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Buildings />,
|
||||||
|
label: t("product.variant.inventory.navigateToItem"),
|
||||||
|
to: `/inventory/${item.id}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
import { InventoryNext, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { InventoryActions } from "./inventory-actions"
|
||||||
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
interface ExtendedInventoryItem extends InventoryNext.InventoryItemDTO {
|
||||||
|
variants: ProductVariantDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ExtendedInventoryItem>()
|
||||||
|
|
||||||
|
export const useInventoryTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("title", {
|
||||||
|
header: t("fields.title"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const title = getValue()
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{title}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("sku", {
|
||||||
|
header: t("fields.sku"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const sku = getValue() as string
|
||||||
|
|
||||||
|
if (!sku) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{sku}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("required_quantity", {
|
||||||
|
header: t("fields.requiredQuantity"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const quantity = getValue()
|
||||||
|
|
||||||
|
if (Number.isNaN(quantity)) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{quantity}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "inventory_quantity",
|
||||||
|
header: t("fields.inventory"),
|
||||||
|
cell: ({ getValue, row: { original: inventory } }) => {
|
||||||
|
if (!inventory.location_levels?.length) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
let quantity = 0
|
||||||
|
let locations = 0
|
||||||
|
|
||||||
|
inventory.location_levels.forEach((level) => {
|
||||||
|
quantity += level.available_quantity
|
||||||
|
locations += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{t("products.variant.tableItem", {
|
||||||
|
availableCount: quantity,
|
||||||
|
locationCount: locations,
|
||||||
|
count: locations,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <InventoryActions item={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
import { InventoryItemDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useInventoryTableColumns } from "./use-inventory-table-columns"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
type VariantInventorySectionProps = {
|
||||||
|
inventoryItems: InventoryItemDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariantInventorySection({
|
||||||
|
inventoryItems,
|
||||||
|
}: VariantInventorySectionProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const columns = useInventoryTableColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: inventoryItems ?? [],
|
||||||
|
columns,
|
||||||
|
count: inventoryItems.length,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Heading level="h2">{t("fields.inventoryItems")}</Heading>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
{/*TODO: add inventory management*/}
|
||||||
|
{/*<ActionMenu*/}
|
||||||
|
{/* groups={[*/}
|
||||||
|
{/* {*/}
|
||||||
|
{/* actions: [*/}
|
||||||
|
{/* {*/}
|
||||||
|
{/* label: t("actions.manageInventoryItems"),*/}
|
||||||
|
{/* to: "edit",*/}
|
||||||
|
{/* icon: <Component />,*/}
|
||||||
|
{/* },*/}
|
||||||
|
{/* ],*/}
|
||||||
|
{/* },*/}
|
||||||
|
{/* ]}*/}
|
||||||
|
{/*/>*/}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={inventoryItems.length}
|
||||||
|
navigateTo={(row) => `/inventory/${row.id}`}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./variant-prices-section"
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import { CurrencyDollar } from "@medusajs/icons"
|
||||||
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
|
import { MoneyAmountDTO, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||||
|
|
||||||
|
type VariantPricesSectionProps = {
|
||||||
|
variant: ProductVariantDTO & { prices: MoneyAmountDTO[] }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariantPricesSection({ variant }: VariantPricesSectionProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const prices = variant.prices
|
||||||
|
.filter((p) => !p.rules?.length)
|
||||||
|
.sort((p1, p2) => p1.currency_code?.localeCompare(p2.currency_code)) // display just currency prices
|
||||||
|
|
||||||
|
const [current, setCurrent] = useState(Math.min(prices.length, 3))
|
||||||
|
|
||||||
|
const hasPrices = !!prices.length
|
||||||
|
|
||||||
|
const displayPrices = prices.slice(0, current)
|
||||||
|
|
||||||
|
const onShowMore = () => {
|
||||||
|
setCurrent(Math.min(current + 3, prices.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex flex-col divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("labels.prices")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `/products/${variant.product_id}/variants/${variant.id}/prices`,
|
||||||
|
icon: <CurrencyDollar />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!hasPrices && <NoRecords className="h-60" />}
|
||||||
|
{displayPrices.map((price) => {
|
||||||
|
return (
|
||||||
|
<div className="txt-small text-ui-fg-subtle flex justify-between px-6 py-4">
|
||||||
|
<span className="font-medium">
|
||||||
|
{price.currency_code.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span>{getLocaleAmount(price.amount, price.currency_code)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{hasPrices && (
|
||||||
|
<div className="txt-small text-ui-fg-subtle flex items-center justify-between px-6 py-4">
|
||||||
|
<span className="font-medium">
|
||||||
|
{t("products.variant.pricesPagination", {
|
||||||
|
total: prices.length,
|
||||||
|
current,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
onClick={onShowMore}
|
||||||
|
disabled={current >= prices.length}
|
||||||
|
className="-mr-3 text-blue-500"
|
||||||
|
variant="transparent"
|
||||||
|
>
|
||||||
|
{t("actions.showMore")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export const VARIANT_DETAIL_FIELDS =
|
||||||
|
"*inventory_items,*inventory_items.inventory,*inventory_items.inventory.location_levels,*options,*options.option,*prices,*prices.price_rules"
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { variantLoader as loader } from "./loader"
|
||||||
|
export { ProductVariantDetail as Component } from "./product-variant-detail"
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { variantsQueryKeys } from "../../../hooks/api/products"
|
||||||
|
import { queryClient } from "../../../lib/query-client"
|
||||||
|
import { VARIANT_DETAIL_FIELDS } from "./constants"
|
||||||
|
import { sdk } from "../../../lib/client"
|
||||||
|
|
||||||
|
const variantDetailQuery = (productId: string, variantId: string) => ({
|
||||||
|
queryKey: variantsQueryKeys.detail(variantId),
|
||||||
|
queryFn: async () =>
|
||||||
|
sdk.admin.product.retrieveVariant(productId, variantId, {
|
||||||
|
fields: VARIANT_DETAIL_FIELDS,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const variantLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const productId = params.id
|
||||||
|
const variantId = params.variant_id
|
||||||
|
|
||||||
|
const query = variantDetailQuery(productId!, variantId!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<any>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { useProductVariant } from "../../../hooks/api/products"
|
||||||
|
|
||||||
|
import { variantLoader } from "./loader"
|
||||||
|
import { VARIANT_DETAIL_FIELDS } from "./constants"
|
||||||
|
import { VariantGeneralSection } from "./components/variant-general-section"
|
||||||
|
import { VariantInventorySection } from "./components/variant-inventory-section"
|
||||||
|
import { VariantPricesSection } from "./components/variant-prices-section"
|
||||||
|
|
||||||
|
export const ProductVariantDetail = () => {
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof variantLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { id, variant_id } = useParams()
|
||||||
|
const { variant, isLoading, isError, error } = useProductVariant(
|
||||||
|
id!,
|
||||||
|
variant_id,
|
||||||
|
{ fields: VARIANT_DETAIL_FIELDS },
|
||||||
|
{
|
||||||
|
initialData: initialData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading || !variant) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
|
||||||
|
<div className="flex w-full flex-col gap-y-3">
|
||||||
|
<VariantGeneralSection variant={variant} />
|
||||||
|
<VariantInventorySection
|
||||||
|
inventoryItems={variant.inventory_items.map((i) => {
|
||||||
|
return {
|
||||||
|
...i.inventory,
|
||||||
|
required_quantity: i.required_quantity,
|
||||||
|
variant,
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="hidden xl:block">
|
||||||
|
<JsonViewSection data={variant} root="product" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
|
||||||
|
<VariantPricesSection variant={variant} />
|
||||||
|
|
||||||
|
<div className="xl:hidden">
|
||||||
|
<JsonViewSection data={variant} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { ProductVariantEdit as Component } from "./product-variant-edit"
|
||||||
|
export { editProductVariantLoader as loader } from "./loader"
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import { ProductEditVariantForm } from "./components/product-edit-variant-form"
|
|||||||
import { editProductVariantLoader } from "./loader"
|
import { editProductVariantLoader } from "./loader"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
export const ProductEditVariant = () => {
|
export const ProductVariantEdit = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
ReturnType<typeof editProductVariantLoader>
|
ReturnType<typeof editProductVariantLoader>
|
||||||
>
|
>
|
||||||
+3
@@ -87,6 +87,9 @@ export const ProductVariantSection = ({
|
|||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
orderBy={["title", "created_at", "updated_at"]}
|
orderBy={["title", "created_at", "updated_at"]}
|
||||||
|
navigateTo={(row) =>
|
||||||
|
`/products/${row.original.product_id}/variants/${row.id}`
|
||||||
|
}
|
||||||
pagination
|
pagination
|
||||||
search
|
search
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { ProductEditVariant as Component } from "./product-edit-variant"
|
|
||||||
@@ -3,11 +3,12 @@ import { Button } 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 * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
import { RouteFocusModal, useRouteModal } from "../../../components/route-modal"
|
import { RouteFocusModal, useRouteModal } from "../../../components/route-modal"
|
||||||
import { useUpdateProductVariantsBatch } from "../../../hooks/api/products"
|
import { useUpdateProductVariantsBatch } from "../../../hooks/api/products"
|
||||||
import { VariantPricingForm } from "../common/variant-pricing-form"
|
import { VariantPricingForm } from "../common/variant-pricing-form"
|
||||||
import { castNumber } from "../../../lib/cast-number"
|
import { castNumber } from "../../../lib/cast-number"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
export const UpdateVariantPricesSchema = zod.object({
|
export const UpdateVariantPricesSchema = zod.object({
|
||||||
variants: zod.array(
|
variants: zod.array(
|
||||||
@@ -25,14 +26,21 @@ export type UpdateVariantPricesSchemaType = zod.infer<
|
|||||||
|
|
||||||
export const PricingEdit = ({
|
export const PricingEdit = ({
|
||||||
product,
|
product,
|
||||||
|
variantId,
|
||||||
}: {
|
}: {
|
||||||
product: HttpTypes.AdminProduct
|
product: HttpTypes.AdminProduct
|
||||||
|
variantId?: string
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const variants = variantId
|
||||||
|
? product.variants.filter((v) => v.id === variantId)
|
||||||
|
: product.variants
|
||||||
|
|
||||||
const form = useForm<UpdateVariantPricesSchemaType>({
|
const form = useForm<UpdateVariantPricesSchemaType>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
variants: product.variants.map((variant: any) => ({
|
variants: variants.map((variant: any) => ({
|
||||||
title: variant.title,
|
title: variant.title,
|
||||||
prices: variant.prices.reduce((acc: any, price: any) => {
|
prices: variant.prices.reduce((acc: any, price: any) => {
|
||||||
acc[price.currency_code] = price.amount
|
acc[price.currency_code] = price.amount
|
||||||
@@ -49,10 +57,10 @@ export const PricingEdit = ({
|
|||||||
const handleSubmit = form.handleSubmit(
|
const handleSubmit = form.handleSubmit(
|
||||||
async (values) => {
|
async (values) => {
|
||||||
const reqData = values.variants.map((variant, ind) => ({
|
const reqData = values.variants.map((variant, ind) => ({
|
||||||
id: product.variants[ind].id,
|
id: variants[ind].id,
|
||||||
prices: Object.entries(variant.prices || {}).map(
|
prices: Object.entries(variant.prices || {}).map(
|
||||||
([currency_code, value]: any) => {
|
([currency_code, value]: any) => {
|
||||||
const id = product.variants[ind].prices.find(
|
const id = variants[ind].prices.find(
|
||||||
(p) => p.currency_code === currency_code
|
(p) => p.currency_code === currency_code
|
||||||
)?.id
|
)?.id
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ export const PricingEdit = ({
|
|||||||
}))
|
}))
|
||||||
await mutateAsync(reqData, {
|
await mutateAsync(reqData, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
handleSuccess(`/products/${product.id}`)
|
handleSuccess("..")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PricingEdit } from "./pricing-edit"
|
|||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
|
||||||
export const ProductPrices = () => {
|
export const ProductPrices = () => {
|
||||||
const { id } = useParams()
|
const { id, variant_id } = useParams()
|
||||||
|
|
||||||
const { product, isLoading, isError, error } = useProduct(id!)
|
const { product, isLoading, isError, error } = useProduct(id!)
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@ export const ProductPrices = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
{!isLoading && product && <PricingEdit product={product} />}
|
{!isLoading && product && (
|
||||||
|
<PricingEdit product={product} variantId={variant_id} />
|
||||||
|
)}
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user