fix(dashboard): Load product variant edit page and fix product detail query key (#10029)
**What** - Fixes Edit Variant form so it properly loads the product variant - Fixes the query key for product details to prevent the cache from being shared between queries for the same ID but with different params. Resolves CMRC-685 Co-authored-by: Frane Polić <16856471+fPolic@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
81208b6e1d
commit
904f0926f1
5
.changeset/modern-walls-appear.md
Normal file
5
.changeset/modern-walls-appear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/dashboard": patch
|
||||
---
|
||||
|
||||
fix(dashboard): Fix query key for product details and load product variant data correctly
|
||||
@@ -87,16 +87,21 @@ export const useDeleteProductOption = (
|
||||
export const useProductVariant = (
|
||||
productId: string,
|
||||
variantId: string,
|
||||
query?: Record<string, any>,
|
||||
query?: HttpTypes.AdminProductVariantParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, FetchError, any, QueryKey>,
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminProductVariantResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminProductVariantResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () =>
|
||||
sdk.admin.product.retrieveVariant(productId, variantId, query),
|
||||
queryKey: variantsQueryKeys.detail(variantId),
|
||||
queryKey: variantsQueryKeys.detail(variantId, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
@@ -238,7 +243,7 @@ export const useProduct = (
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.product.retrieve(id, query),
|
||||
queryKey: productsQueryKeys.detail(id),
|
||||
queryKey: productsQueryKeys.detail(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ export type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
|
||||
lists: () => readonly [...TQueryKey<TKey>["all"], "list"]
|
||||
list: (
|
||||
query?: TListQuery
|
||||
) => readonly [
|
||||
...ReturnType<TQueryKey<TKey>["lists"]>,
|
||||
{ query: TListQuery | undefined },
|
||||
]
|
||||
) => readonly [...ReturnType<TQueryKey<TKey>["lists"]>, { query: TListQuery }]
|
||||
details: () => readonly [...TQueryKey<TKey>["all"], "detail"]
|
||||
detail: (
|
||||
id: TDetailQuery,
|
||||
@@ -16,7 +13,7 @@ export type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
|
||||
) => readonly [
|
||||
...ReturnType<TQueryKey<TKey>["details"]>,
|
||||
TDetailQuery,
|
||||
{ query: TListQuery | undefined },
|
||||
{ query: TListQuery }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,7 +23,7 @@ export type UseQueryOptionsWrapper<
|
||||
// Type thrown in case the queryFn rejects
|
||||
E = Error,
|
||||
// Query key type
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TQueryKey extends QueryKey = QueryKey
|
||||
> = Omit<
|
||||
UseQueryOptions<TQueryFn, E, TQueryFn, TQueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
@@ -35,20 +32,22 @@ export type UseQueryOptionsWrapper<
|
||||
export const queryKeysFactory = <
|
||||
T,
|
||||
TListQueryType = any,
|
||||
TDetailQueryType = string,
|
||||
TDetailQueryType = string
|
||||
>(
|
||||
globalKey: T
|
||||
) => {
|
||||
const queryKeyFactory: TQueryKey<T, TListQueryType, TDetailQueryType> = {
|
||||
all: [globalKey],
|
||||
lists: () => [...queryKeyFactory.all, "list"],
|
||||
list: (query?: TListQueryType) => [...queryKeyFactory.lists(), { query }],
|
||||
list: (query?: TListQueryType) =>
|
||||
[...queryKeyFactory.lists(), query ? { query } : undefined].filter(
|
||||
(k) => !!k
|
||||
),
|
||||
details: () => [...queryKeyFactory.all, "detail"],
|
||||
detail: (id: TDetailQueryType, query?: TListQueryType) => [
|
||||
...queryKeyFactory.details(),
|
||||
id,
|
||||
{ query },
|
||||
],
|
||||
detail: (id: TDetailQueryType, query?: TListQueryType) =>
|
||||
[...queryKeyFactory.details(), id, query ? { query } : undefined].filter(
|
||||
(k) => !!k
|
||||
),
|
||||
}
|
||||
return queryKeyFactory
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ProductVariantDTO } from "@medusajs/types"
|
||||
import { Badge, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { Component, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Badge, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SectionRow } from "../../../../../components/common/section"
|
||||
import { useDeleteVariant } from "../../../../../hooks/api/products"
|
||||
|
||||
type VariantGeneralSectionProps = {
|
||||
variant: ProductVariantDTO
|
||||
variant: HttpTypes.AdminProductVariant
|
||||
}
|
||||
|
||||
export function VariantGeneralSection({ variant }: VariantGeneralSectionProps) {
|
||||
@@ -19,7 +19,7 @@ export function VariantGeneralSection({ variant }: VariantGeneralSectionProps) {
|
||||
|
||||
const hasInventoryKit = variant.inventory?.length > 1
|
||||
|
||||
const { mutateAsync } = useDeleteVariant(variant.product_id, variant.id)
|
||||
const { mutateAsync } = useDeleteVariant(variant.product_id!, variant.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
@@ -85,10 +85,10 @@ export function VariantGeneralSection({ variant }: VariantGeneralSectionProps) {
|
||||
</div>
|
||||
|
||||
<SectionRow title={t("fields.sku")} value={variant.sku} />
|
||||
{variant.options.map((o) => (
|
||||
{variant.options?.map((o) => (
|
||||
<SectionRow
|
||||
key={o.id}
|
||||
title={o.option?.title}
|
||||
title={o.option?.title!}
|
||||
value={<Badge size="2xsmall">{o.value}</Badge>}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { CurrencyDollar } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
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"
|
||||
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type VariantPricesSectionProps = {
|
||||
variant: ProductVariantDTO & { prices: MoneyAmountDTO[] }
|
||||
variant: HttpTypes.AdminProductVariant
|
||||
}
|
||||
|
||||
export function VariantPricesSection({ variant }: VariantPricesSectionProps) {
|
||||
const { t } = useTranslation()
|
||||
const prices = variant.prices
|
||||
.filter((p) => !Object.keys(p.rules || {}).length) // display just currency prices
|
||||
?.filter((p) => !Object.keys(p.rules || {}).length)
|
||||
.sort((p1, p2) => p1.currency_code?.localeCompare(p2.currency_code))
|
||||
|
||||
const hasPrices = !!prices.length
|
||||
const hasPrices = !!prices?.length
|
||||
const [pageSize, setPageSize] = useState(3)
|
||||
const displayPrices = prices.slice(0, pageSize)
|
||||
const displayPrices = prices?.slice(0, pageSize)
|
||||
|
||||
const onShowMore = () => {
|
||||
setPageSize(pageSize + 3)
|
||||
@@ -46,7 +46,7 @@ export function VariantPricesSection({ variant }: VariantPricesSectionProps) {
|
||||
/>
|
||||
</div>
|
||||
{!hasPrices && <NoRecords className="h-60" />}
|
||||
{displayPrices.map((price) => {
|
||||
{displayPrices?.map((price) => {
|
||||
return (
|
||||
<div
|
||||
key={price.id}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { variantsQueryKeys } from "../../../hooks/api/products"
|
||||
import { sdk } from "../../../lib/client"
|
||||
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),
|
||||
queryKey: variantsQueryKeys.detail(variantId, {
|
||||
fields: VARIANT_DETAIL_FIELDS,
|
||||
}),
|
||||
queryFn: async () =>
|
||||
sdk.admin.product.retrieveVariant(productId, variantId, {
|
||||
fields: VARIANT_DETAIL_FIELDS,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { 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 { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { VariantGeneralSection } from "./components/variant-general-section"
|
||||
import {
|
||||
InventorySectionPlaceholder,
|
||||
VariantInventorySection,
|
||||
} from "./components/variant-inventory-section"
|
||||
import { VariantPricesSection } from "./components/variant-prices-section"
|
||||
import { VARIANT_DETAIL_FIELDS } from "./constants"
|
||||
import { variantLoader } from "./loader"
|
||||
|
||||
export const ProductVariantDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
@@ -20,15 +21,22 @@ export const ProductVariantDetail = () => {
|
||||
const { id, variant_id } = useParams()
|
||||
const { variant, isLoading, isError, error } = useProductVariant(
|
||||
id!,
|
||||
variant_id,
|
||||
variant_id!,
|
||||
{ fields: VARIANT_DETAIL_FIELDS },
|
||||
{
|
||||
initialData: initialData,
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading || !variant) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<TwoColumnPageSkeleton
|
||||
mainSections={2}
|
||||
sidebarSections={1}
|
||||
showJSON
|
||||
showMetadata
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -36,38 +44,38 @@ export const ProductVariantDetail = () => {
|
||||
}
|
||||
|
||||
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} />
|
||||
{!variant.manage_inventory ? (
|
||||
<InventorySectionPlaceholder />
|
||||
) : (
|
||||
<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>
|
||||
<TwoColumnPage
|
||||
data={variant}
|
||||
hasOutlet
|
||||
showJSON
|
||||
showMetadata
|
||||
// TODO: Add widgets zones for variant detail page
|
||||
widgets={{
|
||||
after: [],
|
||||
before: [],
|
||||
sideAfter: [],
|
||||
sideBefore: [],
|
||||
}}
|
||||
>
|
||||
<TwoColumnPage.Main>
|
||||
<VariantGeneralSection variant={variant} />
|
||||
{!variant.manage_inventory ? (
|
||||
<InventorySectionPlaceholder />
|
||||
) : (
|
||||
<VariantInventorySection
|
||||
inventoryItems={variant.inventory_items.map((i) => {
|
||||
return {
|
||||
...i.inventory,
|
||||
required_quantity: i.required_quantity,
|
||||
variant,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
<VariantPricesSection variant={variant} />
|
||||
</TwoColumnPage.Sidebar>
|
||||
</TwoColumnPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ const ProductEditVariantSchema = z.object({
|
||||
|
||||
// TODO: Either pass option ID or make the backend handle options constraints differently to handle the lack of IDs
|
||||
export const ProductEditVariantForm = ({
|
||||
product,
|
||||
variant,
|
||||
product,
|
||||
}: ProductEditVariantFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
@@ -63,7 +63,6 @@ export const ProductEditVariantForm = ({
|
||||
ean: variant.ean || "",
|
||||
upc: variant.upc || "",
|
||||
barcode: variant.barcode || "",
|
||||
inventory_quantity: variant.inventory_quantity || "",
|
||||
manage_inventory: variant.manage_inventory || false,
|
||||
allow_backorder: variant.allow_backorder || false,
|
||||
weight: variant.weight || "",
|
||||
@@ -79,7 +78,7 @@ export const ProductEditVariantForm = ({
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateProductVariant(
|
||||
product.id,
|
||||
variant.product_id!,
|
||||
variant.id
|
||||
)
|
||||
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { productsQueryKeys } from "../../../hooks/api/products"
|
||||
import { productVariantQueryKeys } from "../../../hooks/api"
|
||||
import { sdk } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
|
||||
const queryKey = (id: string) => {
|
||||
return [productsQueryKeys.detail(id)]
|
||||
const queryFn = async (id: string, variantId: string) => {
|
||||
return await sdk.admin.product.retrieveVariant(id, variantId)
|
||||
}
|
||||
|
||||
const queryFn = async (id: string) => {
|
||||
return await sdk.admin.product.retrieve(id)
|
||||
}
|
||||
|
||||
const editProductVariantQuery = (id: string) => ({
|
||||
queryKey: queryKey(id),
|
||||
queryFn: async () => queryFn(id),
|
||||
const editProductVariantQuery = (id: string, variantId: string) => ({
|
||||
queryKey: productVariantQueryKeys.detail(variantId),
|
||||
queryFn: async () => queryFn(id, variantId),
|
||||
})
|
||||
|
||||
export const editProductVariantLoader = async ({
|
||||
params,
|
||||
request,
|
||||
}: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = editProductVariantQuery(id!)
|
||||
|
||||
const searchParams = new URL(request.url).searchParams
|
||||
const searchVariantId = searchParams.get("variant_id")
|
||||
|
||||
const variantId = params.variant_id || searchVariantId
|
||||
|
||||
const query = editProductVariantQuery(id!, variantId || searchVariantId!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<ReturnType<typeof queryFn>>(query.queryKey) ??
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
json,
|
||||
useLoaderData,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from "react-router-dom"
|
||||
import { useLoaderData, useParams, useSearchParams } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { useProduct, useProductVariant } from "../../../hooks/api/products"
|
||||
import { ProductEditVariantForm } from "./components/product-edit-variant-form"
|
||||
import { editProductVariantLoader } from "./loader"
|
||||
|
||||
@@ -22,41 +16,46 @@ export const ProductVariantEdit = () => {
|
||||
const [URLSearchParms] = useSearchParams()
|
||||
const searchVariantId = URLSearchParms.get("variant_id")
|
||||
|
||||
const { product, isPending, isFetching, isError, error } = useProduct(
|
||||
const { variant, isPending, isError, error } = useProductVariant(
|
||||
id!,
|
||||
variant_id || searchVariantId!,
|
||||
undefined,
|
||||
{
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
|
||||
const variant = product?.variants.find(
|
||||
(v: HttpTypes.AdminProductVariant) =>
|
||||
v.id === (variant_id || searchVariantId)
|
||||
const {
|
||||
product,
|
||||
isPending: isProductPending,
|
||||
isError: isProductError,
|
||||
error: productError,
|
||||
} = useProduct(
|
||||
variant?.product_id!,
|
||||
{
|
||||
fields: "-variants",
|
||||
},
|
||||
{
|
||||
enabled: !!variant?.product_id,
|
||||
}
|
||||
)
|
||||
|
||||
if (!isPending && !isFetching && !variant) {
|
||||
throw json({
|
||||
status: 404,
|
||||
message: `Variant with ID ${variant_id || searchVariantId} was not found.`,
|
||||
})
|
||||
}
|
||||
const ready = !isPending && !!variant && !isProductPending && !!product
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (isProductError) {
|
||||
throw productError
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("products.variant.edit.header")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{variant && (
|
||||
<ProductEditVariantForm
|
||||
product={product}
|
||||
variant={variant as unknown as HttpTypes.AdminProductVariant}
|
||||
/>
|
||||
)}
|
||||
{ready && <ProductEditVariantForm variant={variant} product={product} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,12 +51,12 @@ export const PricingEdit = ({
|
||||
}, [regions])
|
||||
|
||||
const variants = variantId
|
||||
? product.variants.filter((v) => v.id === variantId)
|
||||
? product.variants?.filter((v) => v.id === variantId)
|
||||
: product.variants
|
||||
|
||||
const form = useForm<UpdateVariantPricesSchemaType>({
|
||||
defaultValues: {
|
||||
variants: variants.map((variant: any) => ({
|
||||
variants: variants?.map((variant: any) => ({
|
||||
title: variant.title,
|
||||
prices: variant.prices.reduce((acc: any, price: any) => {
|
||||
if (price.rules?.region_id) {
|
||||
@@ -90,11 +90,11 @@ export const PricingEdit = ({
|
||||
let existingId = undefined
|
||||
|
||||
if (regionId) {
|
||||
existingId = variants[ind].prices.find(
|
||||
existingId = variants?.[ind]?.prices?.find(
|
||||
(p) => p.rules["region_id"] === regionId
|
||||
)?.id
|
||||
} else {
|
||||
existingId = variants[ind].prices.find(
|
||||
existingId = variants?.[ind]?.prices?.find(
|
||||
(p) =>
|
||||
p.currency_code === currencyCode &&
|
||||
Object.keys(p.rules ?? {}).length === 0
|
||||
|
||||
@@ -7,7 +7,9 @@ import { PricingEdit } from "./pricing-edit"
|
||||
export const ProductPrices = () => {
|
||||
const { id, variant_id } = useParams()
|
||||
|
||||
const { product, isLoading, isError, error } = useProduct(id!)
|
||||
const { product, isLoading, isError, error } = useProduct(id!, {
|
||||
fields: "+variants,+variants.prices",
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
|
||||
Reference in New Issue
Block a user