Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dashboard): Load product variant edit page and fix product detail query key #10029

Merged
merged 14 commits into from
Nov 12, 2024
Merged
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 = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add the same fix here you did for for product (include the query in query key).
If you open edit variant from product and then go to variant page it will break since pulling the variant from the same cache key.

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
8 changes: 4 additions & 4 deletions packages/admin/dashboard/src/lib/query-key-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
query?: TListQuery
) => readonly [
...ReturnType<TQueryKey<TKey>["lists"]>,
{ query: TListQuery | undefined },
{ query: TListQuery | undefined }
]
details: () => readonly [...TQueryKey<TKey>["all"], "detail"]
detail: (
Expand All @@ -16,7 +16,7 @@ export type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
) => readonly [
...ReturnType<TQueryKey<TKey>["details"]>,
TDetailQuery,
{ query: TListQuery | undefined },
{ query: TListQuery | undefined }
]
}

Expand All @@ -26,7 +26,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,7 +35,7 @@ export type UseQueryOptionsWrapper<
export const queryKeysFactory = <
T,
TListQueryType = any,
TDetailQueryType = string,
TDetailQueryType = string
>(
globalKey: T
) => {
Expand Down
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
Loading