feat: Add product routes and components to v2 in admin-next (#6958)
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./list"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
39
packages/admin-next/dashboard/src/hooks/api/categories.tsx
Normal file
39
packages/admin-next/dashboard/src/hooks/api/categories.tsx
Normal 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 }
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
39
packages/admin-next/dashboard/src/hooks/api/tags.tsx
Normal file
39
packages/admin-next/dashboard/src/hooks/api/tags.tsx
Normal 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 }
|
||||
}
|
||||
21
packages/admin-next/dashboard/src/lib/client/categories.ts
Normal file
21
packages/admin-next/dashboard/src/lib/client/categories.ts
Normal 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,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
18
packages/admin-next/dashboard/src/lib/client/tags.ts
Normal file
18
packages/admin-next/dashboard/src/lib/client/tags.ts
Normal 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,
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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}>
|
||||
@@ -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({
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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
|
||||
@@ -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({
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 ?? [],
|
||||
}))
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user