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 9ef95c60b..850c745b3 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 @@ -1,5 +1,565 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`amplify form renderer tests datastore form tests custom form tests should render a create form for child of 1:m relationship 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + Badge, + Button, + Divider, + Flex, + Grid, + Icon, + ScrollView, + Text, + TextField, +} from \\"@aws-amplify/ui-react\\"; +import { + getOverrideProps, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; +import { CompositeToy, CompositeDog } from \\"../models\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { DataStore } from \\"aws-amplify\\"; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, +}) { + const labelElement = {label}; + const [selectedBadgeIndex, setSelectedBadgeIndex] = React.useState(); + const [isEditing, setIsEditing] = React.useState(); + React.useEffect(() => { + if (isEditing) { + inputFieldRef?.current?.focus(); + } + }, [isEditing]); + const removeItem = async (removeIndex) => { + const newItems = items.filter((value, index) => index !== removeIndex); + await onChange(newItems); + setSelectedBadgeIndex(undefined); + }; + const addItem = async () => { + if ( + currentFieldValue !== undefined && + currentFieldValue !== null && + currentFieldValue !== \\"\\" && + !hasError + ) { + const newItems = [...items]; + if (selectedBadgeIndex !== undefined) { + newItems[selectedBadgeIndex] = currentFieldValue; + setSelectedBadgeIndex(undefined); + } else { + newItems.push(currentFieldValue); + } + await onChange(newItems); + setIsEditing(false); + } + }; + const arraySection = ( + + {!!items?.length && ( + + {items.map((value, index) => { + return ( + { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + { + event.stopPropagation(); + removeItem(index); + }} + /> + + ); + })} + + )} + + + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + + {labelElement} + {arraySection} + + ); + } + return ( + + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + + + ) : ( + + {(currentFieldValue || isEditing) && ( + + )} + + + )} + {arraySection} + + ); +} +export default function CreateCompositeToyForm(props) { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + kind: \\"\\", + color: \\"\\", + compositeDogCompositeToysName: undefined, + compositeDogCompositeToysDescription: undefined, + }; + const [kind, setKind] = React.useState(initialValues.kind); + const [color, setColor] = React.useState(initialValues.color); + const [compositeDogCompositeToysName, setCompositeDogCompositeToysName] = + React.useState(initialValues.compositeDogCompositeToysName); + const [ + compositeDogCompositeToysDescription, + setCompositeDogCompositeToysDescription, + ] = React.useState(initialValues.compositeDogCompositeToysDescription); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setKind(initialValues.kind); + setColor(initialValues.color); + setCompositeDogCompositeToysName( + initialValues.compositeDogCompositeToysName + ); + setCurrentCompositeDogCompositeToysNameValue(undefined); + setCompositeDogCompositeToysDescription( + initialValues.compositeDogCompositeToysDescription + ); + setCurrentCompositeDogCompositeToysDescriptionValue(undefined); + setErrors({}); + }; + const [ + currentCompositeDogCompositeToysNameValue, + setCurrentCompositeDogCompositeToysNameValue, + ] = React.useState(undefined); + const compositeDogCompositeToysNameRef = React.createRef(); + const [ + currentCompositeDogCompositeToysDescriptionValue, + setCurrentCompositeDogCompositeToysDescriptionValue, + ] = React.useState(undefined); + const compositeDogCompositeToysDescriptionRef = React.createRef(); + const compositeDogRecords = useDataStoreBinding({ + type: \\"collection\\", + model: CompositeDog, + }).items; + const validations = { + kind: [{ type: \\"Required\\" }], + color: [{ type: \\"Required\\" }], + compositeDogCompositeToysName: [], + compositeDogCompositeToysDescription: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = getDisplayValue + ? getDisplayValue(currentValue) + : currentValue; + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + kind, + color, + compositeDogCompositeToysName, + compositeDogCompositeToysDescription, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + if (onSubmit) { + modelFields = onSubmit(modelFields); + } + try { + Object.entries(modelFields).forEach(([key, value]) => { + if (typeof value === \\"string\\" && value.trim() === \\"\\") { + modelFields[key] = undefined; + } + }); + await DataStore.save(new CompositeToy(modelFields)); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"CreateCompositeToyForm\\")} + {...rest} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + kind: value, + color, + compositeDogCompositeToysName, + compositeDogCompositeToysDescription, + }; + const result = onChange(modelFields); + value = result?.kind ?? value; + } + if (errors.kind?.hasError) { + runValidationTasks(\\"kind\\", value); + } + setKind(value); + }} + onBlur={() => runValidationTasks(\\"kind\\", kind)} + errorMessage={errors.kind?.errorMessage} + hasError={errors.kind?.hasError} + {...getOverrideProps(overrides, \\"kind\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + kind, + color: value, + compositeDogCompositeToysName, + compositeDogCompositeToysDescription, + }; + const result = onChange(modelFields); + value = result?.color ?? value; + } + if (errors.color?.hasError) { + runValidationTasks(\\"color\\", value); + } + setColor(value); + }} + onBlur={() => runValidationTasks(\\"color\\", color)} + errorMessage={errors.color?.errorMessage} + hasError={errors.color?.hasError} + {...getOverrideProps(overrides, \\"color\\")} + > + { + let value = items[0]; + if (onChange) { + const modelFields = { + kind, + color, + compositeDogCompositeToysName: value, + compositeDogCompositeToysDescription, + }; + const result = onChange(modelFields); + value = result?.compositeDogCompositeToysName ?? value; + } + setCompositeDogCompositeToysName(value); + setCurrentCompositeDogCompositeToysNameValue(undefined); + }} + currentFieldValue={currentCompositeDogCompositeToysNameValue} + label={\\"Composite dog composite toys name\\"} + items={ + compositeDogCompositeToysName ? [compositeDogCompositeToysName] : [] + } + hasError={errors.compositeDogCompositeToysName?.hasError} + setFieldValue={setCurrentCompositeDogCompositeToysNameValue} + inputFieldRef={compositeDogCompositeToysNameRef} + defaultFieldValue={\\"\\"} + > + ({ + id: r?.name, + label: r?.name, + }))} + onSelect={({ id }) => { + setCurrentCompositeDogCompositeToysNameValue(id); + }} + onClear={() => { + setCurrentCompositeDogCompositeToysNameDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.compositeDogCompositeToysName?.hasError) { + runValidationTasks(\\"compositeDogCompositeToysName\\", value); + } + setCurrentCompositeDogCompositeToysNameValue(value); + }} + onBlur={() => + runValidationTasks( + \\"compositeDogCompositeToysName\\", + currentCompositeDogCompositeToysNameValue + ) + } + errorMessage={errors.compositeDogCompositeToysName?.errorMessage} + hasError={errors.compositeDogCompositeToysName?.hasError} + ref={compositeDogCompositeToysNameRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"compositeDogCompositeToysName\\")} + > + + { + let value = items[0]; + if (onChange) { + const modelFields = { + kind, + color, + compositeDogCompositeToysName, + compositeDogCompositeToysDescription: value, + }; + const result = onChange(modelFields); + value = result?.compositeDogCompositeToysDescription ?? value; + } + setCompositeDogCompositeToysDescription(value); + setCurrentCompositeDogCompositeToysDescriptionValue(undefined); + }} + currentFieldValue={currentCompositeDogCompositeToysDescriptionValue} + label={\\"Composite dog composite toys description\\"} + items={ + compositeDogCompositeToysDescription + ? [compositeDogCompositeToysDescription] + : [] + } + hasError={errors.compositeDogCompositeToysDescription?.hasError} + setFieldValue={setCurrentCompositeDogCompositeToysDescriptionValue} + inputFieldRef={compositeDogCompositeToysDescriptionRef} + defaultFieldValue={\\"\\"} + > + ({ + id: r?.description, + label: r?.description, + }))} + onSelect={({ id }) => { + setCurrentCompositeDogCompositeToysDescriptionValue(id); + }} + onClear={() => { + setCurrentCompositeDogCompositeToysDescriptionDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.compositeDogCompositeToysDescription?.hasError) { + runValidationTasks(\\"compositeDogCompositeToysDescription\\", value); + } + setCurrentCompositeDogCompositeToysDescriptionValue(value); + }} + onBlur={() => + runValidationTasks( + \\"compositeDogCompositeToysDescription\\", + currentCompositeDogCompositeToysDescriptionValue + ) + } + errorMessage={ + errors.compositeDogCompositeToysDescription?.errorMessage + } + hasError={errors.compositeDogCompositeToysDescription?.hasError} + ref={compositeDogCompositeToysDescriptionRef} + labelHidden={true} + {...getOverrideProps( + overrides, + \\"compositeDogCompositeToysDescription\\" + )} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests custom form tests should render a create form for child of 1:m relationship 2`] = ` +"import * as React from \\"react\\"; +import { AutocompleteProps, GridProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type CreateCompositeToyFormInputValues = { + kind?: string; + color?: string; + compositeDogCompositeToysName?: string; + compositeDogCompositeToysDescription?: string; +}; +export declare type CreateCompositeToyFormValidationValues = { + kind?: ValidationFunction; + color?: ValidationFunction; + compositeDogCompositeToysName?: ValidationFunction; + compositeDogCompositeToysDescription?: ValidationFunction; +}; +export declare type PrimitiveOverrideProps = Partial & React.DOMAttributes; +export declare type CreateCompositeToyFormOverridesProps = { + CreateCompositeToyFormGrid?: PrimitiveOverrideProps; + kind?: PrimitiveOverrideProps; + color?: PrimitiveOverrideProps; + compositeDogCompositeToysName?: PrimitiveOverrideProps; + compositeDogCompositeToysDescription?: PrimitiveOverrideProps; +} & EscapeHatchProps; +export declare type CreateCompositeToyFormProps = React.PropsWithChildren<{ + overrides?: CreateCompositeToyFormOverridesProps | undefined | null; +} & { + clearOnSuccess?: boolean; + onSubmit?: (fields: CreateCompositeToyFormInputValues) => CreateCompositeToyFormInputValues; + onSuccess?: (fields: CreateCompositeToyFormInputValues) => void; + onError?: (fields: CreateCompositeToyFormInputValues, errorMessage: string) => void; + onChange?: (fields: CreateCompositeToyFormInputValues) => CreateCompositeToyFormInputValues; + onValidate?: CreateCompositeToyFormValidationValues; +} & React.CSSProperties>; +export default function CreateCompositeToyForm(props: CreateCompositeToyFormProps): React.ReactElement; +" +`; + exports[`amplify form renderer tests datastore form tests custom form tests should render a create form for model with composite keys 1`] = ` "/* eslint-disable */ import * as React from \\"react\\"; @@ -7997,18 +8557,24 @@ export default function MyMemberForm(props) { } = props; const initialValues = { name: \\"\\", + teamID: undefined, Team: undefined, }; const [name, setName] = React.useState(initialValues.name); + const [teamID, setTeamID] = React.useState(initialValues.teamID); const [Team, setTeam] = React.useState(initialValues.Team); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setName(initialValues.name); + setTeamID(initialValues.teamID); + setCurrentTeamIDValue(undefined); setTeam(initialValues.Team); setCurrentTeamValue(undefined); setCurrentTeamDisplayValue(\\"\\"); setErrors({}); }; + const [currentTeamIDValue, setCurrentTeamIDValue] = React.useState(undefined); + const teamIDRef = React.createRef(); const [currentTeamDisplayValue, setCurrentTeamDisplayValue] = React.useState(\\"\\"); const [currentTeamValue, setCurrentTeamValue] = React.useState(undefined); @@ -8030,6 +8596,7 @@ export default function MyMemberForm(props) { }; const validations = { name: [], + teamID: [{ type: \\"Required\\" }], Team: [], }; const runValidationTasks = async ( @@ -8058,6 +8625,7 @@ export default function MyMemberForm(props) { event.preventDefault(); let modelFields = { name, + teamID, Team, }; const validationResponses = await Promise.all( @@ -8156,6 +8724,7 @@ export default function MyMemberForm(props) { if (onChange) { const modelFields = { name: value, + teamID, Team, }; const result = onChange(modelFields); @@ -8178,6 +8747,61 @@ export default function MyMemberForm(props) { if (onChange) { const modelFields = { name, + teamID: value, + Team, + }; + const result = onChange(modelFields); + value = result?.teamID ?? value; + } + setTeamID(value); + setCurrentTeamIDValue(undefined); + }} + currentFieldValue={currentTeamIDValue} + label={\\"Team id\\"} + items={teamID ? [teamID] : []} + hasError={errors.teamID?.hasError} + setFieldValue={setCurrentTeamIDValue} + inputFieldRef={teamIDRef} + defaultFieldValue={\\"\\"} + > + ({ + id: r?.id, + label: r?.id, + }))} + onSelect={({ id }) => { + setCurrentTeamIDValue(id); + }} + onClear={() => { + setCurrentTeamIDDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.teamID?.hasError) { + runValidationTasks(\\"teamID\\", value); + } + setCurrentTeamIDValue(value); + }} + onBlur={() => runValidationTasks(\\"teamID\\", currentTeamIDValue)} + errorMessage={errors.teamID?.errorMessage} + hasError={errors.teamID?.hasError} + ref={teamIDRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"teamID\\")} + > + + { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + teamID, Team: value, }; const result = onChange(modelFields); @@ -8258,16 +8882,19 @@ export declare type ValidationResponse = { export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; export declare type MyMemberFormInputValues = { name?: string; + teamID?: string; Team?: Team0; }; export declare type MyMemberFormValidationValues = { name?: ValidationFunction; + teamID?: ValidationFunction; Team?: ValidationFunction; }; export declare type PrimitiveOverrideProps = Partial & React.DOMAttributes; export declare type MyMemberFormOverridesProps = { MyMemberFormGrid?: PrimitiveOverrideProps; name?: PrimitiveOverrideProps; + teamID?: PrimitiveOverrideProps; Team?: PrimitiveOverrideProps; } & EscapeHatchProps; export declare type MyMemberFormProps = React.PropsWithChildren<{ @@ -11891,16 +12518,20 @@ export default function MyMemberForm(props) { } = props; const initialValues = { name: \\"\\", + teamID: undefined, Team: undefined, }; const [name, setName] = React.useState(initialValues.name); + const [teamID, setTeamID] = React.useState(initialValues.teamID); const [Team, setTeam] = React.useState(initialValues.Team); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { const cleanValues = memberRecord - ? { ...initialValues, ...memberRecord, Team } + ? { ...initialValues, ...memberRecord, teamID, Team } : initialValues; setName(cleanValues.name); + setTeamID(cleanValues.teamID); + setCurrentTeamIDValue(undefined); setTeam(cleanValues.Team); setCurrentTeamValue(undefined); setCurrentTeamDisplayValue(\\"\\"); @@ -11911,12 +12542,16 @@ export default function MyMemberForm(props) { const queryData = async () => { const record = idProp ? await DataStore.query(Member, idProp) : member; setMemberRecord(record); + const teamIDRecord = record ? await record.teamID : undefined; + setTeamID(teamIDRecord); const TeamRecord = record ? await record.Team : undefined; setTeam(TeamRecord); }; queryData(); }, [idProp, member]); - React.useEffect(resetStateValues, [memberRecord, Team]); + React.useEffect(resetStateValues, [memberRecord, teamID, Team]); + const [currentTeamIDValue, setCurrentTeamIDValue] = React.useState(undefined); + const teamIDRef = React.createRef(); const [currentTeamDisplayValue, setCurrentTeamDisplayValue] = React.useState(\\"\\"); const [currentTeamValue, setCurrentTeamValue] = React.useState(undefined); @@ -11938,6 +12573,7 @@ export default function MyMemberForm(props) { }; const validations = { name: [], + teamID: [{ type: \\"Required\\" }], Team: [], }; const runValidationTasks = async ( @@ -11966,6 +12602,7 @@ export default function MyMemberForm(props) { event.preventDefault(); let modelFields = { name, + teamID, Team, }; const validationResponses = await Promise.all( @@ -12072,6 +12709,7 @@ export default function MyMemberForm(props) { if (onChange) { const modelFields = { name: value, + teamID, Team, }; const result = onChange(modelFields); @@ -12094,6 +12732,62 @@ export default function MyMemberForm(props) { if (onChange) { const modelFields = { name, + teamID: value, + Team, + }; + const result = onChange(modelFields); + value = result?.teamID ?? value; + } + setTeamID(value); + setCurrentTeamIDValue(undefined); + }} + currentFieldValue={currentTeamIDValue} + label={\\"Team id\\"} + items={teamID ? [teamID] : []} + hasError={errors.teamID?.hasError} + setFieldValue={setCurrentTeamIDValue} + inputFieldRef={teamIDRef} + defaultFieldValue={\\"\\"} + > + ({ + id: r?.id, + label: r?.id, + }))} + onSelect={({ id }) => { + setCurrentTeamIDValue(id); + }} + onClear={() => { + setCurrentTeamIDDisplayValue(\\"\\"); + }} + defaultValue={teamID} + onChange={(e) => { + let { value } = e.target; + if (errors.teamID?.hasError) { + runValidationTasks(\\"teamID\\", value); + } + setCurrentTeamIDValue(value); + }} + onBlur={() => runValidationTasks(\\"teamID\\", currentTeamIDValue)} + errorMessage={errors.teamID?.errorMessage} + hasError={errors.teamID?.hasError} + ref={teamIDRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"teamID\\")} + > + + { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + teamID, Team: value, }; const result = onChange(modelFields); @@ -12175,16 +12869,19 @@ export declare type ValidationResponse = { export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; export declare type MyMemberFormInputValues = { name?: string; + teamID?: string; Team?: Team0; }; export declare type MyMemberFormValidationValues = { name?: ValidationFunction; + teamID?: ValidationFunction; Team?: ValidationFunction; }; export declare type PrimitiveOverrideProps = Partial & React.DOMAttributes; export declare type MyMemberFormOverridesProps = { MyMemberFormGrid?: PrimitiveOverrideProps; name?: PrimitiveOverrideProps; + teamID?: PrimitiveOverrideProps; Team?: PrimitiveOverrideProps; } & EscapeHatchProps; export declare type MyMemberFormProps = React.PropsWithChildren<{ @@ -17217,18 +17914,24 @@ export default function MyMemberForm(props) { } = props; const initialValues = { name: \\"\\", + teamID: undefined, Team: undefined, }; const [name, setName] = React.useState(initialValues.name); + const [teamID, setTeamID] = React.useState(initialValues.teamID); const [Team, setTeam] = React.useState(initialValues.Team); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setName(initialValues.name); + setTeamID(initialValues.teamID); + setCurrentTeamIDValue(undefined); setTeam(initialValues.Team); setCurrentTeamValue(undefined); setCurrentTeamDisplayValue(\\"\\"); setErrors({}); }; + const [currentTeamIDValue, setCurrentTeamIDValue] = React.useState(undefined); + const teamIDRef = React.createRef(); const [currentTeamDisplayValue, setCurrentTeamDisplayValue] = React.useState(\\"\\"); const [currentTeamValue, setCurrentTeamValue] = React.useState(undefined); @@ -17250,6 +17953,7 @@ export default function MyMemberForm(props) { }; const validations = { name: [], + teamID: [{ type: \\"Required\\" }], Team: [], }; const runValidationTasks = async ( @@ -17278,6 +17982,7 @@ export default function MyMemberForm(props) { event.preventDefault(); let modelFields = { name, + teamID, Team, }; const validationResponses = await Promise.all( @@ -17376,6 +18081,7 @@ export default function MyMemberForm(props) { if (onChange) { const modelFields = { name: value, + teamID, Team, }; const result = onChange(modelFields); @@ -17398,6 +18104,61 @@ export default function MyMemberForm(props) { if (onChange) { const modelFields = { name, + teamID: value, + Team, + }; + const result = onChange(modelFields); + value = result?.teamID ?? value; + } + setTeamID(value); + setCurrentTeamIDValue(undefined); + }} + currentFieldValue={currentTeamIDValue} + label={\\"Team id\\"} + items={teamID ? [teamID] : []} + hasError={errors.teamID?.hasError} + setFieldValue={setCurrentTeamIDValue} + inputFieldRef={teamIDRef} + defaultFieldValue={\\"\\"} + > + ({ + id: r?.id, + label: r?.id, + }))} + onSelect={({ id }) => { + setCurrentTeamIDValue(id); + }} + onClear={() => { + setCurrentTeamIDDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.teamID?.hasError) { + runValidationTasks(\\"teamID\\", value); + } + setCurrentTeamIDValue(value); + }} + onBlur={() => runValidationTasks(\\"teamID\\", currentTeamIDValue)} + errorMessage={errors.teamID?.errorMessage} + hasError={errors.teamID?.hasError} + ref={teamIDRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"teamID\\")} + > + + { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + teamID, Team: value, }; const result = onChange(modelFields); @@ -17478,16 +18239,19 @@ export declare type ValidationResponse = { export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; export declare type MyMemberFormInputValues = { name?: string; + teamID?: string; Team?: Team0; }; export declare type MyMemberFormValidationValues = { name?: ValidationFunction; + teamID?: ValidationFunction; Team?: ValidationFunction; }; export declare type PrimitiveOverrideProps = Partial & React.DOMAttributes; export declare type MyMemberFormOverridesProps = { MyMemberFormGrid?: PrimitiveOverrideProps; name?: PrimitiveOverrideProps; + teamID?: PrimitiveOverrideProps; Team?: PrimitiveOverrideProps; } & EscapeHatchProps; export declare type MyMemberFormProps = React.PropsWithChildren<{ 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 a29af49fd..795b9b19d 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 @@ -499,6 +499,18 @@ describe('amplify form renderer tests', () => { expect(componentText).not.toContain('CompositeToys'); expect(componentText).not.toContain('CompositeVets'); }); + + it('should render a create form for child of 1:m relationship', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/composite-toy-datastore-create', + 'datastore/composite-relationships', + undefined, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); }); }); }); diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts index b95419ab7..c6b2b8a73 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/relationship.ts @@ -16,7 +16,6 @@ import { CallExpression, factory, IfStatement, NodeFlags, SyntaxKind } from 'typescript'; import { FieldConfigMetadata, - GenericDataRelationshipType, HasManyRelationshipType, InternalError, GenericDataModel, @@ -30,11 +29,7 @@ import { isManyToManyRelationship } from './map-from-fieldConfigs'; import { extractModelAndKeys, getIDValueCallChain, getMatchEveryModelFieldCallExpression } from './model-values'; import { isModelDataType } from './render-checkers'; -export const buildRelationshipQuery = ( - relationship: GenericDataRelationshipType, - importCollection: ImportCollection, -) => { - const { relatedModelName } = relationship; +export const buildRelationshipQuery = (relatedModelName: string, importCollection: ImportCollection) => { const itemsName = getRecordsName(relatedModelName); const objectProperties = [ factory.createPropertyAssignment(factory.createIdentifier('type'), factory.createStringLiteral('collection')), 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 e6e723317..7ceda070a 100644 --- a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -20,7 +20,6 @@ import { FormDefinition, generateFormDefinition, GenericDataSchema, - GenericDataRelationshipType, handleCodegenErrors, mapFormDefinitionToComponent, mapFormMetadata, @@ -518,7 +517,7 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< // Add value state and ref array type fields in ArrayField wrapper - const relationshipCollection: GenericDataRelationshipType[] = []; + const relatedModelNames: Set = new Set(); Object.entries(formMetadata.fieldConfigs).forEach(([field, fieldConfig]) => { const { sanitizedFieldName, componentType, dataType, relationship } = fieldConfig; @@ -562,8 +561,8 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< ); } - if (relationship) { - relationshipCollection.push(relationship); + if (relationship && !relatedModelNames.has(relationship.relatedModelName)) { + relatedModelNames.add(relationship.relatedModelName); } }); @@ -590,10 +589,12 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< model: Author, }).items; */ - if (relationshipCollection.length) { + if (relatedModelNames.size) { this.importCollection.addMappedImport(ImportValue.USE_DATA_STORE_BINDING); statements.push( - ...relationshipCollection.map((relationship) => buildRelationshipQuery(relationship, this.importCollection)), + ...[...relatedModelNames].map((relatedModelName) => + buildRelationshipQuery(relatedModelName, this.importCollection), + ), ); } diff --git a/packages/codegen-ui/example-schemas/forms/composite-toy-datastore-create.json b/packages/codegen-ui/example-schemas/forms/composite-toy-datastore-create.json new file mode 100644 index 000000000..279b623a0 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/composite-toy-datastore-create.json @@ -0,0 +1,12 @@ +{ + "name": "CreateCompositeToyForm", + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "CompositeToy" + }, + "formActionType": "create", + "fields": {}, + "sectionalElements": {}, + "style": {}, + "cta": {} +} \ No newline at end of file diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts index f0631c0b1..9deab6037 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts @@ -398,7 +398,12 @@ describe('mapModelFieldsConfigs', () => { }, }; - const modelFieldsConfigs = mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema }); + const modelFieldsConfigs = mapModelFieldsConfigs({ + dataTypeName: 'Dog', + formDefinition, + dataSchema, + featureFlags: { isRelationshipSupported: true }, + }); expect(formDefinition.elementMatrix).toStrictEqual([]); expect(modelFieldsConfigs).toStrictEqual({ @@ -423,6 +428,139 @@ describe('mapModelFieldsConfigs', () => { }); }); + it('should add not-model type relationship fields to configs and to matrix if it is hasMany index', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Owner: { + primaryKeys: ['id'], + fields: {}, + }, + CompositeDog: { + primaryKeys: ['name', 'description'], + fields: { + name: { + dataType: 'ID', + required: true, + readOnly: false, + isArray: false, + }, + description: { + dataType: 'String', + required: true, + readOnly: false, + isArray: false, + }, + CompositeToys: { + dataType: { + model: 'CompositeToy', + }, + required: false, + readOnly: false, + isArray: true, + relationship: { + type: 'HAS_MANY', + relatedModelName: 'CompositeToy', + relatedModelFields: ['compositeDogCompositeToysName', 'compositeDogCompositeToysDescription'], + }, + }, + }, + }, + CompositeToy: { + fields: { + kind: { + dataType: 'ID', + required: true, + readOnly: false, + isArray: false, + }, + color: { + dataType: 'String', + required: true, + readOnly: false, + isArray: false, + }, + compositeDogCompositeToysName: { + dataType: 'ID', + required: false, + readOnly: false, + isArray: false, + relationship: { + type: 'HAS_ONE', + relatedModelName: 'CompositeDog', + isHasManyIndex: true, + }, + }, + compositeDogCompositeToysDescription: { + dataType: 'String', + required: false, + readOnly: false, + isArray: false, + relationship: { + type: 'HAS_ONE', + relatedModelName: 'CompositeDog', + isHasManyIndex: true, + }, + }, + }, + primaryKeys: ['kind', 'color'], + }, + }, + }; + + const modelFieldsConfigs = mapModelFieldsConfigs({ + dataTypeName: 'CompositeToy', + formDefinition, + dataSchema, + featureFlags: { isRelationshipSupported: true }, + }); + + expect(formDefinition.elementMatrix).toStrictEqual([ + ['kind'], + ['color'], + ['compositeDogCompositeToysName'], + ['compositeDogCompositeToysDescription'], + ]); + expect(modelFieldsConfigs.compositeDogCompositeToysName).toStrictEqual({ + label: 'Composite dog composite toys name', + dataType: 'ID', + inputType: { + type: 'Autocomplete', + required: false, + readOnly: false, + name: 'compositeDogCompositeToysName', + value: 'compositeDogCompositeToysName', + isArray: false, + valueMappings: { + values: [{ value: { bindingProperties: { property: 'CompositeDog', field: 'name' } } }], + bindingProperties: { CompositeDog: { type: 'Data', bindingProperties: { model: 'CompositeDog' } } }, + }, + }, + relationship: { type: 'HAS_ONE', relatedModelName: 'CompositeDog', isHasManyIndex: true }, + }); + expect(modelFieldsConfigs.compositeDogCompositeToysDescription).toStrictEqual({ + label: 'Composite dog composite toys description', + dataType: 'String', + inputType: { + type: 'Autocomplete', + required: false, + readOnly: false, + name: 'compositeDogCompositeToysDescription', + value: 'compositeDogCompositeToysDescription', + isArray: false, + valueMappings: { + values: [{ value: { bindingProperties: { property: 'CompositeDog', field: 'description' } } }], + bindingProperties: { CompositeDog: { type: 'Data', bindingProperties: { model: 'CompositeDog' } } }, + }, + }, + relationship: { type: 'HAS_ONE', relatedModelName: 'CompositeDog', isHasManyIndex: true }, + }); + }); + it('should add nonModel field to matrix if nonModel enabled', () => { const formDefinition: FormDefinition = getBasicFormDefinition(); diff --git a/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts b/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts index b0e9b79a6..f9139c28b 100644 --- a/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts +++ b/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts @@ -114,6 +114,7 @@ describe('getGenericFromDataStore', () => { expect(genericSchema.models.Dog.fields.ownerID.relationship).toStrictEqual({ type: 'HAS_ONE', relatedModelName: 'Owner', + isHasManyIndex: true, }); }); @@ -175,6 +176,7 @@ describe('getGenericFromDataStore', () => { expect(genericSchema.models.Dog.fields.ownerID.relationship).toStrictEqual({ type: 'HAS_ONE', relatedModelName: 'Owner', + isHasManyIndex: true, }); }); 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 bfa2d926c..2cc12c658 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 @@ -34,6 +34,41 @@ import { ExtendedStudioGenericFieldConfig } from '../../types/form/form-definiti import { StudioFormInputFieldProperty } from '../../types/form/input-config'; import { FIELD_TYPE_MAP } from './field-type-map'; +function extractCorrespondingKey({ + thisModel, + relatedModel, + relationshipFieldName, +}: { + thisModel: GenericDataModel; + relatedModel: GenericDataModel; + relationshipFieldName: string; +}): string { + const relationshipField = thisModel.fields[relationshipFieldName]; + if ( + relationshipField.relationship && + 'isHasManyIndex' in relationshipField.relationship && + relationshipField.relationship.isHasManyIndex + ) { + const correspondingFieldTuple = Object.entries(relatedModel.fields).find( + ([, field]) => + field.relationship?.type === 'HAS_MANY' && + field.relationship?.relatedModelFields.includes(relationshipFieldName), + ); + if (correspondingFieldTuple) { + const correspondingField = correspondingFieldTuple[1].relationship; + if (correspondingField?.type === 'HAS_MANY') { + const indexOfKey = correspondingField.relatedModelFields.indexOf(relationshipFieldName); + if (indexOfKey !== -1) { + return relatedModel.primaryKeys[indexOfKey]; + } + } + } + } + + // TODO: support other types + return relationshipFieldName; +} + export function getFieldTypeMapKey(field: GenericDataField): FieldTypeMapKeys { if (typeof field.dataType === 'object' && 'enum' in field.dataType) { return 'Enum'; @@ -96,11 +131,13 @@ function getModelDisplayValue({ } function getValueMappings({ + dataTypeName, fieldName, field, enums, allModels, }: { + dataTypeName: string; fieldName: string; field: GenericDataField; enums: GenericDataSchema['enums']; @@ -126,8 +163,16 @@ function getValueMappings({ const modelName = field.relationship.relatedModelName; const relatedModel = allModels[modelName]; const isModelType = typeof field.dataType === 'object' && 'model' in field.dataType; - // if model, store all keys; else, store field as key - const keys = isModelType ? relatedModel.primaryKeys : [fieldName]; + // if model, store all keys; else, store corresponding primary key + const keys = isModelType + ? relatedModel.primaryKeys + : [ + extractCorrespondingKey({ + thisModel: allModels[dataTypeName], + relatedModel, + relationshipFieldName: fieldName, + }), + ]; const values: StudioFormValueMappings['values'] = keys.map((key) => ({ value: { bindingProperties: { property: modelName, field: key } }, })); @@ -145,11 +190,13 @@ function getValueMappings({ } export function getFieldConfigFromModelField({ + dataTypeName, fieldName, field, dataSchema, setReadOnly, }: { + dataTypeName: string; fieldName: string; field: GenericDataField; dataSchema: GenericDataSchema; @@ -197,7 +244,13 @@ export function getFieldConfigFromModelField({ config.inputType.placeholder = `Search ${field.relationship.relatedModelName}`; } - const valueMappings = getValueMappings({ fieldName, field, enums: dataSchema.enums, allModels: dataSchema.models }); + const valueMappings = getValueMappings({ + dataTypeName, + fieldName, + field, + enums: dataSchema.enums, + allModels: dataSchema.models, + }); if (valueMappings) { config.inputType.valueMappings = valueMappings; } @@ -234,7 +287,9 @@ export function mapModelFieldsConfigs({ field.readOnly || (fieldName === 'id' && field.dataType === 'ID' && field.required) || !checkIsSupportedAsFormField(field, featureFlags) || - (field.relationship && !(typeof field.dataType === 'object' && 'model' in field.dataType)); + (field.relationship && + !(typeof field.dataType === 'object' && 'model' in field.dataType) && + !('isHasManyIndex' in field.relationship && field.relationship.isHasManyIndex)); if (!isAutoExcludedField) { formDefinition.elementMatrix.push([fieldName]); @@ -243,6 +298,7 @@ export function mapModelFieldsConfigs({ const isPrimaryKey = model.primaryKeys.includes(fieldName); modelFieldsConfigs[fieldName] = getFieldConfigFromModelField({ + dataTypeName, fieldName, field, dataSchema, diff --git a/packages/codegen-ui/lib/generic-from-datastore.ts b/packages/codegen-ui/lib/generic-from-datastore.ts index 439ee904e..6f4ef3856 100644 --- a/packages/codegen-ui/lib/generic-from-datastore.ts +++ b/packages/codegen-ui/lib/generic-from-datastore.ts @@ -128,6 +128,7 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener addRelationship(fieldsWithImplicitRelationships, relatedModelName, associatedFieldName, { type: 'HAS_ONE', relatedModelName: model.name, + isHasManyIndex: true, }); } }); diff --git a/packages/codegen-ui/lib/types/data.ts b/packages/codegen-ui/lib/types/data.ts index 5b2502edc..5b0ed0564 100644 --- a/packages/codegen-ui/lib/types/data.ts +++ b/packages/codegen-ui/lib/types/data.ts @@ -44,6 +44,7 @@ export type HasManyRelationshipType = { export type HasOneRelationshipType = { type: 'HAS_ONE'; associatedFields?: string[]; + isHasManyIndex?: boolean; } & CommonRelationshipType; export type BelongsToRelationshipType = { diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts index fefb6a275..9bcf038cd 100644 --- a/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts +++ b/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts @@ -37,6 +37,7 @@ const EXPECTED_SUCCESSFUL_CASES = new Set([ 'DataStoreFormCreateCPKTeacher', 'DataStoreFormUpdateCompositeDog', 'DataStoreFormCreateCompositeDog', + 'DataStoreFormUpdateCompositeToy', 'ComponentWithDataBindingWithPredicate', 'ComponentWithDataBindingWithoutPredicate', 'ComponentWithSimplePropertyBinding', diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/update-form-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/update-form-spec.cy.ts index 3df46e79e..d680bf5ef 100644 --- a/packages/test-generator/integration-test-templates/cypress/e2e/update-form-spec.cy.ts +++ b/packages/test-generator/integration-test-templates/cypress/e2e/update-form-spec.cy.ts @@ -205,4 +205,31 @@ describe('UpdateForms', () => { }); }); }); + + // this model is m in 1:m + describe('DataStoreFormUpdateCompositeToy', () => { + beforeEach(() => { + cy.reload(); + }); + it('should update indices used for 1:m relationships', () => { + cy.get('#dataStoreFormUpdateCompositeToy').within(() => { + getArrayFieldButtonByLabel('Composite dog composite toys name').click(); + typeInAutocomplete('Yundoo{downArrow}{enter}'); + clickAddToArray(); + + getArrayFieldButtonByLabel('Composite dog composite toys description').click(); + typeInAutocomplete('tiny but mighty{downArrow}{enter}'); + clickAddToArray(); + + cy.contains('Submit').click(); + + cy.contains(/chew/).then((recordElement: JQuery) => { + const record = JSON.parse(recordElement.text()); + + expect(record.compositeDogCompositeToysName).to.equal('Yundoo'); + expect(record.compositeDogCompositeToysDescription).to.equal('tiny but mighty'); + }); + }); + }); + }); }); diff --git a/packages/test-generator/integration-test-templates/src/UpdateFormTests.tsx b/packages/test-generator/integration-test-templates/src/UpdateFormTests.tsx index 86b343393..b421bde84 100644 --- a/packages/test-generator/integration-test-templates/src/UpdateFormTests.tsx +++ b/packages/test-generator/integration-test-templates/src/UpdateFormTests.tsx @@ -21,6 +21,7 @@ import { DataStoreFormUpdateAllSupportedFormFields, DataStoreFormUpdateCompositeDog, DataStoreFormUpdateCPKTeacher, + DataStoreFormUpdateCompositeToy, } from './ui-components'; // eslint-disable-line import/extensions, max-len import { Owner, @@ -266,6 +267,8 @@ export default function UpdateFormTests() { const [compositeDogRecordString, setCompositeDogRecordString] = useState(''); const initializeStarted = useRef(false); + const [compositeToyRecord, setCompositeToyRecord] = useState(); + const [compositeToyRecordString, setCompositeToyRecordString] = useState(''); useEffect(() => { const initializeTestState = async () => { if (initializeStarted.current) { @@ -279,6 +282,7 @@ export default function UpdateFormTests() { initializeCPKTeacherTestData({ setCPKTeacherId }), initializeCompositeDogTestData({ setCompositeDogRecord }), ]); + setCompositeToyRecord(await DataStore.query(CompositeToy, { kind: 'chew', color: 'red' })); setInitialized(true); }; @@ -375,6 +379,18 @@ export default function UpdateFormTests() { /> {compositeDogRecordString} + DataStore Form - UpdateCompositeToy + + { + const record = await DataStore.query(CompositeToy, { kind: 'chew', color: 'red' }); + + setCompositeToyRecordString(JSON.stringify(record)); + }} + /> + {compositeToyRecordString} + ); } diff --git a/packages/test-generator/lib/forms/datastore-form-update-composite-toy.json b/packages/test-generator/lib/forms/datastore-form-update-composite-toy.json new file mode 100644 index 000000000..d7a79104f --- /dev/null +++ b/packages/test-generator/lib/forms/datastore-form-update-composite-toy.json @@ -0,0 +1,13 @@ +{ + "id": "kdjfskdfddj", + "name": "DataStoreFormUpdateCompositeToy", + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "CompositeToy" + }, + "formActionType": "update", + "fields": {}, + "sectionalElements": {}, + "style": {}, + "cta": {} +} \ No newline at end of file diff --git a/packages/test-generator/lib/forms/index.ts b/packages/test-generator/lib/forms/index.ts index 8e4c02644..9801e5cf7 100644 --- a/packages/test-generator/lib/forms/index.ts +++ b/packages/test-generator/lib/forms/index.ts @@ -24,3 +24,4 @@ export { default as DataStoreFormUpdateCPKTeacher } from './datastore-form-updat export { default as DataStoreFormUpdateCompositeDog } from './datastore-form-update-composite-dog.json'; export { default as DataStoreFormCreateCPKTeacher } from './datastore-form-create-cpk-teacher.json'; export { default as DataStoreFormCreateCompositeDog } from './datastore-form-create-composite-dog.json'; +export { default as DataStoreFormUpdateCompositeToy } from './datastore-form-update-composite-toy.json';