feat: Add product routes and components to v2 in admin-next (#6958)

This commit is contained in:
Stevche Radevski
2024-04-06 11:59:52 +02:00
committed by GitHub
parent d333db0842
commit 07fb058d96
81 changed files with 597 additions and 144 deletions

View File

@@ -236,6 +236,22 @@
},
"weight": {
"label": "Weight"
},
"options": {
"label": "Product options",
"hint": "Options are used to define the color, size, etc. of the product",
"optionTitle": "Option title",
"variations": "Variations (comma-separated)"
},
"variants": {
"label": "Product variants",
"hint": "Variants left unchecked won't be created, This ranking will affect how the variants are ranked in your frontend."
},
"mid_code": {
"label": "Mid code"
},
"hs_code": {
"label": "HS code"
}
},
"variant": {

View File

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

View File

@@ -0,0 +1,54 @@
import { Checkbox, Text } from "@medusajs/ui"
export interface ListProps<T> {
options: { title: string; value: T }[]
value?: T[]
onChange?: (value: T[]) => void
compare?: (a: T, b: T) => boolean
disabled?: boolean
}
export const List = <T extends any>({
options,
onChange,
value,
compare,
disabled,
}: ListProps<T>) => {
if (options.length === 0) {
return <div>No options</div>
}
return (
<div className="flex-row justify-center border divide-y rounded-lg">
{options.map((option) => {
return (
<div className="flex p-4 gap-x-4">
{onChange && value !== undefined && (
<Checkbox
disabled={disabled}
checked={value.some(
(v) => compare?.(v, option.value) ?? v === option.value
)}
onCheckedChange={(checked) => {
if (checked) {
onChange([...value, option.value])
} else {
onChange(
value.filter(
(v) =>
!(compare?.(v, option.value) ?? v === option.value)
)
)
}
}}
/>
)}
<Text key={option.title}>{option.title}</Text>
</div>
)
})}
</div>
)
}

View File

@@ -0,0 +1,39 @@
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
import { client } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { CategoriesListRes, CategoryRes } from "../../types/api-responses"
const CATEGORIES_QUERY_KEY = "categories" as const
export const categoriesQueryKeys = queryKeysFactory(CATEGORIES_QUERY_KEY)
export const useCategory = (
id: string,
options?: Omit<
UseQueryOptions<CategoryRes, Error, CategoryRes, QueryKey>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: categoriesQueryKeys.detail(id),
queryFn: async () => client.categories.retrieve(id),
...options,
})
return { ...data, ...rest }
}
export const useCategories = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<CategoriesListRes, Error, CategoriesListRes, QueryKey>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: categoriesQueryKeys.list(query),
queryFn: async () => client.categories.list(query),
...options,
})
return { ...data, ...rest }
}

View File

@@ -1,7 +1,18 @@
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
import {
QueryKey,
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery,
} from "@tanstack/react-query"
import { client } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { ProductListRes, ProductRes } from "../../types/api-responses"
import {
ProductDeleteRes,
ProductListRes,
ProductRes,
} from "../../types/api-responses"
import { queryClient } from "../../lib/medusa"
const PRODUCTS_QUERY_KEY = "products" as const
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
@@ -38,3 +49,48 @@ export const useProducts = (
return { ...data, ...rest }
}
export const useCreateProduct = (
options?: UseMutationOptions<ProductRes, Error, any>
) => {
return useMutation({
mutationFn: (payload: any) => client.products.create(payload),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateProduct = (
id: string,
options?: UseMutationOptions<ProductRes, Error, any>
) => {
return useMutation({
mutationFn: (payload: any) => client.products.update(id, payload),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: productsQueryKeys.detail(id) })
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useDeleteProduct = (
id: string,
options?: UseMutationOptions<ProductDeleteRes, Error, void>
) => {
return useMutation({
mutationFn: () => client.products.delete(id),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: productsQueryKeys.detail(id) })
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -0,0 +1,39 @@
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
import { client } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { TagsListRes, TagRes } from "../../types/api-responses"
const TAGS_QUERY_KEY = "tags" as const
export const tagsQueryKeys = queryKeysFactory(TAGS_QUERY_KEY)
export const useTag = (
id: string,
options?: Omit<
UseQueryOptions<TagRes, Error, TagRes, QueryKey>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: tagsQueryKeys.detail(id),
queryFn: async () => client.tags.retrieve(id),
...options,
})
return { ...data, ...rest }
}
export const useTags = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<TagsListRes, Error, TagsListRes, QueryKey>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: tagsQueryKeys.list(query),
queryFn: async () => client.tags.list(query),
...options,
})
return { ...data, ...rest }
}

View File

@@ -0,0 +1,21 @@
import {
ProductCollectionListRes,
ProductCollectionRes,
} from "../../types/api-responses"
import { getRequest } from "./common"
async function listProductCategories(query?: Record<string, any>) {
return getRequest<ProductCollectionListRes>(`/admin/categories`, query)
}
async function retrieveProductCategory(
id: string,
query?: Record<string, any>
) {
return getRequest<ProductCollectionRes>(`/admin/categories/${id}`, query)
}
export const categories = {
list: listProductCategories,
retrieve: retrieveProductCategory,
}

View File

@@ -1,5 +1,6 @@
import { apiKeys } from "./api-keys"
import { auth } from "./auth"
import { categories } from "./categories"
import { collections } from "./collections"
import { currencies } from "./currencies"
import { customers } from "./customers"
@@ -11,18 +12,21 @@ import { regions } from "./regions"
import { salesChannels } from "./sales-channels"
import { stockLocations } from "./stock-locations"
import { stores } from "./stores"
import { tags } from "./tags"
import { users } from "./users"
import { workflowExecutions } from "./workflow-executions"
export const client = {
auth: auth,
apiKeys: apiKeys,
categories: categories,
customers: customers,
currencies: currencies,
collections: collections,
promotions: promotions,
stores: stores,
salesChannels: salesChannels,
tags: tags,
users: users,
regions: regions,
invites: invites,

View File

@@ -1,15 +1,34 @@
import { ProductListRes, ProductRes } from "../../types/api-responses"
import { getRequest } from "./common"
import {
ProductDeleteRes,
ProductListRes,
ProductRes,
} from "../../types/api-responses"
import { deleteRequest, getRequest, postRequest } from "./common"
async function retrieveProduct(id: string, query?: Record<string, any>) {
return getRequest<ProductRes>(`/admin/products/${id}`, query)
}
async function createProduct(payload: any) {
return postRequest<ProductRes>(`/admin/products`, payload)
}
async function listProducts(query?: Record<string, any>) {
return getRequest<ProductListRes>(`/admin/products`, query)
}
async function updateProduct(id: string, payload: any) {
return postRequest<ProductRes>(`/admin/products/${id}`, payload)
}
async function deleteProduct(id: string) {
return deleteRequest<ProductDeleteRes>(`/admin/products/${id}`)
}
export const products = {
retrieve: retrieveProduct,
list: listProducts,
create: createProduct,
update: updateProduct,
delete: deleteProduct,
}

View File

@@ -0,0 +1,18 @@
import {
ProductCollectionListRes,
ProductCollectionRes,
} from "../../types/api-responses"
import { getRequest } from "./common"
async function listProductTags(query?: Record<string, any>) {
return getRequest<ProductCollectionListRes>(`/admin/tags`, query)
}
async function retrieveProductTag(id: string, query?: Record<string, any>) {
return getRequest<ProductCollectionRes>(`/admin/tags/${id}`, query)
}
export const tags = {
list: listProductTags,
retrieve: retrieveProductTag,
}

View File

@@ -5,7 +5,9 @@ import type {
AdminDraftOrdersRes,
AdminGiftCardsRes,
AdminOrdersRes,
AdminProductsRes,
AdminRegionsRes,
AdminSalesChannelsRes,
AdminUserRes,
} from "@medusajs/medusa"
import { Outlet, RouteObject } from "react-router-dom"
@@ -188,66 +190,6 @@ export const v1Routes: RouteObject[] = [
},
],
},
{
path: "/products",
handle: {
crumb: () => "Products",
},
children: [
{
path: "",
lazy: () => import("../../routes/products/product-list"),
children: [
{
path: "create",
lazy: () => import("../../routes/products/product-create"),
},
],
},
{
path: ":id",
lazy: () => import("../../routes/products/product-detail"),
handle: {
crumb: (data: AdminProductsRes) => data.product.title,
},
children: [
{
path: "edit",
lazy: () => import("../../routes/products/product-edit"),
},
{
path: "sales-channels",
lazy: () =>
import("../../routes/products/product-sales-channels"),
},
{
path: "attributes",
lazy: () =>
import("../../routes/products/product-attributes"),
},
{
path: "options/create",
lazy: () =>
import("../../routes/products/product-create-option"),
},
{
path: "options/:option_id/edit",
lazy: () =>
import("../../routes/products/product-edit-option"),
},
{
path: "media",
lazy: () => import("../../routes/products/product-media"),
},
{
path: "variants/:variant_id/edit",
lazy: () =>
import("../../routes/products/product-edit-variant"),
},
],
},
],
},
{
path: "/categories",
handle: {

View File

@@ -2,7 +2,7 @@ import { SalesChannelDTO, UserDTO } from "@medusajs/types"
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
import { Spinner } from "@medusajs/icons"
import { AdminCollectionsRes } from "@medusajs/medusa"
import { AdminCollectionsRes, AdminProductsRes } from "@medusajs/medusa"
import { ErrorBoundary } from "../../components/error/error-boundary"
import { MainLayout } from "../../components/layout-v2/main-layout"
import { SettingsLayout } from "../../components/layout/settings-layout"
@@ -66,6 +66,68 @@ export const v2Routes: RouteObject[] = [
path: "/",
element: <MainLayout />,
children: [
{
path: "/products",
handle: {
crumb: () => "Products",
},
children: [
{
path: "",
lazy: () => import("../../v2-routes/products/product-list"),
children: [
{
path: "create",
lazy: () =>
import("../../v2-routes/products/product-create"),
},
],
},
{
path: ":id",
lazy: () => import("../../v2-routes/products/product-detail"),
handle: {
crumb: (data: AdminProductsRes) => data.product.title,
},
children: [
{
path: "edit",
lazy: () => import("../../v2-routes/products/product-edit"),
},
{
path: "sales-channels",
lazy: () =>
import("../../v2-routes/products/product-sales-channels"),
},
{
path: "attributes",
lazy: () =>
import("../../v2-routes/products/product-attributes"),
},
{
path: "options/create",
lazy: () =>
import("../../v2-routes/products/product-create-option"),
},
{
path: "options/:option_id/edit",
lazy: () =>
import("../../v2-routes/products/product-edit-option"),
},
{
path: "media",
lazy: () =>
import("../../v2-routes/products/product-media"),
},
{
path: "variants/:variant_id/edit",
lazy: () =>
import("../../v2-routes/products/product-edit-variant"),
},
],
},
],
},
{
path: "/orders",
handle: {
@@ -364,7 +426,7 @@ export const v2Routes: RouteObject[] = [
handle: {
crumb: (data: ApiKeyRes) => {
console.log("data", data)
return data.apiKey.title
return data.api_key.title
},
},
children: [

View File

@@ -15,10 +15,10 @@ import { MoneyAmountCell } from "../../../../../../components/table/table-cells/
import { PlaceholderCell } from "../../../../../../components/table/table-cells/common/placeholder-cell"
import { ProductCell } from "../../../../../../components/table/table-cells/product/product-cell"
import { useDataTable } from "../../../../../../hooks/use-data-table"
import { useProductVariantTableFilters } from "../../../../../products/product-detail/components/product-variant-section/use-variant-table-filters"
import { useProductVariantTableQuery } from "../../../../../products/product-detail/components/product-variant-section/use-variant-table-query"
import { useCreateDraftOrder } from "../hooks"
import { ExistingItem } from "../types"
import { useProductVariantTableQuery } from "../../../../../../v2-routes/products/product-detail/components/product-variant-section/use-variant-table-query"
import { useProductVariantTableFilters } from "../../../../../../v2-routes/products/product-detail/components/product-variant-section/use-variant-table-filters"
const PAGE_SIZE = 50
const PREFIX = "av"
@@ -193,7 +193,7 @@ const useVariantTableColumns = () => {
cell: ({ getValue }) => {
const options = getValue()
const displayValue = options?.map((o) => o.value).join(" · ")
const displayValue = options?.map((o: any) => o.value).join(" · ")
return (
<div className="flex size-full items-center overflow-hidden">

View File

@@ -21,6 +21,7 @@ import {
UserDTO,
} from "@medusajs/types"
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
import { ProductTagDTO } from "@medusajs/types/dist/product"
type ListRes = {
count: number
@@ -99,6 +100,14 @@ export type ProductRes = { product: ExtendedProductDTO }
export type ProductListRes = { products: ExtendedProductDTO[] } & ListRes
export type ProductDeleteRes = DeleteRes
// Categories
export type CategoryRes = { category: ProductCategoryDTO }
export type CategoriesListRes = { categories: ProductCategoryDTO[] } & ListRes
// Tags
export type TagRes = { tag: ProductTagDTO }
export type TagsListRes = { tags: ProductTagDTO[] } & ListRes
// Product Types
export type ProductTypeRes = { product_type: ProductTypeDTO }
export type ProductTypeListRes = { product_types: ProductTypeDTO[] } & ListRes

View File

@@ -1,17 +1,16 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateProduct } from "medusa-react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { useUpdateProduct } from "../../../../../hooks/api/products"
type ProductAttributesFormProps = {
product: Product
@@ -57,7 +56,7 @@ export const ProductAttributesForm = ({
resolver: zodResolver(ProductAttributesSchema),
})
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(

View File

@@ -1,16 +1,16 @@
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { ProductAttributesForm } from "./components/product-attributes-form"
import { useProduct } from "../../../hooks/api/products"
export const ProductAttributes = () => {
const { id } = useParams()
const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const { product, isLoading, isError, error } = useProduct(id!)
if (isError) {
throw error

View File

@@ -1,15 +1,15 @@
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { CreateProductOptionForm } from "./components/create-product-option-form"
import { useProduct } from "../../../hooks/api/products"
export const ProductCreateOption = () => {
const { id } = useParams()
const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const { product, isLoading, isError, error } = useProduct(id!)
if (isError) {
throw error

View File

@@ -13,13 +13,6 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
import { SalesChannel } from "@medusajs/medusa"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import {
useAdminCollections,
useAdminProductCategories,
useAdminProductTags,
useAdminProductTypes,
useAdminSalesChannels,
} from "medusa-react"
import { Fragment, useMemo, useState } from "react"
import { CountrySelect } from "../../../../../components/common/country-select"
import { Form } from "../../../../../components/common/form"
@@ -32,6 +25,12 @@ import { useDataTable } from "../../../../../hooks/use-data-table"
import { CreateProductFormReturn } from "./create-product-form"
import { Combobox } from "../../../../../components/common/combobox"
import { FileUpload } from "../../../../../components/common/file-upload"
import { List } from "../../../../../components/common/list"
import { useProductTypes } from "../../../../../hooks/api/product-types"
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"
type CreateProductPropsProps = {
form: CreateProductFormReturn
@@ -46,16 +45,48 @@ const SUPPORTED_FORMATS = [
"image/svg+xml",
]
const permutations = (
data: { title: string; values: string[] }[]
): { [key: string]: string }[] => {
if (data.length === 0) {
return []
}
if (data.length === 1) {
return data[0].values.map((value) => ({ [data[0].title]: value }))
}
const toProcess = data[0]
const rest = data.slice(1)
return toProcess.values.flatMap((value) => {
return permutations(rest).map((permutation) => {
return {
[toProcess.title]: value,
...permutation,
}
})
})
}
const generateNameFromPermutation = (permutation: {
[key: string]: string
}) => {
return Object.values(permutation).join(" / ")
}
export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
const { t } = useTranslation()
const [open, onOpenChange] = useState(false)
const { product_types, isLoading: isLoadingTypes } = useAdminProductTypes()
const { product_tags, isLoading: isLoadingTags } = useAdminProductTags()
const { collections, isLoading: isLoadingCollections } = useAdminCollections()
const { product_types, isLoading: isLoadingTypes } = useProductTypes()
const { product_tags, isLoading: isLoadingTags } = useTags()
const { collections, isLoading: isLoadingCollections } = useCollections()
const { sales_channels, isLoading: isLoadingSalesChannels } =
useAdminSalesChannels()
const { product_categories, isLoading: isLoadingCategories } =
useAdminProductCategories()
useSalesChannels()
const { product_categories, isLoading: isLoadingCategories } = useCategories()
const options = form.watch("options")
const optionPermutations = permutations(options)
// const { append } = useFieldArray({
// name: "images",
@@ -202,14 +233,18 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
<Form.Field
control={form.control}
name="type_id"
render={({ field }) => {
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label optional>
{t("products.fields.type.label")}
</Form.Label>
<Form.Control>
<Select disabled={isLoadingTypes} {...field}>
<Select
disabled={isLoadingTypes}
{...field}
onValueChange={onChange}
>
<Select.Trigger ref={field.ref}>
<Select.Value />
</Select.Trigger>
@@ -229,14 +264,18 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
<Form.Field
control={form.control}
name="collection_id"
render={({ field }) => {
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label optional>
{t("products.fields.collection.label")}
</Form.Label>
<Form.Control>
<Select disabled={isLoadingCollections} {...field}>
<Select
disabled={isLoadingCollections}
{...field}
onValueChange={onChange}
>
<Select.Trigger ref={field.ref}>
<Select.Value />
</Select.Trigger>
@@ -351,6 +390,91 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
</div>
<div id="variants" className="flex flex-col gap-y-8">
<Heading level="h2">{t("products.variants")}</Heading>
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
<Form.Field
control={form.control}
name="options"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>
{t("products.fields.options.label")}
</Form.Label>
<Form.Hint>
{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"] },
])
}}
>
Test
</button>
</Form.Control>
</Form.Item>
)
}}
/>
</div>
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
<Form.Field
control={form.control}
name="variants"
render={({ field: { value, onChange, ...field } }) => {
const selectedOptions = value.map((v) => v.options)
return (
<Form.Item>
<Form.Label optional>
{t("products.fields.variants.label")}
</Form.Label>
<Form.Hint>
{t("products.fields.variants.hint")}
</Form.Hint>
<Form.Control>
<List
{...field}
value={selectedOptions}
onChange={(v) => {
onChange(
v.map((options, i) => {
return {
title:
generateNameFromPermutation(options),
variant_rank: i,
options,
prices: [],
}
})
)
}}
compare={(a, b) => {
return (
generateNameFromPermutation(a) ===
generateNameFromPermutation(b)
)
}}
options={optionPermutations.map((opt) => {
return {
title: generateNameFromPermutation(opt),
value: opt,
}
})}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
<div id="attributes" className="flex flex-col gap-y-8">
@@ -454,8 +578,39 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
)
}}
/>
<Form.Field
control={form.control}
name="mid_code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>
{t("products.fields.mid_code.label")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="hs_code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>
{t("products.fields.hs_code.label")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
</Form.Item>
)
}}
/>
</div>
{/* TODO: Add missing attribute fields */}
</div>
<div id="media" className="flex flex-col gap-y-8">
<Heading level="h2">{t("products.media.label")}</Heading>

View File

@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button } from "@medusajs/ui"
import { useAdminCreateProduct } from "medusa-react"
import { UseFormReturn, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -10,6 +9,7 @@ import {
useRouteModal,
} from "../../../../../components/route-modal"
import { CreateProductDetails } from "./create-product-details"
import { useCreateProduct } from "../../../../../hooks/api/products"
const CreateProductSchema = zod.object({
title: zod.string(),
@@ -30,8 +30,16 @@ const CreateProductSchema = zod.object({
weight: zod.string().optional(),
mid_code: zod.string().optional(),
hs_code: zod.string().optional(),
options: zod.array(
zod.object({
title: zod.string(),
values: zod.array(zod.string()),
})
),
variants: zod.array(
zod.object({
title: zod.string(),
options: zod.record(zod.string(), zod.string()),
variant_rank: zod.number(),
})
),
@@ -55,17 +63,18 @@ export const CreateProductForm = () => {
discountable: true,
tags: [],
sales_channels: [],
options: [],
variants: [],
images: [],
},
resolver: zodResolver(CreateProductSchema),
})
const { mutateAsync, isLoading } = useAdminCreateProduct()
const { mutateAsync, isLoading } = useCreateProduct()
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
{
const handleSubmit = form.handleSubmit(
async (values) => {
const reqData = {
...values,
is_giftcard: false,
tags: values.tags?.map((tag) => ({ value: tag })),
@@ -74,15 +83,21 @@ export const CreateProductForm = () => {
length: values.length ? parseFloat(values.length) : undefined,
height: values.height ? parseFloat(values.height) : undefined,
weight: values.weight ? parseFloat(values.weight) : undefined,
variants: values.variants.map((v) => ({ title: "", prices: [] })),
},
{
variants: values.variants,
} as any
delete reqData.sales_channels
await mutateAsync(reqData, {
onSuccess: ({ product }) => {
handleSuccess(`../${product.id}`)
},
}
)
})
})
},
(err) => {
console.log(err)
}
)
return (
<RouteFocusModal.Form form={form}>

View File

@@ -1,10 +1,10 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { Product } from "@medusajs/medusa"
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui"
import { useAdminDeleteProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteProduct } from "../../../../../hooks/api/products"
type ProductGeneralSectionProps = {
product: Product
@@ -17,7 +17,7 @@ export const ProductGeneralSection = ({
const prompt = usePrompt()
const navigate = useNavigate()
const { mutateAsync } = useAdminDeleteProduct(product.id)
const { mutateAsync } = useDeleteProduct(product.id)
const handleDelete = async () => {
const res = await prompt({

View File

@@ -9,11 +9,11 @@ import {
clx,
usePrompt,
} from "@medusajs/ui"
import { useAdminUpdateProduct } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useUpdateProduct } from "../../../../../hooks/api/products"
type ProductMedisaSectionProps = {
product: Product
@@ -37,7 +37,7 @@ export const ProductMediaSection = ({ product }: ProductMedisaSectionProps) => {
})
}
const { mutateAsync } = useAdminUpdateProduct(product.id)
const { mutateAsync } = useUpdateProduct(product.id)
const handleDelete = async () => {
const ids = Object.keys(selection)

View File

@@ -1,4 +1,3 @@
import { useAdminProduct } from "medusa-react"
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
@@ -15,6 +14,7 @@ import before from "medusa-admin:widgets/product/details/before"
import sideAfter from "medusa-admin:widgets/product/details/side/after"
import sideBefore from "medusa-admin:widgets/product/details/side/before"
import { ProductOrganizationSection } from "./components/product-organization-section"
import { useProduct } from "../../../hooks/api/products"
export const ProductDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -22,13 +22,9 @@ export const ProductDetail = () => {
>
const { id } = useParams()
const { product, isLoading, isError, error } = useAdminProduct(
id!,
undefined,
{
initialData: initialData,
}
)
const { product, isLoading, isError, error } = useProduct(id!, undefined, {
initialData: initialData,
})
if (isLoading || !product) {
return <div>Loading...</div>

View File

@@ -1,15 +1,15 @@
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import { json, useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { CreateProductOptionForm } from "./components/edit-product-option-form"
import { useProduct } from "../../../hooks/api/products"
export const ProductEditOption = () => {
const { id, option_id } = useParams()
const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const { product, isLoading, isError, error } = useProduct(id!)
const option = product?.options.find((o) => o.id === option_id)

View File

@@ -1,11 +1,11 @@
import { ProductVariant } from "@medusajs/medusa"
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import { json, useLoaderData, useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { ProductEditVariantForm } from "./components/product-edit-variant-form"
import { editProductVariantLoader } from "./loader"
import { useProduct } from "../../../hooks/api/products"
export const ProductEditVariant = () => {
const loaderData = useLoaderData() as Awaited<
@@ -15,13 +15,9 @@ export const ProductEditVariant = () => {
const { t } = useTranslation()
const { id, variant_id } = useParams()
const { product, isLoading, isError, error } = useAdminProduct(
id!,
undefined,
{
initialData: loaderData?.initialData,
}
)
const { product, isLoading, isError, error } = useProduct(id!, undefined, {
initialData: loaderData?.initialData,
})
const variant = product?.variants.find(
(v: ProductVariant) => v.id === variant_id

View File

@@ -2,7 +2,6 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { Product } from "@medusajs/medusa"
import { ProductStatus } from "@medusajs/types"
import { Button, Input, Select, Switch, Text, Textarea } from "@medusajs/ui"
import { useAdminUpdateProduct } from "medusa-react"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -12,6 +11,7 @@ import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { useUpdateProduct } from "../../../../../hooks/api/products"
type EditProductFormProps = {
product: Product
@@ -44,7 +44,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
resolver: zodResolver(EditProductSchema),
})
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(

View File

@@ -1,16 +1,16 @@
import { Heading } from "@medusajs/ui"
import { useAdminProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditProductForm } from "./components/edit-product-form"
import { useProduct } from "../../../hooks/api/products"
export const ProductEdit = () => {
const { id } = useParams()
const { t } = useTranslation()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const { product, isLoading, isError, error } = useProduct(id!)
if (isError) {
throw error

View File

@@ -2,7 +2,6 @@ import { PencilSquare, Trash } from "@medusajs/icons"
import type { Product } from "@medusajs/medusa"
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteProduct, useAdminProducts } from "medusa-react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link, Outlet, useLoaderData } from "react-router-dom"
@@ -14,6 +13,10 @@ import { useProductTableFilters } from "../../../../../hooks/table/filters/use-p
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { productsLoader } from "../../loader"
import {
useDeleteProduct,
useProducts,
} from "../../../../../hooks/api/products"
const PAGE_SIZE = 20
@@ -25,7 +28,7 @@ export const ProductListTable = () => {
>
const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE })
const { products, count, isLoading, isError, error } = useAdminProducts(
const { products, count, isLoading, isError, error } = useProducts(
{
...searchParams,
},
@@ -80,7 +83,7 @@ export const ProductListTable = () => {
const ProductActions = ({ product }: { product: Product }) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useAdminDeleteProduct(product.id)
const { mutateAsync } = useDeleteProduct(product.id)
const handleDelete = async () => {
const res = await prompt({

View File

@@ -12,8 +12,8 @@ import { useCallback, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, useLocation } from "react-router-dom"
import { useAdminUpdateProduct } from "medusa-react"
import { RouteFocusModal } from "../../../../../components/route-modal"
import { useUpdateProduct } from "../../../../../hooks/api/products"
type ProductMediaGalleryProps = {
product: Product
@@ -25,7 +25,7 @@ export const ProductMediaGallery = ({ product }: ProductMediaGalleryProps) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
const media = getMedia(product.images, product.thumbnail)

View File

@@ -1,12 +1,12 @@
import { useAdminProduct } from "medusa-react"
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { ProductMediaView } from "./components/product-media-view"
import { useProduct } from "../../../hooks/api/products"
export const ProductMedia = () => {
const { id } = useParams()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const { product, isLoading, isError, error } = useProduct(id!)
const ready = !isLoading && product

View File

@@ -1,12 +1,12 @@
import { useAdminProduct } from "medusa-react"
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { EditSalesChannelsForm } from "./components/edit-sales-channels-form"
import { useProduct } from "../../../hooks/api/products"
export const ProductSalesChannels = () => {
const { id } = useParams()
const { product, isLoading, isError, error } = useAdminProduct(id!)
const { product, isLoading, isError, error } = useProduct(id!)
if (isError) {
throw error

View File

@@ -45,7 +45,7 @@ export const createProductsWorkflow = createWorkflow(
const inputProduct = data.input.products[i]
return p.variants?.map((v, j) => ({
...v,
prices: inputProduct?.variants?.[j]?.prices,
prices: inputProduct?.variants?.[j]?.prices ?? [],
}))
})
.flat()
@@ -54,7 +54,7 @@ export const createProductsWorkflow = createWorkflow(
const pricesToCreate = transform({ variantsWithAssociatedPrices }, (data) =>
data.variantsWithAssociatedPrices.map((v) => ({
prices: v.prices,
prices: v.prices ?? [],
}))
)

View File

@@ -509,6 +509,10 @@ export class AdminPostProductsProductVariantsReq {
@IsOptional()
manage_inventory?: boolean = true
@IsNumber()
@IsOptional()
variant_rank?: number
@IsNumber()
@IsOptional()
weight?: number
@@ -538,9 +542,10 @@ export class AdminPostProductsProductVariantsReq {
metadata?: Record<string, unknown>
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => ProductVariantPricesCreateReq)
prices: ProductVariantPricesCreateReq[]
prices?: ProductVariantPricesCreateReq[]
@IsOptional()
@IsObject()
@@ -588,6 +593,10 @@ export class AdminPostProductsProductVariantsVariantReq {
@IsOptional()
manage_inventory?: boolean
@IsNumber()
@IsOptional()
variant_rank?: number
@IsNumber()
@IsOptional()
weight?: number