Skip to content

Commit

Permalink
fix(dashboard): Load product variant edit page and fix product detail…
Browse files Browse the repository at this point in the history
… 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ć <[email protected]>
  • Loading branch information
kasperkristensen and fPolic authored Nov 12, 2024
1 parent 81208b6 commit 904f092
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 116 deletions.
5 changes: 5 additions & 0 deletions .changeset/modern-walls-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---

fix(dashboard): Fix query key for product details and load product variant data correctly
13 changes: 9 additions & 4 deletions packages/admin/dashboard/src/hooks/api/products.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand Down Expand Up @@ -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,
})

Expand Down
25 changes: 12 additions & 13 deletions packages/admin/dashboard/src/lib/query-key-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,15 @@ 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,
query?: TListQuery
) => readonly [
...ReturnType<TQueryKey<TKey>["details"]>,
TDetailQuery,
{ query: TListQuery | undefined },
{ query: TListQuery }
]
}

Expand All @@ -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"
Expand All @@ -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
}
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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) {
Expand All @@ -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({
Expand Down Expand Up @@ -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>}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<
Expand All @@ -20,54 +21,61 @@ 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) {
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} />
{!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>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 || "",
Expand All @@ -79,7 +78,7 @@ export const ProductEditVariantForm = ({
})

const { mutateAsync, isPending } = useUpdateProductVariant(
product.id,
variant.product_id!,
variant.id
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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) ??
Expand Down
Loading

0 comments on commit 904f092

Please sign in to comment.