From 7f703b5a2f9dd4359fbfbdd6a751f6950ea40425 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Mon, 15 May 2023 17:09:00 +0000 Subject: [PATCH] feat: add graphql support for create form with relationships --- ...studio-ui-codegen-react-forms.test.ts.snap | 6279 +++++++++++++++++ .../studio-ui-codegen-react-forms.test.ts | 153 + .../bidirectional-relationship.ts | 292 +- .../forms/form-renderer-helper/cta-props.ts | 17 +- .../form-renderer-helper/relationship.ts | 370 +- .../lib/forms/react-form-renderer.ts | 20 +- 6 files changed, 6972 insertions(+), 159 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 61bf022a..0d2f4b23 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 @@ -661,6 +661,6285 @@ export default function MyPostForm(props: MyPostFormProps): React.ReactElement { " `; +exports[`amplify form renderer tests GraphQL form tests should generate a create form with belongsTo relationship 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Member, Team } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listTeams } from \\"../graphql/queries\\"; +import { createMember } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise; +export declare type MyMemberFormInputValues = { + name?: string; + teamID?: string; + Team?: Team; +}; +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 type MyMemberFormProps = React.PropsWithChildren< + { + overrides?: MyMemberFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: (fields: MyMemberFormInputValues) => MyMemberFormInputValues; + onSuccess?: (fields: MyMemberFormInputValues) => void; + onError?: (fields: MyMemberFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: MyMemberFormInputValues) => MyMemberFormInputValues; + onValidate?: MyMemberFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = {label}; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 ? ( + <> + + {errorMessage && hasError && ( + + {errorMessage} + + )} + + ) : ( + + {(currentFieldValue || isEditing) && ( + + )} + + + )} + {arraySection} + + ); +} +export default function MyMemberForm( + props: MyMemberFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = 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); + setCurrentTeamIDDisplayValue(\\"\\"); + setTeam(initialValues.Team); + setCurrentTeamValue(undefined); + setCurrentTeamDisplayValue(\\"\\"); + setErrors({}); + }; + const [currentTeamIDDisplayValue, setCurrentTeamIDDisplayValue] = + React.useState(\\"\\"); + const [currentTeamIDValue, setCurrentTeamIDValue] = React.useState(undefined); + const teamIDRef = React.createRef(); + const [currentTeamDisplayValue, setCurrentTeamDisplayValue] = + React.useState(\\"\\"); + const [currentTeamValue, setCurrentTeamValue] = React.useState(undefined); + const TeamRef = React.createRef(); + const getIDValue = { + Team: (r) => JSON.stringify({ id: r?.id }), + }; + const TeamIdSet = new Set( + Array.isArray(Team) + ? Team.map((r) => getIDValue.Team?.(r)) + : getIDValue.Team?.(Team) + ); + const teamRecords = await API.graphql({ query: listTeams }).data.listTeams; + const getDisplayValue = { + teamID: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + Team: (r) => r?.name, + }; + const validations = { + name: [], + teamID: [{ type: \\"Required\\" }], + Team: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + { + event.preventDefault(); + let modelFields = { + name, + teamID, + Team, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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 API.graphql({ + query: createMember, + variables: { input: modelFields }, + }); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"MyMemberForm\\")} + {...rest} + > + + + + + + + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + teamID, + Team, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + > + { + let value = items[0]; + 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} + errorMessage={errors?.teamID?.errorMessage} + getBadgeText={(value) => + value + ? getDisplayValue.teamID(teamRecords.find((r) => r.id === value)) + : \\"\\" + } + setFieldValue={(value) => { + setCurrentTeamIDDisplayValue( + value + ? getDisplayValue.teamID(teamRecords.find((r) => r.id === value)) + : \\"\\" + ); + setCurrentTeamIDValue(value); + }} + inputFieldRef={teamIDRef} + defaultFieldValue={\\"\\"} + > + + arr.findIndex((member) => member?.id === r?.id) === i + ) + .map((r) => ({ + id: r?.id, + label: getDisplayValue.teamID?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentTeamIDValue(id); + setCurrentTeamIDDisplayValue(label); + runValidationTasks(\\"teamID\\", label); + }} + onClear={() => { + setCurrentTeamIDDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.teamID?.hasError) { + runValidationTasks(\\"teamID\\", value); + } + setCurrentTeamIDDisplayValue(value); + setCurrentTeamIDValue(undefined); + }} + 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); + value = result?.Team ?? value; + } + setTeam(value); + setCurrentTeamValue(undefined); + setCurrentTeamDisplayValue(\\"\\"); + }} + currentFieldValue={currentTeamValue} + label={\\"Team Label\\"} + items={Team ? [Team] : []} + hasError={errors?.Team?.hasError} + errorMessage={errors?.Team?.errorMessage} + getBadgeText={getDisplayValue.Team} + setFieldValue={(model) => { + setCurrentTeamDisplayValue(model ? getDisplayValue.Team(model) : \\"\\"); + setCurrentTeamValue(model); + }} + inputFieldRef={TeamRef} + defaultFieldValue={\\"\\"} + > + !TeamIdSet.has(getIDValue.Team?.(r))) + .map((r) => ({ + id: getIDValue.Team?.(r), + label: getDisplayValue.Team?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentTeamValue( + teamRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentTeamDisplayValue(label); + runValidationTasks(\\"Team\\", label); + }} + onClear={() => { + setCurrentTeamDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.Team?.hasError) { + runValidationTasks(\\"Team\\", value); + } + setCurrentTeamDisplayValue(value); + setCurrentTeamValue(undefined); + }} + onBlur={() => runValidationTasks(\\"Team\\", currentTeamDisplayValue)} + errorMessage={errors.Team?.errorMessage} + hasError={errors.Team?.hasError} + ref={TeamRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"Team\\")} + > + + + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should generate a create form with hasMany relationship 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { School, Student } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listStudents } from \\"../graphql/queries\\"; +import { createSchool, updateSchool } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise; +export declare type SchoolCreateFormInputValues = { + name?: string; + Students?: Student[]; +}; +export declare type SchoolCreateFormValidationValues = { + name?: ValidationFunction; + Students?: ValidationFunction; +}; +export declare type PrimitiveOverrideProps = Partial & + React.DOMAttributes; +export declare type SchoolCreateFormOverridesProps = { + SchoolCreateFormGrid?: PrimitiveOverrideProps; + name?: PrimitiveOverrideProps; + Students?: PrimitiveOverrideProps; +} & EscapeHatchProps; +export type SchoolCreateFormProps = React.PropsWithChildren< + { + overrides?: SchoolCreateFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: ( + fields: SchoolCreateFormInputValues + ) => SchoolCreateFormInputValues; + onSuccess?: (fields: SchoolCreateFormInputValues) => void; + onError?: ( + fields: SchoolCreateFormInputValues, + errorMessage: string + ) => void; + onCancel?: () => void; + onChange?: ( + fields: SchoolCreateFormInputValues + ) => SchoolCreateFormInputValues; + onValidate?: SchoolCreateFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = {label}; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 ? ( + <> + + {errorMessage && hasError && ( + + {errorMessage} + + )} + + ) : ( + + {(currentFieldValue || isEditing) && ( + + )} + + + )} + {arraySection} + + ); +} +export default function SchoolCreateForm( + props: SchoolCreateFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + Students: [], + }; + const [name, setName] = React.useState(initialValues.name); + const [Students, setStudents] = React.useState(initialValues.Students); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setStudents(initialValues.Students); + setCurrentStudentsValue(undefined); + setCurrentStudentsDisplayValue(\\"\\"); + setErrors({}); + }; + const [currentStudentsDisplayValue, setCurrentStudentsDisplayValue] = + React.useState(\\"\\"); + const [currentStudentsValue, setCurrentStudentsValue] = + React.useState(undefined); + const StudentsRef = React.createRef(); + const getIDValue = { + Students: (r) => JSON.stringify({ id: r?.id }), + }; + const StudentsIdSet = new Set( + Array.isArray(Students) + ? Students.map((r) => getIDValue.Students?.(r)) + : getIDValue.Students?.(Students) + ); + const studentRecords = await API.graphql({ query: listStudents }).data + .listStudents; + const getDisplayValue = { + Students: (r) => r?.name, + }; + const validations = { + name: [], + Students: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + { + event.preventDefault(); + let modelFields = { + name, + Students, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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; + } + }); + const modelFieldsToSave = { + name: modelFields.name, + }; + const school = await API.graphql({ + query: createSchool, + variables: { input: modelFieldsToSave }, + }); + const promises = []; + promises.push( + ...Students.reduce((promises, original) => { + promises.push( + API.graphql({ + query: updateSchool, + variables: { input: { ...original, schoolID: school.id } }, + }) + ); + return promises; + }, []) + ); + await Promise.all(promises); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"SchoolCreateForm\\")} + {...rest} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + Students, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + > + { + let values = items; + if (onChange) { + const modelFields = { + name, + Students: values, + }; + const result = onChange(modelFields); + values = result?.Students ?? values; + } + setStudents(values); + setCurrentStudentsValue(undefined); + setCurrentStudentsDisplayValue(\\"\\"); + }} + currentFieldValue={currentStudentsValue} + label={\\"Students\\"} + items={Students} + hasError={errors?.Students?.hasError} + errorMessage={errors?.Students?.errorMessage} + getBadgeText={getDisplayValue.Students} + setFieldValue={(model) => { + setCurrentStudentsDisplayValue( + model ? getDisplayValue.Students(model) : \\"\\" + ); + setCurrentStudentsValue(model); + }} + inputFieldRef={StudentsRef} + defaultFieldValue={\\"\\"} + > + !StudentsIdSet.has(getIDValue.Students?.(r))) + .map((r) => ({ + id: getIDValue.Students?.(r), + label: getDisplayValue.Students?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentStudentsValue( + studentRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentStudentsDisplayValue(label); + runValidationTasks(\\"Students\\", label); + }} + onClear={() => { + setCurrentStudentsDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.Students?.hasError) { + runValidationTasks(\\"Students\\", value); + } + setCurrentStudentsDisplayValue(value); + setCurrentStudentsValue(undefined); + }} + onBlur={() => + runValidationTasks(\\"Students\\", currentStudentsDisplayValue) + } + errorMessage={errors.Students?.errorMessage} + hasError={errors.Students?.hasError} + ref={StudentsRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"Students\\")} + > + + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should generate a create form with hasOne relationship 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Author, Book } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listAuthors } from \\"../graphql/queries\\"; +import { createBook } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise; +export declare type BookCreateFormInputValues = { + name?: string; + primaryAuthor?: Author; +}; +export declare type BookCreateFormValidationValues = { + name?: ValidationFunction; + primaryAuthor?: ValidationFunction; +}; +export declare type PrimitiveOverrideProps = Partial & + React.DOMAttributes; +export declare type BookCreateFormOverridesProps = { + BookCreateFormGrid?: PrimitiveOverrideProps; + name?: PrimitiveOverrideProps; + primaryAuthor?: PrimitiveOverrideProps; +} & EscapeHatchProps; +export type BookCreateFormProps = React.PropsWithChildren< + { + overrides?: BookCreateFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: (fields: BookCreateFormInputValues) => BookCreateFormInputValues; + onSuccess?: (fields: BookCreateFormInputValues) => void; + onError?: (fields: BookCreateFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: BookCreateFormInputValues) => BookCreateFormInputValues; + onValidate?: BookCreateFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = {label}; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 ? ( + <> + + {errorMessage && hasError && ( + + {errorMessage} + + )} + + ) : ( + + {(currentFieldValue || isEditing) && ( + + )} + + + )} + {arraySection} + + ); +} +export default function BookCreateForm( + props: BookCreateFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + primaryAuthor: undefined, + }; + const [name, setName] = React.useState(initialValues.name); + const [primaryAuthor, setPrimaryAuthor] = React.useState( + initialValues.primaryAuthor + ); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setPrimaryAuthor(initialValues.primaryAuthor); + setCurrentPrimaryAuthorValue(undefined); + setCurrentPrimaryAuthorDisplayValue(\\"\\"); + setErrors({}); + }; + const [ + currentPrimaryAuthorDisplayValue, + setCurrentPrimaryAuthorDisplayValue, + ] = React.useState(\\"\\"); + const [currentPrimaryAuthorValue, setCurrentPrimaryAuthorValue] = + React.useState(undefined); + const primaryAuthorRef = React.createRef(); + const getIDValue = { + primaryAuthor: (r) => JSON.stringify({ id: r?.id }), + }; + const primaryAuthorIdSet = new Set( + Array.isArray(primaryAuthor) + ? primaryAuthor.map((r) => getIDValue.primaryAuthor?.(r)) + : getIDValue.primaryAuthor?.(primaryAuthor) + ); + const authorRecords = await API.graphql({ query: listAuthors }).data + .listAuthors; + const getDisplayValue = { + primaryAuthor: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + }; + const validations = { + name: [], + primaryAuthor: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + { + event.preventDefault(); + let modelFields = { + name, + primaryAuthor, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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 API.graphql({ + query: createBook, + variables: { input: modelFields }, + }); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"BookCreateForm\\")} + {...rest} + > + + + + + + + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + primaryAuthor, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + > + { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + primaryAuthor: value, + }; + const result = onChange(modelFields); + value = result?.primaryAuthor ?? value; + } + setPrimaryAuthor(value); + setCurrentPrimaryAuthorValue(undefined); + setCurrentPrimaryAuthorDisplayValue(\\"\\"); + }} + currentFieldValue={currentPrimaryAuthorValue} + label={\\"Primary author\\"} + items={primaryAuthor ? [primaryAuthor] : []} + hasError={errors?.primaryAuthor?.hasError} + errorMessage={errors?.primaryAuthor?.errorMessage} + getBadgeText={getDisplayValue.primaryAuthor} + setFieldValue={(model) => { + setCurrentPrimaryAuthorDisplayValue( + model ? getDisplayValue.primaryAuthor(model) : \\"\\" + ); + setCurrentPrimaryAuthorValue(model); + }} + inputFieldRef={primaryAuthorRef} + defaultFieldValue={\\"\\"} + > + !primaryAuthorIdSet.has(getIDValue.primaryAuthor?.(r)) + ) + .map((r) => ({ + id: getIDValue.primaryAuthor?.(r), + label: getDisplayValue.primaryAuthor?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentPrimaryAuthorValue( + authorRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentPrimaryAuthorDisplayValue(label); + runValidationTasks(\\"primaryAuthor\\", label); + }} + onClear={() => { + setCurrentPrimaryAuthorDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.primaryAuthor?.hasError) { + runValidationTasks(\\"primaryAuthor\\", value); + } + setCurrentPrimaryAuthorDisplayValue(value); + setCurrentPrimaryAuthorValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"primaryAuthor\\", + currentPrimaryAuthorDisplayValue + ) + } + errorMessage={errors.primaryAuthor?.errorMessage} + hasError={errors.primaryAuthor?.hasError} + ref={primaryAuthorRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"primaryAuthor\\")} + > + + + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should generate a create form with manyToMany relationship 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + SelectField, + SelectFieldProps, + Text, + TextField, + TextFieldProps, + option, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Post, Tag, TagPost } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listPosts } from \\"../graphql/queries\\"; +import { createTag, createTagPost } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise; +export declare type TagCreateFormInputValues = { + label?: string; + Posts?: Post[]; + statuses?: string[]; +}; +export declare type TagCreateFormValidationValues = { + label?: ValidationFunction; + Posts?: ValidationFunction; + statuses?: ValidationFunction; +}; +export declare type PrimitiveOverrideProps = Partial & + React.DOMAttributes; +export declare type TagCreateFormOverridesProps = { + TagCreateFormGrid?: PrimitiveOverrideProps; + label?: PrimitiveOverrideProps; + Posts?: PrimitiveOverrideProps; + statuses?: PrimitiveOverrideProps; +} & EscapeHatchProps; +export type TagCreateFormProps = React.PropsWithChildren< + { + overrides?: TagCreateFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: (fields: TagCreateFormInputValues) => TagCreateFormInputValues; + onSuccess?: (fields: TagCreateFormInputValues) => void; + onError?: (fields: TagCreateFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: TagCreateFormInputValues) => TagCreateFormInputValues; + onValidate?: TagCreateFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = {label}; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 ? ( + <> + + {errorMessage && hasError && ( + + {errorMessage} + + )} + + ) : ( + + {(currentFieldValue || isEditing) && ( + + )} + + + )} + {arraySection} + + ); +} +export default function TagCreateForm( + props: TagCreateFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + label: \\"\\", + Posts: [], + statuses: [], + }; + const [label, setLabel] = React.useState(initialValues.label); + const [Posts, setPosts] = React.useState(initialValues.Posts); + const [statuses, setStatuses] = React.useState(initialValues.statuses); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setLabel(initialValues.label); + setPosts(initialValues.Posts); + setCurrentPostsValue(undefined); + setCurrentPostsDisplayValue(\\"\\"); + setStatuses(initialValues.statuses); + setCurrentStatusesValue(\\"\\"); + setErrors({}); + }; + const [currentPostsDisplayValue, setCurrentPostsDisplayValue] = + React.useState(\\"\\"); + const [currentPostsValue, setCurrentPostsValue] = React.useState(undefined); + const PostsRef = React.createRef(); + const [currentStatusesValue, setCurrentStatusesValue] = React.useState(\\"\\"); + const statusesRef = React.createRef(); + const getIDValue = { + Posts: (r) => JSON.stringify({ id: r?.id }), + }; + const PostsIdSet = new Set( + Array.isArray(Posts) + ? Posts.map((r) => getIDValue.Posts?.(r)) + : getIDValue.Posts?.(Posts) + ); + const postRecords = await API.graphql({ query: listPosts }).data.listPosts; + const getDisplayValue = { + Posts: (r) => r?.title, + statuses: (r) => { + const enumDisplayValueMap = { + PENDING: \\"Pending\\", + POSTED: \\"Posted\\", + IN_REVIEW: \\"In review\\", + }; + return enumDisplayValueMap[r]; + }, + }; + const validations = { + label: [], + Posts: [], + statuses: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + { + event.preventDefault(); + let modelFields = { + label, + Posts, + statuses, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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; + } + }); + const modelFieldsToSave = { + label: modelFields.label, + statuses: modelFields.statuses, + }; + const tag = await API.graphql({ + query: createTag, + variables: { input: modelFieldsToSave }, + }); + const promises = []; + promises.push( + ...Posts.reduce((promises, post) => { + promises.push( + API.graphql({ + query: createTagPost, + variables: { + input: { + tag, + post, + }, + }, + }) + ); + return promises; + }, []) + ); + await Promise.all(promises); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"TagCreateForm\\")} + {...rest} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + label: value, + Posts, + statuses, + }; + const result = onChange(modelFields); + value = result?.label ?? value; + } + if (errors.label?.hasError) { + runValidationTasks(\\"label\\", value); + } + setLabel(value); + }} + onBlur={() => runValidationTasks(\\"label\\", label)} + errorMessage={errors.label?.errorMessage} + hasError={errors.label?.hasError} + {...getOverrideProps(overrides, \\"label\\")} + > + { + let values = items; + if (onChange) { + const modelFields = { + label, + Posts: values, + statuses, + }; + const result = onChange(modelFields); + values = result?.Posts ?? values; + } + setPosts(values); + setCurrentPostsValue(undefined); + setCurrentPostsDisplayValue(\\"\\"); + }} + currentFieldValue={currentPostsValue} + label={\\"Posts\\"} + items={Posts} + hasError={errors?.Posts?.hasError} + errorMessage={errors?.Posts?.errorMessage} + getBadgeText={getDisplayValue.Posts} + setFieldValue={(model) => { + setCurrentPostsDisplayValue( + model ? getDisplayValue.Posts(model) : \\"\\" + ); + setCurrentPostsValue(model); + }} + inputFieldRef={PostsRef} + defaultFieldValue={\\"\\"} + > + !PostsIdSet.has(getIDValue.Posts?.(r))) + .map((r) => ({ + id: getIDValue.Posts?.(r), + label: getDisplayValue.Posts?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentPostsValue( + postRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentPostsDisplayValue(label); + runValidationTasks(\\"Posts\\", label); + }} + onClear={() => { + setCurrentPostsDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.Posts?.hasError) { + runValidationTasks(\\"Posts\\", value); + } + setCurrentPostsDisplayValue(value); + setCurrentPostsValue(undefined); + }} + onBlur={() => runValidationTasks(\\"Posts\\", currentPostsDisplayValue)} + errorMessage={errors.Posts?.errorMessage} + hasError={errors.Posts?.hasError} + ref={PostsRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"Posts\\")} + > + + { + let values = items; + if (onChange) { + const modelFields = { + label, + Posts, + statuses: values, + }; + const result = onChange(modelFields); + values = result?.statuses ?? values; + } + setStatuses(values); + setCurrentStatusesValue(\\"\\"); + }} + currentFieldValue={currentStatusesValue} + label={\\"Statuses\\"} + items={statuses} + hasError={errors?.statuses?.hasError} + errorMessage={errors?.statuses?.errorMessage} + getBadgeText={getDisplayValue.statuses} + setFieldValue={setCurrentStatusesValue} + inputFieldRef={statusesRef} + defaultFieldValue={\\"\\"} + > + { + let { value } = e.target; + if (errors.statuses?.hasError) { + runValidationTasks(\\"statuses\\", value); + } + setCurrentStatusesValue(value); + }} + onBlur={() => runValidationTasks(\\"statuses\\", currentStatusesValue)} + errorMessage={errors.statuses?.errorMessage} + hasError={errors.statuses?.hasError} + ref={statusesRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"statuses\\")} + > + + + + + + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should generate a create form with multiple hasOne relationships 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Author, Book, Title } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listAuthors, listTitles } from \\"../graphql/queries\\"; +import { createBook } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise; +export declare type BookCreateFormInputValues = { + name?: string; + primaryAuthor?: Author; + primaryTitle?: Title; +}; +export declare type BookCreateFormValidationValues = { + name?: ValidationFunction; + primaryAuthor?: ValidationFunction; + primaryTitle?: ValidationFunction; +}; +export declare type PrimitiveOverrideProps<T> = Partial<T> & + React.DOMAttributes<HTMLDivElement>; +export declare type BookCreateFormOverridesProps = { + BookCreateFormGrid?: PrimitiveOverrideProps<GridProps>; + name?: PrimitiveOverrideProps<TextFieldProps>; + primaryAuthor?: PrimitiveOverrideProps<AutocompleteProps>; + primaryTitle?: PrimitiveOverrideProps<AutocompleteProps>; +} & EscapeHatchProps; +export type BookCreateFormProps = React.PropsWithChildren< + { + overrides?: BookCreateFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: (fields: BookCreateFormInputValues) => BookCreateFormInputValues; + onSuccess?: (fields: BookCreateFormInputValues) => void; + onError?: (fields: BookCreateFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: BookCreateFormInputValues) => BookCreateFormInputValues; + onValidate?: BookCreateFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = <Text>{label}</Text>; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 = ( + <React.Fragment> + {!!items?.length && ( + <ScrollView height=\\"inherit\\" width=\\"inherit\\" maxHeight={\\"7rem\\"}> + {items.map((value, index) => { + return ( + <Badge + key={index} + style={{ + cursor: \\"pointer\\", + alignItems: \\"center\\", + marginRight: 3, + marginTop: 3, + backgroundColor: + index === selectedBadgeIndex ? \\"#B8CEF9\\" : \\"\\", + }} + onClick={() => { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + <Icon + style={{ + cursor: \\"pointer\\", + paddingLeft: 3, + width: 20, + height: 20, + }} + viewBox={{ width: 20, height: 20 }} + paths={[ + { + d: \\"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\\", + stroke: \\"black\\", + }, + ]} + ariaLabel=\\"button\\" + onClick={(event) => { + event.stopPropagation(); + removeItem(index); + }} + /> + </Badge> + ); + })} + </ScrollView> + )} + <Divider orientation=\\"horizontal\\" marginTop={5} /> + </React.Fragment> + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + <React.Fragment> + {labelElement} + {arraySection} + </React.Fragment> + ); + } + return ( + <React.Fragment> + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + <Button + onClick={() => { + setIsEditing(true); + }} + > + Add item + </Button> + {errorMessage && hasError && ( + <Text color={errorStyles.color} fontSize={errorStyles.fontSize}> + {errorMessage} + </Text> + )} + </> + ) : ( + <Flex justifyContent=\\"flex-end\\"> + {(currentFieldValue || isEditing) && ( + <Button + children=\\"Cancel\\" + type=\\"button\\" + size=\\"small\\" + onClick={() => { + setFieldValue(defaultFieldValue); + setIsEditing(false); + setSelectedBadgeIndex(undefined); + }} + ></Button> + )} + <Button + size=\\"small\\" + variation=\\"link\\" + isDisabled={hasError} + onClick={addItem} + > + {selectedBadgeIndex !== undefined ? \\"Save\\" : \\"Add\\"} + </Button> + </Flex> + )} + {arraySection} + </React.Fragment> + ); +} +export default function BookCreateForm( + props: BookCreateFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + primaryAuthor: undefined, + primaryTitle: undefined, + }; + const [name, setName] = React.useState(initialValues.name); + const [primaryAuthor, setPrimaryAuthor] = React.useState( + initialValues.primaryAuthor + ); + const [primaryTitle, setPrimaryTitle] = React.useState( + initialValues.primaryTitle + ); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setPrimaryAuthor(initialValues.primaryAuthor); + setCurrentPrimaryAuthorValue(undefined); + setCurrentPrimaryAuthorDisplayValue(\\"\\"); + setPrimaryTitle(initialValues.primaryTitle); + setCurrentPrimaryTitleValue(undefined); + setCurrentPrimaryTitleDisplayValue(\\"\\"); + setErrors({}); + }; + const [ + currentPrimaryAuthorDisplayValue, + setCurrentPrimaryAuthorDisplayValue, + ] = React.useState(\\"\\"); + const [currentPrimaryAuthorValue, setCurrentPrimaryAuthorValue] = + React.useState(undefined); + const primaryAuthorRef = React.createRef(); + const [currentPrimaryTitleDisplayValue, setCurrentPrimaryTitleDisplayValue] = + React.useState(\\"\\"); + const [currentPrimaryTitleValue, setCurrentPrimaryTitleValue] = + React.useState(undefined); + const primaryTitleRef = React.createRef(); + const getIDValue = { + primaryAuthor: (r) => JSON.stringify({ id: r?.id }), + primaryTitle: (r) => JSON.stringify({ id: r?.id }), + }; + const primaryAuthorIdSet = new Set( + Array.isArray(primaryAuthor) + ? primaryAuthor.map((r) => getIDValue.primaryAuthor?.(r)) + : getIDValue.primaryAuthor?.(primaryAuthor) + ); + const primaryTitleIdSet = new Set( + Array.isArray(primaryTitle) + ? primaryTitle.map((r) => getIDValue.primaryTitle?.(r)) + : getIDValue.primaryTitle?.(primaryTitle) + ); + const authorRecords = await API.graphql({ query: listAuthors }).data + .listAuthors; + const titleRecords = await API.graphql({ query: listTitles }).data.listTitles; + const getDisplayValue = { + primaryAuthor: (r) => r?.name, + primaryTitle: (r) => r?.name, + }; + const validations = { + name: [], + primaryAuthor: [], + primaryTitle: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + <Grid + as=\\"form\\" + rowGap=\\"15px\\" + columnGap=\\"15px\\" + padding=\\"20px\\" + onSubmit={async (event: SyntheticEvent) => { + event.preventDefault(); + let modelFields = { + name, + primaryAuthor, + primaryTitle, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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 API.graphql({ + query: createBook, + variables: { input: modelFields }, + }); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"BookCreateForm\\")} + {...rest} + > + <Flex + justifyContent=\\"space-between\\" + {...getOverrideProps(overrides, \\"CTAFlex\\")} + > + <Button + children=\\"Clear\\" + type=\\"reset\\" + onClick={(event: SyntheticEvent) => { + event.preventDefault(); + resetStateValues(); + }} + {...getOverrideProps(overrides, \\"ClearButton\\")} + ></Button> + <Flex + gap=\\"15px\\" + {...getOverrideProps(overrides, \\"RightAlignCTASubFlex\\")} + > + <Button + children=\\"Cancel\\" + type=\\"button\\" + onClick={() => { + onCancel && onCancel(); + }} + {...getOverrideProps(overrides, \\"CancelButton\\")} + ></Button> + <Button + children=\\"Submit\\" + type=\\"submit\\" + variation=\\"primary\\" + isDisabled={Object.values(errors).some((e) => e?.hasError)} + {...getOverrideProps(overrides, \\"SubmitButton\\")} + ></Button> + </Flex> + </Flex> + <TextField + label=\\"Name\\" + isRequired={false} + isReadOnly={false} + value={name} + onChange={(e) => { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + primaryAuthor, + primaryTitle, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + ></TextField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + primaryAuthor: value, + primaryTitle, + }; + const result = onChange(modelFields); + value = result?.primaryAuthor ?? value; + } + setPrimaryAuthor(value); + setCurrentPrimaryAuthorValue(undefined); + setCurrentPrimaryAuthorDisplayValue(\\"\\"); + }} + currentFieldValue={currentPrimaryAuthorValue} + label={\\"Primary author\\"} + items={primaryAuthor ? [primaryAuthor] : []} + hasError={errors?.primaryAuthor?.hasError} + errorMessage={errors?.primaryAuthor?.errorMessage} + getBadgeText={getDisplayValue.primaryAuthor} + setFieldValue={(model) => { + setCurrentPrimaryAuthorDisplayValue( + model ? getDisplayValue.primaryAuthor(model) : \\"\\" + ); + setCurrentPrimaryAuthorValue(model); + }} + inputFieldRef={primaryAuthorRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Primary author\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search Author\\" + value={currentPrimaryAuthorDisplayValue} + options={authorRecords + .filter( + (r) => !primaryAuthorIdSet.has(getIDValue.primaryAuthor?.(r)) + ) + .map((r) => ({ + id: getIDValue.primaryAuthor?.(r), + label: getDisplayValue.primaryAuthor?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentPrimaryAuthorValue( + authorRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentPrimaryAuthorDisplayValue(label); + runValidationTasks(\\"primaryAuthor\\", label); + }} + onClear={() => { + setCurrentPrimaryAuthorDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.primaryAuthor?.hasError) { + runValidationTasks(\\"primaryAuthor\\", value); + } + setCurrentPrimaryAuthorDisplayValue(value); + setCurrentPrimaryAuthorValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"primaryAuthor\\", + currentPrimaryAuthorDisplayValue + ) + } + errorMessage={errors.primaryAuthor?.errorMessage} + hasError={errors.primaryAuthor?.hasError} + ref={primaryAuthorRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"primaryAuthor\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + primaryAuthor, + primaryTitle: value, + }; + const result = onChange(modelFields); + value = result?.primaryTitle ?? value; + } + setPrimaryTitle(value); + setCurrentPrimaryTitleValue(undefined); + setCurrentPrimaryTitleDisplayValue(\\"\\"); + }} + currentFieldValue={currentPrimaryTitleValue} + label={\\"Primary title\\"} + items={primaryTitle ? [primaryTitle] : []} + hasError={errors?.primaryTitle?.hasError} + errorMessage={errors?.primaryTitle?.errorMessage} + getBadgeText={getDisplayValue.primaryTitle} + setFieldValue={(model) => { + setCurrentPrimaryTitleDisplayValue( + model ? getDisplayValue.primaryTitle(model) : \\"\\" + ); + setCurrentPrimaryTitleValue(model); + }} + inputFieldRef={primaryTitleRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Primary title\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search Title\\" + value={currentPrimaryTitleDisplayValue} + options={titleRecords + .filter((r) => !primaryTitleIdSet.has(getIDValue.primaryTitle?.(r))) + .map((r) => ({ + id: getIDValue.primaryTitle?.(r), + label: getDisplayValue.primaryTitle?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentPrimaryTitleValue( + titleRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentPrimaryTitleDisplayValue(label); + runValidationTasks(\\"primaryTitle\\", label); + }} + onClear={() => { + setCurrentPrimaryTitleDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.primaryTitle?.hasError) { + runValidationTasks(\\"primaryTitle\\", value); + } + setCurrentPrimaryTitleDisplayValue(value); + setCurrentPrimaryTitleValue(undefined); + }} + onBlur={() => + runValidationTasks(\\"primaryTitle\\", currentPrimaryTitleDisplayValue) + } + errorMessage={errors.primaryTitle?.errorMessage} + hasError={errors.primaryTitle?.hasError} + ref={primaryTitleRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"primaryTitle\\")} + ></Autocomplete> + </ArrayField> + </Grid> + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should render a create form for child of 1:m relationship 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { CompositeDog, CompositeToy } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listCompositeDogs } from \\"../graphql/queries\\"; +import { createCompositeToy } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction<T> = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise<ValidationResponse>; +export declare type CreateCompositeToyFormInputValues = { + kind?: string; + color?: string; + compositeDogCompositeToysName?: string; + compositeDogCompositeToysDescription?: string; +}; +export declare type CreateCompositeToyFormValidationValues = { + kind?: ValidationFunction<string>; + color?: ValidationFunction<string>; + compositeDogCompositeToysName?: ValidationFunction<string>; + compositeDogCompositeToysDescription?: ValidationFunction<string>; +}; +export declare type PrimitiveOverrideProps<T> = Partial<T> & + React.DOMAttributes<HTMLDivElement>; +export declare type CreateCompositeToyFormOverridesProps = { + CreateCompositeToyFormGrid?: PrimitiveOverrideProps<GridProps>; + kind?: PrimitiveOverrideProps<TextFieldProps>; + color?: PrimitiveOverrideProps<TextFieldProps>; + compositeDogCompositeToysName?: PrimitiveOverrideProps<AutocompleteProps>; + compositeDogCompositeToysDescription?: PrimitiveOverrideProps<AutocompleteProps>; +} & EscapeHatchProps; +export 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 +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = <Text>{label}</Text>; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 = ( + <React.Fragment> + {!!items?.length && ( + <ScrollView height=\\"inherit\\" width=\\"inherit\\" maxHeight={\\"7rem\\"}> + {items.map((value, index) => { + return ( + <Badge + key={index} + style={{ + cursor: \\"pointer\\", + alignItems: \\"center\\", + marginRight: 3, + marginTop: 3, + backgroundColor: + index === selectedBadgeIndex ? \\"#B8CEF9\\" : \\"\\", + }} + onClick={() => { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + <Icon + style={{ + cursor: \\"pointer\\", + paddingLeft: 3, + width: 20, + height: 20, + }} + viewBox={{ width: 20, height: 20 }} + paths={[ + { + d: \\"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\\", + stroke: \\"black\\", + }, + ]} + ariaLabel=\\"button\\" + onClick={(event) => { + event.stopPropagation(); + removeItem(index); + }} + /> + </Badge> + ); + })} + </ScrollView> + )} + <Divider orientation=\\"horizontal\\" marginTop={5} /> + </React.Fragment> + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + <React.Fragment> + {labelElement} + {arraySection} + </React.Fragment> + ); + } + return ( + <React.Fragment> + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + <Button + onClick={() => { + setIsEditing(true); + }} + > + Add item + </Button> + {errorMessage && hasError && ( + <Text color={errorStyles.color} fontSize={errorStyles.fontSize}> + {errorMessage} + </Text> + )} + </> + ) : ( + <Flex justifyContent=\\"flex-end\\"> + {(currentFieldValue || isEditing) && ( + <Button + children=\\"Cancel\\" + type=\\"button\\" + size=\\"small\\" + onClick={() => { + setFieldValue(defaultFieldValue); + setIsEditing(false); + setSelectedBadgeIndex(undefined); + }} + ></Button> + )} + <Button + size=\\"small\\" + variation=\\"link\\" + isDisabled={hasError} + onClick={addItem} + > + {selectedBadgeIndex !== undefined ? \\"Save\\" : \\"Add\\"} + </Button> + </Flex> + )} + {arraySection} + </React.Fragment> + ); +} +export default function CreateCompositeToyForm( + props: CreateCompositeToyFormProps +): React.ReactElement { + 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); + setCurrentCompositeDogCompositeToysNameDisplayValue(\\"\\"); + setCompositeDogCompositeToysDescription( + initialValues.compositeDogCompositeToysDescription + ); + setCurrentCompositeDogCompositeToysDescriptionValue(undefined); + setCurrentCompositeDogCompositeToysDescriptionDisplayValue(\\"\\"); + setErrors({}); + }; + const [ + currentCompositeDogCompositeToysNameDisplayValue, + setCurrentCompositeDogCompositeToysNameDisplayValue, + ] = React.useState(\\"\\"); + const [ + currentCompositeDogCompositeToysNameValue, + setCurrentCompositeDogCompositeToysNameValue, + ] = React.useState(undefined); + const compositeDogCompositeToysNameRef = React.createRef(); + const [ + currentCompositeDogCompositeToysDescriptionDisplayValue, + setCurrentCompositeDogCompositeToysDescriptionDisplayValue, + ] = React.useState(\\"\\"); + const [ + currentCompositeDogCompositeToysDescriptionValue, + setCurrentCompositeDogCompositeToysDescriptionValue, + ] = React.useState(undefined); + const compositeDogCompositeToysDescriptionRef = React.createRef(); + const compositeDogRecords = await API.graphql({ query: listCompositeDogs }) + .data.listCompositeDogs; + const getDisplayValue = { + compositeDogCompositeToysName: (r) => r?.name, + compositeDogCompositeToysDescription: (r) => r?.description, + }; + const validations = { + kind: [{ type: \\"Required\\" }], + color: [{ type: \\"Required\\" }], + compositeDogCompositeToysName: [], + compositeDogCompositeToysDescription: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + <Grid + as=\\"form\\" + rowGap=\\"15px\\" + columnGap=\\"15px\\" + padding=\\"20px\\" + onSubmit={async (event: SyntheticEvent) => { + 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 API.graphql({ + query: createCompositeToy, + variables: { input: modelFields }, + }); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"CreateCompositeToyForm\\")} + {...rest} + > + <TextField + label=\\"Kind\\" + isRequired={true} + isReadOnly={false} + value={kind} + onChange={(e) => { + 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\\")} + ></TextField> + <TextField + label=\\"Color\\" + isRequired={true} + isReadOnly={false} + value={color} + onChange={(e) => { + 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\\")} + ></TextField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + 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} + errorMessage={errors?.compositeDogCompositeToysName?.errorMessage} + getBadgeText={(value) => + value + ? getDisplayValue.compositeDogCompositeToysName( + compositeDogRecords.find((r) => r.name === value) + ) + : \\"\\" + } + setFieldValue={(value) => { + setCurrentCompositeDogCompositeToysNameDisplayValue( + value + ? getDisplayValue.compositeDogCompositeToysName( + compositeDogRecords.find((r) => r.name === value) + ) + : \\"\\" + ); + setCurrentCompositeDogCompositeToysNameValue(value); + }} + inputFieldRef={compositeDogCompositeToysNameRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Composite dog composite toys name\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search CompositeDog\\" + value={currentCompositeDogCompositeToysNameDisplayValue} + options={compositeDogRecords + .filter( + (r, i, arr) => + arr.findIndex((member) => member?.name === r?.name) === i + ) + .map((r) => ({ + id: r?.name, + label: getDisplayValue.compositeDogCompositeToysName?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentCompositeDogCompositeToysNameValue(id); + setCurrentCompositeDogCompositeToysNameDisplayValue(label); + runValidationTasks(\\"compositeDogCompositeToysName\\", label); + }} + onClear={() => { + setCurrentCompositeDogCompositeToysNameDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.compositeDogCompositeToysName?.hasError) { + runValidationTasks(\\"compositeDogCompositeToysName\\", value); + } + setCurrentCompositeDogCompositeToysNameDisplayValue(value); + setCurrentCompositeDogCompositeToysNameValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"compositeDogCompositeToysName\\", + currentCompositeDogCompositeToysNameValue + ) + } + errorMessage={errors.compositeDogCompositeToysName?.errorMessage} + hasError={errors.compositeDogCompositeToysName?.hasError} + ref={compositeDogCompositeToysNameRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"compositeDogCompositeToysName\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + 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} + errorMessage={ + errors?.compositeDogCompositeToysDescription?.errorMessage + } + getBadgeText={(value) => + value + ? getDisplayValue.compositeDogCompositeToysDescription( + compositeDogRecords.find((r) => r.description === value) + ) + : \\"\\" + } + setFieldValue={(value) => { + setCurrentCompositeDogCompositeToysDescriptionDisplayValue( + value + ? getDisplayValue.compositeDogCompositeToysDescription( + compositeDogRecords.find((r) => r.description === value) + ) + : \\"\\" + ); + setCurrentCompositeDogCompositeToysDescriptionValue(value); + }} + inputFieldRef={compositeDogCompositeToysDescriptionRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Composite dog composite toys description\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search CompositeDog\\" + value={currentCompositeDogCompositeToysDescriptionDisplayValue} + options={compositeDogRecords + .filter( + (r, i, arr) => + arr.findIndex( + (member) => member?.description === r?.description + ) === i + ) + .map((r) => ({ + id: r?.description, + label: getDisplayValue.compositeDogCompositeToysDescription?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentCompositeDogCompositeToysDescriptionValue(id); + setCurrentCompositeDogCompositeToysDescriptionDisplayValue(label); + runValidationTasks(\\"compositeDogCompositeToysDescription\\", label); + }} + onClear={() => { + setCurrentCompositeDogCompositeToysDescriptionDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.compositeDogCompositeToysDescription?.hasError) { + runValidationTasks(\\"compositeDogCompositeToysDescription\\", value); + } + setCurrentCompositeDogCompositeToysDescriptionDisplayValue(value); + setCurrentCompositeDogCompositeToysDescriptionValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"compositeDogCompositeToysDescription\\", + currentCompositeDogCompositeToysDescriptionValue + ) + } + errorMessage={ + errors.compositeDogCompositeToysDescription?.errorMessage + } + hasError={errors.compositeDogCompositeToysDescription?.hasError} + ref={compositeDogCompositeToysDescriptionRef} + labelHidden={true} + {...getOverrideProps( + overrides, + \\"compositeDogCompositeToysDescription\\" + )} + ></Autocomplete> + </ArrayField> + <Flex + justifyContent=\\"space-between\\" + {...getOverrideProps(overrides, \\"CTAFlex\\")} + > + <Button + children=\\"Clear\\" + type=\\"reset\\" + onClick={(event: SyntheticEvent) => { + event.preventDefault(); + resetStateValues(); + }} + {...getOverrideProps(overrides, \\"ClearButton\\")} + ></Button> + <Flex + gap=\\"15px\\" + {...getOverrideProps(overrides, \\"RightAlignCTASubFlex\\")} + > + <Button + children=\\"Submit\\" + type=\\"submit\\" + variation=\\"primary\\" + isDisabled={Object.values(errors).some((e) => e?.hasError)} + {...getOverrideProps(overrides, \\"SubmitButton\\")} + ></Button> + </Flex> + </Flex> + </Grid> + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should render a create form for child of 1:m-belongsTo relationship 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Comment, Org, Post, User } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listOrgs, listPosts, listUsers } from \\"../graphql/queries\\"; +import { createComment } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction<T> = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise<ValidationResponse>; +export declare type CreateCommentFormInputValues = { + name?: string; + post?: Post; + User?: User; + Org?: Org; + postCommentsId?: string; +}; +export declare type CreateCommentFormValidationValues = { + name?: ValidationFunction<string>; + post?: ValidationFunction<Post>; + User?: ValidationFunction<User>; + Org?: ValidationFunction<Org>; + postCommentsId?: ValidationFunction<string>; +}; +export declare type PrimitiveOverrideProps<T> = Partial<T> & + React.DOMAttributes<HTMLDivElement>; +export declare type CreateCommentFormOverridesProps = { + CreateCommentFormGrid?: PrimitiveOverrideProps<GridProps>; + name?: PrimitiveOverrideProps<TextFieldProps>; + post?: PrimitiveOverrideProps<AutocompleteProps>; + User?: PrimitiveOverrideProps<AutocompleteProps>; + Org?: PrimitiveOverrideProps<AutocompleteProps>; + postCommentsId?: PrimitiveOverrideProps<AutocompleteProps>; +} & EscapeHatchProps; +export type CreateCommentFormProps = React.PropsWithChildren< + { + overrides?: CreateCommentFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: ( + fields: CreateCommentFormInputValues + ) => CreateCommentFormInputValues; + onSuccess?: (fields: CreateCommentFormInputValues) => void; + onError?: ( + fields: CreateCommentFormInputValues, + errorMessage: string + ) => void; + onChange?: ( + fields: CreateCommentFormInputValues + ) => CreateCommentFormInputValues; + onValidate?: CreateCommentFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = <Text>{label}</Text>; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 = ( + <React.Fragment> + {!!items?.length && ( + <ScrollView height=\\"inherit\\" width=\\"inherit\\" maxHeight={\\"7rem\\"}> + {items.map((value, index) => { + return ( + <Badge + key={index} + style={{ + cursor: \\"pointer\\", + alignItems: \\"center\\", + marginRight: 3, + marginTop: 3, + backgroundColor: + index === selectedBadgeIndex ? \\"#B8CEF9\\" : \\"\\", + }} + onClick={() => { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + <Icon + style={{ + cursor: \\"pointer\\", + paddingLeft: 3, + width: 20, + height: 20, + }} + viewBox={{ width: 20, height: 20 }} + paths={[ + { + d: \\"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\\", + stroke: \\"black\\", + }, + ]} + ariaLabel=\\"button\\" + onClick={(event) => { + event.stopPropagation(); + removeItem(index); + }} + /> + </Badge> + ); + })} + </ScrollView> + )} + <Divider orientation=\\"horizontal\\" marginTop={5} /> + </React.Fragment> + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + <React.Fragment> + {labelElement} + {arraySection} + </React.Fragment> + ); + } + return ( + <React.Fragment> + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + <Button + onClick={() => { + setIsEditing(true); + }} + > + Add item + </Button> + {errorMessage && hasError && ( + <Text color={errorStyles.color} fontSize={errorStyles.fontSize}> + {errorMessage} + </Text> + )} + </> + ) : ( + <Flex justifyContent=\\"flex-end\\"> + {(currentFieldValue || isEditing) && ( + <Button + children=\\"Cancel\\" + type=\\"button\\" + size=\\"small\\" + onClick={() => { + setFieldValue(defaultFieldValue); + setIsEditing(false); + setSelectedBadgeIndex(undefined); + }} + ></Button> + )} + <Button + size=\\"small\\" + variation=\\"link\\" + isDisabled={hasError} + onClick={addItem} + > + {selectedBadgeIndex !== undefined ? \\"Save\\" : \\"Add\\"} + </Button> + </Flex> + )} + {arraySection} + </React.Fragment> + ); +} +export default function CreateCommentForm( + props: CreateCommentFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + post: undefined, + User: undefined, + Org: undefined, + postCommentsId: undefined, + }; + const [name, setName] = React.useState(initialValues.name); + const [post, setPost] = React.useState(initialValues.post); + const [User, setUser] = React.useState(initialValues.User); + const [Org, setOrg] = React.useState(initialValues.Org); + const [postCommentsId, setPostCommentsId] = React.useState( + initialValues.postCommentsId + ); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setPost(initialValues.post); + setCurrentPostValue(undefined); + setCurrentPostDisplayValue(\\"\\"); + setUser(initialValues.User); + setCurrentUserValue(undefined); + setCurrentUserDisplayValue(\\"\\"); + setOrg(initialValues.Org); + setCurrentOrgValue(undefined); + setCurrentOrgDisplayValue(\\"\\"); + setPostCommentsId(initialValues.postCommentsId); + setCurrentPostCommentsIdValue(undefined); + setCurrentPostCommentsIdDisplayValue(\\"\\"); + setErrors({}); + }; + const [currentPostDisplayValue, setCurrentPostDisplayValue] = + React.useState(\\"\\"); + const [currentPostValue, setCurrentPostValue] = React.useState(undefined); + const postRef = React.createRef(); + const [currentUserDisplayValue, setCurrentUserDisplayValue] = + React.useState(\\"\\"); + const [currentUserValue, setCurrentUserValue] = React.useState(undefined); + const UserRef = React.createRef(); + const [currentOrgDisplayValue, setCurrentOrgDisplayValue] = + React.useState(\\"\\"); + const [currentOrgValue, setCurrentOrgValue] = React.useState(undefined); + const OrgRef = React.createRef(); + const [ + currentPostCommentsIdDisplayValue, + setCurrentPostCommentsIdDisplayValue, + ] = React.useState(\\"\\"); + const [currentPostCommentsIdValue, setCurrentPostCommentsIdValue] = + React.useState(undefined); + const postCommentsIdRef = React.createRef(); + const getIDValue = { + post: (r) => JSON.stringify({ id: r?.id }), + User: (r) => JSON.stringify({ id: r?.id }), + Org: (r) => JSON.stringify({ id: r?.id }), + }; + const postIdSet = new Set( + Array.isArray(post) + ? post.map((r) => getIDValue.post?.(r)) + : getIDValue.post?.(post) + ); + const UserIdSet = new Set( + Array.isArray(User) + ? User.map((r) => getIDValue.User?.(r)) + : getIDValue.User?.(User) + ); + const OrgIdSet = new Set( + Array.isArray(Org) + ? Org.map((r) => getIDValue.Org?.(r)) + : getIDValue.Org?.(Org) + ); + const postRecords = await API.graphql({ query: listPosts }).data.listPosts; + const userRecords = await API.graphql({ query: listUsers }).data.listUsers; + const orgRecords = await API.graphql({ query: listOrgs }).data.listOrgs; + const getDisplayValue = { + post: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + User: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + Org: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + postCommentsId: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + }; + const validations = { + name: [{ type: \\"Required\\" }], + post: [], + User: [], + Org: [{ type: \\"Required\\", validationMessage: \\"Org is required.\\" }], + postCommentsId: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + <Grid + as=\\"form\\" + rowGap=\\"15px\\" + columnGap=\\"15px\\" + padding=\\"20px\\" + onSubmit={async (event: SyntheticEvent) => { + event.preventDefault(); + let modelFields = { + name, + post, + User, + Org, + postCommentsId, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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 API.graphql({ + query: createComment, + variables: { input: modelFields }, + }); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"CreateCommentForm\\")} + {...rest} + > + <TextField + label=\\"Name\\" + isRequired={true} + isReadOnly={false} + value={name} + onChange={(e) => { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + post, + User, + Org, + postCommentsId, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + ></TextField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + post: value, + User, + Org, + postCommentsId, + }; + const result = onChange(modelFields); + value = result?.post ?? value; + } + setPost(value); + setCurrentPostValue(undefined); + setCurrentPostDisplayValue(\\"\\"); + }} + currentFieldValue={currentPostValue} + label={\\"Post\\"} + items={post ? [post] : []} + hasError={errors?.post?.hasError} + errorMessage={errors?.post?.errorMessage} + getBadgeText={getDisplayValue.post} + setFieldValue={(model) => { + setCurrentPostDisplayValue(model ? getDisplayValue.post(model) : \\"\\"); + setCurrentPostValue(model); + }} + inputFieldRef={postRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Post\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search Post\\" + value={currentPostDisplayValue} + options={postRecords + .filter((r) => !postIdSet.has(getIDValue.post?.(r))) + .map((r) => ({ + id: getIDValue.post?.(r), + label: getDisplayValue.post?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentPostValue( + postRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentPostDisplayValue(label); + runValidationTasks(\\"post\\", label); + }} + onClear={() => { + setCurrentPostDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.post?.hasError) { + runValidationTasks(\\"post\\", value); + } + setCurrentPostDisplayValue(value); + setCurrentPostValue(undefined); + }} + onBlur={() => runValidationTasks(\\"post\\", currentPostDisplayValue)} + errorMessage={errors.post?.errorMessage} + hasError={errors.post?.hasError} + ref={postRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"post\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + post, + User: value, + Org, + postCommentsId, + }; + const result = onChange(modelFields); + value = result?.User ?? value; + } + setUser(value); + setCurrentUserValue(undefined); + setCurrentUserDisplayValue(\\"\\"); + }} + currentFieldValue={currentUserValue} + label={\\"User\\"} + items={User ? [User] : []} + hasError={errors?.User?.hasError} + errorMessage={errors?.User?.errorMessage} + getBadgeText={getDisplayValue.User} + setFieldValue={(model) => { + setCurrentUserDisplayValue(model ? getDisplayValue.User(model) : \\"\\"); + setCurrentUserValue(model); + }} + inputFieldRef={UserRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"User\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search User\\" + value={currentUserDisplayValue} + options={userRecords + .filter((r) => !UserIdSet.has(getIDValue.User?.(r))) + .map((r) => ({ + id: getIDValue.User?.(r), + label: getDisplayValue.User?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentUserValue( + userRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentUserDisplayValue(label); + runValidationTasks(\\"User\\", label); + }} + onClear={() => { + setCurrentUserDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.User?.hasError) { + runValidationTasks(\\"User\\", value); + } + setCurrentUserDisplayValue(value); + setCurrentUserValue(undefined); + }} + onBlur={() => runValidationTasks(\\"User\\", currentUserDisplayValue)} + errorMessage={errors.User?.errorMessage} + hasError={errors.User?.hasError} + ref={UserRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"User\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + post, + User, + Org: value, + postCommentsId, + }; + const result = onChange(modelFields); + value = result?.Org ?? value; + } + setOrg(value); + setCurrentOrgValue(undefined); + setCurrentOrgDisplayValue(\\"\\"); + }} + currentFieldValue={currentOrgValue} + label={\\"Org\\"} + items={Org ? [Org] : []} + hasError={errors?.Org?.hasError} + errorMessage={errors?.Org?.errorMessage} + getBadgeText={getDisplayValue.Org} + setFieldValue={(model) => { + setCurrentOrgDisplayValue(model ? getDisplayValue.Org(model) : \\"\\"); + setCurrentOrgValue(model); + }} + inputFieldRef={OrgRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Org\\" + isRequired={true} + isReadOnly={false} + placeholder=\\"Search Org\\" + value={currentOrgDisplayValue} + options={orgRecords + .filter((r) => !OrgIdSet.has(getIDValue.Org?.(r))) + .map((r) => ({ + id: getIDValue.Org?.(r), + label: getDisplayValue.Org?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentOrgValue( + orgRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentOrgDisplayValue(label); + runValidationTasks(\\"Org\\", label); + }} + onClear={() => { + setCurrentOrgDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.Org?.hasError) { + runValidationTasks(\\"Org\\", value); + } + setCurrentOrgDisplayValue(value); + setCurrentOrgValue(undefined); + }} + onBlur={() => runValidationTasks(\\"Org\\", currentOrgDisplayValue)} + errorMessage={errors.Org?.errorMessage} + hasError={errors.Org?.hasError} + ref={OrgRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"Org\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + post, + User, + Org, + postCommentsId: value, + }; + const result = onChange(modelFields); + value = result?.postCommentsId ?? value; + } + setPostCommentsId(value); + setCurrentPostCommentsIdValue(undefined); + }} + currentFieldValue={currentPostCommentsIdValue} + label={\\"Post comments id\\"} + items={postCommentsId ? [postCommentsId] : []} + hasError={errors?.postCommentsId?.hasError} + errorMessage={errors?.postCommentsId?.errorMessage} + getBadgeText={(value) => + value + ? getDisplayValue.postCommentsId( + postRecords.find((r) => r.id === value) + ) + : \\"\\" + } + setFieldValue={(value) => { + setCurrentPostCommentsIdDisplayValue( + value + ? getDisplayValue.postCommentsId( + postRecords.find((r) => r.id === value) + ) + : \\"\\" + ); + setCurrentPostCommentsIdValue(value); + }} + inputFieldRef={postCommentsIdRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Post comments id\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search Post\\" + value={currentPostCommentsIdDisplayValue} + options={postRecords + .filter( + (r, i, arr) => + arr.findIndex((member) => member?.id === r?.id) === i + ) + .map((r) => ({ + id: r?.id, + label: getDisplayValue.postCommentsId?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentPostCommentsIdValue(id); + setCurrentPostCommentsIdDisplayValue(label); + runValidationTasks(\\"postCommentsId\\", label); + }} + onClear={() => { + setCurrentPostCommentsIdDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.postCommentsId?.hasError) { + runValidationTasks(\\"postCommentsId\\", value); + } + setCurrentPostCommentsIdDisplayValue(value); + setCurrentPostCommentsIdValue(undefined); + }} + onBlur={() => + runValidationTasks(\\"postCommentsId\\", currentPostCommentsIdValue) + } + errorMessage={errors.postCommentsId?.errorMessage} + hasError={errors.postCommentsId?.hasError} + ref={postCommentsIdRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"postCommentsId\\")} + ></Autocomplete> + </ArrayField> + <Flex + justifyContent=\\"space-between\\" + {...getOverrideProps(overrides, \\"CTAFlex\\")} + > + <Button + children=\\"Clear\\" + type=\\"reset\\" + onClick={(event: SyntheticEvent) => { + event.preventDefault(); + resetStateValues(); + }} + {...getOverrideProps(overrides, \\"ClearButton\\")} + ></Button> + <Flex + gap=\\"15px\\" + {...getOverrideProps(overrides, \\"RightAlignCTASubFlex\\")} + > + <Button + children=\\"Submit\\" + type=\\"submit\\" + variation=\\"primary\\" + isDisabled={Object.values(errors).some((e) => e?.hasError)} + {...getOverrideProps(overrides, \\"SubmitButton\\")} + ></Button> + </Flex> + </Flex> + </Grid> + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should render a create form for model with composite keys 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { + CompositeBowl, + CompositeDog, + CompositeDogCompositeVet, + CompositeOwner, + CompositeToy, + CompositeVet, +} from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { + listCompositeBowls, + listCompositeOwners, + listCompositeToys, + listCompositeVets, +} from \\"../graphql/queries\\"; +import { + createCompositeDog, + createCompositeDogCompositeVet, + updateCompositeDog, + updateCompositeOwner, +} from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction<T> = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise<ValidationResponse>; +export declare type CreateCompositeDogFormInputValues = { + name?: string; + description?: string; + CompositeBowl?: CompositeBowl; + CompositeOwner?: CompositeOwner; + CompositeToys?: CompositeToy[]; + CompositeVets?: CompositeVet[]; +}; +export declare type CreateCompositeDogFormValidationValues = { + name?: ValidationFunction<string>; + description?: ValidationFunction<string>; + CompositeBowl?: ValidationFunction<CompositeBowl>; + CompositeOwner?: ValidationFunction<CompositeOwner>; + CompositeToys?: ValidationFunction<CompositeToy>; + CompositeVets?: ValidationFunction<CompositeVet>; +}; +export declare type PrimitiveOverrideProps<T> = Partial<T> & + React.DOMAttributes<HTMLDivElement>; +export declare type CreateCompositeDogFormOverridesProps = { + CreateCompositeDogFormGrid?: PrimitiveOverrideProps<GridProps>; + name?: PrimitiveOverrideProps<TextFieldProps>; + description?: PrimitiveOverrideProps<TextFieldProps>; + CompositeBowl?: PrimitiveOverrideProps<AutocompleteProps>; + CompositeOwner?: PrimitiveOverrideProps<AutocompleteProps>; + CompositeToys?: PrimitiveOverrideProps<AutocompleteProps>; + CompositeVets?: PrimitiveOverrideProps<AutocompleteProps>; +} & EscapeHatchProps; +export type CreateCompositeDogFormProps = React.PropsWithChildren< + { + overrides?: CreateCompositeDogFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: ( + fields: CreateCompositeDogFormInputValues + ) => CreateCompositeDogFormInputValues; + onSuccess?: (fields: CreateCompositeDogFormInputValues) => void; + onError?: ( + fields: CreateCompositeDogFormInputValues, + errorMessage: string + ) => void; + onChange?: ( + fields: CreateCompositeDogFormInputValues + ) => CreateCompositeDogFormInputValues; + onValidate?: CreateCompositeDogFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = <Text>{label}</Text>; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 = ( + <React.Fragment> + {!!items?.length && ( + <ScrollView height=\\"inherit\\" width=\\"inherit\\" maxHeight={\\"7rem\\"}> + {items.map((value, index) => { + return ( + <Badge + key={index} + style={{ + cursor: \\"pointer\\", + alignItems: \\"center\\", + marginRight: 3, + marginTop: 3, + backgroundColor: + index === selectedBadgeIndex ? \\"#B8CEF9\\" : \\"\\", + }} + onClick={() => { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + <Icon + style={{ + cursor: \\"pointer\\", + paddingLeft: 3, + width: 20, + height: 20, + }} + viewBox={{ width: 20, height: 20 }} + paths={[ + { + d: \\"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\\", + stroke: \\"black\\", + }, + ]} + ariaLabel=\\"button\\" + onClick={(event) => { + event.stopPropagation(); + removeItem(index); + }} + /> + </Badge> + ); + })} + </ScrollView> + )} + <Divider orientation=\\"horizontal\\" marginTop={5} /> + </React.Fragment> + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + <React.Fragment> + {labelElement} + {arraySection} + </React.Fragment> + ); + } + return ( + <React.Fragment> + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + <Button + onClick={() => { + setIsEditing(true); + }} + > + Add item + </Button> + {errorMessage && hasError && ( + <Text color={errorStyles.color} fontSize={errorStyles.fontSize}> + {errorMessage} + </Text> + )} + </> + ) : ( + <Flex justifyContent=\\"flex-end\\"> + {(currentFieldValue || isEditing) && ( + <Button + children=\\"Cancel\\" + type=\\"button\\" + size=\\"small\\" + onClick={() => { + setFieldValue(defaultFieldValue); + setIsEditing(false); + setSelectedBadgeIndex(undefined); + }} + ></Button> + )} + <Button + size=\\"small\\" + variation=\\"link\\" + isDisabled={hasError} + onClick={addItem} + > + {selectedBadgeIndex !== undefined ? \\"Save\\" : \\"Add\\"} + </Button> + </Flex> + )} + {arraySection} + </React.Fragment> + ); +} +export default function CreateCompositeDogForm( + props: CreateCompositeDogFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + description: \\"\\", + CompositeBowl: undefined, + CompositeOwner: undefined, + CompositeToys: [], + CompositeVets: [], + }; + const [name, setName] = React.useState(initialValues.name); + const [description, setDescription] = React.useState( + initialValues.description + ); + const [CompositeBowl, setCompositeBowl] = React.useState( + initialValues.CompositeBowl + ); + const [CompositeOwner, setCompositeOwner] = React.useState( + initialValues.CompositeOwner + ); + const [CompositeToys, setCompositeToys] = React.useState( + initialValues.CompositeToys + ); + const [CompositeVets, setCompositeVets] = React.useState( + initialValues.CompositeVets + ); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setDescription(initialValues.description); + setCompositeBowl(initialValues.CompositeBowl); + setCurrentCompositeBowlValue(undefined); + setCurrentCompositeBowlDisplayValue(\\"\\"); + setCompositeOwner(initialValues.CompositeOwner); + setCurrentCompositeOwnerValue(undefined); + setCurrentCompositeOwnerDisplayValue(\\"\\"); + setCompositeToys(initialValues.CompositeToys); + setCurrentCompositeToysValue(undefined); + setCurrentCompositeToysDisplayValue(\\"\\"); + setCompositeVets(initialValues.CompositeVets); + setCurrentCompositeVetsValue(undefined); + setCurrentCompositeVetsDisplayValue(\\"\\"); + setErrors({}); + }; + const [ + currentCompositeBowlDisplayValue, + setCurrentCompositeBowlDisplayValue, + ] = React.useState(\\"\\"); + const [currentCompositeBowlValue, setCurrentCompositeBowlValue] = + React.useState(undefined); + const CompositeBowlRef = React.createRef(); + const [ + currentCompositeOwnerDisplayValue, + setCurrentCompositeOwnerDisplayValue, + ] = React.useState(\\"\\"); + const [currentCompositeOwnerValue, setCurrentCompositeOwnerValue] = + React.useState(undefined); + const CompositeOwnerRef = React.createRef(); + const [ + currentCompositeToysDisplayValue, + setCurrentCompositeToysDisplayValue, + ] = React.useState(\\"\\"); + const [currentCompositeToysValue, setCurrentCompositeToysValue] = + React.useState(undefined); + const CompositeToysRef = React.createRef(); + const [ + currentCompositeVetsDisplayValue, + setCurrentCompositeVetsDisplayValue, + ] = React.useState(\\"\\"); + const [currentCompositeVetsValue, setCurrentCompositeVetsValue] = + React.useState(undefined); + const CompositeVetsRef = React.createRef(); + const getIDValue = { + CompositeBowl: (r) => JSON.stringify({ shape: r?.shape, size: r?.size }), + CompositeOwner: (r) => + JSON.stringify({ lastName: r?.lastName, firstName: r?.firstName }), + CompositeToys: (r) => JSON.stringify({ kind: r?.kind, color: r?.color }), + CompositeVets: (r) => + JSON.stringify({ specialty: r?.specialty, city: r?.city }), + }; + const CompositeBowlIdSet = new Set( + Array.isArray(CompositeBowl) + ? CompositeBowl.map((r) => getIDValue.CompositeBowl?.(r)) + : getIDValue.CompositeBowl?.(CompositeBowl) + ); + const CompositeOwnerIdSet = new Set( + Array.isArray(CompositeOwner) + ? CompositeOwner.map((r) => getIDValue.CompositeOwner?.(r)) + : getIDValue.CompositeOwner?.(CompositeOwner) + ); + const CompositeToysIdSet = new Set( + Array.isArray(CompositeToys) + ? CompositeToys.map((r) => getIDValue.CompositeToys?.(r)) + : getIDValue.CompositeToys?.(CompositeToys) + ); + const CompositeVetsIdSet = new Set( + Array.isArray(CompositeVets) + ? CompositeVets.map((r) => getIDValue.CompositeVets?.(r)) + : getIDValue.CompositeVets?.(CompositeVets) + ); + const compositeBowlRecords = await API.graphql({ query: listCompositeBowls }) + .data.listCompositeBowls; + const compositeOwnerRecords = await API.graphql({ + query: listCompositeOwners, + }).data.listCompositeOwners; + const compositeToyRecords = await API.graphql({ query: listCompositeToys }) + .data.listCompositeToys; + const compositeVetRecords = await API.graphql({ query: listCompositeVets }) + .data.listCompositeVets; + const getDisplayValue = { + CompositeBowl: (r) => \`\${r?.shape}\${\\"-\\"}\${r?.size}\`, + CompositeOwner: (r) => \`\${r?.lastName}\${\\"-\\"}\${r?.firstName}\`, + CompositeToys: (r) => \`\${r?.kind}\${\\"-\\"}\${r?.color}\`, + CompositeVets: (r) => \`\${r?.specialty}\${\\"-\\"}\${r?.city}\`, + }; + const validations = { + name: [{ type: \\"Required\\" }], + description: [{ type: \\"Required\\" }], + CompositeBowl: [], + CompositeOwner: [], + CompositeToys: [], + CompositeVets: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + <Grid + as=\\"form\\" + rowGap=\\"15px\\" + columnGap=\\"15px\\" + padding=\\"20px\\" + onSubmit={async (event: SyntheticEvent) => { + event.preventDefault(); + let modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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; + } + }); + const modelFieldsToSave = { + name: modelFields.name, + description: modelFields.description, + CompositeBowl: modelFields.CompositeBowl, + CompositeOwner: modelFields.CompositeOwner, + }; + const compositeDog = await API.graphql({ + query: createCompositeDog, + variables: { input: modelFieldsToSave }, + }); + const promises = []; + const compositeOwnerToLink = modelFields.CompositeOwner; + if (compositeOwnerToLink) { + promises.push( + API.graphql({ + query: updateCompositeOwner, + variables: { + input: { + ...CompositeOwner, + CompositeDog: compositeDog, + }, + }, + }) + ); + const compositeDogToUnlink = + await compositeOwnerToLink.CompositeDog; + if (compositeDogToUnlink) { + promises.push( + API.graphql({ + query: updateCompositeDog, + variables: { + input: { + ...compositeDogToUnlink, + CompositeOwner: undefined, + compositeDogCompositeOwnerLastName: undefined, + compositeDogCompositeOwnerFirstName: undefined, + }, + }, + }) + ); + } + } + promises.push( + ...CompositeToys.reduce((promises, original) => { + promises.push( + API.graphql({ + query: updateCompositeDog, + variables: { + input: { + ...original, + compositeDogCompositeToysName: compositeDog.name, + compositeDogCompositeToysDescription: + compositeDog.description, + }, + }, + }) + ); + return promises; + }, []) + ); + promises.push( + ...CompositeVets.reduce((promises, compositeVet) => { + promises.push( + API.graphql({ + query: createCompositeDogCompositeVet, + variables: { + input: { + compositeDog, + compositeVet, + }, + }, + }) + ); + return promises; + }, []) + ); + await Promise.all(promises); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"CreateCompositeDogForm\\")} + {...rest} + > + <TextField + label=\\"Name\\" + isRequired={true} + isReadOnly={false} + value={name} + onChange={(e) => { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + ></TextField> + <TextField + label=\\"Description\\" + isRequired={true} + isReadOnly={false} + value={description} + onChange={(e) => { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + description: value, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + }; + const result = onChange(modelFields); + value = result?.description ?? value; + } + if (errors.description?.hasError) { + runValidationTasks(\\"description\\", value); + } + setDescription(value); + }} + onBlur={() => runValidationTasks(\\"description\\", description)} + errorMessage={errors.description?.errorMessage} + hasError={errors.description?.hasError} + {...getOverrideProps(overrides, \\"description\\")} + ></TextField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl: value, + CompositeOwner, + CompositeToys, + CompositeVets, + }; + const result = onChange(modelFields); + value = result?.CompositeBowl ?? value; + } + setCompositeBowl(value); + setCurrentCompositeBowlValue(undefined); + setCurrentCompositeBowlDisplayValue(\\"\\"); + }} + currentFieldValue={currentCompositeBowlValue} + label={\\"Composite bowl\\"} + items={CompositeBowl ? [CompositeBowl] : []} + hasError={errors?.CompositeBowl?.hasError} + errorMessage={errors?.CompositeBowl?.errorMessage} + getBadgeText={getDisplayValue.CompositeBowl} + setFieldValue={(model) => { + setCurrentCompositeBowlDisplayValue( + model ? getDisplayValue.CompositeBowl(model) : \\"\\" + ); + setCurrentCompositeBowlValue(model); + }} + inputFieldRef={CompositeBowlRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Composite bowl\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search CompositeBowl\\" + value={currentCompositeBowlDisplayValue} + options={compositeBowlRecords + .filter( + (r) => !CompositeBowlIdSet.has(getIDValue.CompositeBowl?.(r)) + ) + .map((r) => ({ + id: getIDValue.CompositeBowl?.(r), + label: getDisplayValue.CompositeBowl?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentCompositeBowlValue( + compositeBowlRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentCompositeBowlDisplayValue(label); + runValidationTasks(\\"CompositeBowl\\", label); + }} + onClear={() => { + setCurrentCompositeBowlDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeBowl?.hasError) { + runValidationTasks(\\"CompositeBowl\\", value); + } + setCurrentCompositeBowlDisplayValue(value); + setCurrentCompositeBowlValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"CompositeBowl\\", + currentCompositeBowlDisplayValue + ) + } + errorMessage={errors.CompositeBowl?.errorMessage} + hasError={errors.CompositeBowl?.hasError} + ref={CompositeBowlRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeBowl\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner: value, + CompositeToys, + CompositeVets, + }; + const result = onChange(modelFields); + value = result?.CompositeOwner ?? value; + } + setCompositeOwner(value); + setCurrentCompositeOwnerValue(undefined); + setCurrentCompositeOwnerDisplayValue(\\"\\"); + }} + currentFieldValue={currentCompositeOwnerValue} + label={\\"Composite owner\\"} + items={CompositeOwner ? [CompositeOwner] : []} + hasError={errors?.CompositeOwner?.hasError} + errorMessage={errors?.CompositeOwner?.errorMessage} + getBadgeText={getDisplayValue.CompositeOwner} + setFieldValue={(model) => { + setCurrentCompositeOwnerDisplayValue( + model ? getDisplayValue.CompositeOwner(model) : \\"\\" + ); + setCurrentCompositeOwnerValue(model); + }} + inputFieldRef={CompositeOwnerRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Composite owner\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search CompositeOwner\\" + value={currentCompositeOwnerDisplayValue} + options={compositeOwnerRecords + .filter( + (r) => !CompositeOwnerIdSet.has(getIDValue.CompositeOwner?.(r)) + ) + .map((r) => ({ + id: getIDValue.CompositeOwner?.(r), + label: getDisplayValue.CompositeOwner?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentCompositeOwnerValue( + compositeOwnerRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentCompositeOwnerDisplayValue(label); + runValidationTasks(\\"CompositeOwner\\", label); + }} + onClear={() => { + setCurrentCompositeOwnerDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeOwner?.hasError) { + runValidationTasks(\\"CompositeOwner\\", value); + } + setCurrentCompositeOwnerDisplayValue(value); + setCurrentCompositeOwnerValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"CompositeOwner\\", + currentCompositeOwnerDisplayValue + ) + } + errorMessage={errors.CompositeOwner?.errorMessage} + hasError={errors.CompositeOwner?.hasError} + ref={CompositeOwnerRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeOwner\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + onChange={async (items) => { + let values = items; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys: values, + CompositeVets, + }; + const result = onChange(modelFields); + values = result?.CompositeToys ?? values; + } + setCompositeToys(values); + setCurrentCompositeToysValue(undefined); + setCurrentCompositeToysDisplayValue(\\"\\"); + }} + currentFieldValue={currentCompositeToysValue} + label={\\"Composite toys\\"} + items={CompositeToys} + hasError={errors?.CompositeToys?.hasError} + errorMessage={errors?.CompositeToys?.errorMessage} + getBadgeText={getDisplayValue.CompositeToys} + setFieldValue={(model) => { + setCurrentCompositeToysDisplayValue( + model ? getDisplayValue.CompositeToys(model) : \\"\\" + ); + setCurrentCompositeToysValue(model); + }} + inputFieldRef={CompositeToysRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Composite toys\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search CompositeToy\\" + value={currentCompositeToysDisplayValue} + options={compositeToyRecords + .filter( + (r) => !CompositeToysIdSet.has(getIDValue.CompositeToys?.(r)) + ) + .map((r) => ({ + id: getIDValue.CompositeToys?.(r), + label: getDisplayValue.CompositeToys?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentCompositeToysValue( + compositeToyRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentCompositeToysDisplayValue(label); + runValidationTasks(\\"CompositeToys\\", label); + }} + onClear={() => { + setCurrentCompositeToysDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeToys?.hasError) { + runValidationTasks(\\"CompositeToys\\", value); + } + setCurrentCompositeToysDisplayValue(value); + setCurrentCompositeToysValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"CompositeToys\\", + currentCompositeToysDisplayValue + ) + } + errorMessage={errors.CompositeToys?.errorMessage} + hasError={errors.CompositeToys?.hasError} + ref={CompositeToysRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeToys\\")} + ></Autocomplete> + </ArrayField> + <ArrayField + onChange={async (items) => { + let values = items; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets: values, + }; + const result = onChange(modelFields); + values = result?.CompositeVets ?? values; + } + setCompositeVets(values); + setCurrentCompositeVetsValue(undefined); + setCurrentCompositeVetsDisplayValue(\\"\\"); + }} + currentFieldValue={currentCompositeVetsValue} + label={\\"Composite vets\\"} + items={CompositeVets} + hasError={errors?.CompositeVets?.hasError} + errorMessage={errors?.CompositeVets?.errorMessage} + getBadgeText={getDisplayValue.CompositeVets} + setFieldValue={(model) => { + setCurrentCompositeVetsDisplayValue( + model ? getDisplayValue.CompositeVets(model) : \\"\\" + ); + setCurrentCompositeVetsValue(model); + }} + inputFieldRef={CompositeVetsRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Composite vets\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search CompositeVet\\" + value={currentCompositeVetsDisplayValue} + options={compositeVetRecords + .filter( + (r) => !CompositeVetsIdSet.has(getIDValue.CompositeVets?.(r)) + ) + .map((r) => ({ + id: getIDValue.CompositeVets?.(r), + label: getDisplayValue.CompositeVets?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentCompositeVetsValue( + compositeVetRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentCompositeVetsDisplayValue(label); + runValidationTasks(\\"CompositeVets\\", label); + }} + onClear={() => { + setCurrentCompositeVetsDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeVets?.hasError) { + runValidationTasks(\\"CompositeVets\\", value); + } + setCurrentCompositeVetsDisplayValue(value); + setCurrentCompositeVetsValue(undefined); + }} + onBlur={() => + runValidationTasks( + \\"CompositeVets\\", + currentCompositeVetsDisplayValue + ) + } + errorMessage={errors.CompositeVets?.errorMessage} + hasError={errors.CompositeVets?.hasError} + ref={CompositeVetsRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeVets\\")} + ></Autocomplete> + </ArrayField> + <Flex + justifyContent=\\"space-between\\" + {...getOverrideProps(overrides, \\"CTAFlex\\")} + > + <Button + children=\\"Clear\\" + type=\\"reset\\" + onClick={(event: SyntheticEvent) => { + event.preventDefault(); + resetStateValues(); + }} + {...getOverrideProps(overrides, \\"ClearButton\\")} + ></Button> + <Flex + gap=\\"15px\\" + {...getOverrideProps(overrides, \\"RightAlignCTASubFlex\\")} + > + <Button + children=\\"Submit\\" + type=\\"submit\\" + variation=\\"primary\\" + isDisabled={Object.values(errors).some((e) => e?.hasError)} + {...getOverrideProps(overrides, \\"SubmitButton\\")} + ></Button> + </Flex> + </Flex> + </Grid> + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should render thrown error for required parent field 1:1 relationships - Create 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Dog, Owner } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listOwners } from \\"../graphql/queries\\"; +import { createDog, updateOwner } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction<T> = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise<ValidationResponse>; +export declare type CreateDogFormInputValues = { + name?: string; + Owner?: Owner; +}; +export declare type CreateDogFormValidationValues = { + name?: ValidationFunction<string>; + Owner?: ValidationFunction<Owner>; +}; +export declare type PrimitiveOverrideProps<T> = Partial<T> & + React.DOMAttributes<HTMLDivElement>; +export declare type CreateDogFormOverridesProps = { + CreateDogFormGrid?: PrimitiveOverrideProps<GridProps>; + name?: PrimitiveOverrideProps<TextFieldProps>; + Owner?: PrimitiveOverrideProps<AutocompleteProps>; +} & EscapeHatchProps; +export type CreateDogFormProps = React.PropsWithChildren< + { + overrides?: CreateDogFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: (fields: CreateDogFormInputValues) => CreateDogFormInputValues; + onSuccess?: (fields: CreateDogFormInputValues) => void; + onError?: (fields: CreateDogFormInputValues, errorMessage: string) => void; + onChange?: (fields: CreateDogFormInputValues) => CreateDogFormInputValues; + onValidate?: CreateDogFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = <Text>{label}</Text>; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 = ( + <React.Fragment> + {!!items?.length && ( + <ScrollView height=\\"inherit\\" width=\\"inherit\\" maxHeight={\\"7rem\\"}> + {items.map((value, index) => { + return ( + <Badge + key={index} + style={{ + cursor: \\"pointer\\", + alignItems: \\"center\\", + marginRight: 3, + marginTop: 3, + backgroundColor: + index === selectedBadgeIndex ? \\"#B8CEF9\\" : \\"\\", + }} + onClick={() => { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + <Icon + style={{ + cursor: \\"pointer\\", + paddingLeft: 3, + width: 20, + height: 20, + }} + viewBox={{ width: 20, height: 20 }} + paths={[ + { + d: \\"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\\", + stroke: \\"black\\", + }, + ]} + ariaLabel=\\"button\\" + onClick={(event) => { + event.stopPropagation(); + removeItem(index); + }} + /> + </Badge> + ); + })} + </ScrollView> + )} + <Divider orientation=\\"horizontal\\" marginTop={5} /> + </React.Fragment> + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + <React.Fragment> + {labelElement} + {arraySection} + </React.Fragment> + ); + } + return ( + <React.Fragment> + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + <Button + onClick={() => { + setIsEditing(true); + }} + > + Add item + </Button> + {errorMessage && hasError && ( + <Text color={errorStyles.color} fontSize={errorStyles.fontSize}> + {errorMessage} + </Text> + )} + </> + ) : ( + <Flex justifyContent=\\"flex-end\\"> + {(currentFieldValue || isEditing) && ( + <Button + children=\\"Cancel\\" + type=\\"button\\" + size=\\"small\\" + onClick={() => { + setFieldValue(defaultFieldValue); + setIsEditing(false); + setSelectedBadgeIndex(undefined); + }} + ></Button> + )} + <Button + size=\\"small\\" + variation=\\"link\\" + isDisabled={hasError} + onClick={addItem} + > + {selectedBadgeIndex !== undefined ? \\"Save\\" : \\"Add\\"} + </Button> + </Flex> + )} + {arraySection} + </React.Fragment> + ); +} +export default function CreateDogForm( + props: CreateDogFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + Owner: undefined, + }; + const [name, setName] = React.useState(initialValues.name); + const [Owner, setOwner] = React.useState(initialValues.Owner); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setOwner(initialValues.Owner); + setCurrentOwnerValue(undefined); + setCurrentOwnerDisplayValue(\\"\\"); + setErrors({}); + }; + const [currentOwnerDisplayValue, setCurrentOwnerDisplayValue] = + React.useState(\\"\\"); + const [currentOwnerValue, setCurrentOwnerValue] = React.useState(undefined); + const OwnerRef = React.createRef(); + const getIDValue = { + Owner: (r) => JSON.stringify({ id: r?.id }), + }; + const OwnerIdSet = new Set( + Array.isArray(Owner) + ? Owner.map((r) => getIDValue.Owner?.(r)) + : getIDValue.Owner?.(Owner) + ); + const ownerRecords = await API.graphql({ query: listOwners }).data.listOwners; + const getDisplayValue = { + Owner: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + }; + const validations = { + name: [], + Owner: [{ type: \\"Required\\", validationMessage: \\"Owner is required.\\" }], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + <Grid + as=\\"form\\" + rowGap=\\"15px\\" + columnGap=\\"15px\\" + padding=\\"20px\\" + onSubmit={async (event: SyntheticEvent) => { + event.preventDefault(); + let modelFields = { + name, + Owner, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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; + } + }); + const dog = await API.graphql({ + query: createDog, + variables: { input: modelFields }, + }); + const promises = []; + const ownerToLink = modelFields.Owner; + if (ownerToLink) { + promises.push( + API.graphql({ + query: updateOwner, + variables: { + input: { + ...Owner, + Dog: dog, + }, + }, + }) + ); + const dogToUnlink = await ownerToLink.Dog; + if (dogToUnlink) { + if (JSON.stringify(dogToUnlink) !== JSON.stringify(dog)) { + throw Error( + \`Owner \${ownerToLink.id} cannot be linked to Dog because it is already linked to another Dog.\` + ); + } + } + } + await Promise.all(promises); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"CreateDogForm\\")} + {...rest} + > + <TextField + label=\\"Name\\" + isRequired={false} + isReadOnly={false} + value={name} + onChange={(e) => { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + Owner, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + ></TextField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + Owner: value, + }; + const result = onChange(modelFields); + value = result?.Owner ?? value; + } + setOwner(value); + setCurrentOwnerValue(undefined); + setCurrentOwnerDisplayValue(\\"\\"); + }} + currentFieldValue={currentOwnerValue} + label={\\"Owner\\"} + items={Owner ? [Owner] : []} + hasError={errors?.Owner?.hasError} + errorMessage={errors?.Owner?.errorMessage} + getBadgeText={getDisplayValue.Owner} + setFieldValue={(model) => { + setCurrentOwnerDisplayValue( + model ? getDisplayValue.Owner(model) : \\"\\" + ); + setCurrentOwnerValue(model); + }} + inputFieldRef={OwnerRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Owner\\" + isRequired={true} + isReadOnly={false} + placeholder=\\"Search Owner\\" + value={currentOwnerDisplayValue} + options={ownerRecords + .filter((r) => !OwnerIdSet.has(getIDValue.Owner?.(r))) + .map((r) => ({ + id: getIDValue.Owner?.(r), + label: getDisplayValue.Owner?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentOwnerValue( + ownerRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentOwnerDisplayValue(label); + runValidationTasks(\\"Owner\\", label); + }} + onClear={() => { + setCurrentOwnerDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.Owner?.hasError) { + runValidationTasks(\\"Owner\\", value); + } + setCurrentOwnerDisplayValue(value); + setCurrentOwnerValue(undefined); + }} + onBlur={() => runValidationTasks(\\"Owner\\", currentOwnerDisplayValue)} + errorMessage={errors.Owner?.errorMessage} + hasError={errors.Owner?.hasError} + ref={OwnerRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"Owner\\")} + ></Autocomplete> + </ArrayField> + <Flex + justifyContent=\\"space-between\\" + {...getOverrideProps(overrides, \\"CTAFlex\\")} + > + <Button + children=\\"Clear\\" + type=\\"reset\\" + onClick={(event: SyntheticEvent) => { + event.preventDefault(); + resetStateValues(); + }} + {...getOverrideProps(overrides, \\"ClearButton\\")} + ></Button> + <Flex + gap=\\"15px\\" + {...getOverrideProps(overrides, \\"RightAlignCTASubFlex\\")} + > + <Button + children=\\"Submit\\" + type=\\"submit\\" + variation=\\"primary\\" + isDisabled={Object.values(errors).some((e) => e?.hasError)} + {...getOverrideProps(overrides, \\"SubmitButton\\")} + ></Button> + </Flex> + </Flex> + </Grid> + ); +} +" +`; + +exports[`amplify form renderer tests GraphQL form tests should render thrown error for required related field 1:1 relationships - Create 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + AutocompleteProps, + Badge, + Button, + Divider, + Flex, + Grid, + GridProps, + Icon, + ScrollView, + Text, + TextField, + TextFieldProps, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + EscapeHatchProps, + getOverrideProps, +} from \\"@aws-amplify/ui-react/internal\\"; +import { Dog, Owner } from \\"../API\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { listDogs } from \\"../graphql/queries\\"; +import { createOwner, updateDog, updateOwner } from \\"../graphql/mutations\\"; +import { API } from \\"@aws-amplify/api\\"; + +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction<T> = ( + value: T, + validationResponse: ValidationResponse +) => ValidationResponse | Promise<ValidationResponse>; +export declare type CreateOwnerFormInputValues = { + name?: string; + Dog?: Dog; +}; +export declare type CreateOwnerFormValidationValues = { + name?: ValidationFunction<string>; + Dog?: ValidationFunction<Dog>; +}; +export declare type PrimitiveOverrideProps<T> = Partial<T> & + React.DOMAttributes<HTMLDivElement>; +export declare type CreateOwnerFormOverridesProps = { + CreateOwnerFormGrid?: PrimitiveOverrideProps<GridProps>; + name?: PrimitiveOverrideProps<TextFieldProps>; + Dog?: PrimitiveOverrideProps<AutocompleteProps>; +} & EscapeHatchProps; +export type CreateOwnerFormProps = React.PropsWithChildren< + { + overrides?: CreateOwnerFormOverridesProps | undefined | null; + } & { + clearOnSuccess?: boolean; + onSubmit?: ( + fields: CreateOwnerFormInputValues + ) => CreateOwnerFormInputValues; + onSuccess?: (fields: CreateOwnerFormInputValues) => void; + onError?: ( + fields: CreateOwnerFormInputValues, + errorMessage: string + ) => void; + onChange?: ( + fields: CreateOwnerFormInputValues + ) => CreateOwnerFormInputValues; + onValidate?: CreateOwnerFormValidationValues; + } & React.CSSProperties +>; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + errorMessage, +}) { + const labelElement = <Text>{label}</Text>; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + 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 = ( + <React.Fragment> + {!!items?.length && ( + <ScrollView height=\\"inherit\\" width=\\"inherit\\" maxHeight={\\"7rem\\"}> + {items.map((value, index) => { + return ( + <Badge + key={index} + style={{ + cursor: \\"pointer\\", + alignItems: \\"center\\", + marginRight: 3, + marginTop: 3, + backgroundColor: + index === selectedBadgeIndex ? \\"#B8CEF9\\" : \\"\\", + }} + onClick={() => { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + <Icon + style={{ + cursor: \\"pointer\\", + paddingLeft: 3, + width: 20, + height: 20, + }} + viewBox={{ width: 20, height: 20 }} + paths={[ + { + d: \\"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\\", + stroke: \\"black\\", + }, + ]} + ariaLabel=\\"button\\" + onClick={(event) => { + event.stopPropagation(); + removeItem(index); + }} + /> + </Badge> + ); + })} + </ScrollView> + )} + <Divider orientation=\\"horizontal\\" marginTop={5} /> + </React.Fragment> + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + <React.Fragment> + {labelElement} + {arraySection} + </React.Fragment> + ); + } + return ( + <React.Fragment> + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + <Button + onClick={() => { + setIsEditing(true); + }} + > + Add item + </Button> + {errorMessage && hasError && ( + <Text color={errorStyles.color} fontSize={errorStyles.fontSize}> + {errorMessage} + </Text> + )} + </> + ) : ( + <Flex justifyContent=\\"flex-end\\"> + {(currentFieldValue || isEditing) && ( + <Button + children=\\"Cancel\\" + type=\\"button\\" + size=\\"small\\" + onClick={() => { + setFieldValue(defaultFieldValue); + setIsEditing(false); + setSelectedBadgeIndex(undefined); + }} + ></Button> + )} + <Button + size=\\"small\\" + variation=\\"link\\" + isDisabled={hasError} + onClick={addItem} + > + {selectedBadgeIndex !== undefined ? \\"Save\\" : \\"Add\\"} + </Button> + </Flex> + )} + {arraySection} + </React.Fragment> + ); +} +export default function CreateOwnerForm( + props: CreateOwnerFormProps +): React.ReactElement { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + Dog: undefined, + }; + const [name, setName] = React.useState(initialValues.name); + const [Dog, setDog] = React.useState(initialValues.Dog); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setDog(initialValues.Dog); + setCurrentDogValue(undefined); + setCurrentDogDisplayValue(\\"\\"); + setErrors({}); + }; + const [currentDogDisplayValue, setCurrentDogDisplayValue] = + React.useState(\\"\\"); + const [currentDogValue, setCurrentDogValue] = React.useState(undefined); + const DogRef = React.createRef(); + const getIDValue = { + Dog: (r) => JSON.stringify({ id: r?.id }), + }; + const DogIdSet = new Set( + Array.isArray(Dog) + ? Dog.map((r) => getIDValue.Dog?.(r)) + : getIDValue.Dog?.(Dog) + ); + const dogRecords = await API.graphql({ query: listDogs }).data.listDogs; + const getDisplayValue = { + Dog: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + }; + const validations = { + name: [{ type: \\"Required\\" }], + Dog: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && 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 ( + /* @ts-ignore: TS2322 */ + <Grid + as=\\"form\\" + rowGap=\\"15px\\" + columnGap=\\"15px\\" + padding=\\"20px\\" + onSubmit={async (event: SyntheticEvent) => { + event.preventDefault(); + let modelFields = { + name, + Dog, + }; + 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, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[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; + } + }); + const owner = await API.graphql({ + query: createOwner, + variables: { input: modelFields }, + }); + const promises = []; + const dogToLink = modelFields.Dog; + if (dogToLink) { + promises.push( + API.graphql({ + query: updateDog, + variables: { + input: { + ...Dog, + Owner: owner, + }, + }, + }) + ); + const ownerToUnlink = await dogToLink.Owner; + if (ownerToUnlink) { + promises.push( + API.graphql({ + query: updateOwner, + variables: { + input: { + ...ownerToUnlink, + Dog: undefined, + ownerDogId: undefined, + }, + }, + }) + ); + } + } + await Promise.all(promises); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"CreateOwnerForm\\")} + {...rest} + > + <TextField + label=\\"Name\\" + isRequired={true} + isReadOnly={false} + value={name} + onChange={(e) => { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + Dog, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + ></TextField> + <ArrayField + lengthLimit={1} + onChange={async (items) => { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + Dog: value, + }; + const result = onChange(modelFields); + value = result?.Dog ?? value; + } + setDog(value); + setCurrentDogValue(undefined); + setCurrentDogDisplayValue(\\"\\"); + }} + currentFieldValue={currentDogValue} + label={\\"Dog\\"} + items={Dog ? [Dog] : []} + hasError={errors?.Dog?.hasError} + errorMessage={errors?.Dog?.errorMessage} + getBadgeText={getDisplayValue.Dog} + setFieldValue={(model) => { + setCurrentDogDisplayValue(model ? getDisplayValue.Dog(model) : \\"\\"); + setCurrentDogValue(model); + }} + inputFieldRef={DogRef} + defaultFieldValue={\\"\\"} + > + <Autocomplete + label=\\"Dog\\" + isRequired={false} + isReadOnly={false} + placeholder=\\"Search Dog\\" + value={currentDogDisplayValue} + options={dogRecords + .filter((r) => !DogIdSet.has(getIDValue.Dog?.(r))) + .map((r) => ({ + id: getIDValue.Dog?.(r), + label: getDisplayValue.Dog?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentDogValue( + dogRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentDogDisplayValue(label); + runValidationTasks(\\"Dog\\", label); + }} + onClear={() => { + setCurrentDogDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.Dog?.hasError) { + runValidationTasks(\\"Dog\\", value); + } + setCurrentDogDisplayValue(value); + setCurrentDogValue(undefined); + }} + onBlur={() => runValidationTasks(\\"Dog\\", currentDogDisplayValue)} + errorMessage={errors.Dog?.errorMessage} + hasError={errors.Dog?.hasError} + ref={DogRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"Dog\\")} + ></Autocomplete> + </ArrayField> + <Flex + justifyContent=\\"space-between\\" + {...getOverrideProps(overrides, \\"CTAFlex\\")} + > + <Button + children=\\"Clear\\" + type=\\"reset\\" + onClick={(event: SyntheticEvent) => { + event.preventDefault(); + resetStateValues(); + }} + {...getOverrideProps(overrides, \\"ClearButton\\")} + ></Button> + <Flex + gap=\\"15px\\" + {...getOverrideProps(overrides, \\"RightAlignCTASubFlex\\")} + > + <Button + children=\\"Submit\\" + type=\\"submit\\" + variation=\\"primary\\" + isDisabled={Object.values(errors).some((e) => e?.hasError)} + {...getOverrideProps(overrides, \\"SubmitButton\\")} + ></Button> + </Flex> + </Flex> + </Grid> + ); +} +" +`; + 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\\"; 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 83cdcc65..4a0b74a2 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 @@ -687,6 +687,159 @@ describe('amplify form renderer tests', () => { expect(componentText).toMatchSnapshot(); }); + + it('should generate a create form with hasOne relationship', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/book-datastore-relationship', + 'datastore/relationship', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + // check nested model is imported + expect(componentText).toContain('import { Author, Book } from "../API";'); + + // check binding call is generated + expect(componentText).toContain('const authorRecords = await API.graphql({ query: listAuthors'); + + expect(componentText).toMatchSnapshot(); + }); + + it('should generate a create form with multiple hasOne relationships', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/book-datastore-relationship-multiple', + 'datastore/relationship-multiple', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + // check nested model is imported + expect(componentText).toContain('import { Author, Book, Title } from "../API";'); + + // check binding calls are generated + expect(componentText).toContain('const authorRecords = await API.graphql({ query: listAuthors'); + expect(componentText).toContain('const titleRecords = await API.graphql({ query: listTitles'); + + expect(componentText).toMatchSnapshot(); + }); + + it('should generate a create form with belongsTo relationship', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/member-datastore-create', + 'datastore/project-team-model', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + // check nested model is imported + expect(componentText).toContain('import { Member, Team } from "../API";'); + + // check binding call is generated + expect(componentText).toContain('const teamRecords = await API.graphql({ query: listTeams'); + + expect(componentText).toMatchSnapshot(); + }); + + it('should generate a create form with manyToMany relationship', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/tag-datastore-create', + 'datastore/tag-post', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + // check nested model is imported + expect(componentText).toContain('import { Post, Tag, TagPost } from "../API";'); + + // check binding call is generated + expect(componentText).toContain('const postRecords = await API.graphql({ query: listPosts'); + + // check custom display value is set + expect(componentText).toContain('Posts: (r) => r?.title'); + + expect(componentText).toMatchSnapshot(); + }); + + it('should generate a create form with hasMany relationship', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/school-datastore-create', + 'datastore/school-student', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + // check nested model is imported + expect(componentText).toContain('import { School, Student } from "../API";'); + + // check binding call is generated + expect(componentText).toContain('const studentRecords = await API.graphql({ query: listStudents'); + + // check custom display value is set + expect(componentText).toContain('Students: (r) => r?.name'); + + expect(componentText).toMatchSnapshot(); + }); + + it('should render a create form for model with composite keys', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/composite-dog-datastore-create', + 'datastore/composite-relationships', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).toMatchSnapshot(); + }); + + it('should render a create form for child of 1:m relationship', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/composite-toy-datastore-create', + 'datastore/composite-relationships', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).toMatchSnapshot(); + }); + + it('should render a create form for child of 1:m-belongsTo relationship', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/comment-datastore-create', + 'datastore/comment-hasMany-belongsTo-relationships', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).toContain('postCommentsId'); + expect(componentText).not.toContain('postID'); + expect(componentText).not.toContain('userCommentsId'); + expect(componentText).not.toContain('orgCommentsId'); + expect(componentText).toMatchSnapshot(); + }); + + it('should render thrown error for required parent field 1:1 relationships - Create', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/dog-owner-create', + 'datastore/dog-owner-required', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).toContain('if (JSON.stringify(dogToUnlink) !== JSON.stringify(dog)) {'); + expect(componentText).toContain('throw Error('); + expect(componentText).toContain( + 'Owner ${ownerToLink.id} cannot be linked to Dog because it is already linked to another Dog.', + ); + expect(componentText).toMatchSnapshot(); + }); + + it('should render thrown error for required related field 1:1 relationships - Create', () => { + const { componentText } = generateWithAmplifyFormRenderer( + 'forms/owner-dog-create', + 'datastore/dog-owner-required', + rendererConfigWithGraphQL, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).not.toContain('cannot be unlinked because'); + expect(componentText).not.toContain('cannot be linked to '); + expect(componentText).toMatchSnapshot(); + }); }); it('should render form for child of bidirectional 1:m when field defined on parent', () => { diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/bidirectional-relationship.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/bidirectional-relationship.ts index 0eb97b99..bb4bb41f 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/bidirectional-relationship.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/bidirectional-relationship.ts @@ -20,6 +20,7 @@ import { lowerCaseFirst } from '../../helpers'; import { ImportCollection } from '../../imports'; import { isModelDataType } from './render-checkers'; import { getRecordName } from './form-state'; +import { DataApiKind } from '../../react-render-config'; function getFieldBiDirectionalWith({ modelName, @@ -86,12 +87,74 @@ function unlinkModelRecordExpression({ recordNameToUnlink, fieldName, associatedFields, + importCollection, + dataApi, }: { modelName: string; recordNameToUnlink: string; fieldName: string; associatedFields: string[]; + importCollection: ImportCollection; + dataApi?: DataApiKind; }) { + if (dataApi === 'GraphQL') { + const updateMutation = `update${modelName}`; + + return factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('promises'), factory.createIdentifier('push')), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('API'), + factory.createIdentifier('graphql'), + ), + undefined, + [ + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('query'), + factory.createIdentifier(importCollection.addGraphqlMutationImport(updateMutation)), + ), + factory.createPropertyAssignment( + factory.createIdentifier('variables'), + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('input'), + factory.createObjectLiteralExpression( + [ + factory.createSpreadAssignment(factory.createIdentifier(recordNameToUnlink)), + factory.createPropertyAssignment( + factory.createIdentifier(fieldName), + factory.createIdentifier('undefined'), + ), + ...associatedFields.map((field) => + factory.createPropertyAssignment( + factory.createIdentifier(field), + factory.createIdentifier('undefined'), + ), + ), + ], + true, + ), + ), + ], + true, + ), + ), + ], + true, + ), + ], + ), + ], + ), + ); + } + return factory.createExpressionStatement( factory.createCallExpression( factory.createPropertyAccessExpression(factory.createIdentifier('promises'), factory.createIdentifier('push')), @@ -226,6 +289,134 @@ function wrapThrowInIfStatement({ ); } +function linkModelRecordExpression({ + importedRelatedModelName, + relatedRecordToLink, + fieldBiDirectionalWithName, + currentRecord, + importCollection, + dataApi, +}: { + importedRelatedModelName: string; + relatedRecordToLink: string; + fieldBiDirectionalWithName: string; + currentRecord: string; + importCollection: ImportCollection; + dataApi?: DataApiKind; +}) { + if (dataApi === 'GraphQL') { + const updateMutation = `update${importedRelatedModelName}`; + + return factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('promises'), factory.createIdentifier('push')), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('API'), + factory.createIdentifier('graphql'), + ), + undefined, + [ + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('query'), + factory.createIdentifier(importCollection.addGraphqlMutationImport(updateMutation)), + ), + factory.createPropertyAssignment( + factory.createIdentifier('variables'), + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('input'), + factory.createObjectLiteralExpression( + [ + factory.createSpreadAssignment(factory.createIdentifier(importedRelatedModelName)), + factory.createPropertyAssignment( + factory.createIdentifier(fieldBiDirectionalWithName), + factory.createIdentifier(currentRecord), + ), + ], + true, + ), + ), + ], + true, + ), + ), + ], + true, + ), + ], + ), + ], + ), + ); + } + + return 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.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(importedRelatedModelName), + factory.createIdentifier('copyOf'), + ), + undefined, + [ + factory.createIdentifier(relatedRecordToLink), + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('updated'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('updated'), + factory.createIdentifier(fieldBiDirectionalWithName), + ), + factory.createToken(SyntaxKind.EqualsToken), + factory.createIdentifier(currentRecord), + ), + ), + ], + true, + ), + ), + ], + ), + ], + ), + ], + ), + ); +} + export function getBiDirectionalRelationshipStatements({ formActionType, dataSchema, @@ -233,6 +424,7 @@ export function getBiDirectionalRelationshipStatements({ fieldConfig, modelName, savedRecordName, + dataApi, }: { formActionType: 'create' | 'update'; dataSchema: GenericDataSchema; @@ -240,6 +432,7 @@ export function getBiDirectionalRelationshipStatements({ fieldConfig: [string, FieldConfigMetadata]; modelName: string; savedRecordName: string; + dataApi?: DataApiKind; }) { const getFieldBiDirectionalWithReturnValue = getFieldBiDirectionalWith({ modelName, @@ -328,6 +521,8 @@ export function getBiDirectionalRelationshipStatements({ recordNameToUnlink: relatedRecordToUnlink, fieldName: fieldBiDirectionalWithName, associatedFields: associatedFieldsBiDirectionalWith, + importCollection, + dataApi, }), ], true, @@ -338,7 +533,30 @@ export function getBiDirectionalRelationshipStatements({ ); } - /** + /** GraphQL: + const compositeOwnerToLink = modelFields.CompositeOwner; + if (compositeOwnerToLink) { + promises.push(API.graphql({ + query: updateCompositeOwner0, + variables: { input: { ...ownerToLink, Dog: dog }}, + })) + + const compositeDogToUnlink = await compositeOwnerToLink.CompositeDog; + if (compositeDogToUnlink) { + promises.push(API.graphql({ + query: updateCompositeDog, + variables: { input: { + ...compositeDogToUnlink, + compositeDogCompositeOwnerLastName: undefined, + compositeDogCompositeOwnerFirstName: undefined, + CompositeOwner: undefined, + }} + })) + } + } + */ + + /** Datatstore: const compositeOwnerToLink = modelFields.CompositeOwner; if (compositeOwnerToLink) { promises.push(DataStore.save(CompositeOwner0.copyOf(compositeOwnerToLink, (updated) => { @@ -381,68 +599,14 @@ export function getBiDirectionalRelationshipStatements({ factory.createIdentifier(relatedRecordToLink), 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.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(importedRelatedModelName), - factory.createIdentifier('copyOf'), - ), - undefined, - [ - factory.createIdentifier(relatedRecordToLink), - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier('updated'), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - factory.createBlock( - [ - factory.createExpressionStatement( - factory.createBinaryExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('updated'), - factory.createIdentifier(fieldBiDirectionalWithName), - ), - factory.createToken(SyntaxKind.EqualsToken), - factory.createIdentifier(currentRecord), - ), - ), - ], - true, - ), - ), - ], - ), - ], - ), - ], - ), - ), + linkModelRecordExpression({ + importedRelatedModelName, + relatedRecordToLink, + fieldBiDirectionalWithName, + currentRecord, + importCollection, + dataApi, + }), factory.createVariableStatement( undefined, factory.createVariableDeclarationList( @@ -483,6 +647,8 @@ export function getBiDirectionalRelationshipStatements({ recordNameToUnlink: thisModelRecordToUnlink, fieldName, associatedFields, + importCollection, + dataApi, }), ], true, diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts index 160a861b..0d8708f2 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/cta-props.ts @@ -27,8 +27,8 @@ import { import { getModelNameProp, getSetNameIdentifier, lowerCaseFirst } from '../../helpers'; import { getDisplayValueObjectName } from './model-values'; import { - buildHasManyRelationshipDataStoreStatements, - buildManyToManyRelationshipDataStoreStatements, + buildHasManyRelationshipStatements, + buildManyToManyRelationshipStatements, getRelationshipBasedRecordUpdateStatements, } from './relationship'; import { isManyToManyRelationship } from './map-from-fieldConfigs'; @@ -46,7 +46,7 @@ const getRecordCreateCallExpression = ({ savedObjectName: string; importedModelName: string; importCollection: ImportCollection; - dataApi: DataApiKind; + dataApi?: DataApiKind; }) => { if (dataApi === 'GraphQL') { const createMutation = `create${importedModelName}`; @@ -255,7 +255,7 @@ export const buildExpression = ( fieldConfigs: Record<string, FieldConfigMetadata>, dataSchema: GenericDataSchema, importCollection: ImportCollection, - dataApi: DataApiKind = 'DataStore', + dataApi?: DataApiKind, ): Statement[] => { const modelFieldsObjectName = 'modelFields'; const modelFieldsObjectToSaveName = 'modelFieldsToSave'; @@ -276,29 +276,34 @@ export const buildExpression = ( fieldConfig, modelName, savedRecordName, + dataApi, }), ); if (fieldConfigMetaData.relationship?.type === 'HAS_MANY') { if (isManyToManyRelationship(fieldConfigMetaData)) { const joinTable = dataSchema.models[fieldConfigMetaData.relationship.relatedJoinTableName]; relationshipsPromisesAccessStatements.push( - ...buildManyToManyRelationshipDataStoreStatements( + ...buildManyToManyRelationshipStatements( dataStoreActionType, importedModelName, fieldConfig, thisModelPrimaryKeys, joinTable, savedRecordName, + importCollection, + dataApi, ), ); } else { relationshipsPromisesAccessStatements.push( - ...buildHasManyRelationshipDataStoreStatements( + ...buildHasManyRelationshipStatements( dataStoreActionType, importedModelName, fieldConfig, thisModelPrimaryKeys, savedRecordName, + importCollection, + dataApi, ), ); } 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 411afd5e..dc319fec 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 @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { CallExpression, factory, IfStatement, NodeFlags, Statement, SyntaxKind } from 'typescript'; +import { CallExpression, factory, IfStatement, NodeFlags, PropertyAssignment, Statement, SyntaxKind } from 'typescript'; import { FieldConfigMetadata, HasManyRelationshipType, @@ -24,13 +24,62 @@ import { import { getRecordsName, getLinkedDataName, buildAccessChain, getCanUnlinkModelName } from './form-state'; import { buildBaseCollectionVariableStatement } from '../../react-studio-template-renderer-helper'; import { ImportCollection } from '../../imports'; -import { lowerCaseFirst, getSetNameIdentifier } from '../../helpers'; +import { lowerCaseFirst, getSetNameIdentifier, capitalizeFirstLetter } from '../../helpers'; import { isManyToManyRelationship } from './map-from-fieldConfigs'; import { extractModelAndKeys, getIDValueCallChain, getMatchEveryModelFieldCallExpression } from './model-values'; import { isModelDataType } from './render-checkers'; +import { DataApiKind } from '../../react-render-config'; -export const buildRelationshipQuery = (relatedModelName: string, importCollection: ImportCollection) => { +export const buildRelationshipQuery = ( + relatedModelName: string, + importCollection: ImportCollection, + dataApi?: DataApiKind, +) => { const itemsName = getRecordsName(relatedModelName); + + if (dataApi === 'GraphQL') { + const query = `list${importCollection.addModelImport(relatedModelName)}s`; + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(itemsName), + undefined, + undefined, + factory.createAwaitExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('API'), + factory.createIdentifier('graphql'), + ), + undefined, + [ + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('query'), + factory.createIdentifier(importCollection.addGraphqlQueryImport(query)), + ), + ], + false, + ), + ], + ), + factory.createIdentifier('data'), + ), + factory.createIdentifier(query), + ), + ), + ), + ], + NodeFlags.Const, + ), + ); + } + const objectProperties = [ factory.createPropertyAssignment(factory.createIdentifier('type'), factory.createStringLiteral('collection')), factory.createPropertyAssignment( @@ -203,13 +252,15 @@ function createHasManyUpdateRelatedModelBlock({ return factory.createBlock(statements, true); } -export const buildManyToManyRelationshipDataStoreStatements = ( +export const buildManyToManyRelationshipStatements = ( dataStoreActionType: 'update' | 'create', modelName: string, hasManyFieldConfig: [string, FieldConfigMetadata], thisModelPrimaryKeys: string[], joinTable: GenericDataModel, savedModelName: string, + importCollection: ImportCollection, + dataApi?: DataApiKind, ) => { let [fieldName] = hasManyFieldConfig; const [, fieldConfigMetaData] = hasManyFieldConfig; @@ -1049,42 +1100,13 @@ export const buildManyToManyRelationshipDataStoreStatements = ( ), undefined, [ - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('DataStore'), - factory.createIdentifier('save'), - ), - undefined, - [ - factory.createNewExpression( - factory.createIdentifier(relatedJoinTableName), - undefined, - [ - // { - // cpkTeacher: cPKTeacher, - // cpkClass, - // } - factory.createObjectLiteralExpression( - [ - savedModelName === joinTableThisModelName - ? factory.createShorthandPropertyAssignment( - factory.createIdentifier(joinTableThisModelName), - undefined, - ) - : factory.createPropertyAssignment( - factory.createIdentifier(joinTableThisModelName), - factory.createIdentifier(savedModelName), - ), - factory.createShorthandPropertyAssignment( - factory.createIdentifier(joinTableRelatedModelName), - undefined, - ), - ], - true, - ), - ], - ), - ], + getCreateJoinTableExpression( + relatedJoinTableName, + savedModelName, + joinTableThisModelName, + joinTableRelatedModelName, + importCollection, + dataApi, ), ], ), @@ -1269,12 +1291,14 @@ export const buildGetRelationshipModels = (fieldName: string, fieldConfigMetaDat ]; }; -export const buildHasManyRelationshipDataStoreStatements = ( +export const buildHasManyRelationshipStatements = ( dataStoreActionType: 'update' | 'create', modelName: string, hasManyFieldConfig: [string, FieldConfigMetadata], thisModelPrimaryKeys: string[], savedModelName: string, + importCollection: ImportCollection, + dataApi?: DataApiKind, ) => { let [fieldName] = hasManyFieldConfig; const [, fieldConfigMetaData] = hasManyFieldConfig; @@ -1751,6 +1775,16 @@ export const buildHasManyRelationshipDataStoreStatements = ( ), ]; } + + const updateRelatedModelExpression = getUpdateRelatedModelExpression( + savedModelName, + relatedModelName, + relatedModelFields, + thisModelPrimaryKeys, + importCollection, + dataApi, + belongsToFieldOnRelatedModel, + ); return [ factory.createExpressionStatement( factory.createCallExpression( @@ -1800,50 +1834,7 @@ export const buildHasManyRelationshipDataStoreStatements = ( factory.createIdentifier('push'), ), undefined, - [ - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('DataStore'), - factory.createIdentifier('save'), - ), - undefined, - [ - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(relatedModelName), - factory.createIdentifier('copyOf'), - ), - undefined, - [ - factory.createIdentifier('original'), - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier('updated'), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - createHasManyUpdateRelatedModelBlock({ - relatedModelFields, - thisModelPrimaryKeys, - thisModelRecord: savedModelName, - belongsToFieldOnRelatedModel, - }), - ), - ], - ), - ], - ), - ], + [updateRelatedModelExpression], ), ), factory.createReturnStatement(factory.createIdentifier('promises')), @@ -1913,3 +1904,212 @@ export const getRelationshipBasedRecordUpdateStatements = ({ }); return statements; }; + +const getUpdateRelatedModelExpression = ( + savedModelName: string, + relatedModelName: string, + relatedModelFields: string[], + thisModelPrimaryKeys: string[], + importCollection: ImportCollection, + dataApi?: DataApiKind, + belongsToFieldOnRelatedModel?: string, + setToNull?: boolean, +) => { + if (dataApi === 'GraphQL') { + const updateMutation = `update${capitalizeFirstLetter(savedModelName)}`; + const statements: PropertyAssignment[] = relatedModelFields.map((relatedModelField, index) => { + const correspondingPrimaryKey = thisModelPrimaryKeys[index]; + + if (!correspondingPrimaryKey) { + throw new InternalError(`Corresponding primary key not found for ${relatedModelField}`); + } + + return factory.createPropertyAssignment( + factory.createIdentifier(relatedModelField), + setToNull + ? factory.createNull() + : factory.createPropertyAccessExpression( + factory.createIdentifier(savedModelName), + factory.createIdentifier(correspondingPrimaryKey), + ), + ); + }); + + if (belongsToFieldOnRelatedModel) { + statements.push( + factory.createPropertyAssignment( + factory.createIdentifier(belongsToFieldOnRelatedModel), + setToNull ? factory.createNull() : factory.createIdentifier(savedModelName), + ), + ); + } + + /** + * API.graphql({ + * query: updateStudent, + * variables: { input: { ...original, schoolID: school.id }} + * }) + */ + return factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('API'), factory.createIdentifier('graphql')), + undefined, + [ + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('query'), + factory.createIdentifier(importCollection.addGraphqlMutationImport(updateMutation)), + ), + factory.createPropertyAssignment( + factory.createIdentifier('variables'), + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('input'), + factory.createObjectLiteralExpression( + [factory.createSpreadAssignment(factory.createIdentifier('original')), ...statements], + false, + ), + ), + ], + false, + ), + ), + ], + true, + ), + ], + ); + } + + /** + * Datastore.save( + * Student.copyOf(original, (updated) => { + * updated.schoolID = school.id + * }) + * ) + */ + return factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('DataStore'), factory.createIdentifier('save')), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(relatedModelName), + factory.createIdentifier('copyOf'), + ), + undefined, + [ + factory.createIdentifier('original'), + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('updated'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + createHasManyUpdateRelatedModelBlock({ + relatedModelFields, + thisModelPrimaryKeys, + thisModelRecord: savedModelName, + belongsToFieldOnRelatedModel, + setToNull, + }), + ), + ], + ), + ], + ); +}; + +const getCreateJoinTableExpression = ( + relatedJoinTableName: string, + savedModelName: string, + joinTableThisModelName: string, + joinTableRelatedModelName: string, + importCollection: ImportCollection, + dataApi?: DataApiKind, +): CallExpression => { + if (dataApi === 'GraphQL') { + const createMutation = `create${relatedJoinTableName}`; + + return factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('API'), factory.createIdentifier('graphql')), + undefined, + [ + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('query'), + factory.createIdentifier(importCollection.addGraphqlMutationImport(createMutation)), + ), + factory.createPropertyAssignment( + factory.createIdentifier('variables'), + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('input'), + factory.createObjectLiteralExpression( + [ + savedModelName === joinTableThisModelName + ? factory.createShorthandPropertyAssignment( + factory.createIdentifier(joinTableThisModelName), + undefined, + ) + : factory.createPropertyAssignment( + factory.createIdentifier(joinTableThisModelName), + factory.createIdentifier(savedModelName), + ), + factory.createShorthandPropertyAssignment( + factory.createIdentifier(joinTableRelatedModelName), + undefined, + ), + ], + true, + ), + ), + ], + true, + ), + ), + ], + true, + ), + ], + ); + } + + return factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('DataStore'), factory.createIdentifier('save')), + undefined, + [ + factory.createNewExpression(factory.createIdentifier(relatedJoinTableName), undefined, [ + // { + // cpkTeacher: cPKTeacher, + // cpkClass, + // } + factory.createObjectLiteralExpression( + [ + savedModelName === joinTableThisModelName + ? factory.createShorthandPropertyAssignment(factory.createIdentifier(joinTableThisModelName), undefined) + : factory.createPropertyAssignment( + factory.createIdentifier(joinTableThisModelName), + factory.createIdentifier(savedModelName), + ), + factory.createShorthandPropertyAssignment(factory.createIdentifier(joinTableRelatedModelName), undefined), + ], + true, + ), + ]), + ], + ); +}; 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 e5345302..de271c60 100644 --- a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -600,18 +600,28 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< this.importCollection.addModelImport(model); }); - // datastore relationship query - /** + // relationship query + /** GraphQL: + * const authorRecords = await API.graphql( + * { query: listAuthors } + * ).data.listAuthors.items; + */ + /** Datastore: const authorRecords = useDataStoreBinding({ type: 'collection', model: Author, }).items; - */ + */ if (relatedModelNames.size) { - this.importCollection.addMappedImport(ImportValue.USE_DATA_STORE_BINDING); + if (!(this.renderConfig.apiConfiguration?.dataApi === 'GraphQL')) { + this.importCollection.addMappedImport(ImportValue.USE_DATA_STORE_BINDING); + } + + const dataApi = 'apiConfiguration' in this.renderConfig ? this.renderConfig.apiConfiguration?.dataApi : undefined; + statements.push( ...[...relatedModelNames].map((relatedModelName) => - buildRelationshipQuery(relatedModelName, this.importCollection), + buildRelationshipQuery(relatedModelName, this.importCollection, dataApi), ), ); }