From 75924b682f3ff1867f43cf305394f2cfc0b03487 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 16 Feb 2023 10:22:23 +0100 Subject: [PATCH] feat(medusa-react): add product category queries and mutations (#3218) --- .changeset/curvy-schools-study.md | 7 + .../src/resources/admin/product-categories.ts | 8 +- .../medusa-react/mocks/data/fixtures.json | 8 + packages/medusa-react/mocks/handlers/admin.ts | 76 ++++++++ .../medusa-react/src/hooks/admin/index.ts | 1 + .../hooks/admin/product-categories/index.ts | 2 + .../admin/product-categories/mutations.ts | 167 ++++++++++++++++++ .../hooks/admin/product-categories/queries.ts | 54 ++++++ .../product-categories/mutations.test.ts | 132 ++++++++++++++ .../admin/product-categories/queries.test.ts | 37 ++++ .../routes/admin/product-categories/index.ts | 1 + .../list-product-categories.ts | 2 +- 12 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 .changeset/curvy-schools-study.md create mode 100644 packages/medusa-react/src/hooks/admin/product-categories/index.ts create mode 100644 packages/medusa-react/src/hooks/admin/product-categories/mutations.ts create mode 100644 packages/medusa-react/src/hooks/admin/product-categories/queries.ts create mode 100644 packages/medusa-react/test/hooks/admin/product-categories/mutations.test.ts create mode 100644 packages/medusa-react/test/hooks/admin/product-categories/queries.test.ts diff --git a/.changeset/curvy-schools-study.md b/.changeset/curvy-schools-study.md new file mode 100644 index 0000000000..33495faad9 --- /dev/null +++ b/.changeset/curvy-schools-study.md @@ -0,0 +1,7 @@ +--- +"@medusajs/medusa-js": patch +"medusa-react": patch +"@medusajs/medusa": patch +--- + +feat(medusa-react): add product category queries and mutations diff --git a/packages/medusa-js/src/resources/admin/product-categories.ts b/packages/medusa-js/src/resources/admin/product-categories.ts index 36e534ffc1..1a6784c208 100644 --- a/packages/medusa-js/src/resources/admin/product-categories.ts +++ b/packages/medusa-js/src/resources/admin/product-categories.ts @@ -8,8 +8,10 @@ import { AdminProductCategoriesListRes, AdminProductCategoriesCategoryRes, AdminGetProductCategoryParams, + AdminPostProductCategoriesCategoryReq, } from "@medusajs/medusa" import qs from "qs" + import { ResponsePromise } from "../../typings" import BaseResource from "../base" @@ -55,7 +57,7 @@ class AdminProductCategoriesResource extends BaseResource { */ update( productCategoryId: string, - payload: AdminPostProductCategoriesCategoryParams, + payload: AdminPostProductCategoriesCategoryReq, customHeaders: Record = {} ): ResponsePromise { const path = `/admin/product-categories/${productCategoryId}` @@ -63,10 +65,10 @@ class AdminProductCategoriesResource extends BaseResource { } /** - * Retrieve a list of product categorys + * Retrieve a list of product categories * @experimental This feature is under development and may change in the future. * To use this feature please enable featureflag `product_categories` in your medusa backend project. - * @description Retrieve a list of product categorys + * @description Retrieve a list of product categories * @returns the list of product category as well as the pagination properties */ list( diff --git a/packages/medusa-react/mocks/data/fixtures.json b/packages/medusa-react/mocks/data/fixtures.json index 0bbe720df5..41f9eab163 100644 --- a/packages/medusa-react/mocks/data/fixtures.json +++ b/packages/medusa-react/mocks/data/fixtures.json @@ -356,6 +356,14 @@ "updated_at": "2021-03-16T21:24:13.657Z", "metadata": null }, + "product_category": { + "id": "pcat_01F0YESBFAZ0DV6V831JXWH0BG", + "name": "Skinny Jeans", + "handle": "skinny-jeans", + "created_at": "2021-03-16T21:24:07.273Z", + "updated_at": "2021-03-16T21:24:07.273Z", + "deleted_at": null + }, "product_variant": { "id": "variant_01F0YESHR7P2YAYBDBY5B6X3PK", "title": "Test variant", diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 16b813506c..0f76116a09 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -130,6 +130,82 @@ export const adminHandlers = [ ) }), + rest.get("/admin/product-categories/", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + product_categories: fixtures.list("product_category"), + }) + ) + }), + + rest.get("/admin/product-categories/:id", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + product_category: fixtures.get("product_category"), + }) + ) + }), + + rest.post("/admin/product-categories/", (req, res, ctx) => { + const body = req.body as Record + return res( + ctx.status(200), + ctx.json({ + product_category: { + ...fixtures.get("product_category"), + ...body, + }, + }) + ) + }), + + rest.post("/admin/product-categories/:id", (req, res, ctx) => { + const body = req.body as Record + return res( + ctx.status(200), + ctx.json({ + product_category: { + ...fixtures.get("product_category"), + ...body, + }, + }) + ) + }), + + rest.delete("/admin/product-categories/:id", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + id: req.params.id, + object: "product-category", + deleted: true, + }) + ) + }), + + rest.post("/admin/product-categories/:id/products/batch", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + product_category: fixtures.get("product_category"), + }) + ) + }), + + rest.delete( + "/admin/product-categories/:id/products/batch", + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + product_category: fixtures.get("product_category"), + }) + ) + } + ), + rest.post("/admin/gift-cards/", (req, res, ctx) => { const body = req.body as Record return res( diff --git a/packages/medusa-react/src/hooks/admin/index.ts b/packages/medusa-react/src/hooks/admin/index.ts index bb274d7136..896a247075 100644 --- a/packages/medusa-react/src/hooks/admin/index.ts +++ b/packages/medusa-react/src/hooks/admin/index.ts @@ -18,6 +18,7 @@ export * from "./price-lists" export * from "./product-tags" export * from "./product-types" export * from "./products" +export * from "./product-categories" export * from "./publishable-api-keys" export * from "./regions" export * from "./return-reasons" diff --git a/packages/medusa-react/src/hooks/admin/product-categories/index.ts b/packages/medusa-react/src/hooks/admin/product-categories/index.ts new file mode 100644 index 0000000000..a494946b87 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/product-categories/index.ts @@ -0,0 +1,2 @@ +export * from "./queries" +export * from "./mutations" diff --git a/packages/medusa-react/src/hooks/admin/product-categories/mutations.ts b/packages/medusa-react/src/hooks/admin/product-categories/mutations.ts new file mode 100644 index 0000000000..cdc63d45c1 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/product-categories/mutations.ts @@ -0,0 +1,167 @@ +import { + useMutation, + UseMutationOptions, + useQueryClient, +} from "@tanstack/react-query" +import { Response } from "@medusajs/medusa-js" +import { + AdminDeleteProductCategoriesCategoryProductsBatchReq, + AdminPostProductCategoriesCategoryProductsBatchReq, + AdminPostProductCategoriesCategoryReq, + AdminPostProductCategoriesReq, + AdminProductCategoriesCategoryDeleteRes, + AdminProductCategoriesCategoryRes, +} from "@medusajs/medusa" + +import { useMedusa } from "../../../contexts" +import { buildOptions } from "../../utils/buildOptions" +import { adminProductCategoryKeys } from "./queries" +import { adminProductKeys } from "../products" + +/** + * Hook provides a mutation function for creating product categories. + * + * @experimental This feature is under development and may change in the future. + * To use this feature please enable the corresponding feature flag in your medusa backend project. + */ +export const useAdminCreateProductCategory = ( + options?: UseMutationOptions< + Response, + Error, + AdminPostProductCategoriesReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminPostProductCategoriesReq) => + client.admin.productCategories.create(payload), + buildOptions(queryClient, [adminProductCategoryKeys.list()], options) + ) +} + +/** Update a product category + * + * @experimental This feature is under development and may change in the future. + * To use this feature please enable feature flag `product_categories` in your medusa backend project. + * @description updates a product category + * @returns the updated medusa product category + */ +export const useAdminUpdateProductCategory = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostProductCategoriesCategoryReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (payload: AdminPostProductCategoriesCategoryReq) => + client.admin.productCategories.update(id, payload), + buildOptions( + queryClient, + [adminProductCategoryKeys.lists(), adminProductCategoryKeys.detail(id)], + options + ) + ) +} + +/** + * Delete a product category + * + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `product_categories` in your medusa backend project. + * @param id + * @param options + */ +export const useAdminDeleteProductCategory = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + void + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + () => client.admin.productCategories.delete(id), + buildOptions( + queryClient, + [adminProductCategoryKeys.lists(), adminProductCategoryKeys.detail(id)], + options + ) + ) +} + +/** + * Add products to a product category + * + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `product_categories` in your medusa backend project. + * @description Add products to a product category + * @param id + * @param options + */ +export const useAdminAddProductsToCategory = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostProductCategoriesCategoryProductsBatchReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (payload: AdminPostProductCategoriesCategoryProductsBatchReq) => { + return client.admin.productCategories.addProducts(id, payload) + }, + buildOptions( + queryClient, + [ + adminProductCategoryKeys.lists(), + adminProductCategoryKeys.detail(id), + adminProductKeys.list({ product_category_id: [id] }), + ], + options + ) + ) +} + +/** + * Remove products from a product category + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `product_categories` in your medusa backend project. + * @description remove products from a product category + * @param id + * @param options + */ +export const useAdminDeleteProductsFromCategory = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminDeleteProductCategoriesCategoryProductsBatchReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (payload: AdminDeleteProductCategoriesCategoryProductsBatchReq) => { + return client.admin.productCategories.removeProducts(id, payload) + }, + buildOptions( + queryClient, + [ + adminProductCategoryKeys.lists(), + adminProductCategoryKeys.detail(id), + adminProductKeys.list({ product_category_id: [id] }), + ], + options + ) + ) +} diff --git a/packages/medusa-react/src/hooks/admin/product-categories/queries.ts b/packages/medusa-react/src/hooks/admin/product-categories/queries.ts new file mode 100644 index 0000000000..c907414377 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/product-categories/queries.ts @@ -0,0 +1,54 @@ +import { + AdminGetProductCategoriesParams, + AdminProductCategoriesListRes, + AdminGetProductCategoryParams, + AdminProductCategoriesCategoryRes, +} from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { useQuery } from "@tanstack/react-query" + +import { useMedusa } from "../../../contexts" +import { UseQueryOptionsWrapper } from "../../../types" +import { queryKeysFactory } from "../../utils" + +const ADMIN_PRODUCT_CATEGORIES_QUERY_KEY = `product_categories` as const +export const adminProductCategoryKeys = queryKeysFactory( + ADMIN_PRODUCT_CATEGORIES_QUERY_KEY +) +type ProductCategoryQueryKeys = typeof adminProductCategoryKeys + +export const useAdminProductCategories = ( + query?: AdminGetProductCategoriesParams, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + adminProductCategoryKeys.list(query), + () => client.admin.productCategories.list(query), + options + ) + return { ...data, ...rest } as const +} + +export const useAdminProductCategory = ( + id: string, + query?: AdminGetProductCategoryParams, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + adminProductCategoryKeys.detail(id), + () => client.admin.productCategories.retrieve(id, query), + options + ) + + return { ...data, ...rest } as const +} diff --git a/packages/medusa-react/test/hooks/admin/product-categories/mutations.test.ts b/packages/medusa-react/test/hooks/admin/product-categories/mutations.test.ts new file mode 100644 index 0000000000..318e1e6d0b --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/product-categories/mutations.test.ts @@ -0,0 +1,132 @@ +import { renderHook } from "@testing-library/react-hooks" + +import { + useAdminAddProductsToCategory, + useAdminCreateProductCategory, + useAdminDeleteProductCategory, + useAdminDeleteProductsFromCategory, + useAdminUpdateProductCategory, +} from "../../../../src" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" + +describe("useAdminCreateProductCategory hook", () => { + test("creates a product category", async () => { + const category = { + name: "Jeans category", + } + + const { result, waitFor } = renderHook( + () => useAdminCreateProductCategory(), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate(category) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.product_category).toEqual( + expect.objectContaining({ + ...fixtures.get("product_category"), + ...category, + }) + ) + }) +}) + +describe("useAdminUpdateProductCategory hook", () => { + test("updates a product category", async () => { + const category = { + name: "Updated name", + } + + const categoryId = fixtures.get("product_category").id + + const { result, waitFor } = renderHook( + () => useAdminUpdateProductCategory(categoryId), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate(category) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.product_category).toEqual({ + ...fixtures.get("product_category"), + ...category, + }) + }) +}) + +describe("useAdminDeleteProductCategory hook", () => { + test("deletes a product category", async () => { + const id = fixtures.get("product_category").id + + const { result, waitFor } = renderHook( + () => useAdminDeleteProductCategory(id), + { wrapper: createWrapper() } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data).toEqual( + expect.objectContaining({ + id, + object: "product-category", + deleted: true, + }) + ) + }) +}) + +describe("useAdminAddProductsToCategory hook", () => { + test("add products to a product category", async () => { + const id = fixtures.get("product_category").id + const productId = fixtures.get("product").id + + const { result, waitFor } = renderHook( + () => useAdminAddProductsToCategory(id), + { wrapper: createWrapper() } + ) + + result.current.mutate({ product_ids: [{ id: productId }] }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data).toEqual( + expect.objectContaining({ + product_category: fixtures.get("product_category"), + }) + ) + }) +}) + +describe("useAdminDeleteProductsFromCategory hook", () => { + test("remove products from a product category", async () => { + const id = fixtures.get("product_category").id + const productId = fixtures.get("product").id + + const { result, waitFor } = renderHook( + () => useAdminDeleteProductsFromCategory(id), + { wrapper: createWrapper() } + ) + + result.current.mutate({ product_ids: [{ id: productId }] }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data).toEqual( + expect.objectContaining({ + product_category: fixtures.get("product_category"), + }) + ) + }) +}) diff --git a/packages/medusa-react/test/hooks/admin/product-categories/queries.test.ts b/packages/medusa-react/test/hooks/admin/product-categories/queries.test.ts new file mode 100644 index 0000000000..a4f85029b4 --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/product-categories/queries.test.ts @@ -0,0 +1,37 @@ +import { useAdminProductCategory, useAdminProductCategories } from "../../../../src" +import { renderHook } from "@testing-library/react-hooks" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" + +describe("useAdminProductCategories hook", () => { + test("returns a list of categories", async () => { + const categories = fixtures.list("product_category") + + const { result, waitFor } = renderHook(() => useAdminProductCategories(), { + wrapper: createWrapper(), + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.product_categories).toEqual(categories) + }) +}) + +describe("useAdminProductCategory hook", () => { + test("returns a category", async () => { + const category = fixtures.get("product_category") + + const { result, waitFor } = renderHook( + () => useAdminProductCategory(category.id), + { + wrapper: createWrapper(), + } + ) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.product_category).toEqual(category) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/product-categories/index.ts b/packages/medusa/src/api/routes/admin/product-categories/index.ts index c487cf40ec..f598dbdd94 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/index.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/index.ts @@ -144,6 +144,7 @@ export const defaultProductCategoryFields = [ "handle", "is_active", "is_internal", + "parent_category_id", "created_at", "updated_at", ] diff --git a/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts b/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts index 3d87030063..2512ab0dbf 100644 --- a/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts +++ b/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts @@ -13,7 +13,7 @@ import { defaultStoreScope } from "." * description: "Retrieve a list of product categories." * x-authenticated: false * parameters: - * - (query) q {string} Query used for searching product category names orhandles. + * - (query) q {string} Query used for searching product category names or handles. * - (query) parent_category_id {string} Returns categories scoped by parent * - (query) offset=0 {integer} How many product categories to skip in the result. * - (query) limit=100 {integer} Limit the number of product categories returned.