feat(dashboard,js-sdk,types,admin-shared): Add Product Types domain (#7732)

This commit is contained in:
Kasper Fabricius Kristensen
2024-06-17 18:50:55 +02:00
committed by GitHub
parent 70a72ce2df
commit 2d8d2c4255
53 changed files with 1045 additions and 62 deletions

View File

@@ -55,6 +55,13 @@ const PRODUCT_CATEGORY_INJECTION_ZONES = [
"product_category.list.after",
] as const
const PRODUCT_TYPE_INJECTION_ZONES = [
"product_type.details.before",
"product_type.details.after",
"product_type.list.before",
"product_type.list.after",
] as const
const PRICE_LIST_INJECTION_ZONES = [
"price_list.details.before",
"price_list.details.after",
@@ -198,4 +205,5 @@ export const INJECTION_ZONES = [
...WORKFLOW_INJECTION_ZONES,
...CAMPAIGN_INJECTION_ZONES,
...TAX_INJECTION_ZONES,
...PRODUCT_TYPE_INJECTION_ZONES,
] as const

View File

@@ -49,6 +49,10 @@ const useSettingRoutes = (): NavItemProps[] => {
label: t("salesChannels.domain"),
to: "/settings/sales-channels",
},
{
label: t("productTypes.domain"),
to: "/settings/product-types",
},
{
label: t("shippingProfile.domain"),
to: "/settings/shipping-profiles",

View File

@@ -1,26 +1,34 @@
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
import { client } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery,
} from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
const PRODUCT_TYPES_QUERY_KEY = "product_types" as const
const productTypesQueryKeys = queryKeysFactory(PRODUCT_TYPES_QUERY_KEY)
export const productTypesQueryKeys = queryKeysFactory(PRODUCT_TYPES_QUERY_KEY)
export const useProductType = (
id: string,
query?: Record<string, any>,
query?: HttpTypes.AdminProductTypeParams,
options?: Omit<
UseQueryOptions<
{ product_type: HttpTypes.AdminProductType },
Error,
{ product_type: HttpTypes.AdminProductType },
HttpTypes.AdminProductTypeResponse,
FetchError,
HttpTypes.AdminProductTypeResponse,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => client.productTypes.retrieve(id, query),
queryFn: () => sdk.admin.productType.retrieve(id, query),
queryKey: productTypesQueryKeys.detail(id),
...options,
})
@@ -29,22 +37,84 @@ export const useProductType = (
}
export const useProductTypes = (
query?: Record<string, any>,
query?: HttpTypes.AdminProductTypeListParams,
options?: Omit<
UseQueryOptions<
{ product_types: HttpTypes.AdminProductType[] },
Error,
{ product_types: HttpTypes.AdminProductType[] },
HttpTypes.AdminProductTypeListResponse,
FetchError,
HttpTypes.AdminProductTypeListResponse,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => client.productTypes.list(query),
queryFn: () => sdk.admin.productType.list(query),
queryKey: productTypesQueryKeys.list(query),
...options,
})
return { ...data, ...rest }
}
export const useCreateProductType = (
options?: UseMutationOptions<
HttpTypes.AdminProductTypeResponse,
FetchError,
HttpTypes.AdminCreateProductType
>
) => {
return useMutation({
mutationFn: (payload) => sdk.admin.productType.create(payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: productTypesQueryKeys.lists() })
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateProductType = (
id: string,
options?: UseMutationOptions<
HttpTypes.AdminProductTypeResponse,
FetchError,
HttpTypes.AdminUpdateProductType
>
) => {
return useMutation({
mutationFn: (payload) => sdk.admin.productType.update(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: productTypesQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: productTypesQueryKeys.lists() })
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useDeleteProductType = (
id: string,
options?: UseMutationOptions<
HttpTypes.AdminProductTypeDeleteResponse,
FetchError,
void
>
) => {
return useMutation({
mutationFn: () => sdk.admin.productType.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: productTypesQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: productTypesQueryKeys.lists() })
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -1,3 +1,4 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
@@ -223,11 +224,11 @@ export const useProduct = (
}
export const useProducts = (
query?: Record<string, any>,
query?: HttpTypes.AdminProductListParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminProductListResponse,
Error,
FetchError,
HttpTypes.AdminProductListResponse,
QueryKey
>,

View File

@@ -0,0 +1,17 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../components/table/data-table"
export const useDateTableFilters = () => {
const { t } = useTranslation()
const dateFilters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" },
{ label: t("fields.updatedAt"), key: "updated_at" },
].map((f) => ({
key: f.key,
label: f.label,
type: "date",
}))
return dateFilters
}

View File

@@ -7,6 +7,7 @@ const excludeableFields = [
"sales_channel_id",
"collections",
"categories",
"product_types",
] as const
export const useProductTableFilters = (
@@ -14,10 +15,17 @@ export const useProductTableFilters = (
) => {
const { t } = useTranslation()
const { product_types } = useProductTypes({
limit: 1000,
offset: 0,
})
const isProductTypeExcluded = exclude?.includes("product_types")
const { product_types } = useProductTypes(
{
limit: 1000,
offset: 0,
},
{
enabled: !isProductTypeExcluded,
}
)
// const { product_tags } = useAdminProductTags({
// limit: 1000,
@@ -61,7 +69,7 @@ export const useProductTableFilters = (
let filters: Filter[] = []
if (product_types) {
if (product_types && !isProductTypeExcluded) {
const typeFilter: Filter = {
key: "type_id",
label: t("fields.type"),

View File

@@ -44,7 +44,7 @@ export const useProductTableQuery = ({
q,
} = queryObject
const searchParams: HttpTypes.AdminProductParams = {
const searchParams: HttpTypes.AdminProductListParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
sales_channel_id: sales_channel_id?.split(","),

View File

@@ -1647,6 +1647,25 @@
}
}
},
"productTypes": {
"domain": "Product Types",
"create": {
"header": "Create Product Type",
"hint": "Create a new product type to categorize your products.",
"successToast": "Product type {{value}} was successfully created."
},
"edit": {
"header": "Edit Product Type",
"successToast": "Product type {{value}} was successfully updated."
},
"delete": {
"confirmation": "You are about to delete the product type {{value}}. This action cannot be undone.",
"successToast": "Product type {{value}} was successfully deleted."
},
"fields": {
"value": "Value"
}
},
"errors": {
"serverError": "Server error - Try again later.",
"invalidCredentials": "Wrong email or password"
@@ -1758,8 +1777,8 @@
"sent": "Sent",
"salesChannels": "Sales Channels",
"product": "Product",
"createdAt": "Created at",
"updatedAt": "Updated at",
"createdAt": "Created",
"updatedAt": "Updated",
"revokedAt": "Revoked at",
"true": "True",
"false": "False",

View File

@@ -848,6 +848,43 @@ export const RouteMap: RouteObject[] = [
},
],
},
{
path: "product-types",
element: <Outlet />,
handle: {
crumb: () => "Product Types",
},
children: [
{
path: "",
lazy: () =>
import("../../routes/product-types/product-type-list"),
children: [
{
path: "create",
lazy: () =>
import("../../routes/product-types/product-type-create"),
},
],
},
{
path: ":id",
lazy: () =>
import("../../routes/product-types/product-type-detail"),
handle: {
crumb: (data: HttpTypes.AdminProductTypeResponse) =>
data.product_type.value,
},
children: [
{
path: "edit",
lazy: () =>
import("../../routes/product-types/product-type-edit"),
},
],
},
],
},
{
path: "shipping-profiles",
element: <Outlet />,

View File

@@ -0,0 +1,42 @@
import { toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useDeleteProductType } from "../../../../hooks/api/product-types"
export const useDeleteProductTypeAction = (id: string) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useDeleteProductType(id)
const handleDelete = async () => {
const result = await prompt({
title: t("general.areYouSure"),
description: t("productTypes.delete.confirmation"),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!result) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("general.success"), {
description: t("productTypes.delete.successToast"),
dismissLabel: t("actions.close"),
dismissable: true,
})
},
onError: (e) => {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
dismissable: true,
})
},
})
}
return handleDelete
}

View File

@@ -0,0 +1,105 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useCreateProductType } from "../../../../../hooks/api/product-types"
const CreateProductTypeSchema = z.object({
value: z.string().min(1),
})
export const CreateProductTypeForm = () => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof CreateProductTypeSchema>>({
defaultValues: {
value: "",
},
resolver: zodResolver(CreateProductTypeSchema),
})
const { mutateAsync, isPending } = useCreateProductType()
const handleSubmit = form.handleSubmit(
async (values: z.infer<typeof CreateProductTypeSchema>) => {
await mutateAsync(values, {
onSuccess: ({ product_type }) => {
toast.success(t("general.success"), {
description: t("productTypes.create.successToast", {
value: product_type.value,
}),
dismissLabel: t("actions.close"),
dismissable: true,
})
handleSuccess(`/settings/product-types/${product_type.id}`)
},
onError: (e) => {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
dismissable: true,
})
},
})
}
)
return (
<RouteFocusModal.Form form={form}>
<form onSubmit={handleSubmit} className="flex flex-col overflow-hidden">
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button
size="small"
variant="primary"
type="submit"
isLoading={isPending}
>
{t("actions.create")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-col items-center overflow-y-auto p-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("productTypes.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("productTypes.create.hint")}
</Text>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="value"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("productTypes.fields.value")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}

View File

@@ -0,0 +1 @@
export * from "./create-product-type-form"

View File

@@ -0,0 +1 @@
export { ProductTypeCreate as Component } from "./product-type-create"

View File

@@ -0,0 +1,10 @@
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateProductTypeForm } from "./components/create-product-type-form"
export const ProductTypeCreate = () => {
return (
<RouteFocusModal>
<CreateProductTypeForm />
</RouteFocusModal>
)
}

View File

@@ -0,0 +1 @@
export * from "./product-type-general-section"

View File

@@ -0,0 +1,45 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteProductTypeAction } from "../../../common/hooks/use-delete-product-type-action"
type ProductTypeGeneralSectionProps = {
productType: HttpTypes.AdminProductType
}
export const ProductTypeGeneralSection = ({
productType,
}: ProductTypeGeneralSectionProps) => {
const { t } = useTranslation()
const handleDelete = useDeleteProductTypeAction(productType.id)
return (
<Container className="flex items-center justify-between">
<Heading>{productType.value}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: "edit",
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
</Container>
)
}

View File

@@ -0,0 +1 @@
export * from "./product-type-product-section"

View File

@@ -0,0 +1,66 @@
import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
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 ProductTypeProductSectionProps = {
productType: HttpTypes.AdminProductType
}
const PAGE_SIZE = 10
export const ProductTypeProductSection = ({
productType,
}: ProductTypeProductSectionProps) => {
const { t } = useTranslation()
const { searchParams, raw } = useProductTableQuery({
pageSize: PAGE_SIZE,
})
const { products, count, isPending, isError, error } = useProducts({
...searchParams,
type_id: [productType.id],
})
const filters = useProductTableFilters(["product_types"])
const columns = useProductTableColumns()
const { table } = useDataTable({
columns,
data: products,
count: products?.length || 0,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
if (isError) {
throw error
}
return (
<Container className="divide-y p-0">
<div className="px-6 py-4">
<Heading level="h2">{t("products.domain")}</Heading>
</div>
<DataTable
table={table}
filters={filters}
isLoading={isPending}
columns={columns}
count={count}
pageSize={PAGE_SIZE}
navigateTo={({ original }) => `/products/${original.id}`}
orderBy={["title", "created_at", "updated_at"]}
queryObject={raw}
search
pagination
/>
</Container>
)
}

View File

@@ -0,0 +1,2 @@
export { productTypeLoader as loader } from "./loader"
export { ProductTypeDetail as Component } from "./product-type-detail"

View File

@@ -0,0 +1,22 @@
import { LoaderFunctionArgs } from "react-router-dom"
import { HttpTypes } from "@medusajs/types"
import { productTypesQueryKeys } from "../../../hooks/api/product-types"
import { sdk } from "../../../lib/client"
import { queryClient } from "../../../lib/query-client"
const productTypeDetailQuery = (id: string) => ({
queryKey: productTypesQueryKeys.detail(id),
queryFn: async () => sdk.admin.productType.retrieve(id),
})
export const productTypeLoader = async ({ params }: LoaderFunctionArgs) => {
const id = params.id
const query = productTypeDetailQuery(id!)
return (
queryClient.getQueryData<HttpTypes.AdminProductTypeResponse>(
query.queryKey
) ?? (await queryClient.fetchQuery(query))
)
}

View File

@@ -0,0 +1,56 @@
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { useProductType } from "../../../hooks/api/product-types"
import { ProductTypeGeneralSection } from "./components/product-type-general-section"
import { ProductTypeProductSection } from "./components/product-type-product-section"
import { productTypeLoader } from "./loader"
import after from "virtual:medusa/widgets/product_type/details/after"
import before from "virtual:medusa/widgets/product_type/details/before"
export const ProductTypeDetail = () => {
const { id } = useParams()
const initialData = useLoaderData() as Awaited<
ReturnType<typeof productTypeLoader>
>
const { product_type, isPending, isError, error } = useProductType(
id!,
undefined,
{
initialData,
}
)
if (isPending || !product_type) {
return null
}
if (isError) {
throw error
}
return (
<div className="flex flex-col gap-y-3">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={product_type} />
</div>
)
})}
<ProductTypeGeneralSection productType={product_type} />
<ProductTypeProductSection productType={product_type} />
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={product_type} />
</div>
)
})}
<JsonViewSection data={product_type} />
<Outlet />
</div>
)
}

View File

@@ -0,0 +1,102 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { HttpTypes } from "@medusajs/types"
import { Button, Input, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { useUpdateProductType } from "../../../../../hooks/api/product-types"
const EditProductTypeSchema = z.object({
value: z.string().min(1),
})
type EditProductTypeFormProps = {
productType: HttpTypes.AdminProductType
}
export const EditProductTypeForm = ({
productType,
}: EditProductTypeFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof EditProductTypeSchema>>({
defaultValues: {
value: productType.value,
},
resolver: zodResolver(EditProductTypeSchema),
})
const { mutateAsync, isPending } = useUpdateProductType(productType.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
{
value: data.value,
},
{
onSuccess: ({ product_type }) => {
toast.success(t("general.success"), {
description: t("productTypes.edit.successToast", {
value: product_type.value,
}),
dismissable: true,
dismissLabel: t("general.close"),
})
handleSuccess()
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissable: true,
dismissLabel: t("general.close"),
})
},
}
)
})
return (
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden"
>
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
<Form.Field
control={form.control}
name="value"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("productTypes.fields.value")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
)
}

View File

@@ -0,0 +1 @@
export * from "./edit-product-type-form"

View File

@@ -0,0 +1 @@
export { ProductTypeEdit as Component } from "./product-type-edit"

View File

@@ -0,0 +1,28 @@
import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { useProductType } from "../../../hooks/api/product-types"
import { EditProductTypeForm } from "./components/edit-product-type-form"
export const ProductTypeEdit = () => {
const { id } = useParams()
const { t } = useTranslation()
const { product_type, isPending, isError, error } = useProductType(id!)
const ready = !isPending && !!product_type
if (isError) {
throw error
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("productTypes.edit.header")}</Heading>
</RouteDrawer.Header>
{ready && <EditProductTypeForm productType={product_type} />}
</RouteDrawer>
)
}

View File

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

View File

@@ -0,0 +1,41 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteProductTypeAction } from "../../../common/hooks/use-delete-product-type-action"
type ProductTypeRowActionsProps = {
productType: HttpTypes.AdminProductType
}
export const ProductTypeRowActions = ({
productType,
}: ProductTypeRowActionsProps) => {
const { t } = useTranslation()
const handleDelete = useDeleteProductTypeAction(productType.id)
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: `/settings/product-types/${productType.id}/edit`,
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
)
}

View File

@@ -0,0 +1,65 @@
import { Button, Container, Heading } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table"
import { useProductTypes } from "../../../../../hooks/api/product-types"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useProductTypeTableColumns } from "./use-product-type-table-columns"
import { useProductTypeTableFilters } from "./use-product-type-table-filters"
import { useProductTypeTableQuery } from "./use-product-type-table-query"
const PAGE_SIZE = 20
export const ProductTypeListTable = () => {
const { t } = useTranslation()
const { searchParams, raw } = useProductTypeTableQuery({
pageSize: PAGE_SIZE,
})
const { product_types, count, isLoading, isError, error } = useProductTypes(
searchParams,
{
placeholderData: keepPreviousData,
}
)
const filters = useProductTypeTableFilters()
const columns = useProductTypeTableColumns()
const { table } = useDataTable({
columns,
data: product_types,
count,
getRowId: (row) => row.id,
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("productTypes.domain")}</Heading>
<Button size="small" variant="secondary" asChild>
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<DataTable
table={table}
filters={filters}
isLoading={isLoading}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
orderBy={["value", "created_at", "updated_at"]}
navigateTo={({ original }) => original.id}
queryObject={raw}
pagination
search
/>
</Container>
)
}

View File

@@ -0,0 +1,35 @@
import { HttpTypes } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { ProductTypeRowActions } from "./product-table-row-actions"
const columnHelper = createColumnHelper<HttpTypes.AdminProductType>()
export const useProductTypeTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("value", {
header: () => t("productTypes.fields.value"),
cell: ({ getValue }) => getValue(),
}),
columnHelper.accessor("created_at", {
header: () => t("fields.createdAt"),
cell: ({ getValue }) => getValue(),
}),
columnHelper.accessor("updated_at", {
header: () => t("fields.updatedAt"),
cell: ({ getValue }) => getValue(),
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => {
return <ProductTypeRowActions productType={row.original} />
},
}),
],
[t]
)
}

View File

@@ -0,0 +1,7 @@
import { useDateTableFilters } from "../../../../../hooks/table/filters/use-date-table-filters"
export const useProductTypeTableFilters = () => {
const dateFilters = useDateTableFilters()
return dateFilters
}

View File

@@ -0,0 +1,31 @@
import { useQueryParams } from "../../../../../hooks/use-query-params"
type UseProductTypeTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useProductTypeTableQuery = ({
prefix,
pageSize = 20,
}: UseProductTypeTableQueryProps) => {
const queryObject = useQueryParams(
["offset", "q", "order", "created_at", "updated_at"],
prefix
)
const { offset, q, order, created_at, updated_at } = queryObject
const searchParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
q,
}
return {
searchParams,
raw: queryObject,
}
}

View File

@@ -0,0 +1 @@
export { ProductTypeList as Component } from "./product-type-list"

View File

@@ -0,0 +1,28 @@
import { Outlet } from "react-router-dom"
import { ProductTypeListTable } from "./components/product-type-list-table"
import after from "virtual:medusa/widgets/product_type/list/after"
import before from "virtual:medusa/widgets/product_type/list/before"
export const ProductTypeList = () => {
return (
<div className="flex flex-col gap-y-3">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
<ProductTypeListTable />
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
<Outlet />
</div>
)
}

View File

@@ -10,6 +10,7 @@ import { PriceList } from "./price-list"
import { Product } from "./product"
import { ProductCategory } from "./product-category"
import { ProductCollection } from "./product-collection"
import { ProductType } from "./product-type"
import { Region } from "./region"
import { SalesChannel } from "./sales-channel"
import { ShippingOption } from "./shipping-option"
@@ -26,6 +27,7 @@ export class Admin {
public productCategory: ProductCategory
public priceList: PriceList
public product: Product
public productType: ProductType
public upload: Upload
public region: Region
public stockLocation: StockLocation
@@ -47,6 +49,7 @@ export class Admin {
this.productCategory = new ProductCategory(client)
this.priceList = new PriceList(client)
this.product = new Product(client)
this.productType = new ProductType(client)
this.upload = new Upload(client)
this.region = new Region(client)
this.stockLocation = new StockLocation(client)

View File

@@ -0,0 +1,80 @@
import { HttpTypes } from "@medusajs/types"
import { Client } from "../client"
import { ClientHeaders } from "../types"
export class ProductType {
private client: Client
constructor(client: Client) {
this.client = client
}
async create(
body: HttpTypes.AdminCreateProductType,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminProductTypeResponse>(
`/admin/product-types`,
{
method: "POST",
headers,
body,
query,
}
)
}
async update(
id: string,
body: HttpTypes.AdminUpdateProductType,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminProductTypeResponse>(
`/admin/product-types/${id}`,
{
method: "POST",
headers,
body,
query,
}
)
}
async list(
query?: HttpTypes.AdminProductTypeListParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminProductTypeListResponse>(
`/admin/product-types`,
{
headers,
query: query,
}
)
}
async retrieve(
id: string,
query?: HttpTypes.AdminProductTypeParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminProductTypeResponse>(
`/admin/product-types/${id}`,
{
query,
headers,
}
)
}
async delete(id: string, headers?: ClientHeaders) {
return this.client.fetch<HttpTypes.AdminProductTypeDeleteResponse>(
`/admin/product-types/${id}`,
{
method: "DELETE",
headers,
}
)
}
}

View File

@@ -55,7 +55,7 @@ export class Product {
)
}
async list(
queryParams?: HttpTypes.AdminProductParams,
queryParams?: HttpTypes.AdminProductListParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminProductListResponse>(

View File

@@ -16,6 +16,7 @@ export * from "./payment"
export * from "./pricing"
export * from "./product"
export * from "./product-category"
export * from "./product-type"
export * from "./promotion"
export * from "./region"
export * from "./reservation"

View File

@@ -0,0 +1,3 @@
import { BaseProductType } from "../common"
export interface AdminProductType extends BaseProductType {}

View File

@@ -0,0 +1,4 @@
export * from "./entities"
export * from "./payloads"
export * from "./queries"
export * from "./responses"

View File

@@ -0,0 +1,9 @@
export interface AdminCreateProductType {
value: string
metadata?: Record<string, unknown> | null
}
export interface AdminUpdateProductType {
value?: string
metadata?: Record<string, unknown> | null
}

View File

@@ -0,0 +1,15 @@
import { BaseFilterable, OperatorMap } from "../../../dal"
import { FindParams, SelectParams } from "../../common"
export interface AdminProductTypeListParams
extends FindParams,
BaseFilterable<AdminProductTypeListParams> {
q?: string
id?: string | string[]
value?: string | string[]
created_at?: OperatorMap<string>
updated_at?: OperatorMap<string>
deleted_at?: OperatorMap<string>
}
export interface AdminProductTypeParams extends SelectParams {}

View File

@@ -0,0 +1,14 @@
import { DeleteResponse, PaginatedResponse } from "../../common"
import { AdminProductType } from "./entities"
export interface AdminProductTypeResponse {
product_type: AdminProductType
}
export interface AdminProductTypeListResponse
extends PaginatedResponse<{
product_types: AdminProductType[]
}> {}
export interface AdminProductTypeDeleteResponse
extends DeleteResponse<"product_type"> {}

View File

@@ -0,0 +1,8 @@
export interface BaseProductType {
id: string
value: string
created_at?: string
updated_at?: string
deleted_at?: string | null
metadata?: Record<string, unknown> | null
}

View File

@@ -0,0 +1,2 @@
export * from "./admin"
export * from "./store"

View File

@@ -0,0 +1,3 @@
import { BaseProductType } from "../common"
export interface StoreProductType extends BaseProductType {}

View File

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

View File

@@ -1,5 +1,7 @@
import { AdminCollection } from "../../collection"
import { AdminPrice } from "../../pricing"
import { AdminProductCategory } from "../../product-category"
import { AdminProductType } from "../../product-type"
import { AdminSalesChannel } from "../../sales-channel"
import {
BaseProduct,
@@ -7,7 +9,6 @@ import {
BaseProductOption,
BaseProductOptionValue,
BaseProductTag,
BaseProductType,
BaseProductVariant,
ProductStatus,
} from "../common"
@@ -16,14 +17,15 @@ export interface AdminProductVariant extends BaseProductVariant {
prices: AdminPrice[] | null
}
export interface AdminProductTag extends BaseProductTag {}
export interface AdminProductType extends BaseProductType {}
export interface AdminProductOption extends BaseProductOption {}
export interface AdminProductImage extends BaseProductImage {}
export interface AdminProductOptionValue extends BaseProductOptionValue {}
export interface AdminProduct
extends Omit<BaseProduct, "categories" | "variants"> {
collection?: AdminCollection | null
categories?: AdminProductCategory[] | null
sales_channels?: AdminSalesChannel[] | null
variants?: AdminProductVariant[] | null
type: AdminProductType | null
}
export type AdminProductStatus = ProductStatus

View File

@@ -1,16 +1,14 @@
import {
BaseProductListParams,
BaseProductOptionParams,
BaseProductParams,
BaseProductTagParams,
BaseProductTypeParams,
BaseProductVariantParams,
} from "../common"
export interface AdminProductTagParams extends BaseProductTagParams {}
export interface AdminProductTypeParams extends BaseProductTypeParams {}
export interface AdminProductOptionParams extends BaseProductOptionParams {}
export interface AdminProductVariantParams extends BaseProductVariantParams {}
export interface AdminProductParams extends BaseProductParams {
export interface AdminProductListParams extends BaseProductListParams {
price_list_id?: string | string[]
variants?: AdminProductVariantParams
}

View File

@@ -2,6 +2,7 @@ import { BaseFilterable, OperatorMap } from "../../dal"
import { BaseCollection } from "../collection/common"
import { FindParams } from "../common"
import { BaseProductCategory } from "../product-category/common"
import { BaseProductType } from "../product-type/common"
export type ProductStatus = "draft" | "proposed" | "published" | "rejected"
export interface BaseProduct {
@@ -21,10 +22,10 @@ export interface BaseProduct {
hs_code: string | null
mid_code: string | null
material: string | null
collection: BaseCollection | null
collection?: BaseCollection | null
collection_id: string | null
categories?: BaseProductCategory[] | null
type: BaseProductType | null
type?: BaseProductType | null
type_id: string | null
tags: BaseProductTag[] | null
variants: BaseProductVariant[] | null
@@ -72,15 +73,6 @@ export interface BaseProductTag {
metadata?: Record<string, unknown> | null
}
export interface BaseProductType {
id: string
value: string
created_at?: string
updated_at?: string
deleted_at?: string | null
metadata?: Record<string, unknown> | null
}
export interface BaseProductOption {
id: string
title: string
@@ -113,9 +105,9 @@ export interface BaseProductOptionValue {
deleted_at?: string | null
}
export interface BaseProductParams
export interface BaseProductListParams
extends FindParams,
BaseFilterable<BaseProductParams> {
BaseFilterable<BaseProductListParams> {
q?: string
status?: ProductStatus | ProductStatus[]
sales_channel_id?: string | string[]
@@ -143,14 +135,6 @@ export interface BaseProductTagParams
value?: string | string[]
}
export interface BaseProductTypeParams
extends FindParams,
BaseFilterable<BaseProductTypeParams> {
q?: string
id?: string | string[]
value?: string
}
export interface BaseProductOptionParams
extends FindParams,
BaseFilterable<BaseProductOptionParams> {

View File

@@ -1,21 +1,21 @@
import { StoreProductCategory } from "../../product-category"
import { StoreProductType } from "../../product-type"
import {
BaseProduct,
BaseProductImage,
BaseProductOption,
BaseProductOptionValue,
BaseProductTag,
BaseProductType,
BaseProductVariant,
ProductStatus,
} from "../common"
export interface StoreProduct extends Omit<BaseProduct, "categories"> {
categories?: StoreProductCategory[] | null
type?: StoreProductType | null
}
export interface StoreProductVariant extends BaseProductVariant {}
export interface StoreProductTag extends BaseProductTag {}
export interface StoreProductType extends BaseProductType {}
export interface StoreProductOption extends BaseProductOption {}
export interface StoreProductImage extends BaseProductImage {}
export interface StoreProductOptionValue extends BaseProductOptionValue {}

View File

@@ -1,16 +1,14 @@
import {
BaseProductListParams,
BaseProductOptionParams,
BaseProductParams,
BaseProductTagParams,
BaseProductTypeParams,
BaseProductVariantParams,
} from "../common"
export interface StoreProductTagParams extends BaseProductTagParams {}
export interface StoreProductTypeParams extends BaseProductTypeParams {}
export interface StoreProductOptionParams extends BaseProductOptionParams {}
export interface StoreProductVariantParams extends BaseProductVariantParams {}
export interface StoreProductParams extends BaseProductParams {
export interface StoreProductParams extends BaseProductListParams {
// The region ID and currency_code are not params, but are used for the pricing context. Maybe move to separate type definition.
region_id?: string
currency_code?: string

View File

@@ -4,11 +4,11 @@ import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { remapKeysForProduct, remapProductResponse } from "./helpers"
import { refetchEntities, refetchEntity } from "../../utils/refetch-entity"
import { remapKeysForProduct, remapProductResponse } from "./helpers"
export const GET = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminProductParams>,
req: AuthenticatedMedusaRequest<HttpTypes.AdminProductListParams>,
res: MedusaResponse<HttpTypes.AdminProductListResponse>
) => {
const selectFields = remapKeysForProduct(req.remoteQueryConfig.fields ?? [])

View File

@@ -1,14 +1,15 @@
import { HttpTypes } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { NextFunction } from "express"
import { MedusaRequest } from "../../../../types/routing"
import { HttpTypes } from "@medusajs/types"
export function maybeApplyPriceListsFilter() {
return async (req: MedusaRequest, _, next: NextFunction) => {
const filterableFields: HttpTypes.AdminProductParams = req.filterableFields
const filterableFields: HttpTypes.AdminProductListParams =
req.filterableFields
if (!filterableFields.price_list_id) {
return next()