From 48dd83aae18940e3ce4c25fb37d07e4612478160 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Wed, 14 Aug 2024 16:41:08 +0200 Subject: [PATCH 01/13] Prepare products-grid to be reusable --- demo/admin/src/products/ProductsGrid.tsx | 133 ++++++++++++----------- demo/admin/src/products/ProductsPage.tsx | 27 ++++- 2 files changed, 95 insertions(+), 65 deletions(-) diff --git a/demo/admin/src/products/ProductsGrid.tsx b/demo/admin/src/products/ProductsGrid.tsx index fc50703e7d..24fb9baf2d 100644 --- a/demo/admin/src/products/ProductsGrid.tsx +++ b/demo/admin/src/products/ProductsGrid.tsx @@ -8,20 +8,20 @@ import { GridColDef, GridColumnsButton, GridFilterButton, - MainContent, muiGridFilterToGql, muiGridSortToGql, - StackLink, + ToolbarActions, ToolbarFillSpace, ToolbarItem, useBufferedRowCount, useDataGridRemote, usePersistentColumnState, } from "@comet/admin"; -import { Add as AddIcon, Edit, StateFilled } from "@comet/admin-icons"; +import { StateFilled } from "@comet/admin-icons"; import { DamImageBlock } from "@comet/cms-admin"; -import { Button, IconButton, useTheme } from "@mui/material"; -import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { useTheme } from "@mui/material"; +import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { GQLProductFilter } from "@src/graphql.generated"; import gql from "graphql-tag"; import * as React from "react"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; @@ -41,7 +41,7 @@ import { } from "./ProductsGrid.generated"; import { ProductsGridPreviewAction } from "./ProductsGridPreviewAction"; -function ProductsGridToolbar() { +function ProductsGridToolbar({ toolbarAction }: { toolbarAction?: React.ReactNode }) { return ( @@ -54,16 +54,19 @@ function ProductsGridToolbar() { - - - + {toolbarAction && {toolbarAction}} ); } +type Props = { + filter?: GQLProductFilter; + toolbarAction?: React.ReactNode; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rowAction?: (params: GridRenderCellParams) => React.ReactNode; + hideCrudContextMenu?: boolean; +}; -export function ProductsGrid() { +export function ProductsGrid({ filter, toolbarAction, rowAction, hideCrudContextMenu }: Props) { const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; const sortModel = dataGridProps.sortModel; const client = useApolloClient(); @@ -220,52 +223,55 @@ export function ProductsGrid() { return ( <> - - - - { - await client.mutate({ - mutation: createProductMutation, - variables: { - input: { - description: input.description, - image: DamImageBlock.state2Output(DamImageBlock.input2State(input.image)), - inStock: input.inStock, - price: input.price, - slug: input.slug, - title: input.title, - type: input.type, - category: input.category?.id, - tags: input.tags.map((tag) => tag.id), - colors: input.colors, - articleNumbers: input.articleNumbers, - discounts: input.discounts, - statistics: { views: 0 }, + {rowAction && rowAction(params)} + {!hideCrudContextMenu && ( + { + await client.mutate({ + mutation: createProductMutation, + variables: { + input: { + description: input.description, + image: DamImageBlock.state2Output(DamImageBlock.input2State(input.image)), + inStock: input.inStock, + price: input.price, + slug: input.slug, + title: input.title, + type: input.type, + category: input.category?.id, + tags: input.tags.map((tag) => tag.id), + colors: input.colors, + articleNumbers: input.articleNumbers, + discounts: input.discounts, + statistics: { views: 0 }, + }, }, - }, - }); - }} - onDelete={async () => { - await client.mutate({ - mutation: deleteProductMutation, - variables: { id: params.row.id }, - }); - }} - refetchQueries={["ProductsList"]} - copyData={() => { - return filterByFragment(productsFragment, params.row); - }} - /> + }); + }} + onDelete={async () => { + await client.mutate({ + mutation: deleteProductMutation, + variables: { id: params.row.id }, + }); + }} + refetchQueries={["ProductsList"]} + copyData={() => { + return filterByFragment(productsFragment, params.row); + }} + /> + )} ); }, }, ]; + const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel); + const { data, loading, error } = useQuery(productsQuery, { variables: { - ...muiGridFilterToGql(columns, dataGridProps.filterModel), + filter: filter ? { and: [gqlFilter, filter] } : gqlFilter, + search: gqlSearch, offset: dataGridProps.page * dataGridProps.pageSize, limit: dataGridProps.pageSize, sort: muiGridSortToGql(sortModel, dataGridProps.apiRef), @@ -275,20 +281,21 @@ export function ProductsGrid() { const rowCount = useBufferedRowCount(data?.products.totalCount); return ( - - - + ); } diff --git a/demo/admin/src/products/ProductsPage.tsx b/demo/admin/src/products/ProductsPage.tsx index 7604a752ef..7c0f4e45c1 100644 --- a/demo/admin/src/products/ProductsPage.tsx +++ b/demo/admin/src/products/ProductsPage.tsx @@ -5,6 +5,7 @@ import { SaveBoundary, SaveBoundarySaveButton, Stack, + StackLink, StackPage, StackSwitch, StackToolbar, @@ -13,9 +14,11 @@ import { ToolbarBackButton, ToolbarFillSpace, } from "@comet/admin"; +import { Add as AddIcon, Edit } from "@comet/admin-icons"; import { ContentScopeIndicator } from "@comet/cms-admin"; +import { Button, IconButton } from "@mui/material"; import React from "react"; -import { useIntl } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import { ProductForm } from "./ProductForm"; import { ProductPriceForm } from "./ProductPriceForm"; @@ -42,7 +45,27 @@ const ProductsPage: React.FC = () => { } /> - + + } + component={StackLink} + pageName="add" + payload="add" + variant="contained" + color="primary" + > + + + } + rowAction={(params) => ( + + + + )} + /> + {(selectedProductId) => ( From 461acc3fac9911e7a25d9d9d44b5e60ecbfd18c7 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Wed, 14 Aug 2024 16:41:30 +0200 Subject: [PATCH 02/13] Move products-category-page to handmade in menu --- demo/admin/src/common/MasterMenu.tsx | 16 ++++++++-------- .../categories/ProductCategoriesPage.tsx | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx index f177c6c0cd..92d98515c5 100644 --- a/demo/admin/src/common/MasterMenu.tsx +++ b/demo/admin/src/common/MasterMenu.tsx @@ -255,14 +255,6 @@ export const masterMenuData: MasterMenuData = [ component: ProductsPage, }, }, - { - type: "route", - primary: , - route: { - path: "/product-categories", - component: ProductCategoriesPage, - }, - }, { type: "route", primary: , @@ -294,6 +286,14 @@ export const masterMenuData: MasterMenuData = [ component: ManufacturersHandmadePage, }, }, + { + type: "route", + primary: , + route: { + path: "/product-categories", + component: ProductCategoriesPage, + }, + }, ], }, ], diff --git a/demo/admin/src/products/categories/ProductCategoriesPage.tsx b/demo/admin/src/products/categories/ProductCategoriesPage.tsx index bb93df83e5..64f884b8c6 100644 --- a/demo/admin/src/products/categories/ProductCategoriesPage.tsx +++ b/demo/admin/src/products/categories/ProductCategoriesPage.tsx @@ -33,7 +33,7 @@ const ProductCategoriesPage: React.FC = () => { const intl = useIntl(); return ( - + } /> From 239b79d609f2109d83130bf492971420f0efc053 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Wed, 14 Aug 2024 16:55:49 +0200 Subject: [PATCH 03/13] Prepare api --- demo/api/schema.gql | 2 ++ .../entities/product-category.entity.ts | 2 +- .../generated/dto/product-category.input.ts | 9 ++++++-- .../generated/product-category.resolver.ts | 23 ++++++++++++++++--- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/demo/api/schema.gql b/demo/api/schema.gql index a87b1f2453..d9cbb18930 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -1453,11 +1453,13 @@ input ProductUpdateInput { input ProductCategoryInput { title: String! slug: String! + products: [ID!]! = [] } input ProductCategoryUpdateInput { title: String slug: String + products: [ID!] } input ProductTagInput { diff --git a/demo/api/src/products/entities/product-category.entity.ts b/demo/api/src/products/entities/product-category.entity.ts index dcbdbd9997..926d7ae814 100644 --- a/demo/api/src/products/entities/product-category.entity.ts +++ b/demo/api/src/products/entities/product-category.entity.ts @@ -28,7 +28,7 @@ export class ProductCategory extends BaseEntity { //search: true, //not implemented //filter: true, //not implemented //sort: true, //not implemented - input: false, //default is true + input: true, //default is true }) @OneToMany(() => Product, (products) => products.category) products = new Collection(this); diff --git a/demo/api/src/products/generated/dto/product-category.input.ts b/demo/api/src/products/generated/dto/product-category.input.ts index 966b1d3d67..4cfef5962f 100644 --- a/demo/api/src/products/generated/dto/product-category.input.ts +++ b/demo/api/src/products/generated/dto/product-category.input.ts @@ -1,8 +1,8 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. import { IsSlug, PartialType } from "@comet/cms-api"; -import { Field, InputType } from "@nestjs/graphql"; -import { IsNotEmpty, IsString } from "class-validator"; +import { Field, ID, InputType } from "@nestjs/graphql"; +import { IsArray, IsNotEmpty, IsString, IsUUID } from "class-validator"; @InputType() export class ProductCategoryInput { @@ -16,6 +16,11 @@ export class ProductCategoryInput { @IsSlug() @Field() slug: string; + + @Field(() => [ID], { defaultValue: [] }) + @IsArray() + @IsUUID(undefined, { each: true }) + products: string[]; } @InputType() diff --git a/demo/api/src/products/generated/product-category.resolver.ts b/demo/api/src/products/generated/product-category.resolver.ts index 57cd6bba04..b09893657a 100644 --- a/demo/api/src/products/generated/product-category.resolver.ts +++ b/demo/api/src/products/generated/product-category.resolver.ts @@ -1,7 +1,7 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. import { AffectedEntity, extractGraphqlFields, gqlArgsToMikroOrmQuery, RequiredPermission } from "@comet/cms-api"; -import { FindOptions } from "@mikro-orm/core"; +import { FindOptions, Reference } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityManager, EntityRepository } from "@mikro-orm/postgresql"; import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; @@ -19,6 +19,7 @@ export class ProductCategoryResolver { constructor( private readonly entityManager: EntityManager, @InjectRepository(ProductCategory) private readonly repository: EntityRepository, + @InjectRepository(Product) private readonly productRepository: EntityRepository, ) {} @Query(() => ProductCategory) @@ -65,10 +66,18 @@ export class ProductCategoryResolver { @Mutation(() => ProductCategory) async createProductCategory(@Args("input", { type: () => ProductCategoryInput }) input: ProductCategoryInput): Promise { + const { products: productsInput, ...assignInput } = input; const productCategory = this.repository.create({ - ...input, + ...assignInput, }); + if (productsInput) { + const products = await this.productRepository.find({ id: productsInput }); + if (products.length != productsInput.length) throw new Error("Couldn't find all products that were passed as input"); + await productCategory.products.loadItems(); + productCategory.products.set(products.map((product) => Reference.create(product))); + } + await this.entityManager.flush(); return productCategory; @@ -82,10 +91,18 @@ export class ProductCategoryResolver { ): Promise { const productCategory = await this.repository.findOneOrFail(id); + const { products: productsInput, ...assignInput } = input; productCategory.assign({ - ...input, + ...assignInput, }); + if (productsInput) { + const products = await this.productRepository.find({ id: productsInput }); + if (products.length != productsInput.length) throw new Error("Couldn't find all products that were passed as input"); + await productCategory.products.loadItems(); + productCategory.products.set(products.map((product) => Reference.create(product))); + } + await this.entityManager.flush(); return productCategory; From 5e313210c056be0ffd17c6b013ee31287cf35d92 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Wed, 14 Aug 2024 21:52:48 +0200 Subject: [PATCH 04/13] Add select-grid, assign-grid and assigned-grid --- .../categories/AssignProductsGrid.tsx | 86 +++++++ .../categories/AssignedProductsGrid.tsx | 57 +++++ .../categories/ProductCategoriesPage.tsx | 26 +- .../categories/ProductsSelectGrid.tsx | 226 ++++++++++++++++++ 4 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 demo/admin/src/products/categories/AssignProductsGrid.tsx create mode 100644 demo/admin/src/products/categories/AssignedProductsGrid.tsx create mode 100644 demo/admin/src/products/categories/ProductsSelectGrid.tsx diff --git a/demo/admin/src/products/categories/AssignProductsGrid.tsx b/demo/admin/src/products/categories/AssignProductsGrid.tsx new file mode 100644 index 0000000000..840a71e033 --- /dev/null +++ b/demo/admin/src/products/categories/AssignProductsGrid.tsx @@ -0,0 +1,86 @@ +import { gql, useApolloClient, useQuery } from "@apollo/client"; +import { Savable } from "@comet/admin"; +import { CircularProgress } from "@mui/material"; +import { + GQLGetProductIdsForProductCategoryQuery, + GQLGetProductIdsForProductCategoryQueryVariables, + GQLSetProductCategoryMutation, + GQLSetProductCategoryMutationVariables, +} from "@src/products/categories/AssignProductsGrid.generated"; +import { ProductsSelectGrid } from "@src/products/categories/ProductsSelectGrid"; +import isEqual from "lodash.isequal"; +import React, { useEffect, useMemo, useState } from "react"; +import { FormattedMessage } from "react-intl"; + +const setProductCategoryMutation = gql` + mutation SetProductCategory($id: ID!, $input: [ID!]!) { + updateProductCategory(id: $id, input: { products: $input }) { + id + } + } +`; + +const getProductIdsForProductCategory = gql` + query GetProductIdsForProductCategory($id: ID!) { + products(filter: { category: { equal: $id } }) { + nodes { + id + } + } + } +`; + +interface FormProps { + productCategoryId: string; +} + +export function AssignProductsGrid({ productCategoryId }: FormProps): React.ReactElement { + const client = useApolloClient(); + + const { data, error, loading } = useQuery( + getProductIdsForProductCategory, + { + variables: { id: productCategoryId }, + }, + ); + + const initialValues = useMemo(() => { + return data?.products.nodes ? data.products.nodes.map((product) => product.id) : []; + }, [data]); + const [values, setValues] = useState(initialValues); + useEffect(() => { + setValues(initialValues); + }, [initialValues]); + + if (error) return ; + if (loading) return ; + + return ( + <> + { + await client.mutate({ + mutation: setProductCategoryMutation, + variables: { id: productCategoryId, input: values }, + update: (cache, result) => cache.evict({ fieldName: "products" }), + }); + return true; + }} + hasChanges={!isEqual(initialValues.sort(), values.sort())} + doReset={() => { + setValues(initialValues); + }} + /> + { + setValues(newSelectionModel.map((rowId) => String(rowId))); + }, + }} + /> + + ); +} diff --git a/demo/admin/src/products/categories/AssignedProductsGrid.tsx b/demo/admin/src/products/categories/AssignedProductsGrid.tsx new file mode 100644 index 0000000000..fecb991f90 --- /dev/null +++ b/demo/admin/src/products/categories/AssignedProductsGrid.tsx @@ -0,0 +1,57 @@ +import { CancelButton, SaveBoundary, SaveBoundarySaveButton } from "@comet/admin"; +import { Add as AddIcon } from "@comet/admin-icons"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { AssignProductsGrid } from "@src/products/categories/AssignProductsGrid"; +import { ProductsGrid } from "@src/products/ProductsGrid"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +type Props = { + productCategoryId: string; +}; + +export function AssignedProductsGrid({ productCategoryId }: Props): React.ReactElement { + const [isOpen, setIsOpen] = React.useState(false); + + const handleCloseDialog = () => { + setIsOpen(false); + }; + return ( + <> + } onClick={() => setIsOpen(true)} variant="contained" color="primary"> + + + } + filter={{ category: { equal: productCategoryId } }} + /> + { + setIsOpen(false); + }} + > + + + + + + + + + {/* TODO Missing close-dialog-unsaved-changes-check */} + + + + + + + ); +} diff --git a/demo/admin/src/products/categories/ProductCategoriesPage.tsx b/demo/admin/src/products/categories/ProductCategoriesPage.tsx index 64f884b8c6..a1f2789398 100644 --- a/demo/admin/src/products/categories/ProductCategoriesPage.tsx +++ b/demo/admin/src/products/categories/ProductCategoriesPage.tsx @@ -1,5 +1,7 @@ import { MainContent, + RouterTab, + RouterTabs, SaveBoundary, SaveBoundarySaveButton, Stack, @@ -12,6 +14,8 @@ import { ToolbarFillSpace, } from "@comet/admin"; import { ContentScopeIndicator } from "@comet/cms-admin"; +import { Box } from "@mui/material"; +import { AssignedProductsGrid } from "@src/products/categories/AssignedProductsGrid"; import React from "react"; import { useIntl } from "react-intl"; @@ -45,7 +49,27 @@ const ProductCategoriesPage: React.FC = () => { {(selectedId) => ( - + + + + + + + + + + )} diff --git a/demo/admin/src/products/categories/ProductsSelectGrid.tsx b/demo/admin/src/products/categories/ProductsSelectGrid.tsx new file mode 100644 index 0000000000..4057baced1 --- /dev/null +++ b/demo/admin/src/products/categories/ProductsSelectGrid.tsx @@ -0,0 +1,226 @@ +import { useQuery } from "@apollo/client"; +import { + DataGridToolbar, + GridCellContent, + GridColDef, + GridColumnsButton, + GridFilterButton, + muiGridFilterToGql, + muiGridSortToGql, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { useTheme } from "@mui/material"; +import { GridCallbackDetails } from "@mui/x-data-grid/models/api"; +import { GridInputSelectionModel, GridSelectionModel } from "@mui/x-data-grid/models/gridSelectionModel"; +import { DataGridPro, GridFilterInputSingleSelect, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import gql from "graphql-tag"; +import * as React from "react"; +import { FormattedNumber, useIntl } from "react-intl"; + +import { + GQLProductsSelectGridItemFragment, + GQLProductsSelectGridListQuery, + GQLProductsSelectGridListQueryVariables, + GQLProductsSelectGridRelationsQuery, + GQLProductsSelectGridRelationsQueryVariables, +} from "./ProductsSelectGrid.generated"; + +function ProductsGridToolbar() { + return ( + + + + + + + + + + + + + ); +} + +type Props = { + dataGridProps?: { + checkboxSelection: boolean; + keepNonExistentRowsSelected: boolean; + selectionModel?: GridInputSelectionModel; + onSelectionModelChange?: (selectionModel: GridSelectionModel, details: GridCallbackDetails) => void; + }; +}; + +export function ProductsSelectGrid({ dataGridProps: forwardedDataGridProps }: Props) { + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid"), ...forwardedDataGridProps }; + const sortModel = dataGridProps.sortModel; + const { data: relationsData } = useQuery( + productRelationsQuery, + ); + const intl = useIntl(); + const theme = useTheme(); + + const columns: GridColDef[] = [ + { + field: "overview", + headerName: "Overview", + minWidth: 200, + flex: 1, + sortBy: ["title", "price", "type", "category"], + visible: theme.breakpoints.down("md"), + renderCell: ({ row }) => { + const secondaryValues = [ + typeof row.price === "number" && intl.formatNumber(row.price, { style: "currency", currency: "EUR" }), + row.type, + row.category?.title, + row.inStock + ? intl.formatMessage({ id: "comet.products.product.inStock", defaultMessage: "In Stock" }) + : intl.formatMessage({ id: "comet.products.product.outOfStock", defaultMessage: "Out of Stock" }), + ]; + return ; + }, + }, + { + field: "title", + headerName: "Title", + minWidth: 150, + flex: 1, + visible: theme.breakpoints.up("md"), + }, + { field: "description", headerName: "Description", flex: 1, minWidth: 150 }, + { + field: "price", + headerName: "Price", + minWidth: 100, + flex: 1, + type: "number", + visible: theme.breakpoints.up("md"), + renderCell: ({ row }) => (typeof row.price === "number" ? : "-"), + }, + { + field: "type", + headerName: "Type", + width: 100, + type: "singleSelect", + visible: theme.breakpoints.up("md"), + valueOptions: ["Cap", "Shirt", "Tie"], + }, + { + field: "additionalTypes", + headerName: "Additional Types", + width: 150, + renderCell: (params) => <>{params.row.additionalTypes.join(", ")}, + filterOperators: [ + { + value: "contains", + getApplyFilterFn: (filterItem) => { + throw new Error("not implemented, we filter server side"); + }, + InputComponent: GridFilterInputSingleSelect, + }, + ], + valueOptions: ["Cap", "Shirt", "Tie"], + }, + { + field: "tags", + headerName: "Tags", + flex: 1, + minWidth: 150, + renderCell: (params) => <>{params.row.tags.map((tag) => tag.title).join(", ")}, + filterOperators: [ + { + value: "contains", + getApplyFilterFn: (filterItem) => { + throw new Error("not implemented, we filter server side"); + }, + InputComponent: GridFilterInputSingleSelect, + }, + ], + valueOptions: relationsData?.productTags.nodes.map((i) => ({ value: i.id, label: i.title })), + }, + ]; + + const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel); + + const { data, loading, error } = useQuery(productsQuery, { + variables: { + filter: gqlFilter, + search: gqlSearch, + offset: dataGridProps.page * dataGridProps.pageSize, + limit: dataGridProps.pageSize, + sort: muiGridSortToGql(sortModel, dataGridProps.apiRef), + }, + }); + const rows = data?.products.nodes ?? []; + const rowCount = useBufferedRowCount(data?.products.totalCount); + + return ( + + ); +} + +const productsFragment = gql` + fragment ProductsSelectGridItem on Product { + id + title + description + price + type + additionalTypes + inStock + status + category { + id + title + } + tags { + id + title + } + } +`; + +const productsQuery = gql` + query ProductsSelectGridList($offset: Int!, $limit: Int!, $sort: [ProductSort!], $filter: ProductFilter, $search: String) { + products(offset: $offset, limit: $limit, sort: $sort, filter: $filter, search: $search) { + nodes { + id + ...ProductsSelectGridItem + } + totalCount + } + } + ${productsFragment} +`; + +const productRelationsQuery = gql` + query ProductsSelectGridRelations { + productCategories { + nodes { + id + title + } + } + productTags { + nodes { + id + title + } + } + } +`; From 07591597d251f4dce7a5cc9d40522c8165da398a Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 30 Aug 2024 16:19:44 +0200 Subject: [PATCH 05/13] Copy ProductsGrid as AssignedProductsGrid, restore ProductsGrid --- demo/admin/src/products/ProductsGrid.tsx | 133 +++---- demo/admin/src/products/ProductsPage.tsx | 27 +- .../categories/AssignedProductsGrid.tsx | 372 +++++++++++++++--- .../categories/AssignedProductsTab.tsx | 57 +++ .../categories/ProductCategoriesPage.tsx | 4 +- 5 files changed, 447 insertions(+), 146 deletions(-) create mode 100644 demo/admin/src/products/categories/AssignedProductsTab.tsx diff --git a/demo/admin/src/products/ProductsGrid.tsx b/demo/admin/src/products/ProductsGrid.tsx index 24fb9baf2d..fc50703e7d 100644 --- a/demo/admin/src/products/ProductsGrid.tsx +++ b/demo/admin/src/products/ProductsGrid.tsx @@ -8,20 +8,20 @@ import { GridColDef, GridColumnsButton, GridFilterButton, + MainContent, muiGridFilterToGql, muiGridSortToGql, - ToolbarActions, + StackLink, ToolbarFillSpace, ToolbarItem, useBufferedRowCount, useDataGridRemote, usePersistentColumnState, } from "@comet/admin"; -import { StateFilled } from "@comet/admin-icons"; +import { Add as AddIcon, Edit, StateFilled } from "@comet/admin-icons"; import { DamImageBlock } from "@comet/cms-admin"; -import { useTheme } from "@mui/material"; -import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; -import { GQLProductFilter } from "@src/graphql.generated"; +import { Button, IconButton, useTheme } from "@mui/material"; +import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; import gql from "graphql-tag"; import * as React from "react"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; @@ -41,7 +41,7 @@ import { } from "./ProductsGrid.generated"; import { ProductsGridPreviewAction } from "./ProductsGridPreviewAction"; -function ProductsGridToolbar({ toolbarAction }: { toolbarAction?: React.ReactNode }) { +function ProductsGridToolbar() { return ( @@ -54,19 +54,16 @@ function ProductsGridToolbar({ toolbarAction }: { toolbarAction?: React.ReactNod - {toolbarAction && {toolbarAction}} + + + ); } -type Props = { - filter?: GQLProductFilter; - toolbarAction?: React.ReactNode; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - rowAction?: (params: GridRenderCellParams) => React.ReactNode; - hideCrudContextMenu?: boolean; -}; -export function ProductsGrid({ filter, toolbarAction, rowAction, hideCrudContextMenu }: Props) { +export function ProductsGrid() { const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; const sortModel = dataGridProps.sortModel; const client = useApolloClient(); @@ -223,55 +220,52 @@ export function ProductsGrid({ filter, toolbarAction, rowAction, hideCrudContext return ( <> - {rowAction && rowAction(params)} - {!hideCrudContextMenu && ( - { - await client.mutate({ - mutation: createProductMutation, - variables: { - input: { - description: input.description, - image: DamImageBlock.state2Output(DamImageBlock.input2State(input.image)), - inStock: input.inStock, - price: input.price, - slug: input.slug, - title: input.title, - type: input.type, - category: input.category?.id, - tags: input.tags.map((tag) => tag.id), - colors: input.colors, - articleNumbers: input.articleNumbers, - discounts: input.discounts, - statistics: { views: 0 }, - }, + + + + { + await client.mutate({ + mutation: createProductMutation, + variables: { + input: { + description: input.description, + image: DamImageBlock.state2Output(DamImageBlock.input2State(input.image)), + inStock: input.inStock, + price: input.price, + slug: input.slug, + title: input.title, + type: input.type, + category: input.category?.id, + tags: input.tags.map((tag) => tag.id), + colors: input.colors, + articleNumbers: input.articleNumbers, + discounts: input.discounts, + statistics: { views: 0 }, }, - }); - }} - onDelete={async () => { - await client.mutate({ - mutation: deleteProductMutation, - variables: { id: params.row.id }, - }); - }} - refetchQueries={["ProductsList"]} - copyData={() => { - return filterByFragment(productsFragment, params.row); - }} - /> - )} + }, + }); + }} + onDelete={async () => { + await client.mutate({ + mutation: deleteProductMutation, + variables: { id: params.row.id }, + }); + }} + refetchQueries={["ProductsList"]} + copyData={() => { + return filterByFragment(productsFragment, params.row); + }} + /> ); }, }, ]; - const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel); - const { data, loading, error } = useQuery(productsQuery, { variables: { - filter: filter ? { and: [gqlFilter, filter] } : gqlFilter, - search: gqlSearch, + ...muiGridFilterToGql(columns, dataGridProps.filterModel), offset: dataGridProps.page * dataGridProps.pageSize, limit: dataGridProps.pageSize, sort: muiGridSortToGql(sortModel, dataGridProps.apiRef), @@ -281,21 +275,20 @@ export function ProductsGrid({ filter, toolbarAction, rowAction, hideCrudContext const rowCount = useBufferedRowCount(data?.products.totalCount); return ( - + + + ); } diff --git a/demo/admin/src/products/ProductsPage.tsx b/demo/admin/src/products/ProductsPage.tsx index 7c0f4e45c1..7604a752ef 100644 --- a/demo/admin/src/products/ProductsPage.tsx +++ b/demo/admin/src/products/ProductsPage.tsx @@ -5,7 +5,6 @@ import { SaveBoundary, SaveBoundarySaveButton, Stack, - StackLink, StackPage, StackSwitch, StackToolbar, @@ -14,11 +13,9 @@ import { ToolbarBackButton, ToolbarFillSpace, } from "@comet/admin"; -import { Add as AddIcon, Edit } from "@comet/admin-icons"; import { ContentScopeIndicator } from "@comet/cms-admin"; -import { Button, IconButton } from "@mui/material"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import { ProductForm } from "./ProductForm"; import { ProductPriceForm } from "./ProductPriceForm"; @@ -45,27 +42,7 @@ const ProductsPage: React.FC = () => { } /> - - } - component={StackLink} - pageName="add" - payload="add" - variant="contained" - color="primary" - > - - - } - rowAction={(params) => ( - - - - )} - /> - + {(selectedProductId) => ( diff --git a/demo/admin/src/products/categories/AssignedProductsGrid.tsx b/demo/admin/src/products/categories/AssignedProductsGrid.tsx index fecb991f90..244f27b953 100644 --- a/demo/admin/src/products/categories/AssignedProductsGrid.tsx +++ b/demo/admin/src/products/categories/AssignedProductsGrid.tsx @@ -1,57 +1,331 @@ -import { CancelButton, SaveBoundary, SaveBoundarySaveButton } from "@comet/admin"; -import { Add as AddIcon } from "@comet/admin-icons"; -import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; -import { AssignProductsGrid } from "@src/products/categories/AssignProductsGrid"; -import { ProductsGrid } from "@src/products/ProductsGrid"; -import React from "react"; -import { FormattedMessage } from "react-intl"; +import { useApolloClient, useQuery } from "@apollo/client"; +import { + CrudVisibility, + DataGridToolbar, + GridCellContent, + GridColDef, + GridColumnsButton, + GridFilterButton, + muiGridFilterToGql, + muiGridSortToGql, + ToolbarActions, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { StateFilled } from "@comet/admin-icons"; +import { useTheme } from "@mui/material"; +import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { GQLProductFilter } from "@src/graphql.generated"; +import { ProductsGridPreviewAction } from "@src/products/ProductsGridPreviewAction"; +import gql from "graphql-tag"; +import * as React from "react"; +import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; +import { + GQLProductGridRelationsQuery, + GQLProductGridRelationsQueryVariables, + GQLProductsListManualFragment, + GQLProductsListQuery, + GQLProductsListQueryVariables, + GQLUpdateProductStatusMutation, + GQLUpdateProductStatusMutationVariables, +} from "./AssignedProductsGrid.generated"; + +function ProductsGridToolbar({ toolbarAction }: { toolbarAction?: React.ReactNode }) { + return ( + + + + + + + + + + + + {toolbarAction && {toolbarAction}} + + ); +} type Props = { - productCategoryId: string; + filter?: GQLProductFilter; + toolbarAction?: React.ReactNode; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rowAction?: (params: GridRenderCellParams) => React.ReactNode; }; -export function AssignedProductsGrid({ productCategoryId }: Props): React.ReactElement { - const [isOpen, setIsOpen] = React.useState(false); +export function AssignedProductsGrid({ filter, toolbarAction, rowAction }: Props) { + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; + const sortModel = dataGridProps.sortModel; + const client = useApolloClient(); + const { data: relationsData } = useQuery(productRelationsQuery); + const intl = useIntl(); + const theme = useTheme(); - const handleCloseDialog = () => { - setIsOpen(false); - }; - return ( - <> - } onClick={() => setIsOpen(true)} variant="contained" color="primary"> - - - } - filter={{ category: { equal: productCategoryId } }} - /> - { - setIsOpen(false); - }} - > - - - - - [] = [ + { + field: "overview", + headerName: "Overview", + minWidth: 200, + flex: 1, + sortBy: ["title", "price", "type", "category", "inStock"], + visible: theme.breakpoints.down("md"), + renderCell: ({ row }) => { + const secondaryValues = [ + typeof row.price === "number" && intl.formatNumber(row.price, { style: "currency", currency: "EUR" }), + row.type, + row.category?.title, + row.inStock + ? intl.formatMessage({ id: "comet.products.product.inStock", defaultMessage: "In Stock" }) + : intl.formatMessage({ id: "comet.products.product.outOfStock", defaultMessage: "Out of Stock" }), + ]; + return ; + }, + }, + { + field: "title", + headerName: "Title", + minWidth: 150, + flex: 1, + visible: theme.breakpoints.up("md"), + }, + { field: "description", headerName: "Description", flex: 1, minWidth: 150 }, + { + field: "price", + headerName: "Price", + minWidth: 100, + flex: 1, + type: "number", + visible: theme.breakpoints.up("md"), + renderCell: ({ row }) => (typeof row.price === "number" ? : "-"), + }, + { + field: "type", + headerName: "Type", + width: 100, + type: "singleSelect", + visible: theme.breakpoints.up("md"), + valueOptions: ["Cap", "Shirt", "Tie"], + }, + { + field: "additionalTypes", + headerName: "Additional Types", + width: 150, + renderCell: (params) => <>{params.row.additionalTypes.join(", ")}, + filterOperators: [ + { + value: "contains", + getApplyFilterFn: (filterItem) => { + throw new Error("not implemented, we filter server side"); + }, + InputComponent: GridFilterInputSingleSelect, + }, + ], + valueOptions: ["Cap", "Shirt", "Tie"], + }, + { + field: "category", + headerName: "Category", + flex: 1, + minWidth: 100, + renderCell: (params) => <>{params.row.category?.title}, + type: "singleSelect", + visible: theme.breakpoints.up("md"), + valueOptions: relationsData?.productCategories.nodes.map((i) => ({ value: i.id, label: i.title })), + }, + { + field: "tags", + headerName: "Tags", + flex: 1, + minWidth: 150, + renderCell: (params) => <>{params.row.tags.map((tag) => tag.title).join(", ")}, + filterOperators: [ + { + label: "Search", + value: "search", + getApplyFilterFn: (filterItem) => { + throw new Error("not implemented, we filter server side"); + }, + InputComponent: GridFilterInputValue, + }, + ], + }, + { + field: "inStock", + headerName: "In Stock", + flex: 1, + minWidth: 80, + visible: theme.breakpoints.up("md"), + renderCell: (params) => ( + } + primaryText={ + params.row.inStock ? ( + + ) : ( + + ) + } + /> + ), + }, + { + field: "availableSince", + headerName: "Available Since", + width: 130, + type: "date", + valueGetter: ({ row }) => row.availableSince && new Date(row.availableSince), + }, + { + field: "status", + headerName: "Status", + flex: 1, + minWidth: 130, + type: "boolean", + valueGetter: (params) => params.row.status == "Published", + renderCell: (params) => { + return ( + { + await client.mutate({ + mutation: updateProductStatusMutation, + variables: { id: params.row.id, status: status ? "Published" : "Unpublished" }, + optimisticResponse: { + __typename: "Mutation", + updateProduct: { __typename: "Product", id: params.row.id, status: status ? "Published" : "Unpublished" }, + }, + }); }} - > - - - - {/* TODO Missing close-dialog-unsaved-changes-check */} - - - - - - + /> + ); + }, + }, + { + field: "action", + headerName: "", + sortable: false, + filterable: false, + width: 106, + renderCell: (params) => { + return ( + <> + + {rowAction && rowAction(params)} + + ); + }, + }, + ]; + + const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel); + + const { data, loading, error } = useQuery(productsQuery, { + variables: { + filter: filter ? { and: [gqlFilter, filter] } : gqlFilter, + search: gqlSearch, + offset: dataGridProps.page * dataGridProps.pageSize, + limit: dataGridProps.pageSize, + sort: muiGridSortToGql(sortModel, dataGridProps.apiRef), + }, + }); + const rows = data?.products.nodes ?? []; + const rowCount = useBufferedRowCount(data?.products.totalCount); + + return ( + ); } + +const productsFragment = gql` + fragment ProductsListManual on Product { + id + slug + title + description + price + type + additionalTypes + inStock + image + status + category { + id + title + } + tags { + id + title + } + colors { + name + hexCode + } + variants { + id + } + articleNumbers + discounts { + quantity + price + } + availableSince + } +`; + +const productsQuery = gql` + query ProductsList($offset: Int, $limit: Int, $sort: [ProductSort!], $filter: ProductFilter, $search: String) { + products(offset: $offset, limit: $limit, sort: $sort, filter: $filter, search: $search) { + nodes { + id + ...ProductsListManual + } + totalCount + } + } + ${productsFragment} +`; + +const productRelationsQuery = gql` + query ProductGridRelations { + productCategories { + nodes { + id + title + } + } + productTags { + nodes { + id + title + } + } + } +`; + +const updateProductStatusMutation = gql` + mutation UpdateProductStatus($id: ID!, $status: ProductStatus!) { + updateProduct(id: $id, input: { status: $status }) { + id + status + } + } +`; diff --git a/demo/admin/src/products/categories/AssignedProductsTab.tsx b/demo/admin/src/products/categories/AssignedProductsTab.tsx new file mode 100644 index 0000000000..5028e8b214 --- /dev/null +++ b/demo/admin/src/products/categories/AssignedProductsTab.tsx @@ -0,0 +1,57 @@ +import { CancelButton, SaveBoundary, SaveBoundarySaveButton } from "@comet/admin"; +import { Add as AddIcon } from "@comet/admin-icons"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { AssignedProductsGrid } from "@src/products/categories/AssignedProductsGrid"; +import { AssignProductsGrid } from "@src/products/categories/AssignProductsGrid"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +type Props = { + productCategoryId: string; +}; + +export function AssignedProductsTab({ productCategoryId }: Props): React.ReactElement { + const [isOpen, setIsOpen] = React.useState(false); + + const handleCloseDialog = () => { + setIsOpen(false); + }; + return ( + <> + } onClick={() => setIsOpen(true)} variant="contained" color="primary"> + + + } + filter={{ category: { equal: productCategoryId } }} + /> + { + setIsOpen(false); + }} + > + + + + + + + + + {/* TODO Missing close-dialog-unsaved-changes-check */} + + + + + + + ); +} diff --git a/demo/admin/src/products/categories/ProductCategoriesPage.tsx b/demo/admin/src/products/categories/ProductCategoriesPage.tsx index a1f2789398..82f71f6c98 100644 --- a/demo/admin/src/products/categories/ProductCategoriesPage.tsx +++ b/demo/admin/src/products/categories/ProductCategoriesPage.tsx @@ -15,7 +15,7 @@ import { } from "@comet/admin"; import { ContentScopeIndicator } from "@comet/cms-admin"; import { Box } from "@mui/material"; -import { AssignedProductsGrid } from "@src/products/categories/AssignedProductsGrid"; +import { AssignedProductsTab } from "@src/products/categories/AssignedProductsTab"; import React from "react"; import { useIntl } from "react-intl"; @@ -66,7 +66,7 @@ const ProductCategoriesPage: React.FC = () => { })} > - + From 74d1c5a5389fc8690785d4ba73332987acf6470d Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 30 Sep 2024 08:45:10 +0200 Subject: [PATCH 06/13] Remove useMemo, use data.products directly --- .../products/categories/AssignProductsGrid.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/demo/admin/src/products/categories/AssignProductsGrid.tsx b/demo/admin/src/products/categories/AssignProductsGrid.tsx index 840a71e033..9e6b90e5f1 100644 --- a/demo/admin/src/products/categories/AssignProductsGrid.tsx +++ b/demo/admin/src/products/categories/AssignProductsGrid.tsx @@ -9,7 +9,7 @@ import { } from "@src/products/categories/AssignProductsGrid.generated"; import { ProductsSelectGrid } from "@src/products/categories/ProductsSelectGrid"; import isEqual from "lodash.isequal"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useState } from "react"; import { FormattedMessage } from "react-intl"; const setProductCategoryMutation = gql` @@ -44,13 +44,10 @@ export function AssignProductsGrid({ productCategoryId }: FormProps): React.Reac }, ); - const initialValues = useMemo(() => { - return data?.products.nodes ? data.products.nodes.map((product) => product.id) : []; - }, [data]); - const [values, setValues] = useState(initialValues); + const [values, setValues] = useState(data?.products.nodes ? data.products.nodes.map((product) => product.id) : []); useEffect(() => { - setValues(initialValues); - }, [initialValues]); + if (data?.products.nodes) setValues(data.products.nodes.map((product) => product.id)); + }, [data?.products.nodes]); if (error) return ; if (loading) return ; @@ -66,9 +63,9 @@ export function AssignProductsGrid({ productCategoryId }: FormProps): React.Reac }); return true; }} - hasChanges={!isEqual(initialValues.sort(), values.sort())} + hasChanges={!isEqual((data?.products.nodes.map((product) => product.id) ?? []).sort(), values.sort())} doReset={() => { - setValues(initialValues); + setValues(data?.products.nodes.map((product) => product.id) ?? []); }} /> Date: Mon, 30 Sep 2024 09:02:03 +0200 Subject: [PATCH 07/13] Import from apollo --- demo/admin/src/products/categories/ProductsSelectGrid.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo/admin/src/products/categories/ProductsSelectGrid.tsx b/demo/admin/src/products/categories/ProductsSelectGrid.tsx index 4057baced1..9bcd609ad4 100644 --- a/demo/admin/src/products/categories/ProductsSelectGrid.tsx +++ b/demo/admin/src/products/categories/ProductsSelectGrid.tsx @@ -1,4 +1,4 @@ -import { useQuery } from "@apollo/client"; +import gql, { useQuery } from "@apollo/client"; import { DataGridToolbar, GridCellContent, @@ -17,7 +17,6 @@ import { useTheme } from "@mui/material"; import { GridCallbackDetails } from "@mui/x-data-grid/models/api"; import { GridInputSelectionModel, GridSelectionModel } from "@mui/x-data-grid/models/gridSelectionModel"; import { DataGridPro, GridFilterInputSingleSelect, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; -import gql from "graphql-tag"; import * as React from "react"; import { FormattedNumber, useIntl } from "react-intl"; From 9a4c5b75ecb98f31ce487278b7e69ab94f8c9eb6 Mon Sep 17 00:00:00 2001 From: Johannes Obermair Date: Mon, 30 Sep 2024 16:00:51 +0200 Subject: [PATCH 08/13] Make handmade products runnable again --- demo/admin/src/products/categories/AssignedProductsGrid.tsx | 2 +- demo/admin/src/products/categories/ProductsSelectGrid.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/admin/src/products/categories/AssignedProductsGrid.tsx b/demo/admin/src/products/categories/AssignedProductsGrid.tsx index 244f27b953..4073845429 100644 --- a/demo/admin/src/products/categories/AssignedProductsGrid.tsx +++ b/demo/admin/src/products/categories/AssignedProductsGrid.tsx @@ -214,7 +214,7 @@ export function AssignedProductsGrid({ filter, toolbarAction, rowAction }: Props renderCell: (params) => { return ( <> - + {rowAction && rowAction(params)} ); diff --git a/demo/admin/src/products/categories/ProductsSelectGrid.tsx b/demo/admin/src/products/categories/ProductsSelectGrid.tsx index 9bcd609ad4..def5787266 100644 --- a/demo/admin/src/products/categories/ProductsSelectGrid.tsx +++ b/demo/admin/src/products/categories/ProductsSelectGrid.tsx @@ -1,4 +1,4 @@ -import gql, { useQuery } from "@apollo/client"; +import { gql, useQuery } from "@apollo/client"; import { DataGridToolbar, GridCellContent, From ca70b20638b384f065c9b957c374148561b1a729 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 30 Sep 2024 09:02:03 +0200 Subject: [PATCH 09/13] Import from apollo --- demo/admin/src/products/categories/AssignedProductsGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/admin/src/products/categories/AssignedProductsGrid.tsx b/demo/admin/src/products/categories/AssignedProductsGrid.tsx index 4073845429..ffe35744b4 100644 --- a/demo/admin/src/products/categories/AssignedProductsGrid.tsx +++ b/demo/admin/src/products/categories/AssignedProductsGrid.tsx @@ -20,7 +20,7 @@ import { useTheme } from "@mui/material"; import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; import { GQLProductFilter } from "@src/graphql.generated"; import { ProductsGridPreviewAction } from "@src/products/ProductsGridPreviewAction"; -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; import * as React from "react"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; From 3e53f2f1c546b061a764544a643de6e28ab5bb5e Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 1 Oct 2024 09:38:36 +0200 Subject: [PATCH 10/13] Use box instead of dialog-content --- .../src/products/categories/AssignedProductsGrid.tsx | 3 +-- .../src/products/categories/AssignedProductsTab.tsx | 12 +++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/demo/admin/src/products/categories/AssignedProductsGrid.tsx b/demo/admin/src/products/categories/AssignedProductsGrid.tsx index ffe35744b4..e19298f22b 100644 --- a/demo/admin/src/products/categories/AssignedProductsGrid.tsx +++ b/demo/admin/src/products/categories/AssignedProductsGrid.tsx @@ -1,4 +1,4 @@ -import { useApolloClient, useQuery } from "@apollo/client"; +import { gql, useApolloClient, useQuery } from "@apollo/client"; import { CrudVisibility, DataGridToolbar, @@ -20,7 +20,6 @@ import { useTheme } from "@mui/material"; import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; import { GQLProductFilter } from "@src/graphql.generated"; import { ProductsGridPreviewAction } from "@src/products/ProductsGridPreviewAction"; -import { gql } from "@apollo/client"; import * as React from "react"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; diff --git a/demo/admin/src/products/categories/AssignedProductsTab.tsx b/demo/admin/src/products/categories/AssignedProductsTab.tsx index 5028e8b214..43eb66088a 100644 --- a/demo/admin/src/products/categories/AssignedProductsTab.tsx +++ b/demo/admin/src/products/categories/AssignedProductsTab.tsx @@ -1,6 +1,6 @@ import { CancelButton, SaveBoundary, SaveBoundarySaveButton } from "@comet/admin"; import { Add as AddIcon } from "@comet/admin-icons"; -import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { Box, Button, Dialog, DialogActions, DialogTitle } from "@mui/material"; import { AssignedProductsGrid } from "@src/products/categories/AssignedProductsGrid"; import { AssignProductsGrid } from "@src/products/categories/AssignProductsGrid"; import React from "react"; @@ -36,15 +36,9 @@ export function AssignedProductsTab({ productCategoryId }: Props): React.ReactEl - + - + {/* TODO Missing close-dialog-unsaved-changes-check */} From 5941248e2774d003d5d3ca99a65c164254a35ad7 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 1 Oct 2024 09:43:24 +0200 Subject: [PATCH 11/13] Switch box to maincontent --- demo/admin/src/products/categories/ProductCategoriesPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/demo/admin/src/products/categories/ProductCategoriesPage.tsx b/demo/admin/src/products/categories/ProductCategoriesPage.tsx index b642971922..75ad9c6580 100644 --- a/demo/admin/src/products/categories/ProductCategoriesPage.tsx +++ b/demo/admin/src/products/categories/ProductCategoriesPage.tsx @@ -14,7 +14,6 @@ import { ToolbarFillSpace, } from "@comet/admin"; import { ContentScopeIndicator } from "@comet/cms-admin"; -import { Box } from "@mui/material"; import { AssignedProductsTab } from "@src/products/categories/AssignedProductsTab"; import React from "react"; import { useIntl } from "react-intl"; @@ -65,9 +64,9 @@ const ProductCategoriesPage = () => { defaultMessage: "Assigned Products", })} > - + - + From f88d678af5084b0cebc9ad5b39aff0e71d3c7129 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 1 Oct 2024 10:30:34 +0200 Subject: [PATCH 12/13] Use suggestion from johnnyomair to simplify state and hooks --- .../src/products/categories/AssignProductsGrid.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/admin/src/products/categories/AssignProductsGrid.tsx b/demo/admin/src/products/categories/AssignProductsGrid.tsx index 9e6b90e5f1..3010281d64 100644 --- a/demo/admin/src/products/categories/AssignProductsGrid.tsx +++ b/demo/admin/src/products/categories/AssignProductsGrid.tsx @@ -9,7 +9,7 @@ import { } from "@src/products/categories/AssignProductsGrid.generated"; import { ProductsSelectGrid } from "@src/products/categories/ProductsSelectGrid"; import isEqual from "lodash.isequal"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { FormattedMessage } from "react-intl"; const setProductCategoryMutation = gql` @@ -41,13 +41,13 @@ export function AssignProductsGrid({ productCategoryId }: FormProps): React.Reac getProductIdsForProductCategory, { variables: { id: productCategoryId }, + onCompleted: (data) => { + setValues(data.products.nodes.map((product) => product.id)); + }, }, ); - const [values, setValues] = useState(data?.products.nodes ? data.products.nodes.map((product) => product.id) : []); - useEffect(() => { - if (data?.products.nodes) setValues(data.products.nodes.map((product) => product.id)); - }, [data?.products.nodes]); + const [values, setValues] = useState([]); if (error) return ; if (loading) return ; From 1fa0f0c5cb3928ecd42de1ffc0524ed03117e486 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 1 Oct 2024 10:37:55 +0200 Subject: [PATCH 13/13] Use comet-loading instead of mui --- demo/admin/src/products/categories/AssignProductsGrid.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/demo/admin/src/products/categories/AssignProductsGrid.tsx b/demo/admin/src/products/categories/AssignProductsGrid.tsx index 3010281d64..c84cccc032 100644 --- a/demo/admin/src/products/categories/AssignProductsGrid.tsx +++ b/demo/admin/src/products/categories/AssignProductsGrid.tsx @@ -1,6 +1,5 @@ import { gql, useApolloClient, useQuery } from "@apollo/client"; -import { Savable } from "@comet/admin"; -import { CircularProgress } from "@mui/material"; +import { Loading, Savable } from "@comet/admin"; import { GQLGetProductIdsForProductCategoryQuery, GQLGetProductIdsForProductCategoryQueryVariables, @@ -50,7 +49,7 @@ export function AssignProductsGrid({ productCategoryId }: FormProps): React.Reac const [values, setValues] = useState([]); if (error) return ; - if (loading) return ; + if (loading) return ; return ( <>