Skip to content

Commit

Permalink
Admin Generator (Future): Add support for field set layout-field (#2254)
Browse files Browse the repository at this point in the history
- add form-layout type "fieldSet"
- support default props "title", "supportText", "collapsible" and
"initiallyExpanded"
- support placeholder for "supportText" filled from entity-data, but
only for edit-mode (add does not have values)
- only formFields allowed in accordion

this should be possible

![image](https://github.com/vivid-planet/comet/assets/1324250/ce4542c5-14fe-49fe-849e-bbee430dd3f2)
  • Loading branch information
Ben-Ho authored Jul 19, 2024
1 parent aa1ddee commit f9ac3fd
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 110 deletions.
45 changes: 32 additions & 13 deletions demo/admin/src/products/future/ProductForm.cometGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,39 @@ export const ProductForm: FormConfig<GQLProduct> = {
fragmentName: "ProductFormDetails", // configurable as it must be unique across project
fields: [
{
type: "text",
name: "title",
label: "Titel", // default is generated from name (camelCaseToHumanReadable)
required: true, // default is inferred from gql schema
validate: { name: "validateTitle", import: "./validateTitle" },
type: "fieldSet",
name: "mainData",
title: "Main Data",
supportText: "Product: {title}",
collapsible: false,
initiallyExpanded: true,
fields: [
{
type: "text",
name: "title",
label: "Titel", // default is generated from name (camelCaseToHumanReadable)
required: true, // default is inferred from gql schema
validate: { name: "validateTitle", import: "./validateTitle" },
},
{ type: "text", name: "slug" },
{ type: "date", name: "createdAt", label: "Created", readOnly: true },
{ type: "text", name: "description", label: "Description", multiline: true },
{ type: "staticSelect", name: "type", label: "Type", required: true /*, values: from gql schema (TODO overridable)*/ },
{ type: "asyncSelect", name: "category", rootQuery: "productCategories" },
],
},
{
type: "fieldSet",
name: "additionalData",
title: "Additional Data",
collapsible: true,
initiallyExpanded: false,
fields: [
{ type: "boolean", name: "inStock" },
{ type: "date", name: "availableSince" },
{ type: "block", name: "image", label: "Image", block: { name: "DamImageBlock", import: "@comet/cms-admin" } },
],
},
{ type: "text", name: "slug" },
{ type: "date", name: "createdAt", label: "Created", readOnly: true },
{ type: "text", name: "description", label: "Description", multiline: true },
{ type: "staticSelect", name: "type", label: "Type", required: true /*, values: from gql schema (TODO overridable)*/ },
{ type: "asyncSelect", name: "category", rootQuery: "productCategories" },
{ type: "boolean", name: "inStock" },
{ type: "date", name: "availableSince" },
{ type: "block", name: "image", label: "Image", block: { name: "DamImageBlock", import: "@comet/cms-admin" } },
],
};

Expand Down
185 changes: 104 additions & 81 deletions demo/admin/src/products/future/generated/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { gql, useApolloClient, useQuery } from "@apollo/client";
import {
AsyncSelectField,
Field,
FieldSet,
filterByFragment,
FinalForm,
FinalFormCheckbox,
Expand All @@ -24,6 +25,7 @@ import { FormControlLabel, InputAdornment, MenuItem } from "@mui/material";
import { FormApi } from "final-form";
import isEqual from "lodash.isequal";
import React from "react";
import { FormSpy } from "react-final-form";
import { FormattedMessage } from "react-intl";

import { validateTitle } from "../validateTitle";
Expand Down Expand Up @@ -139,90 +141,111 @@ export function ProductForm({ id }: FormProps): React.ReactElement {
<>
{saveConflict.dialogs}
<MainContent>
<TextField
required
fullWidth
name="title"
label={<FormattedMessage id="product.title" defaultMessage="Titel" />}
validate={validateTitle}
/>

<TextField required fullWidth name="slug" label={<FormattedMessage id="product.slug" defaultMessage="Slug" />} />

<Field
readOnly
disabled
endAdornment={
<InputAdornment position="end">
<Lock />
</InputAdornment>
<FieldSet
initiallyExpanded
title={<FormattedMessage id="product.mainData.title" defaultMessage="Main Data" />}
supportText={
mode === "edit" && (
<FormSpy subscription={{ values: true }}>
{({ values }) => (
<FormattedMessage
id="product.mainData.supportText"
defaultMessage="Product: {title}"
values={{ ...values }}
/>
)}
</FormSpy>
)
}
fullWidth
name="createdAt"
component={FinalFormDatePicker}
label={<FormattedMessage id="product.createdAt" defaultMessage="Created" />}
/>

<TextAreaField
required
fullWidth
name="description"
label={<FormattedMessage id="product.description" defaultMessage="Description" />}
/>
<Field required fullWidth name="type" label={<FormattedMessage id="product.type" defaultMessage="Type" />}>
{(props) => (
<FinalFormSelect {...props}>
<MenuItem value="Cap">
<FormattedMessage id="product.type.cap" defaultMessage="Cap" />
</MenuItem>
<MenuItem value="Shirt">
<FormattedMessage id="product.type.shirt" defaultMessage="Shirt" />
</MenuItem>
<MenuItem value="Tie">
<FormattedMessage id="product.type.tie" defaultMessage="Tie" />
</MenuItem>
</FinalFormSelect>
)}
</Field>
<AsyncSelectField
fullWidth
name="category"
label={<FormattedMessage id="product.category" defaultMessage="Category" />}
loadOptions={async () => {
const { data } = await client.query<GQLProductCategoriesSelectQuery, GQLProductCategoriesSelectQueryVariables>({
query: gql`
query ProductCategoriesSelect {
productCategories {
nodes {
id
title
>
<TextField
required
fullWidth
name="title"
label={<FormattedMessage id="product.title" defaultMessage="Titel" />}
validate={validateTitle}
/>

<TextField required fullWidth name="slug" label={<FormattedMessage id="product.slug" defaultMessage="Slug" />} />

<Field
readOnly
disabled
endAdornment={
<InputAdornment position="end">
<Lock />
</InputAdornment>
}
fullWidth
name="createdAt"
component={FinalFormDatePicker}
label={<FormattedMessage id="product.createdAt" defaultMessage="Created" />}
/>

<TextAreaField
required
fullWidth
name="description"
label={<FormattedMessage id="product.description" defaultMessage="Description" />}
/>
<Field required fullWidth name="type" label={<FormattedMessage id="product.type" defaultMessage="Type" />}>
{(props) => (
<FinalFormSelect {...props}>
<MenuItem value="Cap">
<FormattedMessage id="product.type.cap" defaultMessage="Cap" />
</MenuItem>
<MenuItem value="Shirt">
<FormattedMessage id="product.type.shirt" defaultMessage="Shirt" />
</MenuItem>
<MenuItem value="Tie">
<FormattedMessage id="product.type.tie" defaultMessage="Tie" />
</MenuItem>
</FinalFormSelect>
)}
</Field>
<AsyncSelectField
fullWidth
name="category"
label={<FormattedMessage id="product.category" defaultMessage="Category" />}
loadOptions={async () => {
const { data } = await client.query<GQLProductCategoriesSelectQuery, GQLProductCategoriesSelectQueryVariables>({
query: gql`
query ProductCategoriesSelect {
productCategories {
nodes {
id
title
}
}
}
}
`,
});
return data.productCategories.nodes;
}}
getOptionLabel={(option) => option.title}
/>
<Field name="inStock" label="" type="checkbox" fullWidth>
{(props) => (
<FormControlLabel
label={<FormattedMessage id="product.inStock" defaultMessage="In Stock" />}
control={<FinalFormCheckbox {...props} />}
/>
)}
</Field>

<Field
fullWidth
name="availableSince"
component={FinalFormDatePicker}
label={<FormattedMessage id="product.availableSince" defaultMessage="Available Since" />}
/>
<Field name="image" isEqual={isEqual}>
{createFinalFormBlock(rootBlocks.image)}
</Field>
`,
});
return data.productCategories.nodes;
}}
getOptionLabel={(option) => option.title}
/>
</FieldSet>

<FieldSet collapsible title={<FormattedMessage id="product.additionalData.title" defaultMessage="Additional Data" />}>
<Field name="inStock" label="" type="checkbox" fullWidth>
{(props) => (
<FormControlLabel
label={<FormattedMessage id="product.inStock" defaultMessage="In Stock" />}
control={<FinalFormCheckbox {...props} />}
/>
)}
</Field>

<Field
fullWidth
name="availableSince"
component={FinalFormDatePicker}
label={<FormattedMessage id="product.availableSince" defaultMessage="Available Since" />}
/>
<Field name="image" isEqual={isEqual}>
{createFinalFormBlock(rootBlocks.image)}
</Field>
</FieldSet>
</MainContent>
</>
)}
Expand Down
56 changes: 41 additions & 15 deletions packages/admin/cms-admin/src/generator/future/generateForm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { IntrospectionQuery } from "graphql";

import { generateFormLayout } from "./generateForm/generateFormLayout";
import { getForwardedGqlArgs } from "./generateForm/getForwardedGqlArgs";
import { generateFormField } from "./generateFormField";
import { FormConfig, FormFieldConfig, GeneratorReturn } from "./generator";
import { FormConfig, FormFieldConfig, GeneratorReturn, isFormFieldConfig, isFormLayoutConfig } from "./generator";
import { findMutationTypeOrThrow } from "./utils/findMutationType";
import { generateImportsCode, Imports } from "./utils/generateImportsCode";
import { isFieldOptional } from "./utils/isFieldOptional";
Expand Down Expand Up @@ -40,14 +41,24 @@ export function generateForm(

const createMutationType = addMode && findMutationTypeOrThrow(config.createMutation ?? `create${gqlType}`, gqlIntrospection);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formFields = config.fields.reduce<FormFieldConfig<any>[]>((acc, field) => {
if (isFormLayoutConfig(field)) {
acc.push(...field.fields);
} else if (isFormFieldConfig(field)) {
acc.push(field);
}
return acc;
}, []);

const gqlArgs: ReturnType<typeof getForwardedGqlArgs>["gqlArgs"] = [];
if (createMutationType) {
const {
imports: forwardedGqlArgsImports,
props: forwardedGqlArgsProps,
gqlArgs: forwardedGqlArgs,
} = getForwardedGqlArgs({
fields: config.fields,
fields: formFields,
gqlOperation: createMutationType,
gqlIntrospection,
});
Expand All @@ -66,7 +77,7 @@ export function generateForm(

const { formPropsTypeCode, formPropsParamsCode } = generateFormPropsCode(props);

const rootBlockFields = config.fields
const rootBlockFields = formFields
.filter((field) => field.type == "block")
.map((field) => {
// map is for ts to infer block type correctly
Expand All @@ -80,10 +91,10 @@ export function generateForm(
});
});

const numberFields = config.fields.filter((field) => field.type == "number");
const booleanFields = config.fields.filter((field) => field.type == "boolean");
const dateFields = config.fields.filter((field) => field.type == "date");
const readOnlyFields = config.fields.filter((field) => field.readOnly);
const numberFields = formFields.filter((field) => field.type == "number");
const booleanFields = formFields.filter((field) => field.type == "boolean");
const dateFields = formFields.filter((field) => field.type == "date");
const readOnlyFields = formFields.filter((field) => field.readOnly);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isOptional = (fieldConfig: FormFieldConfig<any>) => {
Expand All @@ -93,18 +104,33 @@ export function generateForm(
let hooksCode = "";
let formValueToGqlInputCode = "";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const generateFormFieldCode = (field: FormFieldConfig<any>) => {
const generated = generateFormField({ gqlIntrospection, baseOutputFilename }, field, config);
for (const name in generated.gqlDocuments) {
gqlDocuments[name] = generated.gqlDocuments[name];
}
imports.push(...generated.imports);
hooksCode += generated.hooksCode;
formValueToGqlInputCode += generated.formValueToGqlInputCode;
formFragmentFields.push(generated.formFragmentField);
return generated.code;
};

const formFragmentFields: string[] = [];
const fieldsCode = config.fields
.map((field) => {
const generated = generateFormField({ gqlIntrospection, baseOutputFilename }, field, config);
for (const name in generated.gqlDocuments) {
gqlDocuments[name] = generated.gqlDocuments[name];
if (isFormFieldConfig(field)) {
return generateFormFieldCode(field);
} else if (isFormLayoutConfig(field)) {
const formLayoutFieldsCode = field.fields.map((field) => generateFormFieldCode(field)).join("\n");
const generated = generateFormLayout(field, formLayoutFieldsCode, config);
imports.push(...generated.imports);
formFragmentFields.push(...generated.formFragmentFields);
return generated.code;
} else {
throw new Error("Not supported config");
}
imports.push(...generated.imports);
hooksCode += generated.hooksCode;
formValueToGqlInputCode += generated.formValueToGqlInputCode;
formFragmentFields.push(generated.formFragmentField);
return generated.code;
})
.join("\n");

Expand Down
Loading

0 comments on commit f9ac3fd

Please sign in to comment.