From 810a5959a19f1e12254e076ad33efe29f924e698 Mon Sep 17 00:00:00 2001 From: Charles Shin Date: Fri, 9 Dec 2022 12:27:21 -0800 Subject: [PATCH] feat: disable form submit when model is invalid (#826) * feat: disable form submit when model is invalid - fixed typo on existing test * fix: exposing form data type to form-renderer helper --- ...studio-ui-codegen-react-forms.test.ts.snap | 107 ++++++++----- .../lib/forms/form-renderer-helper.ts | 149 ++++++++++++++---- .../forms/flex-datastore-create.json | 4 +- .../lib/types/form/form-metadata.ts | 12 ++ packages/codegen-ui/lib/types/form/index.ts | 20 +-- .../lib/utils/form-component-metadata.ts | 1 + 6 files changed, 210 insertions(+), 83 deletions(-) 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 index cccd55972..5ce44aeb3 100644 --- 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 @@ -4021,10 +4021,15 @@ export default function MyPostForm(props) { e?.hasError)} + isDisabled={ + !(id || post) || Object.values(errors).some((e) => e?.hasError) + } {...getOverrideProps(overrides, \\"SubmitButton\\")} > @@ -4229,10 +4236,15 @@ export default function MyPostForm(props) { e?.hasError)} + isDisabled={ + !(id || post) || Object.values(errors).some((e) => e?.hasError) + } {...getOverrideProps(overrides, \\"SubmitButton\\")} > @@ -4464,10 +4478,9 @@ function ArrayField({ ); } -export default function MyFlexUpdateForm(props) { +export default function MyFlexCreateForm(props) { const { - id, - flex, + clearOnSuccess = true, onSuccess, onError, onSubmit, @@ -4493,6 +4506,7 @@ export default function MyFlexUpdateForm(props) { ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { +<<<<<<< HEAD const cleanValues = { ...initialValues, ...flexRecord }; setUsername(cleanValues.username); setCaption(cleanValues.caption); @@ -4501,17 +4515,17 @@ export default function MyFlexUpdateForm(props) { setTags(cleanValues.tags ?? []); setCurrentTagsValue(\\"\\"); setProfile_url(cleanValues.profile_url); +======= + setUsername(initialValues.username); + setCaption(initialValues.caption); + setCustomtags(initialValues.Customtags); + setCurrentCustomtagsValue(undefined); + setTags(initialValues.tags); + setCurrentTagsValue(undefined); + setProfile_url(initialValues.profile_url); +>>>>>>> 6898d10 (feat: disable form submit when model is invalid (#826)) setErrors({}); }; - const [flexRecord, setFlexRecord] = React.useState(flex); - React.useEffect(() => { - const queryData = async () => { - const record = id ? await DataStore.query(Flex0, id) : flex0; - setFlexRecord(record); - }; - queryData(); - }, [id, flex]); - React.useEffect(resetStateValues, [flexRecord]); const [currentCustomtagsValue, setCurrentCustomtagsValue] = React.useState(\\"\\"); const CustomtagsRef = React.createRef(); @@ -4577,21 +4591,20 @@ export default function MyFlexUpdateForm(props) { modelFields = onSubmit(modelFields); } try { - await DataStore.save( - Flex0.copyOf(flexRecord, (updated) => { - Object.assign(updated, modelFields); - }) - ); + await DataStore.save(new Flex0(modelFields)); if (onSuccess) { onSuccess(modelFields); } + if (clearOnSuccess) { + resetStateValues(); + } } catch (err) { if (onError) { onError(modelFields, err.message); } } }} - {...getOverrideProps(overrides, \\"MyFlexUpdateForm\\")} + {...getOverrideProps(overrides, \\"MyFlexCreateForm\\")} {...rest} > >>>>>> 6898d10 (feat: disable form submit when model is invalid (#826)) onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4668,7 +4689,10 @@ export default function MyFlexUpdateForm(props) { isRequired={false} isReadOnly={false} placeholder=\\"i love code\\" +<<<<<<< HEAD value={caption} +======= +>>>>>>> 6898d10 (feat: disable form submit when model is invalid (#826)) onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4786,7 +4810,10 @@ export default function MyFlexUpdateForm(props) { label=\\"Profile url\\" isRequired={false} isReadOnly={false} +<<<<<<< HEAD value={profile_url} +======= +>>>>>>> 6898d10 (feat: disable form submit when model is invalid (#826)) onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4818,7 +4845,6 @@ export default function MyFlexUpdateForm(props) { exports[`amplify form renderer tests datastore form tests should render a create form with colliding model name 2`] = ` "import * as React from \\"react\\"; -import { Flex as Flex0 } from \\"../models\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { GridProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; export declare type ValidationResponse = { @@ -4826,14 +4852,14 @@ export declare type ValidationResponse = { errorMessage?: string; }; export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; -export declare type MyFlexUpdateFormInputValues = { +export declare type MyFlexCreateFormInputValues = { username?: string; caption?: string; Customtags?: string[]; tags?: string[]; profile_url?: string; }; -export declare type MyFlexUpdateFormValidationValues = { +export declare type MyFlexCreateFormValidationValues = { username?: ValidationFunction; caption?: ValidationFunction; Customtags?: ValidationFunction; @@ -4841,8 +4867,8 @@ export declare type MyFlexUpdateFormValidationValues = { profile_url?: ValidationFunction; }; export declare type FormProps = Partial & React.DOMAttributes; -export declare type MyFlexUpdateFormOverridesProps = { - MyFlexUpdateFormGrid?: FormProps; +export declare type MyFlexCreateFormOverridesProps = { + MyFlexCreateFormGrid?: FormProps; RowGrid0?: FormProps; username?: FormProps; caption?: FormProps; @@ -4850,19 +4876,18 @@ export declare type MyFlexUpdateFormOverridesProps = { tags?: FormProps; profile_url?: FormProps; } & EscapeHatchProps; -export declare type MyFlexUpdateFormProps = React.PropsWithChildren<{ - overrides?: MyFlexUpdateFormOverridesProps | undefined | null; +export declare type MyFlexCreateFormProps = React.PropsWithChildren<{ + overrides?: MyFlexCreateFormOverridesProps | undefined | null; } & { - id?: string; - flex?: Flex0; - onSubmit?: (fields: MyFlexUpdateFormInputValues) => MyFlexUpdateFormInputValues; - onSuccess?: (fields: MyFlexUpdateFormInputValues) => void; - onError?: (fields: MyFlexUpdateFormInputValues, errorMessage: string) => void; + clearOnSuccess?: boolean; + onSubmit?: (fields: MyFlexCreateFormInputValues) => MyFlexCreateFormInputValues; + onSuccess?: (fields: MyFlexCreateFormInputValues) => void; + onError?: (fields: MyFlexCreateFormInputValues, errorMessage: string) => void; onCancel?: () => void; - onChange?: (fields: MyFlexUpdateFormInputValues) => MyFlexUpdateFormInputValues; - onValidate?: MyFlexUpdateFormValidationValues; + onChange?: (fields: MyFlexCreateFormInputValues) => MyFlexCreateFormInputValues; + onValidate?: MyFlexCreateFormValidationValues; } & React.CSSProperties>; -export default function MyFlexUpdateForm(props: MyFlexUpdateFormProps): React.ReactElement; +export default function MyFlexCreateForm(props: MyFlexCreateFormProps): React.ReactElement; " `; @@ -7080,10 +7105,15 @@ export default function InputGalleryUpdateForm(props) { e?.hasError)} + isDisabled={ + !(id || inputGallery) || + Object.values(errors).some((e) => e?.hasError) + } {...getOverrideProps(overrides, \\"SubmitButton\\")} > diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts index 812457b63..5241af75c 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts @@ -143,6 +143,9 @@ export const createValidationExpression = (validationRules: FieldValidationConfi export const addFormAttributes = (component: StudioComponent | StudioComponentChild, formMetadata: FormMetadata) => { const { name: componentName, componentType } = component; + const { + dataType: { dataTypeName }, + } = formMetadata; const attributes: JsxAttribute[] = []; /* boolean => RadioGroupField @@ -283,54 +286,136 @@ export const addFormAttributes = (component: StudioComponent | StudioComponentCh ), ), ); + if (formMetadata.formActionType === 'update' && formMetadata.dataType.dataSourceType === 'DataStore') { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('isDisabled'), + factory.createJsxExpression( + undefined, + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createIdentifier('id'), + factory.createToken(SyntaxKind.BarBarToken), + factory.createIdentifier(lowerCaseFirst(dataTypeName)), + ), + ), + ), + ), + ), + ); + } } if (componentName === 'SubmitButton') { - attributes.push( - factory.createJsxAttribute( - factory.createIdentifier('isDisabled'), - factory.createJsxExpression( - undefined, - factory.createCallExpression( - factory.createPropertyAccessExpression( + if (formMetadata.formActionType === 'update' && formMetadata.dataType.dataSourceType === 'DataStore') { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('isDisabled'), + factory.createJsxExpression( + undefined, + factory.createBinaryExpression( + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createIdentifier('id'), + factory.createToken(SyntaxKind.BarBarToken), + factory.createIdentifier(lowerCaseFirst(dataTypeName)), + ), + ), + ), + SyntaxKind.BarBarToken, factory.createCallExpression( factory.createPropertyAccessExpression( - factory.createIdentifier('Object'), - factory.createIdentifier('values'), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Object'), + factory.createIdentifier('values'), + ), + undefined, + [factory.createIdentifier('errors')], + ), + factory.createIdentifier('some'), ), undefined, - [factory.createIdentifier('errors')], - ), - factory.createIdentifier('some'), - ), - undefined, - [ - factory.createArrowFunction( - undefined, - undefined, [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier('e'), + factory.createArrowFunction( undefined, undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('e'), + undefined, + undefined, + undefined, + ), + ], undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createPropertyAccessChain( + factory.createIdentifier('e'), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('hasError'), + ), ), ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - factory.createPropertyAccessChain( - factory.createIdentifier('e'), - factory.createToken(SyntaxKind.QuestionDotToken), - factory.createIdentifier('hasError'), + ), + ), + ), + ), + ); + } else { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('isDisabled'), + factory.createJsxExpression( + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Object'), + factory.createIdentifier('values'), + ), + undefined, + [factory.createIdentifier('errors')], ), + factory.createIdentifier('some'), ), - ], + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('e'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createPropertyAccessChain( + factory.createIdentifier('e'), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('hasError'), + ), + ), + ], + ), ), ), - ), - ); + ); + } } if (componentName === 'CancelButton') { attributes.push( diff --git a/packages/codegen-ui/example-schemas/forms/flex-datastore-create.json b/packages/codegen-ui/example-schemas/forms/flex-datastore-create.json index b08063adf..f34857b48 100644 --- a/packages/codegen-ui/example-schemas/forms/flex-datastore-create.json +++ b/packages/codegen-ui/example-schemas/forms/flex-datastore-create.json @@ -1,6 +1,6 @@ { - "name": "MyFlexUpdateForm", - "formActionType": "update", + "name": "MyFlexCreateForm", + "formActionType": "create", "dataType": { "dataSourceType": "DataStore", "dataTypeName": "Flex" diff --git a/packages/codegen-ui/lib/types/form/form-metadata.ts b/packages/codegen-ui/lib/types/form/form-metadata.ts index 21768b201..42a5b92ae 100644 --- a/packages/codegen-ui/lib/types/form/form-metadata.ts +++ b/packages/codegen-ui/lib/types/form/form-metadata.ts @@ -22,6 +22,17 @@ import { FormStyleConfig, StudioFormStyle } from './style'; */ export type StudioFormActionType = 'create' | 'update'; +export type StudioDataSourceType = 'DataStore' | 'Custom'; + +/** + * Data type definition for StudioForm + */ +export type StudioFormDataType = { + dataSourceType: StudioDataSourceType; + + dataTypeName: string; +}; + export type FieldConfigMetadata = { // ex. name field has a string validation type where the rule is char length > 5 validationRules: FieldValidationConfiguration[]; @@ -37,6 +48,7 @@ export type FieldConfigMetadata = { export type FormMetadata = { id?: string; formActionType: StudioFormActionType; + dataType: StudioFormDataType; name: string; fieldConfigs: Record; layoutConfigs: Record; diff --git a/packages/codegen-ui/lib/types/form/index.ts b/packages/codegen-ui/lib/types/form/index.ts index 542aee7fe..1ad3edb5a 100644 --- a/packages/codegen-ui/lib/types/form/index.ts +++ b/packages/codegen-ui/lib/types/form/index.ts @@ -21,18 +21,13 @@ import { FormDefinition, ModelFieldsConfigs, FieldTypeMapKeys, ButtonConfig } fr import { StudioFieldInputConfig, StudioFormValueMappings } from './input-config'; import { StudioFieldPosition } from './position'; import { StudioFormCTA } from './form-cta'; -import { FormMetadata, FieldConfigMetadata, StudioFormActionType } from './form-metadata'; - -export type StudioDataSourceType = 'DataStore' | 'Custom'; - -/** - * Data type definition for StudioForm - */ -export type StudioFormDataType = { - dataSourceType: StudioDataSourceType; - - dataTypeName: string; -}; +import { + FormMetadata, + FieldConfigMetadata, + StudioFormActionType, + StudioFormDataType, + StudioDataSourceType, +} from './form-metadata'; /** * This is the base type for all StudioForms @@ -86,6 +81,7 @@ export type { SectionalElement, StudioFormFieldConfig, StudioFormActionType, + StudioDataSourceType, FormDefinition, FormMetadata, FieldConfigMetadata, diff --git a/packages/codegen-ui/lib/utils/form-component-metadata.ts b/packages/codegen-ui/lib/utils/form-component-metadata.ts index 58405238a..bdbedda49 100644 --- a/packages/codegen-ui/lib/utils/form-component-metadata.ts +++ b/packages/codegen-ui/lib/utils/form-component-metadata.ts @@ -66,6 +66,7 @@ export const mapFormMetadata = (form: StudioForm, formDefinition: FormDefinition id: form.id, name: form.name, formActionType: form.formActionType, + dataType: form.dataType, layoutConfigs: formDefinition.form.layoutStyle, fieldConfigs: inputElementEntries.reduce>((configs, [name, config]) => { const updatedConfigs = configs;