From 80d3c0ed81380fe36849d6baffd0ba4dca8bd6e5 Mon Sep 17 00:00:00 2001 From: swaysway <7465495+SwaySway@users.noreply.github.com> Date: Thu, 7 Jul 2022 23:44:51 +0000 Subject: [PATCH] feat: add react-studio-form-renderer --- ...studio-ui-codegen-react-forms.test.ts.snap | 440 ++++++++++++++++++ .../form-renderer-helper.test.ts.snap | 21 + .../react-forms/form-renderer-helper.test.ts | 76 +++ .../studio-ui-codegen-react-forms.test.ts | 78 ++++ .../amplify-form-renderer.ts | 131 +++--- .../lib/amplify-ui-renderers/form.ts | 16 +- .../lib/forms/form-renderer-helper.ts | 288 ++++++++++++ packages/codegen-ui-react/lib/forms/index.ts | 17 + .../lib/forms/react-form-renderer.ts | 324 +++++++++++++ packages/codegen-ui-react/lib/index.ts | 1 + .../lib/react-studio-template-renderer.ts | 2 +- .../example-schemas/datastore/post.json | 81 ++++ .../forms/post-custom-create.json | 30 ++ .../forms/post-datastore-create.json | 11 + .../forms/post-datastore-update.json | 11 + .../form-to-component.test.ts | 20 +- .../helpers/datastore-model.test.ts | 21 +- .../form-to-component.ts | 166 +++---- .../generate-form-definition.ts | 6 +- .../helpers/datastore-model.ts | 4 +- .../lib/generate-form-definition/index.ts | 2 +- packages/codegen-ui/lib/types/data.ts | 17 +- .../lib/types/form/form-definition.ts | 1 + .../lib/types/form/form-metadata.ts | 29 ++ packages/codegen-ui/lib/types/form/index.ts | 5 +- packages/codegen-ui/lib/types/form/style.ts | 6 +- .../lib/utils/component-metadata.ts | 2 + .../lib/utils/form-component-metadata.ts | 34 ++ packages/codegen-ui/lib/utils/index.ts | 1 + packages/codegen-ui/lib/validation-helper.ts | 17 + 30 files changed, 1644 insertions(+), 214 deletions(-) create mode 100644 packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts create mode 100644 packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts create mode 100644 packages/codegen-ui-react/lib/forms/form-renderer-helper.ts create mode 100644 packages/codegen-ui-react/lib/forms/index.ts create mode 100644 packages/codegen-ui-react/lib/forms/react-form-renderer.ts create mode 100644 packages/codegen-ui/example-schemas/datastore/post.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-custom-create.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-datastore-create.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-datastore-update.json create mode 100644 packages/codegen-ui/lib/types/form/form-metadata.ts create mode 100644 packages/codegen-ui/lib/utils/form-component-metadata.ts diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap new file mode 100644 index 000000000..6dc098c1a --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap @@ -0,0 +1,440 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`amplify form renderer tests custom form tests should render a custom backed form 1`] = ` +"/* eslint-disable */ +import React from \\"react\\"; +import { + getOverrideProps, + useStateMutationAction, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Button, Flex, Grid, TextField } from \\"@aws-amplify/ui-react\\"; +export default function customDataForm(props) { + const { + onSubmit: customDataFormOnSubmit, + onCancel, + overrides, + ...rest + } = props; + const [customDataFormFields, setCustomDataFormFields] = + useStateMutationAction({}); + return ( +
{ + event.preventDefault(); + customDataFormOnSubmit(customDataFormFields); + }} + {...rest} + {...getOverrideProps(overrides, \\"customDataForm\\")} + > + + + + + + + + + + + + + + + +
+ ); +} +" +`; + +exports[`amplify form renderer tests custom form tests should render a custom backed form 2`] = ` +"import React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +export declare type customDataFormProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +} & { + onSubmit: (fields: Record) => void; + onCancel?: () => void; +}>; +export default function customDataForm(props: customDataFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a create form 1`] = ` +"/* eslint-disable */ +import React from \\"react\\"; +import { + getOverrideProps, + useDataStoreCreateAction, + useStateMutationAction, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Post } from \\"../models\\"; +import { schema } from \\"../models/schema\\"; +import { Button, Flex, Grid, TextField } from \\"@aws-amplify/ui-react\\"; +export default function myPostForm(props) { + const { onSubmitBefore, onSubmitComplete, onCancel, overrides, ...rest } = + props; + const [myPostFormFields, setMyPostFormFields] = useStateMutationAction({}); + const myPostFormOnSubmit = useDataStoreCreateAction({ + model: Post, + fields: myPostFormFields, + schema: schema, + }); + return ( +
{ + event.preventDefault(); + myPostFormOnSubmit(); + }} + {...rest} + {...getOverrideProps(overrides, \\"myPostForm\\")} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a create form 2`] = ` +"import React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +export declare type myPostFormProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +} & { + onSubmitBefore?: (fields: Record) => Record; + onSubmitComplete?: ({ saveSuccessful, errorMessage }: { + saveSuccessful: string; + errorMessage?: string; + }) => void; + onCancel?: () => void; +}>; +export default function myPostForm(props: myPostFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a update form 1`] = ` +"/* eslint-disable */ +import React from \\"react\\"; +import { + getOverrideProps, + useDataStoreUpdateAction, + useStateMutationAction, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Post } from \\"../models\\"; +import { schema } from \\"../models/schema\\"; +import { Button, Flex, Grid, TextField } from \\"@aws-amplify/ui-react\\"; +export default function myPostForm(props) { + const { id, onSubmitBefore, onSubmitComplete, onCancel, overrides, ...rest } = + props; + const [myPostFormFields, setMyPostFormFields] = useStateMutationAction({}); + const myPostFormOnSubmit = useDataStoreUpdateAction({ + model: Post, + fields: myPostFormFields, + id: id, + schema: schema, + }); + return ( +
{ + event.preventDefault(); + myPostFormOnSubmit(); + }} + {...rest} + {...getOverrideProps(overrides, \\"myPostForm\\")} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a update form 2`] = ` +"import React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +export declare type myPostFormProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +} & { + id: string; + onSubmitBefore?: (fields: Record) => Record; + onSubmitComplete?: ({ saveSuccessful, errorMessage }: { + saveSuccessful: string; + errorMessage?: string; + }) => void; + onCancel?: () => void; +}>; +export default function myPostForm(props: myPostFormProps): React.ReactElement; +" +`; diff --git a/packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap new file mode 100644 index 000000000..2632d9563 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`form-render utils should generate a datastore function 1`] = `"const mySampleFormOnSubmit = useDataStoreCreateAction({ model: Post, fields: mySampleFormFields, schema: schema });"`; + +exports[`form-render utils should generate before & complete types if datastore config is set 1`] = ` +"{ + onSubmitBefore?: (fields: Record) => Record; + onSubmitComplete?: ({ saveSuccessful, errorMessage }: { + saveSuccessful: string; + errorMessage?: string; + }) => void; + onCancel?: () => void; +}" +`; + +exports[`form-render utils should generate regular onsubmit if dataSourceType is custom 1`] = ` +"{ + onSubmit: (fields: Record) => void; + onCancel?: () => void; +}" +`; diff --git a/packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts b/packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts new file mode 100644 index 000000000..0507d52b4 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts @@ -0,0 +1,76 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { StudioForm } from '@aws-amplify/codegen-ui'; +import { EmitHint, Node } from 'typescript'; +import { ImportCollection } from '../../imports'; +import { buildFormPropNode, buildDataStoreActionStatement } from '../../forms'; +import { buildPrinter, defaultRenderConfig } from '../../react-studio-template-renderer-helper'; + +describe('form-render utils', () => { + let printNode: (node: Node) => string; + + beforeAll(() => { + const { printer, file } = buildPrinter('myFileMock', defaultRenderConfig); + printNode = (node: Node) => { + return printer.printNode(EmitHint.Unspecified, node, file); + }; + }); + + it('should generate a datastore function', () => { + const form: StudioForm = { + name: 'mySampleForm', + formActionType: 'create', + dataType: { dataSourceType: 'DataStore', dataTypeName: 'Post' }, + fields: {}, + sectionalElements: {}, + style: {}, + }; + const importCollection = new ImportCollection(); + const mutationStatement: any = buildDataStoreActionStatement(form, importCollection); + expect(mutationStatement).toBeDefined(); + const node = printNode(mutationStatement); + expect(node).toMatchSnapshot(); + }); + + it('should generate before & complete types if datastore config is set', () => { + const form: StudioForm = { + name: 'mySampleForm', + formActionType: 'create', + dataType: { dataSourceType: 'DataStore', dataTypeName: 'Post' }, + fields: {}, + sectionalElements: {}, + style: {}, + }; + + const propSignatures = buildFormPropNode(form); + const node = printNode(propSignatures); + expect(node).toMatchSnapshot(); + }); + + it('should generate regular onsubmit if dataSourceType is custom', () => { + const form: StudioForm = { + name: 'myCustomForm', + formActionType: 'create', + dataType: { dataSourceType: 'Custom', dataTypeName: 'Custom' }, + fields: {}, + sectionalElements: {}, + style: {}, + }; + const propSignatures = buildFormPropNode(form); + const node = printNode(propSignatures); + expect(node).toMatchSnapshot(); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts new file mode 100644 index 000000000..a9fa082b1 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts @@ -0,0 +1,78 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { StudioTemplateRendererFactory } from '@aws-amplify/codegen-ui/lib/template-renderer-factory'; +import { DataStoreModelInfo, StudioForm, SchemaModel } from '@aws-amplify/codegen-ui'; +import { ReactRenderConfig, AmplifyFormRenderer, ModuleKind, ScriptKind, ScriptTarget } from '..'; +import { loadSchemaFromJSONFile } from './__utils__'; + +export const defaultCLIRenderConfig: ReactRenderConfig = { + module: ModuleKind.ES2020, + target: ScriptTarget.ES2020, + script: ScriptKind.JSX, + renderTypeDeclarations: true, +}; + +export const generateWithAmplifyFormRenderer = ( + formJsonFile: string, + dataSchemaJsonFile: string | undefined, + renderConfig: ReactRenderConfig = defaultCLIRenderConfig, +): { componentText: string; declaration?: string } => { + let dataStoreInfo: DataStoreModelInfo | undefined; + if (dataSchemaJsonFile) { + const { fields } = loadSchemaFromJSONFile(dataSchemaJsonFile); + dataStoreInfo = { fields: Object.values(fields).map((value) => value) }; + } + const rendererFactory = new StudioTemplateRendererFactory( + (component: StudioForm) => new AmplifyFormRenderer(component, dataStoreInfo, renderConfig), + ); + + const renderer = rendererFactory.buildRenderer(loadSchemaFromJSONFile(formJsonFile)); + return renderer.renderComponent(); +}; + +describe('amplify form renderer tests', () => { + describe('datastore form tests', () => { + it('should generate a create form', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/post-datastore-create', + 'datastore/post', + ); + expect(componentText).toContain('useDataStoreCreateAction'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should generate a update form', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/post-datastore-update', + 'datastore/post', + ); + expect(componentText).toContain('useDataStoreUpdateAction'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + }); + + describe('custom form tests', () => { + it('should render a custom backed form', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/post-custom-create', undefined); + expect(componentText).toContain('customDataFormOnSubmit(customDataFormFields)'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts index eb5cfed50..a5a185488 100644 --- a/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts @@ -13,15 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - StudioNode, - StudioComponent, - StudioComponentChild, - mapFormToComponent, - SchemaModel, - StudioForm, -} from '@aws-amplify/codegen-ui'; +import { StudioNode, StudioComponent, StudioComponentChild } from '@aws-amplify/codegen-ui'; import { JsxElement, JsxFragment, JsxSelfClosingElement } from 'typescript'; + // add primitives in alphabetical order import { AlertProps, @@ -72,34 +66,24 @@ import { TextProps, } from '@aws-amplify/ui-react'; import { Primitive } from '../primitive'; -import { ReactStudioTemplateRenderer } from '../react-studio-template-renderer'; import CustomComponentRenderer from './customComponent'; import FormRenderer from './form'; import { ReactComponentRenderer } from '../react-component-renderer'; -import { ReactRenderConfig } from '../react-render-config'; - -export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { - protected form: StudioForm; - - constructor(form: StudioForm, modelSchema: SchemaModel, renderConfig: ReactRenderConfig) { - const component = mapFormToComponent(form, modelSchema); - super(component, renderConfig); - this.form = form; - // TODO: update metadata with form definition (either here or in render element) - } +import { ReactFormTemplateRenderer } from '../forms'; +export class AmplifyFormRenderer extends ReactFormTemplateRenderer { renderJsx( - component: StudioComponent | StudioComponentChild, + formComponent: StudioComponent | StudioComponentChild, parent?: StudioNode, ): JsxElement | JsxFragment | JsxSelfClosingElement { - const node = new StudioNode(component, parent); + const node = new StudioNode(formComponent, parent); const renderChildren = (children: StudioComponentChild[]) => children.map((child) => this.renderJsx(child, node)); // add Primitive in alphabetical order - switch (component.componentType) { + switch (formComponent.componentType) { case Primitive.Alert: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -107,7 +91,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Badge: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -115,7 +99,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Button: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -123,7 +107,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ButtonGroup: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -131,7 +115,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Card: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -139,7 +123,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.CheckboxField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -147,8 +131,9 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case 'form': return new FormRenderer( - component, - this.form, + formComponent, + // this component is the current form + this.component, this.componentMetadata, this.importCollection, parent, @@ -156,7 +141,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Divider: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -164,7 +149,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Expander: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -172,7 +157,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ExpanderItem: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -180,7 +165,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Flex: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -188,7 +173,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Grid: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -196,7 +181,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Heading: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -204,7 +189,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Icon: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -212,7 +197,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Image: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -220,7 +205,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Link: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -228,7 +213,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Loader: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -236,7 +221,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.MenuButton: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -244,7 +229,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.MenuItem: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -252,7 +237,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Menu: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -260,7 +245,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Pagination: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -268,7 +253,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.PasswordField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -276,7 +261,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.PhoneNumberField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -284,7 +269,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Placeholder: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -292,7 +277,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Radio: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -300,7 +285,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.RadioGroupField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -308,7 +293,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Rating: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -316,7 +301,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ScrollView: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -324,7 +309,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SearchField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -332,7 +317,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SelectField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -340,7 +325,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SliderField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -348,7 +333,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.StepperField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -356,7 +341,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SwitchField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -364,7 +349,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TabItem: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -372,7 +357,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Tabs: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -380,7 +365,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Table: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -388,7 +373,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableBody: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -396,7 +381,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableCell: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -404,7 +389,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableFoot: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -412,7 +397,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableHead: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -420,7 +405,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableRow: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -428,7 +413,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Text: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -436,7 +421,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TextAreaField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -444,7 +429,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TextField: return new ReactComponentRenderer>( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -452,7 +437,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ToggleButton: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -460,7 +445,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ToggleButtonGroup: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -468,7 +453,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.View: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -476,7 +461,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.VisuallyHidden: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -484,7 +469,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { default: return new CustomComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts index c758e2665..d1dc844b3 100644 --- a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts @@ -16,6 +16,7 @@ import { BaseComponentProps } from '@aws-amplify/ui-react'; import { ComponentMetadata, + getFormFieldStateName, StudioComponent, StudioComponentChild, StudioForm, @@ -70,12 +71,16 @@ export default class FormRenderer extends ReactComponentRenderer = { + create: 'Amplify.DataStoreCreateItemAction', + update: 'Amplify.DataStoreUpdateItemAction', +}; + +export const FieldStateVariable = (componentName: string): StateStudioComponentProperty => ({ + componentName, + property: 'fields', +}); + +/** + * - formFields + */ +export const buildFieldStateStatements = (formName: string, importCollection: ImportCollection) => { + importCollection.addMappedImport(ImportValue.USE_STATE_MUTATION_ACTION); + + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createArrayBindingPattern([ + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier(getStateName(FieldStateVariable(formName))), + undefined, + ), + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier(getSetStateName(FieldStateVariable(formName))), + undefined, + ), + ]), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('useStateMutationAction'), undefined, [ + factory.createObjectLiteralExpression(), + ]), + ), + ], + NodeFlags.Const, + ), + ); +}; +/** + * + * @param form StudioForm + * @param importCollection ImportCollection + * @returns ActionStatement + * renders the state variable for datastore and adds imports + * + * ex. for form create + * const myFormonSubmit = useDataStoreCreateAction({ + * model: myModel, + * fields: myFormFields, + * schema: schema + * }); + */ +export const buildDataStoreActionStatement = (form: StudioForm, importCollection: ImportCollection) => { + const { + dataType: { dataTypeName }, + formActionType, + } = form; + const actionHookImportValue = getActionHookImportValue(FormTypeDataStoreMap[formActionType]); + importCollection.addMappedImport(actionHookImportValue); + importCollection.addImport(ImportSource.LOCAL_MODELS, dataTypeName); + const properties = [ + // model name + factory.createPropertyAssignment(factory.createIdentifier('model'), factory.createIdentifier(dataTypeName)), + // fields object name + factory.createPropertyAssignment( + factory.createIdentifier('fields'), + factory.createIdentifier(getStateName(FieldStateVariable(form.name))), + ), + ]; + if (formActionType === 'update') { + properties.push(factory.createPropertyAssignment(factory.createIdentifier('id'), factory.createIdentifier('id'))); + } + addSchemaToArguments(properties, importCollection); + + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(getActionIdentifier(form.name, 'onSubmit')), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier(actionHookImportValue), undefined, [ + factory.createObjectLiteralExpression(properties, false), + ]), + ), + ], + NodeFlags.Const, + ), + ); +}; + +export const buildMutationBindings = (form: StudioForm) => { + const { + dataType: { dataSourceType }, + formActionType, + } = form; + const elements: BindingElement[] = []; + if (dataSourceType === 'DataStore') { + if (formActionType === 'update') { + elements.push(factory.createBindingElement(undefined, undefined, factory.createIdentifier('id'), undefined)); + } + elements.push( + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onSubmitBefore'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onSubmitComplete'), undefined), + ); + } else { + elements.push( + factory.createBindingElement( + undefined, + factory.createIdentifier('onSubmit'), + getActionIdentifier(form.name, 'onSubmit'), + undefined, + ), + ); + } + elements.push(factory.createBindingElement(undefined, undefined, factory.createIdentifier('onCancel'), undefined)); + return elements; +}; + +/* + generate params in typed props + - datastore (onSubmitBefore(fields) & onSubmitComplete({saveSuccessful, errorMessage})) + - if update include id + - custom (onSubmit(fields)) + */ +export const buildFormPropNode = (form: StudioForm) => { + const { + dataType: { dataSourceType }, + formActionType, + } = form; + const propSignatures: PropertySignature[] = []; + if (dataSourceType === 'DataStore') { + if (formActionType === 'update') { + propSignatures.push( + factory.createPropertySignature( + undefined, + factory.createIdentifier('id'), + undefined, + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ), + ); + } + propSignatures.push( + factory.createPropertySignature( + undefined, + 'onSubmitBefore', + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'fields', + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('Record'), [ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]), + undefined, + ), + ], + factory.createTypeReferenceNode(factory.createIdentifier('Record'), [ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]), + ), + ), + factory.createPropertySignature( + undefined, + 'onSubmitComplete', + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createObjectBindingPattern([ + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier('saveSuccessful'), + undefined, + ), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('errorMessage'), undefined), + ]), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + 'saveSuccessful', + undefined, + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ), + factory.createPropertySignature( + undefined, + 'errorMessage', + factory.createToken(SyntaxKind.QuestionToken), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ), + ]), + undefined, + ), + ], + factory.createKeywordTypeNode(SyntaxKind.VoidKeyword), + ), + ), + ); + } + if (dataSourceType === 'Custom') { + propSignatures.push( + factory.createPropertySignature( + undefined, + 'onSubmit', + undefined, + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'fields', + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('Record'), [ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]), + undefined, + ), + ], + factory.createKeywordTypeNode(SyntaxKind.VoidKeyword), + ), + ), + ); + } + // onCancel?: () => void + propSignatures.push( + factory.createPropertySignature( + undefined, + 'onCancel', + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode(undefined, [], factory.createKeywordTypeNode(SyntaxKind.VoidKeyword)), + ), + ); + return factory.createTypeLiteralNode(propSignatures); +}; + +/** + * TODO + * - form valid boolean + * - error objects { hasErrror: boolean, errorMessage: string } + */ +export const buildValidationStateStatements = () => {}; diff --git a/packages/codegen-ui-react/lib/forms/index.ts b/packages/codegen-ui-react/lib/forms/index.ts new file mode 100644 index 000000000..f85f82d36 --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/index.ts @@ -0,0 +1,17 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +export * from './form-renderer-helper'; +export * from './react-form-renderer'; diff --git a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts new file mode 100644 index 000000000..891843f2a --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -0,0 +1,324 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentMetadata, + computeComponentMetadata, + DataStoreModelInfo, + FormDefinition, + generateFormDefinition, + handleCodegenErrors, + mapFormDefinitionToComponent, + mapFormMetadata, + StudioComponent, + StudioForm, + StudioNode, + StudioTemplateRenderer, + validateFormSchema, +} from '@aws-amplify/codegen-ui'; +import { EOL } from 'os'; +import { + addSyntheticLeadingComment, + BindingElement, + EmitHint, + factory, + FunctionDeclaration, + JsxElement, + JsxFragment, + JsxSelfClosingElement, + Modifier, + NodeFlags, + ScriptKind, + Statement, + SyntaxKind, + TypeAliasDeclaration, +} from 'typescript'; +import { ImportCollection, ImportValue } from '../imports'; +import { PrimitiveTypeParameter, Primitive } from '../primitive'; +import { getComponentPropName } from '../react-component-render-helper'; +import { ReactOutputManager } from '../react-output-manager'; +import { ReactRenderConfig, scriptKindToFileExtension } from '../react-render-config'; +import { + buildPrinter, + defaultRenderConfig, + getDeclarationFilename, + transpile, +} from '../react-studio-template-renderer-helper'; +import { RequiredKeys } from '../utils/type-utils'; +import { + buildFieldStateStatements, + buildFormPropNode, + buildDataStoreActionStatement, + buildMutationBindings, +} from './form-renderer-helper'; + +export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< + string, + StudioForm, + ReactOutputManager, + { + componentText: string; + renderComponentToFilesystem: (outputPath: string) => Promise; + } +> { + protected importCollection = new ImportCollection(); + + protected renderConfig: RequiredKeys; + + protected formDefinition: FormDefinition; + + protected formComponent: StudioComponent; + + protected componentMetadata: ComponentMetadata; + + public fileName: string; + + /* + TODO: Change to use generic dataschema + */ + constructor(component: StudioForm, modelInfo: DataStoreModelInfo | undefined, renderConfig: ReactRenderConfig) { + super(component, new ReactOutputManager(), renderConfig); + this.renderConfig = { + ...defaultRenderConfig, + ...renderConfig, + }; + // the super class creates a component aka form which is what we pass in this extended implmentation + this.fileName = `${this.component.name}.${scriptKindToFileExtension(this.renderConfig.script)}`; + + this.formDefinition = generateFormDefinition({ form: component, modelInfo }); + + // create a studio component which will represent the structure of the form + this.formComponent = mapFormDefinitionToComponent(this.component.name, this.formDefinition); + + this.componentMetadata = computeComponentMetadata(this.formComponent); + this.componentMetadata.formMetadata = mapFormMetadata(this.component, this.formDefinition); + } + + @handleCodegenErrors + renderComponentOnly() { + const variableStatements = this.buildVariableStatements(); + const jsx = this.renderJsx(this.formComponent); + const requiredDataModels = []; + + const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + + const imports = this.importCollection.buildImportStatements(); + + let importsText = ''; + + imports.forEach((importStatement) => { + const result = printer.printNode(EmitHint.Unspecified, importStatement, file); + importsText += result + EOL; + }); + + const wrappedFunction = this.renderFunctionWrapper(this.component.name, variableStatements, jsx, false); + + const result = printer.printNode(EmitHint.Unspecified, wrappedFunction, file); + + // do not produce declaration becuase it is not used + const { componentText: compText } = transpile(result, { ...this.renderConfig, renderTypeDeclarations: false }); + + if (this.component.dataType.dataSourceType === 'DataStore') { + requiredDataModels.push(this.component.dataType.dataTypeName); + // TODO: require other models if form is handling querying relational models + } + + return { compText, importsText, requiredDataModels }; + } + + renderComponentInternal() { + const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + + // build form related variable statments + const variableStatements = this.buildVariableStatements(); + const jsx = this.renderJsx(this.formComponent); + + const wrappedFunction = this.renderFunctionWrapper(this.component.name, variableStatements, jsx, true); + const propsDeclaration = this.renderBindingPropsType(); + + const imports = this.importCollection.buildImportStatements(); + + let componentText = `/* eslint-disable */${EOL}`; + + imports.forEach((importStatement) => { + const result = printer.printNode(EmitHint.Unspecified, importStatement, file); + componentText += result + EOL; + }); + + componentText += EOL; + + const propsPrinted = printer.printNode(EmitHint.Unspecified, propsDeclaration, file); + componentText += propsPrinted; + + const result = printer.printNode(EmitHint.Unspecified, wrappedFunction, file); + componentText += result; + + const { componentText: transpiledComponentText, declaration } = transpile(componentText, this.renderConfig); + + return { + componentText: transpiledComponentText, + declaration, + renderComponentToFilesystem: async (outputPath: string) => { + await this.renderComponentToFilesystem(transpiledComponentText)(this.fileName)(outputPath); + if (declaration) { + await this.renderComponentToFilesystem(declaration)(getDeclarationFilename(this.fileName))(outputPath); + } + }, + }; + } + + renderFunctionWrapper( + componentName: string, + variableStatements: Statement[], + jsx: JsxElement | JsxFragment | JsxSelfClosingElement, + renderExport: boolean, + ): FunctionDeclaration { + const componentPropType = getComponentPropName(componentName); + const jsxStatement = factory.createReturnStatement( + factory.createParenthesizedExpression( + this.renderConfig.script !== ScriptKind.TSX + ? jsx + : /* add ts-ignore comment above jsx statement. Generated props are incompatible with amplify-ui props */ + addSyntheticLeadingComment( + factory.createParenthesizedExpression(jsx), + SyntaxKind.MultiLineCommentTrivia, + ' @ts-ignore: TS2322 ', + true, + ), + ), + ); + const codeBlockContent = variableStatements.concat([jsxStatement]); + const modifiers: Modifier[] = renderExport + ? [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DefaultKeyword)] + : []; + const typeParameter = PrimitiveTypeParameter[Primitive[this.formComponent?.componentType as Primitive]]; + // only use type parameter reference if one was declared + const typeParameterReference = typeParameter && typeParameter.declaration() ? typeParameter.reference() : undefined; + return factory.createFunctionDeclaration( + undefined, + modifiers, + undefined, + factory.createIdentifier(componentName), + typeParameter ? typeParameter.declaration() : undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'props', + undefined, + factory.createTypeReferenceNode(componentPropType, typeParameterReference), + undefined, + ), + ], + factory.createTypeReferenceNode( + factory.createQualifiedName(factory.createIdentifier('React'), factory.createIdentifier('ReactElement')), + undefined, + ), + factory.createBlock(codeBlockContent, true), + ); + } + + abstract renderJsx(component: StudioComponent, parent?: StudioNode): JsxElement | JsxFragment | JsxSelfClosingElement; + + private renderBindingPropsType(): TypeAliasDeclaration { + const escapeHatchTypeNode = factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('overrides'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(factory.createIdentifier('EscapeHatchProps'), undefined), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + factory.createLiteralTypeNode(factory.createNull()), + ]), + ), + ]); + const formPropType = getComponentPropName(this.component.name); + + this.importCollection.addMappedImport(ImportValue.ESCAPE_HATCH_PROPS); + + return factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword)], + factory.createIdentifier(formPropType), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('React.PropsWithChildren'), [ + factory.createIntersectionTypeNode([escapeHatchTypeNode, buildFormPropNode(this.component)]), + ]), + ); + } + + /** + * Variable Statements need for forms + * - props passed into form component + * - useState + * - form fields + * - valid state for form + * - error object { hasErrror: boolean, errorMessage: string } + * - datastore operation (conditional if form is backed by datastore) + * - this is the datastore mutation function which will be used by the helpers + */ + private buildVariableStatements() { + const statements: Statement[] = []; + const elements: BindingElement[] = []; + + // add in hooks for before/complete with ds and basic onSubmit with props + elements.push(...buildMutationBindings(this.component)); + + // overrides + elements.push(factory.createBindingElement(undefined, undefined, factory.createIdentifier('overrides'), undefined)); + + // get rest of props to pass to top level component + elements.push( + factory.createBindingElement( + factory.createToken(SyntaxKind.DotDotDotToken), + undefined, + factory.createIdentifier('rest'), + undefined, + ), + ); + + // add binding elments to statements + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createObjectBindingPattern(elements), + undefined, + undefined, + factory.createIdentifier('props'), + ), + ], + NodeFlags.Const, + ), + ), + ); + + statements.push(buildFieldStateStatements(this.component.name, this.importCollection)); + // build data source action + if (this.component.dataType.dataSourceType === 'DataStore') { + statements.push(buildDataStoreActionStatement(this.component, this.importCollection)); + } + return statements; + } + + protected validateSchema(component: StudioForm): void { + validateFormSchema(component); + } +} diff --git a/packages/codegen-ui-react/lib/index.ts b/packages/codegen-ui-react/lib/index.ts index 762909345..17ef40979 100644 --- a/packages/codegen-ui-react/lib/index.ts +++ b/packages/codegen-ui-react/lib/index.ts @@ -21,6 +21,7 @@ export * from './react-output-config'; export * from './react-render-config'; export * from './react-output-manager'; export * from './amplify-ui-renderers/amplify-renderer'; +export * from './amplify-ui-renderers/amplify-form-renderer'; export * from './primitive'; export * from './react-index-studio-template-renderer'; export * from './react-required-dependency-provider'; diff --git a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts index ef4df80fd..6e427df15 100644 --- a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts @@ -516,7 +516,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer return factory.createTypeLiteralNode(propSignatures); } - private buildVariableStatements(component: StudioComponent): Statement[] { + protected buildVariableStatements(component: StudioComponent): Statement[] { const statements: Statement[] = []; const elements: BindingElement[] = []; if (isStudioComponentWithBinding(component)) { diff --git a/packages/codegen-ui/example-schemas/datastore/post.json b/packages/codegen-ui/example-schemas/datastore/post.json new file mode 100644 index 000000000..f16d9e19f --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/post.json @@ -0,0 +1,81 @@ +{ + "name": "Post", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "caption": { + "name": "caption", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "username": { + "name": "username", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "post_url": { + "name": "post_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "profile_url": { + "name": "profile_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "Posts", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "private", + "provider": "iam", + "operations": [ + "create", + "update", + "delete", + "read" + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-custom-create.json b/packages/codegen-ui/example-schemas/forms/post-custom-create.json new file mode 100644 index 000000000..1efa396de --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-custom-create.json @@ -0,0 +1,30 @@ +{ + "name": "customDataForm", + "formActionType": "create", + "dataType": { + "dataSourceType": "Custom", + "dataTypeName": "Post" + }, + "fields": { + "name": { + "inputType": { + "required": true, + "type": "TextField", + "name": "name", + "defaultValue": "John Doe" + }, + "label": "name" + }, + "email": { + "inputType": { + "required": true, + "type": "TextField", + "name": "email", + "defaultValue": "johndoe@amplify.com" + }, + "label": "E-mail" + } + }, + "sectionalElements": {}, + "style": {} +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-datastore-create.json b/packages/codegen-ui/example-schemas/forms/post-datastore-create.json new file mode 100644 index 000000000..337ccf865 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-datastore-create.json @@ -0,0 +1,11 @@ +{ + "name": "myPostForm", + "formActionType": "create", + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "Post" + }, + "fields": {}, + "sectionalElements": {}, + "style": {} +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-datastore-update.json b/packages/codegen-ui/example-schemas/forms/post-datastore-update.json new file mode 100644 index 000000000..7372f78d4 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-datastore-update.json @@ -0,0 +1,11 @@ +{ + "name": "myPostForm", + "formActionType": "update", + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "Post" + }, + "fields": {}, + "sectionalElements": {}, + "style": {} +} \ No newline at end of file diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts index c15f04e22..6bf1f4b1a 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts @@ -13,8 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { postSchema } from '../__utils__/mock-schemas'; -import { mapFormToComponent } from '../../generate-form-definition/form-to-component'; +// import { postSchema } from '../__utils__/mock-schemas'; import { StudioForm } from '../../types'; describe('formToComponent', () => { @@ -28,21 +27,6 @@ describe('formToComponent', () => { style: {}, }; - // shallow test of mapper - const component = mapFormToComponent(myForm, postSchema.models.Post); - expect(component).toBeDefined(); - expect(component.children).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: 'mySampleFormGrid', - componentType: 'Grid', - properties: expect.objectContaining({ - columnGap: { value: '1rem' }, - rowGap: { value: '1rem' }, - }), - children: expect.any(Array), - }), - ]), - ); + expect(myForm).toBeDefined(); }); }); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts index d7a5e5546..1c74e89e5 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts @@ -15,7 +15,7 @@ */ import { addDataStoreModelField } from '../../../generate-form-definition/helpers'; -import { FormDefinition, ModelFieldsConfigs } from '../../../types'; +import { FormDefinition, ModelFieldsConfigs, ModelField } from '../../../types'; describe('addDataStoreModelField', () => { it('should map to elementMatrix and add to modelFieldsConfigs', () => { @@ -26,7 +26,13 @@ describe('addDataStoreModelField', () => { elementMatrix: [], }; - const dataStoreModelField = { name: 'name', type: 'String', isReadOnly: false, isRequired: false, isArray: false }; + const dataStoreModelField: ModelField = { + name: 'name', + type: 'String', + isReadOnly: false, + isRequired: false, + isArray: false, + }; const modelFieldsConfigs: ModelFieldsConfigs = {}; @@ -47,7 +53,13 @@ describe('addDataStoreModelField', () => { elementMatrix: [], }; - const dataStoreModelField = { name: 'name', type: 'String', isReadOnly: false, isRequired: false, isArray: true }; + const dataStoreModelField: ModelField = { + name: 'name', + type: 'String', + isReadOnly: false, + isRequired: false, + isArray: true, + }; expect(() => addDataStoreModelField(formDefinition, {}, dataStoreModelField)).toThrow(); }); @@ -68,6 +80,7 @@ describe('addDataStoreModelField', () => { isArray: false, }; - expect(() => addDataStoreModelField(formDefinition, {}, dataStoreModelField)).toThrow(); + // adding any so no TypeError is thrown as opposed to a ValidationError + expect(() => addDataStoreModelField(formDefinition, {}, dataStoreModelField as any)).toThrow(); }); }); diff --git a/packages/codegen-ui/lib/generate-form-definition/form-to-component.ts b/packages/codegen-ui/lib/generate-form-definition/form-to-component.ts index 2e3e0cf7b..9f9220b2e 100644 --- a/packages/codegen-ui/lib/generate-form-definition/form-to-component.ts +++ b/packages/codegen-ui/lib/generate-form-definition/form-to-component.ts @@ -13,66 +13,81 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SchemaModel, ModelFields, isGraphQLScalarType } from '@aws-amplify/datastore'; import { StudioComponent, - StudioForm, StudioComponentChild, - StudioComponentProperty, - DataStoreCreateItemAction, - DataStoreUpdateItemAction, - FixedStudioComponentProperty, + FormDefinition, + FormDefinitionElement, + FormStyleConfig, + StudioComponentProperties, + StudioFormStyle, } from '../types'; -// map the datastore schema fields into form fields -export const mapFieldsToForm = (fields: ModelFields) => { - const formFields: StudioComponentChild[] = []; - Object.entries(fields).forEach(([fieldName, fieldValue]) => { - // TODO: expand studio component child to also support other non text fields - if (isGraphQLScalarType(fieldValue.type) && !fieldValue.isArray) { - formFields.push({ - name: `${fieldName}Field`, - componentType: 'TextField', - properties: { - name: { - value: fieldName, - }, - label: { - value: fieldName, - }, - placeholder: { - value: `${fieldValue.type}`, - }, - ...(fieldValue.isRequired && { - required: { - value: 'true', - type: 'boolean', - }, - }), - }, - }); - } - }); +const getStyleResolvedValue = (config?: FormStyleConfig): string | undefined => { + return config?.value ?? config?.tokenReference; +}; - return formFields; +export const resolveStyles = ( + style: StudioFormStyle, +): Record, string | undefined> => { + return { + verticalGap: getStyleResolvedValue(style.verticalGap), + horizontalGap: getStyleResolvedValue(style.horizontalGap), + outerPadding: getStyleResolvedValue(style.outerPadding), + }; }; -export const mapParentGrid = (name: string, children: StudioComponentChild[] = []): StudioComponentChild => { +export const parentGrid = ( + name: string, + style: StudioFormStyle, + children: StudioComponentChild[], +): StudioComponentChild => { + const { verticalGap, horizontalGap } = resolveStyles(style); return { - name: `${name}Grid`, + name, componentType: 'Grid', properties: { - columnGap: { - value: '1rem', - }, - rowGap: { - value: '1rem', - }, + ...(horizontalGap && { columnGap: { value: horizontalGap } }), + ...(verticalGap && { rowGap: { value: verticalGap } }), }, children, }; }; +const mapFieldElementProps = (element: FormDefinitionElement) => { + const props: StudioComponentProperties = {}; + Object.entries(element.props).forEach(([key, value]) => { + props[key] = { value: `${value}`, type: `${typeof value}` }; + }); + return props; +}; + +export const fieldComponentMapper = (name: string, formDefinition: FormDefinition): StudioComponentChild => { + // will accept a field matrix from a defnition and map + const fieldChildren = formDefinition.elementMatrix.map((row: string[], rowIdx: number) => { + return { + name: `RowGrid${rowIdx}`, + componentType: 'Grid', + properties: { + columnGap: { value: 'inherit' }, + rowGap: { value: 'inherit' }, + ...(row.length > 0 && { + templateColumns: { value: `repeat(${row.length}, auto)` }, + }), + }, + children: row.map((column, colIdx) => { + const element: FormDefinitionElement = formDefinition.elements[column]; + return { + name: `${element.componentType}${colIdx}`, + componentType: element.componentType, + properties: mapFieldElementProps(element), + }; + }), + }; + }); + return parentGrid(`${name}Grid`, formDefinition.form.layoutStyle, fieldChildren); +}; + export const ctaButtonConfig = (): StudioComponentChild => { return { name: 'CTAFlex', @@ -117,7 +132,7 @@ export const ctaButtonConfig = (): StudioComponentChild => { }, { componentType: 'Button', - name: 'onSubmitDataStore', + name: 'SubmitButton', properties: { label: { value: 'Submit', @@ -136,66 +151,17 @@ export const ctaButtonConfig = (): StudioComponentChild => { }; }; -export const mapOnSubmitEvent = ( - form: StudioForm, - childrenFormFields: StudioComponentChild[], -): DataStoreCreateItemAction | DataStoreUpdateItemAction => { - if (form.formActionType === 'create') { - return { - action: 'Amplify.DataStoreCreateItemAction', - parameters: { - model: form.dataType.dataTypeName, - fields: childrenFormFields.reduce( - (prev: { [propertyName: string]: StudioComponentProperty }, { name, properties }) => { - return { - ...prev, - [(properties.name as any).value]: { - componentName: name, - property: 'value', - }, - }; - }, - {}, - ), - }, - } as DataStoreCreateItemAction; - } - /** - * TODO: Read DataStore Spec to find CustomPrimaryKey if not ID - */ - const { value: primaryKey } = childrenFormFields.find( - ({ properties }) => (properties.name as FixedStudioComponentProperty).value === 'id', - )?.properties.name as FixedStudioComponentProperty; - return { - action: 'Amplify.DataStoreUpdateItemAction', - parameters: { - model: form.dataType.dataTypeName, - id: { - value: primaryKey || 'id', - }, - }, - } as DataStoreUpdateItemAction; -}; - -export const mapFormToComponent = (form: StudioForm, dataSchema: SchemaModel): StudioComponent => { - // here we can merge the datastore schema with the form - // right now it's only creating fields from the existing datastore schema - // TODO: manage merging fields from form and datastore - const childrenFormFields = mapFieldsToForm(dataSchema.fields); - +export const mapFormDefinitionToComponent = (name: string, formDefinition: FormDefinition) => { const component: StudioComponent = { - name: form.name, + name, + componentType: 'form', properties: {}, bindingProperties: { onCancel: { type: 'Event' }, }, - events: { - onSubmit: mapOnSubmitEvent(form, childrenFormFields), - }, - // codegen will default to rendering the component with this name - componentType: 'form', - children: [mapParentGrid(form.name, childrenFormFields), ctaButtonConfig()], + events: {}, + // TODO: change cta button config based on formDefinition cta layout + children: [fieldComponentMapper(name, formDefinition), ctaButtonConfig()], }; - return component; }; diff --git a/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts b/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts index e8442044a..09de7a583 100644 --- a/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts +++ b/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts @@ -25,7 +25,7 @@ import { } from './helpers'; import { StudioForm, - DataStoreModelField, + DataStoreModelInfo, SectionalElement, StudioFormFieldConfig, FormDefinition, @@ -39,13 +39,14 @@ import { * @param form StudioForm, converted from the API shape. * @param modelInfo (Optional) holds type information about the DataStore model fields being represented. * @returns a definition that translates to rendered JSX elements. + * TODO: Change to use generic data schema */ export function generateFormDefinition({ form, modelInfo, }: { form: StudioForm; - modelInfo?: { fields: DataStoreModelField[] }; + modelInfo?: DataStoreModelInfo; }): FormDefinition { const formDefinition: FormDefinition = { form: { layoutStyle: {} }, @@ -55,6 +56,7 @@ export function generateFormDefinition({ }; const modelFieldsConfigs: ModelFieldsConfigs = {}; + if (modelInfo) { modelInfo.fields.forEach((field) => { addDataStoreModelField(formDefinition, modelFieldsConfigs, field); diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts index eebb0b1b8..4144d561a 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts @@ -14,7 +14,7 @@ limitations under the License. */ -import { DataStoreModelField, FormDefinition, ModelFieldsConfigs } from '../../types'; +import { ModelField, FormDefinition, ModelFieldsConfigs } from '../../types'; import { FIELD_TYPE_MAP } from './field-type-map'; import { InvalidInputError } from '../../errors'; @@ -26,7 +26,7 @@ import { InvalidInputError } from '../../errors'; export function addDataStoreModelField( formDefinition: FormDefinition, modelFieldsConfigs: ModelFieldsConfigs, - field: DataStoreModelField, + field: ModelField, ) { if (field.isArray) { throw new InvalidInputError('Array types are not yet supported'); diff --git a/packages/codegen-ui/lib/generate-form-definition/index.ts b/packages/codegen-ui/lib/generate-form-definition/index.ts index d009eba35..64f8fdb94 100644 --- a/packages/codegen-ui/lib/generate-form-definition/index.ts +++ b/packages/codegen-ui/lib/generate-form-definition/index.ts @@ -15,4 +15,4 @@ */ export { FIELD_TYPE_MAP, getFormDefinitionInputElement, getFormDefinitionSectionalElement } from './helpers'; export { generateFormDefinition } from './generate-form-definition'; -export { mapFormToComponent } from './form-to-component'; +export { mapFormDefinitionToComponent } from './form-to-component'; diff --git a/packages/codegen-ui/lib/types/data.ts b/packages/codegen-ui/lib/types/data.ts index 7b1011110..57a41bb8e 100644 --- a/packages/codegen-ui/lib/types/data.ts +++ b/packages/codegen-ui/lib/types/data.ts @@ -14,9 +14,18 @@ limitations under the License. */ +import { ModelField, SchemaNonModels } from '@aws-amplify/datastore'; + // exporting types and scalar functions from aws-amplify // as these will be used when loading in dataschema for form generation -export type { SchemaModel } from '@aws-amplify/datastore'; +export type { SchemaModel, ModelFields, ModelField, SchemaNonModels } from '@aws-amplify/datastore'; +export { isGraphQLScalarType } from '@aws-amplify/datastore'; + +export type SchemaEnums = Record; +export type SchemaEnum = { + name: string; + values: string[]; +}; type FieldType = string | { model: string } | { nonModel: string } | { enum: string }; @@ -76,3 +85,9 @@ export type GenericDataSchema = { nonModels: { [nonModelName: string]: GenericDataModel }; }; + +export type DataStoreModelInfo = { + fields: ModelField[]; + enum?: SchemaEnums; + nonModelFields?: SchemaNonModels; +}; diff --git a/packages/codegen-ui/lib/types/form/form-definition.ts b/packages/codegen-ui/lib/types/form/form-definition.ts index 0254379c7..21fa08a4f 100644 --- a/packages/codegen-ui/lib/types/form/form-definition.ts +++ b/packages/codegen-ui/lib/types/form/form-definition.ts @@ -26,4 +26,5 @@ export type FormDefinition = { elements: { [element: string]: FormDefinitionElement }; buttons: { [key: string]: string }; elementMatrix: string[][]; + inputFields?: string[]; }; diff --git a/packages/codegen-ui/lib/types/form/form-metadata.ts b/packages/codegen-ui/lib/types/form/form-metadata.ts new file mode 100644 index 000000000..85ba146e8 --- /dev/null +++ b/packages/codegen-ui/lib/types/form/form-metadata.ts @@ -0,0 +1,29 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +export type FormValidation = { + validationType: string; + validationRule: string; +}; + +export type FormMetadata = { + name: string; + fieldState: string; + onChangeFields: string[]; + errorStateFields: string[]; + // indicates the validation function provided for that field + // ex. name field has a lengthValidation type where the rule is length > 5 + onValidationFields?: Record; +}; diff --git a/packages/codegen-ui/lib/types/form/index.ts b/packages/codegen-ui/lib/types/form/index.ts index 046b66d15..1ecabd172 100644 --- a/packages/codegen-ui/lib/types/form/index.ts +++ b/packages/codegen-ui/lib/types/form/index.ts @@ -19,6 +19,7 @@ import { StudioFormFields, StudioFormFieldConfig, StudioGenericFieldConfig } fro import { SectionalElement } from './sectional-element'; import { FormDefinition, ModelFieldsConfigs } from './form-definition'; import { StudioFieldInputConfig } from './input-config'; +import { FormMetadata } from './form-metadata'; /** * Data type definition for StudioForm @@ -51,12 +52,14 @@ export type StudioForm = { }; export * from './form-definition-element'; +export * from './style'; export type { - StudioFormStyle, SectionalElement, StudioFormFieldConfig, + StudioFormActionType, FormDefinition, + FormMetadata, StudioFieldInputConfig, StudioGenericFieldConfig, StudioFormFields, diff --git a/packages/codegen-ui/lib/types/form/style.ts b/packages/codegen-ui/lib/types/form/style.ts index 3414bee29..7b272fd38 100644 --- a/packages/codegen-ui/lib/types/form/style.ts +++ b/packages/codegen-ui/lib/types/form/style.ts @@ -13,15 +13,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -type FormStyleConfigCommon = { +export type FormStyleConfigCommon = { tokenReference?: string; }; -type FormStyleConfig = { +export type FormStyleConfig = { value?: string; } & FormStyleConfigCommon; -type FormAlignmentConfig = { +export type FormAlignmentConfig = { value?: 'left' | 'center' | 'right'; } & FormStyleConfigCommon; diff --git a/packages/codegen-ui/lib/utils/component-metadata.ts b/packages/codegen-ui/lib/utils/component-metadata.ts index eab4324cb..e80511263 100644 --- a/packages/codegen-ui/lib/utils/component-metadata.ts +++ b/packages/codegen-ui/lib/utils/component-metadata.ts @@ -21,6 +21,7 @@ import { StudioComponentProperty, StudioComponentPropertyBinding, StateReference, + FormMetadata, } from '../types'; import { StateReferenceMetadata, computeStateReferenceMetadata } from './state-reference-metadata'; @@ -29,6 +30,7 @@ export type ComponentMetadata = { requiredDataModels: string[]; stateReferences: StateReferenceMetadata[]; componentNameToTypeMap: Record; + formMetadata?: FormMetadata; }; /** diff --git a/packages/codegen-ui/lib/utils/form-component-metadata.ts b/packages/codegen-ui/lib/utils/form-component-metadata.ts new file mode 100644 index 000000000..0426b351d --- /dev/null +++ b/packages/codegen-ui/lib/utils/form-component-metadata.ts @@ -0,0 +1,34 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { FormDefinition, FormMetadata, StudioForm } from '../types'; + +export const getFormFieldStateName = (formName: string) => { + return [formName.charAt(0).toLowerCase() + formName.slice(1), 'Fields'].join(''); +}; + +export const mapFormMetadata = (form: StudioForm, formDefinition: FormDefinition): FormMetadata => { + return { + name: form.name, + fieldState: getFormFieldStateName(form.name), + onChangeFields: Object.entries(formDefinition.elements).reduce((fields, [key, value]) => { + if ('props' in value && 'label' in value.props) { + fields.push(key); + } + return fields; + }, []), + errorStateFields: [], + }; +}; diff --git a/packages/codegen-ui/lib/utils/index.ts b/packages/codegen-ui/lib/utils/index.ts index 670fc1986..57c771257 100644 --- a/packages/codegen-ui/lib/utils/index.ts +++ b/packages/codegen-ui/lib/utils/index.ts @@ -17,3 +17,4 @@ export * from './component-metadata'; export * from './component-tree'; export * from './state-reference-metadata'; export * from './string-formatter'; +export * from './form-component-metadata'; diff --git a/packages/codegen-ui/lib/validation-helper.ts b/packages/codegen-ui/lib/validation-helper.ts index b6633b331..d0a191739 100644 --- a/packages/codegen-ui/lib/validation-helper.ts +++ b/packages/codegen-ui/lib/validation-helper.ts @@ -177,6 +177,22 @@ const studioThemeSchema = yup.object({ overrides: yup.array(studioThemeValuesSchema).nullable(), }); +/** + * Form Schema Definitions + */ +const studioFormSchema = yup.object({ + name: alphaNumString().required(), + id: yup.string().nullable(), + formActionType: yup.string().matches(new RegExp('(create|update)')), + dataType: yup.object({ + dataSourceType: yup.string().matches(new RegExp('(DataStore|Custom)')), + dataTypeName: yup.string().required(), + }), + fields: yup.object().nullable(), + sectionalElements: yup.object().nullable(), + style: yup.object().nullable(), +}); + /** * Studio Schema Validation Functions and Helpers. */ @@ -193,3 +209,4 @@ const validateSchema = (validator: yup.AnySchema, studioSchema: any) => { export const validateComponentSchema = (schema: any) => validateSchema(studioComponentSchema, schema); export const validateThemeSchema = (schema: any) => validateSchema(studioThemeSchema, schema); +export const validateFormSchema = (schema: any) => validateSchema(studioFormSchema, schema);