feat: Categories retrieve + list API (#7009)

This commit is contained in:
Oli Juhl
2024-04-08 19:26:34 +02:00
committed by GitHub
parent db2f0ef53f
commit 6cc9a5e469
41 changed files with 1176 additions and 35 deletions
@@ -182,6 +182,7 @@ export const DataTableRoot = <TData,>({
{table.getRowModel().rows.map((row) => {
const to = navigateTo ? navigateTo(row) : undefined
const isRowDisabled = hasSelect && !row.getCanSelect()
return (
<Table.Row
key={row.id}
@@ -192,7 +193,8 @@ export const DataTableRoot = <TData,>({
"cursor-pointer": !!to,
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
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}
@@ -211,6 +213,15 @@ export const DataTableRoot = <TData,>({
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 (
<Table.Cell
key={cell.id}
@@ -221,9 +232,14 @@ export const DataTableRoot = <TData,>({
isStickyCell && hasSelect && !isSelectCell,
"after:bg-ui-border-base":
showStickyBorder && isStickyCell && !isSelectCell,
"bg-ui-bg-subtle hover:bg-ui-bg-subtle":
"!bg-ui-bg-disabled !hover:bg-ui-bg-disabled":
isRowDisabled,
})}
style={{
paddingLeft: depthOffset
? `${depthOffset}px`
: undefined,
}}
>
{flexRender(
cell.column.columnDef.cell,
@@ -1,21 +1,30 @@
import {
AdminProductCategoryListResponse,
AdminProductCategoryResponse,
} from "@medusajs/types"
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,
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<CategoryRes, Error, CategoryRes, QueryKey>,
UseQueryOptions<
AdminProductCategoryResponse,
Error,
AdminProductCategoryResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: categoriesQueryKeys.detail(id),
queryFn: async () => client.categories.retrieve(id),
queryKey: categoriesQueryKeys.detail(id, query),
queryFn: async () => client.categories.retrieve(id, query),
...options,
})
@@ -25,7 +34,12 @@ export const useCategory = (
export const useCategories = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<CategoriesListRes, Error, CategoriesListRes, QueryKey>,
UseQueryOptions<
AdminProductCategoryListResponse,
Error,
AdminProductCategoryListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
@@ -3,7 +3,11 @@ import { Filter } from "../../../components/table/data-table"
import { useProductTypes } from "../../api/product-types"
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 = (
exclude?: (typeof excludeableFields)[number][]
@@ -33,11 +37,15 @@ export const useProductTableFilters = (
}
)
const isCategoryExcluded = exclude?.includes("categories")
// const { product_categories } = useAdminProductCategories({
// limit: 1000,
// offset: 0,
// fields: "id,name",
// expand: "",
// }, {
// enabled: !isCategoryExcluded,
// })
const isCollectionExcluded = exclude?.includes("collections")
@@ -5,6 +5,7 @@ import {
Row,
RowSelectionState,
getCoreRowModel,
getExpandedRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table"
@@ -22,7 +23,9 @@ type UseDataTableProps<TData> = {
updater: OnChangeFn<RowSelectionState>
}
enablePagination?: boolean
enableExpandableRows?: boolean
getRowId?: (original: TData, index: number) => string
getSubRows?: (original: TData) => TData[]
meta?: Record<string, unknown>
prefix?: string
}
@@ -34,7 +37,9 @@ export const useDataTable = <TData,>({
pageSize: _pageSize = 20,
enablePagination = true,
enableRowSelection = false,
enableExpandableRows = false,
rowSelection: _rowSelection,
getSubRows,
getRowId,
meta,
prefix,
@@ -107,6 +112,7 @@ export const useDataTable = <TData,>({
pageCount: Math.ceil((count ?? 0) / pageSize),
enableRowSelection,
getRowId,
getSubRows,
onRowSelectionChange: enableRowSelection ? setRowSelection : undefined,
onPaginationChange: enablePagination
? (onPaginationChange as OnChangeFn<PaginationState>)
@@ -115,6 +121,9 @@ export const useDataTable = <TData,>({
getPaginationRowModel: enablePagination
? getPaginationRowModel()
: undefined,
getExpandedRowModel: enableExpandableRows
? getExpandedRowModel()
: undefined,
manualPagination: enablePagination ? true : undefined,
meta,
})
@@ -1,18 +1,24 @@
import {
ProductCollectionListRes,
ProductCollectionRes,
} from "../../types/api-responses"
AdminProductCategoryListResponse,
AdminProductCategoryResponse,
} from "@medusajs/types"
import { getRequest } from "./common"
async function listProductCategories(query?: Record<string, any>) {
return getRequest<ProductCollectionListRes>(`/admin/categories`, query)
return getRequest<AdminProductCategoryListResponse>(
`/admin/product-categories`,
query
)
}
async function retrieveProductCategory(
id: string,
query?: Record<string, any>
) {
return getRequest<ProductCollectionRes>(`/admin/categories/${id}`, query)
return getRequest<AdminProductCategoryResponse>(
`/admin/product-categories/${id}`,
query
)
}
export const categories = {
@@ -11,8 +11,13 @@ type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
]
details: () => readonly [...TQueryKey<TKey>["all"], "detail"]
detail: (
id: TDetailQuery
) => readonly [...ReturnType<TQueryKey<TKey>["details"]>, TDetailQuery]
id: TDetailQuery,
query?: TListQuery
) => readonly [
...ReturnType<TQueryKey<TKey>["details"]>,
TDetailQuery,
{ query: TListQuery | undefined },
]
}
export type UseQueryOptionsWrapper<
@@ -39,7 +44,11 @@ export const queryKeysFactory = <
lists: () => [...queryKeyFactory.all, "list"],
list: (query?: TListQueryType) => [...queryKeyFactory.lists(), { query }],
details: () => [...queryKeyFactory.all, "detail"],
detail: (id: TDetailQueryType) => [...queryKeyFactory.details(), id],
detail: (id: TDetailQueryType, query?: TListQueryType) => [
...queryKeyFactory.details(),
id,
{ query },
],
}
return queryKeyFactory
}
@@ -1,5 +1,4 @@
import { SalesChannelDTO, UserDTO } from "@medusajs/types"
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
import { AdminCustomersRes } from "@medusajs/client-types"
import { Spinner } from "@medusajs/icons"
import {
AdminCollectionsRes,
@@ -7,14 +6,19 @@ import {
AdminPromotionRes,
AdminRegionsRes,
} 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 { MainLayout } from "../../components/layout-v2/main-layout"
import { SettingsLayout } from "../../components/layout/settings-layout"
import { useMe } from "../../hooks/api/users"
import { AdminApiKeyResponse } from "@medusajs/types"
import { SearchProvider } from "../search-provider"
import { SidebarProvider } from "../sidebar-provider"
import { AdminCustomersRes } from "@medusajs/client-types"
export const ProtectedRoute = () => {
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",
handle: {
@@ -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>
)
}
@@ -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>
)
}
@@ -0,0 +1 @@
export * from "./category-general-section"
@@ -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>
)
}
@@ -0,0 +1 @@
export * from "./category-organization-section"
@@ -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>
)
}
@@ -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>
)
}
@@ -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]
)
}
@@ -0,0 +1 @@
export * from "./category-list-table"
@@ -0,0 +1 @@
export { CategoryList as Component } from "./category-list"
@@ -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,
}))
}