diff --git a/.changeset/happy-fans-pump.md b/.changeset/happy-fans-pump.md new file mode 100644 index 0000000000..a220014ade --- /dev/null +++ b/.changeset/happy-fans-pump.md @@ -0,0 +1,5 @@ +--- +"@comet/cms-admin": minor +--- + +Add future Admin Generator that works with configuration files diff --git a/demo/admin/package.json b/demo/admin/package.json index 6532122413..e263351ac4 100644 --- a/demo/admin/package.json +++ b/demo/admin/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "admin-generator": "rimraf 'src/*/generated' && comet-admin-generator generate crud-generator-config.ts", + "admin-generator": "rimraf 'src/*/generated' && comet-admin-generator generate crud-generator-config.ts && comet-admin-generator future-generate", "build": "run-s intl:compile && run-p gql:types generate-block-types && cross-env BABEL_ENV=production webpack --config ./webpack.config.ts --env production --mode production --progress", "generate-block-types": "comet generate-block-types --inputs", "generate-block-types:watch": "chokidar -s \"**/block-meta.json\" -c \"npm run generate-block-types\"", diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx index 5c0a392cf0..c09df809c5 100644 --- a/demo/admin/src/common/MasterMenu.tsx +++ b/demo/admin/src/common/MasterMenu.tsx @@ -20,6 +20,7 @@ import { Page } from "@src/pages/Page"; import { categoryToUrlParam, pageTreeCategories, urlParamToCategory } from "@src/pageTree/pageTreeCategories"; import { PredefinedPage } from "@src/predefinedPage/PredefinedPage"; import ProductCategoriesPage from "@src/products/categories/ProductCategoriesPage"; +import { ProductsPage as FutureProductsPage } from "@src/products/future/ProductsPage"; import { ProductsPage } from "@src/products/generated/ProductsPage"; import ProductsHandmadePage from "@src/products/ProductsPage"; import ProductTagsPage from "@src/products/tags/ProductTagsPage"; @@ -164,6 +165,13 @@ export const masterMenuData: MasterMenuData = [ primary: , icon: , submenu: [ + { + primary: , + route: { + path: "/products-future", + component: FutureProductsPage, + }, + }, { primary: , route: { diff --git a/demo/admin/src/products/future/ProductForm.cometGen.ts b/demo/admin/src/products/future/ProductForm.cometGen.ts new file mode 100644 index 0000000000..c3ea1dba9e --- /dev/null +++ b/demo/admin/src/products/future/ProductForm.cometGen.ts @@ -0,0 +1,56 @@ +import { future_FormConfig as FormConfig } from "@comet/cms-admin"; +import { GQLProduct } from "@src/graphql.generated"; + +export const ProductForm: FormConfig = { + type: "form", + gqlType: "Product", + fragmentName: "ProductFormDetails", // configurable as it must be unique across project + fields: [ + { + type: "text", + name: "title", + label: "Titel", // default is generated from name (camelCaseToHumanReadable) + required: true, // default is inferred from gql schema + }, + { type: "text", name: "packageDimensions.height", label: "Height" }, + { type: "text", name: "slug" }, + { type: "text", name: "description", label: "Description", multiline: true }, + { type: "staticSelect", name: "type", label: "Type" /*, values: from gql schema (TODO overridable)*/ }, + //TODO { type: "asyncSelect", name: "category", label: "Category" /*, endpoint: from gql schema (overridable)*/ }, + { type: "number", name: "price" }, + { type: "boolean", name: "inStock" }, + { type: "date", name: "availableSince" }, + { type: "block", name: "image", label: "Image", block: { name: "PixelImageBlock", import: "@comet/cms-admin" } }, + ], +}; + +/* +TODO +export const tabsConfig: TabsConfig = { + type: "tabs", + tabs: [{ name: "form", content: formConfig }], +}; + +//alternative syntax for the above +export const tabsConfig2: TabsConfig = { + type: "tabs", + tabs: [ + { + name: "form", + content: { + type: "form", + gqlType: "Product", + fields: [ + { type: "text", name: "title", label: "Titel" }, + { type: "text", name: "slug", label: "Slug" }, + { type: "text", name: "description", label: "Description", multiline: true }, + { type: "staticSelect", name: "type", label: "Type" / *, values: from gql schema (overridable)* / }, + { type: "asyncSelect", name: "type", label: "Type" / *, endpoint: from gql schema (overridable)* / }, + { type: "block", name: "image", label: "Image", block: PixelImageBlock }, + ], + } satisfies FormConfig, + }, + ], +}; + +*/ diff --git a/demo/admin/src/products/future/ProductsGrid.cometGen.ts b/demo/admin/src/products/future/ProductsGrid.cometGen.ts new file mode 100644 index 0000000000..a4375d4b3d --- /dev/null +++ b/demo/admin/src/products/future/ProductsGrid.cometGen.ts @@ -0,0 +1,16 @@ +import { future_GridConfig as GridConfig } from "@comet/cms-admin"; +import { GQLProduct } from "@src/graphql.generated"; + +export const ProductsGrid: GridConfig = { + type: "grid", + gqlType: "Product", + fragmentName: "ProductsGridFuture", // configurable as it must be unique across project + columns: [ + { type: "text", name: "title", headerName: "Titel", width: 150 }, + { type: "text", name: "description", headerName: "Description", width: 150 }, + { type: "number", name: "price", headerName: "Price", width: 150 }, + { type: "staticSelect", name: "type" /*, values: from gql schema (TODO overridable)*/ }, + { type: "date", name: "availableSince" }, + { type: "dateTime", name: "createdAt" }, + ], +}; diff --git a/demo/admin/src/products/future/ProductsPage.tsx b/demo/admin/src/products/future/ProductsPage.tsx new file mode 100644 index 0000000000..c98aa6fc13 --- /dev/null +++ b/demo/admin/src/products/future/ProductsPage.tsx @@ -0,0 +1,25 @@ +import { Stack, StackPage, StackSwitch } from "@comet/admin"; +import * as React from "react"; +import { useIntl } from "react-intl"; + +import { ProductForm } from "./generated/ProductForm"; +import { ProductsGrid } from "./generated/ProductsGrid"; + +export function ProductsPage(): React.ReactElement { + const intl = useIntl(); + return ( + + + + + + + {(selectedId) => } + + + + + + + ); +} diff --git a/demo/admin/src/products/future/generated/ProductForm.gql.tsx b/demo/admin/src/products/future/generated/ProductForm.gql.tsx new file mode 100644 index 0000000000..c82849f842 --- /dev/null +++ b/demo/admin/src/products/future/generated/ProductForm.gql.tsx @@ -0,0 +1,49 @@ +// 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 } from "@apollo/client"; + +export const productFormFragment = gql` + fragment ProductFormDetails on Product { + title + packageDimensions { + height + } + slug + description + type + price + inStock + availableSince + image + } +`; +export const productQuery = gql` + query Product($id: ID!) { + product(id: $id) { + id + updatedAt + ...ProductFormDetails + } + } + ${productFormFragment} +`; +export const createProductMutation = gql` + mutation CreateProduct($input: ProductInput!) { + createProduct(input: $input) { + id + updatedAt + ...ProductFormDetails + } + } + ${productFormFragment} +`; +export const updateProductMutation = gql` + mutation UpdateProduct($id: ID!, $input: ProductUpdateInput!, $lastUpdatedAt: DateTime) { + updateProduct(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + id + updatedAt + ...ProductFormDetails + } + } + ${productFormFragment} +`; diff --git a/demo/admin/src/products/future/generated/ProductForm.tsx b/demo/admin/src/products/future/generated/ProductForm.tsx new file mode 100644 index 0000000000..924995163e --- /dev/null +++ b/demo/admin/src/products/future/generated/ProductForm.tsx @@ -0,0 +1,240 @@ +// 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 { useApolloClient, useQuery } from "@apollo/client"; +import { + Field, + FinalForm, + FinalFormCheckbox, + FinalFormInput, + FinalFormSaveSplitButton, + FinalFormSelect, + FinalFormSubmitEvent, + Loading, + MainContent, + Toolbar, + ToolbarActions, + ToolbarFillSpace, + ToolbarItem, + ToolbarTitleItem, + useFormApiRef, + useStackApi, + useStackSwitchApi, +} from "@comet/admin"; +import { FinalFormDatePicker } from "@comet/admin-date-time"; +import { ArrowLeft } from "@comet/admin-icons"; +import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; +import { DamImageBlock, EditPageLayout, PixelImageBlock, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { FormControlLabel, IconButton, MenuItem } from "@mui/material"; +import { FormApi } from "final-form"; +import { filter } from "graphql-anywhere"; +import isEqual from "lodash.isequal"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { createProductMutation, productFormFragment, productQuery, updateProductMutation } from "./ProductForm.gql"; +import { + GQLCreateProductMutation, + GQLCreateProductMutationVariables, + GQLProductFormDetailsFragment, + GQLProductQuery, + GQLProductQueryVariables, + GQLUpdateProductMutation, + GQLUpdateProductMutationVariables, +} from "./ProductForm.gql.generated"; + +const rootBlocks = { + image: DamImageBlock, +}; + +type FormValues = Omit & { + price: string; + image: BlockState; +}; + +interface FormProps { + id?: string; +} + +export function ProductForm({ id }: FormProps): React.ReactElement { + const stackApi = useStackApi(); + const client = useApolloClient(); + const mode = id ? "edit" : "add"; + const formApiRef = useFormApiRef(); + const stackSwitchApi = useStackSwitchApi(); + + const { data, error, loading, refetch } = useQuery( + productQuery, + id ? { variables: { id } } : { skip: true }, + ); + + const initialValues = React.useMemo>( + () => + data?.product + ? { + ...filter(productFormFragment, data.product), + price: String(data.product.price), + image: rootBlocks.image.input2State(data.product.image), + } + : { + inStock: false, + image: rootBlocks.image.defaultValues(), + }, + [data], + ); + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "product", id); + return resolveHasSaveConflict(data?.product.updatedAt, updatedAt); + }, + formApiRef, + loadLatestVersion: async () => { + await refetch(); + }, + }); + + const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); + const output = { + ...formValues, + price: parseFloat(formValues.price), + image: rootBlocks.image.state2Output(formValues.image), + }; + if (mode === "edit") { + if (!id) throw new Error(); + await client.mutate({ + mutation: updateProductMutation, + variables: { id, input: output, lastUpdatedAt: data?.product.updatedAt }, + }); + } else { + const { data: mutationResponse } = await client.mutate({ + mutation: createProductMutation, + variables: { input: output }, + }); + if (!event.navigatingBack) { + const id = mutationResponse?.createProduct.id; + if (id) { + setTimeout(() => { + stackSwitchApi.activatePage(`edit`, id); + }); + } + } + } + }; + + if (error) throw error; + + if (loading) { + return ; + } + + return ( + + apiRef={formApiRef} + onSubmit={handleSubmit} + mode={mode} + initialValues={initialValues} + initialValuesEqual={isEqual} //required to compare block data correctly + subscription={{}} + > + {() => ( + + {saveConflict.dialogs} + + + + + + + + + {({ input }) => + input.value ? input.value : + } + + + + + + + + + } + /> + + } + /> + + } + /> + + } + /> + }> + {(props) => ( + + + + + + + + + + + + )} + + + } + /> + + {(props) => ( + } + control={} + /> + )} + + + } + /> + + {createFinalFormBlock(PixelImageBlock)} + + + + )} + + ); +} diff --git a/demo/admin/src/products/future/generated/ProductsGrid.tsx b/demo/admin/src/products/future/generated/ProductsGrid.tsx new file mode 100644 index 0000000000..f92d806cad --- /dev/null +++ b/demo/admin/src/products/future/generated/ProductsGrid.tsx @@ -0,0 +1,211 @@ +// 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, useApolloClient, useQuery } from "@apollo/client"; +import { + CrudContextMenu, + GridFilterButton, + MainContent, + muiGridFilterToGql, + muiGridSortToGql, + StackLink, + Toolbar, + ToolbarActions, + ToolbarAutomaticTitleItem, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { Add as AddIcon, Edit } from "@comet/admin-icons"; +import { DamImageBlock } from "@comet/cms-admin"; +import { Button, IconButton } from "@mui/material"; +import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import * as React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { + GQLCreateProductMutation, + GQLCreateProductMutationVariables, + GQLDeleteProductMutation, + GQLDeleteProductMutationVariables, + GQLProductsGridFutureFragment, + GQLProductsGridQuery, + GQLProductsGridQueryVariables, +} from "./ProductsGrid.generated"; + +const productsFragment = gql` + fragment ProductsGridFuture on Product { + id + updatedAt + title + visible + slug + description + type + price + inStock + soldCount + availableSince + image + 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 { + ...ProductsGridFuture + } + totalCount + } + } + ${productsFragment} +`; + +const deleteProductMutation = gql` + mutation DeleteProduct($id: ID!) { + deleteProduct(id: $id) + } +`; + +const createProductMutation = gql` + mutation CreateProduct($input: ProductInput!) { + createProduct(input: $input) { + id + } + } +`; + +function ProductsGridToolbar() { + return ( + + + + + + + + + + + + + + ); +} + +export function ProductsGrid(): React.ReactElement { + const client = useApolloClient(); + const intl = useIntl(); + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; + + const columns: GridColDef[] = [ + { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), width: 150 }, + { field: "description", headerName: intl.formatMessage({ id: "product.description", defaultMessage: "Description" }), width: 150 }, + { field: "price", headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), width: 150 }, + { + field: "type", + headerName: intl.formatMessage({ id: "product.type", defaultMessage: "Type" }), + type: "singleSelect", + valueOptions: [ + { value: "Cap", label: intl.formatMessage({ id: "product.type.cap", defaultMessage: "Cap" }) }, + { value: "Shirt", label: intl.formatMessage({ id: "product.type.shirt", defaultMessage: "Shirt" }) }, + { value: "Tie", label: intl.formatMessage({ id: "product.type.tie", defaultMessage: "Tie" }) }, + ], + width: 150, + }, + { + field: "availableSince", + headerName: intl.formatMessage({ id: "product.availableSince", defaultMessage: "Available Since" }), + type: "date", + valueGetter: ({ value }) => value && new Date(value), + width: 150, + }, + { + field: "createdAt", + headerName: intl.formatMessage({ id: "product.createdAt", defaultMessage: "Created At" }), + type: "dateTime", + valueGetter: ({ value }) => value && new Date(value), + width: 150, + }, + { + field: "actions", + headerName: "", + sortable: false, + filterable: false, + type: "actions", + renderCell: (params) => { + return ( + <> + + + + { + const row = params.row; + return { + title: row.title, + slug: row.slug, + description: row.description, + type: row.type, + price: row.price, + inStock: row.inStock, + availableSince: row.availableSince, + image: DamImageBlock.state2Output(DamImageBlock.input2State(row.image)), + }; + }} + onPaste={async ({ input }) => { + await client.mutate({ + mutation: createProductMutation, + variables: { input }, + }); + }} + onDelete={async () => { + await client.mutate({ + mutation: deleteProductMutation, + variables: { id: params.row.id }, + }); + }} + refetchQueries={[productsQuery]} + /> + + ); + }, + }, + ]; + + 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/demo/admin/src/products/generated/ProductForm.gql.ts b/demo/admin/src/products/generated/ProductForm.gql.ts index c3bafd48a6..a0c438713c 100644 --- a/demo/admin/src/products/generated/ProductForm.gql.ts +++ b/demo/admin/src/products/generated/ProductForm.gql.ts @@ -11,6 +11,7 @@ export const productFormFragment = gql` type price inStock + availableSince image } `; diff --git a/demo/admin/src/products/generated/ProductForm.tsx b/demo/admin/src/products/generated/ProductForm.tsx index eedbf4c97f..c9a9fd0a8a 100644 --- a/demo/admin/src/products/generated/ProductForm.tsx +++ b/demo/admin/src/products/generated/ProductForm.tsx @@ -21,6 +21,7 @@ import { useStackApi, useStackSwitchApi, } from "@comet/admin"; +import { FinalFormDatePicker } from "@comet/admin-date-time"; import { ArrowLeft } from "@comet/admin-icons"; import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; import { DamImageBlock, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; @@ -205,6 +206,12 @@ export function ProductForm({ id }: FormProps): React.ReactElement { /> )} + } + /> {createFinalFormBlock(rootBlocks.image)} diff --git a/demo/admin/src/products/generated/ProductsGrid.tsx b/demo/admin/src/products/generated/ProductsGrid.tsx index fb1abbed82..266b625f9b 100644 --- a/demo/admin/src/products/generated/ProductsGrid.tsx +++ b/demo/admin/src/products/generated/ProductsGrid.tsx @@ -47,6 +47,7 @@ const productsFragment = gql` price inStock soldCount + availableSince image createdAt } @@ -129,6 +130,13 @@ export function ProductsGrid(): React.ReactElement { { field: "price", headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), type: "number", width: 150 }, { field: "inStock", headerName: intl.formatMessage({ id: "product.inStock", defaultMessage: "In Stock" }), type: "boolean", width: 150 }, { field: "soldCount", headerName: intl.formatMessage({ id: "product.soldCount", defaultMessage: "Sold Count" }), type: "number", width: 150 }, + { + field: "availableSince", + headerName: intl.formatMessage({ id: "product.availableSince", defaultMessage: "Available Since" }), + type: "dateTime", + valueGetter: ({ value }) => value && new Date(value), + width: 150, + }, { field: "image", headerName: intl.formatMessage({ id: "product.image", defaultMessage: "Image" }), @@ -168,6 +176,7 @@ export function ProductsGrid(): React.ReactElement { type: row.type, price: row.price, inStock: row.inStock, + availableSince: row.availableSince, image: DamImageBlock.state2Output(DamImageBlock.input2State(row.image)), }; }} diff --git a/demo/api/schema.gql b/demo/api/schema.gql index 6709484995..5194b0e89d 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -468,7 +468,8 @@ type Product implements DocumentInterface { type: ProductType! price: Float inStock: Boolean! - soldCount: Float! + soldCount: Float + availableSince: DateTime image: DamImageBlockData! discounts: [ProductDiscounts!]! articleNumbers: [String!]! @@ -819,6 +820,7 @@ input ProductFilter { price: NumberFilter inStock: BooleanFilter soldCount: NumberFilter + availableSince: DateFilter category: ManyToOneFilter createdAt: DateFilter updatedAt: DateFilter @@ -861,6 +863,7 @@ enum ProductSortField { price inStock soldCount + availableSince category createdAt updatedAt @@ -1144,6 +1147,7 @@ input ProductInput { type: ProductType! price: Float inStock: Boolean! = true + availableSince: DateTime image: DamImageBlockInput! discounts: [ProductDiscountsInput!]! = [] articleNumbers: [String!]! = [] @@ -1171,6 +1175,7 @@ input ProductUpdateInput { type: ProductType price: Float inStock: Boolean + availableSince: DateTime image: DamImageBlockInput discounts: [ProductDiscountsInput!] articleNumbers: [String!] diff --git a/demo/api/src/db/migrations/Migration20240205074742.ts b/demo/api/src/db/migrations/Migration20240205074742.ts new file mode 100644 index 0000000000..90e6bb8087 --- /dev/null +++ b/demo/api/src/db/migrations/Migration20240205074742.ts @@ -0,0 +1,12 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240205074742 extends Migration { + + async up(): Promise { + this.addSql('alter table "Product" add column "availableSince" date null;'); + } + + async down(): Promise { + this.addSql('alter table "Product" drop column "availableSince";'); + } +} diff --git a/demo/api/src/products/entities/product.entity.ts b/demo/api/src/products/entities/product.entity.ts index 3f7fa4ffef..4936dcaff8 100644 --- a/demo/api/src/products/entities/product.entity.ts +++ b/demo/api/src/products/entities/product.entity.ts @@ -124,12 +124,16 @@ export class Product extends BaseEntity implements DocumentInterf inStock: boolean = true; @Property({ type: types.decimal, nullable: true }) - @Field() + @Field({ nullable: true }) @CrudField({ input: false, }) soldCount?: number; + @Property({ type: types.date, nullable: true }) + @Field({ nullable: true }) + availableSince?: Date; + @Property({ customType: new RootBlockType(DamImageBlock) }) @Field(() => RootBlockDataScalar(DamImageBlock)) @RootBlock(DamImageBlock) diff --git a/demo/api/src/products/generated/dto/product.filter.ts b/demo/api/src/products/generated/dto/product.filter.ts index 23be911d56..258fcd26af 100644 --- a/demo/api/src/products/generated/dto/product.filter.ts +++ b/demo/api/src/products/generated/dto/product.filter.ts @@ -60,6 +60,12 @@ export class ProductFilter { @Type(() => NumberFilter) soldCount?: NumberFilter; + @Field(() => DateFilter, { nullable: true }) + @ValidateNested() + @IsOptional() + @Type(() => DateFilter) + availableSince?: DateFilter; + @Field(() => ManyToOneFilter, { nullable: true }) @ValidateNested() @IsOptional() diff --git a/demo/api/src/products/generated/dto/product.input.ts b/demo/api/src/products/generated/dto/product.input.ts index 48f522f147..63f118a719 100644 --- a/demo/api/src/products/generated/dto/product.input.ts +++ b/demo/api/src/products/generated/dto/product.input.ts @@ -4,7 +4,7 @@ import { BlockInputInterface, isBlockInputInterface } from "@comet/blocks-api"; import { DamImageBlock, IsNullable, IsSlug, PartialType, RootBlockInputScalar } from "@comet/cms-api"; import { Field, ID, InputType } from "@nestjs/graphql"; import { Transform, Type } from "class-transformer"; -import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsString, IsUUID, ValidateNested } from "class-validator"; +import { IsArray, IsBoolean, IsDate, IsEnum, IsNotEmpty, IsNumber, IsString, IsUUID, ValidateNested } from "class-validator"; import { ProductDimensions, ProductDiscounts, ProductPackageDimensions } from "../../entities/product.entity"; import { ProductType } from "../../entities/product-type.enum"; @@ -44,6 +44,11 @@ export class ProductInput { @Field({ defaultValue: true }) inStock: boolean; + @IsNullable() + @IsDate() + @Field({ nullable: true }) + availableSince?: Date; + @IsNotEmpty() @Field(() => RootBlockInputScalar(DamImageBlock)) @Transform(({ value }) => (isBlockInputInterface(value) ? value : DamImageBlock.blockInputFactory(value)), { toClassOnly: true }) diff --git a/demo/api/src/products/generated/dto/product.sort.ts b/demo/api/src/products/generated/dto/product.sort.ts index 7bae651c2f..d87a5f1611 100644 --- a/demo/api/src/products/generated/dto/product.sort.ts +++ b/demo/api/src/products/generated/dto/product.sort.ts @@ -13,6 +13,7 @@ export enum ProductSortField { price = "price", inStock = "inStock", soldCount = "soldCount", + availableSince = "availableSince", category = "category", createdAt = "createdAt", updatedAt = "updatedAt", diff --git a/packages/admin/cms-admin/package.json b/packages/admin/cms-admin/package.json index 9a04bc99c7..36d8fb033f 100644 --- a/packages/admin/cms-admin/package.json +++ b/packages/admin/cms-admin/package.json @@ -44,6 +44,7 @@ "@graphql-tools/load": "^7.8.14", "@graphql-typed-document-node/core": "^3.1.1", "@mui/lab": "^5.0.0-alpha.76", + "change-case": "^4.1.2", "class-validator": "0.13.2", "clsx": "^1.1.1", "commander": "^10.0.1", @@ -58,6 +59,7 @@ "lodash.isequal": "^4.0.0", "lodash.set": "^4.3.2", "mime-db": "^1.0.0", + "object-path": "^0.11.8", "p-debounce": "^4.0.0", "pluralize": "^8.0.0", "prop-types": "^15.7.2", @@ -107,6 +109,7 @@ "@types/lodash.set": "^4.3.6", "@types/mime-db": "^1.43.1", "@types/node": "^18.0.0", + "@types/object-path": "^0.11.4", "@types/pluralize": "^0.0.29", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", @@ -124,6 +127,7 @@ "draft-js": "^0.11.7", "eslint": "^8.0.0", "final-form": "^4.20.9", + "glob": "^10.3.10", "graphql": "^15.0.0", "jest": "^29.5.0", "jest-junit": "^15.0.0", diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts new file mode 100644 index 0000000000..9feff93f4a --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -0,0 +1,282 @@ +import { IntrospectionQuery } from "graphql"; + +import { generateFormField } from "./generateFormField"; +import { FormConfigInternal, GeneratorReturn } from "./generator"; +import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; +import { findRootBlocks } from "./utils/findRootBlocks"; +import { generateFieldListGqlString } from "./utils/generateFieldList"; +import { generateImportsCode, Imports } from "./utils/generateImportsCode"; + +export function generateForm( + { + exportName, + baseOutputFilename, + targetDirectory, + gqlIntrospection, + }: { exportName: string; baseOutputFilename: string; targetDirectory: string; gqlIntrospection: IntrospectionQuery }, + config: FormConfigInternal, +): GeneratorReturn { + const gqlType = config.gqlType; + const title = config.title ?? camelCaseToHumanReadable(gqlType); + const instanceGqlType = gqlType[0].toLowerCase() + gqlType.substring(1); + const gqlDocuments: Record = {}; + const imports: Imports = []; + + const fieldNamesFromConfig = config.fields.map((field) => field.name); + const fieldList = generateFieldListGqlString(fieldNamesFromConfig); + + // TODO make RootBlocks configurable (from config) + const rootBlocks = findRootBlocks({ gqlType, targetDirectory }, gqlIntrospection); + + const numberFields = config.fields.filter((field) => field.type == "number"); + const booleanFields = config.fields.filter((field) => field.type == "boolean"); + + const fragmentName = config.fragmentName ?? `${gqlType}Form`; + gqlDocuments[`${instanceGqlType}FormFragment`] = ` + fragment ${fragmentName} on ${gqlType} { ${fieldList} } + `; + + gqlDocuments[`${instanceGqlType}Query`] = ` + query ${gqlType}($id: ID!) { + ${instanceGqlType}(id: $id) { + id + updatedAt + ...${fragmentName} + } + } + \${${`${instanceGqlType}FormFragment`}} + `; + + gqlDocuments[`create${gqlType}Mutation`] = ` + mutation Create${gqlType}($input: ${gqlType}Input!) { + create${gqlType}(input: $input) { + id + updatedAt + ...${fragmentName} + } + } + \${${`${instanceGqlType}FormFragment`}} + `; + + gqlDocuments[`update${gqlType}Mutation`] = ` + mutation Update${gqlType}($id: ID!, $input: ${gqlType}UpdateInput!, $lastUpdatedAt: DateTime) { + update${gqlType}(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + id + updatedAt + ...${fragmentName} + } + } + \${${`${instanceGqlType}FormFragment`}} + `; + + const fieldsCode = config.fields + .map((field) => { + const generated = generateFormField({ gqlIntrospection }, field, config); + for (const name in generated.gqlDocuments) { + gqlDocuments[name] = generated.gqlDocuments[name]; + } + imports.push(...generated.imports); + return generated.code; + }) + .join("\n"); + + const code = `import { useApolloClient, useQuery } from "@apollo/client"; + import { + Field, + FinalForm, + FinalFormCheckbox, + FinalFormInput, + FinalFormSaveSplitButton, + FinalFormSelect, + FinalFormSubmitEvent, + Loading, + MainContent, + Toolbar, + ToolbarActions, + ToolbarFillSpace, + ToolbarItem, + ToolbarTitleItem, + useFormApiRef, + useStackApi, + useStackSwitchApi, + } from "@comet/admin"; + import { ArrowLeft } from "@comet/admin-icons"; + import { FinalFormDatePicker } from "@comet/admin-date-time"; + import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; + import { EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; + import { FormControlLabel, IconButton, MenuItem } from "@mui/material"; + import { FormApi } from "final-form"; + import { filter } from "graphql-anywhere"; + import isEqual from "lodash.isequal"; + import React from "react"; + import { FormattedMessage } from "react-intl"; + ${generateImportsCode(imports)} + + import { + create${gqlType}Mutation, + ${instanceGqlType}FormFragment, + ${instanceGqlType}Query, + update${gqlType}Mutation, + } from "./${baseOutputFilename}.gql"; + import { + GQLCreate${gqlType}Mutation, + GQLCreate${gqlType}MutationVariables, + GQL${fragmentName}Fragment, + GQL${gqlType}Query, + GQL${gqlType}QueryVariables, + GQLUpdate${gqlType}Mutation, + GQLUpdate${gqlType}MutationVariables, + } from "./${baseOutputFilename}.gql.generated"; + ${Object.entries(rootBlocks) + .map(([rootBlockKey, rootBlock]) => `import { ${rootBlock.name} } from "${rootBlock.import}";`) + .join("\n")} + + ${ + Object.keys(rootBlocks).length > 0 + ? `const rootBlocks = { + ${Object.entries(rootBlocks).map(([rootBlockKey, rootBlock]) => `${rootBlockKey}: ${rootBlock.name}`)} + };` + : "" + } + + type FormValues = ${ + numberFields.length > 0 + ? `Omit `"${String(field.name)}"`).join(" | ")}>` + : `GQL${fragmentName}Fragment` + } ${ + numberFields.length > 0 || Object.keys(rootBlocks).length > 0 + ? `& { + ${numberFields.map((field) => `${String(field.name)}: string;`).join("\n")} + ${Object.keys(rootBlocks) + .map((rootBlockKey) => `${rootBlockKey}: BlockState;`) + .join("\n")} + }` + : "" + }; + + interface FormProps { + id?: string; + } + + export function ${exportName}({ id }: FormProps): React.ReactElement { + const stackApi = useStackApi(); + const client = useApolloClient(); + const mode = id ? "edit" : "add"; + const formApiRef = useFormApiRef(); + const stackSwitchApi = useStackSwitchApi(); + + const { data, error, loading, refetch } = useQuery( + ${instanceGqlType}Query, + id ? { variables: { id } } : { skip: true }, + ); + + const initialValues = React.useMemo>(() => data?.${instanceGqlType} + ? { + ...filter(${instanceGqlType}FormFragment, data.${instanceGqlType}), + ${numberFields.map((field) => `${String(field.name)}: String(data.${instanceGqlType}.${String(field.name)}),`).join("\n")} + ${Object.keys(rootBlocks) + .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.input2State(data.${instanceGqlType}.${rootBlockKey}),`) + .join("\n")} + } + : { + ${booleanFields.map((field) => `${String(field.name)}: false,`).join("\n")} + ${Object.keys(rootBlocks) + .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.defaultValues(),`) + .join("\n")} + } + , [data]); + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "${instanceGqlType}", id); + return resolveHasSaveConflict(data?.${instanceGqlType}.updatedAt, updatedAt); + }, + formApiRef, + loadLatestVersion: async () => { + await refetch(); + }, + }); + + const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); + const output = { + ...formValues, + ${numberFields.map((field) => `${String(field.name)}: parseFloat(formValues.${String(field.name)}),`).join("\n")} + ${Object.keys(rootBlocks) + .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.state2Output(formValues.${rootBlockKey}),`) + .join("\n")} + }; + if (mode === "edit") { + if (!id) throw new Error(); + await client.mutate({ + mutation: update${gqlType}Mutation, + variables: { id, input: output, lastUpdatedAt: data?.${instanceGqlType}.updatedAt }, + }); + } else { + const { data: mutationResponse } = await client.mutate({ + mutation: create${gqlType}Mutation, + variables: { input: output }, + }); + if (!event.navigatingBack) { + const id = mutationResponse?.create${gqlType}.id; + if (id) { + setTimeout(() => { + stackSwitchApi.activatePage(\`edit\`, id); + }); + } + } + } + }; + + if (error) throw error; + + if (loading) { + return ; + } + + return ( + + apiRef={formApiRef} + onSubmit={handleSubmit} + mode={mode} + initialValues={initialValues} + initialValuesEqual={isEqual} //required to compare block data correctly + subscription={{}} + > + {() => ( + + {saveConflict.dialogs} + + + + + + + + + {({ input }) => + input.value ? input.value : + } + + + + + + + + + ${fieldsCode} + + + )} + + ); + } + + `; + + return { + code, + gqlDocuments, + }; +} diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField.ts b/packages/admin/cms-admin/src/generator/future/generateFormField.ts new file mode 100644 index 0000000000..c22accb1dc --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/generateFormField.ts @@ -0,0 +1,118 @@ +import { IntrospectionEnumType, IntrospectionNamedTypeRef, IntrospectionQuery } from "graphql"; + +import { FormConfigInternal, FormFieldConfigInternal, GeneratorReturn } from "./generator"; +import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; +import { generateFieldListFromIntrospection } from "./utils/generateFieldList"; +import { Imports } from "./utils/generateImportsCode"; + +export function generateFormField( + { gqlIntrospection }: { gqlIntrospection: IntrospectionQuery }, + config: FormFieldConfigInternal, + formConfig: FormConfigInternal, +): GeneratorReturn & { imports: Imports } { + const gqlType = formConfig.gqlType; + const instanceGqlType = gqlType[0].toLowerCase() + gqlType.substring(1); + + const name = String(config.name); + const label = config.label ?? camelCaseToHumanReadable(name); + + const introspectedTypes = gqlIntrospection.__schema.types; + const introspectionObject = introspectedTypes.find((type) => type.kind === "OBJECT" && type.name === gqlType); + if (!introspectionObject || introspectionObject.kind !== "OBJECT") throw new Error(`didn't find object ${gqlType} in gql introspection`); + + const introspectedFields = generateFieldListFromIntrospection(gqlIntrospection, gqlType); + + const introspectionFieldWithPath = introspectedFields.find((field) => field.path === name); + if (!introspectionFieldWithPath) throw new Error(`didn't find field ${name} in gql introspection type ${gqlType}`); + const introspectionField = introspectionFieldWithPath.field; + const introspectionFieldType = introspectionField.type.kind === "NON_NULL" ? introspectionField.type.ofType : introspectionField.type; + + const requiredByIntrospection = introspectionField.type.kind == "NON_NULL"; + + const required = config.required ?? requiredByIntrospection; //if undefined default to requiredByIntrospection + + //TODO verify introspectionField.type is compatbile with config.type + + const imports: Imports = []; + let code = ""; + if (config.type == "text") { + code = ` + } + />`; + } else if (config.type == "number") { + code = ` + } + />`; + //TODO MUI suggest not using type=number https://mui.com/material-ui/react-text-field/#type-quot-number-quot + } else if (config.type == "boolean") { + code = ` + {(props) => ( + } + control={} + /> + )} + `; + } else if (config.type == "date") { + code = ` + } + />`; + } else if (config.type == "block") { + imports.push({ + name: config.block.name, + importPath: config.block.import, + }); + code = ` + {createFinalFormBlock(${config.block.name})} + `; + } else if (config.type == "staticSelect") { + if (config.values) { + throw new Error("custom values for staticSelect is not yet supported"); // TODO add support + } + const enumType = gqlIntrospection.__schema.types.find( + (t) => t.kind === "ENUM" && t.name === (introspectionFieldType as IntrospectionNamedTypeRef).name, + ) as IntrospectionEnumType | undefined; + if (!enumType) throw new Error(`Enum type ${(introspectionFieldType as IntrospectionNamedTypeRef).name} not found for field ${name}`); + const values = enumType.enumValues.map((i) => i.name); + code = `}> + {(props) => + + ${values + .map((value) => { + const id = `${instanceGqlType}.${name}.${value.charAt(0).toLowerCase() + value.slice(1)}`; + const label = ``; + return `${label}`; + }) + .join("\n")} + + } + `; + } else { + throw new Error(`Unsupported type: ${config.type}`); + } + return { + code, + gqlDocuments: {}, + imports, + }; +} diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts new file mode 100644 index 0000000000..e87f518247 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -0,0 +1,467 @@ +import { + IntrospectionEnumType, + IntrospectionInputObjectType, + IntrospectionInputValue, + IntrospectionNamedTypeRef, + IntrospectionObjectType, + IntrospectionQuery, +} from "graphql"; +import { plural } from "pluralize"; + +import { GeneratorReturn, GridConfig, GridConfigInternal } from "./generator"; +import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; +import { findRootBlocks } from "./utils/findRootBlocks"; + +function tsCodeRecordToString(object: Record) { + return `{${Object.entries(object) + .filter(([key, value]) => value !== undefined) + .map(([key, value]) => `${key}: ${value},`) + .join("\n")}}`; +} + +function findQueryType(queryName: string, schema: IntrospectionQuery) { + const queryType = schema.__schema.types.find((type) => type.name === schema.__schema.queryType.name) as IntrospectionObjectType | undefined; + if (!queryType) throw new Error("Can't find Query type in gql schema"); + const ret = queryType.fields.find((field) => field.name === queryName); + if (!ret) throw new Error(`Can't find query ${queryName} in gql schema`); + return ret; +} + +function findQueryTypeOrThrow(queryName: string, schema: IntrospectionQuery) { + const ret = findQueryType(queryName, schema); + if (!ret) throw new Error(`Can't find query ${queryName} in gql schema`); + return ret; +} + +function findMutationType(mutationName: string, schema: IntrospectionQuery) { + if (!schema.__schema.mutationType) throw new Error("Schema has no Mutation type"); + const queryType = schema.__schema.types.find((type) => type.name === schema.__schema.mutationType?.name) as IntrospectionObjectType | undefined; + if (!queryType) throw new Error("Can't find Mutation type in gql schema"); + return queryType.fields.find((field) => field.name === mutationName); +} + +function findInputObjectType(input: IntrospectionInputValue, schema: IntrospectionQuery) { + let type = input.type; + if (type.kind == "NON_NULL") { + type = type.ofType; + } + if (type.kind !== "INPUT_OBJECT") { + throw new Error("must be INPUT_OBJECT"); + } + const typeName = type.name; + const filterType = schema.__schema.types.find((type) => type.kind === "INPUT_OBJECT" && type.name === typeName) as + | IntrospectionInputObjectType + | undefined; + return filterType; +} + +export function generateGrid( + { + exportName, + baseOutputFilename, + targetDirectory, + gqlIntrospection, + }: { exportName: string; baseOutputFilename: string; targetDirectory: string; gqlIntrospection: IntrospectionQuery }, + config: GridConfigInternal, +): GeneratorReturn { + const gqlType = config.gqlType; + const gqlTypePlural = plural(gqlType); + //const title = config.title ?? camelCaseToHumanReadable(gqlType); + const instanceGqlType = gqlType[0].toLowerCase() + gqlType.substring(1); + const instanceGqlTypePlural = gqlTypePlural[0].toLowerCase() + gqlTypePlural.substring(1); + const gridQuery = instanceGqlType != instanceGqlTypePlural ? instanceGqlTypePlural : `${instanceGqlTypePlural}List`; + const gqlDocuments: Record = {}; + //const imports: Imports = []; + + const rootBlocks = findRootBlocks({ gqlType, targetDirectory }, gqlIntrospection); + + const gridQueryType = findQueryTypeOrThrow(gridQuery, gqlIntrospection); + + const createMutationType = findMutationType(`create${gqlType}`, gqlIntrospection); + + const hasDeleteMutation = !!findMutationType(`delete${gqlType}`, gqlIntrospection); + const hasCreateMutation = !!createMutationType; + + const filterArg = gridQueryType.args.find((arg) => arg.name === "filter"); + const hasFilter = !!filterArg; + let filterFields: string[] = []; + if (filterArg) { + const filterType = findInputObjectType(filterArg, gqlIntrospection); + if (!filterType) throw new Error("Can't find filter type"); + filterFields = filterType.inputFields.map((f) => f.name); + } + + const sortArg = gridQueryType.args.find((arg) => arg.name === "sort"); + const hasSort = !!sortArg; + let sortFields: string[] = []; + if (sortArg) { + if (sortArg.type.kind !== "LIST") { + throw new Error("Sort argument must be LIST"); + } + if (sortArg.type.ofType.kind !== "NON_NULL") { + throw new Error("Sort argument must be LIST->NON_NULL"); + } + if (sortArg.type.ofType.ofType.kind !== "INPUT_OBJECT") { + throw new Error("Sort argument must be LIST->NON_NULL->INPUT_OBJECT"); + } + const sortTypeName = sortArg.type.ofType.ofType.name; + const sortType = gqlIntrospection.__schema.types.find((type) => type.kind === "INPUT_OBJECT" && type.name === sortTypeName) as + | IntrospectionInputObjectType + | undefined; + if (!sortType) throw new Error("Can't find sort type"); + const sortField = sortType.inputFields.find((i) => i.name == "field"); + if (!sortField) throw new Error("Can't find sortFieldName"); + if (sortField.type.kind !== "NON_NULL") throw new Error("sortField must be NON_NULL"); + if (sortField.type.ofType.kind != "ENUM") throw new Error("sortField must be NON_NULL->ENUM"); + const sortFieldEnumName = sortField.type.ofType.name; + const sortInputEnum = gqlIntrospection.__schema.types.find((type) => type.kind === "ENUM" && type.name === sortFieldEnumName) as + | IntrospectionEnumType + | undefined; + if (!sortInputEnum) throw new Error("Can't find sortInputEnum"); + sortFields = sortInputEnum.enumValues.map((v) => v.name); + } + + const hasSearch = gridQueryType.args.some((arg) => arg.name === "search"); + const hasScope = gridQueryType.args.some((arg) => arg.name === "scope"); + + const schemaEntity = gqlIntrospection.__schema.types.find((type) => type.kind === "OBJECT" && type.name === gqlType) as + | IntrospectionObjectType + | undefined; + if (!schemaEntity) throw new Error("didn't find entity in schema types"); + + //we load /all/ fields as we need it for copy/paste TODO: lazy load during copy? + const fieldsToLoad = schemaEntity.fields + .filter((field) => { + if (field.name === "id" || field.name === "scope") return false; + return true; + }) + .filter((field) => { + let type = field.type; + if (type.kind == "NON_NULL") type = type.ofType; + if (type.kind == "LIST") return false; + if (type.kind == "OBJECT") return false; //TODO support nested objects + return true; + }); + + const gridColumnFields = config.columns.map((column) => { + const type = column.type; + const name = String(column.name); + + let renderCell: string | undefined = undefined; + let valueGetter: string | undefined = undefined; + + let gridType: "number" | "boolean" | "dateTime" | "date" | undefined; + + if (type == "dateTime") { + valueGetter = `({ value }) => value && new Date(value)`; + gridType = "dateTime"; + } else if (type == "date") { + valueGetter = `({ value }) => value && new Date(value)`; + gridType = "date"; + } else if (type == "block") { + if (rootBlocks[name]) { + renderCell = `(params) => { + return ; + }`; + } + } else if (type == "staticSelect") { + if (column.values) { + throw new Error("custom values for staticSelect is not yet supported"); // TODO add support + } + const introspectionField = schemaEntity.fields.find((field) => field.name === name); + if (!introspectionField) throw new Error(`didn't find field ${name} in gql introspection type ${gqlType}`); + const introspectionFieldType = introspectionField.type.kind === "NON_NULL" ? introspectionField.type.ofType : introspectionField.type; + + const enumType = gqlIntrospection.__schema.types.find( + (t) => t.kind === "ENUM" && t.name === (introspectionFieldType as IntrospectionNamedTypeRef).name, + ) as IntrospectionEnumType | undefined; + if (!enumType) throw new Error(`Enum type not found`); + const values = enumType.enumValues.map((i) => i.name); + const valueOptions = `[${values + .map((i) => { + const id = `${instanceGqlType}.${name}.${i.charAt(0).toLowerCase() + i.slice(1)}`; + const label = `intl.formatMessage({ id: "${id}", defaultMessage: "${camelCaseToHumanReadable(i)}" })`; + return `{value: ${JSON.stringify(i)}, label: ${label}}, `; + }) + .join(" ")}]`; + + return { + name, + type, + gridType: "singleSelect" as const, + valueOptions, + }; + } + + //TODO suppoort n:1 relation with singleSelect + + return { + name, + headerName: column.headerName, + width: column.width, + type, + gridType, + renderCell, + valueGetter, + }; + }); + + let createMutationInputFields: readonly IntrospectionInputValue[] = []; + { + const inputArg = createMutationType?.args.find((arg) => arg.name === "input"); + if (inputArg) { + const inputType = findInputObjectType(inputArg, gqlIntrospection); + if (!inputType) throw new Error("Can't find input type"); + createMutationInputFields = inputType.inputFields.filter((field) => + fieldsToLoad.some((gridColumnField) => gridColumnField.name == field.name), + ); + } + } + + const fragmentName = config.fragmentName ?? `${gqlTypePlural}Form`; + + const code = `import { gql, useApolloClient, useQuery } from "@apollo/client"; + import { + CrudContextMenu, + GridFilterButton, + MainContent, + muiGridFilterToGql, + muiGridSortToGql, + StackLink, + Toolbar, + ToolbarActions, + ToolbarAutomaticTitleItem, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, + } from "@comet/admin"; + import { Add as AddIcon, Edit } from "@comet/admin-icons"; + import { BlockPreviewContent } from "@comet/blocks-admin"; + import { Alert, Button, Box, IconButton } from "@mui/material"; + import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; + import { useContentScope } from "@src/common/ContentScopeProvider"; + import { + GQL${gqlTypePlural}GridQuery, + GQL${gqlTypePlural}GridQueryVariables, + GQL${fragmentName}Fragment, + GQLCreate${gqlType}Mutation, + GQLCreate${gqlType}MutationVariables, + GQLDelete${gqlType}Mutation, + GQLDelete${gqlType}MutationVariables + } from "./${gqlTypePlural}Grid.generated"; + import * as React from "react"; + import { FormattedMessage, useIntl } from "react-intl"; + ${Object.entries(rootBlocks) + .map(([rootBlockKey, rootBlock]) => `import { ${rootBlock.name} } from "${rootBlock.import}";`) + .join("\n")} + + const ${instanceGqlTypePlural}Fragment = gql\` + fragment ${fragmentName} on ${gqlType} { + id + ${fieldsToLoad.map((field) => field.name).join("\n")} + } + \`; + + const ${instanceGqlTypePlural}Query = gql\` + query ${gqlTypePlural}Grid($offset: Int, $limit: Int${hasSort ? `, $sort: [${gqlType}Sort!]` : ""}${hasSearch ? `, $search: String` : ""}${ + hasFilter ? `, $filter: ${gqlType}Filter` : "" + }${hasScope ? `, $scope: ${gqlType}ContentScopeInput!` : ""}) { + ${gridQuery}(offset: $offset, limit: $limit${hasSort ? `, sort: $sort` : ""}${hasSearch ? `, search: $search` : ""}${ + hasFilter ? `, filter: $filter` : "" + }${hasScope ? `, scope: $scope` : ""}) { + nodes { + ...${fragmentName} + } + totalCount + } + } + \${${instanceGqlTypePlural}Fragment} + \`; + + + ${ + hasDeleteMutation + ? `const delete${gqlType}Mutation = gql\` + mutation Delete${gqlType}($id: ID!) { + delete${gqlType}(id: $id) + } + \`;` + : "" + } + + ${ + hasCreateMutation + ? `const create${gqlType}Mutation = gql\` + mutation Create${gqlType}(${hasScope ? `$scope: ${gqlType}ContentScopeInput!, ` : ""}$input: ${gqlType}Input!) { + create${gqlType}(${hasScope ? `scope: $scope, ` : ""}input: $input) { + id + } + } + \`;` + : "" + } + + function ${gqlTypePlural}GridToolbar() { + return ( + + + ${ + hasSearch + ? ` + + ` + : "" + } + ${ + hasFilter + ? ` + + ` + : "" + } + + ${ + hasCreateMutation + ? ` + + ` + : "" + } + + ); + } + + + export function ${gqlTypePlural}Grid(): React.ReactElement { + const client = useApolloClient(); + const intl = useIntl(); + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("${gqlTypePlural}Grid") }; + ${hasScope ? `const { scope } = useContentScope();` : ""} + + const columns: GridColDef[] = [ + ${gridColumnFields + .map((column) => + tsCodeRecordToString({ + field: `"${column.name}"`, + headerName: `intl.formatMessage({ id: "${instanceGqlType}.${column.name}", defaultMessage: "${ + column.headerName || camelCaseToHumanReadable(column.name) + }" })`, + type: column.gridType ? `"${column.gridType}"` : undefined, + filterable: !filterFields.includes(column.name) ? `false` : undefined, + sortable: !sortFields.includes(column.name) ? `false` : undefined, + valueGetter: column.valueGetter, + valueOptions: column.valueOptions, + width: column.width ? String(column.width) : "150", + renderCell: column.renderCell, + }), + ) + .join(",\n")}, + { + field: "actions", + headerName: "", + sortable: false, + filterable: false, + type: "actions", + renderCell: (params) => { + return ( + <> + + + + { + const row = params.row; + return { + ${createMutationInputFields + .map((field) => { + if (rootBlocks[field.name]) { + const blockName = rootBlocks[field.name].name; + return `${field.name}: ${blockName}.state2Output(${blockName}.input2State(row.${field.name}))`; + } else { + return `${field.name}: row.${field.name}`; + } + }) + .join(",\n")} + }; + }} + onPaste={async ({ input }) => { + await client.mutate({ + mutation: create${gqlType}Mutation, + variables: { ${hasScope ? `scope, ` : ""}input }, + }); + }} + ` + : "" + } + ${ + hasDeleteMutation + ? ` + onDelete={async () => { + await client.mutate({ + mutation: delete${gqlType}Mutation, + variables: { id: params.row.id }, + }); + }} + ` + : "" + } + refetchQueries={[${instanceGqlTypePlural}Query]} + /> + + ); + }, + }, + ]; + + ${ + hasFilter || hasSearch + ? `const { ${hasFilter ? `filter: gqlFilter, ` : ""}${ + hasSearch ? `search: gqlSearch, ` : "" + } } = muiGridFilterToGql(columns, dataGridProps.filterModel);` + : "" + } + + const { data, loading, error } = useQuery(${instanceGqlTypePlural}Query, { + variables: { + ${hasScope ? `scope,` : ""} + ${hasFilter ? `filter: gqlFilter,` : ""} + ${hasSearch ? `search: gqlSearch,` : ""} + offset: dataGridProps.page * dataGridProps.pageSize, + limit: dataGridProps.pageSize, + sort: muiGridSortToGql(dataGridProps.sortModel), + }, + }); + const rowCount = useBufferedRowCount(data?.${gridQuery}.totalCount); + if (error) throw error; + const rows = data?.${gridQuery}.nodes ?? []; + + return ( + + + + ); + } + `; + + return { + code, + gqlDocuments, + }; +} diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts new file mode 100644 index 0000000000..1abc5d2e7d --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -0,0 +1,118 @@ +import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"; +import { loadSchema } from "@graphql-tools/load"; +import { glob } from "glob"; +import { introspectionFromSchema } from "graphql"; +import { basename, dirname } from "path"; + +import { generateForm } from "./generateForm"; +import { generateGrid } from "./generateGrid"; +import { Leaves, Paths } from "./utils/deepKeyOf"; +import { writeGenerated } from "./utils/writeGenerated"; + +type BlockReference = { + name: string; + import: string; +}; + +export type GeneratorEntity = { __typename?: string }; + +export type FormFieldConfigInternal = + // extra internal type to avoid "Type instantiation is excessively deep and possibly infinite." because of name-typing and simplify typing + ( + | { type: "text"; multiline?: boolean } + | { type: "number" } + | { type: "boolean" } + | { type: "date" } + // TODO | { type: "dateTime" } + | { type: "staticSelect"; values?: string[] } + | { type: "asyncSelect"; values?: string[] } + | { type: "block"; block: BlockReference } + ) & { name: string; label?: string; required?: boolean }; +export type FormFieldConfig = FormFieldConfigInternal & { name: Leaves | Paths }; + +export type FormConfigInternal = { + type: "form"; + gqlType: string; + fragmentName?: string; + fields: FormFieldConfigInternal[]; + title?: string; +}; +export type FormConfig = FormConfigInternal & { + gqlType: T["__typename"]; + fields: FormFieldConfig[]; +}; + +export type TabsConfig = { type: "tabs"; tabs: { name: string; content: GeneratorConfig }[] }; + +export type GridColumnConfigInternal = // extra internal type to avoid "Type instantiation is excessively deep and possibly infinite." because of name-typing and simplify typing + ( + | { type: "text" } + | { type: "number" } + | { type: "boolean" } + | { type: "date" } + | { type: "dateTime" } + | { type: "staticSelect"; values?: string[] } + | { type: "block"; block: BlockReference } + ) & { name: string; headerName?: string; width?: number }; +export type GridColumnConfig = Omit & { name: Leaves | Paths }; +export type GridConfigInternal = { + type: "grid"; + gqlType: string; + fragmentName?: string; + columns: GridColumnConfigInternal[]; +}; +export type GridConfig = Omit & { + gqlType: T["__typename"]; + columns: GridColumnConfig[]; +}; + +export type GeneratorConfig = FormConfigInternal | GridConfigInternal | TabsConfig; + +export type GeneratorReturn = { code: string; gqlDocuments: Record }; + +export async function runFutureGenerate() { + const schema = await loadSchema("./schema.gql", { + loaders: [new GraphQLFileLoader()], + }); + const gqlIntrospection = introspectionFromSchema(schema); + + const files = await glob("src/**/*.cometGen.ts"); + for (const file of files) { + let outputCode = ""; + let gqlDocumentsOutputCode = ""; + const targetDirectory = `${dirname(file)}/generated`; + const baseOutputFilename = basename(file).replace(/\.cometGen\.ts$/, ""); + const configs = await import(`${process.cwd()}/${file.replace(/\.ts$/, "")}`); + //const configs = await import(`${process.cwd()}/${file}`); + + for (const exportName in configs) { + const config = configs[exportName] as GeneratorConfig; + let generated: GeneratorReturn; + if (config.type == "form") { + generated = generateForm({ exportName, gqlIntrospection, baseOutputFilename, targetDirectory }, config); + } else if (config.type == "grid") { + generated = generateGrid({ exportName, gqlIntrospection, baseOutputFilename, targetDirectory }, config); + } else { + throw new Error(`Unknown config type: ${config.type}`); + } + outputCode += generated.code; + for (const queryName in generated.gqlDocuments) { + gqlDocumentsOutputCode += `export const ${queryName} = gql\`${generated.gqlDocuments[queryName]}\`\n`; + } + } + + { + const codeOuputFilename = `${targetDirectory}/${basename(file.replace(/\.cometGen\.ts$/, ""))}.tsx`; + await writeGenerated(codeOuputFilename, outputCode); + } + + if (gqlDocumentsOutputCode != "") { + const gqlDocumentsOuputFilename = `${targetDirectory}/${basename(file.replace(/\.cometGen\.ts$/, ""))}.gql.tsx`; + gqlDocumentsOutputCode = `import { gql } from "@apollo/client"; + + ${gqlDocumentsOutputCode} + `; + await writeGenerated(gqlDocumentsOuputFilename, gqlDocumentsOutputCode); + } + } +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/camelCaseToHumanReadable.ts b/packages/admin/cms-admin/src/generator/future/utils/camelCaseToHumanReadable.ts new file mode 100644 index 0000000000..a82e7e2547 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/camelCaseToHumanReadable.ts @@ -0,0 +1,5 @@ +import { capitalCase } from "change-case"; + +export function camelCaseToHumanReadable(s: string) { + return capitalCase(s); +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts b/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts new file mode 100644 index 0000000000..9f65c5bff7 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts @@ -0,0 +1,18 @@ +// https://stackoverflow.com/a/58436959 + +type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]; +type Join = K extends string | number ? (P extends string | number ? `${K}${"" extends P ? "" : "."}${P}` : never) : never; + +export type Paths = [D] extends [never] + ? never + : T extends object + ? { + [K in keyof T]-?: K extends string | number ? `${K}` | Join> : never; + }[keyof T] + : ""; + +export type Leaves = [D] extends [never] + ? never + : T extends object + ? { [K in keyof T]-?: Join> }[keyof T] + : ""; diff --git a/packages/admin/cms-admin/src/generator/future/utils/findRootBlocks.ts b/packages/admin/cms-admin/src/generator/future/utils/findRootBlocks.ts new file mode 100644 index 0000000000..f887d50bfb --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/findRootBlocks.ts @@ -0,0 +1,65 @@ +import { existsSync } from "fs"; +import { IntrospectionObjectType, IntrospectionQuery } from "graphql"; + +const fallbackLibraryBlocks: { [key: string]: string } = { + AnchorBlock: "@comet/cms-admin", + DamImageBlock: "@comet/cms-admin", + DamVideoBlock: "@comet/cms-admin", + ExternalLinkBlock: "@comet/cms-admin", + InternalLinkBlock: "@comet/cms-admin", + PixelImageBlock: "@comet/cms-admin", + SpaceBlock: "@comet/blocks-admin", + SvgImageBlock: "@comet/cms-admin", + YouTubeVideoBlock: "@comet/blocks-admin", +}; + +export function findRootBlocks({ gqlType, targetDirectory }: { gqlType: string; targetDirectory: string }, schema: IntrospectionQuery) { + const ret: Record = {}; + + const schemaEntity = schema.__schema.types.find((type) => type.kind === "OBJECT" && type.name === gqlType) as IntrospectionObjectType | undefined; + if (!schemaEntity) throw new Error("didn't find entity in schema types"); + schemaEntity.fields.forEach((field) => { + if (ret[field.name]) return; // already defined + let type = field.type; + if (type.kind == "NON_NULL") type = type.ofType; + if (type.kind == "SCALAR" && type.name.endsWith("BlockData")) { + let match = false; + const blockName = `${type.name.replace(/BlockData$/, "")}Block`; + const checkNames = [ + { + folderName: `${targetDirectory.replace(/\/generated$/, "")}/blocks`, + import: `../blocks/${blockName}`, + }, + { + folderName: `src/common/blocks`, + import: `@src/common/blocks/${blockName}`, + }, + ]; + for (const checkName of checkNames) { + if (existsSync(`${checkName.folderName}/${blockName}.tsx`)) { + match = true; + ret[field.name] = { + import: checkName.import, + name: blockName, + }; + break; + } + } + if (!match) { + const fallback = fallbackLibraryBlocks[blockName]; + if (fallback) { + ret[field.name] = { + import: fallback, + name: blockName, + }; + match = true; + } + } + if (!match) { + throw new Error(`Didn't find admin block for ${blockName} in ${checkNames.map((c) => c.folderName).join(" or ")}`); + } + } + }); + + return ret; +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts new file mode 100644 index 0000000000..800fe3e1b3 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts @@ -0,0 +1,55 @@ +import { IntrospectionField, IntrospectionQuery, IntrospectionType } from "graphql"; +import objectPath from "object-path"; + +export function generateFieldListGqlString(fields: string[]) { + type FieldsObjectType = { [key: string]: FieldsObjectType | boolean }; + const fieldsObject: FieldsObjectType = fields.reduce((acc, fieldName) => { + objectPath.set(acc, fieldName, true); + return acc; + }, {}); + + const recursiveStringify = (obj: FieldsObjectType): string => { + let ret = ""; + let prefixField = ""; + for (const key in obj) { + const valueForKey = obj[key]; + if (typeof valueForKey === "boolean") { + ret += `${prefixField}${key}`; + } else { + ret += `${prefixField}${key} { ${recursiveStringify(valueForKey)} }`; + } + prefixField = " "; + } + return ret; + }; + return recursiveStringify(fieldsObject); +} + +function fieldListFromIntrospectionTypeRecursive( + types: readonly IntrospectionType[], + type: string, + parentPath?: string, +): { path: string; field: IntrospectionField }[] { + const typeDef = types.find((typeDef) => typeDef.name === type); + if (!typeDef || typeDef.kind !== "OBJECT") return []; + + return typeDef.fields.reduce<{ path: string; field: IntrospectionField }[]>((acc, field) => { + const path = `${parentPath ? `${parentPath}.` : ""}${field.name}`; + if (field.type.kind === "OBJECT") { + const subFields = fieldListFromIntrospectionTypeRecursive(types, field.type.name, path); + acc.push(...subFields); + } else { + acc.push({ + path: path, + field: field, + }); + } + return acc; + }, []); +} +export function generateFieldListFromIntrospection( + gqlIntrospection: IntrospectionQuery, + type: string, +): { path: string; field: IntrospectionField }[] { + return fieldListFromIntrospectionTypeRecursive(gqlIntrospection.__schema.types, type); +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateImportsCode.ts b/packages/admin/cms-admin/src/generator/future/utils/generateImportsCode.ts new file mode 100644 index 0000000000..e4fc011206 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/generateImportsCode.ts @@ -0,0 +1,28 @@ +export type Imports = Array<{ + name: string; + importPath: string; +}>; + +// generate imports code and filter duplicates +export function generateImportsCode(imports: Imports): string { + const importsNameToPath: Map = new Map(); // name -> importPath + const filteredImports = imports.filter((imp) => { + if (importsNameToPath.has(imp.name)) { + if (importsNameToPath.get(imp.name) !== imp.importPath) { + throw new Error(`Duplicate import name ${imp.name}`); + } else { + // duplicate import, skip + return false; + } + } + importsNameToPath.set(imp.name, imp.importPath); + return true; + }); + + const importsString = filteredImports + .map((imp) => { + return `import { ${imp.name} } from "${imp.importPath}";`; + }) + .join("\n"); + return importsString; +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/writeGenerated.ts b/packages/admin/cms-admin/src/generator/future/utils/writeGenerated.ts new file mode 100644 index 0000000000..589fdc8f90 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/writeGenerated.ts @@ -0,0 +1,22 @@ +import { ESLint } from "eslint"; +import { promises as fs } from "fs"; +import * as path from "path"; + +export async function writeGenerated(filePath: string, contents: string): Promise { + const header = `// 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. + `; + await fs.mkdir(path.dirname(filePath), { recursive: true }); + const eslint = new ESLint({ + cwd: process.cwd(), + fix: true, + }); + const lintResult = await eslint.lintText(header + contents, { + filePath, + }); + + const output = lintResult[0] && lintResult[0].output ? lintResult[0].output : lintResult[0].source; + await fs.writeFile(filePath, output ?? contents); + // eslint-disable-next-line no-console + console.log(`generated ${filePath}`); +} diff --git a/packages/admin/cms-admin/src/generator/generate.ts b/packages/admin/cms-admin/src/generator/generate.ts index 00bbe6e780..cff0a5f1f4 100644 --- a/packages/admin/cms-admin/src/generator/generate.ts +++ b/packages/admin/cms-admin/src/generator/generate.ts @@ -3,6 +3,7 @@ import { loadSchema } from "@graphql-tools/load"; import { Command } from "commander"; import { introspectionFromSchema, IntrospectionQuery } from "graphql"; +import { runFutureGenerate } from "./future/generator"; import { writeCrudForm } from "./generateForm"; import { writeCrudGrid } from "./generateGrid"; import { writeCrudPage } from "./generatePage"; @@ -14,19 +15,25 @@ async function writeCrud(options: CrudGeneratorConfig, schema: IntrospectionQuer await writeCrudPage(options, schema); } -const generate = new Command("generate").argument("").action(async (configFile: string) => { - const schema = await loadSchema("./schema.gql", { - loaders: [new GraphQLFileLoader()], - }); - const introspection = introspectionFromSchema(schema); - const configs: CrudGeneratorConfig[] = (await import(configFile)).default; - for (const config of configs) { - await writeCrud(config, introspection); - } -}); - const program = new Command(); -program.addCommand(generate); +program.addCommand( + new Command("generate").argument("").action(async (configFile: string) => { + const schema = await loadSchema("./schema.gql", { + loaders: [new GraphQLFileLoader()], + }); + const introspection = introspectionFromSchema(schema); + const configs: CrudGeneratorConfig[] = (await import(configFile)).default; + for (const config of configs) { + await writeCrud(config, introspection); + } + }), +); + +program.addCommand( + new Command("future-generate").action(async () => { + await runFutureGenerate(); + }), +); program.parse(); diff --git a/packages/admin/cms-admin/src/index.ts b/packages/admin/cms-admin/src/index.ts index 4fe3a22358..90b5738b02 100644 --- a/packages/admin/cms-admin/src/index.ts +++ b/packages/admin/cms-admin/src/index.ts @@ -67,6 +67,13 @@ export { queryUpdatedAt } from "./form/queryUpdatedAt"; export { serializeInitialValues } from "./form/serializeInitialValues"; export { SyncFields } from "./form/SyncFields"; export { useFormSaveConflict } from "./form/useFormSaveConflict"; +export type { + FormConfig as future_FormConfig, + FormFieldConfig as future_FormFieldConfig, + GeneratorConfig as future_GeneratorConfig, + GridColumnConfig as future_GridColumnConfig, + GridConfig as future_GridConfig, +} from "./generator/future/generator"; export { CrudGeneratorConfig } from "./generator/types"; export { createHttpClient } from "./http/createHttpClient"; export { LocaleProvider } from "./locale/LocaleProvider"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c06eed0649..3ddc4c9b0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -826,25 +826,25 @@ importers: version: 3.7.4(graphql@15.8.0)(react-dom@17.0.2)(react@17.0.2) '@comet/admin': specifier: '*' - version: link:../packages/admin/admin + version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) '@comet/admin-color-picker': specifier: '*' - version: link:../packages/admin/admin-color-picker + version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) '@comet/admin-date-time': specifier: '*' - version: link:../packages/admin/admin-date-time + version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) '@comet/admin-icons': specifier: '*' - version: link:../packages/admin/admin-icons + version: 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) '@comet/admin-react-select': specifier: '*' - version: link:../packages/admin/admin-react-select + version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) '@comet/admin-rte': specifier: '*' - version: link:../packages/admin/admin-rte + version: 6.0.0(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(final-form@4.20.9)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react@17.0.2) '@comet/admin-theme': specifier: '*' - version: link:../packages/admin/admin-theme + version: 6.0.0(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/system@5.11.5)(react-dom@17.0.2)(react@17.0.2) '@docusaurus/core': specifier: 2.4.1 version: 2.4.1(@docusaurus/types@2.4.1)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.4) @@ -2039,6 +2039,9 @@ importers: '@mui/lab': specifier: ^5.0.0-alpha.76 version: 5.0.0-alpha.117(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/material@5.11.6)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + change-case: + specifier: ^4.1.2 + version: 4.1.2 class-validator: specifier: 0.13.2 version: 0.13.2 @@ -2081,6 +2084,9 @@ importers: mime-db: specifier: ^1.0.0 version: 1.52.0 + object-path: + specifier: ^0.11.8 + version: 0.11.8 p-debounce: specifier: ^4.0.0 version: 4.0.0 @@ -2220,6 +2226,9 @@ importers: '@types/node': specifier: ^18.0.0 version: 18.15.3 + '@types/object-path': + specifier: ^0.11.4 + version: 0.11.4 '@types/pluralize': specifier: ^0.0.29 version: 0.0.29 @@ -2271,6 +2280,9 @@ importers: final-form: specifier: ^4.20.9 version: 4.20.9 + glob: + specifier: ^10.3.10 + version: 10.3.10 graphql: specifier: ^15.0.0 version: 15.8.0 @@ -7013,6 +7025,243 @@ packages: requiresBuild: true optional: true + /@comet/admin-color-picker@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): + resolution: {integrity: sha512-5gYTW4E2W0PLT1251TYtG8dUtx28pwEWE1O8tjlXGMge5e3YBz9MCrPE6Nn9EqfkJ89MJnSagCK2dLi8BdJ0CQ==} + peerDependencies: + '@mui/icons-material': ^5.0.0 + '@mui/material': ^5.0.0 + '@mui/styles': ^5.0.0 + react: ^17.0 + react-dom: ^17.0 + react-final-form: ^6.3.1 + react-intl: ^5.24.6 + dependencies: + '@comet/admin': 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) + '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) + '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) + '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) + clsx: 1.2.1 + react: 17.0.2 + react-colorful: 5.6.1(react-dom@17.0.2)(react@17.0.2) + react-dom: 17.0.2(react@17.0.2) + react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) + react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) + tinycolor2: 1.5.2 + use-debounce: 6.0.1(react@17.0.2) + transitivePeerDependencies: + - '@apollo/client' + - '@emotion/react' + - '@emotion/styled' + - '@mui/x-data-grid' + - '@mui/x-data-grid-premium' + - '@mui/x-data-grid-pro' + - '@types/react' + - final-form + - graphql + - history + - react-dnd + - react-router + - react-router-dom + dev: false + + /@comet/admin-date-time@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): + resolution: {integrity: sha512-fm2wfw0RNl+LiB+3DE491QDlcOddL052+Cjr80A6bzHNbhD6N3DpL3nsWbF5BYZL1cX66hJSFrBgmR13d3hxNQ==} + peerDependencies: + '@mui/material': ^5.0.0 + '@mui/styles': ^5.0.0 + react: ^17.0 + react-dom: ^17.0 + react-final-form: ^6.5.7 + react-intl: ^5.24.6 + dependencies: + '@comet/admin': 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) + '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) + '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) + '@mui/utils': 5.11.2(react@17.0.2) + clsx: 1.2.1 + date-fns: 2.29.3 + react: 17.0.2 + react-date-range: 1.4.0(date-fns@2.29.3)(react@17.0.2) + react-dom: 17.0.2(react@17.0.2) + react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) + react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) + transitivePeerDependencies: + - '@apollo/client' + - '@emotion/react' + - '@emotion/styled' + - '@mui/icons-material' + - '@mui/x-data-grid' + - '@mui/x-data-grid-premium' + - '@mui/x-data-grid-pro' + - '@types/react' + - final-form + - graphql + - history + - react-dnd + - react-router + - react-router-dom + dev: false + + /@comet/admin-icons@6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2): + resolution: {integrity: sha512-VNh+62H3dnZ5pvJEUX5MJ2zLir6fiTPXYkDl8yCmJ48Vm/YxagTljlCUbp+DcJYrM6PA/qMlheE6lnqdIG3+LA==} + peerDependencies: + '@mui/material': ^5.0.0 + react: ^17.0 + react-dom: ^17.0 + dependencies: + '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + dev: false + + /@comet/admin-react-select@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): + resolution: {integrity: sha512-36VWyrmorT6Sqvd5Fu7hocpFCWituEQRyruEexG2HdWq1Ts4B+yyheCul9lfcla3A+XMHPqmK9t7oyuUP/yG2Q==} + peerDependencies: + '@mui/icons-material': ^5.0.0 + '@mui/material': ^5.0.0 + '@mui/styles': ^5.0.0 + final-form: ^4.16.1 + react: ^17.0 + react-dom: ^17.0 + react-final-form: ^6.3.1 + react-select: ^3.0.4 + dependencies: + '@comet/admin': 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) + '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) + '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) + classnames: 2.3.2 + final-form: 4.20.9 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) + transitivePeerDependencies: + - '@apollo/client' + - '@emotion/react' + - '@emotion/styled' + - '@mui/x-data-grid' + - '@mui/x-data-grid-premium' + - '@mui/x-data-grid-pro' + - '@types/react' + - graphql + - history + - react-dnd + - react-intl + - react-router + - react-router-dom + dev: false + + /@comet/admin-rte@6.0.0(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(final-form@4.20.9)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react@17.0.2): + resolution: {integrity: sha512-9WddOXtYH71a4hmjyexZHurEauEV6Tr2BFmQjCaf77LUrHOuDbH9/FMicqyu10lrD61zvJRrxLxsEbpt6r7WnQ==} + peerDependencies: + '@mui/icons-material': ^5.0.0 + '@mui/material': ^5.0.0 + '@mui/styles': ^5.0.0 + draft-js: ^0.11.4 + final-form: ^4.16.1 + react: ^17.0 + react-dom: ^17.0 + react-final-form: ^6.3.1 + react-intl: ^5.10.0 + dependencies: + '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) + '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) + '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) + detect-browser: 5.3.0 + draftjs-conductor: 3.0.0(draft-js@0.11.7) + final-form: 4.20.9 + immutable: 3.7.6 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) + react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) + dev: false + + /@comet/admin-theme@6.0.0(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/system@5.11.5)(react-dom@17.0.2)(react@17.0.2): + resolution: {integrity: sha512-3KouYLXS7UY9UdALfC2NNX6V+GaLilaSdpLTRRFa4y7Ss+bgktW13t19AvP3D9A3H78+3zn3Y2BPn19o+xwZ+Q==} + peerDependencies: + '@mui/material': ^5.0.0 + '@mui/styles': ^5.0.0 + '@mui/system': ^5.0.0 + react: ^17.0 + dependencies: + '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) + '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) + '@mui/system': 5.11.5(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react@17.0.2) + '@mui/utils': 5.11.2(react@17.0.2) + react: 17.0.2 + transitivePeerDependencies: + - react-dom + dev: false + + /@comet/admin@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): + resolution: {integrity: sha512-sKqKw2KD9BgvCjctzg86O3V3T5U7ouUCCMPRRszW7kmR5WF+OMQGCpS8PolSu7WS+mTunLxMV9ZhbS08HQzcJQ==} + peerDependencies: + '@apollo/client': ^3.7.0 + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/icons-material': ^5.0.0 + '@mui/material': ^5.0.0 + '@mui/styles': ^5.0.0 + '@mui/x-data-grid': ^5.0.0 + '@mui/x-data-grid-premium': ^5.0.0 + '@mui/x-data-grid-pro': ^5.0.0 + final-form: ^4.16.1 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + history: ^4.10.1 + react: ^17.0 + react-dnd: ^16.0.0 + react-dom: ^17.0 + react-final-form: ^6.3.1 + react-intl: ^5.10.0 + react-router: ^5.1.2 + react-router-dom: ^5.1.2 + peerDependenciesMeta: + '@mui/x-data-grid-premium': + optional: true + '@mui/x-data-grid-pro': + optional: true + react-dnd: + optional: true + dependencies: + '@apollo/client': 3.7.4(graphql@15.8.0)(react-dom@17.0.2)(react@17.0.2) + '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) + '@emotion/react': 11.9.3(@babel/core@7.22.11)(@types/react@17.0.53)(react@17.0.2) + '@emotion/styled': 11.10.5(@babel/core@7.22.11)(@emotion/react@11.9.3)(@types/react@17.0.53)(react@17.0.2) + '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) + '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) + '@mui/private-theming': 5.11.2(@types/react@17.0.53)(react@17.0.2) + '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) + '@mui/x-data-grid': 5.17.20(@mui/material@5.11.6)(@mui/system@5.11.5)(react-dom@17.0.2)(react@17.0.2) + clsx: 1.2.1 + exceljs: 3.10.0 + file-saver: 2.0.5 + final-form: 4.20.9 + final-form-set-field-data: 1.0.2(final-form@4.20.9) + graphql: 15.8.0 + history: 4.10.1 + http-status-codes: 2.3.0 + is-mobile: 4.0.0 + lodash.debounce: 4.0.8 + lodash.isequal: 4.5.0 + query-string: 6.14.1 + react: 17.0.2 + react-dnd: 16.0.1(@types/node@18.15.3)(@types/react@17.0.53)(react@17.0.2) + react-dom: 17.0.2(react@17.0.2) + react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) + react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) + react-router: 5.3.4(react@17.0.2) + react-router-dom: 5.3.4(react@17.0.2) + use-constant: 1.1.1(react@17.0.2) + uuid: 9.0.0 + transitivePeerDependencies: + - '@types/react' + dev: false + /@comet/dev-process-manager@2.3.2: resolution: {integrity: sha512-SOP1H8rZBpNhgRzFMofiZPtXYzU16s/uD4ME3J7IXPtqsHNkSjm+WD1LzpK1czVqWAGgowZsXbjL46cYPt41oA==} engines: {node: '>=14'} @@ -8506,7 +8755,7 @@ packages: '@graphql-tools/code-file-loader': 7.3.16(@babel/core@7.20.12)(graphql@15.8.0) '@graphql-tools/git-loader': 7.2.16(@babel/core@7.20.12)(graphql@15.8.0) '@graphql-tools/github-loader': 7.3.23(@babel/core@7.20.12)(graphql@15.8.0) - '@graphql-tools/graphql-file-loader': 7.5.14(graphql@15.8.0) + '@graphql-tools/graphql-file-loader': 7.5.17(graphql@15.8.0) '@graphql-tools/json-file-loader': 7.4.15(graphql@15.8.0) '@graphql-tools/load': 7.8.0(graphql@15.8.0) '@graphql-tools/prisma-loader': 7.2.54(@types/node@18.15.3)(graphql@15.8.0) @@ -8937,25 +9186,12 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 dependencies: - '@graphql-tools/import': 6.7.15(graphql@15.8.0) + '@graphql-tools/import': 6.7.18(graphql@15.8.0) '@graphql-tools/utils': 7.10.0(graphql@15.8.0) graphql: 15.8.0 tslib: 2.1.0 dev: true - /@graphql-tools/graphql-file-loader@7.5.14(graphql@15.8.0): - resolution: {integrity: sha512-JGer4g57kq4wtsvqv8uZsT4ZG1lLsz1x5yHDfSj2OxyiWw2f1jFkzgby7Ut3H2sseJiQzeeDYZcbm06qgR32pg==} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - dependencies: - '@graphql-tools/import': 6.7.15(graphql@15.8.0) - '@graphql-tools/utils': 9.1.4(graphql@15.8.0) - globby: 11.1.0 - graphql: 15.8.0 - tslib: 2.4.1 - unixify: 1.0.0 - dev: true - /@graphql-tools/graphql-file-loader@7.5.17(graphql@15.8.0): resolution: {integrity: sha512-hVwwxPf41zOYgm4gdaZILCYnKB9Zap7Ys9OhY1hbwuAuC4MMNY9GpUjoTU3CQc3zUiPoYStyRtUGkHSJZ3HxBw==} peerDependencies: @@ -8985,17 +9221,6 @@ packages: - supports-color dev: true - /@graphql-tools/import@6.7.15(graphql@15.8.0): - resolution: {integrity: sha512-WNhvauAt2I2iUg+JdQK5oQebKLXqUZWe8naP13K1jOkbTQT7hK3P/4I9AaVmzt0KXRJW5Uow3RgdHZ7eUBKVsA==} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - dependencies: - '@graphql-tools/utils': 9.1.4(graphql@15.8.0) - graphql: 15.8.0 - resolve-from: 5.0.0 - tslib: 2.4.1 - dev: true - /@graphql-tools/import@6.7.18(graphql@15.8.0): resolution: {integrity: sha512-XQDdyZTp+FYmT7as3xRWH/x8dx0QZA2WZqfMF5EWb36a0PiH7WwlRQYIdyYXj8YCLpiWkeBXgBRHmMnwEYR8iQ==} peerDependencies: @@ -9530,6 +9755,18 @@ packages: resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.0.1 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@istanbuljs/load-nyc-config@1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -11849,6 +12086,13 @@ packages: regexpu-core: 4.8.0 dev: false + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + /@pkgr/utils@2.3.1: resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -14163,6 +14407,10 @@ packages: resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==} dev: false + /@types/object-path@0.11.4: + resolution: {integrity: sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw==} + dev: true + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} @@ -17051,7 +17299,6 @@ packages: no-case: 3.0.4 tslib: 2.4.1 upper-case-first: 2.0.2 - dev: true /capture-exit@2.0.0: resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} @@ -17146,7 +17393,6 @@ packages: sentence-case: 3.0.4 snake-case: 3.0.4 tslib: 2.4.1 - dev: true /change-case@5.2.0: resolution: {integrity: sha512-L6VzznESnMIKKdKhVzCG+KPz4+x1FWbjOs1AdhoHStV3qo8aySMRGPUoqC0aL1ThKaQNGhAu6ZfHL/QAyQRuiw==} @@ -17678,7 +17924,6 @@ packages: no-case: 3.0.4 tslib: 2.4.1 upper-case: 2.0.2 - dev: true /constants-browserify@1.0.0: resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} @@ -20547,6 +20792,14 @@ packages: signal-exit: 3.0.7 dev: false + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + /forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} @@ -21053,6 +21306,18 @@ packages: /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + /glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} dependencies: @@ -21652,7 +21917,6 @@ packages: dependencies: capital-case: 1.0.4 tslib: 2.4.1 - dev: true /headers-utils@3.0.2: resolution: {integrity: sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ==} @@ -22870,6 +23134,15 @@ packages: iterate-iterator: 1.0.2 dev: false + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + /jaeger-client@3.19.0: resolution: {integrity: sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw==} engines: {node: '>=10'} @@ -24297,6 +24570,11 @@ packages: highlight.js: 10.7.3 dev: false + /lru-cache@10.1.0: + resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + engines: {node: 14 || >=16.14} + dev: true + /lru-cache@4.0.2: resolution: {integrity: sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==} dependencies: @@ -24756,6 +25034,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -24800,6 +25085,11 @@ packages: dependencies: yallist: 4.0.0 + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -25371,6 +25661,11 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + /object-path@0.11.8: + resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} + engines: {node: '>= 10.12.0'} + dev: false + /object-visit@1.0.1: resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} engines: {node: '>=0.10.0'} @@ -25895,7 +26190,6 @@ packages: dependencies: dot-case: 3.0.4 tslib: 2.4.1 - dev: true /path-dirname@1.0.2: resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} @@ -25950,6 +26244,14 @@ packages: path-root-regex: 0.1.2 dev: true + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.1.0 + minipass: 7.0.4 + dev: true + /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -28602,7 +28904,6 @@ packages: no-case: 3.0.4 tslib: 2.4.1 upper-case-first: 2.0.2 - dev: true /serialize-javascript@4.0.0: resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} @@ -28776,6 +29077,11 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /signedsource@1.0.0: resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} dev: true @@ -28889,7 +29195,6 @@ packages: dependencies: dot-case: 3.0.4 tslib: 2.4.1 - dev: true /snapdragon-node@2.1.1: resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} @@ -30849,13 +31154,11 @@ packages: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} dependencies: tslib: 2.4.1 - dev: true /upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} dependencies: tslib: 2.4.1 - dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -31815,7 +32118,6 @@ packages: ansi-styles: 6.2.1 string-width: 5.1.2 strip-ansi: 7.0.1 - dev: false /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}