feat: Add product routes and components to v2 in admin-next (#6958)
This commit is contained in:
1 parent
d333db0842
commit
07fb058d96
81 files changed
+593
-140
No files matched your search
@@ -236,6 +236,22 @@
|
|||||||
},
|
},
|
||||||
"weight": {
|
"weight": {
|
||||||
"label": "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": {
|
"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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 { client } from "../../lib/client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
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
|
const PRODUCTS_QUERY_KEY = "products" as const
|
||||||
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
||||||
@@ -38,3 +49,48 @@ export const useProducts = (
|
|||||||
|
|
||||||
return { ...data, ...rest }
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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 { apiKeys } from "./api-keys"
|
||||||
import { auth } from "./auth"
|
import { auth } from "./auth"
|
||||||
|
import { categories } from "./categories"
|
||||||
import { collections } from "./collections"
|
import { collections } from "./collections"
|
||||||
import { currencies } from "./currencies"
|
import { currencies } from "./currencies"
|
||||||
import { customers } from "./customers"
|
import { customers } from "./customers"
|
||||||
@@ -11,18 +12,21 @@ import { regions } from "./regions"
|
|||||||
import { salesChannels } from "./sales-channels"
|
import { salesChannels } from "./sales-channels"
|
||||||
import { stockLocations } from "./stock-locations"
|
import { stockLocations } from "./stock-locations"
|
||||||
import { stores } from "./stores"
|
import { stores } from "./stores"
|
||||||
|
import { tags } from "./tags"
|
||||||
import { users } from "./users"
|
import { users } from "./users"
|
||||||
import { workflowExecutions } from "./workflow-executions"
|
import { workflowExecutions } from "./workflow-executions"
|
||||||
|
|
||||||
export const client = {
|
export const client = {
|
||||||
auth: auth,
|
auth: auth,
|
||||||
apiKeys: apiKeys,
|
apiKeys: apiKeys,
|
||||||
|
categories: categories,
|
||||||
customers: customers,
|
customers: customers,
|
||||||
currencies: currencies,
|
currencies: currencies,
|
||||||
collections: collections,
|
collections: collections,
|
||||||
promotions: promotions,
|
promotions: promotions,
|
||||||
stores: stores,
|
stores: stores,
|
||||||
salesChannels: salesChannels,
|
salesChannels: salesChannels,
|
||||||
|
tags: tags,
|
||||||
users: users,
|
users: users,
|
||||||
regions: regions,
|
regions: regions,
|
||||||
invites: invites,
|
invites: invites,
|
||||||
|
|||||||
@@ -1,15 +1,34 @@
|
|||||||
import { ProductListRes, ProductRes } from "../../types/api-responses"
|
import {
|
||||||
import { getRequest } from "./common"
|
ProductDeleteRes,
|
||||||
|
ProductListRes,
|
||||||
|
ProductRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
async function retrieveProduct(id: string, query?: Record<string, any>) {
|
async function retrieveProduct(id: string, query?: Record<string, any>) {
|
||||||
return getRequest<ProductRes>(`/admin/products/${id}`, query)
|
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>) {
|
async function listProducts(query?: Record<string, any>) {
|
||||||
return getRequest<ProductListRes>(`/admin/products`, query)
|
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 = {
|
export const products = {
|
||||||
retrieve: retrieveProduct,
|
retrieve: retrieveProduct,
|
||||||
list: listProducts,
|
list: listProducts,
|
||||||
|
create: createProduct,
|
||||||
|
update: updateProduct,
|
||||||
|
delete: deleteProduct,
|
||||||
}
|
}
|
||||||
@@ -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,
|
AdminDraftOrdersRes,
|
||||||
AdminGiftCardsRes,
|
AdminGiftCardsRes,
|
||||||
AdminOrdersRes,
|
AdminOrdersRes,
|
||||||
AdminProductsRes,
|
AdminRegionsRes,
|
||||||
|
AdminSalesChannelsRes,
|
||||||
|
AdminUserRes,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { Outlet, RouteObject } from "react-router-dom"
|
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",
|
path: "/categories",
|
||||||
handle: {
|
handle: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SalesChannelDTO, UserDTO } from "@medusajs/types"
|
|||||||
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
import { Spinner } from "@medusajs/icons"
|
import { Spinner } from "@medusajs/icons"
|
||||||
import { AdminCollectionsRes } from "@medusajs/medusa"
|
import { AdminCollectionsRes, AdminProductsRes } from "@medusajs/medusa"
|
||||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||||
import { MainLayout } from "../../components/layout-v2/main-layout"
|
import { MainLayout } from "../../components/layout-v2/main-layout"
|
||||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||||
@@ -66,6 +66,68 @@ export const v2Routes: RouteObject[] = [
|
|||||||
path: "/",
|
path: "/",
|
||||||
element: <MainLayout />,
|
element: <MainLayout />,
|
||||||
children: [
|
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",
|
path: "/orders",
|
||||||
handle: {
|
handle: {
|
||||||
@@ -364,7 +426,7 @@ export const v2Routes: RouteObject[] = [
|
|||||||
handle: {
|
handle: {
|
||||||
crumb: (data: ApiKeyRes) => {
|
crumb: (data: ApiKeyRes) => {
|
||||||
console.log("data", data)
|
console.log("data", data)
|
||||||
return data.apiKey.title
|
return data.api_key.title
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
+3
-3
@@ -15,10 +15,10 @@ import { MoneyAmountCell } from "../../../../../../components/table/table-cells/
|
|||||||
import { PlaceholderCell } from "../../../../../../components/table/table-cells/common/placeholder-cell"
|
import { PlaceholderCell } from "../../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
import { ProductCell } from "../../../../../../components/table/table-cells/product/product-cell"
|
import { ProductCell } from "../../../../../../components/table/table-cells/product/product-cell"
|
||||||
import { useDataTable } from "../../../../../../hooks/use-data-table"
|
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 { useCreateDraftOrder } from "../hooks"
|
||||||
import { ExistingItem } from "../types"
|
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 PAGE_SIZE = 50
|
||||||
const PREFIX = "av"
|
const PREFIX = "av"
|
||||||
@@ -193,7 +193,7 @@ const useVariantTableColumns = () => {
|
|||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const options = getValue()
|
const options = getValue()
|
||||||
|
|
||||||
const displayValue = options?.map((o) => o.value).join(" · ")
|
const displayValue = options?.map((o: any) => o.value).join(" · ")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center overflow-hidden">
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
UserDTO,
|
UserDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
|
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
|
||||||
|
import { ProductTagDTO } from "@medusajs/types/dist/product"
|
||||||
|
|
||||||
type ListRes = {
|
type ListRes = {
|
||||||
count: number
|
count: number
|
||||||
@@ -99,6 +100,14 @@ export type ProductRes = { product: ExtendedProductDTO }
|
|||||||
export type ProductListRes = { products: ExtendedProductDTO[] } & ListRes
|
export type ProductListRes = { products: ExtendedProductDTO[] } & ListRes
|
||||||
export type ProductDeleteRes = DeleteRes
|
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
|
// Product Types
|
||||||
export type ProductTypeRes = { product_type: ProductTypeDTO }
|
export type ProductTypeRes = { product_type: ProductTypeDTO }
|
||||||
export type ProductTypeListRes = { product_types: ProductTypeDTO[] } & ListRes
|
export type ProductTypeListRes = { product_types: ProductTypeDTO[] } & ListRes
|
||||||
|
|||||||
File renamed without changes.
+2
-3
@@ -1,17 +1,16 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Button, Input } from "@medusajs/ui"
|
import { Button, Input } from "@medusajs/ui"
|
||||||
import { useAdminUpdateProduct } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
import { CountrySelect } from "../../../../../components/common/country-select"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type ProductAttributesFormProps = {
|
type ProductAttributesFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -57,7 +56,7 @@ export const ProductAttributesForm = ({
|
|||||||
resolver: zodResolver(ProductAttributesSchema),
|
resolver: zodResolver(ProductAttributesSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
|
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
File renamed without changes.
+2
-2
@@ -1,16 +1,16 @@
|
|||||||
import { Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { ProductAttributesForm } from "./components/product-attributes-form"
|
import { ProductAttributesForm } from "./components/product-attributes-form"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductAttributes = () => {
|
export const ProductAttributes = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { product, isLoading, isError, error } = useAdminProduct(id!)
|
const { product, isLoading, isError, error } = useProduct(id!)
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,15 +1,15 @@
|
|||||||
import { Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { CreateProductOptionForm } from "./components/create-product-option-form"
|
import { CreateProductOptionForm } from "./components/create-product-option-form"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductCreateOption = () => {
|
export const ProductCreateOption = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { product, isLoading, isError, error } = useAdminProduct(id!)
|
const { product, isLoading, isError, error } = useProduct(id!)
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
+173
-18
@@ -13,13 +13,6 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
|
|||||||
|
|
||||||
import { SalesChannel } from "@medusajs/medusa"
|
import { SalesChannel } from "@medusajs/medusa"
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
import {
|
|
||||||
useAdminCollections,
|
|
||||||
useAdminProductCategories,
|
|
||||||
useAdminProductTags,
|
|
||||||
useAdminProductTypes,
|
|
||||||
useAdminSalesChannels,
|
|
||||||
} from "medusa-react"
|
|
||||||
import { Fragment, useMemo, useState } from "react"
|
import { Fragment, useMemo, useState } from "react"
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
import { CountrySelect } from "../../../../../components/common/country-select"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
@@ -32,6 +25,12 @@ import { useDataTable } from "../../../../../hooks/use-data-table"
|
|||||||
import { CreateProductFormReturn } from "./create-product-form"
|
import { CreateProductFormReturn } from "./create-product-form"
|
||||||
import { Combobox } from "../../../../../components/common/combobox"
|
import { Combobox } from "../../../../../components/common/combobox"
|
||||||
import { FileUpload } from "../../../../../components/common/file-upload"
|
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 = {
|
type CreateProductPropsProps = {
|
||||||
form: CreateProductFormReturn
|
form: CreateProductFormReturn
|
||||||
@@ -46,16 +45,48 @@ const SUPPORTED_FORMATS = [
|
|||||||
"image/svg+xml",
|
"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) => {
|
export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, onOpenChange] = useState(false)
|
const [open, onOpenChange] = useState(false)
|
||||||
const { product_types, isLoading: isLoadingTypes } = useAdminProductTypes()
|
const { product_types, isLoading: isLoadingTypes } = useProductTypes()
|
||||||
const { product_tags, isLoading: isLoadingTags } = useAdminProductTags()
|
const { product_tags, isLoading: isLoadingTags } = useTags()
|
||||||
const { collections, isLoading: isLoadingCollections } = useAdminCollections()
|
const { collections, isLoading: isLoadingCollections } = useCollections()
|
||||||
const { sales_channels, isLoading: isLoadingSalesChannels } =
|
const { sales_channels, isLoading: isLoadingSalesChannels } =
|
||||||
useAdminSalesChannels()
|
useSalesChannels()
|
||||||
const { product_categories, isLoading: isLoadingCategories } =
|
const { product_categories, isLoading: isLoadingCategories } = useCategories()
|
||||||
useAdminProductCategories()
|
|
||||||
|
const options = form.watch("options")
|
||||||
|
const optionPermutations = permutations(options)
|
||||||
|
|
||||||
// const { append } = useFieldArray({
|
// const { append } = useFieldArray({
|
||||||
// name: "images",
|
// name: "images",
|
||||||
@@ -202,14 +233,18 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="type_id"
|
name="type_id"
|
||||||
render={({ field }) => {
|
render={({ field: { onChange, ...field } }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<Form.Label optional>
|
||||||
{t("products.fields.type.label")}
|
{t("products.fields.type.label")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Select disabled={isLoadingTypes} {...field}>
|
<Select
|
||||||
|
disabled={isLoadingTypes}
|
||||||
|
{...field}
|
||||||
|
onValueChange={onChange}
|
||||||
|
>
|
||||||
<Select.Trigger ref={field.ref}>
|
<Select.Trigger ref={field.ref}>
|
||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
@@ -229,14 +264,18 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="collection_id"
|
name="collection_id"
|
||||||
render={({ field }) => {
|
render={({ field: { onChange, ...field } }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<Form.Label optional>
|
||||||
{t("products.fields.collection.label")}
|
{t("products.fields.collection.label")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Select disabled={isLoadingCollections} {...field}>
|
<Select
|
||||||
|
disabled={isLoadingCollections}
|
||||||
|
{...field}
|
||||||
|
onValueChange={onChange}
|
||||||
|
>
|
||||||
<Select.Trigger ref={field.ref}>
|
<Select.Trigger ref={field.ref}>
|
||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
@@ -351,6 +390,91 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div id="variants" className="flex flex-col gap-y-8">
|
<div id="variants" className="flex flex-col gap-y-8">
|
||||||
<Heading level="h2">{t("products.variants")}</Heading>
|
<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>
|
||||||
|
|
||||||
<div id="attributes" className="flex flex-col gap-y-8">
|
<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>
|
</div>
|
||||||
{/* TODO: Add missing attribute fields */}
|
|
||||||
</div>
|
</div>
|
||||||
<div id="media" className="flex flex-col gap-y-8">
|
<div id="media" className="flex flex-col gap-y-8">
|
||||||
<Heading level="h2">{t("products.media.label")}</Heading>
|
<Heading level="h2">{t("products.media.label")}</Heading>
|
||||||
+24
-9
@@ -1,6 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Button } from "@medusajs/ui"
|
import { Button } from "@medusajs/ui"
|
||||||
import { useAdminCreateProduct } from "medusa-react"
|
|
||||||
import { UseFormReturn, useForm } from "react-hook-form"
|
import { UseFormReturn, useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { CreateProductDetails } from "./create-product-details"
|
import { CreateProductDetails } from "./create-product-details"
|
||||||
|
import { useCreateProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
const CreateProductSchema = zod.object({
|
const CreateProductSchema = zod.object({
|
||||||
title: zod.string(),
|
title: zod.string(),
|
||||||
@@ -30,8 +30,16 @@ const CreateProductSchema = zod.object({
|
|||||||
weight: zod.string().optional(),
|
weight: zod.string().optional(),
|
||||||
mid_code: zod.string().optional(),
|
mid_code: zod.string().optional(),
|
||||||
hs_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(
|
variants: zod.array(
|
||||||
zod.object({
|
zod.object({
|
||||||
|
title: zod.string(),
|
||||||
|
options: zod.record(zod.string(), zod.string()),
|
||||||
variant_rank: zod.number(),
|
variant_rank: zod.number(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
@@ -55,17 +63,18 @@ export const CreateProductForm = () => {
|
|||||||
discountable: true,
|
discountable: true,
|
||||||
tags: [],
|
tags: [],
|
||||||
sales_channels: [],
|
sales_channels: [],
|
||||||
|
options: [],
|
||||||
variants: [],
|
variants: [],
|
||||||
images: [],
|
images: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateProductSchema),
|
resolver: zodResolver(CreateProductSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminCreateProduct()
|
const { mutateAsync, isLoading } = useCreateProduct()
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(
|
||||||
await mutateAsync(
|
async (values) => {
|
||||||
{
|
const reqData = {
|
||||||
...values,
|
...values,
|
||||||
is_giftcard: false,
|
is_giftcard: false,
|
||||||
tags: values.tags?.map((tag) => ({ value: tag })),
|
tags: values.tags?.map((tag) => ({ value: tag })),
|
||||||
@@ -74,15 +83,21 @@ export const CreateProductForm = () => {
|
|||||||
length: values.length ? parseFloat(values.length) : undefined,
|
length: values.length ? parseFloat(values.length) : undefined,
|
||||||
height: values.height ? parseFloat(values.height) : undefined,
|
height: values.height ? parseFloat(values.height) : undefined,
|
||||||
weight: values.weight ? parseFloat(values.weight) : 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 }) => {
|
onSuccess: ({ product }) => {
|
||||||
handleSuccess(`../${product.id}`)
|
handleSuccess(`../${product.id}`)
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.log(err)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui"
|
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui"
|
||||||
import { useAdminDeleteProduct } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useDeleteProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type ProductGeneralSectionProps = {
|
type ProductGeneralSectionProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -17,7 +17,7 @@ export const ProductGeneralSection = ({
|
|||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { mutateAsync } = useAdminDeleteProduct(product.id)
|
const { mutateAsync } = useDeleteProduct(product.id)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
File renamed without changes.
+2
-2
@@ -9,11 +9,11 @@ import {
|
|||||||
clx,
|
clx,
|
||||||
usePrompt,
|
usePrompt,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { useAdminUpdateProduct } from "medusa-react"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type ProductMedisaSectionProps = {
|
type ProductMedisaSectionProps = {
|
||||||
product: Product
|
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 handleDelete = async () => {
|
||||||
const ids = Object.keys(selection)
|
const ids = Object.keys(selection)
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-7
@@ -1,4 +1,3 @@
|
|||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
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 sideAfter from "medusa-admin:widgets/product/details/side/after"
|
||||||
import sideBefore from "medusa-admin:widgets/product/details/side/before"
|
import sideBefore from "medusa-admin:widgets/product/details/side/before"
|
||||||
import { ProductOrganizationSection } from "./components/product-organization-section"
|
import { ProductOrganizationSection } from "./components/product-organization-section"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductDetail = () => {
|
export const ProductDetail = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
@@ -22,13 +22,9 @@ export const ProductDetail = () => {
|
|||||||
>
|
>
|
||||||
|
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { product, isLoading, isError, error } = useAdminProduct(
|
const { product, isLoading, isError, error } = useProduct(id!, undefined, {
|
||||||
id!,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (isLoading || !product) {
|
if (isLoading || !product) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,15 +1,15 @@
|
|||||||
import { Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { json, useParams } from "react-router-dom"
|
import { json, useParams } from "react-router-dom"
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { CreateProductOptionForm } from "./components/edit-product-option-form"
|
import { CreateProductOptionForm } from "./components/edit-product-option-form"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductEditOption = () => {
|
export const ProductEditOption = () => {
|
||||||
const { id, option_id } = useParams()
|
const { id, option_id } = useParams()
|
||||||
const { t } = useTranslation()
|
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)
|
const option = product?.options.find((o) => o.id === option_id)
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-7
@@ -1,11 +1,11 @@
|
|||||||
import { ProductVariant } from "@medusajs/medusa"
|
import { ProductVariant } from "@medusajs/medusa"
|
||||||
import { Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { json, useLoaderData, useParams } from "react-router-dom"
|
import { json, useLoaderData, useParams } from "react-router-dom"
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { ProductEditVariantForm } from "./components/product-edit-variant-form"
|
import { ProductEditVariantForm } from "./components/product-edit-variant-form"
|
||||||
import { editProductVariantLoader } from "./loader"
|
import { editProductVariantLoader } from "./loader"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductEditVariant = () => {
|
export const ProductEditVariant = () => {
|
||||||
const loaderData = useLoaderData() as Awaited<
|
const loaderData = useLoaderData() as Awaited<
|
||||||
@@ -15,13 +15,9 @@ export const ProductEditVariant = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id, variant_id } = useParams()
|
const { id, variant_id } = useParams()
|
||||||
|
|
||||||
const { product, isLoading, isError, error } = useAdminProduct(
|
const { product, isLoading, isError, error } = useProduct(id!, undefined, {
|
||||||
id!,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
initialData: loaderData?.initialData,
|
initialData: loaderData?.initialData,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const variant = product?.variants.find(
|
const variant = product?.variants.find(
|
||||||
(v: ProductVariant) => v.id === variant_id
|
(v: ProductVariant) => v.id === variant_id
|
||||||
+2
-2
@@ -2,7 +2,6 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
|||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { ProductStatus } from "@medusajs/types"
|
import { ProductStatus } from "@medusajs/types"
|
||||||
import { Button, Input, Select, Switch, Text, Textarea } from "@medusajs/ui"
|
import { Button, Input, Select, Switch, Text, Textarea } from "@medusajs/ui"
|
||||||
import { useAdminUpdateProduct } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type EditProductFormProps = {
|
type EditProductFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -44,7 +44,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
|||||||
resolver: zodResolver(EditProductSchema),
|
resolver: zodResolver(EditProductSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
|
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,16 +1,16 @@
|
|||||||
import { Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { EditProductForm } from "./components/edit-product-form"
|
import { EditProductForm } from "./components/edit-product-form"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductEdit = () => {
|
export const ProductEdit = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { product, isLoading, isError, error } = useAdminProduct(id!)
|
const { product, isLoading, isError, error } = useProduct(id!)
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
File renamed without changes.
+6
-3
@@ -2,7 +2,6 @@ import { PencilSquare, Trash } from "@medusajs/icons"
|
|||||||
import type { Product } from "@medusajs/medusa"
|
import type { Product } from "@medusajs/medusa"
|
||||||
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useAdminDeleteProduct, useAdminProducts } from "medusa-react"
|
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, Outlet, useLoaderData } from "react-router-dom"
|
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 { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { productsLoader } from "../../loader"
|
import { productsLoader } from "../../loader"
|
||||||
|
import {
|
||||||
|
useDeleteProduct,
|
||||||
|
useProducts,
|
||||||
|
} from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -25,7 +28,7 @@ export const ProductListTable = () => {
|
|||||||
>
|
>
|
||||||
|
|
||||||
const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE })
|
const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE })
|
||||||
const { products, count, isLoading, isError, error } = useAdminProducts(
|
const { products, count, isLoading, isError, error } = useProducts(
|
||||||
{
|
{
|
||||||
...searchParams,
|
...searchParams,
|
||||||
},
|
},
|
||||||
@@ -80,7 +83,7 @@ export const ProductListTable = () => {
|
|||||||
const ProductActions = ({ product }: { product: Product }) => {
|
const ProductActions = ({ product }: { product: Product }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const { mutateAsync } = useAdminDeleteProduct(product.id)
|
const { mutateAsync } = useDeleteProduct(product.id)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -12,8 +12,8 @@ import { useCallback, useEffect, useState } from "react"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, useLocation } from "react-router-dom"
|
import { Link, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
import { useAdminUpdateProduct } from "medusa-react"
|
|
||||||
import { RouteFocusModal } from "../../../../../components/route-modal"
|
import { RouteFocusModal } from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type ProductMediaGalleryProps = {
|
type ProductMediaGalleryProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -25,7 +25,7 @@ export const ProductMediaGallery = ({ product }: ProductMediaGalleryProps) => {
|
|||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
|
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
|
||||||
|
|
||||||
const media = getMedia(product.images, product.thumbnail)
|
const media = getMedia(product.images, product.thumbnail)
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { ProductMediaView } from "./components/product-media-view"
|
import { ProductMediaView } from "./components/product-media-view"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductMedia = () => {
|
export const ProductMedia = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
|
||||||
const { product, isLoading, isError, error } = useAdminProduct(id!)
|
const { product, isLoading, isError, error } = useProduct(id!)
|
||||||
|
|
||||||
const ready = !isLoading && product
|
const ready = !isLoading && product
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
import { useAdminProduct } from "medusa-react"
|
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { EditSalesChannelsForm } from "./components/edit-sales-channels-form"
|
import { EditSalesChannelsForm } from "./components/edit-sales-channels-form"
|
||||||
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
export const ProductSalesChannels = () => {
|
export const ProductSalesChannels = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { product, isLoading, isError, error } = useAdminProduct(id!)
|
const { product, isLoading, isError, error } = useProduct(id!)
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
@@ -45,7 +45,7 @@ export const createProductsWorkflow = createWorkflow(
|
|||||||
const inputProduct = data.input.products[i]
|
const inputProduct = data.input.products[i]
|
||||||
return p.variants?.map((v, j) => ({
|
return p.variants?.map((v, j) => ({
|
||||||
...v,
|
...v,
|
||||||
prices: inputProduct?.variants?.[j]?.prices,
|
prices: inputProduct?.variants?.[j]?.prices ?? [],
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.flat()
|
.flat()
|
||||||
@@ -54,7 +54,7 @@ export const createProductsWorkflow = createWorkflow(
|
|||||||
|
|
||||||
const pricesToCreate = transform({ variantsWithAssociatedPrices }, (data) =>
|
const pricesToCreate = transform({ variantsWithAssociatedPrices }, (data) =>
|
||||||
data.variantsWithAssociatedPrices.map((v) => ({
|
data.variantsWithAssociatedPrices.map((v) => ({
|
||||||
prices: v.prices,
|
prices: v.prices ?? [],
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -509,6 +509,10 @@ export class AdminPostProductsProductVariantsReq {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
manage_inventory?: boolean = true
|
manage_inventory?: boolean = true
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
variant_rank?: number
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
weight?: number
|
weight?: number
|
||||||
@@ -538,9 +542,10 @@ export class AdminPostProductsProductVariantsReq {
|
|||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => ProductVariantPricesCreateReq)
|
@Type(() => ProductVariantPricesCreateReq)
|
||||||
prices: ProductVariantPricesCreateReq[]
|
prices?: ProductVariantPricesCreateReq[]
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@@ -588,6 +593,10 @@ export class AdminPostProductsProductVariantsVariantReq {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
manage_inventory?: boolean
|
manage_inventory?: boolean
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
variant_rank?: number
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
weight?: number
|
weight?: number
|
||||||
|
|||||||
Reference in new issue
Block a user