feat: Categories retrieve + list API (#7009)
This commit is contained in:
1 parent
db2f0ef53f
commit
6cc9a5e469
41 files changed
+1176
-35
No files matched your search
@@ -0,0 +1,79 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { createAdminUser } from "../../../helpers/create-admin-user"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||||
|
const adminHeaders = {
|
||||||
|
headers: { "x-medusa-access-token": "test_token" },
|
||||||
|
}
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
env,
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
describe("Product categories - Admin", () => {
|
||||||
|
let container
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
container = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should correctly query categories by q", async () => {
|
||||||
|
const productService = container.resolve("productModuleService")
|
||||||
|
const categoryOne = await productService.createCategory({
|
||||||
|
name: "Category One",
|
||||||
|
})
|
||||||
|
const categoryTwo = await productService.createCategory({
|
||||||
|
name: "Category Two",
|
||||||
|
parent_category_id: categoryOne.id,
|
||||||
|
})
|
||||||
|
const categoryThree = await productService.createCategory({
|
||||||
|
name: "Category Three",
|
||||||
|
parent_category_id: categoryTwo.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
"/admin/product-categories?q=Category",
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.product_categories).toHaveLength(3)
|
||||||
|
expect(response.data.product_categories).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: categoryOne.id,
|
||||||
|
name: "Category One",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: categoryTwo.id,
|
||||||
|
name: "Category Two",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: categoryThree.id,
|
||||||
|
name: "Category Three",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const responseTwo = await api.get(
|
||||||
|
"/admin/product-categories?q=three",
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(responseTwo.status).toEqual(200)
|
||||||
|
expect(responseTwo.data.product_categories).toHaveLength(1)
|
||||||
|
expect(responseTwo.data.product_categories).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: categoryThree.id,
|
||||||
|
name: "Category Three",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -293,7 +293,20 @@
|
|||||||
"removeProductsWarning_other": "You are about to remove {{count}} products from the collection. This action cannot be undone."
|
"removeProductsWarning_other": "You are about to remove {{count}} products from the collection. This action cannot be undone."
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"domain": "Categories"
|
"domain": "Categories",
|
||||||
|
"organization": {
|
||||||
|
"header": "Organization",
|
||||||
|
"pathLabel": "Path",
|
||||||
|
"pathExpandTooltip": "Show full path",
|
||||||
|
"childrenLabel": "Children"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"visibility": "Visibility",
|
||||||
|
"active": "Active",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"internal": "Internal",
|
||||||
|
"public": "Public"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"domain": "Inventory",
|
"domain": "Inventory",
|
||||||
|
|||||||
+18
-2
@@ -182,6 +182,7 @@ export const DataTableRoot = <TData,>({
|
|||||||
{table.getRowModel().rows.map((row) => {
|
{table.getRowModel().rows.map((row) => {
|
||||||
const to = navigateTo ? navigateTo(row) : undefined
|
const to = navigateTo ? navigateTo(row) : undefined
|
||||||
const isRowDisabled = hasSelect && !row.getCanSelect()
|
const isRowDisabled = hasSelect && !row.getCanSelect()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Row
|
<Table.Row
|
||||||
key={row.id}
|
key={row.id}
|
||||||
@@ -192,7 +193,8 @@ export const DataTableRoot = <TData,>({
|
|||||||
"cursor-pointer": !!to,
|
"cursor-pointer": !!to,
|
||||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
row.getIsSelected(),
|
row.getIsSelected(),
|
||||||
"bg-ui-bg-subtle hover:bg-ui-bg-subtle": isRowDisabled,
|
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
||||||
|
isRowDisabled,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={to ? () => navigate(to) : undefined}
|
onClick={to ? () => navigate(to) : undefined}
|
||||||
@@ -211,6 +213,15 @@ export const DataTableRoot = <TData,>({
|
|||||||
|
|
||||||
const isStickyCell = isSelectCell || isFirstCell
|
const isStickyCell = isSelectCell || isFirstCell
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the table has nested rows, we need to offset the cell padding
|
||||||
|
* to indicate the depth of the row.
|
||||||
|
*/
|
||||||
|
const depthOffset =
|
||||||
|
row.depth > 0 && isFirstCell
|
||||||
|
? row.depth * 14 + 24
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Cell
|
<Table.Cell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
@@ -221,9 +232,14 @@ export const DataTableRoot = <TData,>({
|
|||||||
isStickyCell && hasSelect && !isSelectCell,
|
isStickyCell && hasSelect && !isSelectCell,
|
||||||
"after:bg-ui-border-base":
|
"after:bg-ui-border-base":
|
||||||
showStickyBorder && isStickyCell && !isSelectCell,
|
showStickyBorder && isStickyCell && !isSelectCell,
|
||||||
"bg-ui-bg-subtle hover:bg-ui-bg-subtle":
|
"!bg-ui-bg-disabled !hover:bg-ui-bg-disabled":
|
||||||
isRowDisabled,
|
isRowDisabled,
|
||||||
})}
|
})}
|
||||||
|
style={{
|
||||||
|
paddingLeft: depthOffset
|
||||||
|
? `${depthOffset}px`
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
|
import {
|
||||||
|
AdminProductCategoryListResponse,
|
||||||
|
AdminProductCategoryResponse,
|
||||||
|
} from "@medusajs/types"
|
||||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
import { QueryKey, UseQueryOptions, 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 { CategoriesListRes, CategoryRes } from "../../types/api-responses"
|
|
||||||
|
|
||||||
const CATEGORIES_QUERY_KEY = "categories" as const
|
const CATEGORIES_QUERY_KEY = "categories" as const
|
||||||
export const categoriesQueryKeys = queryKeysFactory(CATEGORIES_QUERY_KEY)
|
export const categoriesQueryKeys = queryKeysFactory(CATEGORIES_QUERY_KEY)
|
||||||
|
|
||||||
export const useCategory = (
|
export const useCategory = (
|
||||||
id: string,
|
id: string,
|
||||||
|
query?: Record<string, any>,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<CategoryRes, Error, CategoryRes, QueryKey>,
|
UseQueryOptions<
|
||||||
|
AdminProductCategoryResponse,
|
||||||
|
Error,
|
||||||
|
AdminProductCategoryResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryKey: categoriesQueryKeys.detail(id),
|
queryKey: categoriesQueryKeys.detail(id, query),
|
||||||
queryFn: async () => client.categories.retrieve(id),
|
queryFn: async () => client.categories.retrieve(id, query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -25,7 +34,12 @@ export const useCategory = (
|
|||||||
export const useCategories = (
|
export const useCategories = (
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<CategoriesListRes, Error, CategoriesListRes, QueryKey>,
|
UseQueryOptions<
|
||||||
|
AdminProductCategoryListResponse,
|
||||||
|
Error,
|
||||||
|
AdminProductCategoryListResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { Filter } from "../../../components/table/data-table"
|
|||||||
import { useProductTypes } from "../../api/product-types"
|
import { useProductTypes } from "../../api/product-types"
|
||||||
import { useSalesChannels } from "../../api/sales-channels"
|
import { useSalesChannels } from "../../api/sales-channels"
|
||||||
|
|
||||||
const excludeableFields = ["sales_channel_id", "collections"] as const
|
const excludeableFields = [
|
||||||
|
"sales_channel_id",
|
||||||
|
"collections",
|
||||||
|
"categories",
|
||||||
|
] as const
|
||||||
|
|
||||||
export const useProductTableFilters = (
|
export const useProductTableFilters = (
|
||||||
exclude?: (typeof excludeableFields)[number][]
|
exclude?: (typeof excludeableFields)[number][]
|
||||||
@@ -33,11 +37,15 @@ export const useProductTableFilters = (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isCategoryExcluded = exclude?.includes("categories")
|
||||||
|
|
||||||
// const { product_categories } = useAdminProductCategories({
|
// const { product_categories } = useAdminProductCategories({
|
||||||
// limit: 1000,
|
// limit: 1000,
|
||||||
// offset: 0,
|
// offset: 0,
|
||||||
// fields: "id,name",
|
// fields: "id,name",
|
||||||
// expand: "",
|
// expand: "",
|
||||||
|
// }, {
|
||||||
|
// enabled: !isCategoryExcluded,
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const isCollectionExcluded = exclude?.includes("collections")
|
const isCollectionExcluded = exclude?.includes("collections")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
RowSelectionState,
|
RowSelectionState,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
|
getExpandedRowModel,
|
||||||
getPaginationRowModel,
|
getPaginationRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
@@ -22,7 +23,9 @@ type UseDataTableProps<TData> = {
|
|||||||
updater: OnChangeFn<RowSelectionState>
|
updater: OnChangeFn<RowSelectionState>
|
||||||
}
|
}
|
||||||
enablePagination?: boolean
|
enablePagination?: boolean
|
||||||
|
enableExpandableRows?: boolean
|
||||||
getRowId?: (original: TData, index: number) => string
|
getRowId?: (original: TData, index: number) => string
|
||||||
|
getSubRows?: (original: TData) => TData[]
|
||||||
meta?: Record<string, unknown>
|
meta?: Record<string, unknown>
|
||||||
prefix?: string
|
prefix?: string
|
||||||
}
|
}
|
||||||
@@ -34,7 +37,9 @@ export const useDataTable = <TData,>({
|
|||||||
pageSize: _pageSize = 20,
|
pageSize: _pageSize = 20,
|
||||||
enablePagination = true,
|
enablePagination = true,
|
||||||
enableRowSelection = false,
|
enableRowSelection = false,
|
||||||
|
enableExpandableRows = false,
|
||||||
rowSelection: _rowSelection,
|
rowSelection: _rowSelection,
|
||||||
|
getSubRows,
|
||||||
getRowId,
|
getRowId,
|
||||||
meta,
|
meta,
|
||||||
prefix,
|
prefix,
|
||||||
@@ -107,6 +112,7 @@ export const useDataTable = <TData,>({
|
|||||||
pageCount: Math.ceil((count ?? 0) / pageSize),
|
pageCount: Math.ceil((count ?? 0) / pageSize),
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
getRowId,
|
getRowId,
|
||||||
|
getSubRows,
|
||||||
onRowSelectionChange: enableRowSelection ? setRowSelection : undefined,
|
onRowSelectionChange: enableRowSelection ? setRowSelection : undefined,
|
||||||
onPaginationChange: enablePagination
|
onPaginationChange: enablePagination
|
||||||
? (onPaginationChange as OnChangeFn<PaginationState>)
|
? (onPaginationChange as OnChangeFn<PaginationState>)
|
||||||
@@ -115,6 +121,9 @@ export const useDataTable = <TData,>({
|
|||||||
getPaginationRowModel: enablePagination
|
getPaginationRowModel: enablePagination
|
||||||
? getPaginationRowModel()
|
? getPaginationRowModel()
|
||||||
: undefined,
|
: undefined,
|
||||||
|
getExpandedRowModel: enableExpandableRows
|
||||||
|
? getExpandedRowModel()
|
||||||
|
: undefined,
|
||||||
manualPagination: enablePagination ? true : undefined,
|
manualPagination: enablePagination ? true : undefined,
|
||||||
meta,
|
meta,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
ProductCollectionListRes,
|
AdminProductCategoryListResponse,
|
||||||
ProductCollectionRes,
|
AdminProductCategoryResponse,
|
||||||
} from "../../types/api-responses"
|
} from "@medusajs/types"
|
||||||
import { getRequest } from "./common"
|
import { getRequest } from "./common"
|
||||||
|
|
||||||
async function listProductCategories(query?: Record<string, any>) {
|
async function listProductCategories(query?: Record<string, any>) {
|
||||||
return getRequest<ProductCollectionListRes>(`/admin/categories`, query)
|
return getRequest<AdminProductCategoryListResponse>(
|
||||||
|
`/admin/product-categories`,
|
||||||
|
query
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retrieveProductCategory(
|
async function retrieveProductCategory(
|
||||||
id: string,
|
id: string,
|
||||||
query?: Record<string, any>
|
query?: Record<string, any>
|
||||||
) {
|
) {
|
||||||
return getRequest<ProductCollectionRes>(`/admin/categories/${id}`, query)
|
return getRequest<AdminProductCategoryResponse>(
|
||||||
|
`/admin/product-categories/${id}`,
|
||||||
|
query
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const categories = {
|
export const categories = {
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
|
|||||||
]
|
]
|
||||||
details: () => readonly [...TQueryKey<TKey>["all"], "detail"]
|
details: () => readonly [...TQueryKey<TKey>["all"], "detail"]
|
||||||
detail: (
|
detail: (
|
||||||
id: TDetailQuery
|
id: TDetailQuery,
|
||||||
) => readonly [...ReturnType<TQueryKey<TKey>["details"]>, TDetailQuery]
|
query?: TListQuery
|
||||||
|
) => readonly [
|
||||||
|
...ReturnType<TQueryKey<TKey>["details"]>,
|
||||||
|
TDetailQuery,
|
||||||
|
{ query: TListQuery | undefined },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseQueryOptionsWrapper<
|
export type UseQueryOptionsWrapper<
|
||||||
@@ -39,7 +44,11 @@ export const queryKeysFactory = <
|
|||||||
lists: () => [...queryKeyFactory.all, "list"],
|
lists: () => [...queryKeyFactory.all, "list"],
|
||||||
list: (query?: TListQueryType) => [...queryKeyFactory.lists(), { query }],
|
list: (query?: TListQueryType) => [...queryKeyFactory.lists(), { query }],
|
||||||
details: () => [...queryKeyFactory.all, "detail"],
|
details: () => [...queryKeyFactory.all, "detail"],
|
||||||
detail: (id: TDetailQueryType) => [...queryKeyFactory.details(), id],
|
detail: (id: TDetailQueryType, query?: TListQueryType) => [
|
||||||
|
...queryKeyFactory.details(),
|
||||||
|
id,
|
||||||
|
{ query },
|
||||||
|
],
|
||||||
}
|
}
|
||||||
return queryKeyFactory
|
return queryKeyFactory
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { SalesChannelDTO, UserDTO } from "@medusajs/types"
|
import { AdminCustomersRes } from "@medusajs/client-types"
|
||||||
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
|
||||||
import { Spinner } from "@medusajs/icons"
|
import { Spinner } from "@medusajs/icons"
|
||||||
import {
|
import {
|
||||||
AdminCollectionsRes,
|
AdminCollectionsRes,
|
||||||
@@ -7,14 +6,19 @@ import {
|
|||||||
AdminPromotionRes,
|
AdminPromotionRes,
|
||||||
AdminRegionsRes,
|
AdminRegionsRes,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
AdminApiKeyResponse,
|
||||||
|
AdminProductCategoryResponse,
|
||||||
|
SalesChannelDTO,
|
||||||
|
UserDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
||||||
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"
|
||||||
import { useMe } from "../../hooks/api/users"
|
import { useMe } from "../../hooks/api/users"
|
||||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
|
||||||
import { SearchProvider } from "../search-provider"
|
import { SearchProvider } from "../search-provider"
|
||||||
import { SidebarProvider } from "../sidebar-provider"
|
import { SidebarProvider } from "../sidebar-provider"
|
||||||
import { AdminCustomersRes } from "@medusajs/client-types"
|
|
||||||
|
|
||||||
export const ProtectedRoute = () => {
|
export const ProtectedRoute = () => {
|
||||||
const { user, isLoading } = useMe()
|
const { user, isLoading } = useMe()
|
||||||
@@ -143,6 +147,27 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/categories",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Categories",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
lazy: () => import("../../v2-routes/categories/category-list"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/categories/category-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminProductCategoryResponse) =>
|
||||||
|
data.product_category.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/orders",
|
path: "/orders",
|
||||||
handle: {
|
handle: {
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
import { useLoaderData, useParams } from "react-router-dom"
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { useCategory } from "../../../hooks/api/categories"
|
||||||
|
import { CategoryGeneralSection } from "./components/category-general-section"
|
||||||
|
import { CategoryOrganizationSection } from "./components/category-organization-section"
|
||||||
|
import { CategoryProductSection } from "./components/category-product-section"
|
||||||
|
import { categoryLoader } from "./loader"
|
||||||
|
|
||||||
|
export const CategoryDetail = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof categoryLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { product_category, isLoading, isError, error } = useCategory(
|
||||||
|
id!,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
initialData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading || !product_category) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
|
||||||
|
<div className="flex w-full flex-col gap-y-2">
|
||||||
|
<CategoryGeneralSection category={product_category} />
|
||||||
|
<CategoryProductSection category={product_category} />
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<JsonViewSection data={product_category} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 lg:mt-0 lg:max-w-[400px]">
|
||||||
|
<CategoryOrganizationSection category={product_category} />
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<JsonViewSection data={product_category} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { Container, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { getIsActiveProps, getIsInternalProps } from "../../../common/utils"
|
||||||
|
|
||||||
|
type CategoryGeneralSectionProps = {
|
||||||
|
category: AdminProductCategoryResponse["product_category"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategoryGeneralSection = ({
|
||||||
|
category,
|
||||||
|
}: CategoryGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const activeProps = getIsActiveProps(category.is_active, t)
|
||||||
|
const internalProps = getIsInternalProps(category.is_internal, t)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{category.name}</Heading>
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<StatusBadge color={activeProps.color}>
|
||||||
|
{activeProps.label}
|
||||||
|
</StatusBadge>
|
||||||
|
<StatusBadge color={internalProps.color}>
|
||||||
|
{internalProps.label}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
<ActionMenu groups={[]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 gap-3 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.description")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{category.description || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 gap-3 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.handle")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
/{category.handle}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./category-general-section"
|
||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
import { FolderIllustration, TriangleRightMini } from "@medusajs/icons"
|
||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { Badge, Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { InlineLink } from "../../../../../components/common/inline-link"
|
||||||
|
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||||
|
import { useCategory } from "../../../../../hooks/api/categories"
|
||||||
|
import { getCategoryChildren, getCategoryPath } from "../../../common/utils"
|
||||||
|
|
||||||
|
type CategoryOrganizationSectionProps = {
|
||||||
|
category: AdminProductCategoryResponse["product_category"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategoryOrganizationSection = ({
|
||||||
|
category,
|
||||||
|
}: CategoryOrganizationSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("categories.organization.header")}</Heading>
|
||||||
|
<ActionMenu groups={[]} />
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start gap-3 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("categories.organization.pathLabel")}
|
||||||
|
</Text>
|
||||||
|
<PathDisplay category={category} />
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-start gap-3 px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("categories.organization.childrenLabel")}
|
||||||
|
</Text>
|
||||||
|
<ChildrenDisplay category={category} />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PathDisplay = ({
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
category: AdminProductCategoryResponse["product_category"]
|
||||||
|
}) => {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
product_category: withParents,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useCategory(category.id, {
|
||||||
|
include_ancestors_tree: true,
|
||||||
|
fields: "id,name,parent_category",
|
||||||
|
})
|
||||||
|
|
||||||
|
const chips = useMemo(() => getCategoryPath(withParents), [withParents])
|
||||||
|
|
||||||
|
if (isLoading || !withParents) {
|
||||||
|
return <Skeleton className="h-5 w-16" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chips.length) {
|
||||||
|
return (
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chips.length > 1 && !expanded) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[20px_1fr] items-start gap-x-2">
|
||||||
|
<FolderIllustration />
|
||||||
|
<div className="flex items-center gap-x-0.5">
|
||||||
|
<Tooltip content={t("categories.organization.pathExpandTooltip")}>
|
||||||
|
<button
|
||||||
|
className="outline-none"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setExpanded(true)}
|
||||||
|
>
|
||||||
|
<Text size="xsmall" leading="compact" weight="plus">
|
||||||
|
...
|
||||||
|
</Text>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<TriangleRightMini />
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
leading="compact"
|
||||||
|
weight="plus"
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
|
{chips[chips.length - 1].name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chips.length > 1 && expanded) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[20px_1fr] items-start gap-x-2">
|
||||||
|
<FolderIllustration />
|
||||||
|
<div className="gap- flex flex-wrap items-center gap-x-0.5 gap-y-1">
|
||||||
|
{chips.map((chip, index) => {
|
||||||
|
return (
|
||||||
|
<div key={chip.id} className="flex items-center gap-x-0.5">
|
||||||
|
{index === chips.length - 1 ? (
|
||||||
|
<Text size="xsmall" leading="compact" weight="plus">
|
||||||
|
{chip.name}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<InlineLink
|
||||||
|
to={`/categories/${chip.id}`}
|
||||||
|
className="txt-compact-xsmall-plus text-ui-fg-subtle hover:text-ui-fg-base focus-visible:text-ui-fg-base"
|
||||||
|
>
|
||||||
|
{chip.name}
|
||||||
|
</InlineLink>
|
||||||
|
)}
|
||||||
|
{index < chips.length - 1 && <TriangleRightMini />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 items-start gap-x-2">
|
||||||
|
{chips.map((chip, index) => (
|
||||||
|
<div key={chip.id} className="flex items-center gap-x-0.5">
|
||||||
|
<Text size="xsmall" leading="compact" weight="plus">
|
||||||
|
{chip.name}
|
||||||
|
</Text>
|
||||||
|
{index < chips.length - 1 && <TriangleRightMini />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChildrenDisplay = ({
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
category: AdminProductCategoryResponse["product_category"]
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
product_category: withChildren,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useCategory(category.id, {
|
||||||
|
include_descendants_tree: true,
|
||||||
|
fields: "id,name,category_children",
|
||||||
|
})
|
||||||
|
|
||||||
|
const chips = useMemo(() => getCategoryChildren(withChildren), [withChildren])
|
||||||
|
|
||||||
|
if (isLoading || !withChildren) {
|
||||||
|
return <Skeleton className="h-5 w-16" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chips.length) {
|
||||||
|
return (
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{chips.map((chip) => (
|
||||||
|
<Badge key={chip.id} size="2xsmall" asChild>
|
||||||
|
<Link to={`/categories/${chip.id}`}>{chip.name}</Link>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./category-organization-section"
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useProducts } from "../../../../../hooks/api/products"
|
||||||
|
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
|
||||||
|
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
||||||
|
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
|
||||||
|
type CategoryProductSectionProps = {
|
||||||
|
category: AdminProductCategoryResponse["product_category"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export const CategoryProductSection = ({
|
||||||
|
category,
|
||||||
|
}: CategoryProductSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { raw, searchParams } = useProductTableQuery({ pageSize: PAGE_SIZE })
|
||||||
|
const { products, count, isLoading, isError, error } = useProducts(
|
||||||
|
{
|
||||||
|
...searchParams,
|
||||||
|
category_id: [category.id],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = useProductTableColumns()
|
||||||
|
const filters = useProductTableFilters(["categories"])
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: products || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
getRowId: (original) => original.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
enableRowSelection: true,
|
||||||
|
enablePagination: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("products.domain")}</Heading>
|
||||||
|
<ActionMenu groups={[]} />
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
filters={filters}
|
||||||
|
columns={columns}
|
||||||
|
orderBy={["title", "created_at", "updated_at"]}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
navigateTo={(row) => row.id}
|
||||||
|
isLoading={isLoading}
|
||||||
|
queryObject={raw}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./category-product-section"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { CategoryDetail as Component } from "./category-detail"
|
||||||
|
export { categoryLoader as loader } from "./loader"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { categoriesQueryKeys } from "../../../hooks/api/categories"
|
||||||
|
import { client } from "../../../lib/client"
|
||||||
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const categoryDetailQuery = (id: string) => ({
|
||||||
|
queryKey: categoriesQueryKeys.detail(id),
|
||||||
|
queryFn: async () => client.categories.retrieve(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const categoryLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = categoryDetailQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<AdminProductCategoryResponse>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { CategoryListTable } from "./components/category-list-table"
|
||||||
|
|
||||||
|
export const CategoryList = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<CategoryListTable />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
import { PencilSquare, TriangleRightMini } from "@medusajs/icons"
|
||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { Container, Heading, IconButton, Text, clx } from "@medusajs/ui"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
||||||
|
import {
|
||||||
|
TextCell,
|
||||||
|
TextHeader,
|
||||||
|
} from "../../../../../components/table/table-cells/common/text-cell"
|
||||||
|
import { useCategories } from "../../../../../hooks/api/categories"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useCategoryTableQuery } from "../../../common/hooks/use-category-table-query"
|
||||||
|
import {
|
||||||
|
getCategoryPath,
|
||||||
|
getIsActiveProps,
|
||||||
|
getIsInternalProps,
|
||||||
|
} from "../../../common/utils"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const CategoryListTable = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { raw, searchParams } = useCategoryTableQuery({ pageSize: PAGE_SIZE })
|
||||||
|
|
||||||
|
const query = raw.q
|
||||||
|
? {
|
||||||
|
include_ancestors_tree: true,
|
||||||
|
fields: "id,name,handle,is_active,is_internal,parent_category",
|
||||||
|
...searchParams,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
include_descendants_tree: true,
|
||||||
|
parent_category_id: "null",
|
||||||
|
fields: "id,name,category_children,handle,is_internal,is_active",
|
||||||
|
...searchParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { product_categories, count, isLoading, isError, error } =
|
||||||
|
useCategories(
|
||||||
|
{
|
||||||
|
...query,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = useCategoryTableColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: product_categories || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
getRowId: (original) => original.id,
|
||||||
|
getSubRows: (original) => original.category_children,
|
||||||
|
enableExpandableRows: true,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{t("categories.domain")}</Heading>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
count={count}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
isLoading={isLoading}
|
||||||
|
navigateTo={(row) => row.id}
|
||||||
|
queryObject={raw}
|
||||||
|
search
|
||||||
|
pagination
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryRowActions = ({
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
category: AdminProductCategoryResponse["product_category"]
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
to: `${category.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper =
|
||||||
|
createColumnHelper<AdminProductCategoryResponse["product_category"]>()
|
||||||
|
|
||||||
|
const useCategoryTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: () => <TextHeader text={t("fields.name")} />,
|
||||||
|
cell: ({ getValue, row }) => {
|
||||||
|
const expandHandler = row.getToggleExpandedHandler()
|
||||||
|
|
||||||
|
console.log(row.original)
|
||||||
|
|
||||||
|
if (row.original.parent_category !== undefined) {
|
||||||
|
const path = getCategoryPath(row.original)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center">
|
||||||
|
{path.map((chip) => (
|
||||||
|
<div key={chip.id}>
|
||||||
|
<Text>{chip.name}</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center gap-x-3 overflow-hidden">
|
||||||
|
<div className="flex size-7 items-center justify-center">
|
||||||
|
{row.getCanExpand() ? (
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
expandHandler()
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
>
|
||||||
|
<TriangleRightMini
|
||||||
|
className={clx({
|
||||||
|
"rotate-90 transition-transform will-change-transform":
|
||||||
|
row.getIsExpanded(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<span className="truncate">{getValue()}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("handle", {
|
||||||
|
header: () => <TextHeader text={t("fields.handle")} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
return <TextCell text={`/${getValue()}`} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("is_active", {
|
||||||
|
header: () => <TextHeader text={t("fields.status")} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const { color, label } = getIsActiveProps(getValue(), t)
|
||||||
|
|
||||||
|
return <StatusCell color={color}>{label}</StatusCell>
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("is_internal", {
|
||||||
|
header: () => <TextHeader text={t("categories.fields.visibility")} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const { color, label } = getIsInternalProps(getValue(), t)
|
||||||
|
|
||||||
|
return <StatusCell color={color}>{label}</StatusCell>
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <CategoryRowActions category={row.original} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./category-list-table"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CategoryList as Component } from "./category-list"
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useCategoryTableQuery = ({
|
||||||
|
pageSize = 20,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize?: number
|
||||||
|
prefix?: string
|
||||||
|
}) => {
|
||||||
|
const raw = useQueryParams(["q", "offset", "order"], prefix)
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
q: raw.q,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: raw.offset ? Number(raw.offset) : 0,
|
||||||
|
order: raw.order,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
searchParams,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import { TFunction } from "i18next"
|
||||||
|
|
||||||
|
export function getIsActiveProps(
|
||||||
|
isActive: boolean,
|
||||||
|
t: TFunction
|
||||||
|
): { color: "green" | "red"; label: string } {
|
||||||
|
switch (isActive) {
|
||||||
|
case true:
|
||||||
|
return {
|
||||||
|
label: t("categories.fields.active"),
|
||||||
|
color: "green",
|
||||||
|
}
|
||||||
|
case false:
|
||||||
|
return {
|
||||||
|
label: t("categories.fields.inactive"),
|
||||||
|
color: "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIsInternalProps(
|
||||||
|
isInternal: boolean,
|
||||||
|
t: TFunction
|
||||||
|
): { color: "blue" | "green"; label: string } {
|
||||||
|
switch (isInternal) {
|
||||||
|
case true:
|
||||||
|
return {
|
||||||
|
label: t("categories.fields.internal"),
|
||||||
|
color: "blue",
|
||||||
|
}
|
||||||
|
case false:
|
||||||
|
return {
|
||||||
|
label: t("categories.fields.public"),
|
||||||
|
color: "green",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChipProps = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategoryPath(
|
||||||
|
category?: AdminProductCategoryResponse["product_category"]
|
||||||
|
): ChipProps[] {
|
||||||
|
if (!category) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = category.parent_category
|
||||||
|
? getCategoryPath(category.parent_category)
|
||||||
|
: []
|
||||||
|
path.push({ id: category.id, name: category.name })
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategoryChildren(
|
||||||
|
category?: AdminProductCategoryResponse["product_category"]
|
||||||
|
): ChipProps[] {
|
||||||
|
if (!category || !category.category_children) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return category.category_children.map((child) => ({
|
||||||
|
id: child.id,
|
||||||
|
name: child.name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../types/routing"
|
||||||
|
import { AdminProductCategoryParamsType } from "../validators"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminProductCategoryParamsType>,
|
||||||
|
res: MedusaResponse<AdminProductCategoryResponse>
|
||||||
|
) => {
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "product_category",
|
||||||
|
variables: {
|
||||||
|
filters: req.filterableFields,
|
||||||
|
id: req.params.id,
|
||||||
|
},
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [product_category] = await remoteQuery(queryObject)
|
||||||
|
|
||||||
|
res.json({ product_category })
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||||
|
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||||
|
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||||
|
import * as QueryConfig from "./query-config"
|
||||||
|
import {
|
||||||
|
AdminProductCategoriesParams,
|
||||||
|
AdminProductCategoryParams,
|
||||||
|
} from "./validators"
|
||||||
|
|
||||||
|
export const adminProductCategoryRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
|
{
|
||||||
|
method: ["ALL"],
|
||||||
|
matcher: "/admin/product-categories*",
|
||||||
|
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/admin/product-categories",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminProductCategoriesParams,
|
||||||
|
QueryConfig.listProductCategoryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/admin/product-categories/:id",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminProductCategoryParams,
|
||||||
|
QueryConfig.retrieveProductCategoryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export const defaults = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"handle",
|
||||||
|
"is_active",
|
||||||
|
"is_internal",
|
||||||
|
"rank",
|
||||||
|
"parent_category_id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"metadata",
|
||||||
|
|
||||||
|
"parent_category.id",
|
||||||
|
"parent_category.name",
|
||||||
|
"category_children.id",
|
||||||
|
"category_children.name",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const retrieveProductCategoryConfig = {
|
||||||
|
defaults,
|
||||||
|
isList: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listProductCategoryConfig = {
|
||||||
|
defaults,
|
||||||
|
defaultLimit: 50,
|
||||||
|
isList: true,
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { AdminProductCategoryListResponse } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../types/routing"
|
||||||
|
import { AdminProductCategoriesParamsType } from "./validators"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminProductCategoriesParamsType>,
|
||||||
|
res: MedusaResponse<AdminProductCategoryListResponse>
|
||||||
|
) => {
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "product_category",
|
||||||
|
variables: {
|
||||||
|
filters: req.filterableFields,
|
||||||
|
...req.remoteQueryConfig.pagination,
|
||||||
|
},
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows: product_categories, metadata } = await remoteQuery(queryObject)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
product_categories,
|
||||||
|
count: metadata.count,
|
||||||
|
offset: metadata.skip,
|
||||||
|
limit: metadata.take,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
||||||
|
import {
|
||||||
|
createFindParams,
|
||||||
|
createOperatorMap,
|
||||||
|
createSelectParams,
|
||||||
|
} from "../../utils/validators"
|
||||||
|
|
||||||
|
export type AdminProductCategoryParamsType = z.infer<
|
||||||
|
typeof AdminProductCategoryParams
|
||||||
|
>
|
||||||
|
export const AdminProductCategoryParams = createSelectParams().merge(
|
||||||
|
z.object({
|
||||||
|
include_ancestors_tree: z.preprocess(
|
||||||
|
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
||||||
|
z.boolean().optional()
|
||||||
|
),
|
||||||
|
include_descendants_tree: z.preprocess(
|
||||||
|
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
||||||
|
z.boolean().optional()
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AdminProductCategoriesParamsType = z.infer<
|
||||||
|
typeof AdminProductCategoriesParams
|
||||||
|
>
|
||||||
|
export const AdminProductCategoriesParams = createFindParams({
|
||||||
|
offset: 0,
|
||||||
|
limit: 50,
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
q: z.string().optional(),
|
||||||
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
description: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
handle: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
parent_category_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
include_ancestors_tree: z.preprocess(
|
||||||
|
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
||||||
|
z.boolean().optional()
|
||||||
|
),
|
||||||
|
include_descendants_tree: z.preprocess(
|
||||||
|
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
||||||
|
z.boolean().optional()
|
||||||
|
),
|
||||||
|
created_at: createOperatorMap().optional(),
|
||||||
|
updated_at: createOperatorMap().optional(),
|
||||||
|
deleted_at: createOperatorMap().optional(),
|
||||||
|
$and: z.lazy(() => AdminProductCategoriesParams.array()).optional(),
|
||||||
|
$or: z.lazy(() => AdminProductCategoriesParams.array()).optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ProductStatus } from "@medusajs/utils"
|
import { ProductStatus } from "@medusajs/utils"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
||||||
import {
|
import {
|
||||||
createFindParams,
|
createFindParams,
|
||||||
createOperatorMap,
|
createOperatorMap,
|
||||||
createSelectParams,
|
createSelectParams,
|
||||||
} from "../../utils/validators"
|
} from "../../utils/validators"
|
||||||
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
|
||||||
|
|
||||||
const statusEnum = z.nativeEnum(ProductStatus)
|
const statusEnum = z.nativeEnum(ProductStatus)
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ export const AdminGetProductsParams = createFindParams({
|
|||||||
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
||||||
z.boolean().optional()
|
z.boolean().optional()
|
||||||
),
|
),
|
||||||
|
category_id: z.string().array().optional(),
|
||||||
price_list_id: z.string().array().optional(),
|
price_list_id: z.string().array().optional(),
|
||||||
sales_channel_id: z.string().array().optional(),
|
sales_channel_id: z.string().array().optional(),
|
||||||
collection_id: z.string().array().optional(),
|
collection_id: z.string().array().optional(),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares"
|
|||||||
import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares"
|
import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares"
|
||||||
import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares"
|
import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares"
|
||||||
import { adminPricingRoutesMiddlewares } from "./admin/pricing/middlewares"
|
import { adminPricingRoutesMiddlewares } from "./admin/pricing/middlewares"
|
||||||
|
import { adminProductCategoryRoutesMiddlewares } from "./admin/product-categories/middlewares"
|
||||||
import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares"
|
import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares"
|
||||||
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
|
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
|
||||||
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
|
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
|
||||||
@@ -67,5 +68,6 @@ export const config: MiddlewaresConfig = {
|
|||||||
...adminProductTypeRoutesMiddlewares,
|
...adminProductTypeRoutesMiddlewares,
|
||||||
...adminUploadRoutesMiddlewares,
|
...adminUploadRoutesMiddlewares,
|
||||||
...adminFulfillmentSetsRoutesMiddlewares,
|
...adminFulfillmentSetsRoutesMiddlewares,
|
||||||
|
...adminProductCategoryRoutesMiddlewares,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import { NextFunction } from "express"
|
|
||||||
import { MedusaRequest, MedusaResponse } from "../../types/routing"
|
|
||||||
import { zodValidator } from "./validate-body"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { removeUndefinedProperties } from "../../utils"
|
|
||||||
import { omit } from "lodash"
|
|
||||||
import { BaseEntity, QueryConfig, RequestQueryFields } from "@medusajs/types"
|
import { BaseEntity, QueryConfig, RequestQueryFields } from "@medusajs/types"
|
||||||
|
import { NextFunction } from "express"
|
||||||
|
import { omit } from "lodash"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "../../types/routing"
|
||||||
|
import { removeUndefinedProperties } from "../../utils"
|
||||||
import {
|
import {
|
||||||
prepareListQuery,
|
prepareListQuery,
|
||||||
prepareRetrieveQuery,
|
prepareRetrieveQuery,
|
||||||
} from "../../utils/get-query-config"
|
} from "../../utils/get-query-config"
|
||||||
import { FindConfig } from "@medusajs/types"
|
import { zodValidator } from "./validate-body"
|
||||||
/**
|
/**
|
||||||
* Normalize an input query, especially from array like query params to an array type
|
* Normalize an input query, especially from array like query params to an array type
|
||||||
* e.g: /admin/orders/?fields[]=id,status,cart_id becomes { fields: ["id", "status", "cart_id"] }
|
* e.g: /admin/orders/?fields[]=id,status,cart_id becomes { fields: ["id", "status", "cart_id"] }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
DALUtils,
|
DALUtils,
|
||||||
|
Searchable,
|
||||||
createPsqlIndexStatementHelper,
|
createPsqlIndexStatementHelper,
|
||||||
generateEntityId,
|
generateEntityId,
|
||||||
kebabCase,
|
kebabCase,
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
|
|
||||||
import Product from "./product"
|
import Product from "./product"
|
||||||
|
|
||||||
const categoryHandleIndexName = "IDX_category_handle_unique"
|
const categoryHandleIndexName = "IDX_category_handle_unique"
|
||||||
@@ -47,9 +47,11 @@ class ProductCategory {
|
|||||||
@PrimaryKey({ columnType: "text" })
|
@PrimaryKey({ columnType: "text" })
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", nullable: false })
|
@Property({ columnType: "text", nullable: false })
|
||||||
name?: string
|
name?: string
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", default: "", nullable: false })
|
@Property({ columnType: "text", default: "", nullable: false })
|
||||||
description?: string
|
description?: string
|
||||||
|
|
||||||
@@ -124,11 +126,14 @@ class ProductCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { em } = args
|
const { em } = args
|
||||||
const parentCategoryId = args.changeSet?.entity?.parent_category?.id
|
|
||||||
let parentCategory: ProductCategory | null = null
|
let parentCategory: ProductCategory | null = null
|
||||||
|
|
||||||
if (parentCategoryId) {
|
if (this.parent_category_id) {
|
||||||
parentCategory = await em.findOne(ProductCategory, parentCategoryId)
|
parentCategory = await em.findOne(
|
||||||
|
ProductCategory,
|
||||||
|
this.parent_category_id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentCategory) {
|
if (parentCategory) {
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import {
|
|||||||
ProductCategoryTransformOptions,
|
ProductCategoryTransformOptions,
|
||||||
ProductTypes,
|
ProductTypes,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { DALUtils, MedusaError, isDefined } from "@medusajs/utils"
|
import {
|
||||||
|
DALUtils,
|
||||||
|
MedusaError,
|
||||||
|
isDefined
|
||||||
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
LoadStrategy,
|
LoadStrategy,
|
||||||
FilterQuery as MikroFilterQuery,
|
FilterQuery as MikroFilterQuery,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
FreeTextSearchFilterKey,
|
||||||
InjectManager,
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
@@ -76,6 +77,17 @@ export default class ProductCategoryService<
|
|||||||
delete filters.include_descendants_tree
|
delete filters.include_descendants_tree
|
||||||
delete filters.include_ancestors_tree
|
delete filters.include_ancestors_tree
|
||||||
|
|
||||||
|
// Apply free text search filter
|
||||||
|
if (filters?.q) {
|
||||||
|
config.filters ??= {}
|
||||||
|
config.filters[FreeTextSearchFilterKey] = {
|
||||||
|
value: filters.q,
|
||||||
|
fromEntity: ProductCategory.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
delete filters.q
|
||||||
|
}
|
||||||
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||||
filters,
|
filters,
|
||||||
config
|
config
|
||||||
@@ -102,6 +114,17 @@ export default class ProductCategoryService<
|
|||||||
delete filters.include_descendants_tree
|
delete filters.include_descendants_tree
|
||||||
delete filters.include_ancestors_tree
|
delete filters.include_ancestors_tree
|
||||||
|
|
||||||
|
// Apply free text search filter
|
||||||
|
if (filters?.q) {
|
||||||
|
config.filters ??= {}
|
||||||
|
config.filters[FreeTextSearchFilterKey] = {
|
||||||
|
value: filters.q,
|
||||||
|
fromEntity: ProductCategory.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
delete filters.q
|
||||||
|
}
|
||||||
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||||
filters,
|
filters,
|
||||||
config
|
config
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { RepositoryTransformOptions } from "../common"
|
|||||||
import { Context } from "../shared-context"
|
import { Context } from "../shared-context"
|
||||||
import {
|
import {
|
||||||
BaseFilterable,
|
BaseFilterable,
|
||||||
FilterQuery as InternalFilterQuery,
|
|
||||||
FilterQuery,
|
FilterQuery,
|
||||||
FindOptions,
|
FindOptions,
|
||||||
|
FilterQuery as InternalFilterQuery,
|
||||||
UpsertWithReplaceConfig,
|
UpsertWithReplaceConfig,
|
||||||
} from "./index"
|
} from "./index"
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from "./pricing"
|
|||||||
export * from "./sales-channel"
|
export * from "./sales-channel"
|
||||||
export * from "./stock-locations"
|
export * from "./stock-locations"
|
||||||
export * from "./tax"
|
export * from "./tax"
|
||||||
|
export * from "./product-category"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-category"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { PaginatedResponse } from "../../../common"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
interface ProductCategoryResponse {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string | null
|
||||||
|
handle: string | null
|
||||||
|
is_active: boolean
|
||||||
|
is_internal: boolean
|
||||||
|
rank: number | null
|
||||||
|
parent_category_id: string | null
|
||||||
|
created_at: string | Date
|
||||||
|
updated_at: string | Date
|
||||||
|
|
||||||
|
parent_category: ProductCategoryResponse
|
||||||
|
category_children: ProductCategoryResponse[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface AdminProductCategoryResponse {
|
||||||
|
product_category: ProductCategoryResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface AdminProductCategoryListResponse extends PaginatedResponse {
|
||||||
|
product_categories: ProductCategoryResponse[]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./admin"
|
||||||
@@ -900,6 +900,10 @@ export interface FilterableProductCategoryProps
|
|||||||
* Whether to include parents of retrieved product categories.
|
* Whether to include parents of retrieved product categories.
|
||||||
*/
|
*/
|
||||||
include_ancestors_tree?: boolean
|
include_ancestors_tree?: boolean
|
||||||
|
/**
|
||||||
|
* Filter product categories based on searchable fields
|
||||||
|
*/
|
||||||
|
q?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user