From 516b8c16756a448071d800ae18228eec919f1d39 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 13 Feb 2024 16:09:18 +0100 Subject: [PATCH 01/13] Supported nested fields --- .../products/future/ProductForm.cometGen.ts | 3 + .../products/future/ProductsGrid.cometGen.ts | 2 + .../future/generated/ProductForm.gql.tsx | 5 ++ .../products/future/generated/ProductForm.tsx | 42 +++++++++++- .../future/generated/ProductsGrid.tsx | 14 ++++ packages/admin/cms-admin/package.json | 2 + .../src/generator/future/generateForm.ts | 58 ++++------------- .../src/generator/future/generateFormField.ts | 25 +++---- .../src/generator/future/generateGrid.ts | 5 +- .../src/generator/future/generator.ts | 32 ++++++--- .../utils/convertObjectToStructuredString.ts | 15 +++++ .../src/generator/future/utils/deepKeyOf.ts | 18 +++++ .../generator/future/utils/findRootBlocks.ts | 3 +- .../future/utils/generateFieldList.ts | 65 +++++++++++++++++++ .../utils/generateFormValuesTypeDefinition.ts | 54 +++++++++++++++ .../utils/generateInitialValuesValue.ts | 54 +++++++++++++++ .../future/utils/generateOutputObject.ts | 35 ++++++++++ pnpm-lock.yaml | 15 +++++ 18 files changed, 376 insertions(+), 71 deletions(-) create mode 100644 packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts create mode 100644 packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts create mode 100644 packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts create mode 100644 packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts create mode 100644 packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts create mode 100644 packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts diff --git a/demo/admin/src/products/future/ProductForm.cometGen.ts b/demo/admin/src/products/future/ProductForm.cometGen.ts index 621fdc406c..4bba88220e 100644 --- a/demo/admin/src/products/future/ProductForm.cometGen.ts +++ b/demo/admin/src/products/future/ProductForm.cometGen.ts @@ -12,6 +12,9 @@ export const ProductForm: FormConfig = { label: "Titel", // default is generated from name (camelCaseToHumanReadable) required: true, // default is inferred from gql schema }, + { type: "number", name: "packageDimensions.height", label: "Height" }, + { type: "number", name: "packageDimensions.width", label: "Width" }, + { type: "number", name: "packageDimensions.depth", label: "Depth" }, { type: "text", name: "slug" }, { type: "text", name: "description", label: "Description", multiline: true }, { type: "staticSelect", name: "type", label: "Type" /*, values: from gql schema (TODO overridable)*/ }, diff --git a/demo/admin/src/products/future/ProductsGrid.cometGen.ts b/demo/admin/src/products/future/ProductsGrid.cometGen.ts index a4375d4b3d..e4962a39d5 100644 --- a/demo/admin/src/products/future/ProductsGrid.cometGen.ts +++ b/demo/admin/src/products/future/ProductsGrid.cometGen.ts @@ -9,6 +9,8 @@ export const ProductsGrid: GridConfig = { { 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: "number", name: "packageDimensions.height", headerName: "Height", width: 50 }, + { type: "number", name: "packageDimensions.width", headerName: "Width", width: 50 }, { 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/generated/ProductForm.gql.tsx b/demo/admin/src/products/future/generated/ProductForm.gql.tsx index 82c222c320..4fca1444b0 100644 --- a/demo/admin/src/products/future/generated/ProductForm.gql.tsx +++ b/demo/admin/src/products/future/generated/ProductForm.gql.tsx @@ -5,6 +5,11 @@ import { gql } from "@apollo/client"; export const productFormFragment = gql` fragment ProductFormDetails on Product { title + packageDimensions { + height + width + depth + } slug description type diff --git a/demo/admin/src/products/future/generated/ProductForm.tsx b/demo/admin/src/products/future/generated/ProductForm.tsx index 47bdbb2e10..0afdef1cd4 100644 --- a/demo/admin/src/products/future/generated/ProductForm.tsx +++ b/demo/admin/src/products/future/generated/ProductForm.tsx @@ -46,7 +46,8 @@ const rootBlocks = { image: DamImageBlock, }; -type FormValues = Omit & { +type FormValues = Omit & { + packageDimensions: { height: string; width: string; depth: string }; price: string; image: BlockState; }; @@ -72,6 +73,13 @@ export function ProductForm({ id }: FormProps): React.ReactElement { data?.product ? { ...filter(productFormFragment, data.product), + packageDimensions: data.product.packageDimensions + ? { + height: String(data.product.packageDimensions.height), + width: String(data.product.packageDimensions.width), + depth: String(data.product.packageDimensions.depth), + } + : undefined, price: String(data.product.price), image: rootBlocks.image.input2State(data.product.image), } @@ -97,6 +105,11 @@ export function ProductForm({ id }: FormProps): React.ReactElement { if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); const output = { ...formValues, + packageDimensions: { + height: parseFloat(formValues.packageDimensions.height), + width: parseFloat(formValues.packageDimensions.width), + depth: parseFloat(formValues.packageDimensions.depth), + }, price: parseFloat(formValues.price), image: rootBlocks.image.state2Output(formValues.image), }; @@ -167,6 +180,33 @@ export function ProductForm({ id }: FormProps): React.ReactElement { label={} /> + } + /> + + } + /> + + } + /> + , + config: FormConfigInternal, ): GeneratorReturn { const gqlType = config.gqlType; const title = config.title ?? camelCaseToHumanReadable(gqlType); @@ -22,17 +25,15 @@ export function generateForm( const gqlDocuments: Record = {}; const imports: Imports = []; + const fieldNamesFromConfig: string[] = 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} { - ${config.fields.map((field) => field.name).join("\n")} - } + fragment ${fragmentName} on ${gqlType} { ${fieldList} } `; gqlDocuments[`${instanceGqlType}Query`] = ` @@ -138,20 +139,7 @@ export function generateForm( : "" } - 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")} - }` - : "" - }; + type FormValues = ${generateFormValuesTypeDefinition({ fragmentName, rootBlocks, config })}; interface FormProps { id?: string; @@ -169,21 +157,7 @@ export function generateForm( 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 initialValues = ${generateInitialValuesValue({ config, fragmentName, rootBlocks, instanceGqlType })}; const saveConflict = useFormSaveConflict({ checkConflict: async () => { @@ -198,13 +172,7 @@ export function generateForm( 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")} - }; + const output = ${generateOutputObject({ rootBlocks, config })}; if (mode === "edit") { if (!id) throw new Error(); await client.mutate({ diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField.ts b/packages/admin/cms-admin/src/generator/future/generateFormField.ts index f0c3c5a4f6..c22accb1dc 100644 --- a/packages/admin/cms-admin/src/generator/future/generateFormField.ts +++ b/packages/admin/cms-admin/src/generator/future/generateFormField.ts @@ -1,15 +1,14 @@ -import { IntrospectionEnumType, IntrospectionNamedTypeRef, IntrospectionObjectType, IntrospectionQuery } from "graphql"; +import { IntrospectionEnumType, IntrospectionNamedTypeRef, IntrospectionQuery } from "graphql"; -import { FormConfig, FormFieldConfig, GeneratorReturn } from "./generator"; +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 }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: FormFieldConfig, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - formConfig: FormConfig, + config: FormFieldConfigInternal, + formConfig: FormConfigInternal, ): GeneratorReturn & { imports: Imports } { const gqlType = formConfig.gqlType; const instanceGqlType = gqlType[0].toLowerCase() + gqlType.substring(1); @@ -17,13 +16,15 @@ export function generateFormField( const name = String(config.name); const label = config.label ?? camelCaseToHumanReadable(name); - const introspectionObject = gqlIntrospection.__schema.types.find((type) => type.kind === "OBJECT" && type.name === gqlType) as - | IntrospectionObjectType - | undefined; - if (!introspectionObject) throw new Error(`didn't find object ${gqlType} in gql introspection`); + 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 introspectionField = introspectionObject.fields.find((field) => field.name === name); - if (!introspectionField) throw new Error(`didn't find field ${name} in gql introspection type ${gqlType}`); + 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"; diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index 88aec5e413..0150e31bc9 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -8,7 +8,7 @@ import { } from "graphql"; import { plural } from "pluralize"; -import { GeneratorReturn, GridConfig } from "./generator"; +import { GeneratorReturn, GridConfigInternal } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; import { findRootBlocks } from "./utils/findRootBlocks"; @@ -62,8 +62,7 @@ export function generateGrid( targetDirectory, gqlIntrospection, }: { exportName: string; baseOutputFilename: string; targetDirectory: string; gqlIntrospection: IntrospectionQuery }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: GridConfig, + config: GridConfigInternal, ): GeneratorReturn { const gqlType = config.gqlType; const gqlTypePlural = plural(gqlType); diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index 31a0a6bec7..c12ca508f7 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -6,6 +6,7 @@ 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 = { @@ -13,7 +14,9 @@ type BlockReference = { import: string; }; -export type FormFieldConfig = ( +export type GeneratorEntity = { __typename?: string }; + +export type FormFieldConfigInternal = ( | { type: "text"; multiline?: boolean } | { type: "number" } | { type: "boolean" } @@ -22,19 +25,24 @@ export type FormFieldConfig = ( | { type: "staticSelect"; values?: string[] } | { type: "asyncSelect"; values?: string[] } | { type: "block"; block: BlockReference } -) & { name: keyof T; label?: string; required?: boolean }; +) & { name: string; label?: string; required?: boolean }; +export type FormFieldConfig = FormFieldConfigInternal & { name: Leaves | Paths }; -export type FormConfig = { +export type FormConfigInternal = { type: "form"; - gqlType: T["__typename"]; + gqlType: string; fragmentName?: string; - fields: FormFieldConfig[]; title?: string; + fields: FormFieldConfigInternal[]; +}; +export type FormConfig = FormConfigInternal & { + gqlType: T["__typename"]; + fields: FormFieldConfig[]; }; export type TabsConfig = { type: "tabs"; tabs: { name: string; content: GeneratorConfig }[] }; -export type GridColumnConfig = ( +export type GridColumnConfigInternal = ( | { type: "text" } | { type: "number" } | { type: "boolean" } @@ -42,11 +50,17 @@ export type GridColumnConfig = ( | { type: "dateTime" } | { type: "staticSelect"; values?: string[] } | { type: "block"; block: BlockReference } -) & { name: keyof T; headerName?: string; width?: number }; -export type GridConfig = { +) & { name: string; headerName?: string; width?: number }; +export type GridColumnConfig = GridColumnConfigInternal & { name: Leaves | Paths }; + +export type GridConfigInternal = { type: "grid"; - gqlType: T["__typename"]; + gqlType: string; fragmentName?: string; + columns: GridColumnConfigInternal[]; +}; +export type GridConfig = GridConfigInternal & { + gqlType: T["__typename"]; columns: GridColumnConfig[]; }; diff --git a/packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts b/packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts new file mode 100644 index 0000000000..2f2b6bf1a7 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts @@ -0,0 +1,15 @@ +export type FieldsObjectType = { [key: string]: FieldsObjectType | string }; +export function convertObjectToStructuredString(obj: FieldsObjectType) { + let ret = ""; + let prefixField = ""; + for (const key in obj) { + const valueForKey = obj[key]; + if (typeof valueForKey === "string") { + ret += `${prefixField}${key}${valueForKey}`; + } else { + ret += `${prefixField}${key}: { ${convertObjectToStructuredString(valueForKey)} }`; + } + prefixField = " "; + } + return ret; +} 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 index f887d50bfb..7865d7a49e 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/findRootBlocks.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/findRootBlocks.ts @@ -13,8 +13,9 @@ const fallbackLibraryBlocks: { [key: string]: string } = { YouTubeVideoBlock: "@comet/blocks-admin", }; +export type RootBlocks = Record; export function findRootBlocks({ gqlType, targetDirectory }: { gqlType: string; targetDirectory: string }, schema: IntrospectionQuery) { - const ret: Record = {}; + const ret: RootBlocks = {}; 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"); 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..ee283aca92 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts @@ -0,0 +1,65 @@ +import { IntrospectionField, IntrospectionQuery, IntrospectionType } from "graphql"; +import objectPath from "object-path"; + +type FieldsObjectType = { [key: string]: FieldsObjectType | boolean | string }; +const recursiveStringify = (obj: FieldsObjectType, objectSuffix?: string): string => { + let ret = ""; + let prefixField = ""; + for (const key in obj) { + const valueForKey = obj[key]; + if (typeof valueForKey === "boolean") { + ret += `${prefixField}${key}`; + } else if (typeof valueForKey === "string") { + ret += `${prefixField}${key}${valueForKey}`; + } else { + ret += `${prefixField}${key}${objectSuffix ? objectSuffix : ""} { ${recursiveStringify(valueForKey)} }`; + } + prefixField = " "; + } + return ret; +}; + +export function getRootProps(fields: string[]): string[] { + const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { + objectPath.set(acc, field, true); + return acc; + }, {}); + return Object.keys(fieldsObject); +} + +export function generateFieldListGqlString(fields: string[]) { + const fieldsObject: FieldsObjectType = fields.reduce((acc, fieldName) => { + objectPath.set(acc, fieldName, true); + return acc; + }, {}); + 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/generateFormValuesTypeDefinition.ts b/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts new file mode 100644 index 0000000000..4bbd513d79 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts @@ -0,0 +1,54 @@ +import objectPath from "object-path"; + +import { FormConfigInternal, FormFieldConfigInternal } from "../generator"; +import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; +import { RootBlocks } from "./findRootBlocks"; +import { getRootProps } from "./generateFieldList"; + +export function generateFormValuesTypeDefinition({ + config, + rootBlocks, + fragmentName, +}: { + config: FormConfigInternal; + rootBlocks: RootBlocks; + fragmentName: string; +}) { + const numberFields = config.fields.filter((field) => field.type == "number"); + const rootPropsContainingNumberField = getRootProps(numberFields.map((field) => field.name)); + return `${ + rootPropsContainingNumberField.length > 0 + ? `Omit `"${String(rootProp)}"`).join(" | ")}>` + : `GQL${fragmentName}Fragment` + } ${ + rootPropsContainingNumberField.length > 0 || Object.keys(rootBlocks).length > 0 + ? `& { + ${rootPropsContainingNumberField.map((rootProp) => generateFieldTypesStructureForRootProp(rootProp, config.fields)).join("\n")} + ${Object.keys(rootBlocks) + .map((rootBlockKey) => `${rootBlockKey}: BlockState;`) + .join("\n")} + }` + : "" + }`; +} + +function generateFieldTypesStructureForRootProp(rootProp: string, fields: FormFieldConfigInternal[]) { + const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { + if (field.name.includes(rootProp)) { + if (field.type === "number") { + objectPath.set(acc, field.name, `: string;`); + } else if (field.type === "text" || field.type === "asyncSelect" || field.type === "staticSelect") { + objectPath.set(acc, field.name, `: string;`); + } else if (field.type === "date") { + objectPath.set(acc, field.name, `: Date;`); + } else if (field.type === "boolean") { + objectPath.set(acc, field.name, `: boolean;`); + } else if (field.type === "block") { + // TODO how does this type look like? Is this correct? + objectPath.set(acc, field.name, `: BlockState`); + } + } + return acc; + }, {}); + return convertObjectToStructuredString(fieldsObject); +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts b/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts new file mode 100644 index 0000000000..ace68fe78c --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts @@ -0,0 +1,54 @@ +import objectPath from "object-path"; + +import { FormConfigInternal } from "../generator"; +import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; +import { RootBlocks } from "./findRootBlocks"; +import { getRootProps } from "./generateFieldList"; + +export function generateInitialValuesValue({ + instanceGqlType, + fragmentName, + config, + rootBlocks, +}: { + instanceGqlType: string; + fragmentName: string; + config: FormConfigInternal; + rootBlocks: RootBlocks; +}) { + const booleanFields = config.fields.filter((field) => field.type == "boolean"); + const numberFields = config.fields.filter((field) => field.type == "number" && !field.name.includes(".")); + const nestedNumberFields = config.fields.filter((field) => field.type === "number" && field.name.includes(".")); + const rootPropsContainingNumberField = getRootProps(nestedNumberFields.map((field) => field.name)); + return `React.useMemo>(() => data?.${instanceGqlType} + ? { + ...filter(${instanceGqlType}FormFragment, data.${instanceGqlType}), + ${rootPropsContainingNumberField + .map((rootProp) => { + const fieldsObject: FieldsObjectType = config.fields.reduce((acc, field) => { + if (field.name.includes(rootProp)) { + const nameWithoutPrefix = field.name.substring(rootProp.length + 1); + if (field.type === "number") { + objectPath.set(acc, nameWithoutPrefix, `: String(data.${instanceGqlType}.${field.name}),`); + } else { + objectPath.set(acc, nameWithoutPrefix, `: data.${instanceGqlType}.${field.name},`); + } + } + return acc; + }, {}); + return `${rootProp}: data.${instanceGqlType}.${rootProp} ? { ${convertObjectToStructuredString(fieldsObject)} } : undefined,`; + }) + .join("\n")} + ${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])`; +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts new file mode 100644 index 0000000000..6790d66a9e --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts @@ -0,0 +1,35 @@ +import objectPath from "object-path"; + +import { FormConfigInternal } from "../generator"; +import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; +import { RootBlocks } from "./findRootBlocks"; +import { getRootProps } from "./generateFieldList"; + +export function generateOutputObject({ config, rootBlocks }: { config: FormConfigInternal; rootBlocks: RootBlocks }) { + const numberFields = config.fields.filter((field) => field.type == "number" && !field.name.includes(".")); + const nestedNumberFields = config.fields.filter((field) => field.type === "number" && field.name.includes(".")); + const rootPropsContainingNumberField = getRootProps(nestedNumberFields.map((field) => field.name)); + return `{ + ...formValues, + ${rootPropsContainingNumberField + .map((rootProp) => { + const fieldsObject: FieldsObjectType = config.fields.reduce((acc, field) => { + if (field.name.includes(rootProp)) { + const nameWithoutPrefix = field.name.substring(rootProp.length + 1); + if (field.type === "number") { + objectPath.set(acc, nameWithoutPrefix, `: parseFloat(formValues.${field.name}),`); + } else { + objectPath.set(acc, nameWithoutPrefix, `: formValues.${field.name},`); + } + } + return acc; + }, {}); + return `${rootProp}: { ${convertObjectToStructuredString(fieldsObject)} },`; + }) + .join("\n")} + ${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")} + }`; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8965af23b..590486ea54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1834,6 +1834,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 @@ -1973,6 +1976,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 @@ -14404,6 +14410,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==} @@ -25642,6 +25652,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'} From 921286ff573892ab37e9a9242fdf5cd330a89e25 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Thu, 15 Feb 2024 14:46:13 +0100 Subject: [PATCH 02/13] Support non_null nested --- .../src/generator/future/utils/generateFieldList.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts index ee283aca92..b2d72bb0b7 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts @@ -45,8 +45,12 @@ function fieldListFromIntrospectionTypeRecursive( 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); + let outputType = field.type; + if (outputType.kind === "NON_NULL") { + outputType = outputType.ofType; + } + if (outputType.kind === "OBJECT") { + const subFields = fieldListFromIntrospectionTypeRecursive(types, outputType.name, path); acc.push(...subFields); } else { acc.push({ From 98cef80265e3a06aa366bd93aae242bf457fe370 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 16 Feb 2024 09:48:09 +0100 Subject: [PATCH 03/13] Remove unused import --- packages/admin/cms-admin/src/generator/future/generateForm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts index 1862943d08..60ec237414 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -4,7 +4,7 @@ import { generateFormField } from "./generateFormField"; import { FormConfigInternal, GeneratorReturn } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; import { findRootBlocks } from "./utils/findRootBlocks"; -import { generateFieldListGqlString, getRootProps } from "./utils/generateFieldList"; +import { generateFieldListGqlString } from "./utils/generateFieldList"; import { generateFormValuesTypeDefinition } from "./utils/generateFormValuesTypeDefinition"; import { generateImportsCode, Imports } from "./utils/generateImportsCode"; import { generateInitialValuesValue } from "./utils/generateInitialValuesValue"; From 357f7f50435e692a231552b5f2b146654b120907 Mon Sep 17 00:00:00 2001 From: Ben-Ho Date: Thu, 22 Feb 2024 10:52:56 +0100 Subject: [PATCH 04/13] Demo: Remove product package dimensions (#1730) - `packageDimensions` was nullable, although the database fields aren't nullable (caused an DB error if null was saved) - Admin Generator (present and future) doesn't support nested fields (for future it is WIP, present will never get it) - Easy solution: remove it - #1729 will showcase nested fields and everything (as replacement for packageDimensions) --- demo/admin/src/products/ProductForm.tsx | 1 - demo/admin/src/products/ProductsGrid.tsx | 6 ----- demo/api/schema.gql | 15 ----------- .../db/migrations/Migration20240222081515.ts | 10 +++++++ .../src/products/entities/product.entity.ts | 26 ------------------- .../products/generated/dto/product.input.ts | 8 +----- 6 files changed, 11 insertions(+), 55 deletions(-) create mode 100644 demo/api/src/db/migrations/Migration20240222081515.ts diff --git a/demo/admin/src/products/ProductForm.tsx b/demo/admin/src/products/ProductForm.tsx index cc38b67ecb..98120c6fd3 100644 --- a/demo/admin/src/products/ProductForm.tsx +++ b/demo/admin/src/products/ProductForm.tsx @@ -114,7 +114,6 @@ function ProductForm({ id }: FormProps): React.ReactElement { variants: [], articleNumbers: [], discounts: [], - packageDimensions: { width: 0, height: 0, depth: 0 }, statistics: { views: 0 }, }; if (mode === "edit") { diff --git a/demo/admin/src/products/ProductsGrid.tsx b/demo/admin/src/products/ProductsGrid.tsx index 75c760de09..8f60c712cd 100644 --- a/demo/admin/src/products/ProductsGrid.tsx +++ b/demo/admin/src/products/ProductsGrid.tsx @@ -165,7 +165,6 @@ function ProductsGrid() { })), articleNumbers: input.articleNumbers, discounts: input.discounts, - packageDimensions: input.packageDimensions, statistics: { views: 0 }, }, }, @@ -245,11 +244,6 @@ const productsFragment = gql` quantity price } - packageDimensions { - width - height - depth - } } `; diff --git a/demo/api/schema.gql b/demo/api/schema.gql index b12b44c023..90807fcb6c 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -452,12 +452,6 @@ type ProductDimensions { depth: Float! } -type ProductPackageDimensions { - width: Float! - height: Float! - depth: Float! -} - type Product implements DocumentInterface { id: ID! updatedAt: DateTime! @@ -474,7 +468,6 @@ type Product implements DocumentInterface { discounts: [ProductDiscounts!]! articleNumbers: [String!]! dimensions: ProductDimensions - packageDimensions: ProductPackageDimensions statistics: ProductStatistics createdAt: DateTime! category: ProductCategory @@ -596,12 +589,6 @@ input ProductDimensionsInput { depth: Float! } -input ProductPackageDimensionsInput { - width: Float! - height: Float! - depth: Float! -} - input RedirectScopeInput { domain: String! } @@ -1152,7 +1139,6 @@ input ProductInput { discounts: [ProductDiscountsInput!]! = [] articleNumbers: [String!]! = [] dimensions: ProductDimensionsInput - packageDimensions: ProductPackageDimensionsInput statistics: ProductStatisticsInput variants: [ProductVariantInput!]! = [] category: ID = null @@ -1180,7 +1166,6 @@ input ProductUpdateInput { discounts: [ProductDiscountsInput!] articleNumbers: [String!] dimensions: ProductDimensionsInput - packageDimensions: ProductPackageDimensionsInput statistics: ProductStatisticsInput variants: [ProductVariantInput!] category: ID diff --git a/demo/api/src/db/migrations/Migration20240222081515.ts b/demo/api/src/db/migrations/Migration20240222081515.ts new file mode 100644 index 0000000000..6b2d3cada0 --- /dev/null +++ b/demo/api/src/db/migrations/Migration20240222081515.ts @@ -0,0 +1,10 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240222081515 extends Migration { + + async up(): Promise { + this.addSql('alter table "Product" drop column "packageDimensions_width";'); + this.addSql('alter table "Product" drop column "packageDimensions_height";'); + this.addSql('alter table "Product" drop column "packageDimensions_depth";'); + } +} diff --git a/demo/api/src/products/entities/product.entity.ts b/demo/api/src/products/entities/product.entity.ts index 4936dcaff8..f615e15903 100644 --- a/demo/api/src/products/entities/product.entity.ts +++ b/demo/api/src/products/entities/product.entity.ts @@ -3,8 +3,6 @@ import { CrudField, CrudGenerator, DamImageBlock, DocumentInterface, RootBlockDa import { BaseEntity, Collection, - Embeddable, - Embedded, Entity, Enum, ManyToMany, @@ -55,26 +53,6 @@ export class ProductDimensions { depth: number; } -@Embeddable() -@ObjectType() -@InputType("ProductPackageDimensionsInput") -export class ProductPackageDimensions { - @Property({ type: types.integer }) - @Field() - @IsNumber() - width: number; - - @Property({ type: types.integer }) - @Field() - @IsNumber() - height: number; - - @Property({ type: types.integer }) - @Field() - @IsNumber() - depth: number; -} - @ObjectType({ implements: () => [DocumentInterface], }) @@ -151,10 +129,6 @@ export class Product extends BaseEntity implements DocumentInterf @Field(() => ProductDimensions, { nullable: true }) dimensions?: ProductDimensions = undefined; - @Embedded(() => ProductPackageDimensions, { nullable: true }) - @Field(() => ProductPackageDimensions, { nullable: true }) - packageDimensions?: ProductPackageDimensions = undefined; - @OneToOne(() => ProductStatistics, { inversedBy: "product", owner: true, ref: true, nullable: true }) @Field(() => ProductStatistics, { nullable: true }) statistics?: Ref = undefined; diff --git a/demo/api/src/products/generated/dto/product.input.ts b/demo/api/src/products/generated/dto/product.input.ts index 63f118a719..f8b5223125 100644 --- a/demo/api/src/products/generated/dto/product.input.ts +++ b/demo/api/src/products/generated/dto/product.input.ts @@ -6,7 +6,7 @@ import { Field, ID, InputType } from "@nestjs/graphql"; import { Transform, Type } from "class-transformer"; import { IsArray, IsBoolean, IsDate, IsEnum, IsNotEmpty, IsNumber, IsString, IsUUID, ValidateNested } from "class-validator"; -import { ProductDimensions, ProductDiscounts, ProductPackageDimensions } from "../../entities/product.entity"; +import { ProductDimensions, ProductDiscounts } from "../../entities/product.entity"; import { ProductType } from "../../entities/product-type.enum"; import { ProductStatisticsInput } from "./product-statistics.nested.input"; import { ProductVariantInput } from "./product-variant.nested.input"; @@ -74,12 +74,6 @@ export class ProductInput { @Field(() => ProductDimensions, { nullable: true }) dimensions?: ProductDimensions; - @IsNullable() - @ValidateNested() - @Type(() => ProductPackageDimensions) - @Field(() => ProductPackageDimensions, { nullable: true }) - packageDimensions?: ProductPackageDimensions; - @IsNullable() @Field(() => ProductStatisticsInput, { nullable: true }) @Type(() => ProductStatisticsInput) From 1cc85e69908c0364a183e6a53e2d42951519a05c Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 23 Feb 2024 12:35:19 +0100 Subject: [PATCH 05/13] Remove changes for grid-generator --- .../src/generator/future/generateGrid.ts | 5 +++-- .../cms-admin/src/generator/future/generator.ts | 16 +++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index 0150e31bc9..88aec5e413 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -8,7 +8,7 @@ import { } from "graphql"; import { plural } from "pluralize"; -import { GeneratorReturn, GridConfigInternal } from "./generator"; +import { GeneratorReturn, GridConfig } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; import { findRootBlocks } from "./utils/findRootBlocks"; @@ -62,7 +62,8 @@ export function generateGrid( targetDirectory, gqlIntrospection, }: { exportName: string; baseOutputFilename: string; targetDirectory: string; gqlIntrospection: IntrospectionQuery }, - config: GridConfigInternal, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: GridConfig, ): GeneratorReturn { const gqlType = config.gqlType; const gqlTypePlural = plural(gqlType); diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index a940505c5a..9c1d56f7ba 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -25,7 +25,7 @@ export type FormFieldConfigInternal = ( | { type: "staticSelect"; values?: string[] } | { type: "asyncSelect"; values?: string[] } | { type: "block"; block: ImportReference } -) & { name: string; label?: string; required?: boolean; validate?: ImportReference; helperText?: string }; +) & { name: string; label?: string; required?: boolean; validate?: ImportReference; helperText?: string }; export type FormFieldConfig = FormFieldConfigInternal & { name: Leaves | Paths }; export type FormConfigInternal = { @@ -42,7 +42,7 @@ export type FormConfig = FormConfigInternal & { export type TabsConfig = { type: "tabs"; tabs: { name: string; content: GeneratorConfig }[] }; -export type GridColumnConfigInternal = ( +export type GridColumnConfig = ( | { type: "text" } | { type: "number" } | { type: "boolean" } @@ -50,17 +50,11 @@ export type GridColumnConfigInternal = ( | { type: "dateTime" } | { type: "staticSelect"; values?: string[] } | { type: "block"; block: ImportReference } -) & { name: string; headerName?: string; width?: number }; -export type GridColumnConfig = GridColumnConfigInternal & { name: Leaves | Paths }; - -export type GridConfigInternal = { +) & { name: keyof T; headerName?: string; width?: number }; +export type GridConfig = { type: "grid"; - gqlType: string; - fragmentName?: string; - columns: GridColumnConfigInternal[]; -}; -export type GridConfig = GridConfigInternal & { gqlType: T["__typename"]; + fragmentName?: string; columns: GridColumnConfig[]; }; From cc2014d6c6414006a635292f241b4ec1552f3d3d Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 23 Feb 2024 12:39:49 +0100 Subject: [PATCH 06/13] Remove unused parameter --- .../cms-admin/src/generator/future/utils/generateFieldList.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts index b2d72bb0b7..77ac5527fb 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts @@ -2,7 +2,7 @@ import { IntrospectionField, IntrospectionQuery, IntrospectionType } from "graph import objectPath from "object-path"; type FieldsObjectType = { [key: string]: FieldsObjectType | boolean | string }; -const recursiveStringify = (obj: FieldsObjectType, objectSuffix?: string): string => { +const recursiveStringify = (obj: FieldsObjectType): string => { let ret = ""; let prefixField = ""; for (const key in obj) { @@ -12,7 +12,7 @@ const recursiveStringify = (obj: FieldsObjectType, objectSuffix?: string): strin } else if (typeof valueForKey === "string") { ret += `${prefixField}${key}${valueForKey}`; } else { - ret += `${prefixField}${key}${objectSuffix ? objectSuffix : ""} { ${recursiveStringify(valueForKey)} }`; + ret += `${prefixField}${key} { ${recursiveStringify(valueForKey)} }`; } prefixField = " "; } From b64d3848a3a251a643b1c1b13e42891fc5c7d2c3 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 27 Feb 2024 08:49:00 +0100 Subject: [PATCH 07/13] Use UsableFields-Type provided by @nsams to simplify type supporting writing form-config --- .../src/generator/future/generateForm.ts | 5 +++-- .../src/generator/future/generateFormField.ts | 8 +++++--- .../src/generator/future/generator.ts | 17 ++++++----------- .../src/generator/future/utils/deepKeyOf.ts | 18 ------------------ .../utils/generateFormValuesTypeDefinition.ts | 8 +++++--- .../future/utils/generateInitialValuesValue.ts | 5 +++-- .../future/utils/generateOutputObject.ts | 5 +++-- .../src/generator/future/utils/usableFields.ts | 11 +++++++++++ 8 files changed, 36 insertions(+), 41 deletions(-) delete mode 100644 packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts create mode 100644 packages/admin/cms-admin/src/generator/future/utils/usableFields.ts diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts index 448c502f69..faf6fd1951 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -1,7 +1,7 @@ import { IntrospectionQuery } from "graphql"; import { generateFormField } from "./generateFormField"; -import { FormConfigInternal, GeneratorReturn } from "./generator"; +import { FormConfig, GeneratorReturn } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; import { findRootBlocks } from "./utils/findRootBlocks"; import { generateFieldListGqlString } from "./utils/generateFieldList"; @@ -17,7 +17,8 @@ export function generateForm( targetDirectory, gqlIntrospection, }: { exportName: string; baseOutputFilename: string; targetDirectory: string; gqlIntrospection: IntrospectionQuery }, - config: FormConfigInternal, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: FormConfig, ): GeneratorReturn { const gqlType = config.gqlType; const title = config.title ?? camelCaseToHumanReadable(gqlType); diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField.ts b/packages/admin/cms-admin/src/generator/future/generateFormField.ts index e11e5e91bd..2739fd1a3c 100644 --- a/packages/admin/cms-admin/src/generator/future/generateFormField.ts +++ b/packages/admin/cms-admin/src/generator/future/generateFormField.ts @@ -1,14 +1,16 @@ import { IntrospectionEnumType, IntrospectionNamedTypeRef, IntrospectionQuery } from "graphql"; -import { FormConfigInternal, FormFieldConfigInternal, GeneratorReturn } from "./generator"; +import { FormConfig, FormFieldConfig, 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, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: FormFieldConfig, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + formConfig: FormConfig, ): GeneratorReturn & { imports: Imports } { const gqlType = formConfig.gqlType; const instanceGqlType = gqlType[0].toLowerCase() + gqlType.substring(1); diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index 9c1d56f7ba..4297566064 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -6,7 +6,7 @@ import { basename, dirname } from "path"; import { generateForm } from "./generateForm"; import { generateGrid } from "./generateGrid"; -import { Leaves, Paths } from "./utils/deepKeyOf"; +import { UsableFields } from "./utils/usableFields"; import { writeGenerated } from "./utils/writeGenerated"; type ImportReference = { @@ -16,7 +16,7 @@ type ImportReference = { export type GeneratorEntity = { __typename?: string }; -export type FormFieldConfigInternal = ( +export type FormFieldConfig = ( | { type: "text"; multiline?: boolean } | { type: "number" } | { type: "boolean" } @@ -25,19 +25,14 @@ export type FormFieldConfigInternal = ( | { type: "staticSelect"; values?: string[] } | { type: "asyncSelect"; values?: string[] } | { type: "block"; block: ImportReference } -) & { name: string; label?: string; required?: boolean; validate?: ImportReference; helperText?: string }; -export type FormFieldConfig = FormFieldConfigInternal & { name: Leaves | Paths }; +) & { name: UsableFields; label?: string; required?: boolean; validate?: ImportReference; helperText?: string }; -export type FormConfigInternal = { +export type FormConfig = { type: "form"; - gqlType: string; - fragmentName?: string; - title?: string; - fields: FormFieldConfigInternal[]; -}; -export type FormConfig = FormConfigInternal & { gqlType: T["__typename"]; + fragmentName?: string; fields: FormFieldConfig[]; + title?: string; }; export type TabsConfig = { type: "tabs"; tabs: { name: string; content: GeneratorConfig }[] }; diff --git a/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts b/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts deleted file mode 100644 index 9f65c5bff7..0000000000 --- a/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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/generateFormValuesTypeDefinition.ts b/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts index 4bbd513d79..6837cb7a73 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts @@ -1,6 +1,6 @@ import objectPath from "object-path"; -import { FormConfigInternal, FormFieldConfigInternal } from "../generator"; +import { FormConfig, FormFieldConfig } from "../generator"; import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; import { RootBlocks } from "./findRootBlocks"; import { getRootProps } from "./generateFieldList"; @@ -10,7 +10,8 @@ export function generateFormValuesTypeDefinition({ rootBlocks, fragmentName, }: { - config: FormConfigInternal; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: FormConfig; rootBlocks: RootBlocks; fragmentName: string; }) { @@ -32,7 +33,8 @@ export function generateFormValuesTypeDefinition({ }`; } -function generateFieldTypesStructureForRootProp(rootProp: string, fields: FormFieldConfigInternal[]) { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function generateFieldTypesStructureForRootProp(rootProp: string, fields: FormFieldConfig[]) { const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { if (field.name.includes(rootProp)) { if (field.type === "number") { diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts b/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts index ace68fe78c..1b7f5cced0 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts @@ -1,6 +1,6 @@ import objectPath from "object-path"; -import { FormConfigInternal } from "../generator"; +import { FormConfig } from "../generator"; import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; import { RootBlocks } from "./findRootBlocks"; import { getRootProps } from "./generateFieldList"; @@ -13,7 +13,8 @@ export function generateInitialValuesValue({ }: { instanceGqlType: string; fragmentName: string; - config: FormConfigInternal; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: FormConfig; rootBlocks: RootBlocks; }) { const booleanFields = config.fields.filter((field) => field.type == "boolean"); diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts index 6790d66a9e..25324e493f 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts @@ -1,11 +1,12 @@ import objectPath from "object-path"; -import { FormConfigInternal } from "../generator"; +import { FormConfig } from "../generator"; import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; import { RootBlocks } from "./findRootBlocks"; import { getRootProps } from "./generateFieldList"; -export function generateOutputObject({ config, rootBlocks }: { config: FormConfigInternal; rootBlocks: RootBlocks }) { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function generateOutputObject({ config, rootBlocks }: { config: FormConfig; rootBlocks: RootBlocks }) { const numberFields = config.fields.filter((field) => field.type == "number" && !field.name.includes(".")); const nestedNumberFields = config.fields.filter((field) => field.type === "number" && field.name.includes(".")); const rootPropsContainingNumberField = getRootProps(nestedNumberFields.map((field) => field.name)); diff --git a/packages/admin/cms-admin/src/generator/future/utils/usableFields.ts b/packages/admin/cms-admin/src/generator/future/utils/usableFields.ts new file mode 100644 index 0000000000..b6cfed09c1 --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/usableFields.ts @@ -0,0 +1,11 @@ +type GqlLeaves = "__typename" extends keyof T + ? { + [K in keyof T as K extends "__typename" ? never : K]-?: GqlLeaves; + } + : never; + +type FieldNames = { + [K in keyof T]: `${Exclude}${FieldNames extends never ? "" : `.${FieldNames}`}`; +}[keyof T]; + +export type UsableFields = FieldNames>; From 88166fadd45d7abcd144dacedfb55242689b1475 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Wed, 28 Feb 2024 14:38:18 +0100 Subject: [PATCH 08/13] Improve UsableFields-Type to support optional objects --- .../src/generator/future/utils/usableFields.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/admin/cms-admin/src/generator/future/utils/usableFields.ts b/packages/admin/cms-admin/src/generator/future/utils/usableFields.ts index b6cfed09c1..ed7fcc1e06 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/usableFields.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/usableFields.ts @@ -1,7 +1,10 @@ -type GqlLeaves = "__typename" extends keyof T - ? { - [K in keyof T as K extends "__typename" ? never : K]-?: GqlLeaves; - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type GqlLeaves = T extends any + ? "__typename" extends keyof T + ? { + [K in keyof T as K extends "__typename" ? never : K]-?: GqlLeaves; + } + : never : never; type FieldNames = { From d28aef23c0e4fa656a1c2b98910dc857690168d7 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 29 Mar 2024 08:23:32 +0100 Subject: [PATCH 09/13] Update generated code to match best-practice --- demo/admin/src/common/MasterMenu.tsx | 8 + .../future/ManufacturerForm.cometGen.ts | 26 ++ .../src/products/future/ManufacturersPage.tsx | 21 + .../future/generated/ManufacturerForm.gql.tsx | 62 +++ .../future/generated/ManufacturerForm.tsx | 398 ++++++++++++++++++ .../products/future/generated/ProductForm.tsx | 39 +- .../src/generator/future/generateForm.ts | 4 +- .../src/generator/future/generateFormField.ts | 5 - .../future/utils/generateFieldList.ts | 18 +- .../utils/generateFormValuesTypeDefinition.ts | 138 +++--- .../utils/generateInitialValuesValue.ts | 139 +++--- .../future/utils/generateOutputObject.ts | 113 +++-- 12 files changed, 794 insertions(+), 177 deletions(-) create mode 100644 demo/admin/src/products/future/ManufacturerForm.cometGen.ts create mode 100644 demo/admin/src/products/future/ManufacturersPage.tsx create mode 100644 demo/admin/src/products/future/generated/ManufacturerForm.gql.tsx create mode 100644 demo/admin/src/products/future/generated/ManufacturerForm.tsx diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx index c09df809c5..1de7ca4112 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 { ManufacturersPage as FutureManufacturersPage } from "@src/products/future/ManufacturersPage"; import { ProductsPage as FutureProductsPage } from "@src/products/future/ProductsPage"; import { ProductsPage } from "@src/products/generated/ProductsPage"; import ProductsHandmadePage from "@src/products/ProductsPage"; @@ -172,6 +173,13 @@ export const masterMenuData: MasterMenuData = [ component: FutureProductsPage, }, }, + { + primary: , + route: { + path: "/manufacturers-future", + component: FutureManufacturersPage, + }, + }, { primary: , route: { diff --git a/demo/admin/src/products/future/ManufacturerForm.cometGen.ts b/demo/admin/src/products/future/ManufacturerForm.cometGen.ts new file mode 100644 index 0000000000..b9b7fadd2e --- /dev/null +++ b/demo/admin/src/products/future/ManufacturerForm.cometGen.ts @@ -0,0 +1,26 @@ +import { future_FormConfig as FormConfig } from "@comet/cms-admin"; +import { GQLManufacturer } from "@src/graphql.generated"; + +export const ManufacturerForm: FormConfig = { + type: "form", + gqlType: "Manufacturer", + fragmentName: "ManufacturerFormDetails", + fields: [ + { type: "text", name: "address.street" }, + { type: "number", name: "address.streetNumber" }, + { type: "number", name: "address.zip" }, + { type: "text", name: "address.country" }, + { type: "text", name: "address.alternativeAddress.street" }, + { type: "number", name: "address.alternativeAddress.streetNumber" }, + { type: "number", name: "address.alternativeAddress.zip" }, + { type: "text", name: "address.alternativeAddress.country" }, + { type: "text", name: "addressAsEmbeddable.street" }, + { type: "number", name: "addressAsEmbeddable.streetNumber" }, + { type: "number", name: "addressAsEmbeddable.zip" }, + { type: "text", name: "addressAsEmbeddable.country" }, + { type: "text", name: "addressAsEmbeddable.alternativeAddress.street" }, + { type: "number", name: "addressAsEmbeddable.alternativeAddress.streetNumber" }, + { type: "number", name: "addressAsEmbeddable.alternativeAddress.zip" }, + { type: "text", name: "addressAsEmbeddable.alternativeAddress.country" }, + ], +}; diff --git a/demo/admin/src/products/future/ManufacturersPage.tsx b/demo/admin/src/products/future/ManufacturersPage.tsx new file mode 100644 index 0000000000..ef1abe4b80 --- /dev/null +++ b/demo/admin/src/products/future/ManufacturersPage.tsx @@ -0,0 +1,21 @@ +import { Stack, StackPage, StackSwitch } from "@comet/admin"; +import * as React from "react"; +import { useIntl } from "react-intl"; + +import { ManufacturerForm } from "./generated/ManufacturerForm"; + +export function ManufacturersPage(): React.ReactElement { + const intl = useIntl(); + return ( + + + + + + + {(selectedId) => } + + + + ); +} diff --git a/demo/admin/src/products/future/generated/ManufacturerForm.gql.tsx b/demo/admin/src/products/future/generated/ManufacturerForm.gql.tsx new file mode 100644 index 0000000000..15159c42aa --- /dev/null +++ b/demo/admin/src/products/future/generated/ManufacturerForm.gql.tsx @@ -0,0 +1,62 @@ +// 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 manufacturerFormFragment = gql` + fragment ManufacturerFormDetails on Manufacturer { + address { + street + streetNumber + zip + country + alternativeAddress { + street + streetNumber + zip + country + } + } + addressAsEmbeddable { + street + streetNumber + zip + country + alternativeAddress { + street + streetNumber + zip + country + } + } + } +`; +export const manufacturerQuery = gql` + query Manufacturer($id: ID!) { + manufacturer(id: $id) { + id + updatedAt + ...ManufacturerFormDetails + } + } + ${manufacturerFormFragment} +`; +export const createManufacturerMutation = gql` + mutation CreateManufacturer($input: ManufacturerInput!) { + createManufacturer(input: $input) { + id + updatedAt + ...ManufacturerFormDetails + } + } + ${manufacturerFormFragment} +`; +export const updateManufacturerMutation = gql` + mutation UpdateManufacturer($id: ID!, $input: ManufacturerUpdateInput!) { + updateManufacturer(id: $id, input: $input) { + id + updatedAt + ...ManufacturerFormDetails + } + } + ${manufacturerFormFragment} +`; diff --git a/demo/admin/src/products/future/generated/ManufacturerForm.tsx b/demo/admin/src/products/future/generated/ManufacturerForm.tsx new file mode 100644 index 0000000000..16806ce3d3 --- /dev/null +++ b/demo/admin/src/products/future/generated/ManufacturerForm.tsx @@ -0,0 +1,398 @@ +// 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, + FinalFormInput, + FinalFormSaveSplitButton, + FinalFormSubmitEvent, + Loading, + MainContent, + TextField, + Toolbar, + ToolbarActions, + ToolbarFillSpace, + ToolbarItem, + ToolbarTitleItem, + useFormApiRef, + useStackApi, + useStackSwitchApi, +} from "@comet/admin"; +import { ArrowLeft } from "@comet/admin-icons"; +import { EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { IconButton } 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 { createManufacturerMutation, manufacturerFormFragment, manufacturerQuery, updateManufacturerMutation } from "./ManufacturerForm.gql"; +import { + GQLCreateManufacturerMutation, + GQLCreateManufacturerMutationVariables, + GQLManufacturerFormDetailsFragment, + GQLManufacturerQuery, + GQLManufacturerQueryVariables, + GQLUpdateManufacturerMutation, + GQLUpdateManufacturerMutationVariables, +} from "./ManufacturerForm.gql.generated"; + +type FormValues = Omit & { + address: + | (Omit, "streetNumber" | "zip" | "alternativeAddress"> & { + streetNumber: string | null; + zip: string; + alternativeAddress: + | (Omit["alternativeAddress"]>, "streetNumber" | "zip"> & { + streetNumber: string | null; + zip: string; + }) + | null; + }) + | null; + addressAsEmbeddable: Omit & { + streetNumber: string | null; + zip: string; + alternativeAddress: Omit & { + streetNumber: string | null; + zip: string; + }; + }; +}; + +interface FormProps { + id?: string; +} + +export function ManufacturerForm({ 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( + manufacturerQuery, + id ? { variables: { id } } : { skip: true }, + ); + + const initialValues = React.useMemo>(() => { + const filteredData = data ? filter(manufacturerFormFragment, data.manufacturer) : undefined; + if (!filteredData) return {}; + + return { + ...filteredData, + address: filteredData.address + ? { + ...filteredData.address, + streetNumber: filteredData.address.streetNumber ? String(filteredData.address.streetNumber) : null, + zip: String(filteredData.address.zip), + alternativeAddress: filteredData.address.alternativeAddress + ? { + ...filteredData.address.alternativeAddress, + streetNumber: filteredData.address.alternativeAddress.streetNumber + ? String(filteredData.address.alternativeAddress.streetNumber) + : null, + zip: String(filteredData.address.alternativeAddress.zip), + } + : null, + } + : null, + addressAsEmbeddable: { + ...filteredData.addressAsEmbeddable, + streetNumber: filteredData.addressAsEmbeddable.streetNumber ? String(filteredData.addressAsEmbeddable.streetNumber) : null, + zip: String(filteredData.addressAsEmbeddable.zip), + alternativeAddress: { + ...filteredData.addressAsEmbeddable.alternativeAddress, + streetNumber: filteredData.addressAsEmbeddable.alternativeAddress.streetNumber + ? String(filteredData.addressAsEmbeddable.alternativeAddress.streetNumber) + : null, + zip: String(filteredData.addressAsEmbeddable.alternativeAddress.zip), + }, + }, + }; + }, [data]); + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "manufacturer", id); + return resolveHasSaveConflict(data?.manufacturer.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, + address: formValues.address + ? { + ...formValues.address, + streetNumber: formValues.address.streetNumber ? parseInt(formValues.address.streetNumber) : null, + zip: parseInt(formValues.address.zip), + alternativeAddress: formValues.address.alternativeAddress + ? { + ...formValues.address.alternativeAddress, + streetNumber: formValues.address.alternativeAddress.streetNumber + ? parseInt(formValues.address.alternativeAddress.streetNumber) + : null, + zip: parseInt(formValues.address.alternativeAddress.zip), + } + : null, + } + : null, + addressAsEmbeddable: { + ...formValues.addressAsEmbeddable, + streetNumber: formValues.addressAsEmbeddable.streetNumber ? parseInt(formValues.addressAsEmbeddable.streetNumber) : null, + zip: parseInt(formValues.addressAsEmbeddable.zip), + alternativeAddress: { + ...formValues.addressAsEmbeddable.alternativeAddress, + streetNumber: formValues.addressAsEmbeddable.alternativeAddress.streetNumber + ? parseInt(formValues.addressAsEmbeddable.alternativeAddress.streetNumber) + : null, + zip: parseInt(formValues.addressAsEmbeddable.alternativeAddress.zip), + }, + }, + }; + if (mode === "edit") { + if (!id) throw new Error(); + const { ...updateInput } = output; + await client.mutate({ + mutation: updateManufacturerMutation, + variables: { id, input: updateInput }, + }); + } else { + const { data: mutationResponse } = await client.mutate({ + mutation: createManufacturerMutation, + variables: { input: output }, + }); + if (!event.navigatingBack) { + const id = mutationResponse?.createManufacturer.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 + ) : ( + + ) + } + + + + + + + + + } + /> + + } + /> + + } + /> + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + } + /> + + + } + /> + + } + /> + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + )} + + ); +} diff --git a/demo/admin/src/products/future/generated/ProductForm.tsx b/demo/admin/src/products/future/generated/ProductForm.tsx index 007a455a61..a436cd5dc7 100644 --- a/demo/admin/src/products/future/generated/ProductForm.tsx +++ b/demo/admin/src/products/future/generated/ProductForm.tsx @@ -53,8 +53,8 @@ const rootBlocks = { image: DamImageBlock, }; -type FormValues = Omit & { - price?: string; +type FormValues = Omit & { + price: string | null; image: BlockState; }; @@ -74,21 +74,23 @@ export function ProductForm({ id }: FormProps): React.ReactElement { id ? { variables: { id } } : { skip: true }, ); - const initialValues = React.useMemo>( - () => - data?.product - ? { - ...filter(productFormFragment, data.product), + const initialValues = React.useMemo>(() => { + const filteredData = data ? filter(productFormFragment, data.product) : undefined; + if (!filteredData) { + return { + inStock: false, + image: rootBlocks.image.defaultValues(), + }; + } - price: data.product.price ? String(data.product.price) : undefined, - image: rootBlocks.image.input2State(data.product.image), - } - : { - inStock: false, - image: rootBlocks.image.defaultValues(), - }, - [data], - ); + return { + ...filteredData, + createdAt: new Date(filteredData.createdAt), + price: filteredData.price ? String(filteredData.price) : null, + availableSince: filteredData.availableSince ? new Date(filteredData.availableSince) : null, + image: rootBlocks.image.input2State(filteredData.image), + }; + }, [data]); const saveConflict = useFormSaveConflict({ checkConflict: async () => { @@ -105,9 +107,8 @@ export function ProductForm({ id }: FormProps): React.ReactElement { if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); const output = { ...formValues, - category: formValues.category?.id, - - price: formValues.price ? parseFloat(formValues.price) : null, + category: formValues.category ? formValues.category?.id : null, + price: formValues.price ? parseInt(formValues.price) : null, image: rootBlocks.image.state2Output(formValues.image), }; if (mode === "edit") { diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts index f67d0990c5..4e6f9d6503 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -160,7 +160,7 @@ export function generateForm( : "" } - type FormValues = ${generateFormValuesTypeDefinition({ fragmentName, rootBlocks, config, gqlType, gqlIntrospection })}; + type FormValues = ${generateFormValuesTypeDefinition({ config, fragmentName, gqlType, gqlIntrospection })}; interface FormProps { id?: string; @@ -193,7 +193,7 @@ export function generateForm( const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); - const output = ${generateOutputObject({ rootBlocks, config })}; + const output = ${generateOutputObject({ config, rootBlocks, gqlType, gqlIntrospection })}; if (mode === "edit") { if (!id) throw new Error(); const { ${readOnlyFields.map((field) => `${String(field.name)},`).join("")} ...updateInput } = output; diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField.ts b/packages/admin/cms-admin/src/generator/future/generateFormField.ts index 6319d1c73b..2019f4771f 100644 --- a/packages/admin/cms-admin/src/generator/future/generateFormField.ts +++ b/packages/admin/cms-admin/src/generator/future/generateFormField.ts @@ -94,10 +94,6 @@ export function generateFormField( ${validateCode} />`; //TODO MUI suggest not using type=number https://mui.com/material-ui/react-text-field/#type-quot-number-quot - let assignment = `parseFloat(formValues.${String(name)})`; - if (isFieldOptional({ config, gqlIntrospection: gqlIntrospection, gqlType: gqlType })) { - assignment = `formValues.${name} ? ${assignment} : null`; - } } else if (config.type == "boolean") { code = ` {(props) => ( @@ -235,7 +231,6 @@ export function generateFormField( return result.data.${rootQuery}.nodes; });`; - code = ` { @@ -21,14 +21,6 @@ const recursiveStringify = (obj: FieldsObjectType): string => { return ret; }; -export function getRootProps(fields: string[]): string[] { - const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { - objectPath.set(acc, field, true); - return acc; - }, {}); - return Object.keys(fieldsObject); -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any export function generateFieldListGqlString(fields: FormFieldConfig[], gqlType: string, gqlIntrospection: IntrospectionQuery) { const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { @@ -79,14 +71,6 @@ export function generateFieldListGqlString(fields: FormFieldConfig[], gqlTy return recursiveStringify(fieldsObject); } -export function generateFieldListGqlStringForGrid(fields: GridColumnConfig[]) { - const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { - objectPath.set(acc, String(field.name), true); - return acc; - }, {}); - return recursiveStringify(fieldsObject); -} - function fieldListFromIntrospectionTypeRecursive( types: readonly IntrospectionType[], type: string, diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts b/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts index 42de95b5a5..b960369094 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts @@ -1,73 +1,101 @@ -import { IntrospectionQuery } from "graphql"; +import { IntrospectionQuery, IntrospectionTypeRef } from "graphql"; import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; -import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; -import { RootBlocks } from "./findRootBlocks"; -import { getRootProps } from "./generateFieldList"; -import { isFieldOptional } from "./isFieldOptional"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type SimpleFormFieldConfig = FormFieldConfig; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type SimpleFormConfig = Omit, "fields"> & { fields: SimpleFormFieldConfig[] }; export function generateFormValuesTypeDefinition({ config, - rootBlocks, fragmentName, gqlIntrospection, gqlType, }: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: FormConfig; - rootBlocks: RootBlocks; + config: SimpleFormConfig; fragmentName: string; gqlIntrospection: IntrospectionQuery; gqlType: string; }) { - const numberFields = config.fields.filter((field) => field.type == "number"); - const rootPropsContainingNumberField = getRootProps(numberFields.map((field) => field.name)); - return `${ - rootPropsContainingNumberField.length > 0 - ? `Omit `"${String(rootProp)}"`).join(" | ")}>` - : `GQL${fragmentName}Fragment` - } ${ - rootPropsContainingNumberField.length > 0 || Object.keys(rootBlocks).length > 0 - ? `& { - ${rootPropsContainingNumberField - .map((rootProp) => generateFieldTypesStructureForRootProp(rootProp, config.fields, gqlIntrospection, gqlType)) - .join("\n")} - ${Object.keys(rootBlocks) - .map((rootBlockKey) => `${rootBlockKey}: BlockState;`) - .join("\n")} - }` - : "" - }`; -} + // number need to be types as string to support final-form-number-field features + // block are special because they need to be converted between input and output. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function generateFieldTypesStructureForRootProp( - rootProp: string, - fields: FormFieldConfig[], - gqlIntrospection: IntrospectionQuery, - gqlType: string, -) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const isOptional = (fieldConfig: FormFieldConfig) => { - return isFieldOptional({ config: fieldConfig, gqlIntrospection: gqlIntrospection, gqlType: gqlType }); - }; - const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { - if (field.name.includes(rootProp)) { - if (field.type === "number") { - objectPath.set(acc, field.name, `${isOptional(field) ? `?` : ``}: string;`); - } else if (field.type === "text" || field.type === "asyncSelect" || field.type === "staticSelect") { - objectPath.set(acc, field.name, `${isOptional(field) ? `?` : ``}: string;`); - } else if (field.type === "date") { - objectPath.set(acc, field.name, `${isOptional(field) ? `?` : ``}: Date;`); - } else if (field.type === "boolean") { - objectPath.set(acc, field.name, `${isOptional(field) ? `?` : ``}: boolean;`); - } else if (field.type === "block") { - // TODO how does this type look like? Is this correct? - objectPath.set(acc, field.name, `${isOptional(field) ? `?` : ``}: BlockState`); - } - } + const introspectedType = gqlIntrospection.__schema.types.find((type) => type.name === gqlType); + if (!introspectedType || introspectedType.kind !== "OBJECT") throw new Error(`kind of ${gqlType} is not object, but should be.`); // this should not happen + + const fieldsObject: FieldsObjectType = config.fields.reduce((acc, field) => { + if (field.type !== "number" && field.type !== "block") return acc; + + objectPath.set(acc, field.name, { fieldConfig: field, isFieldObject: true }); return acc; }, {}); - return convertObjectToStructuredString(fieldsObject); + + return Object.keys(fieldsObject).length + ? generateFormValueStringForNestedObject({ + typeToReplaceFields: `GQL${fragmentName}Fragment`, + object: fieldsObject, + typeRef: introspectedType, + gqlIntrospection, + }) + : `GQL${fragmentName}Fragment`; +} + +type FieldsObjectType = { [key: string]: FieldsObjectType | { fieldConfig: SimpleFormFieldConfig; isFieldObject: true } }; +function generateFormValueStringForNestedObject({ + typeToReplaceFields, + object, + typeRef, + gqlIntrospection, +}: { + typeToReplaceFields: string; + object: FieldsObjectType; + typeRef: IntrospectionTypeRef; + gqlIntrospection: IntrospectionQuery; +}): string { + if (typeRef.kind === "NON_NULL") typeRef = typeRef.ofType; + if (typeRef.kind !== "OBJECT") throw new Error(`Type must not be other than OBJECT but is ${typeRef.kind}.`); + + const typeName = typeRef.name; + const introspectionType = gqlIntrospection.__schema.types.find((type) => type.name === typeName); + if (!introspectionType || introspectionType.kind !== "OBJECT") throw new Error(`No type found for ${typeName}.`); + + const omitValues = Object.keys(object) + .map((key) => `"${key}"`) + .join(" | "); + return `Omit<${typeToReplaceFields}, ${omitValues}> & { + ${Object.entries(object) + .map(([key, value]) => { + const introspectionField = introspectionType.fields.find((field) => field.name === key); + if (!introspectionField) throw new Error(`IntrospectionField for ${key} not found.`); + + const isNullable = introspectionField.type.kind !== "NON_NULL"; + + let type; + if (value.isFieldObject) { + const fieldConfig = value.fieldConfig as SimpleFormFieldConfig; + if (fieldConfig.type === "number") { + type = `string`; + } else if (fieldConfig.type === "block") { + type = `BlockState`; + } else { + throw new Error(`Field of type ${fieldConfig.type} currently not supported.`); + } + } else { + let typeToReplaceFieldsForKey = `${typeToReplaceFields}["${key}"]`; + if (isNullable) { + typeToReplaceFieldsForKey = `NonNullable<${typeToReplaceFieldsForKey}>`; + } + type = generateFormValueStringForNestedObject({ + typeToReplaceFields: typeToReplaceFieldsForKey, + object: value as FieldsObjectType, + typeRef: introspectionField.type, + gqlIntrospection, + }); + } + return `${key}: ${type}${isNullable ? ` | null` : ``}`; + }) + .join(";\n")}; + }`; } diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts b/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts index 87efa3be95..05af91d526 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts @@ -1,11 +1,8 @@ -import { IntrospectionQuery } from "graphql"; +import { IntrospectionQuery, IntrospectionTypeRef } from "graphql"; import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; -import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; import { RootBlocks } from "./findRootBlocks"; -import { getRootProps } from "./generateFieldList"; -import { isFieldOptional } from "./isFieldOptional"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormFieldConfig = FormFieldConfig & { name: string }; @@ -27,53 +24,99 @@ export function generateInitialValuesValue({ gqlIntrospection: IntrospectionQuery; gqlType: string; }) { + // number-fields need to be converted to string to match type + // block-fields do have some default-config + // boolean-fields are defaulted to false + // date-fields need to be converted to Date + + const introspectedType = gqlIntrospection.__schema.types.find((type) => type.name === gqlType); + if (!introspectedType || introspectedType.kind !== "OBJECT") throw new Error(`kind of ${gqlType} is not object, but should be.`); // this should not happen + const booleanFields = config.fields.filter((field) => field.type == "boolean"); - const numberFields = config.fields.filter((field) => field.type == "number" && !field.name.includes(".")); - const nestedNumberFields = config.fields.filter((field) => field.type === "number" && field.name.includes(".")); - const rootPropsContainingNumberField = getRootProps(nestedNumberFields.map((field) => field.name)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const isOptional = (fieldConfig: FormFieldConfig) => { - return isFieldOptional({ config: fieldConfig, gqlIntrospection: gqlIntrospection, gqlType: gqlType }); - }; - return `React.useMemo>(() => data?.${instanceGqlType} - ? { - ...filter(${instanceGqlType}FormFragment, data.${instanceGqlType}), - ${rootPropsContainingNumberField - .map((rootProp) => { - const fieldsObject: FieldsObjectType = config.fields.reduce((acc, field) => { - if (field.name.includes(rootProp)) { - const nameWithoutPrefix = field.name.substring(rootProp.length + 1); - let assignment = - field.type === "number" ? `String(data.${instanceGqlType}.${field.name})` : `data.${instanceGqlType}.${field.name}`; - if (isOptional(field)) { - assignment = `data.${instanceGqlType}.${String(field.name)} ? ${assignment} : undefined`; - } - objectPath.set(acc, nameWithoutPrefix, `: ${assignment},`); - } - return acc; - }, {}); - return `${rootProp}: data.${instanceGqlType}.${rootProp} ? { ${convertObjectToStructuredString(fieldsObject)} } : undefined,`; - }) - .join("\n")} - ${numberFields - .map((field) => { - let assignment = `String(data.${instanceGqlType}.${String(field.name)})`; - if (isOptional(field)) { - assignment = `data.${instanceGqlType}.${String(field.name)} ? ${assignment} : undefined`; - } - return `${String(field.name)}: ${assignment},`; - }) - .join("\n")} - ${Object.keys(rootBlocks) - .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.input2State(data.${instanceGqlType}.${rootBlockKey}),`) - .join("\n")} + const fieldsObject: FieldsObjectType = config.fields.reduce((acc, field) => { + if (field.type !== "number" && field.type !== "date") return acc; + + objectPath.set(acc, field.name, { fieldConfig: field, isFieldObject: true }); + return acc; + }, {}); + + return `React.useMemo>(() => { + const filteredData = data ? filter(${instanceGqlType}FormFragment, data.${instanceGqlType}) : undefined; + ${ + booleanFields.length || Object.keys(rootBlocks).length + ? `if (!filteredData) { + return { + ${booleanFields.map((field) => `${String(field.name)}: false,`).join("\n")} + ${Object.keys(rootBlocks) + .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.defaultValues(),`) + .join("\n")} + }; + }` + : `if (!filteredData) return {};` } - : { - ${booleanFields.map((field) => `${String(field.name)}: false,`).join("\n")} + + return { + ...filteredData, + ${generateInitialValuesStringForNestedObject({ path: "filteredData", object: fieldsObject, typeRef: introspectedType, gqlIntrospection })} ${Object.keys(rootBlocks) - .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.defaultValues(),`) + .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.input2State(filteredData.${rootBlockKey}),`) .join("\n")} - } - , [data])`; + }; + }, [data])`; +} + +type FieldsObjectType = { [key: string]: FieldsObjectType | { fieldConfig: SimpleFormFieldConfig; isFieldObject: true } }; +function generateInitialValuesStringForNestedObject({ + path, + object, + typeRef, + gqlIntrospection, +}: { + path: string; + object: FieldsObjectType; + typeRef: IntrospectionTypeRef; + gqlIntrospection: IntrospectionQuery; +}): string { + if (typeRef.kind === "NON_NULL") typeRef = typeRef.ofType; + if (typeRef.kind !== "OBJECT") throw new Error(`Type must not be other than OBJECT but is ${typeRef.kind}.`); + + const typeName = typeRef.name; + const introspectionType = gqlIntrospection.__schema.types.find((type) => type.name === typeName); + if (!introspectionType || introspectionType.kind !== "OBJECT") throw new Error(`No type found for ${typeName}.`); + + return `${Object.entries(object) + .map(([key, value]) => { + const introspectionField = introspectionType.fields.find((field) => field.name === key); + if (!introspectionField) throw new Error(`IntrospectionField for ${key} not found.`); + + const isNullable = introspectionField.type.kind !== "NON_NULL"; + + let assignment; + if (value.isFieldObject) { + const fieldConfig = value.fieldConfig as SimpleFormFieldConfig; + if (fieldConfig.type === "number") { + assignment = `String(${path}.${key})`; + } else if (fieldConfig.type === "date") { + assignment = `new Date(${path}.${key})`; + } else { + throw new Error(`Field of type ${fieldConfig.type} currently not supported.`); + } + } else { + assignment = `{ + ...${path}.${key}, + ${generateInitialValuesStringForNestedObject({ + path: `${path}.${key}`, + object: value as FieldsObjectType, + typeRef: introspectionField.type, + gqlIntrospection, + })} + }`; + } + if (isNullable) { + assignment = `${path}.${key} ? ${assignment} : null`; + } + return `${key}: ${assignment}`; + }) + .join(",\n")},`; } diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts index 49a62d06ba..f945b0e6ec 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts @@ -1,48 +1,99 @@ +import { IntrospectionQuery, IntrospectionTypeRef } from "graphql"; import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; -import { convertObjectToStructuredString, FieldsObjectType } from "./convertObjectToStructuredString"; import { RootBlocks } from "./findRootBlocks"; -import { getRootProps } from "./generateFieldList"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormFieldConfig = FormFieldConfig & { name: string }; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormConfig = Omit, "fields"> & { fields: SimpleFormFieldConfig[] }; -export function generateOutputObject({ config, rootBlocks }: { config: SimpleFormConfig; rootBlocks: RootBlocks }) { - const numberFields = config.fields.filter((field) => field.type == "number" && !field.name.includes(".")); - const asyncSelectFields = config.fields.filter((field) => field.type === "asyncSelect"); - const nestedNumberFields = config.fields.filter((field) => field.type === "number" && field.name.includes(".")); - const rootPropsContainingNumberField = getRootProps(nestedNumberFields.map((field) => field.name)); +export function generateOutputObject({ + config, + rootBlocks, + gqlIntrospection, + gqlType, +}: { + config: SimpleFormConfig; + rootBlocks: RootBlocks; + gqlIntrospection: IntrospectionQuery; + gqlType: string; +}) { + // number-fields need to be converted into number again + // asyncSelect-fields need to use field.id + // block-fields need to be converted from state2Output + + const introspectedType = gqlIntrospection.__schema.types.find((type) => type.name === gqlType); + if (!introspectedType || introspectedType.kind !== "OBJECT") throw new Error(`kind of ${gqlType} is not object, but should be.`); // this should not happen + + const fieldsObject: FieldsObjectType = config.fields.reduce((acc, field) => { + if (field.type !== "number" && field.type !== "asyncSelect") return acc; + + objectPath.set(acc, field.name, { fieldConfig: field, isFieldObject: true }); + return acc; + }, {}); + return `{ ...formValues, - ${asyncSelectFields - .map((field) => { - return `${field.name}: formValues.${field.name}?.id,`; - }) - .join("\n")} - ${rootPropsContainingNumberField - .map((rootProp) => { - const fieldsObject: FieldsObjectType = config.fields.reduce((acc, field) => { - if (field.name.includes(rootProp)) { - const nameWithoutPrefix = field.name.substring(rootProp.length + 1); - if (field.type === "number") { - objectPath.set(acc, nameWithoutPrefix, `: parseFloat(formValues.${field.name}),`); - } else { - objectPath.set(acc, nameWithoutPrefix, `: formValues.${field.name},`); - } - } - return acc; - }, {}); - return `${rootProp}: { ${convertObjectToStructuredString(fieldsObject)} },`; - }) - .join("\n")} - ${numberFields - .map((field) => `${String(field.name)}: formValues.${String(field.name)} ? parseFloat(formValues.${String(field.name)}) : null,`) - .join("\n")} + ${generateOutputObjectStringForNestedObject({ path: "formValues", object: fieldsObject, typeRef: introspectedType, gqlIntrospection })} ${Object.keys(rootBlocks) .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.state2Output(formValues.${rootBlockKey}),`) .join("\n")} }`; } + +type FieldsObjectType = { [key: string]: FieldsObjectType | { fieldConfig: SimpleFormFieldConfig; isFieldObject: true } }; +function generateOutputObjectStringForNestedObject({ + path, + object, + typeRef, + gqlIntrospection, +}: { + path: string; + object: FieldsObjectType; + typeRef: IntrospectionTypeRef; + gqlIntrospection: IntrospectionQuery; +}): string { + if (typeRef.kind === "NON_NULL") typeRef = typeRef.ofType; + if (typeRef.kind !== "OBJECT") throw new Error(`Type must not be other than OBJECT but is ${typeRef.kind}.`); + + const typeName = typeRef.name; + const introspectionType = gqlIntrospection.__schema.types.find((type) => type.name === typeName); + if (!introspectionType || introspectionType.kind !== "OBJECT") throw new Error(`No type found for ${typeName}.`); + + return `${Object.entries(object) + .map(([key, value]) => { + const introspectionField = introspectionType.fields.find((field) => field.name === key); + if (!introspectionField) throw new Error(`IntrospectionField for ${key} not found.`); + + const isNullable = introspectionField.type.kind !== "NON_NULL"; + + let assignment; + if (value.isFieldObject) { + const fieldConfig = value.fieldConfig as SimpleFormFieldConfig; + if (fieldConfig.type === "number") { + assignment = `parseInt(${path}.${key})`; + } else if (fieldConfig.type === "asyncSelect") { + assignment = `${path}.${key}?.id`; + } else { + throw new Error(`Field of type ${fieldConfig.type} currently not supported.`); + } + } else { + assignment = `{ + ...${path}.${key}, + ${generateOutputObjectStringForNestedObject({ + path: `${path}.${key}`, + object: value as FieldsObjectType, + typeRef: introspectionField.type, + gqlIntrospection, + })} + }`; + } + if (isNullable) { + assignment = `${path}.${key} ? ${assignment} : null`; + } + return `${key}: ${assignment}`; + }) + .join(",\n")},`; +} From 1854a54a00be733bf0aaca6e3ef006a1c6130580 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 29 Mar 2024 10:11:47 +0100 Subject: [PATCH 10/13] Remove unnecessary ? --- demo/admin/src/products/future/generated/ProductForm.tsx | 2 +- .../src/generator/future/utils/generateOutputObject.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/admin/src/products/future/generated/ProductForm.tsx b/demo/admin/src/products/future/generated/ProductForm.tsx index a436cd5dc7..8995fcf3e7 100644 --- a/demo/admin/src/products/future/generated/ProductForm.tsx +++ b/demo/admin/src/products/future/generated/ProductForm.tsx @@ -107,7 +107,7 @@ export function ProductForm({ id }: FormProps): React.ReactElement { if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); const output = { ...formValues, - category: formValues.category ? formValues.category?.id : null, + category: formValues.category ? formValues.category.id : null, price: formValues.price ? parseInt(formValues.price) : null, image: rootBlocks.image.state2Output(formValues.image), }; diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts index f945b0e6ec..7b18b93551 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts +++ b/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts @@ -75,7 +75,7 @@ function generateOutputObjectStringForNestedObject({ if (fieldConfig.type === "number") { assignment = `parseInt(${path}.${key})`; } else if (fieldConfig.type === "asyncSelect") { - assignment = `${path}.${key}?.id`; + assignment = `${path}.${key}.id`; } else { throw new Error(`Field of type ${fieldConfig.type} currently not supported.`); } From 8b7db40a697f2afccc91515658641ab3858e2763 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 29 Mar 2024 10:50:49 +0100 Subject: [PATCH 11/13] Move generateForm-specific helper functions into separate folder --- .../admin/cms-admin/src/generator/future/generateForm.ts | 6 +++--- .../generateFormValuesTypeDefinition.ts | 0 .../{utils => generateForm}/generateInitialValuesValue.ts | 2 +- .../future/{utils => generateForm}/generateOutputObject.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename packages/admin/cms-admin/src/generator/future/{utils => generateForm}/generateFormValuesTypeDefinition.ts (100%) rename packages/admin/cms-admin/src/generator/future/{utils => generateForm}/generateInitialValuesValue.ts (98%) rename packages/admin/cms-admin/src/generator/future/{utils => generateForm}/generateOutputObject.ts (98%) diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts index 4e6f9d6503..46f53d844a 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -1,14 +1,14 @@ import { IntrospectionQuery } from "graphql"; +import { generateFormValuesTypeDefinition } from "./generateForm/generateFormValuesTypeDefinition"; +import { generateInitialValuesValue } from "./generateForm/generateInitialValuesValue"; +import { generateOutputObject } from "./generateForm/generateOutputObject"; import { generateFormField } from "./generateFormField"; import { FormConfig, FormFieldConfig, GeneratorReturn } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; import { findRootBlocks } from "./utils/findRootBlocks"; import { generateFieldListGqlString } from "./utils/generateFieldList"; -import { generateFormValuesTypeDefinition } from "./utils/generateFormValuesTypeDefinition"; import { generateImportsCode, Imports } from "./utils/generateImportsCode"; -import { generateInitialValuesValue } from "./utils/generateInitialValuesValue"; -import { generateOutputObject } from "./utils/generateOutputObject"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormFieldConfig = FormFieldConfig & { name: string }; diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts b/packages/admin/cms-admin/src/generator/future/generateForm/generateFormValuesTypeDefinition.ts similarity index 100% rename from packages/admin/cms-admin/src/generator/future/utils/generateFormValuesTypeDefinition.ts rename to packages/admin/cms-admin/src/generator/future/generateForm/generateFormValuesTypeDefinition.ts diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts b/packages/admin/cms-admin/src/generator/future/generateForm/generateInitialValuesValue.ts similarity index 98% rename from packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts rename to packages/admin/cms-admin/src/generator/future/generateForm/generateInitialValuesValue.ts index 05af91d526..6a1dd66a75 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateInitialValuesValue.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm/generateInitialValuesValue.ts @@ -2,7 +2,7 @@ import { IntrospectionQuery, IntrospectionTypeRef } from "graphql"; import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; -import { RootBlocks } from "./findRootBlocks"; +import { RootBlocks } from "../utils/findRootBlocks"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormFieldConfig = FormFieldConfig & { name: string }; diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts b/packages/admin/cms-admin/src/generator/future/generateForm/generateOutputObject.ts similarity index 98% rename from packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts rename to packages/admin/cms-admin/src/generator/future/generateForm/generateOutputObject.ts index 7b18b93551..b9e066ba6c 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateOutputObject.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm/generateOutputObject.ts @@ -2,7 +2,7 @@ import { IntrospectionQuery, IntrospectionTypeRef } from "graphql"; import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; -import { RootBlocks } from "./findRootBlocks"; +import { RootBlocks } from "../utils/findRootBlocks"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormFieldConfig = FormFieldConfig & { name: string }; From e858cad89b3bc2b098d9ce3e3406b14e0f9d2483 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 29 Mar 2024 11:28:27 +0100 Subject: [PATCH 12/13] Adding comments and remove unnecessary generic-usage --- .../admin/cms-admin/src/generator/future/generateForm.ts | 5 +++-- .../future/generateForm/generateFormValuesTypeDefinition.ts | 1 + .../future/generateForm/generateInitialValuesValue.ts | 3 ++- .../generator/future/generateForm/generateOutputObject.ts | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts index 46f53d844a..261b1db998 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -10,8 +10,9 @@ import { findRootBlocks } from "./utils/findRootBlocks"; import { generateFieldListGqlString } from "./utils/generateFieldList"; import { generateImportsCode, Imports } from "./utils/generateImportsCode"; +// Retype FormFieldConfig and FormConfig to fix "Type instantiation is excessively deep and possibly infinite." // eslint-disable-next-line @typescript-eslint/no-explicit-any -type SimpleFormFieldConfig = FormFieldConfig & { name: string }; +type SimpleFormFieldConfig = FormFieldConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormConfig = Omit, "fields"> & { fields: SimpleFormFieldConfig[] }; @@ -45,7 +46,7 @@ export function generateForm( let hooksCode = ""; const fieldsCode = config.fields - .map((field) => { + .map((field) => { const generated = generateFormField({ gqlIntrospection }, field, config); for (const name in generated.gqlDocuments) { gqlDocuments[name] = generated.gqlDocuments[name]; diff --git a/packages/admin/cms-admin/src/generator/future/generateForm/generateFormValuesTypeDefinition.ts b/packages/admin/cms-admin/src/generator/future/generateForm/generateFormValuesTypeDefinition.ts index b960369094..94a7157d3e 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm/generateFormValuesTypeDefinition.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm/generateFormValuesTypeDefinition.ts @@ -3,6 +3,7 @@ import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; +// Retype FormFieldConfig and FormConfig to fix "Type instantiation is excessively deep and possibly infinite." // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormFieldConfig = FormFieldConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/admin/cms-admin/src/generator/future/generateForm/generateInitialValuesValue.ts b/packages/admin/cms-admin/src/generator/future/generateForm/generateInitialValuesValue.ts index 6a1dd66a75..154f2dae3d 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm/generateInitialValuesValue.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm/generateInitialValuesValue.ts @@ -4,8 +4,9 @@ import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; import { RootBlocks } from "../utils/findRootBlocks"; +// Retype FormFieldConfig and FormConfig to fix "Type instantiation is excessively deep and possibly infinite." // eslint-disable-next-line @typescript-eslint/no-explicit-any -type SimpleFormFieldConfig = FormFieldConfig & { name: string }; +type SimpleFormFieldConfig = FormFieldConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormConfig = Omit, "fields"> & { fields: SimpleFormFieldConfig[] }; diff --git a/packages/admin/cms-admin/src/generator/future/generateForm/generateOutputObject.ts b/packages/admin/cms-admin/src/generator/future/generateForm/generateOutputObject.ts index b9e066ba6c..d65bf094c3 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm/generateOutputObject.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm/generateOutputObject.ts @@ -4,8 +4,9 @@ import objectPath from "object-path"; import { FormConfig, FormFieldConfig } from "../generator"; import { RootBlocks } from "../utils/findRootBlocks"; +// Retype FormFieldConfig and FormConfig to fix "Type instantiation is excessively deep and possibly infinite." // eslint-disable-next-line @typescript-eslint/no-explicit-any -type SimpleFormFieldConfig = FormFieldConfig & { name: string }; +type SimpleFormFieldConfig = FormFieldConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any type SimpleFormConfig = Omit, "fields"> & { fields: SimpleFormFieldConfig[] }; From 6a0b75a4f38958b26a137714e69fb88e8d36d547 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 29 Mar 2024 11:52:26 +0100 Subject: [PATCH 13/13] Simplify, remove and move util-files --- .../src/generator/future/generateForm.ts | 6 +- .../generateGqlFieldList.ts} | 77 +++++++------------ .../src/generator/future/generateFormField.ts | 2 +- .../generateFormField/generateFieldList.ts | 31 ++++++++ .../utils/convertObjectToStructuredString.ts | 15 ---- 5 files changed, 60 insertions(+), 71 deletions(-) rename packages/admin/cms-admin/src/generator/future/{utils/generateFieldList.ts => generateForm/generateGqlFieldList.ts} (66%) create mode 100644 packages/admin/cms-admin/src/generator/future/generateFormField/generateFieldList.ts delete mode 100644 packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts index 261b1db998..cbecec5d9c 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -1,13 +1,13 @@ import { IntrospectionQuery } from "graphql"; import { generateFormValuesTypeDefinition } from "./generateForm/generateFormValuesTypeDefinition"; +import { generateGqlFieldList } from "./generateForm/generateGqlFieldList"; import { generateInitialValuesValue } from "./generateForm/generateInitialValuesValue"; import { generateOutputObject } from "./generateForm/generateOutputObject"; import { generateFormField } from "./generateFormField"; import { FormConfig, FormFieldConfig, GeneratorReturn } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; import { findRootBlocks } from "./utils/findRootBlocks"; -import { generateFieldListGqlString } from "./utils/generateFieldList"; import { generateImportsCode, Imports } from "./utils/generateImportsCode"; // Retype FormFieldConfig and FormConfig to fix "Type instantiation is excessively deep and possibly infinite." @@ -31,8 +31,6 @@ export function generateForm( const gqlDocuments: Record = {}; const imports: Imports = []; - const fieldList = generateFieldListGqlString(config.fields, gqlType, gqlIntrospection); - // TODO make RootBlocks configurable (from config) const rootBlocks = findRootBlocks({ gqlType, targetDirectory }, gqlIntrospection); @@ -59,7 +57,7 @@ export function generateForm( const fragmentName = config.fragmentName ?? `${gqlType}Form`; gqlDocuments[`${instanceGqlType}FormFragment`] = ` - fragment ${fragmentName} on ${gqlType} { ${fieldList} } + fragment ${fragmentName} on ${gqlType} { ${generateGqlFieldList({ fields: config.fields, gqlType, gqlIntrospection })} } `; gqlDocuments[`${instanceGqlType}Query`] = ` diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts b/packages/admin/cms-admin/src/generator/future/generateForm/generateGqlFieldList.ts similarity index 66% rename from packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts rename to packages/admin/cms-admin/src/generator/future/generateForm/generateGqlFieldList.ts index 932aff3224..661f288be7 100644 --- a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm/generateGqlFieldList.ts @@ -1,28 +1,20 @@ -import { IntrospectionField, IntrospectionObjectType, IntrospectionQuery, IntrospectionType } from "graphql"; +import { IntrospectionObjectType, IntrospectionQuery } from "graphql"; import objectPath from "object-path"; +import { generateFieldListFromIntrospection } from "../generateFormField/generateFieldList"; import { FormFieldConfig } from "../generator"; type FieldsObjectType = { [key: string]: FieldsObjectType | boolean | string }; -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 if (typeof valueForKey === "string") { - ret += `${prefixField}${key}${valueForKey}`; - } else { - ret += `${prefixField}${key} { ${recursiveStringify(valueForKey)} }`; - } - prefixField = " "; - } - return ret; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function generateFieldListGqlString(fields: FormFieldConfig[], gqlType: string, gqlIntrospection: IntrospectionQuery) { +export function generateGqlFieldList({ + fields, + gqlType, + gqlIntrospection, +}: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fields: FormFieldConfig[]; + gqlType: string; + gqlIntrospection: IntrospectionQuery; +}) { const fieldsObject: FieldsObjectType = fields.reduce((acc, field) => { if (field.type === "asyncSelect") { const name = String(field.name); @@ -71,36 +63,19 @@ export function generateFieldListGqlString(fields: FormFieldConfig[], gqlTy 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}`; - let outputType = field.type; - if (outputType.kind === "NON_NULL") { - outputType = outputType.ofType; - } - if (outputType.kind === "OBJECT") { - const subFields = fieldListFromIntrospectionTypeRecursive(types, outputType.name, path); - acc.push({ path: path, field: field }); - acc.push(...subFields); +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 if (typeof valueForKey === "string") { + ret += `${prefixField}${key}${valueForKey}`; } else { - acc.push({ - path: path, - field: field, - }); + ret += `${prefixField}${key} { ${recursiveStringify(valueForKey)} }`; } - return acc; - }, []); -} -export function generateFieldListFromIntrospection( - gqlIntrospection: IntrospectionQuery, - type: string, -): { path: string; field: IntrospectionField }[] { - return fieldListFromIntrospectionTypeRecursive(gqlIntrospection.__schema.types, type); -} + prefixField = " "; + } + return ret; +}; diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField.ts b/packages/admin/cms-admin/src/generator/future/generateFormField.ts index 2019f4771f..41877221a5 100644 --- a/packages/admin/cms-admin/src/generator/future/generateFormField.ts +++ b/packages/admin/cms-admin/src/generator/future/generateFormField.ts @@ -1,8 +1,8 @@ import { IntrospectionEnumType, IntrospectionNamedTypeRef, IntrospectionObjectType, IntrospectionQuery } from "graphql"; +import { generateFieldListFromIntrospection } from "./generateFormField/generateFieldList"; import { FormConfig, FormFieldConfig, GeneratorReturn } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; -import { generateFieldListFromIntrospection } from "./utils/generateFieldList"; import { Imports } from "./utils/generateImportsCode"; import { isFieldOptional } from "./utils/isFieldOptional"; diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField/generateFieldList.ts b/packages/admin/cms-admin/src/generator/future/generateFormField/generateFieldList.ts new file mode 100644 index 0000000000..1a2f5bf5fc --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/generateFormField/generateFieldList.ts @@ -0,0 +1,31 @@ +import { IntrospectionField, IntrospectionQuery, IntrospectionType } from "graphql"; + +export function generateFieldListFromIntrospection( + gqlIntrospection: IntrospectionQuery, + type: string, +): { path: string; field: IntrospectionField }[] { + return fieldListFromIntrospectionTypeRecursive(gqlIntrospection.__schema.types, type); +} + +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}`; + let type = field.type; + if (type.kind === "NON_NULL") type = type.ofType; + + if (type.kind === "OBJECT") { + acc.push({ path: path, field: field }); + acc.push(...fieldListFromIntrospectionTypeRecursive(types, type.name, path)); + } else { + acc.push({ path: path, field: field }); + } + return acc; + }, []); +} diff --git a/packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts b/packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts deleted file mode 100644 index 2f2b6bf1a7..0000000000 --- a/packages/admin/cms-admin/src/generator/future/utils/convertObjectToStructuredString.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type FieldsObjectType = { [key: string]: FieldsObjectType | string }; -export function convertObjectToStructuredString(obj: FieldsObjectType) { - let ret = ""; - let prefixField = ""; - for (const key in obj) { - const valueForKey = obj[key]; - if (typeof valueForKey === "string") { - ret += `${prefixField}${key}${valueForKey}`; - } else { - ret += `${prefixField}${key}: { ${convertObjectToStructuredString(valueForKey)} }`; - } - prefixField = " "; - } - return ret; -}