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 (
+
+ );
+}
+"
+`;
+
+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 (
+
+ );
+}
+"
+`;
+
+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 (
+
+ );
+}
+"
+`;
+
+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);