From 3173e7d43ec7519335b9b0ba8c1caf47c82a4520 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 20 Aug 2024 17:23:48 +0200 Subject: [PATCH 01/13] Admin Generator (Future): Forward dataGridProps for various reasons e.g. it's possible to use the generated grid as select-grid or style something --- .../products/future/ProductsGrid.cometGen.ts | 1 + .../future/generated/ProductsGrid.tsx | 7 ++++--- .../src/generator/future/generateGrid.ts | 20 ++++++++++++++++--- .../src/generator/future/generator.ts | 1 + 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/demo/admin/src/products/future/ProductsGrid.cometGen.ts b/demo/admin/src/products/future/ProductsGrid.cometGen.ts index d5e39bcd0b..f1ee555a28 100644 --- a/demo/admin/src/products/future/ProductsGrid.cometGen.ts +++ b/demo/admin/src/products/future/ProductsGrid.cometGen.ts @@ -8,6 +8,7 @@ export const ProductsGrid: GridConfig = { filterProp: true, toolbarActionProp: true, rowActionProp: true, + dataGridPropsProp: true, columns: [ { type: "boolean", name: "inStock", headerName: "In stock", width: 90 }, { type: "text", name: "title", headerName: "Titel", minWidth: 200, maxWidth: 250 }, diff --git a/demo/admin/src/products/future/generated/ProductsGrid.tsx b/demo/admin/src/products/future/generated/ProductsGrid.tsx index 65b4c2df7e..c7677983b9 100644 --- a/demo/admin/src/products/future/generated/ProductsGrid.tsx +++ b/demo/admin/src/products/future/generated/ProductsGrid.tsx @@ -17,7 +17,7 @@ import { usePersistentColumnState, } from "@comet/admin"; import { DamImageBlock } from "@comet/cms-admin"; -import { DataGridPro, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { DataGridPro, DataGridProProps, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; import { GQLProductFilter } from "@src/graphql.generated"; import * as React from "react"; import { useIntl } from "react-intl"; @@ -91,12 +91,13 @@ type Props = { toolbarAction?: React.ReactNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any rowAction?: (params: GridRenderCellParams) => React.ReactNode; + dataGridProps?: DataGridProProps; }; -export function ProductsGrid({ filter, toolbarAction, rowAction }: Props): React.ReactElement { +export function ProductsGrid({ filter, toolbarAction, rowAction, dataGridProps: forwardedDataGridProps }: Props): React.ReactElement { const client = useApolloClient(); const intl = useIntl(); - const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid"), ...forwardedDataGridProps }; const columns: GridColDef[] = [ { field: "inStock", headerName: intl.formatMessage({ id: "product.inStock", defaultMessage: "In stock" }), type: "boolean", width: 90 }, diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index e3ed671436..d1c25f55c0 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -41,7 +41,7 @@ function findQueryTypeOrThrow(queryName: string, schema: IntrospectionQuery) { return ret; } -export type Prop = { type: string; optional: boolean; name: string }; +export type Prop = { type: string; optional: boolean; name: string; destructionAlias?: string }; function generateGridPropsCode(props: Prop[]): { gridPropsTypeCode: string; gridPropsParamsCode: string } { if (!props.length) return { gridPropsTypeCode: "", gridPropsParamsCode: "" }; const uniqueProps = props.reduce((acc, prop) => { @@ -71,7 +71,9 @@ function generateGridPropsCode(props: Prop[]): { gridPropsTypeCode: string; grid ) .join("\n")} };`, - gridPropsParamsCode: `{${uniqueProps.map((prop) => prop.name).join(", ")}}: Props`, + gridPropsParamsCode: `{${uniqueProps + .map((prop) => `${prop.name}${prop.destructionAlias ? `: ${prop.destructionAlias}` : ``}`) + .join(", ")}}: Props`, }; } @@ -326,6 +328,16 @@ export function generateGrid( }); } + if (config.dataGridPropsProp) { + imports.push({ name: "DataGridProProps", importPath: "@mui/x-data-grid-pro" }); + props.push({ + name: "dataGridProps", + destructionAlias: "forwardedDataGridProps", + type: `DataGridProProps`, + optional: true, + }); + } + const { gridPropsTypeCode, gridPropsParamsCode } = generateGridPropsCode(props); const code = `import { gql, useApolloClient, useQuery } from "@apollo/client"; @@ -477,7 +489,9 @@ export function generateGrid( export function ${gqlTypePlural}Grid(${gridPropsParamsCode}): React.ReactElement { ${allowCopyPaste || allowDeleting ? "const client = useApolloClient();" : ""} const intl = useIntl(); - const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("${gqlTypePlural}Grid") }; + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("${gqlTypePlural}Grid")${ + config.dataGridPropsProp ? `, ...forwardedDataGridProps` : `` + } }; ${hasScope ? `const { scope } = useContentScope();` : ""} const columns: GridColDef[] = [ diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index 3e71148324..bf5e4b9c8e 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -81,6 +81,7 @@ export type GridConfig = { toolbar?: boolean; toolbarActionProp?: boolean; rowActionProp?: boolean; + dataGridPropsProp?: boolean; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any From 3a00300fe2115b84668c21ca372729345276c48a Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Thu, 22 Aug 2024 09:06:24 +0200 Subject: [PATCH 02/13] Reduce forwarded props to selection-props --- .../products/future/ProductsGrid.cometGen.ts | 1 - .../future/SelectProductsGrid.cometGen.ts | 18 +++ .../future/generated/ProductsGrid.tsx | 7 +- .../future/generated/SelectProductsGrid.tsx | 146 ++++++++++++++++++ .../src/generator/future/generateGrid.ts | 14 +- .../src/generator/future/generator.ts | 2 +- 6 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 demo/admin/src/products/future/SelectProductsGrid.cometGen.ts create mode 100644 demo/admin/src/products/future/generated/SelectProductsGrid.tsx diff --git a/demo/admin/src/products/future/ProductsGrid.cometGen.ts b/demo/admin/src/products/future/ProductsGrid.cometGen.ts index f1ee555a28..d5e39bcd0b 100644 --- a/demo/admin/src/products/future/ProductsGrid.cometGen.ts +++ b/demo/admin/src/products/future/ProductsGrid.cometGen.ts @@ -8,7 +8,6 @@ export const ProductsGrid: GridConfig = { filterProp: true, toolbarActionProp: true, rowActionProp: true, - dataGridPropsProp: true, columns: [ { type: "boolean", name: "inStock", headerName: "In stock", width: 90 }, { type: "text", name: "title", headerName: "Titel", minWidth: 200, maxWidth: 250 }, diff --git a/demo/admin/src/products/future/SelectProductsGrid.cometGen.ts b/demo/admin/src/products/future/SelectProductsGrid.cometGen.ts new file mode 100644 index 0000000000..a7e5934f60 --- /dev/null +++ b/demo/admin/src/products/future/SelectProductsGrid.cometGen.ts @@ -0,0 +1,18 @@ +import { future_GridConfig as GridConfig } from "@comet/cms-admin"; +import { GQLProduct } from "@src/graphql.generated"; + +export const SelectProductsGrid: GridConfig = { + type: "grid", + gqlType: "Product", + fragmentName: "SelectProductsGridFuture", + readOnly: true, + selectionProps: true, + columns: [ + { type: "text", name: "title", headerName: "Titel", minWidth: 200, maxWidth: 250 }, + { type: "text", name: "description", headerName: "Description" }, + { type: "number", name: "price", headerName: "Price", maxWidth: 150 }, + { type: "staticSelect", name: "type", maxWidth: 150, values: [{ value: "Cap", label: "great Cap" }, "Shirt", "Tie"] }, + { type: "date", name: "availableSince", width: 140 }, + { type: "dateTime", name: "createdAt", width: 170 }, + ], +}; diff --git a/demo/admin/src/products/future/generated/ProductsGrid.tsx b/demo/admin/src/products/future/generated/ProductsGrid.tsx index c7677983b9..65b4c2df7e 100644 --- a/demo/admin/src/products/future/generated/ProductsGrid.tsx +++ b/demo/admin/src/products/future/generated/ProductsGrid.tsx @@ -17,7 +17,7 @@ import { usePersistentColumnState, } from "@comet/admin"; import { DamImageBlock } from "@comet/cms-admin"; -import { DataGridPro, DataGridProProps, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { DataGridPro, GridRenderCellParams, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; import { GQLProductFilter } from "@src/graphql.generated"; import * as React from "react"; import { useIntl } from "react-intl"; @@ -91,13 +91,12 @@ type Props = { toolbarAction?: React.ReactNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any rowAction?: (params: GridRenderCellParams) => React.ReactNode; - dataGridProps?: DataGridProProps; }; -export function ProductsGrid({ filter, toolbarAction, rowAction, dataGridProps: forwardedDataGridProps }: Props): React.ReactElement { +export function ProductsGrid({ filter, toolbarAction, rowAction }: Props): React.ReactElement { const client = useApolloClient(); const intl = useIntl(); - const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid"), ...forwardedDataGridProps }; + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; const columns: GridColDef[] = [ { field: "inStock", headerName: intl.formatMessage({ id: "product.inStock", defaultMessage: "In stock" }), type: "boolean", width: 90 }, diff --git a/demo/admin/src/products/future/generated/SelectProductsGrid.tsx b/demo/admin/src/products/future/generated/SelectProductsGrid.tsx new file mode 100644 index 0000000000..5219eb1cc3 --- /dev/null +++ b/demo/admin/src/products/future/generated/SelectProductsGrid.tsx @@ -0,0 +1,146 @@ +// This file has been generated by comet admin-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { gql, useQuery } from "@apollo/client"; +import { + DataGridToolbar, + GridColDef, + GridFilterButton, + muiGridFilterToGql, + muiGridSortToGql, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { DataGridPro, DataGridProProps, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import * as React from "react"; +import { useIntl } from "react-intl"; + +import { GQLProductsGridQuery, GQLProductsGridQueryVariables, GQLSelectProductsGridFutureFragment } from "./SelectProductsGrid.generated"; + +const productsFragment = gql` + fragment SelectProductsGridFuture on Product { + id + title + description + price + type + availableSince + createdAt + } +`; + +const productsQuery = gql` + query ProductsGrid($offset: Int!, $limit: Int!, $sort: [ProductSort!], $search: String, $filter: ProductFilter) { + products(offset: $offset, limit: $limit, sort: $sort, search: $search, filter: $filter) { + nodes { + ...SelectProductsGridFuture + } + totalCount + } + } + ${productsFragment} +`; + +function ProductsGridToolbar() { + return ( + + + + + + + + + + ); +} + +type Props = { + selectionProps?: { + checkboxSelection: boolean; + keepNonExistentRowsSelected: boolean; + selectionModel: DataGridProProps["selectionModel"]; + onSelectionModelChange: DataGridProProps["onSelectionModelChange"]; + }; +}; + +export function ProductsGrid({ selectionProps }: Props): React.ReactElement { + const intl = useIntl(); + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid"), ...selectionProps }; + + const columns: GridColDef[] = [ + { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), flex: 1, maxWidth: 250, minWidth: 200 }, + { + field: "description", + headerName: intl.formatMessage({ id: "product.description", defaultMessage: "Description" }), + flex: 1, + minWidth: 150, + }, + { + field: "price", + headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), + type: "number", + flex: 1, + maxWidth: 150, + minWidth: 150, + }, + { + field: "type", + headerName: intl.formatMessage({ id: "product.type", defaultMessage: "Type" }), + type: "singleSelect", + valueOptions: [ + { value: "Cap", label: intl.formatMessage({ id: "product.type.cap", defaultMessage: "great Cap" }) }, + { value: "Shirt", label: intl.formatMessage({ id: "product.type.shirt", defaultMessage: "Shirt" }) }, + { value: "Tie", label: intl.formatMessage({ id: "product.type.tie", defaultMessage: "Tie" }) }, + ], + flex: 1, + maxWidth: 150, + minWidth: 150, + }, + { + field: "availableSince", + headerName: intl.formatMessage({ id: "product.availableSince", defaultMessage: "Available Since" }), + type: "date", + valueGetter: ({ row }) => row.availableSince && new Date(row.availableSince), + width: 140, + }, + { + field: "createdAt", + headerName: intl.formatMessage({ id: "product.createdAt", defaultMessage: "Created At" }), + type: "dateTime", + valueGetter: ({ row }) => row.createdAt && new Date(row.createdAt), + width: 170, + }, + ]; + + 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(dataGridProps.sortModel), + }, + }); + const rowCount = useBufferedRowCount(data?.products.totalCount); + if (error) throw error; + const rows = data?.products.nodes ?? []; + + return ( + + ); +} diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index d1c25f55c0..590019ddde 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -328,12 +328,16 @@ export function generateGrid( }); } - if (config.dataGridPropsProp) { + if (config.selectionProps) { imports.push({ name: "DataGridProProps", importPath: "@mui/x-data-grid-pro" }); props.push({ - name: "dataGridProps", - destructionAlias: "forwardedDataGridProps", - type: `DataGridProProps`, + name: "selectionProps", + type: `{ + checkboxSelection: boolean; + keepNonExistentRowsSelected: boolean; + selectionModel: DataGridProProps["selectionModel"]; + onSelectionModelChange: DataGridProProps["onSelectionModelChange"]; + }`, optional: true, }); } @@ -490,7 +494,7 @@ export function generateGrid( ${allowCopyPaste || allowDeleting ? "const client = useApolloClient();" : ""} const intl = useIntl(); const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("${gqlTypePlural}Grid")${ - config.dataGridPropsProp ? `, ...forwardedDataGridProps` : `` + config.selectionProps ? `, ...selectionProps` : `` } }; ${hasScope ? `const { scope } = useContentScope();` : ""} diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index bf5e4b9c8e..a8e2eae03f 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -81,7 +81,7 @@ export type GridConfig = { toolbar?: boolean; toolbarActionProp?: boolean; rowActionProp?: boolean; - dataGridPropsProp?: boolean; + selectionProps?: boolean; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any From d01cd3841357292fc66ca81ffc073b8a4e172bb1 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Thu, 22 Aug 2024 09:14:52 +0200 Subject: [PATCH 03/13] Update api to allow changing product.category in bulk --- 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 fa86c9853dc354bc370a02762a2de00022c05b31 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Thu, 22 Aug 2024 09:16:38 +0200 Subject: [PATCH 04/13] Add AssignProductsGrid using SelectProductsGrids --- .../products/future/AssignProductsGrid.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 demo/admin/src/products/future/AssignProductsGrid.tsx diff --git a/demo/admin/src/products/future/AssignProductsGrid.tsx b/demo/admin/src/products/future/AssignProductsGrid.tsx new file mode 100644 index 0000000000..281e921028 --- /dev/null +++ b/demo/admin/src/products/future/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/future/AssignProductsGrid.generated"; +import { ProductsGrid as SelectProductsGrid } from "@src/products/future/generated/SelectProductsGrid"; +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))); + }, + }} + /> + + ); +} From c4bbd2c68ee96cb91fddf91fef532f26440a6232 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 30 Aug 2024 06:34:44 +0200 Subject: [PATCH 05/13] Flatten and reduce prop-api --- .../future/generated/SelectProductsGrid.tsx | 19 +++++++++++-------- .../src/generator/future/generateGrid.ts | 16 ++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/demo/admin/src/products/future/generated/SelectProductsGrid.tsx b/demo/admin/src/products/future/generated/SelectProductsGrid.tsx index 5219eb1cc3..2eb2b7fb34 100644 --- a/demo/admin/src/products/future/generated/SelectProductsGrid.tsx +++ b/demo/admin/src/products/future/generated/SelectProductsGrid.tsx @@ -58,17 +58,20 @@ function ProductsGridToolbar() { } type Props = { - selectionProps?: { - checkboxSelection: boolean; - keepNonExistentRowsSelected: boolean; - selectionModel: DataGridProProps["selectionModel"]; - onSelectionModelChange: DataGridProProps["onSelectionModelChange"]; - }; + selectionModel?: DataGridProProps["selectionModel"]; + onSelectionModelChange?: DataGridProProps["onSelectionModelChange"]; }; -export function ProductsGrid({ selectionProps }: Props): React.ReactElement { +export function ProductsGrid({ selectionModel, onSelectionModelChange }: Props): React.ReactElement { const intl = useIntl(); - const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid"), ...selectionProps }; + const dataGridProps = { + ...useDataGridRemote(), + ...usePersistentColumnState("ProductsGrid"), + selectionModel, + onSelectionModelChange, + checkboxSelection: true, + keepNonExistentRowsSelected: true, + }; const columns: GridColDef[] = [ { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), flex: 1, maxWidth: 250, minWidth: 200 }, diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index 590019ddde..ceecc46bcb 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -331,13 +331,13 @@ export function generateGrid( if (config.selectionProps) { imports.push({ name: "DataGridProProps", importPath: "@mui/x-data-grid-pro" }); props.push({ - name: "selectionProps", - type: `{ - checkboxSelection: boolean; - keepNonExistentRowsSelected: boolean; - selectionModel: DataGridProProps["selectionModel"]; - onSelectionModelChange: DataGridProProps["onSelectionModelChange"]; - }`, + name: "selectionModel", + type: `DataGridProProps["selectionModel"]`, + optional: true, + }); + props.push({ + name: "onSelectionModelChange", + type: `DataGridProProps["onSelectionModelChange"]`, optional: true, }); } @@ -494,7 +494,7 @@ export function generateGrid( ${allowCopyPaste || allowDeleting ? "const client = useApolloClient();" : ""} const intl = useIntl(); const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("${gqlTypePlural}Grid")${ - config.selectionProps ? `, ...selectionProps` : `` + config.selectionProps ? `, selectionModel, onSelectionModelChange, checkboxSelection: true, keepNonExistentRowsSelected: true` : `` } }; ${hasScope ? `const { scope } = useContentScope();` : ""} From 079569f71dd8cbd957d585b824d1c71d8541bce7 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 2 Sep 2024 22:42:48 +0200 Subject: [PATCH 06/13] Fix lint error --- demo/admin/src/products/future/AssignProductsGrid.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/demo/admin/src/products/future/AssignProductsGrid.tsx b/demo/admin/src/products/future/AssignProductsGrid.tsx index 281e921028..1cf3cb01ee 100644 --- a/demo/admin/src/products/future/AssignProductsGrid.tsx +++ b/demo/admin/src/products/future/AssignProductsGrid.tsx @@ -72,13 +72,9 @@ export function AssignProductsGrid({ productCategoryId }: FormProps): React.Reac }} /> { - setValues(newSelectionModel.map((rowId) => String(rowId))); - }, + selectionModel={values} + onSelectionModelChange={(newSelectionModel) => { + setValues(newSelectionModel.map((rowId) => String(rowId))); }} /> From dbf2a90306664bdcbb029b53d8fbb11fd47065b9 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 9 Sep 2024 10:56:51 +0200 Subject: [PATCH 07/13] Add support for singleSelect --- .../admin/cms-admin/src/generator/future/generateGrid.ts | 6 +++++- packages/admin/cms-admin/src/generator/future/generator.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index ceecc46bcb..85ef9eda2e 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -494,7 +494,11 @@ export function generateGrid( ${allowCopyPaste || allowDeleting ? "const client = useApolloClient();" : ""} const intl = useIntl(); const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("${gqlTypePlural}Grid")${ - config.selectionProps ? `, selectionModel, onSelectionModelChange, checkboxSelection: true, keepNonExistentRowsSelected: true` : `` + config.selectionProps === "multiSelect" + ? `, selectionModel, onSelectionModelChange, checkboxSelection: true, keepNonExistentRowsSelected: true` + : config.selectionProps === "singleSelect" + ? `, selectionModel, onSelectionModelChange, checkboxSelection: false, keepNonExistentRowsSelected: false, disableSelectionOnClick: true` + : `` } }; ${hasScope ? `const { scope } = useContentScope();` : ""} diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index a8e2eae03f..af3917b043 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -81,7 +81,7 @@ export type GridConfig = { toolbar?: boolean; toolbarActionProp?: boolean; rowActionProp?: boolean; - selectionProps?: boolean; + selectionProps?: "multiSelect" | "singleSelect"; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any From 2c9190c60a152c7eabcfaebaa3a13b640a859339 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 10 Sep 2024 13:22:20 +0200 Subject: [PATCH 08/13] Use correct value --- demo/admin/src/products/future/SelectProductsGrid.cometGen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/admin/src/products/future/SelectProductsGrid.cometGen.ts b/demo/admin/src/products/future/SelectProductsGrid.cometGen.ts index a7e5934f60..32888b9b92 100644 --- a/demo/admin/src/products/future/SelectProductsGrid.cometGen.ts +++ b/demo/admin/src/products/future/SelectProductsGrid.cometGen.ts @@ -6,7 +6,7 @@ export const SelectProductsGrid: GridConfig = { gqlType: "Product", fragmentName: "SelectProductsGridFuture", readOnly: true, - selectionProps: true, + selectionProps: "multiSelect", columns: [ { type: "text", name: "title", headerName: "Titel", minWidth: 200, maxWidth: 250 }, { type: "text", name: "description", headerName: "Description" }, From 50b0b254e3638b90448b8737a93105283e56602a Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 1 Oct 2024 08:55:51 +0200 Subject: [PATCH 09/13] Remove useMemo --- .../src/products/future/AssignProductsGrid.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/demo/admin/src/products/future/AssignProductsGrid.tsx b/demo/admin/src/products/future/AssignProductsGrid.tsx index 1cf3cb01ee..a3fe613ab9 100644 --- a/demo/admin/src/products/future/AssignProductsGrid.tsx +++ b/demo/admin/src/products/future/AssignProductsGrid.tsx @@ -9,7 +9,7 @@ import { } from "@src/products/future/AssignProductsGrid.generated"; import { ProductsGrid as SelectProductsGrid } from "@src/products/future/generated/SelectProductsGrid"; 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: Tue, 1 Oct 2024 08:56:08 +0200 Subject: [PATCH 10/13] Use comet loading --- demo/admin/src/products/future/AssignProductsGrid.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/demo/admin/src/products/future/AssignProductsGrid.tsx b/demo/admin/src/products/future/AssignProductsGrid.tsx index a3fe613ab9..aae7807ce3 100644 --- a/demo/admin/src/products/future/AssignProductsGrid.tsx +++ b/demo/admin/src/products/future/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 }, [data?.products.nodes]); if (error) return ; - if (loading) return ; + if (loading) return ; return ( <> From adcdfc195ef7d4f56282630f13669101eb266f8c Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 1 Oct 2024 10:36:29 +0200 Subject: [PATCH 11/13] Use suggestion from johnnyomair to simplify state and hooks --- demo/admin/src/products/future/AssignProductsGrid.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/admin/src/products/future/AssignProductsGrid.tsx b/demo/admin/src/products/future/AssignProductsGrid.tsx index aae7807ce3..c48d0663c2 100644 --- a/demo/admin/src/products/future/AssignProductsGrid.tsx +++ b/demo/admin/src/products/future/AssignProductsGrid.tsx @@ -8,7 +8,7 @@ import { } from "@src/products/future/AssignProductsGrid.generated"; import { ProductsGrid as SelectProductsGrid } from "@src/products/future/generated/SelectProductsGrid"; import isEqual from "lodash.isequal"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { FormattedMessage } from "react-intl"; const setProductCategoryMutation = gql` @@ -40,13 +40,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 33cd64f8accd6174587f8efaeb0e6b48690e1528 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 4 Nov 2024 12:48:04 +0100 Subject: [PATCH 12/13] Fix lint --- .../cms-admin/src/generator/future/generateGrid.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index f94d00ef8c..b408172c7c 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -640,12 +640,12 @@ export function generateGrid( .join(",\n")} ] }` : "" }), ...usePersistentColumnState("${gqlTypePlural}Grid")${ - config.selectionProps === "multiSelect" - ? `, selectionModel, onSelectionModelChange, checkboxSelection: true, keepNonExistentRowsSelected: true` - : config.selectionProps === "singleSelect" - ? `, selectionModel, onSelectionModelChange, checkboxSelection: false, keepNonExistentRowsSelected: false, disableSelectionOnClick: true` - : `` - } }; + config.selectionProps === "multiSelect" + ? `, selectionModel, onSelectionModelChange, checkboxSelection: true, keepNonExistentRowsSelected: true` + : config.selectionProps === "singleSelect" + ? `, selectionModel, onSelectionModelChange, checkboxSelection: false, keepNonExistentRowsSelected: false, disableSelectionOnClick: true` + : `` + } }; ${hasScope ? `const { scope } = useContentScope();` : ""} ${gridNeedsTheme ? `const theme = useTheme();` : ""} From cf4e6b4ec62b01d99f228aa33e1840f231c759b8 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 4 Nov 2024 16:07:28 +0100 Subject: [PATCH 13/13] Rerun admin-generator to fix lint-issue --- .../future/generated/SelectProductsGrid.tsx | 27 ++++++++++++++----- demo/api/schema.gql | 4 +-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/demo/admin/src/products/future/generated/SelectProductsGrid.tsx b/demo/admin/src/products/future/generated/SelectProductsGrid.tsx index 2eb2b7fb34..7dc291dda0 100644 --- a/demo/admin/src/products/future/generated/SelectProductsGrid.tsx +++ b/demo/admin/src/products/future/generated/SelectProductsGrid.tsx @@ -7,6 +7,7 @@ import { GridFilterButton, muiGridFilterToGql, muiGridSortToGql, + renderStaticSelectCell, ToolbarFillSpace, ToolbarItem, useBufferedRowCount, @@ -74,7 +75,7 @@ export function ProductsGrid({ selectionModel, onSelectionModelChange }: Props): }; const columns: GridColDef[] = [ - { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), flex: 1, maxWidth: 250, minWidth: 200 }, + { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), flex: 1, minWidth: 200, maxWidth: 250 }, { field: "description", headerName: intl.formatMessage({ id: "product.description", defaultMessage: "Description" }), @@ -86,27 +87,39 @@ export function ProductsGrid({ selectionModel, onSelectionModelChange }: Props): headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), type: "number", flex: 1, - maxWidth: 150, minWidth: 150, + maxWidth: 150, }, { field: "type", headerName: intl.formatMessage({ id: "product.type", defaultMessage: "Type" }), type: "singleSelect", + valueFormatter: ({ value }) => value?.toString(), valueOptions: [ - { value: "Cap", label: intl.formatMessage({ id: "product.type.cap", defaultMessage: "great Cap" }) }, - { value: "Shirt", label: intl.formatMessage({ id: "product.type.shirt", defaultMessage: "Shirt" }) }, - { value: "Tie", label: intl.formatMessage({ id: "product.type.tie", defaultMessage: "Tie" }) }, + { + value: "Cap", + label: intl.formatMessage({ id: "product.type.cap", defaultMessage: "great Cap" }), + }, + { + value: "Shirt", + label: intl.formatMessage({ id: "product.type.shirt", defaultMessage: "Shirt" }), + }, + { + value: "Tie", + label: intl.formatMessage({ id: "product.type.tie", defaultMessage: "Tie" }), + }, ], + renderCell: renderStaticSelectCell, flex: 1, - maxWidth: 150, minWidth: 150, + maxWidth: 150, }, { field: "availableSince", headerName: intl.formatMessage({ id: "product.availableSince", defaultMessage: "Available Since" }), type: "date", valueGetter: ({ row }) => row.availableSince && new Date(row.availableSince), + valueFormatter: ({ value }) => (value ? intl.formatDate(value) : ""), width: 140, }, { @@ -114,6 +127,8 @@ export function ProductsGrid({ selectionModel, onSelectionModelChange }: Props): headerName: intl.formatMessage({ id: "product.createdAt", defaultMessage: "Created At" }), type: "dateTime", valueGetter: ({ row }) => row.createdAt && new Date(row.createdAt), + valueFormatter: ({ value }) => + value ? intl.formatDate(value, { day: "numeric", month: "numeric", year: "numeric", hour: "numeric", minute: "numeric" }) : "", width: 170, }, ]; diff --git a/demo/api/schema.gql b/demo/api/schema.gql index ebe165add3..45bb6b2809 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -1478,15 +1478,15 @@ input ProductUpdateInput { input ProductCategoryInput { title: String! slug: String! - products: [ID!]! = [] position: Int + products: [ID!]! = [] } input ProductCategoryUpdateInput { title: String slug: String - products: [ID!] position: Int + products: [ID!] } input ProductTagInput {