From d5ac0633f52d7c536c6cf098286ef72ecab74afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Thu, 16 May 2024 09:07:36 +0200 Subject: [PATCH] feat(dashboard): collection product management (#7333) * feat: implement collection management * fix: toasts * fix: use query keys from the lib --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .../layout/main-layout/main-layout.tsx | 9 +- .../dashboard/src/hooks/api/collections.tsx | 24 + .../dashboard/src/lib/client/collections.ts | 12 + .../dashboard/src/types/api-payloads.ts | 8 +- .../add-products-to-collection-form.tsx | 480 ++++++++++-------- .../collection-product-section.tsx | 52 +- 6 files changed, 337 insertions(+), 248 deletions(-) diff --git a/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx index 1ef5b4c5ec..109f7cdb79 100644 --- a/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx +++ b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx @@ -97,11 +97,10 @@ const useCoreRoutes = (): Omit[] => { label: t("products.domain"), to: "/products", items: [ - // TODO: Enable when domin is introduced - // { - // label: t("collections.domain"), - // to: "/collections", - // }, + { + label: t("collections.domain"), + to: "/collections", + }, { label: t("categories.domain"), to: "/categories", diff --git a/packages/admin-next/dashboard/src/hooks/api/collections.tsx b/packages/admin-next/dashboard/src/hooks/api/collections.tsx index 53a6ccf7ab..7d241f9728 100644 --- a/packages/admin-next/dashboard/src/hooks/api/collections.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/collections.tsx @@ -10,6 +10,7 @@ import { queryClient } from "../../lib/medusa" import { queryKeysFactory } from "../../lib/query-key-factory" import { CreateProductCollectionReq, + UpdateProductCollectionProductsReq, UpdateProductCollectionReq, } from "../../types/api-payloads" import { @@ -86,6 +87,29 @@ export const useUpdateCollection = ( }) } +export const useUpdateCollectionProducts = ( + id: string, + options?: UseMutationOptions< + ProductCollectionRes, + Error, + UpdateProductCollectionProductsReq + > +) => { + return useMutation({ + mutationFn: (payload: UpdateProductCollectionProductsReq) => + client.collections.updateProducts(id, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: collectionsQueryKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: collectionsQueryKeys.detail(id), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useCreateCollection = ( options?: UseMutationOptions< ProductCollectionRes, diff --git a/packages/admin-next/dashboard/src/lib/client/collections.ts b/packages/admin-next/dashboard/src/lib/client/collections.ts index f6e8d59951..3bf418ec5c 100644 --- a/packages/admin-next/dashboard/src/lib/client/collections.ts +++ b/packages/admin-next/dashboard/src/lib/client/collections.ts @@ -1,5 +1,6 @@ import { CreateProductCollectionReq, + UpdateProductCollectionProductsReq, UpdateProductCollectionReq, } from "../../types/api-payloads" import { @@ -31,6 +32,16 @@ async function createProductCollection(payload: CreateProductCollectionReq) { return postRequest(`/admin/collections`, payload) } +async function updateProductCollectionProducts( + id: string, + payload: UpdateProductCollectionProductsReq +) { + return postRequest( + `/admin/collections/${id}/products`, + payload + ) +} + async function deleteProductCollection(id: string) { return deleteRequest(`/admin/collections/${id}`) } @@ -39,6 +50,7 @@ export const collections = { list: listProductCollections, retrieve: retrieveProductCollection, update: updateProductCollection, + updateProducts: updateProductCollectionProducts, create: createProductCollection, delete: deleteProductCollection, } diff --git a/packages/admin-next/dashboard/src/types/api-payloads.ts b/packages/admin-next/dashboard/src/types/api-payloads.ts index e9f317bbdc..a8dc8f53b2 100644 --- a/packages/admin-next/dashboard/src/types/api-payloads.ts +++ b/packages/admin-next/dashboard/src/types/api-payloads.ts @@ -69,8 +69,8 @@ export type CreateInviteReq = CreateInviteDTO export type CreateStockLocationReq = CreateStockLocationInput export type UpdateStockLocationReq = UpdateStockLocationInput export type UpdateStockLocationSalesChannelsReq = { - add: string[] - remove: string[] + add?: string[] + remove?: string[] } export type CreateFulfillmentSetReq = CreateFulfillmentSetDTO export type CreateServiceZoneReq = CreateServiceZoneDTO @@ -86,6 +86,10 @@ export type CreateShippingProfileReq = CreateShippingProfileDTO // Product Collections export type CreateProductCollectionReq = CreateProductCollectionDTO export type UpdateProductCollectionReq = UpdateProductCollectionDTO +export type UpdateProductCollectionProductsReq = { + add?: string[] + remove?: string[] +} // Price Lists export type CreatePriceListReq = CreatePriceListDTO diff --git a/packages/admin-next/dashboard/src/v2-routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx b/packages/admin-next/dashboard/src/v2-routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx index a1f7013261..0bee3d60f9 100644 --- a/packages/admin-next/dashboard/src/v2-routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx @@ -1,11 +1,47 @@ -import type { Product } from "@medusajs/medusa" -import { ProductCollectionDTO } from "@medusajs/types" -import { Checkbox, Tooltip } from "@medusajs/ui" -import { createColumnHelper } from "@tanstack/react-table" -import { useMemo } from "react" +import { ProductCollectionDTO, ProductDTO } from "@medusajs/types" +import { + Button, + Checkbox, + clx, + Hint, + Table, + toast, + Tooltip, +} from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { keepPreviousData } from "@tanstack/react-query" +import { + createColumnHelper, + flexRender, + getCoreRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from "@tanstack/react-table" +import { Fragment, useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import * as zod from "zod" import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { useQueryParams } from "../../../../../hooks/use-query-params" +import { + productsQueryKeys, + useProducts, +} from "../../../../../hooks/api/products" +import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll.tsx" +import { Query } from "../../../../../components/filtering/query" +import { OrderBy } from "../../../../../components/filtering/order-by" +import { + NoRecords, + NoResults, +} from "../../../../../components/common/empty-table-content" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useUpdateCollectionProducts } from "../../../../../hooks/api/collections" +import { queryClient } from "../../../../../lib/medusa" // Re-add when supported on the backend @@ -13,7 +49,7 @@ type AddProductsToCollectionFormProps = { collection: ProductCollectionDTO } -const AddProductsToSalesChannelSchema = zod.object({ +const AddProductsToCollectionSchema = zod.object({ product_ids: zod.array(zod.string()).min(1), }) @@ -22,242 +58,242 @@ const PAGE_SIZE = 50 export const AddProductsToCollectionForm = ({ collection, }: AddProductsToCollectionFormProps) => { - // const { t } = useTranslation() - // const { handleSuccess } = useRouteModal() + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() - // const form = useForm>({ - // defaultValues: { - // product_ids: [], - // }, - // resolver: zodResolver(AddProductsToSalesChannelSchema), - // }) + const form = useForm>({ + defaultValues: { + product_ids: [], + }, + resolver: zodResolver(AddProductsToCollectionSchema), + }) - // const { setValue } = form + const { setValue } = form - // const { mutateAsync, isLoading: isMutating } = - // useAdminAddProductsToCollection(collection.id) + const { mutateAsync, isPending: isMutating } = useUpdateCollectionProducts( + collection.id + ) - // const [{ pageIndex, pageSize }, setPagination] = useState({ - // pageIndex: 0, - // pageSize: PAGE_SIZE, - // }) + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) - // const pagination = useMemo( - // () => ({ - // pageIndex, - // pageSize, - // }), - // [pageIndex, pageSize] - // ) + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) - // const [rowSelection, setRowSelection] = useState({}) + const [rowSelection, setRowSelection] = useState({}) - // useEffect(() => { - // setValue( - // "product_ids", - // Object.keys(rowSelection).filter((k) => rowSelection[k]), - // { - // shouldDirty: true, - // shouldTouch: true, - // } - // ) - // }, [rowSelection, setValue]) + useEffect(() => { + setValue( + "product_ids", + Object.keys(rowSelection).filter((k) => rowSelection[k]), + { + shouldDirty: true, + shouldTouch: true, + } + ) + }, [rowSelection, setValue]) - // const params = useQueryParams(["q", "order"]) + const params = useQueryParams(["q", "order"]) - // const { products, count, isLoading, isError, error } = useAdminProducts( - // { - // expand: "variants,sales_channels", - // ...params, - // }, - // { - // keepPreviousData: true, - // } - // ) + const { products, count, isLoading, isError, error } = useProducts( + { + fields: "*variants,*sales_channels", + ...params, + }, + { + placeholderData: keepPreviousData, + } + ) - // const columns = useColumns() + const columns = useColumns() - // const table = useReactTable({ - // data: (products ?? []) as Product[], - // columns, - // pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - // state: { - // pagination, - // rowSelection, - // }, - // onPaginationChange: setPagination, - // onRowSelectionChange: setRowSelection, - // getCoreRowModel: getCoreRowModel(), - // manualPagination: true, - // getRowId: (row) => row.id, - // enableRowSelection(row) { - // return row.original.collection_id !== collection.id - // }, - // meta: { - // collectionId: collection.id, - // }, - // }) + const table = useReactTable({ + data: (products ?? []) as ProductDTO[], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + getRowId: (row) => row.id, + enableRowSelection(row) { + return row.original.collection_id !== collection.id + }, + meta: { + collectionId: collection.id, + }, + }) - // const handleSubmit = form.handleSubmit(async (values) => { - // await mutateAsync( - // { - // product_ids: values.product_ids.map((p) => p), - // }, - // { - // onSuccess: () => { - // /** - // * Invalidate the products list query to refetch products and - // * determine if they are added to the collection or not. - // */ - // queryClient.invalidateQueries(adminProductKeys.lists()) - // handleSuccess() - // }, - // } - // ) - // }) + const handleSubmit = form.handleSubmit(async (values) => { + try { + await mutateAsync({ + add: values.product_ids, + }) + /** + * Invalidate the products list query to refetch products and + * determine if they are added to the collection or not. + */ + queryClient.invalidateQueries(productsQueryKeys.lists()) + handleSuccess() + } catch (e) { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + } + }) - // const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll() + const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll() - // const noRecords = - // !isLoading && - // products?.length === 0 && - // !Object.values(params).filter((v) => v).length + const noRecords = + !isLoading && + products?.length === 0 && + !Object.values(params).filter((v) => v).length - // if (isError) { - // throw error - // } + if (isError) { + throw error + } return ( - // - //
- // - //
- // {form.formState.errors.product_ids && ( - // - // {form.formState.errors.product_ids.message} - // - // )} - // - // - // - // - //
- //
- // - // {!noRecords && ( - //
- //
- //
- // - // - //
- //
- // )} - // {!noRecords ? ( - // - //
- // {!isLoading && !products?.length ? ( - //
- // - //
- // ) : ( - // - // - // {table.getHeaderGroups().map((headerGroup) => { - // return ( - // - // {headerGroup.headers.map((header) => { - // return ( - // - // {flexRender( - // header.column.columnDef.header, - // header.getContext() - // )} - // - // ) - // })} - // - // ) - // })} - // - // - // {table.getRowModel().rows.map((row) => ( - // - // {row.getVisibleCells().map((cell) => ( - // - // {flexRender( - // cell.column.columnDef.cell, - // cell.getContext() - // )} - // - // ))} - // - // ))} - // - //
- // )} - //
- //
- // - //
- //
- // ) : ( - //
- // - // {/* TODO: fix this, and add NoRecords as well */} - //
- // )} - //
- //
- //
-
NOT IMPLEMETED
+ +
+ +
+ {form.formState.errors.product_ids && ( + + {form.formState.errors.product_ids.message} + + )} + + + + +
+
+ + {!noRecords && ( +
+
+
+ + +
+
+ )} + {!noRecords ? ( + +
+ {!isLoading && !products?.length ? ( +
+ +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ )} +
+
+ +
+
+ ) : ( +
+ +
+ )} +
+
+
) } -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() const useColumns = () => { const { t } = useTranslation() diff --git a/packages/admin-next/dashboard/src/v2-routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx b/packages/admin-next/dashboard/src/v2-routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx index 933008f7f4..2ee3ded4c2 100644 --- a/packages/admin-next/dashboard/src/v2-routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx @@ -1,19 +1,23 @@ import { PencilSquare, Plus, Trash } from "@medusajs/icons" import { ProductCollectionDTO } from "@medusajs/types" -import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui" +import { Checkbox, Container, Heading, toast, usePrompt } from "@medusajs/ui" import { keepPreviousData } from "@tanstack/react-query" import { createColumnHelper } from "@tanstack/react-table" -import { useAdminRemoveProductsFromCollection } from "medusa-react" import { useMemo } from "react" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" import { DataTable } from "../../../../../components/table/data-table" -import { useProducts } from "../../../../../hooks/api/products" +import { + productsQueryKeys, + 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" import { ExtendedProductDTO } from "../../../../../types/api-responses" +import { useUpdateCollectionProducts } from "../../../../../hooks/api/collections" +import { queryClient } from "../../../../../lib/medusa" type CollectionProductSectionProps = { collection: ProductCollectionDTO @@ -55,8 +59,8 @@ export const CollectionProductSection = ({ }) const prompt = usePrompt() - // Not implemented in 2.0 - // const { mutateAsync } = useAdminRemoveProductsFromCollection(collection.id) + + const { mutateAsync } = useUpdateCollectionProducts(collection.id) const handleRemove = async (selection: Record) => { const ids = Object.keys(selection) @@ -74,16 +78,18 @@ export const CollectionProductSection = ({ return } - // await mutateAsync( - // { - // product_ids: ids, - // }, - // { - // onSuccess: () => { - // queryClient.invalidateQueries(adminProductKeys.lists()) - // }, - // } - // ) + try { + await mutateAsync({ + remove: ids, + }) + + queryClient.invalidateQueries(productsQueryKeys.lists()) + } catch (e) { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + } } if (isError) { @@ -141,7 +147,7 @@ const ProductActions = ({ }) => { const { t } = useTranslation() const prompt = usePrompt() - const { mutateAsync } = useAdminRemoveProductsFromCollection(collectionId) + const { mutateAsync } = useUpdateCollectionProducts(collectionId) const handleRemove = async () => { const res = await prompt({ @@ -156,10 +162,18 @@ const ProductActions = ({ if (!res) { return } + try { + await mutateAsync({ + remove: [product.id], + }) - await mutateAsync({ - product_ids: [product.id], - }) + queryClient.invalidateQueries(productsQueryKeys.lists()) + } catch (e) { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + } } return (