From e8587e9f957ea901b7e45d956175234af9c93dc5 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Sat, 6 Apr 2024 18:58:53 +0200 Subject: [PATCH] feat: Add support for defining options when creating product (#6981) --- .../public/locales/en-US/translation.json | 1 + .../src/components/common/keypair/index.tsx | 1 + .../src/components/common/keypair/keypair.tsx | 149 ++++++++++++++++++ .../src/components/common/list/list.tsx | 2 +- .../dashboard/src/hooks/api/products.tsx | 57 +++++++ .../dashboard/src/lib/client/products.ts | 24 +++ .../create-product-details.tsx | 42 +++-- .../product-sales-channel-section.tsx | 5 +- .../product-variant-section.tsx | 18 +-- .../use-variant-table-columns.tsx | 6 +- 10 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 packages/admin-next/dashboard/src/components/common/keypair/index.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/keypair/keypair.tsx diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index cfc472c920..a6cfc68e25 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -241,6 +241,7 @@ "options": { "label": "Product options", "hint": "Options are used to define the color, size, etc. of the product", + "add": "Add option", "optionTitle": "Option title", "variations": "Variations (comma-separated)" }, diff --git a/packages/admin-next/dashboard/src/components/common/keypair/index.tsx b/packages/admin-next/dashboard/src/components/common/keypair/index.tsx new file mode 100644 index 0000000000..5215aa1988 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/keypair/index.tsx @@ -0,0 +1 @@ +export * from "./keypair" diff --git a/packages/admin-next/dashboard/src/components/common/keypair/keypair.tsx b/packages/admin-next/dashboard/src/components/common/keypair/keypair.tsx new file mode 100644 index 0000000000..160b02000d --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/keypair/keypair.tsx @@ -0,0 +1,149 @@ +import { Plus, Trash } from "@medusajs/icons" +import { Button, Input, Table } from "@medusajs/ui" +import { useState } from "react" + +interface KeyPair { + key: string + value: string +} + +export interface KeypairProps { + labels: { + add: string + key?: string + value?: string + } + value: KeyPair[] + onChange: (value: KeyPair[]) => void + disabled?: boolean +} + +export const Keypair = ({ labels, onChange, value }: KeypairProps) => { + const addKeyPair = () => { + onChange([...value, { key: ``, value: `` }]) + } + + const deleteKeyPair = (index: number) => { + return () => { + onChange(value.filter((_, i) => i !== index)) + } + } + + const onKeyChange = (index: number) => { + return (key: string) => { + const newArr = value.map((pair, i) => { + if (i === index) { + return { key, value: pair.value } + } + return pair + }) + + onChange(newArr) + } + } + + const onValueChange = (index: number) => { + return (val: string) => { + const newArr = value.map((pair, i) => { + if (i === index) { + return { key: pair.key, value: val } + } + return pair + }) + + onChange(newArr) + } + } + + return ( +
+ + + + {labels.key} + {labels.value} + + + + {value.map((pair, index) => { + return ( + + ) + })} + +
+ +
+ ) +} + +type FieldProps = { + field: KeyPair + labels: { + key?: string + value?: string + } + updateKey: (key: string) => void + updateValue: (value: string) => void + onDelete: () => void +} + +const Field: React.FC = ({ + field, + updateKey, + updateValue, + onDelete, +}) => { + const [key, setKey] = useState(field.key) + const [value, setValue] = useState(field.value) + + return ( + + + updateKey(key)} + value={key} + onChange={(e) => { + setKey(e.currentTarget.value) + }} + /> + + + updateValue(value)} + value={value} + onChange={(e) => { + setValue(e.currentTarget.value) + }} + /> + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/list/list.tsx b/packages/admin-next/dashboard/src/components/common/list/list.tsx index 4817c4cb8c..a058177d05 100644 --- a/packages/admin-next/dashboard/src/components/common/list/list.tsx +++ b/packages/admin-next/dashboard/src/components/common/list/list.tsx @@ -16,7 +16,7 @@ export const List = ({ disabled, }: ListProps) => { if (options.length === 0) { - return
No options
+ return null } return ( diff --git a/packages/admin-next/dashboard/src/hooks/api/products.tsx b/packages/admin-next/dashboard/src/hooks/api/products.tsx index a84209c591..00cb49b1b4 100644 --- a/packages/admin-next/dashboard/src/hooks/api/products.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/products.tsx @@ -17,6 +17,63 @@ import { queryClient } from "../../lib/medusa" const PRODUCTS_QUERY_KEY = "products" as const export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY) +const VARIANTS_QUERY_KEY = "product_variants" as const +export const variantsQueryKeys = queryKeysFactory(VARIANTS_QUERY_KEY) + +export const useProductVariant = ( + productId: string, + variantId: string, + query?: Record, + options?: Omit< + UseQueryOptions, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => client.products.retrieveVariant(productId, variantId, query), + queryKey: variantsQueryKeys.detail(variantId), + ...options, + }) + + return { ...data, ...rest } +} + +export const useProductVariants = ( + productId: string, + query?: Record, + options?: Omit< + UseQueryOptions, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => client.products.listVariants(productId, query), + queryKey: variantsQueryKeys.list(query), + ...options, + }) + + return { ...data, ...rest } +} + +export const useDeleteVariant = ( + productId: string, + variantId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: () => client.products.deleteVariant(productId, variantId), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: variantsQueryKeys.detail(variantId), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useProduct = ( id: string, query?: Record, diff --git a/packages/admin-next/dashboard/src/lib/client/products.ts b/packages/admin-next/dashboard/src/lib/client/products.ts index 8871fffdd7..6341954f9d 100644 --- a/packages/admin-next/dashboard/src/lib/client/products.ts +++ b/packages/admin-next/dashboard/src/lib/client/products.ts @@ -25,10 +25,34 @@ async function deleteProduct(id: string) { return deleteRequest(`/admin/products/${id}`) } +async function retrieveVariant( + productId: string, + variantId: string, + query?: Record +) { + return getRequest( + `/admin/products/${productId}/variants/${variantId}`, + query + ) +} + +async function listVariants(productId: string, query?: Record) { + return getRequest(`/admin/products/${productId}/variants`, query) +} + +async function deleteVariant(productId: string, variantId: string) { + return deleteRequest( + `/admin/products/${productId}/variants/${variantId}` + ) +} + export const products = { retrieve: retrieveProduct, list: listProducts, create: createProduct, update: updateProduct, delete: deleteProduct, + retrieveVariant, + listVariants, + deleteVariant, } diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-details.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-details.tsx index b3b657677e..07a3374c84 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-details.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-create/components/create-product-form/create-product-details.tsx @@ -31,6 +31,7 @@ import { useCollections } from "../../../../../hooks/api/collections" import { useSalesChannels } from "../../../../../hooks/api/sales-channels" import { useCategories } from "../../../../../hooks/api/categories" import { useTags } from "../../../../../hooks/api/tags" +import { Keypair } from "../../../../../components/common/keypair" type CreateProductPropsProps = { form: CreateProductFormReturn @@ -394,7 +395,14 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { { + render={({ field: { onChange, value } }) => { + const normalizedValue = value.map((v) => { + return { + key: v.title, + value: v.values.join(","), + } + }) + return ( @@ -404,20 +412,26 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { {t("products.fields.options.hint")} - + value={normalizedValue} + onChange={(newVal) => + onChange( + newVal.map((v) => { + return { + title: v.key, + values: v.value + .split(",") + .map((v) => v.trim()), + } + }) + ) + } + /> ) diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx index e67ee16ffc..55d6faf381 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx @@ -1,7 +1,7 @@ import { Channels, PencilSquare } from "@medusajs/icons" import { Product } from "@medusajs/medusa" import { Container, Heading, Text, Tooltip } from "@medusajs/ui" -import { useAdminSalesChannels } from "medusa-react" +// import { useAdminSalesChannels } from "medusa-react" import { Trans, useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" @@ -12,7 +12,8 @@ type ProductSalesChannelSectionProps = { export const ProductSalesChannelSection = ({ product, }: ProductSalesChannelSectionProps) => { - const { count } = useAdminSalesChannels() + // const { count } = useAdminSalesChannels() + const count = 0 const { t } = useTranslation() const availableInSalesChannels = diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx index f2e11ee1c2..9bf91cd15c 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/product-variant-section.tsx @@ -1,7 +1,6 @@ import { Plus } from "@medusajs/icons" import { Product } from "@medusajs/medusa" import { Container, Heading } from "@medusajs/ui" -import { useAdminProductVariants } from "medusa-react" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" @@ -10,6 +9,7 @@ import { useDataTable } from "../../../../../hooks/use-data-table" import { useProductVariantTableColumns } from "./use-variant-table-columns" import { useProductVariantTableFilters } from "./use-variant-table-filters" import { useProductVariantTableQuery } from "./use-variant-table-query" +import { useProductVariants } from "../../../../../hooks/api/products" type ProductVariantSectionProps = { product: Product @@ -25,16 +25,12 @@ export const ProductVariantSection = ({ const { searchParams, raw } = useProductVariantTableQuery({ pageSize: PAGE_SIZE, }) - const { variants, count, isLoading, isError, error } = - useAdminProductVariants( - product.id, - { - ...searchParams, - }, - { - keepPreviousData: true, - } - ) + const { variants, count, isLoading, isError, error } = useProductVariants( + product.id, + { + ...searchParams, + } + ) const filters = useProductVariantTableFilters() const columns = useProductVariantTableColumns(product) diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/use-variant-table-columns.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/use-variant-table-columns.tsx index 836c87fe2d..790196524d 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/use-variant-table-columns.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-detail/components/product-variant-section/use-variant-table-columns.tsx @@ -2,12 +2,12 @@ import { PencilSquare, Trash } from "@medusajs/icons" import { Product, ProductVariant } from "@medusajs/medusa" import { Badge, usePrompt } from "@medusajs/ui" import { createColumnHelper } from "@tanstack/react-table" -import { useAdminDeleteVariant } from "medusa-react" import { useMemo } from "react" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" +import { useDeleteVariant } from "../../../../../hooks/api/products" const VariantActions = ({ variant, @@ -16,7 +16,7 @@ const VariantActions = ({ variant: ProductVariant product: Product }) => { - const { mutateAsync } = useAdminDeleteVariant(product.id) + const { mutateAsync } = useDeleteVariant(product.id, variant.id) const { t } = useTranslation() const prompt = usePrompt() @@ -34,7 +34,7 @@ const VariantActions = ({ return } - await mutateAsync(variant.id) + await mutateAsync() } return (