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 (
+
+
+
+
+
+
+
+
+
+
+ } component={StackLink} pageName="add" payload="add" variant="contained" color="primary">
+
+
+
+
+ );
+}
+
+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 ``;
+ })
+ .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
+ ? `
+ } component={StackLink} pageName="add" payload="add" variant="contained" color="primary">
+
+
+ `
+ : ""
+ }
+
+ );
+ }
+
+
+ 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==}