feat: Add support for defining options when creating product (#6981)
This commit is contained in:
1 parent
bc06ad2db4
commit
e8587e9f95
10 files changed
+269
-26
No files matched your search
@@ -241,6 +241,7 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"label": "Product options",
|
"label": "Product options",
|
||||||
"hint": "Options are used to define the color, size, etc. of the product",
|
"hint": "Options are used to define the color, size, etc. of the product",
|
||||||
|
"add": "Add option",
|
||||||
"optionTitle": "Option title",
|
"optionTitle": "Option title",
|
||||||
"variations": "Variations (comma-separated)"
|
"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,
|
disabled,
|
||||||
}: ListProps<T>) => {
|
}: ListProps<T>) => {
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
return <div>No options</div>
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,6 +17,63 @@ import { queryClient } from "../../lib/medusa"
|
|||||||
const PRODUCTS_QUERY_KEY = "products" as const
|
const PRODUCTS_QUERY_KEY = "products" as const
|
||||||
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
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 = (
|
export const useProduct = (
|
||||||
id: string,
|
id: string,
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
|
|||||||
@@ -25,10 +25,34 @@ async function deleteProduct(id: string) {
|
|||||||
return deleteRequest<ProductDeleteRes>(`/admin/products/${id}`)
|
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 = {
|
export const products = {
|
||||||
retrieve: retrieveProduct,
|
retrieve: retrieveProduct,
|
||||||
list: listProducts,
|
list: listProducts,
|
||||||
create: createProduct,
|
create: createProduct,
|
||||||
update: updateProduct,
|
update: updateProduct,
|
||||||
delete: deleteProduct,
|
delete: deleteProduct,
|
||||||
|
retrieveVariant,
|
||||||
|
listVariants,
|
||||||
|
deleteVariant,
|
||||||
}
|
}
|
||||||
+28
-14
@@ -31,6 +31,7 @@ import { useCollections } from "../../../../../hooks/api/collections"
|
|||||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||||
import { useCategories } from "../../../../../hooks/api/categories"
|
import { useCategories } from "../../../../../hooks/api/categories"
|
||||||
import { useTags } from "../../../../../hooks/api/tags"
|
import { useTags } from "../../../../../hooks/api/tags"
|
||||||
|
import { Keypair } from "../../../../../components/common/keypair"
|
||||||
|
|
||||||
type CreateProductPropsProps = {
|
type CreateProductPropsProps = {
|
||||||
form: CreateProductFormReturn
|
form: CreateProductFormReturn
|
||||||
@@ -394,7 +395,14 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="options"
|
name="options"
|
||||||
render={({ field }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const normalizedValue = value.map((v) => {
|
||||||
|
return {
|
||||||
|
key: v.title,
|
||||||
|
value: v.values.join(","),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<Form.Label optional>
|
||||||
@@ -404,20 +412,26 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
{t("products.fields.options.hint")}
|
{t("products.fields.options.hint")}
|
||||||
</Form.Hint>
|
</Form.Hint>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<button
|
<Keypair
|
||||||
type="button"
|
labels={{
|
||||||
onClick={() => {
|
add: t("products.fields.options.add"),
|
||||||
field.onChange([
|
key: t("products.fields.options.optionTitle"),
|
||||||
{
|
value: t("products.fields.options.variations"),
|
||||||
title: "Color",
|
|
||||||
values: ["Red", "Blue", "Green"],
|
|
||||||
},
|
|
||||||
{ title: "Size", values: ["S", "M", "L"] },
|
|
||||||
])
|
|
||||||
}}
|
}}
|
||||||
>
|
value={normalizedValue}
|
||||||
Test
|
onChange={(newVal) =>
|
||||||
</button>
|
onChange(
|
||||||
|
newVal.map((v) => {
|
||||||
|
return {
|
||||||
|
title: v.key,
|
||||||
|
values: v.value
|
||||||
|
.split(",")
|
||||||
|
.map((v) => v.trim()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
import { Channels, PencilSquare } from "@medusajs/icons"
|
import { Channels, PencilSquare } from "@medusajs/icons"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
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 { Trans, useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
|
||||||
@@ -12,7 +12,8 @@ type ProductSalesChannelSectionProps = {
|
|||||||
export const ProductSalesChannelSection = ({
|
export const ProductSalesChannelSection = ({
|
||||||
product,
|
product,
|
||||||
}: ProductSalesChannelSectionProps) => {
|
}: ProductSalesChannelSectionProps) => {
|
||||||
const { count } = useAdminSalesChannels()
|
// const { count } = useAdminSalesChannels()
|
||||||
|
const count = 0
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const availableInSalesChannels =
|
const availableInSalesChannels =
|
||||||
|
|||||||
+2
-6
@@ -1,7 +1,6 @@
|
|||||||
import { Plus } from "@medusajs/icons"
|
import { Plus } from "@medusajs/icons"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Container, Heading } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
import { useAdminProductVariants } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
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 { useProductVariantTableColumns } from "./use-variant-table-columns"
|
||||||
import { useProductVariantTableFilters } from "./use-variant-table-filters"
|
import { useProductVariantTableFilters } from "./use-variant-table-filters"
|
||||||
import { useProductVariantTableQuery } from "./use-variant-table-query"
|
import { useProductVariantTableQuery } from "./use-variant-table-query"
|
||||||
|
import { useProductVariants } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type ProductVariantSectionProps = {
|
type ProductVariantSectionProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -25,14 +25,10 @@ export const ProductVariantSection = ({
|
|||||||
const { searchParams, raw } = useProductVariantTableQuery({
|
const { searchParams, raw } = useProductVariantTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
const { variants, count, isLoading, isError, error } =
|
const { variants, count, isLoading, isError, error } = useProductVariants(
|
||||||
useAdminProductVariants(
|
|
||||||
product.id,
|
product.id,
|
||||||
{
|
{
|
||||||
...searchParams,
|
...searchParams,
|
||||||
},
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -2,12 +2,12 @@ import { PencilSquare, Trash } from "@medusajs/icons"
|
|||||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||||
import { Badge, usePrompt } from "@medusajs/ui"
|
import { Badge, usePrompt } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useAdminDeleteVariant } from "medusa-react"
|
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { useDeleteVariant } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
const VariantActions = ({
|
const VariantActions = ({
|
||||||
variant,
|
variant,
|
||||||
@@ -16,7 +16,7 @@ const VariantActions = ({
|
|||||||
variant: ProductVariant
|
variant: ProductVariant
|
||||||
product: Product
|
product: Product
|
||||||
}) => {
|
}) => {
|
||||||
const { mutateAsync } = useAdminDeleteVariant(product.id)
|
const { mutateAsync } = useDeleteVariant(product.id, variant.id)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const VariantActions = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutateAsync(variant.id)
|
await mutateAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in new issue
Block a user