diff --git a/packages/admin-next/admin-shared/src/extensions/widgets/constants.ts b/packages/admin-next/admin-shared/src/extensions/widgets/constants.ts index 67ffe46f1d..f372e96c00 100644 --- a/packages/admin-next/admin-shared/src/extensions/widgets/constants.ts +++ b/packages/admin-next/admin-shared/src/extensions/widgets/constants.ts @@ -62,6 +62,13 @@ const PRODUCT_TYPE_INJECTION_ZONES = [ "product_type.list.after", ] as const +const PRODUCT_TAG_INJECTION_ZONES = [ + "product_tag.details.before", + "product_tag.details.after", + "product_tag.list.before", + "product_tag.list.after", +] as const + const PRICE_LIST_INJECTION_ZONES = [ "price_list.details.before", "price_list.details.after", @@ -206,4 +213,5 @@ export const INJECTION_ZONES = [ ...CAMPAIGN_INJECTION_ZONES, ...TAX_INJECTION_ZONES, ...PRODUCT_TYPE_INJECTION_ZONES, + ...PRODUCT_TAG_INJECTION_ZONES, ] as const diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx index 792b252bf5..500576b0b0 100644 --- a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx +++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx @@ -50,6 +50,10 @@ const useSettingRoutes = (): NavItemProps[] => { label: t("productTypes.domain"), to: "/settings/product-types", }, + { + label: t("productTags.domain"), + to: "/settings/product-tags", + }, { label: t("stockLocations.domain"), to: "/settings/locations", diff --git a/packages/admin-next/dashboard/src/hooks/api/tags.tsx b/packages/admin-next/dashboard/src/hooks/api/tags.tsx index 0bc7f9bdae..8a306c53e8 100644 --- a/packages/admin-next/dashboard/src/hooks/api/tags.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/tags.tsx @@ -1,14 +1,22 @@ import { FetchError } from "@medusajs/js-sdk" import { HttpTypes } from "@medusajs/types" -import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query" +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 TAGS_QUERY_KEY = "tags" as const -export const tagsQueryKeys = queryKeysFactory(TAGS_QUERY_KEY) +export const productTagsQueryKeys = queryKeysFactory(TAGS_QUERY_KEY) -export const useTag = ( +export const useProductTag = ( id: string, + query?: HttpTypes.AdminProductTagParams, options?: Omit< UseQueryOptions< HttpTypes.AdminProductTagResponse, @@ -20,7 +28,7 @@ export const useTag = ( > ) => { const { data, ...rest } = useQuery({ - queryKey: tagsQueryKeys.detail(id), + queryKey: productTagsQueryKeys.detail(id, query), queryFn: async () => sdk.admin.productTag.retrieve(id), ...options, }) @@ -28,7 +36,7 @@ export const useTag = ( return { ...data, ...rest } } -export const useTags = ( +export const useProductTags = ( query?: HttpTypes.AdminProductTagListParams, options?: Omit< UseQueryOptions< @@ -41,10 +49,80 @@ export const useTags = ( > ) => { const { data, ...rest } = useQuery({ - queryKey: tagsQueryKeys.list(query), + queryKey: productTagsQueryKeys.list(query), queryFn: async () => sdk.admin.productTag.list(query), ...options, }) return { ...data, ...rest } } + +export const useCreateProductTag = ( + query?: HttpTypes.AdminProductTagParams, + options?: UseMutationOptions< + HttpTypes.AdminProductTagResponse, + FetchError, + HttpTypes.AdminCreateProductTag + > +) => { + return useMutation({ + mutationFn: async (data) => sdk.admin.productTag.create(data, query), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: productTagsQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateProductTag = ( + id: string, + query?: HttpTypes.AdminProductTagParams, + options?: UseMutationOptions< + HttpTypes.AdminProductTagResponse, + FetchError, + HttpTypes.AdminUpdateProductTag + > +) => { + return useMutation({ + mutationFn: async (data) => sdk.admin.productTag.update(id, data, query), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: productTagsQueryKeys.lists(), + }) + queryClient.invalidateQueries({ + queryKey: productTagsQueryKeys.detail(data.product_tag.id, query), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteProductTag = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminProductTagDeleteResponse, + FetchError, + void + > +) => { + return useMutation({ + mutationFn: async () => sdk.admin.productTag.delete(id), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: productTagsQueryKeys.lists(), + }) + queryClient.invalidateQueries({ + queryKey: productTagsQueryKeys.detail(id), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx index 2ec28a3fb9..8fc0035c6a 100644 --- a/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx +++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx @@ -1,5 +1,6 @@ import { useTranslation } from "react-i18next" import { Filter } from "../../../components/table/data-table" +import { useProductTags } from "../../api" import { useProductTypes } from "../../api/product-types" import { useSalesChannels } from "../../api/sales-channels" @@ -8,6 +9,7 @@ const excludeableFields = [ "collections", "categories", "product_types", + "product_tags", ] as const export const useProductTableFilters = ( @@ -27,6 +29,13 @@ export const useProductTableFilters = ( } ) + const isProductTagExcluded = exclude?.includes("product_tags") + + const { product_tags } = useProductTags({ + limit: 1000, + offset: 0, + }) + // const { product_tags } = useAdminProductTags({ // limit: 1000, // offset: 0, @@ -84,6 +93,21 @@ export const useProductTableFilters = ( filters = [...filters, typeFilter] } + if (product_tags && !isProductTagExcluded) { + const tagFilter: Filter = { + key: "tags", + label: t("fields.tag"), + type: "select", + multiple: true, + options: product_tags.map((t) => ({ + label: t.value, + value: t.id, + })), + } + + filters = [...filters, tagFilter] + } + // if (product_tags) { // const tagFilter: Filter = { // key: "tags", @@ -144,21 +168,21 @@ export const useProductTableFilters = ( // filters = [...filters, collectionFilter] // } - const giftCardFilter: Filter = { - key: "is_giftcard", - label: t("fields.giftCard"), - type: "select", - options: [ - { - label: t("fields.true"), - value: "true", - }, - { - label: t("fields.false"), - value: "false", - }, - ], - } + // const giftCardFilter: Filter = { + // key: "is_giftcard", + // label: t("fields.giftCard"), + // type: "select", + // options: [ + // { + // label: t("fields.true"), + // value: "true", + // }, + // { + // label: t("fields.false"), + // value: "false", + // }, + // ], + // } const statusFilter: Filter = { key: "status", @@ -194,7 +218,7 @@ export const useProductTableFilters = ( type: "date", })) - filters = [...filters, statusFilter, giftCardFilter, ...dateFilters] + filters = [...filters, statusFilter, ...dateFilters] return filters } diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index 029b62b2df..6a30cb0f6d 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -2172,6 +2172,26 @@ "value": "Value" } }, + "productTags": { + "domain": "Product Tags", + "create": { + "header": "Create Product Tag", + "subtitle": "Create a new product tag to categorize your products.", + "successToast": "Product tag {{value}} was successfully created." + }, + "edit": { + "header": "Edit Product Tag", + "subtitle": "Edit the value of the product tag.", + "successToast": "Product tag {{value}} was successfully updated." + }, + "delete": { + "confirmation": "You are about to delete the product tag {{value}}. This action cannot be undone.", + "successToast": "Product tag {{value}} was successfully deleted." + }, + "fields": { + "value": "Value" + } + }, "notifications": { "domain": "Notifications", "emptyState": { diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx index 17f5df3e52..f83603d2ef 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx @@ -1,9 +1,7 @@ import { - AdminApiKeyResponse, AdminProductCategoryResponse, AdminTaxRegionResponse, HttpTypes, - SalesChannelDTO, } from "@medusajs/types" import { Outlet, RouteObject } from "react-router-dom" @@ -739,7 +737,7 @@ export const RouteMap: RouteObject[] = [ path: ":id", lazy: () => import("../../routes/users/user-detail"), handle: { - crumb: (data: { user: UserDTO }) => data.user.email, + crumb: (data: HttpTypes.AdminUserResponse) => data.user.email, }, children: [ { @@ -776,7 +774,7 @@ export const RouteMap: RouteObject[] = [ lazy: () => import("../../routes/sales-channels/sales-channel-detail"), handle: { - crumb: (data: { sales_channel: SalesChannelDTO }) => + crumb: (data: HttpTypes.AdminSalesChannelResponse) => data.sales_channel.name, }, children: [ @@ -944,7 +942,43 @@ export const RouteMap: RouteObject[] = [ }, ], }, - + { + path: "product-tags", + element: , + handle: { + crumb: () => "Product Tags", + }, + children: [ + { + path: "", + lazy: () => + import("../../routes/product-tags/product-tag-list"), + children: [ + { + path: "create", + lazy: () => + import("../../routes/product-tags/product-tag-create"), + }, + ], + }, + { + path: ":id", + lazy: () => + import("../../routes/product-tags/product-tag-detail"), + handle: { + crumb: (data: HttpTypes.AdminProductTagResponse) => + data.product_tag.value, + }, + children: [ + { + path: "edit", + lazy: () => + import("../../routes/product-tags/product-tag-edit"), + }, + ], + }, + ], + }, { path: "workflows", element: , @@ -1051,7 +1085,7 @@ export const RouteMap: RouteObject[] = [ "../../routes/api-key-management/api-key-management-detail" ), handle: { - crumb: (data: AdminApiKeyResponse) => { + crumb: (data: HttpTypes.AdminApiKeyResponse) => { return data.api_key.title }, }, @@ -1110,7 +1144,7 @@ export const RouteMap: RouteObject[] = [ "../../routes/api-key-management/api-key-management-detail" ), handle: { - crumb: (data: AdminApiKeyResponse) => { + crumb: (data: HttpTypes.AdminApiKeyResponse) => { return data.api_key.title }, }, diff --git a/packages/admin-next/dashboard/src/routes/product-tags/common/hooks/use-delete-product-tag-action.tsx b/packages/admin-next/dashboard/src/routes/product-tags/common/hooks/use-delete-product-tag-action.tsx new file mode 100644 index 0000000000..8bad7f9278 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/common/hooks/use-delete-product-tag-action.tsx @@ -0,0 +1,52 @@ +import { HttpTypes } from "@medusajs/types" +import { toast, usePrompt } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import { useDeleteProductTag } from "../../../../hooks/api" + +type UseDeleteProductTagActionProps = { + productTag: HttpTypes.AdminProductTag +} + +export const useDeleteProductTagAction = ({ + productTag, +}: UseDeleteProductTagActionProps) => { + const { t } = useTranslation() + const prompt = usePrompt() + const navigate = useNavigate() + + const { mutateAsync } = useDeleteProductTag(productTag.id) + + const handleDelete = async () => { + const confirmed = await prompt({ + title: t("general.areYouSure"), + description: t("productTags.delete.confirmation", { + value: productTag.value, + }), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!confirmed) { + return + } + + await mutateAsync(undefined, { + onSuccess: () => { + toast.success( + t("productTags.delete.successToast", { + value: productTag.value, + }) + ) + navigate("/settings/product-tags", { + replace: true, + }) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + } + + return handleDelete +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/components/product-tag-create-form/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/components/product-tag-create-form/index.ts new file mode 100644 index 0000000000..96590d5e19 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/components/product-tag-create-form/index.ts @@ -0,0 +1 @@ +export * from "./product-tag-create-form" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/components/product-tag-create-form/product-tag-create-form.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/components/product-tag-create-form/product-tag-create-form.tsx new file mode 100644 index 0000000000..9ffeb8da43 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/components/product-tag-create-form/product-tag-create-form.tsx @@ -0,0 +1,99 @@ +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/modals" +import { useCreateProductTag } from "../../../../../hooks/api" + +const ProductTagCreateSchema = z.object({ + value: z.string().min(1), +}) + +export const ProductTagCreateForm = () => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + value: "", + }, + resolver: zodResolver(ProductTagCreateSchema), + }) + + const { mutateAsync, isPending } = useCreateProductTag() + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync(data, { + onSuccess: ({ product_tag }) => { + toast.success( + t("productTags.create.successToast", { + value: product_tag.value, + }) + ) + handleSuccess(`../${product_tag.id}`) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + }) + + return ( + +
+ + +
+
+ + {t("productTags.create.header")} + + + + {t("productTags.create.subtitle")} + + +
+
+ { + return ( + + {t("productTags.fields.value")} + + + + + + ) + }} + /> +
+
+
+ +
+ + + + +
+
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/index.ts new file mode 100644 index 0000000000..1d436588ac --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/index.ts @@ -0,0 +1 @@ +export { ProductTagCreate as Component } from "./product-tag-create" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/product-tag-create.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/product-tag-create.tsx new file mode 100644 index 0000000000..61df2a739d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-create/product-tag-create.tsx @@ -0,0 +1,10 @@ +import { RouteFocusModal } from "../../../components/modals" +import { ProductTagCreateForm } from "./components/product-tag-create-form" + +export const ProductTagCreate = () => { + return ( + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-general-section/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-general-section/index.ts new file mode 100644 index 0000000000..35d2dd8ad2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-general-section/index.ts @@ -0,0 +1 @@ +export * from "./product-tag-general-section" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-general-section/product-tag-general-section.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-general-section/product-tag-general-section.tsx new file mode 100644 index 0000000000..ed2d78b22b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-general-section/product-tag-general-section.tsx @@ -0,0 +1,48 @@ +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 { useDeleteProductTagAction } from "../../../common/hooks/use-delete-product-tag-action" + +type ProductTagGeneralSectionProps = { + productTag: HttpTypes.AdminProductTag +} + +export const ProductTagGeneralSection = ({ + productTag, +}: ProductTagGeneralSectionProps) => { + const { t } = useTranslation() + const handleDelete = useDeleteProductTagAction({ productTag }) + + return ( + +
+ # + {productTag.value} +
+ , + label: t("actions.edit"), + to: "edit", + }, + ], + }, + { + actions: [ + { + icon: , + label: t("actions.delete"), + onClick: handleDelete, + }, + ], + }, + ]} + /> +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/index.ts new file mode 100644 index 0000000000..1e151a6dea --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/index.ts @@ -0,0 +1 @@ +export * from "./product-tag-product-section" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/product-tag-product-section.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/product-tag-product-section.tsx new file mode 100644 index 0000000000..67aa0dd410 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/product-tag-product-section.tsx @@ -0,0 +1,69 @@ +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" +import { useProductTableColumns } from "../../../../../hooks/table/columns" +import { useProductTableFilters } from "../../../../../hooks/table/filters" +import { useProductTableQuery } from "../../../../../hooks/table/query" +import { useDataTable } from "../../../../../hooks/use-data-table" + +type ProductTagProductSectionProps = { + productTag: HttpTypes.AdminProductTag +} + +const PAGE_SIZE = 10 +const PREFIX = "pt" + +export const ProductTagProductSection = ({ + productTag, +}: ProductTagProductSectionProps) => { + const { t } = useTranslation() + + const { searchParams, raw } = useProductTableQuery({ + pageSize: PAGE_SIZE, + prefix: PREFIX, + }) + + const { products, count, isPending, isError, error } = useProducts({ + ...searchParams, + tags: productTag.id, + }) + + const filters = useProductTableFilters(["product_tags"]) + const columns = useProductTableColumns() + + const { table } = useDataTable({ + data: products, + count, + columns, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + prefix: PREFIX, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("products.domain")} +
+ row.original.id} + search + pagination + orderBy={["title", "status", "created_at", "updated_at"]} + /> +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/index.ts new file mode 100644 index 0000000000..5464ac6b2e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/index.ts @@ -0,0 +1,2 @@ +export { productTagLoader as loader } from "./loader" +export { ProductTagDetail as Component } from "./product-tag-detail" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/loader.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/loader.ts new file mode 100644 index 0000000000..896faa6016 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/loader.ts @@ -0,0 +1,20 @@ +import { LoaderFunctionArgs } from "react-router-dom" + +import { productTagsQueryKeys } from "../../../hooks/api" +import { sdk } from "../../../lib/client" +import { queryClient } from "../../../lib/query-client" + +const productTagDetailQuery = (id: string) => ({ + queryKey: productTagsQueryKeys.detail(id), + queryFn: async () => sdk.admin.productTag.retrieve(id), +}) + +export const productTagLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = productTagDetailQuery(id!) + + return ( + queryClient.getQueryData(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/product-tag-detail.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/product-tag-detail.tsx new file mode 100644 index 0000000000..2f963ebd15 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/product-tag-detail.tsx @@ -0,0 +1,42 @@ +import { useLoaderData, useParams } from "react-router-dom" + +import { SingleColumnPageSkeleton } from "../../../components/common/skeleton" +import { SingleColumnPage } from "../../../components/layout/pages" +import { useProductTag } from "../../../hooks/api" +import { ProductTagGeneralSection } from "./components/product-tag-general-section" +import { ProductTagProductSection } from "./components/product-tag-product-section" +import { productTagLoader } from "./loader" + +import after from "virtual:medusa/widgets/product_tag/details/after" +import before from "virtual:medusa/widgets/product_tag/details/before" + +export const ProductTagDetail = () => { + const { id } = useParams() + + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { product_tag, isPending, isError, error } = useProductTag( + id!, + undefined, + { + initialData, + } + ) + + if (isPending || !product_tag) { + return + } + + if (isError) { + throw error + } + + return ( + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/components/product-tag-edit-form/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/components/product-tag-edit-form/index.ts new file mode 100644 index 0000000000..b988a4397a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/components/product-tag-edit-form/index.ts @@ -0,0 +1 @@ +export * from "./product-tag-edit-form" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/components/product-tag-edit-form/product-tag-edit-form.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/components/product-tag-edit-form/product-tag-edit-form.tsx new file mode 100644 index 0000000000..86132da402 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/components/product-tag-edit-form/product-tag-edit-form.tsx @@ -0,0 +1,87 @@ +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/modals" +import { useUpdateProductTag } from "../../../../../hooks/api" + +type ProductTagEditFormProps = { + productTag: HttpTypes.AdminProductTag +} + +const ProductTagEditSchema = z.object({ + value: z.string().min(1), +}) + +export const ProductTagEditForm = ({ productTag }: ProductTagEditFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + value: productTag.value, + }, + resolver: zodResolver(ProductTagEditSchema), + }) + + const { mutateAsync, isPending } = useUpdateProductTag(productTag.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync(data, { + onSuccess: ({ product_tag }) => { + toast.success( + t("productTags.edit.successToast", { + value: product_tag.value, + }) + ) + handleSuccess() + }, + onError: (error) => { + toast.error(error.message) + }, + }) + }) + + return ( + +
+ + { + return ( + + {t("productTags.fields.value")} + + + + + + ) + }} + /> + + +
+ + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/index.ts new file mode 100644 index 0000000000..c6ee325b6b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/index.ts @@ -0,0 +1 @@ +export { ProductTagEdit as Component } from "./product-tag-edit" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/product-tag-edit.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/product-tag-edit.tsx new file mode 100644 index 0000000000..a390c58aa9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-edit/product-tag-edit.tsx @@ -0,0 +1,33 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { RouteDrawer } from "../../../components/modals" +import { useProductTag } from "../../../hooks/api" +import { ProductTagEditForm } from "./components/product-tag-edit-form" + +export const ProductTagEdit = () => { + const { id } = useParams() + const { t } = useTranslation() + + const { product_tag, isPending, isError, error } = useProductTag(id!) + + const ready = !isPending && !!product_tag + + if (isError) { + throw error + } + + return ( + + + + {t("productTags.edit.header")} + + + {t("productTags.edit.subtitle")} + + + {ready && } + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/components/product-tag-list-table/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/components/product-tag-list-table/index.ts new file mode 100644 index 0000000000..1cf3c38db6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/components/product-tag-list-table/index.ts @@ -0,0 +1 @@ +export * from "./product-tag-list-table" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/components/product-tag-list-table/product-tag-list-table.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/components/product-tag-list-table/product-tag-list-table.tsx new file mode 100644 index 0000000000..067ea7f0c0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/components/product-tag-list-table/product-tag-list-table.tsx @@ -0,0 +1,129 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { HttpTypes } from "@medusajs/types" +import { Button, Container, Heading } 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 { Link, useLoaderData } from "react-router-dom" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { DataTable } from "../../../../../components/table/data-table" +import { useProductTags } from "../../../../../hooks/api" +import { useProductTagTableColumns } from "../../../../../hooks/table/columns" +import { useProductTagTableFilters } from "../../../../../hooks/table/filters" +import { useProductTagTableQuery } from "../../../../../hooks/table/query" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { useDeleteProductTagAction } from "../../../common/hooks/use-delete-product-tag-action" +import { productTagListLoader } from "../../loader" + +const PAGE_SIZE = 20 + +export const ProductTagListTable = () => { + const { t } = useTranslation() + const { searchParams, raw } = useProductTagTableQuery({ + pageSize: PAGE_SIZE, + }) + + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { product_tags, count, isPending, isError, error } = useProductTags( + searchParams, + { + initialData, + placeholderData: keepPreviousData, + } + ) + + const columns = useColumns() + const filters = useProductTagTableFilters() + + const { table } = useDataTable({ + data: product_tags, + count, + columns, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("productTags.domain")} + +
+ row.original.id} + search + pagination + orderBy={["value", "created_at", "updated_at"]} + /> +
+ ) +} + +const ProductTagRowActions = ({ + productTag, +}: { + productTag: HttpTypes.AdminProductTag +}) => { + const { t } = useTranslation() + const handleDelete = useDeleteProductTagAction({ productTag }) + + return ( + , + label: t("actions.edit"), + to: `${productTag.id}/edit`, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("actions.delete"), + onClick: handleDelete, + }, + ], + }, + ]} + /> + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const base = useProductTagTableColumns() + + return useMemo( + () => [ + ...base, + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [base] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/index.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/index.ts new file mode 100644 index 0000000000..ed0826b455 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/index.ts @@ -0,0 +1,2 @@ +export { productTagListLoader as loader } from "./loader" +export { ProductTagList as Component } from "./product-tag-list" diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/loader.ts b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/loader.ts new file mode 100644 index 0000000000..e1e9f58507 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/loader.ts @@ -0,0 +1,34 @@ +import { HttpTypes } from "@medusajs/types" +import { LoaderFunctionArgs } from "react-router-dom" + +import { productTagsQueryKeys } from "../../../hooks/api" +import { sdk } from "../../../lib/client" +import { queryClient } from "../../../lib/query-client" + +const productTagListQuery = (query?: HttpTypes.AdminProductTagListParams) => ({ + queryKey: productTagsQueryKeys.list(query), + queryFn: async () => sdk.admin.productTag.list(query), +}) + +export const productTagListLoader = async ({ request }: LoaderFunctionArgs) => { + const searchParams = new URL(request.url).searchParams + + const queryObject: Record = {} + + searchParams.forEach((value, key) => { + try { + queryObject[key] = JSON.parse(value) + } catch (_e) { + queryObject[key] = value + } + }) + + const query = productTagListQuery( + queryObject as HttpTypes.AdminProductTagListParams + ) + + return ( + queryClient.getQueryData(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/product-tag-list.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/product-tag-list.tsx new file mode 100644 index 0000000000..72311e86a1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-list/product-tag-list.tsx @@ -0,0 +1,21 @@ +import { SingleColumnPage } from "../../../components/layout/pages" +import { ProductTagListTable } from "./components/product-tag-list-table" + +import after from "virtual:medusa/widgets/product_tag/list/after" +import before from "virtual:medusa/widgets/product_tag/list/before" + +export const ProductTagList = () => { + return ( + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/tax-regions/common/components/target-form/target-form.tsx b/packages/admin-next/dashboard/src/routes/tax-regions/common/components/target-form/target-form.tsx index 61702c32dd..cdc893c3fe 100644 --- a/packages/admin-next/dashboard/src/routes/tax-regions/common/components/target-form/target-form.tsx +++ b/packages/admin-next/dashboard/src/routes/tax-regions/common/components/target-form/target-form.tsx @@ -2,46 +2,46 @@ import { HttpTypes } from "@medusajs/types" import { Button, Checkbox } from "@medusajs/ui" import { keepPreviousData } from "@tanstack/react-query" import { - OnChangeFn, - RowSelectionState, - createColumnHelper, + OnChangeFn, + RowSelectionState, + createColumnHelper, } from "@tanstack/react-table" import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { useSearchParams } from "react-router-dom" import { - StackedDrawer, - StackedFocusModal, + StackedDrawer, + StackedFocusModal, } from "../../../../../components/modals" import { DataTable } from "../../../../../components/table/data-table" import { - useCollections, - useCustomerGroups, - useProductTypes, - useProducts, - useTags, + useCollections, + useCustomerGroups, + useProductTags, + useProductTypes, + useProducts, } from "../../../../../hooks/api" import { - useCollectionTableColumns, - useCustomerGroupTableColumns, - useProductTableColumns, - useProductTagTableColumns, - useProductTypeTableColumns, + useCollectionTableColumns, + useCustomerGroupTableColumns, + useProductTableColumns, + useProductTagTableColumns, + useProductTypeTableColumns, } from "../../../../../hooks/table/columns" import { - useCollectionTableFilters, - useCustomerGroupTableFilters, - useProductTableFilters, - useProductTagTableFilters, - useProductTypeTableFilters, + useCollectionTableFilters, + useCustomerGroupTableFilters, + useProductTableFilters, + useProductTagTableFilters, + useProductTypeTableFilters, } from "../../../../../hooks/table/filters" import { - useCollectionTableQuery, - useCustomerGroupTableQuery, - useProductTableQuery, - useProductTagTableQuery, - useProductTypeTableQuery, + useCollectionTableQuery, + useCustomerGroupTableQuery, + useProductTableQuery, + useProductTagTableQuery, + useProductTypeTableQuery, } from "../../../../../hooks/table/query" import { useDataTable } from "../../../../../hooks/use-data-table" import { TaxRateRuleReferenceType } from "../../constants" @@ -664,7 +664,7 @@ const ProductTagTable = ({ prefix: PREFIX_PRODUCT_TAG, }) - const { product_tags, count, isLoading, isError, error } = useTags( + const { product_tags, count, isLoading, isError, error } = useProductTags( searchParams, { placeholderData: keepPreviousData, diff --git a/packages/admin-next/dashboard/src/routes/tax-regions/common/components/tax-override-card/tax-override-card.tsx b/packages/admin-next/dashboard/src/routes/tax-regions/common/components/tax-override-card/tax-override-card.tsx index 5d9a4557a9..b4264c5aad 100644 --- a/packages/admin-next/dashboard/src/routes/tax-regions/common/components/tax-override-card/tax-override-card.tsx +++ b/packages/admin-next/dashboard/src/routes/tax-regions/common/components/tax-override-card/tax-override-card.tsx @@ -1,8 +1,8 @@ import { - ArrowDownRightMini, - PencilSquare, - Trash, - TriangleRightMini, + ArrowDownRightMini, + PencilSquare, + Trash, + TriangleRightMini, } from "@medusajs/icons" import { HttpTypes } from "@medusajs/types" import { Badge, IconButton, StatusBadge, Text, Tooltip } from "@medusajs/ui" @@ -17,7 +17,7 @@ import { useCollections } from "../../../../../hooks/api/collections" import { useCustomerGroups } from "../../../../../hooks/api/customer-groups" import { useProductTypes } from "../../../../../hooks/api/product-types" import { useProducts } from "../../../../../hooks/api/products" -import { useTags } from "../../../../../hooks/api/tags" +import { useProductTags } from "../../../../../hooks/api/tags" import { formatPercentage } from "../../../../../lib/percentage-helpers" import { TaxRateRuleReferenceType } from "../../constants" import { useDeleteTaxRateAction } from "../../hooks" @@ -277,7 +277,7 @@ const useReferenceValues = ( } ) - const tags = useTags( + const tags = useProductTags( { id: ids, limit: 10, diff --git a/packages/admin-next/dashboard/src/routes/tax-regions/tax-region-tax-override-edit/tax-region-tax-override-edit.tsx b/packages/admin-next/dashboard/src/routes/tax-regions/tax-region-tax-override-edit/tax-region-tax-override-edit.tsx index 429b7234f6..3bfcf73a81 100644 --- a/packages/admin-next/dashboard/src/routes/tax-regions/tax-region-tax-override-edit/tax-region-tax-override-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/tax-regions/tax-region-tax-override-edit/tax-region-tax-override-edit.tsx @@ -8,7 +8,7 @@ import { useCollections } from "../../../hooks/api/collections" import { useCustomerGroups } from "../../../hooks/api/customer-groups" import { useProductTypes } from "../../../hooks/api/product-types" import { useProducts } from "../../../hooks/api/products" -import { useTags } from "../../../hooks/api/tags" +import { useProductTags } from "../../../hooks/api/tags" import { useTaxRate } from "../../../hooks/api/tax-rates" import { TaxRateRuleReferenceType } from "../common/constants" import { TaxRegionTaxOverrideEditForm } from "./components/tax-region-tax-override-edit-form" @@ -93,7 +93,7 @@ const useDefaultRulesValues = ( }, { ids: idsByReferenceType[TaxRateRuleReferenceType.PRODUCT_TAG], - hook: useTags, + hook: useProductTags, key: TaxRateRuleReferenceType.PRODUCT_TAG, getResult: (result: any) => result.tags.map((tag: any) => ({ diff --git a/packages/core/types/src/http/product/common.ts b/packages/core/types/src/http/product/common.ts index 2506126fab..d6f1152be7 100644 --- a/packages/core/types/src/http/product/common.ts +++ b/packages/core/types/src/http/product/common.ts @@ -111,13 +111,11 @@ export interface BaseProductListParams handle?: string | string[] id?: string | string[] is_giftcard?: boolean - tags?: { - value?: string[] - } + tags?: string | string[] type_id?: string | string[] - category_id?: string | string[] | OperatorMap - categories?: { id: OperatorMap } | { id: OperatorMap } - collection_id?: string | string[] | OperatorMap + category_id?: string | string[] + categories?: string | string[] + collection_id?: string | string[] created_at?: OperatorMap updated_at?: OperatorMap deleted_at?: OperatorMap diff --git a/packages/medusa/src/api/utils/common-validators/products/index.ts b/packages/medusa/src/api/utils/common-validators/products/index.ts index 5f187e78c9..226f06cf6a 100644 --- a/packages/medusa/src/api/utils/common-validators/products/index.ts +++ b/packages/medusa/src/api/utils/common-validators/products/index.ts @@ -11,11 +11,11 @@ export const GetProductsParams = z.object({ title: z.string().nullish(), handle: z.string().nullish(), is_giftcard: OptionalBooleanValidator, - category_id: z.string().array().nullish(), - sales_channel_id: z.string().array().nullish(), - collection_id: z.string().array().nullish(), - tags: z.string().array().optional(), - type_id: z.string().array().optional(), + category_id: z.union([z.string(), z.array(z.string())]).nullish(), + sales_channel_id: z.union([z.string(), z.array(z.string())]).nullish(), + collection_id: z.union([z.string(), z.array(z.string())]).nullish(), + tags: z.union([z.string(), z.array(z.string())]).optional(), + type_id: z.union([z.string(), z.array(z.string())]).optional(), created_at: createOperatorMap().optional(), updated_at: createOperatorMap().optional(), deleted_at: createOperatorMap().optional(),