feat: Add support for defining options when creating product (#6981)
This commit is contained in:
@@ -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)"
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./keypair"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user