From e7852c3cc6e10a2924309181ff148fee5e585570 Mon Sep 17 00:00:00 2001 From: Chenwei Zhang Date: Tue, 15 Nov 2022 16:21:40 -0800 Subject: [PATCH] feat: adding many to many support for form creation --- ...studio-ui-codegen-react-forms.test.ts.snap | 187 +++++++++------ .../studio-ui-codegen-react-forms.test.ts | 18 ++ .../lib/amplify-ui-renderers/form.ts | 10 +- .../forms/form-renderer-helper/all-props.ts | 9 +- .../forms/form-renderer-helper/cta-props.ts | 27 ++- .../form-renderer-helper/display-value.ts | 5 + .../event-handler-props.ts | 39 +++ .../forms/form-renderer-helper/form-state.ts | 11 +- .../form-renderer-helper/relationship.ts | 133 +++++++++- .../render-array-field.ts | 9 +- .../forms/form-renderer-helper/value-props.ts | 4 + .../lib/forms/react-form-renderer.ts | 2 +- .../lib/utils/forms/array-field-component.ts | 1 + .../example-schemas/datastore/tag-post.json | 227 ++++++++++++++++++ .../forms/tag-datastore-create.json | 36 +++ .../helpers/model-fields-configs.ts | 6 + .../codegen-ui/lib/generic-from-datastore.ts | 17 +- packages/codegen-ui/lib/types/data.ts | 2 + 18 files changed, 652 insertions(+), 91 deletions(-) create mode 100644 packages/codegen-ui/example-schemas/datastore/tag-post.json create mode 100644 packages/codegen-ui/example-schemas/forms/tag-datastore-create.json 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 0a85e68b3..3151cbea9 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 @@ -19,12 +19,12 @@ export default function CustomDataForm(props) { const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = props; const initialValues = { - name: undefined, - email: undefined, + name: \\"\\", + email: \\"\\", city: undefined, category: undefined, pages: 0, - phone: undefined, + phone: \\"\\", }; const [name, setName] = React.useState(initialValues.name); const [email, setEmail] = React.useState(initialValues.email); @@ -110,6 +110,7 @@ export default function CustomDataForm(props) { label=\\"name\\" isRequired={true} defaultValue=\\"John Doe\\" + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -138,6 +139,7 @@ export default function CustomDataForm(props) { label=\\"E-mail\\" isRequired={true} defaultValue=\\"johndoe@amplify.com\\" + value={email} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -415,8 +417,8 @@ export default function CustomDataForm(props) { ...rest } = props; const initialValues = { - name: undefined, - email: undefined, + name: \\"\\", + email: \\"\\", \\"metadata-field\\": undefined, city: undefined, category: undefined, @@ -520,6 +522,7 @@ export default function CustomDataForm(props) { label=\\"name\\" isRequired={true} defaultValue=\\"John Doe\\" + value={name} defaultValue={name} onChange={(e) => { let { value } = e.target; @@ -549,6 +552,7 @@ export default function CustomDataForm(props) { label=\\"E-mail\\" isRequired={true} defaultValue=\\"johndoe@amplify.com\\" + value={email} defaultValue={email} onChange={(e) => { let { value } = e.target; @@ -966,7 +970,7 @@ export default function CustomDataForm(props) { const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = props; const initialValues = { - name: undefined, + name: \\"\\", email: [], phone: [], }; @@ -977,14 +981,14 @@ export default function CustomDataForm(props) { const resetStateValues = () => { setName(initialValues.name); setEmail(initialValues.email); - setCurrentEmailValue(undefined); + setCurrentEmailValue(\\"\\"); setPhone(initialValues.phone); - setCurrentPhoneValue(undefined); + setCurrentPhoneValue(\\"\\"); setErrors({}); }; - const [currentEmailValue, setCurrentEmailValue] = React.useState(undefined); + const [currentEmailValue, setCurrentEmailValue] = React.useState(\\"\\"); const emailRef = React.createRef(); - const [currentPhoneValue, setCurrentPhoneValue] = React.useState(undefined); + const [currentPhoneValue, setCurrentPhoneValue] = React.useState(\\"\\"); const phoneRef = React.createRef(); const validations = { name: [{ type: \\"Required\\" }], @@ -1048,6 +1052,7 @@ export default function CustomDataForm(props) { label=\\"name\\" isRequired={true} defaultValue=\\"John Doe\\" + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -1082,7 +1087,7 @@ export default function CustomDataForm(props) { values = result?.email ?? values; } setEmail(values); - setCurrentEmailValue(undefined); + setCurrentEmailValue(\\"\\"); }} currentFieldValue={currentEmailValue} label={\\"E-mail\\"} @@ -1090,7 +1095,7 @@ export default function CustomDataForm(props) { hasError={errors.email?.hasError} setFieldValue={setCurrentEmailValue} inputFieldRef={emailRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -1587,6 +1593,7 @@ export default function MyPostForm(props) { isRequired={false} isReadOnly={false} placeholder=\\"i love code\\" + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -1628,7 +1635,7 @@ export default function MyPostForm(props) { values = result?.Customtags ?? values; } setCustomtags(values); - setCurrentCustomtagsValue(undefined); + setCurrentCustomtagsValue(\\"\\"); }} currentFieldValue={currentCustomtagsValue} label={\\"Tags\\"} @@ -1636,7 +1643,7 @@ export default function MyPostForm(props) { hasError={errors.Customtags?.hasError} setFieldValue={setCurrentCustomtagsValue} inputFieldRef={CustomtagsRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -2097,6 +2104,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -2128,6 +2136,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -2156,6 +2165,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -2246,7 +2256,7 @@ export default function NestedJson(props) { values = result?.Nicknames1 ?? values; } setNicknames1(values); - setCurrentNicknames1Value(undefined); + setCurrentNicknames1Value(\\"\\"); }} currentFieldValue={currentNicknames1Value} label={\\"Nick Names1\\"} @@ -2254,7 +2264,7 @@ export default function NestedJson(props) { hasError={errors.Nicknames1?.hasError} setFieldValue={setCurrentNicknames1Value} inputFieldRef={Nicknames1Ref} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -2596,7 +2607,7 @@ export default function NestedJson(props) { const { initialData, onSubmit, onValidate, onChange, overrides, ...rest } = props; const initialValues = { - firstName: undefined, + firstName: \\"\\", \\"last-Name\\": undefined, lastName: [], bio: {}, @@ -2611,7 +2622,7 @@ export default function NestedJson(props) { setFirstName(cleanValues.firstName); setLastName(cleanValues[\\"last-Name\\"]); setLastName1(cleanValues.lastName ?? []); - setCurrentLastName1Value(undefined); + setCurrentLastName1Value(\\"\\"); setBio(cleanValues.bio); setErrors({}); }; @@ -2624,8 +2635,7 @@ export default function NestedJson(props) { setBio(initialData.bio); } }, []); - const [currentLastName1Value, setCurrentLastName1Value] = - React.useState(undefined); + const [currentLastName1Value, setCurrentLastName1Value] = React.useState(\\"\\"); const lastName1Ref = React.createRef(); const validations = { firstName: [], @@ -2690,6 +2700,7 @@ export default function NestedJson(props) { > { let { value } = e.target; @@ -2753,7 +2764,7 @@ export default function NestedJson(props) { values = result?.lastName ?? values; } setLastName1(values); - setCurrentLastName1Value(undefined); + setCurrentLastName1Value(\\"\\"); }} currentFieldValue={currentLastName1Value} label={\\"lastName\\"} @@ -2761,7 +2772,7 @@ export default function NestedJson(props) { hasError={errors.lastName?.hasError} setFieldValue={setCurrentLastName1Value} inputFieldRef={lastName1Ref} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; @@ -2814,6 +2826,7 @@ export default function NestedJson(props) { > { let { value } = e.target; @@ -2932,7 +2945,7 @@ export default function CustomWithSectionalElements(props) { const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = props; const initialValues = { - name: undefined, + name: \\"\\", }; const [name, setName] = React.useState(initialValues.name); const [errors, setErrors] = React.useState({}); @@ -3001,6 +3014,7 @@ export default function CustomWithSectionalElements(props) { > { let { value } = e.target; if (onChange) { @@ -3124,11 +3138,11 @@ export default function MyPostForm(props) { ...rest } = props; const initialValues = { - caption: undefined, - username: undefined, - post_url: undefined, + caption: \\"\\", + username: \\"\\", + post_url: \\"\\", metadata: undefined, - profile_url: undefined, + profile_url: \\"\\", }; const [caption, setCaption] = React.useState(initialValues.caption); const [username, setUsername] = React.useState(initialValues.username); @@ -3257,6 +3271,7 @@ export default function MyPostForm(props) { label=\\"Caption\\" isRequired={false} isReadOnly={false} + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -3284,6 +3299,7 @@ export default function MyPostForm(props) { label=\\"Username\\" isRequired={false} isReadOnly={false} + value={username} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -3756,6 +3772,7 @@ export default function MyMemberForm(props) { label=\\"Name\\" isRequired={false} isReadOnly={false} + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -3814,6 +3831,9 @@ export default function MyMemberForm(props) { setCurrentTeamValue(teamRecords.find((r) => r.id === id)); setCurrentTeamDisplayValue(label); }} + onClear={() => { + setCurrentAuthorIdDisplayValue(\\"\\"); + }} onChange={(e) => { let { value } = e.target; if (errors.team?.hasError) { @@ -4194,6 +4214,7 @@ export default function BookCreateForm(props) { label=\\"Name\\" isRequired={false} isReadOnly={false} + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4254,6 +4275,9 @@ export default function BookCreateForm(props) { ); setCurrentPrimaryAuthorDisplayValue(label); }} + onClear={() => { + setCurrentTeamDisplayValue(\\"\\"); + }} onChange={(e) => { let { value } = e.target; if (errors.primaryAuthor?.hasError) { @@ -4494,7 +4518,7 @@ export default function BookCreateForm(props) { ...rest } = props; const initialValues = { - name: undefined, + name: \\"\\", primaryAuthor: undefined, primaryTitle: undefined, }; @@ -4519,7 +4543,7 @@ export default function BookCreateForm(props) { const [ currentPrimaryAuthorDisplayValue, setCurrentPrimaryAuthorDisplayValue, - ] = React.useState(undefined); + ] = React.useState(\\"\\"); const [currentPrimaryAuthorValue, setCurrentPrimaryAuthorValue] = React.useState(undefined); const primaryAuthorRef = React.createRef(); @@ -4653,6 +4677,7 @@ export default function BookCreateForm(props) { label=\\"Name\\" isRequired={false} isReadOnly={false} + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4689,16 +4714,16 @@ export default function BookCreateForm(props) { } setPrimaryAuthor(value); setCurrentPrimaryAuthorValue(undefined); - setCurrentPrimaryAuthorDisplayValue(undefined); + setCurrentPrimaryAuthorDisplayValue(\\"\\"); }} currentFieldValue={currentPrimaryAuthorValue} label={\\"Primary author\\"} items={primaryAuthor ? [primaryAuthor] : []} hasError={errors.primaryAuthor?.hasError} getBadgeText={getDisplayValue.primaryAuthor} - setFieldValue={currentPrimaryAuthorDisplayValue} + setFieldValue={setCurrentPrimaryAuthorDisplayValue} inputFieldRef={primaryAuthorRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { + setCurrentPrimaryAuthorDisplayValue(\\"\\"); + }} onChange={(e) => { let { value } = e.target; if (errors.primaryAuthor?.hasError) { @@ -6015,10 +6043,10 @@ export default function BlogCreateForm(props) { ...rest } = props; const initialValues = { - title: undefined, - content: undefined, + title: \\"\\", + content: \\"\\", switch: false, - published: undefined, + published: \\"\\", editedAt: [], }; const [title, setTitle] = React.useState(initialValues.title); @@ -6033,11 +6061,10 @@ export default function BlogCreateForm(props) { setSwitch1(initialValues.switch); setPublished(initialValues.published); setEditedAt(initialValues.editedAt); - setCurrentEditedAtValue(undefined); + setCurrentEditedAtValue(\\"\\"); setErrors({}); }; - const [currentEditedAtValue, setCurrentEditedAtValue] = - React.useState(undefined); + const [currentEditedAtValue, setCurrentEditedAtValue] = React.useState(\\"\\"); const editedAtRef = React.createRef(); const validations = { title: [], @@ -6143,6 +6170,7 @@ export default function BlogCreateForm(props) { label=\\"Title\\" isRequired={false} isReadOnly={false} + value={title} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -6170,6 +6198,7 @@ export default function BlogCreateForm(props) { label=\\"Content\\" isRequired={false} isReadOnly={false} + value={content} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -6264,7 +6293,7 @@ export default function BlogCreateForm(props) { values = result?.editedAt ?? values; } setEditedAt(values); - setCurrentEditedAtValue(undefined); + setCurrentEditedAtValue(\\"\\"); }} currentFieldValue={currentEditedAtValue} label={\\"Edited at\\"} @@ -6272,7 +6301,7 @@ export default function BlogCreateForm(props) { hasError={errors.editedAt?.hasError} setFieldValue={setCurrentEditedAtValue} inputFieldRef={editedAtRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -8887,6 +8917,7 @@ export default function PostCreateFormRow(props) { isRequired={false} isReadOnly={false} placeholder=\\"i love code\\" + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -9291,7 +9322,7 @@ export default function MyMemberForm(props) { ...rest } = props; const initialValues = { - name: undefined, + name: \\"\\", team: undefined, }; const [name, setName] = React.useState(initialValues.name); @@ -9301,11 +9332,11 @@ export default function MyMemberForm(props) { setName(initialValues.name); setTeam(initialValues.team); setCurrentTeamValue(undefined); - setCurrentTeamDisplayValue(undefined); + setCurrentTeamDisplayValue(\\"\\"); setErrors({}); }; const [currentTeamDisplayValue, setCurrentTeamDisplayValue] = - React.useState(undefined); + React.useState(\\"\\"); const [currentTeamValue, setCurrentTeamValue] = React.useState(undefined); const teamRef = React.createRef(); const teamRecords = useDataStoreBinding({ @@ -9426,6 +9457,7 @@ export default function MyMemberForm(props) { label=\\"Name\\" isRequired={false} isReadOnly={false} + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -9460,16 +9492,16 @@ export default function MyMemberForm(props) { } setTeam(value); setCurrentTeamValue(undefined); - setCurrentTeamDisplayValue(undefined); + setCurrentTeamDisplayValue(\\"\\"); }} currentFieldValue={currentTeamValue} label={\\"Team Label\\"} items={team ? [team] : []} hasError={errors.team?.hasError} getBadgeText={getDisplayValue.team} - setFieldValue={currentTeamDisplayValue} + setFieldValue={setCurrentTeamDisplayValue} inputFieldRef={teamRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > r.id === id)); setCurrentTeamDisplayValue(label); }} + onClear={() => { + setCurrentTeamDisplayValue(\\"\\"); + }} onChange={(e) => { let { value } = e.target; if (errors.team?.hasError) { 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 index 192626d3d..b4efb8560 100644 --- 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 @@ -87,6 +87,24 @@ describe('amplify form renderer tests', () => { expect(declaration).toMatchSnapshot(); }); + it('should generate a create form with manyToMany relationship', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/tag-datastore-create', + 'datastore/tag-post', + ); + // check nested model is imported + expect(componentText).toContain('import { Post, Tag, TagPost } from "../models";'); + + // check binding call is generated + expect(componentText).toContain('const postRecords = useDataStoreBinding({'); + + // check custom display value is set + expect(componentText).toContain('Posts: (record) => record?.title'); + + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + it('should render form with a two inputs in row', () => { const { componentText, declaration } = generateWithAmplifyFormRenderer( 'forms/post-datastore-create-row', 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 1cff9b512..d1d9dfe2a 100644 --- a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts @@ -94,6 +94,14 @@ export default class FormRenderer extends ReactComponentRenderer fieldConfigMetaData.relationship?.type === 'HAS_MANY', + ); + const onSubmitIdentifier = factory.createIdentifier('onSubmit'); if (dataSourceType === 'Custom') { @@ -128,7 +136,7 @@ export default class FormRenderer extends ReactComponentRenderer { +export const buildDataStoreExpression = ( + dataStoreActionType: 'update' | 'create', + importedModelName: string, + hasManyFieldConfigs: [string, FieldConfigMetadata][], +) => { if (dataStoreActionType === 'update') { return [ factory.createVariableStatement( @@ -101,6 +107,25 @@ export const buildDataStoreExpression = (dataStoreActionType: 'update' | 'create ), ]; } + + // TODO: Many to Many update action, this condition dataStoreActionType === 'create' can be moved inside the functon + if (hasManyFieldConfigs.length > 0 && dataStoreActionType === 'create') { + return hasManyFieldConfigs + .map((hasManyFieldConfig) => { + const [, fieldConfigMetaData] = hasManyFieldConfig; + if ( + fieldConfigMetaData.relationship?.type === 'HAS_MANY' && + fieldConfigMetaData.relationship.relatedJoinTableName + ) { + return buildManyToManyRelationshipCreateStatements(importedModelName, hasManyFieldConfig); + } + return []; + }) + .reduce((statements, statement) => { + return [...statements, ...statement]; + }, []); + } + return [ factory.createExpressionStatement( factory.createAwaitExpression( diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/display-value.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/display-value.ts index f2c1d1cb6..cf42dd970 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/display-value.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/display-value.ts @@ -266,5 +266,10 @@ export function getModelsToImport(fieldConfig: FieldConfigMetadata): string[] { }); } + // Import join table model + if (fieldConfig.relationship?.type === 'HAS_MANY' && fieldConfig.relationship.relatedJoinTableName) { + modelDependencies.push(fieldConfig.relationship.relatedJoinTableName); + } + return modelDependencies; } diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts index 21475a89d..bb7030e37 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts @@ -44,7 +44,9 @@ import { getCurrentDisplayValueName, getCurrentValueIdentifier, getCurrentValueName, + getDefaultValueExpression, getRecordsName, + getSetNameIdentifier, setFieldState, setStateExpression, } from './form-state'; @@ -130,6 +132,43 @@ export function buildOnBlurStatement(fieldName: string, fieldConfig: FieldConfig ); } +/** + * e.g. + * onClear={() => { + * setCurrentTeamDisplayValue(''); + * }} + */ +export function buildOnClearStatement(fieldName: string, fieldConfig: FieldConfigMetadata) { + const { componentType, dataType } = fieldConfig; + const renderedFieldName = fieldConfig.sanitizedFieldName || fieldName; + + return factory.createJsxAttribute( + factory.createIdentifier('onClear'), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + getSetNameIdentifier(getCurrentDisplayValueName(renderedFieldName)), + undefined, + [getDefaultValueExpression(fieldName, componentType, dataType, false, true)], + ), + ), + ], + true, + ), + ), + ), + ); +} + /** * if the onChange variable is defined it will send the current state of the fields into the function * the function expects all fields in return diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts index 0595eb440..f74079cad 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts @@ -127,8 +127,11 @@ export const getDefaultValueExpression = ( componentType: string, dataType?: DataFieldDataType, isArray?: boolean, + isDisplayValue?: boolean, ): Expression => { const componentTypeToDefaultValueMap: { [key: string]: Expression } = { + Autocomplete: isDisplayValue ? factory.createStringLiteral('') : factory.createIdentifier('undefined'), + TextField: factory.createStringLiteral(''), ToggleButton: factory.createFalse(), SwitchField: factory.createFalse(), StepperField: factory.createNumericLiteral(0), @@ -296,11 +299,15 @@ export const resetStateFunction = (fieldConfigs: Record { + const [fieldName, fieldConfigMetaData] = hasManyFieldConfig; + const { relatedModelField, relatedJoinFieldName, relatedJoinTableName } = + fieldConfigMetaData.relationship as HasManyRelationshipType; + return [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(relatedModelField), + undefined, + undefined, + factory.createAwaitExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('DataStore'), + factory.createIdentifier('save'), + ), + undefined, + [ + factory.createNewExpression(factory.createIdentifier(modelName), undefined, [ + factory.createIdentifier('modelFields'), + ]), + ], + ), + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createExpressionStatement( + factory.createAwaitExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('Promise'), factory.createIdentifier('all')), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(fieldName), + factory.createIdentifier('reduce'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('promises'), + undefined, + undefined, + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(relatedJoinFieldName as string), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('promises'), + factory.createIdentifier('push'), + ), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('DataStore'), + factory.createIdentifier('save'), + ), + undefined, + [ + factory.createNewExpression( + factory.createIdentifier(relatedJoinTableName as string), + undefined, + [ + factory.createObjectLiteralExpression( + [ + factory.createShorthandPropertyAssignment( + factory.createIdentifier(relatedModelField), + undefined, + ), + factory.createShorthandPropertyAssignment( + factory.createIdentifier(relatedJoinFieldName as string), + undefined, + ), + ], + true, + ), + ], + ), + ], + ), + ], + ), + ), + factory.createReturnStatement(factory.createIdentifier('promises')), + ], + true, + ), + ), + factory.createArrayLiteralExpression([], false), + ], + ), + ], + ), + ), + ), + ]; +}; diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts index b72c41b2b..b3f8c2461 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts @@ -69,7 +69,7 @@ function getOnChangeAttribute({ factory.createCallExpression( factory.createIdentifier(`set${capitalizeFirstLetter(getCurrentDisplayValueName(renderedFieldName))}`), undefined, - [getDefaultValueExpression(fieldName, componentType, dataType)], + [getDefaultValueExpression(fieldName, componentType, dataType, false, true)], ), ), ); @@ -212,7 +212,7 @@ export const renderArrayFieldComponent = ( let setFieldValueIdentifier = setStateName; if (isModelDataType(fieldConfig)) { - setFieldValueIdentifier = factory.createIdentifier(getCurrentDisplayValueName(renderedFieldName)); + setFieldValueIdentifier = getSetNameIdentifier(getCurrentDisplayValueName(renderedFieldName)); props.push( factory.createJsxAttribute( factory.createIdentifier('getBadgeText'), @@ -235,7 +235,10 @@ export const renderArrayFieldComponent = ( ), factory.createJsxAttribute( factory.createIdentifier('defaultFieldValue'), - factory.createJsxExpression(undefined, getDefaultValueExpression(fieldName, componentType, dataType)), + factory.createJsxExpression( + undefined, + getDefaultValueExpression(fieldName, componentType, dataType, false, true), + ), ), ); diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/value-props.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/value-props.ts index 51513a043..e68aa2f17 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/value-props.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/value-props.ts @@ -86,6 +86,10 @@ export const renderValueAttribute = ({ } const controlledComponentToAttributesMap: { [key: string]: JsxAttribute } = { + TextField: factory.createJsxAttribute( + factory.createIdentifier('value'), + factory.createJsxExpression(undefined, valueIdentifier), + ), ToggleButton: factory.createJsxAttribute( factory.createIdentifier('isPressed'), factory.createJsxExpression(undefined, valueIdentifier), diff --git a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts index 2e4a90d04..fb7d99208 100644 --- a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -473,7 +473,7 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< statements.push( buildUseStateExpression( getCurrentDisplayValueName(renderedName), - getDefaultValueExpression(formMetadata.name, componentType, dataType), + getDefaultValueExpression(formMetadata.name, componentType, dataType, false, true), ), ); } diff --git a/packages/codegen-ui-react/lib/utils/forms/array-field-component.ts b/packages/codegen-ui-react/lib/utils/forms/array-field-component.ts index 63a685dc3..2a02281f1 100644 --- a/packages/codegen-ui-react/lib/utils/forms/array-field-component.ts +++ b/packages/codegen-ui-react/lib/utils/forms/array-field-component.ts @@ -18,6 +18,7 @@ import { addUseEffectWrapper } from '../generate-react-hooks'; export const generateArrayFieldComponent = () => { const iconPath = 'M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z'; + const arraySection = 'arraySection'; const bodyBlock = [ factory.createVariableStatement( diff --git a/packages/codegen-ui/example-schemas/datastore/tag-post.json b/packages/codegen-ui/example-schemas/datastore/tag-post.json new file mode 100644 index 000000000..78c08a227 --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/tag-post.json @@ -0,0 +1,227 @@ +{ + "models": { + "Tag": { + "name": "Tag", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "label": { + "name": "label", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "Posts": { + "name": "Posts", + "isArray": true, + "type": { + "model": "TagPost" + }, + "isRequired": false, + "attributes": [], + "isArrayNullable": true, + "association": { + "connectionType": "HAS_MANY", + "associatedWith": "tag" + } + }, + "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": "Tags", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "public", + "operations": ["create", "update", "delete", "read"] + } + ] + } + } + ] + }, + "Post": { + "name": "Post", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "title": { + "name": "title", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "content": { + "name": "content", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "Tags": { + "name": "Tags", + "isArray": true, + "type": { + "model": "TagPost" + }, + "isRequired": false, + "attributes": [], + "isArrayNullable": true, + "association": { + "connectionType": "HAS_MANY", + "associatedWith": "post" + } + }, + "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": "public", + "operations": ["create", "update", "delete", "read"] + } + ] + } + } + ] + }, + "TagPost": { + "name": "TagPost", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "tag": { + "name": "tag", + "isArray": false, + "type": { + "model": "Tag" + }, + "isRequired": true, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetName": "tagID" + } + }, + "post": { + "name": "post", + "isArray": false, + "type": { + "model": "Post" + }, + "isRequired": true, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetName": "postID" + } + }, + "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": "TagPosts", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "key", + "properties": { + "name": "byTag", + "fields": ["tagID"] + } + }, + { + "type": "key", + "properties": { + "name": "byPost", + "fields": ["postID"] + } + } + ] + } + }, + "enums": {}, + "nonModels": {}, + "codegenVersion": "3.3.1", + "version": "6661fbcb644d38cea3d27e2933e70457" +} diff --git a/packages/codegen-ui/example-schemas/forms/tag-datastore-create.json b/packages/codegen-ui/example-schemas/forms/tag-datastore-create.json new file mode 100644 index 000000000..abff2fbf5 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/tag-datastore-create.json @@ -0,0 +1,36 @@ +{ + "name": "TagCreateForm", + "dataType": { "dataSourceType": "DataStore", "dataTypeName": "Tag" }, + "formActionType": "create", + "fields": { + "Posts": { + "inputType": { + "type": "Autocomplete", + "valueMappings": { + "values": [ + { + "value": { + "bindingProperties": { + "property": "Post", + "field": "id" + } + }, + "displayValue": { + "bindingProperties": { + "property": "Post", + "field": "title" + } + } + } + ], + "bindingProperties": { + "Post": { "type": "Data", "bindingProperties": { "model": "Post" } } + } + } + } + } + }, + "sectionalElements": {}, + "style": {}, + "cta": { "clear": {}, "cancel": {}, "submit": {} } +} diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts index 1146a3527..56bd72e27 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts @@ -93,6 +93,12 @@ export function getFieldConfigFromModelField({ const { defaultComponent } = FIELD_TYPE_MAP[fieldTypeMapKey]; + // When the relationship is many to many, set data type to the actual related model instead of the join table + if (field.relationship && field.relationship.type === 'HAS_MANY' && field.relationship.relatedJoinTableName) { + const dataType = field.dataType as { model: string }; + dataType.model = field.relationship.relatedModelName; + } + const config: ExtendedStudioGenericFieldConfig & { inputType: StudioFieldInputConfig } = { label: sentenceCase(fieldName), dataType: field.dataType, diff --git a/packages/codegen-ui/lib/generic-from-datastore.ts b/packages/codegen-ui/lib/generic-from-datastore.ts index 065768415..d47ecb301 100644 --- a/packages/codegen-ui/lib/generic-from-datastore.ts +++ b/packages/codegen-ui/lib/generic-from-datastore.ts @@ -73,7 +73,8 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener const relationshipType = field.association.connectionType; let relatedModelName = field.type.model; - + let relatedJoinFieldName; + let relatedJoinTableName; let modelRelationship: GenericDataRelationshipType | undefined; if (relationshipType === 'HAS_MANY' && 'associatedWith' in field.association) { @@ -99,6 +100,8 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener ); if (relatedJoinField && typeof relatedJoinField.type === 'object' && 'model' in relatedJoinField.type) { relatedModelName = relatedJoinField.type.model; + relatedJoinFieldName = relatedJoinField.name; + relatedJoinTableName = field.type.model; } // if the associated model is not a join table, note implicit relationship for associated field } else { @@ -107,16 +110,22 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener relatedModelName: model.name, }); } - modelRelationship = { type: relationshipType, relatedModelName, relatedModelField: associatedFieldName }; + modelRelationship = { + type: relationshipType, + relatedModelName, + relatedModelField: associatedFieldName, + relatedJoinFieldName, + relatedJoinTableName, + }; } // note implicit relationship for associated field within same model if ( relationshipType === 'HAS_ONE' && ('targetName' in field.association || 'targetNames' in field.association) && - (field.association.targetName || field.association.targetNames) + field.association.targetName ) { - const targetName = field.association.targetName || field.association.targetNames?.[0]; + const { targetName } = field.association; if (targetName) { addRelationship(fieldsWithImplicitRelationships, model.name, targetName, { type: relationshipType, diff --git a/packages/codegen-ui/lib/types/data.ts b/packages/codegen-ui/lib/types/data.ts index 27c93eed4..138250b43 100644 --- a/packages/codegen-ui/lib/types/data.ts +++ b/packages/codegen-ui/lib/types/data.ts @@ -37,6 +37,8 @@ export type CommonRelationshipType = { export type HasManyRelationshipType = { type: 'HAS_MANY'; relatedModelField: string; + relatedJoinFieldName?: string; + relatedJoinTableName?: string; } & CommonRelationshipType; export type HasOneRelationshipType = {