feat: Add support for defining options when creating product (#6981)

This commit is contained in:
Stevche Radevski
2024-04-06 18:58:53 +02:00
committed by GitHub
parent bc06ad2db4
commit e8587e9f95
10 changed files with 274 additions and 31 deletions

View File

@@ -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)"
},

View File

@@ -0,0 +1 @@
export * from "./keypair"

View File

@@ -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 (
<div>
<Table className="w-full">
<Table.Header className="border-t-0">
<Table.Row>
<Table.HeaderCell>{labels.key}</Table.HeaderCell>
<Table.HeaderCell>{labels.value}</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{value.map((pair, index) => {
return (
<Field
labels={labels}
field={pair}
updateKey={onKeyChange(index)}
updateValue={onValueChange(index)}
onDelete={deleteKeyPair(index)}
/>
)
})}
</Table.Body>
</Table>
<Button
variant="secondary"
size="small"
type="button"
className="w-full mt-4"
onClick={addKeyPair}
>
<Plus />
{labels.add}
</Button>
</div>
)
}
type FieldProps = {
field: KeyPair
labels: {
key?: string
value?: string
}
updateKey: (key: string) => void
updateValue: (value: string) => void
onDelete: () => void
}
const Field: React.FC<FieldProps> = ({
field,
updateKey,
updateValue,
onDelete,
}) => {
const [key, setKey] = useState(field.key)
const [value, setValue] = useState(field.value)
return (
<Table.Row>
<Table.Cell className="!p-0 h-0">
<Input
className="rounded-none bg-transparent"
onBlur={() => updateKey(key)}
value={key}
onChange={(e) => {
setKey(e.currentTarget.value)
}}
/>
</Table.Cell>
<Table.Cell className="!p-0 h-0">
<Input
className="rounded-none bg-transparent"
onBlur={() => updateValue(value)}
value={value}
onChange={(e) => {
setValue(e.currentTarget.value)
}}
/>
</Table.Cell>
<Table.Cell className="!p-0 h-0 border-r">
<Button
variant="transparent"
size="small"
type="button"
onClick={onDelete}
>
<Trash />
</Button>
</Table.Cell>
</Table.Row>
)
}

View File

@@ -16,7 +16,7 @@ export const List = <T extends any>({
disabled,
}: ListProps<T>) => {
if (options.length === 0) {
return <div>No options</div>
return null
}
return (

View File

@@ -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<string, any>,
options?: Omit<
UseQueryOptions<any, Error, any, QueryKey>,
"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<string, any>,
options?: Omit<
UseQueryOptions<any, Error, any, QueryKey>,
"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<any, Error, void>
) => {
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<string, any>,

View File

@@ -25,10 +25,34 @@ async function deleteProduct(id: string) {
return deleteRequest<ProductDeleteRes>(`/admin/products/${id}`)
}
async function retrieveVariant(
productId: string,
variantId: string,
query?: Record<string, any>
) {
return getRequest<any>(
`/admin/products/${productId}/variants/${variantId}`,
query
)
}
async function listVariants(productId: string, query?: Record<string, any>) {
return getRequest<any>(`/admin/products/${productId}/variants`, query)
}
async function deleteVariant(productId: string, variantId: string) {
return deleteRequest<any>(
`/admin/products/${productId}/variants/${variantId}`
)
}
export const products = {
retrieve: retrieveProduct,
list: listProducts,
create: createProduct,
update: updateProduct,
delete: deleteProduct,
retrieveVariant,
listVariants,
deleteVariant,
}

View File

@@ -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) => {
<Form.Field
control={form.control}
name="options"
render={({ field }) => {
render={({ field: { onChange, value } }) => {
const normalizedValue = value.map((v) => {
return {
key: v.title,
value: v.values.join(","),
}
})
return (
<Form.Item>
<Form.Label optional>
@@ -404,20 +412,26 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
{t("products.fields.options.hint")}
</Form.Hint>
<Form.Control>
<button
type="button"
onClick={() => {
field.onChange([
{
title: "Color",
values: ["Red", "Blue", "Green"],
},
{ title: "Size", values: ["S", "M", "L"] },
])
<Keypair
labels={{
add: t("products.fields.options.add"),
key: t("products.fields.options.optionTitle"),
value: t("products.fields.options.variations"),
}}
>
Test
</button>
value={normalizedValue}
onChange={(newVal) =>
onChange(
newVal.map((v) => {
return {
title: v.key,
values: v.value
.split(",")
.map((v) => v.trim()),
}
})
)
}
/>
</Form.Control>
</Form.Item>
)

View File

@@ -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 =

View File

@@ -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)

View File

@@ -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 (