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 4ddd85ff7..29cba4864 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 @@ -2262,6 +2262,1115 @@ export default function MyPostForm(props: MyPostFormProps): React.ReactElement; " `; +exports[`amplify form renderer tests datastore form tests custom form tests should render an update form for model with composite keys 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Autocomplete, + Badge, + Button, + Divider, + Flex, + Grid, + Icon, + ScrollView, + Text, + TextField, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +import { + getOverrideProps, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; +import { + CompositeDog, + CompositeBowl as CompositeBowl0, + CompositeOwner as CompositeOwner0, + CompositeToy, + CompositeVet, + CompositeDogCompositeVet, +} from \\"../models\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { DataStore } from \\"aws-amplify\\"; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, +}) { + const labelElement = {label}; + const { tokens } = 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 ? ( + <> + + + ) : ( + + {(currentFieldValue || isEditing) && ( + + )} + + + )} + {arraySection} + + ); +} +export default function UpdateCompositeDogForm(props) { + const { + name: nameProp, + compositeDog, + onSuccess, + onError, + onSubmit, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: undefined, + description: undefined, + CompositeBowl: undefined, + CompositeOwner: undefined, + CompositeToys: [], + CompositeVets: [], + compositeDogCompositeBowlSize: undefined, + compositeDogCompositeOwnerFirstName: undefined, + }; + 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 [compositeDogCompositeBowlSize, setCompositeDogCompositeBowlSize] = + React.useState(initialValues.compositeDogCompositeBowlSize); + const [ + compositeDogCompositeOwnerFirstName, + setCompositeDogCompositeOwnerFirstName, + ] = React.useState(initialValues.compositeDogCompositeOwnerFirstName); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + const cleanValues = compositeDogRecord + ? { + ...initialValues, + ...compositeDogRecord, + CompositeBowl, + CompositeOwner, + CompositeToys: linkedCompositeToys, + CompositeVets: linkedCompositeVets, + } + : initialValues; + setName(cleanValues.name); + setDescription(cleanValues.description); + setCompositeBowl(cleanValues.CompositeBowl); + setCurrentCompositeBowlValue(undefined); + setCurrentCompositeBowlDisplayValue(\\"\\"); + setCompositeOwner(cleanValues.CompositeOwner); + setCurrentCompositeOwnerValue(undefined); + setCurrentCompositeOwnerDisplayValue(\\"\\"); + setCompositeToys(cleanValues.CompositeToys ?? []); + setCurrentCompositeToysValue(undefined); + setCurrentCompositeToysDisplayValue(\\"\\"); + setCompositeVets(cleanValues.CompositeVets ?? []); + setCurrentCompositeVetsValue(undefined); + setCurrentCompositeVetsDisplayValue(\\"\\"); + setCompositeDogCompositeBowlSize(cleanValues.compositeDogCompositeBowlSize); + setCompositeDogCompositeOwnerFirstName( + cleanValues.compositeDogCompositeOwnerFirstName + ); + setErrors({}); + }; + const [compositeDogRecord, setCompositeDogRecord] = + React.useState(compositeDog); + const [linkedCompositeToys, setLinkedCompositeToys] = React.useState([]); + const [linkedCompositeVets, setLinkedCompositeVets] = React.useState([]); + React.useEffect(() => { + const queryData = async () => { + const record = nameProp + ? await DataStore.query(CompositeDog, nameProp) + : compositeDog; + setCompositeDogRecord(record); + const CompositeBowlRecord = record + ? await record.CompositeBowl + : undefined; + setCompositeBowl(CompositeBowlRecord); + const CompositeOwnerRecord = record + ? await record.CompositeOwner + : undefined; + setCompositeOwner(CompositeOwnerRecord); + const linkedCompositeToys = record + ? await record.CompositeToys.toArray() + : []; + setLinkedCompositeToys(linkedCompositeToys); + const linkedCompositeVets = record + ? await Promise.all( + ( + await record.CompositeVets.toArray() + ).map((r) => { + return r.compositeVet; + }) + ) + : []; + setLinkedCompositeVets(linkedCompositeVets); + }; + queryData(); + }, [nameProp, compositeDog]); + React.useEffect(resetStateValues, [ + compositeDogRecord, + CompositeBowl, + CompositeOwner, + linkedCompositeToys, + linkedCompositeVets, + ]); + 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 = useDataStoreBinding({ + type: \\"collection\\", + model: CompositeBowl0, + }).items; + const compositeOwnerRecords = useDataStoreBinding({ + type: \\"collection\\", + model: CompositeOwner0, + }).items; + const compositeToyRecords = useDataStoreBinding({ + type: \\"collection\\", + model: CompositeToy, + }).items; + const compositeVetRecords = useDataStoreBinding({ + type: \\"collection\\", + model: CompositeVet, + }).items; + 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: [], + compositeDogCompositeBowlSize: [], + compositeDogCompositeOwnerFirstName: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = getDisplayValue + ? getDisplayValue(currentValue) + : currentValue; + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName, + }; + 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 { + const promises = []; + const compositeToysToLink = []; + const compositeToysToUnLink = []; + const compositeToysSet = new Set(); + const linkedCompositeToysSet = new Set(); + CompositeToys.forEach((r) => compositeToysSet.add(r.kind)); + linkedCompositeToys.forEach((r) => + linkedCompositeToysSet.add(r.kind) + ); + linkedCompositeToys.forEach((r) => { + if (!compositeToysSet.has(r.id)) { + compositeToysToUnLink.push(r); + } + }); + CompositeToys.forEach((r) => { + if (!linkedCompositeToysSet.has(r.id)) { + compositeToysToLink.push(r); + } + }); + compositeToysToUnLink.forEach((original) => { + promises.push( + DataStore.save( + CompositeToy.copyOf(original, (updated) => { + updated.compositeDogCompositeToysName = null; + }) + ) + ); + }); + compositeToysToLink.forEach((original) => { + promises.push( + DataStore.save( + CompositeToy.copyOf(original, (updated) => { + updated.compositeDogCompositeToysName = + compositeDogRecord.name; + }) + ) + ); + }); + const compositeVetsToLinkMap = new Map(); + const compositeVetsToUnLinkMap = new Map(); + const compositeVetsMap = new Map(); + const linkedCompositeVetsMap = new Map(); + CompositeVets.forEach((r) => { + const count = compositeVetsMap.get(r.specialty); + const newCount = count ? count + 1 : 1; + compositeVetsMap.set(r.specialty, newCount); + }); + linkedCompositeVets.forEach((r) => { + const count = linkedCompositeVetsMap.get(r.specialty); + const newCount = count ? count + 1 : 1; + linkedCompositeVetsMap.set(r.specialty, newCount); + }); + linkedCompositeVetsMap.forEach((count, id) => { + const newCount = compositeVetsMap.get(id); + if (newCount) { + const diffCount = count - newCount; + if (diffCount > 0) { + compositeVetsToUnLinkMap.set(id, diffCount); + } + } else { + compositeVetsToUnLinkMap.set(id, count); + } + }); + compositeVetsMap.forEach((count, id) => { + const originalCount = linkedCompositeVetsMap.get(id); + if (originalCount) { + const diffCount = count - originalCount; + if (diffCount > 0) { + compositeVetsToLinkMap.set(id, diffCount); + } + } else { + compositeVetsToLinkMap.set(id, count); + } + }); + compositeVetsToUnLinkMap.forEach(async (count, id) => { + const compositeDogCompositeVetRecords = await DataStore.query( + CompositeDogCompositeVet, + (r) => + r.and((r) => [ + r.compositeVetID.eq(id), + r.compositeDogID.eq(compositeDogRecord.name), + ]) + ); + for (let i = 0; i < count; i++) { + promises.push( + DataStore.delete(compositeDogCompositeVetRecords[i]) + ); + } + }); + compositeVetsToLinkMap.forEach((count, id) => { + for (let i = count; i > 0; i--) { + promises.push( + DataStore.save( + new CompositeDogCompositeVet({ + compositeDogID: compositeDogRecord.name, + compositeVetID: id, + }) + ) + ); + } + }); + promises.push( + DataStore.save( + CompositeDog.copyOf(compositeDogRecord, (updated) => { + Object.assign(updated, modelFields); + if (!modelFields.CompositeBowl) + updated.compositeDogCompositeBowlShape = undefined; + if (!modelFields.CompositeOwner) + updated.compositeDogCompositeOwnerLastName = undefined; + }) + ) + ); + await Promise.all(promises); + if (onSuccess) { + onSuccess(modelFields); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...rest} + {...getOverrideProps(overrides, \\"UpdateCompositeDogForm\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName, + }; + 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 } = e.target; + if (onChange) { + const modelFields = { + name, + description: value, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName, + }; + 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\\")} + > + { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl: value, + CompositeOwner, + CompositeToys, + CompositeVets, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName, + }; + 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} + getBadgeText={getDisplayValue.CompositeBowl} + setFieldValue={(model) => { + setCurrentCompositeBowlDisplayValue( + getDisplayValue.CompositeBowl(model) + ); + setCurrentCompositeBowlValue(model); + }} + inputFieldRef={CompositeBowlRef} + defaultFieldValue={\\"\\"} + > + !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); + }} + onClear={() => { + setCurrentCompositeBowlDisplayValue(\\"\\"); + }} + defaultValue={CompositeBowl} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeBowl?.hasError) { + runValidationTasks(\\"CompositeBowl\\", value); + } + setCurrentCompositeBowlDisplayValue(value); + setCurrentCompositeBowlValue(undefined); + }} + onBlur={() => runValidationTasks(\\"CompositeBowl\\", CompositeBowl)} + errorMessage={errors.CompositeBowl?.errorMessage} + hasError={errors.CompositeBowl?.hasError} + ref={CompositeBowlRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeBowl\\")} + > + + { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner: value, + CompositeToys, + CompositeVets, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName, + }; + 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} + getBadgeText={getDisplayValue.CompositeOwner} + setFieldValue={(model) => { + setCurrentCompositeOwnerDisplayValue( + getDisplayValue.CompositeOwner(model) + ); + setCurrentCompositeOwnerValue(model); + }} + inputFieldRef={CompositeOwnerRef} + defaultFieldValue={\\"\\"} + > + !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); + }} + onClear={() => { + setCurrentCompositeOwnerDisplayValue(\\"\\"); + }} + defaultValue={CompositeOwner} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeOwner?.hasError) { + runValidationTasks(\\"CompositeOwner\\", value); + } + setCurrentCompositeOwnerDisplayValue(value); + setCurrentCompositeOwnerValue(undefined); + }} + onBlur={() => runValidationTasks(\\"CompositeOwner\\", CompositeOwner)} + errorMessage={errors.CompositeOwner?.errorMessage} + hasError={errors.CompositeOwner?.hasError} + ref={CompositeOwnerRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeOwner\\")} + > + + { + let values = items; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys: values, + CompositeVets, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName, + }; + const result = onChange(modelFields); + values = result?.CompositeToys ?? values; + } + setCompositeToys(values); + setCurrentCompositeToysValue(undefined); + setCurrentCompositeToysDisplayValue(\\"\\"); + }} + currentFieldValue={currentCompositeToysValue} + label={\\"Composite toys\\"} + items={CompositeToys} + hasError={errors.CompositeToys?.hasError} + getBadgeText={getDisplayValue.CompositeToys} + setFieldValue={(model) => { + setCurrentCompositeToysDisplayValue( + getDisplayValue.CompositeToys(model) + ); + setCurrentCompositeToysValue(model); + }} + inputFieldRef={CompositeToysRef} + defaultFieldValue={\\"\\"} + > + !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); + }} + onClear={() => { + setCurrentCompositeToysDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeToys?.hasError) { + runValidationTasks(\\"CompositeToys\\", value); + } + setCurrentCompositeToysDisplayValue(value); + setCurrentCompositeToysValue(undefined); + }} + onBlur={() => + runValidationTasks(\\"CompositeToys\\", currentCompositeToysValue) + } + errorMessage={errors.CompositeToys?.errorMessage} + hasError={errors.CompositeToys?.hasError} + ref={CompositeToysRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeToys\\")} + > + + { + let values = items; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets: values, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName, + }; + const result = onChange(modelFields); + values = result?.CompositeVets ?? values; + } + setCompositeVets(values); + setCurrentCompositeVetsValue(undefined); + setCurrentCompositeVetsDisplayValue(\\"\\"); + }} + currentFieldValue={currentCompositeVetsValue} + label={\\"Composite vets\\"} + items={CompositeVets} + hasError={errors.CompositeVets?.hasError} + getBadgeText={getDisplayValue.CompositeVets} + setFieldValue={(model) => { + setCurrentCompositeVetsDisplayValue( + getDisplayValue.CompositeVets(model) + ); + setCurrentCompositeVetsValue(model); + }} + inputFieldRef={CompositeVetsRef} + defaultFieldValue={\\"\\"} + > + !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); + }} + onClear={() => { + setCurrentCompositeVetsDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.CompositeVets?.hasError) { + runValidationTasks(\\"CompositeVets\\", value); + } + setCurrentCompositeVetsDisplayValue(value); + setCurrentCompositeVetsValue(undefined); + }} + onBlur={() => + runValidationTasks(\\"CompositeVets\\", currentCompositeVetsValue) + } + errorMessage={errors.CompositeVets?.errorMessage} + hasError={errors.CompositeVets?.hasError} + ref={CompositeVetsRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"CompositeVets\\")} + > + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + compositeDogCompositeBowlSize: value, + compositeDogCompositeOwnerFirstName, + }; + const result = onChange(modelFields); + value = result?.compositeDogCompositeBowlSize ?? value; + } + if (errors.compositeDogCompositeBowlSize?.hasError) { + runValidationTasks(\\"compositeDogCompositeBowlSize\\", value); + } + setCompositeDogCompositeBowlSize(value); + }} + onBlur={() => + runValidationTasks( + \\"compositeDogCompositeBowlSize\\", + compositeDogCompositeBowlSize + ) + } + errorMessage={errors.compositeDogCompositeBowlSize?.errorMessage} + hasError={errors.compositeDogCompositeBowlSize?.hasError} + {...getOverrideProps(overrides, \\"compositeDogCompositeBowlSize\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + description, + CompositeBowl, + CompositeOwner, + CompositeToys, + CompositeVets, + compositeDogCompositeBowlSize, + compositeDogCompositeOwnerFirstName: value, + }; + const result = onChange(modelFields); + value = result?.compositeDogCompositeOwnerFirstName ?? value; + } + if (errors.compositeDogCompositeOwnerFirstName?.hasError) { + runValidationTasks(\\"compositeDogCompositeOwnerFirstName\\", value); + } + setCompositeDogCompositeOwnerFirstName(value); + }} + onBlur={() => + runValidationTasks( + \\"compositeDogCompositeOwnerFirstName\\", + compositeDogCompositeOwnerFirstName + ) + } + errorMessage={errors.compositeDogCompositeOwnerFirstName?.errorMessage} + hasError={errors.compositeDogCompositeOwnerFirstName?.hasError} + {...getOverrideProps(overrides, \\"compositeDogCompositeOwnerFirstName\\")} + > + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests custom form tests should render an update form for model with composite keys 2`] = ` +"import * as React from \\"react\\"; +import { AutocompleteProps, GridProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { CompositeDog, CompositeBowl as CompositeBowl0, CompositeOwner as CompositeOwner0, CompositeToy, CompositeVet } from \\"../models\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UpdateCompositeDogFormInputValues = { + name?: string; + description?: string; + CompositeBowl?: CompositeBowl0; + CompositeOwner?: CompositeOwner0; + CompositeToys?: CompositeToy[]; + CompositeVets?: CompositeVet[]; + compositeDogCompositeBowlSize?: string; + compositeDogCompositeOwnerFirstName?: string; +}; +export declare type UpdateCompositeDogFormValidationValues = { + name?: ValidationFunction; + description?: ValidationFunction; + CompositeBowl?: ValidationFunction; + CompositeOwner?: ValidationFunction; + CompositeToys?: ValidationFunction; + CompositeVets?: ValidationFunction; + compositeDogCompositeBowlSize?: ValidationFunction; + compositeDogCompositeOwnerFirstName?: ValidationFunction; +}; +export declare type PrimitiveOverrideProps = Partial & React.DOMAttributes; +export declare type UpdateCompositeDogFormOverridesProps = { + UpdateCompositeDogFormGrid?: PrimitiveOverrideProps; + name?: PrimitiveOverrideProps; + description?: PrimitiveOverrideProps; + CompositeBowl?: PrimitiveOverrideProps; + CompositeOwner?: PrimitiveOverrideProps; + CompositeToys?: PrimitiveOverrideProps; + CompositeVets?: PrimitiveOverrideProps; + compositeDogCompositeBowlSize?: PrimitiveOverrideProps; + compositeDogCompositeOwnerFirstName?: PrimitiveOverrideProps; +} & EscapeHatchProps; +export declare type UpdateCompositeDogFormProps = React.PropsWithChildren<{ + overrides?: UpdateCompositeDogFormOverridesProps | undefined | null; +} & { + name?: string; + compositeDog?: CompositeDog; + onSubmit?: (fields: UpdateCompositeDogFormInputValues) => UpdateCompositeDogFormInputValues; + onSuccess?: (fields: UpdateCompositeDogFormInputValues) => void; + onError?: (fields: UpdateCompositeDogFormInputValues, errorMessage: string) => void; + onChange?: (fields: UpdateCompositeDogFormInputValues) => UpdateCompositeDogFormInputValues; + onValidate?: UpdateCompositeDogFormValidationValues; +} & React.CSSProperties>; +export default function UpdateCompositeDogForm(props: UpdateCompositeDogFormProps): React.ReactElement; +" +`; + exports[`amplify form renderer tests datastore form tests custom form tests should render an update form for model with cpk 1`] = ` "/* eslint-disable */ import * as React from \\"react\\"; @@ -2464,21 +3573,6 @@ export default function UpdateCPKTeacherForm(props) { const [CPKProjects, setCPKProjects] = React.useState( initialValues.CPKProjects ); - const CPKStudentIdSet = new Set( - Array.isArray(CPKStudent) - ? CPKStudent.map((cPKStudent) => cPKStudent.id) - : CPKStudent?.id - ); - const CPKClassesIdSet = new Set( - Array.isArray(CPKClasses) - ? CPKClasses.map((cPKClass) => cPKClass.id) - : CPKClasses?.id - ); - const CPKProjectsIdSet = new Set( - Array.isArray(CPKProjects) - ? CPKProjects.map((cPKProject) => cPKProject.id) - : CPKProjects?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { const cleanValues = cPKTeacherRecord @@ -2551,6 +3645,28 @@ export default function UpdateCPKTeacherForm(props) { const [currentCPKProjectsValue, setCurrentCPKProjectsValue] = React.useState(undefined); const CPKProjectsRef = React.createRef(); + const getIDValue = { + CPKStudent: (r) => + JSON.stringify({ specialStudentId: r?.specialStudentId }), + CPKClasses: (r) => JSON.stringify({ specialClassId: r?.specialClassId }), + CPKProjects: (r) => + JSON.stringify({ specialProjectId: r?.specialProjectId }), + }; + const CPKStudentIdSet = new Set( + Array.isArray(CPKStudent) + ? CPKStudent.map((r) => getIDValue.CPKStudent?.(r)) + : getIDValue.CPKStudent?.(CPKStudent) + ); + const CPKClassesIdSet = new Set( + Array.isArray(CPKClasses) + ? CPKClasses.map((r) => getIDValue.CPKClasses?.(r)) + : getIDValue.CPKClasses?.(CPKClasses) + ); + const CPKProjectsIdSet = new Set( + Array.isArray(CPKProjects) + ? CPKProjects.map((r) => getIDValue.CPKProjects?.(r)) + : getIDValue.CPKProjects?.(CPKProjects) + ); const cPKStudentRecords = useDataStoreBinding({ type: \\"collection\\", model: CPKStudent0, @@ -2564,9 +3680,9 @@ export default function UpdateCPKTeacherForm(props) { model: CPKProject, }).items; const getDisplayValue = { - CPKStudent: (record) => record?.specialStudentId, - CPKClasses: (record) => record?.specialClassId, - CPKProjects: (record) => record?.specialProjectId, + CPKStudent: (r) => r?.specialStudentId, + CPKClasses: (r) => r?.specialClassId, + CPKProjects: (r) => r?.specialProjectId, }; const validations = { specialTeacherId: [{ type: \\"Required\\" }], @@ -2818,14 +3934,18 @@ export default function UpdateCPKTeacherForm(props) { isReadOnly={false} value={currentCPKStudentDisplayValue} options={cPKStudentRecords - .filter((cPKStudent) => !CPKStudentIdSet.has(cPKStudent.id)) + .filter((r) => !CPKStudentIdSet.has(getIDValue.CPKStudent?.(r))) .map((r) => ({ - id: r.specialStudentId, + id: getIDValue.CPKStudent?.(r), label: getDisplayValue.CPKStudent?.(r), }))} onSelect={({ id, label }) => { setCurrentCPKStudentValue( - cPKStudentRecords.find((r) => r.specialStudentId === id) + cPKStudentRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) ); setCurrentCPKStudentDisplayValue(label); }} @@ -2884,14 +4004,18 @@ export default function UpdateCPKTeacherForm(props) { isReadOnly={false} value={currentCPKClassesDisplayValue} options={cPKClassRecords - .filter((cPKClass) => !CPKClassesIdSet.has(cPKClass.id)) + .filter((r) => !CPKClassesIdSet.has(getIDValue.CPKClasses?.(r))) .map((r) => ({ - id: r.specialClassId, + id: getIDValue.CPKClasses?.(r), label: getDisplayValue.CPKClasses?.(r), }))} onSelect={({ id, label }) => { setCurrentCPKClassesValue( - cPKClassRecords.find((r) => r.specialClassId === id) + cPKClassRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) ); setCurrentCPKClassesDisplayValue(label); }} @@ -2951,14 +4075,18 @@ export default function UpdateCPKTeacherForm(props) { isReadOnly={false} value={currentCPKProjectsDisplayValue} options={cPKProjectRecords - .filter((cPKProject) => !CPKProjectsIdSet.has(cPKProject.id)) + .filter((r) => !CPKProjectsIdSet.has(getIDValue.CPKProjects?.(r))) .map((r) => ({ - id: r.specialProjectId, + id: getIDValue.CPKProjects?.(r), label: getDisplayValue.CPKProjects?.(r), }))} onSelect={({ id, label }) => { setCurrentCPKProjectsValue( - cPKProjectRecords.find((r) => r.specialProjectId === id) + cPKProjectRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) ); setCurrentCPKProjectsDisplayValue(label); }} @@ -5015,9 +6143,6 @@ export default function TagCreateForm(props) { const [label, setLabel] = React.useState(initialValues.label); const [Posts, setPosts] = React.useState(initialValues.Posts); const [statuses, setStatuses] = React.useState(initialValues.statuses); - const PostsIdSet = new Set( - Array.isArray(Posts) ? Posts.map((post) => post.id) : Posts?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setLabel(initialValues.label); @@ -5035,19 +6160,27 @@ export default function TagCreateForm(props) { const [currentStatusesValue, setCurrentStatusesValue] = React.useState(undefined); 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 = useDataStoreBinding({ type: \\"collection\\", model: Post, }).items; const getDisplayValue = { - Posts: (record) => record?.title, - statuses: (record) => { + Posts: (r) => r?.title, + statuses: (r) => { const enumDisplayValueMap = { PENDING: \\"Pending\\", POSTED: \\"Posted\\", IN_REVIEW: \\"In review\\", }; - return enumDisplayValueMap[record]; + return enumDisplayValueMap[r]; }, }; const validations = { @@ -5203,13 +6336,19 @@ export default function TagCreateForm(props) { isReadOnly={false} value={currentPostsDisplayValue} options={postRecords - .filter((post) => !PostsIdSet.has(post.id)) + .filter((r) => !PostsIdSet.has(getIDValue.Posts?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Posts?.(r), label: getDisplayValue.Posts?.(r), }))} onSelect={({ id, label }) => { - setCurrentPostsValue(postRecords.find((r) => r.id === id)); + setCurrentPostsValue( + postRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentPostsDisplayValue(label); }} onClear={() => { @@ -5558,9 +6697,6 @@ export default function MyMemberForm(props) { }; const [name, setName] = React.useState(initialValues.name); const [Team, setTeam] = React.useState(initialValues.Team); - const TeamIdSet = new Set( - Array.isArray(Team) ? Team.map((team) => team.id) : Team?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setName(initialValues.name); @@ -5573,12 +6709,20 @@ export default function MyMemberForm(props) { 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 = useDataStoreBinding({ type: \\"collection\\", model: Team0, }).items; const getDisplayValue = { - Team: (record) => record?.name, + Team: (r) => r?.name, }; const validations = { name: [], @@ -5748,13 +6892,19 @@ export default function MyMemberForm(props) { isReadOnly={false} value={currentTeamDisplayValue} options={teamRecords - .filter((team) => !TeamIdSet.has(team.id)) + .filter((r) => !TeamIdSet.has(getIDValue.Team?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Team?.(r), label: getDisplayValue.Team?.(r), }))} onSelect={({ id, label }) => { - setCurrentTeamValue(teamRecords.find((r) => r.id === id)); + setCurrentTeamValue( + teamRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentTeamDisplayValue(label); }} onClear={() => { @@ -6009,11 +7159,6 @@ export default function SchoolCreateForm(props) { }; const [name, setName] = React.useState(initialValues.name); const [Students, setStudents] = React.useState(initialValues.Students); - const StudentsIdSet = new Set( - Array.isArray(Students) - ? Students.map((student) => student.id) - : Students?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setName(initialValues.name); @@ -6027,12 +7172,20 @@ export default function SchoolCreateForm(props) { 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 = useDataStoreBinding({ type: \\"collection\\", model: Student, }).items; const getDisplayValue = { - Students: (record) => record?.name, + Students: (r) => r?.name, }; const validations = { name: [], @@ -6182,13 +7335,19 @@ export default function SchoolCreateForm(props) { isReadOnly={false} value={currentStudentsDisplayValue} options={studentRecords - .filter((student) => !StudentsIdSet.has(student.id)) + .filter((r) => !StudentsIdSet.has(getIDValue.Students?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Students?.(r), label: getDisplayValue.Students?.(r), }))} onSelect={({ id, label }) => { - setCurrentStudentsValue(studentRecords.find((r) => r.id === id)); + setCurrentStudentsValue( + studentRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentStudentsDisplayValue(label); }} onClear={() => { @@ -6476,11 +7635,6 @@ export default function BookCreateForm(props) { const [primaryAuthor, setPrimaryAuthor] = React.useState( initialValues.primaryAuthor ); - const primaryAuthorIdSet = new Set( - Array.isArray(primaryAuthor) - ? primaryAuthor.map((author) => author.id) - : primaryAuthor?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setName(initialValues.name); @@ -6496,12 +7650,20 @@ export default function BookCreateForm(props) { 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 = useDataStoreBinding({ type: \\"collection\\", model: Author, }).items; const getDisplayValue = { - primaryAuthor: (record) => record?.name, + primaryAuthor: (r) => r?.name, }; const validations = { name: [], @@ -6673,14 +7835,20 @@ export default function BookCreateForm(props) { isReadOnly={false} value={currentPrimaryAuthorDisplayValue} options={authorRecords - .filter((author) => !primaryAuthorIdSet.has(author.id)) + .filter( + (r) => !primaryAuthorIdSet.has(getIDValue.primaryAuthor?.(r)) + ) .map((r) => ({ - id: r.id, + id: getIDValue.primaryAuthor?.(r), label: getDisplayValue.primaryAuthor?.(r), }))} onSelect={({ id, label }) => { setCurrentPrimaryAuthorValue( - authorRecords.find((r) => r.id === id) + authorRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) ); setCurrentPrimaryAuthorDisplayValue(label); }} @@ -6939,9 +8107,6 @@ export default function TagCreateForm(props) { const [label, setLabel] = React.useState(initialValues.label); const [Posts, setPosts] = React.useState(initialValues.Posts); const [statuses, setStatuses] = React.useState(initialValues.statuses); - const PostsIdSet = new Set( - Array.isArray(Posts) ? Posts.map((post) => post.id) : Posts?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setLabel(initialValues.label); @@ -6959,19 +8124,27 @@ export default function TagCreateForm(props) { const [currentStatusesValue, setCurrentStatusesValue] = React.useState(undefined); 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 = useDataStoreBinding({ type: \\"collection\\", model: Post, }).items; const getDisplayValue = { - Posts: (record) => record?.title, - statuses: (record) => { + Posts: (r) => r?.title, + statuses: (r) => { const enumDisplayValueMap = { PENDING: \\"Pending\\", POSTED: \\"Posted\\", IN_REVIEW: \\"In review\\", }; - return enumDisplayValueMap[record]; + return enumDisplayValueMap[r]; }, }; const validations = { @@ -7127,13 +8300,19 @@ export default function TagCreateForm(props) { isReadOnly={false} value={currentPostsDisplayValue} options={postRecords - .filter((post) => !PostsIdSet.has(post.id)) + .filter((r) => !PostsIdSet.has(getIDValue.Posts?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Posts?.(r), label: getDisplayValue.Posts?.(r), }))} onSelect={({ id, label }) => { - setCurrentPostsValue(postRecords.find((r) => r.id === id)); + setCurrentPostsValue( + postRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentPostsDisplayValue(label); }} onClear={() => { @@ -7488,16 +8667,6 @@ export default function BookCreateForm(props) { const [primaryTitle, setPrimaryTitle] = React.useState( initialValues.primaryTitle ); - const primaryAuthorIdSet = new Set( - Array.isArray(primaryAuthor) - ? primaryAuthor.map((author) => author.id) - : primaryAuthor?.id - ); - const primaryTitleIdSet = new Set( - Array.isArray(primaryTitle) - ? primaryTitle.map((title) => title.id) - : primaryTitle?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setName(initialValues.name); @@ -7521,6 +8690,20 @@ export default function BookCreateForm(props) { 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 = useDataStoreBinding({ type: \\"collection\\", model: Author, @@ -7530,8 +8713,8 @@ export default function BookCreateForm(props) { model: Title, }).items; const getDisplayValue = { - primaryAuthor: (record) => record?.name, - primaryTitle: (record) => record?.name, + primaryAuthor: (r) => r?.name, + primaryTitle: (r) => r?.name, }; const validations = { name: [], @@ -7707,14 +8890,20 @@ export default function BookCreateForm(props) { isReadOnly={false} value={currentPrimaryAuthorDisplayValue} options={authorRecords - .filter((author) => !primaryAuthorIdSet.has(author.id)) + .filter( + (r) => !primaryAuthorIdSet.has(getIDValue.primaryAuthor?.(r)) + ) .map((r) => ({ - id: r.id, + id: getIDValue.primaryAuthor?.(r), label: getDisplayValue.primaryAuthor?.(r), }))} onSelect={({ id, label }) => { setCurrentPrimaryAuthorValue( - authorRecords.find((r) => r.id === id) + authorRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) ); setCurrentPrimaryAuthorDisplayValue(label); }} @@ -7774,13 +8963,19 @@ export default function BookCreateForm(props) { isReadOnly={false} value={currentPrimaryTitleDisplayValue} options={titleRecords - .filter((title) => !primaryTitleIdSet.has(title.id)) + .filter((r) => !primaryTitleIdSet.has(getIDValue.primaryTitle?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.primaryTitle?.(r), label: getDisplayValue.primaryTitle?.(r), }))} onSelect={({ id, label }) => { - setCurrentPrimaryTitleValue(titleRecords.find((r) => r.id === id)); + setCurrentPrimaryTitleValue( + titleRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentPrimaryTitleDisplayValue(label); }} onClear={() => { @@ -8538,11 +9733,6 @@ export default function SchoolUpdateForm(props) { }; const [name, setName] = React.useState(initialValues.name); const [Students, setStudents] = React.useState(initialValues.Students); - const StudentsIdSet = new Set( - Array.isArray(Students) - ? Students.map((student) => student.id) - : Students?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { const cleanValues = schoolRecord @@ -8571,12 +9761,20 @@ export default function SchoolUpdateForm(props) { 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 = useDataStoreBinding({ type: \\"collection\\", model: Student, }).items; const getDisplayValue = { - Students: (record) => record?.name, + Students: (r) => r?.name, }; const validations = { name: [], @@ -8754,13 +9952,19 @@ export default function SchoolUpdateForm(props) { isReadOnly={false} value={currentStudentsDisplayValue} options={studentRecords - .filter((student) => !StudentsIdSet.has(student.id)) + .filter((r) => !StudentsIdSet.has(getIDValue.Students?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Students?.(r), label: getDisplayValue.Students?.(r), }))} onSelect={({ id, label }) => { - setCurrentStudentsValue(studentRecords.find((r) => r.id === id)); + setCurrentStudentsValue( + studentRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentStudentsDisplayValue(label); }} onClear={() => { @@ -9048,9 +10252,6 @@ export default function MyMemberForm(props) { }; const [name, setName] = React.useState(initialValues.name); const [Team, setTeam] = React.useState(initialValues.Team); - const TeamIdSet = new Set( - Array.isArray(Team) ? Team.map((team) => team.id) : Team?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { const cleanValues = memberRecord @@ -9077,12 +10278,20 @@ export default function MyMemberForm(props) { 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 = useDataStoreBinding({ type: \\"collection\\", model: Team0, }).items; const getDisplayValue = { - Team: (record) => record?.name, + Team: (r) => r?.name, }; const validations = { name: [], @@ -9255,13 +10464,19 @@ export default function MyMemberForm(props) { isReadOnly={false} value={currentTeamDisplayValue} options={teamRecords - .filter((team) => !TeamIdSet.has(team.id)) + .filter((r) => !TeamIdSet.has(getIDValue.Team?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Team?.(r), label: getDisplayValue.Team?.(r), }))} onSelect={({ id, label }) => { - setCurrentTeamValue(teamRecords.find((r) => r.id === id)); + setCurrentTeamValue( + teamRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentTeamDisplayValue(label); }} onClear={() => { @@ -9522,9 +10737,6 @@ export default function TagUpdateForm(props) { const [label, setLabel] = React.useState(initialValues.label); const [Posts, setPosts] = React.useState(initialValues.Posts); const [statuses, setStatuses] = React.useState(initialValues.statuses); - const PostsIdSet = new Set( - Array.isArray(Posts) ? Posts.map((post) => post.id) : Posts?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { const cleanValues = tagRecord @@ -9565,19 +10777,27 @@ export default function TagUpdateForm(props) { const [currentStatusesValue, setCurrentStatusesValue] = React.useState(undefined); 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 = useDataStoreBinding({ type: \\"collection\\", model: Post, }).items; const getDisplayValue = { - Posts: (record) => record?.title, - statuses: (record) => { + Posts: (r) => r?.title, + statuses: (r) => { const enumDisplayValueMap = { PENDING: \\"Pending\\", POSTED: \\"Posted\\", IN_REVIEW: \\"In review\\", }; - return enumDisplayValueMap[record]; + return enumDisplayValueMap[r]; }, }; const validations = { @@ -9782,13 +11002,19 @@ export default function TagUpdateForm(props) { isReadOnly={false} value={currentPostsDisplayValue} options={postRecords - .filter((post) => !PostsIdSet.has(post.id)) + .filter((r) => !PostsIdSet.has(getIDValue.Posts?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Posts?.(r), label: getDisplayValue.Posts?.(r), }))} onSelect={({ id, label }) => { - setCurrentPostsValue(postRecords.find((r) => r.id === id)); + setCurrentPostsValue( + postRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentPostsDisplayValue(label); }} onClear={() => { @@ -14068,9 +15294,6 @@ export default function MyMemberForm(props) { }; const [name, setName] = React.useState(initialValues.name); const [Team, setTeam] = React.useState(initialValues.Team); - const TeamIdSet = new Set( - Array.isArray(Team) ? Team.map((team) => team.id) : Team?.id - ); const [errors, setErrors] = React.useState({}); const resetStateValues = () => { setName(initialValues.name); @@ -14083,12 +15306,20 @@ export default function MyMemberForm(props) { 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 = useDataStoreBinding({ type: \\"collection\\", model: Team0, }).items; const getDisplayValue = { - Team: (record) => record?.name, + Team: (r) => r?.name, }; const validations = { name: [], @@ -14258,13 +15489,19 @@ export default function MyMemberForm(props) { isReadOnly={false} value={currentTeamDisplayValue} options={teamRecords - .filter((team) => !TeamIdSet.has(team.id)) + .filter((r) => !TeamIdSet.has(getIDValue.Team?.(r))) .map((r) => ({ - id: r.id, + id: getIDValue.Team?.(r), label: getDisplayValue.Team?.(r), }))} onSelect={({ id, label }) => { - setCurrentTeamValue(teamRecords.find((r) => r.id === id)); + setCurrentTeamValue( + teamRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); setCurrentTeamDisplayValue(label); }} onClear={() => { 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 998de0329..6b4a6c5ed 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 @@ -97,7 +97,7 @@ describe('amplify form renderer tests', () => { // Check that custom field label is working as expected expect(componentText).toContain('Team Label'); // Check that Autocomplete custom display value is set - expect(componentText).toContain('Team: (record) => record?.name'); + expect(componentText).toContain('Team: (r) => r?.name'); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); @@ -115,7 +115,7 @@ describe('amplify form renderer tests', () => { expect(componentText).toContain('const postRecords = useDataStoreBinding({'); // check custom display value is set - expect(componentText).toContain('Posts: (record) => record?.title'); + expect(componentText).toContain('Posts: (r) => r?.title'); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); @@ -136,7 +136,7 @@ describe('amplify form renderer tests', () => { expect(componentText).toContain('await record.Posts.toArray()'); // check custom display value is set - expect(componentText).toContain('Posts: (record) => record?.title'); + expect(componentText).toContain('Posts: (r) => r?.title'); // check linked data useState is generate expect(componentText).toContain('const [linkedPosts, setLinkedPosts] = React.useState([]);'); @@ -154,8 +154,8 @@ describe('amplify form renderer tests', () => { 'datastore/tag-post', ); // get displayValue function - expect(componentText).toContain('statuses: (record) => {'); - expect(componentText).toContain('return enumDisplayValueMap[record];'); + expect(componentText).toContain('statuses: (r) => {'); + expect(componentText).toContain('return enumDisplayValueMap[r];'); // ArrayField returns the item on a badge click expect(componentText).toContain('setFieldValue(items[index]);'); // set the badgeText param @@ -178,7 +178,7 @@ describe('amplify form renderer tests', () => { expect(componentText).toContain('const studentRecords = useDataStoreBinding({'); // check custom display value is set - expect(componentText).toContain('Students: (record) => record?.name'); + expect(componentText).toContain('Students: (r) => r?.name'); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); @@ -199,7 +199,7 @@ describe('amplify form renderer tests', () => { expect(componentText).toContain('const linkedStudents = record ? await record.Students.toArray() : [];'); // check custom display value is set - expect(componentText).toContain('Students: (record) => record?.name'); + expect(componentText).toContain('Students: (r) => r?.name'); // check linked data useState is generate expect(componentText).toContain('const [linkedStudents, setLinkedStudents] = React.useState([]);'); @@ -385,8 +385,8 @@ describe('amplify form renderer tests', () => { // hasOne expect(componentText).toContain('specialTeacherId: specialTeacherIdProp'); expect(componentText).toContain('await DataStore.query(CPKTeacher, specialTeacherIdProp)'); - expect(componentText).toContain('Student: (record) => record?.specialStudentId'); - expect(componentText).toContain('id: r.specialStudentId'); + expect(componentText).toContain('Student: (r) => r?.specialStudentId'); + expect(componentText).toContain('JSON.stringify({ specialStudentId: r?.specialStudentId })'); // manyToMany expect(componentText).toContain('const count = cPKClassesMap.get(r.specialClassId)'); @@ -404,6 +404,16 @@ describe('amplify form renderer tests', () => { expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); }); + + it('should render an update form for model with composite keys', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/composite-dog-datastore-update', + 'datastore/composite-relationships', + ); + + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); }); }); }); diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/all-props.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/all-props.ts index bf0cca36e..e9535e167 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/all-props.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/all-props.ts @@ -25,7 +25,7 @@ import { } from './event-handler-props'; import { getArrayChildRefName, resetValuesName } from './form-state'; import { shouldWrapInArrayField } from './render-checkers'; -import { getAutocompleteOptionsProp } from './display-value'; +import { getAutocompleteOptionsProp } from './model-values'; import { buildCtaLayoutProperties } from '../../react-component-render-helper'; export const addFormAttributes = (component: StudioComponent | StudioComponentChild, formMetadata: FormMetadata) => { 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 60309e96d..a2b1e3304 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 @@ -24,7 +24,7 @@ import { ExpressionStatement, } from 'typescript'; import { lowerCaseFirst } from '../../helpers'; -import { getDisplayValueObjectName } from './display-value'; +import { getDisplayValueObjectName } from './model-values'; import { getSetNameIdentifier } from './form-state'; import { buildHasManyRelationshipDataStoreStatements, diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/display-value.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/display-value.ts deleted file mode 100644 index 0366f99ef..000000000 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/display-value.ts +++ /dev/null @@ -1,370 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"). - You may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import { - FieldConfigMetadata, - InvalidInputError, - isValidVariableName, - StudioFormValueMappings, -} from '@aws-amplify/codegen-ui'; -import { StudioFormInputFieldProperty } from '@aws-amplify/codegen-ui/lib/types/form/input-config'; -import { isEnumFieldType } from '@aws-amplify/datastore'; -import { - Expression, - factory, - JsxAttribute, - PropertyAssignment, - SyntaxKind, - NodeFlags, - CallExpression, - VariableStatement, -} from 'typescript'; -import { lowerCaseFirst } from '../../helpers'; -import { - buildBindingExpression, - buildConcatExpression, - isBoundProperty, - isConcatenatedProperty, - isFixedPropertyWithValue, -} from '../../react-component-render-helper'; -import { getRecordsName } from './form-state'; -import { getElementAccessExpression } from './invalid-variable-helpers'; -import { isModelDataType } from './render-checkers'; - -export const getDisplayValueObjectName = 'getDisplayValue'; - -/** - authorRecords.map((r) => ({ - id: r.id, - label: r.id, - })) - */ - -/** - examples: - for model - - authorRecords.map((r) => ({ - id: r.id, - label: getDisplayValue['primaryAuthor']?.(r), - })) - - for scalar - - authorRecords.map((r) => ({ - id: r.id, - label: r.id, - })) - */ -function getModelTypeSuggestions({ - modelName, - fieldName, - key, - isModelType, -}: { - modelName: string; - fieldName: string; - key: string; - isModelType: boolean; -}): CallExpression { - const recordString = 'r'; - - const labelExpression = isModelType - ? factory.createCallChain( - getElementAccessExpression('getDisplayValue', fieldName), - factory.createToken(SyntaxKind.QuestionDotToken), - undefined, - [factory.createIdentifier(recordString)], - ) - : getElementAccessExpression(recordString, key); - - const filterOptionsExpression = isModelType - ? factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(getRecordsName(modelName)), - factory.createIdentifier('filter'), - ), - undefined, - [ - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier(lowerCaseFirst(modelName)), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(`${fieldName}IdSet`), - factory.createIdentifier('has'), - ), - undefined, - [ - factory.createPropertyAccessExpression( - factory.createIdentifier(lowerCaseFirst(modelName)), - factory.createIdentifier('id'), - ), - ], - ), - ), - ), - ], - ) - : factory.createIdentifier(getRecordsName(modelName)); - - return factory.createCallExpression( - factory.createPropertyAccessExpression(filterOptionsExpression, factory.createIdentifier('map')), - undefined, - [ - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier(recordString), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - factory.createParenthesizedExpression( - factory.createObjectLiteralExpression( - [ - factory.createPropertyAssignment( - factory.createIdentifier('id'), - getElementAccessExpression(recordString, key), - ), - factory.createPropertyAssignment(factory.createIdentifier('label'), labelExpression), - ], - true, - ), - ), - ), - ], - ); -} - -export function extractModelAndKey(valueMappings?: StudioFormValueMappings): { model?: string; key?: string } { - let model: undefined | string; - let key: undefined | string; - const bindingProperty = valueMappings?.bindingProperties && Object.values(valueMappings.bindingProperties)[0]; - if (bindingProperty && bindingProperty.type === 'Data') { - model = bindingProperty.bindingProperties.model; - const { value } = valueMappings.values[0]; - if (isBoundProperty(value) && value.bindingProperties.field) { - key = value.bindingProperties.field; - } - } - - return { model, key }; -} - -/** - example: - options={authorRecords.map(r) => ({ - id: r.id, - label: getDisplayValue['primaryAuthor']?.(r) ?? r.id, - }))} - */ -export function getAutocompleteOptionsProp({ - fieldName, - fieldConfig, -}: { - fieldName: string; - fieldConfig: FieldConfigMetadata; -}): JsxAttribute { - let options: Expression | undefined; - - const { valueMappings } = fieldConfig; - const { model, key } = extractModelAndKey(valueMappings); - - if (model && key) { - options = getModelTypeSuggestions({ - modelName: model, - fieldName, - key, - isModelType: isModelDataType(fieldConfig), - }); - } - - if (!options) { - throw new InvalidInputError(`Invalid value mappings on ${fieldName}`); - } - - return factory.createJsxAttribute( - factory.createIdentifier('options'), - factory.createJsxExpression(undefined, options), - ); -} - -// impure helper -/* eslint-disable no-param-reassign */ -function replaceProperty(prop: StudioFormInputFieldProperty, toReplace: string, replaceWith: string): void { - if (isBoundProperty(prop) && prop.bindingProperties.property === toReplace) { - prop.bindingProperties.property = replaceWith; - } - if (isConcatenatedProperty(prop)) { - prop.concat.forEach((subProp) => replaceProperty(subProp as StudioFormInputFieldProperty, toReplace, replaceWith)); - } -} -/* eslint-enable no-param-reassign */ - -export function getDisplayValueObject(displayValueFunctions: PropertyAssignment[]) { - return factory.createVariableStatement( - undefined, - factory.createVariableDeclarationList( - [ - factory.createVariableDeclaration( - factory.createIdentifier(getDisplayValueObjectName), - undefined, - undefined, - factory.createObjectLiteralExpression(displayValueFunctions, true), - ), - ], - NodeFlags.Const, - ), - ); -} - -// example - primaryAuthor: (record) => record?.name, -export function buildDisplayValueFunction(fieldName: string, fieldConfig: FieldConfigMetadata): PropertyAssignment { - const recordString = 'record'; - const propertyName = isValidVariableName(fieldName) - ? factory.createIdentifier(fieldName) - : factory.createStringLiteral(fieldName); - let additionalStatements: VariableStatement[] = []; - const { key: primaryKey } = extractModelAndKey(fieldConfig.valueMappings); - - let renderedDisplayValue: Expression = factory.createPropertyAccessChain( - factory.createIdentifier(recordString), - factory.createToken(SyntaxKind.QuestionDotToken), - // if this expression is used, primaryKey should exist - factory.createIdentifier(primaryKey || ''), - ); - - if (isModelDataType(fieldConfig) && fieldConfig.valueMappings) { - const valueConfig = fieldConfig.valueMappings.values[0]; - if (valueConfig) { - const displayValueProperty = valueConfig.displayValue || valueConfig.value; - const modelName = fieldConfig.dataType.model; - replaceProperty(displayValueProperty, modelName, recordString); - if (isConcatenatedProperty(displayValueProperty)) { - renderedDisplayValue = buildConcatExpression(displayValueProperty); - } else if (isBoundProperty(displayValueProperty)) { - renderedDisplayValue = buildBindingExpression(displayValueProperty); - } - } - } - - if (isEnumFieldType(fieldConfig.dataType) && fieldConfig.valueMappings && fieldConfig.isArray) { - const displayValueMapName = `enumDisplayValueMap`; - additionalStatements = [ - factory.createVariableStatement( - undefined, - factory.createVariableDeclarationList( - [ - factory.createVariableDeclaration( - factory.createIdentifier(displayValueMapName), - undefined, - undefined, - factory.createObjectLiteralExpression( - fieldConfig.valueMappings.values.map((v) => { - let value = ''; - let displayValue = ''; - if (isFixedPropertyWithValue(v.value)) { - value = v.value.value.toString(); - } - if (v.displayValue && isFixedPropertyWithValue(v.displayValue)) { - displayValue = v.displayValue.value.toString(); - } - if (value === '') { - throw Error('Enum cannot have an empty value'); - } - return factory.createPropertyAssignment( - factory.createStringLiteral(value), - factory.createStringLiteral(displayValue ?? value), - ); - }), - - true, - ), - ), - ], - NodeFlags.Const, - ), - ), - ]; - renderedDisplayValue = factory.createElementAccessExpression( - factory.createIdentifier(displayValueMapName), - factory.createIdentifier(recordString), - ); - } - - return factory.createPropertyAssignment( - propertyName, - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier(recordString), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - additionalStatements.length - ? factory.createBlock([...additionalStatements, factory.createReturnStatement(renderedDisplayValue)], false) - : renderedDisplayValue, - ), - ); -} - -export function getModelsToImport(fieldConfig: FieldConfigMetadata): string[] { - const modelDependencies: string[] = []; - if (fieldConfig.valueMappings && fieldConfig.valueMappings.bindingProperties) { - Object.values(fieldConfig.valueMappings.bindingProperties).forEach((prop) => { - if (prop.type === 'Data' && prop.bindingProperties.model) { - modelDependencies.push(prop.bindingProperties.model); - } - }); - } - - // Import join table model - if (fieldConfig.relationship?.type === 'HAS_MANY' && fieldConfig.relationship.relatedJoinTableName) { - modelDependencies.push(fieldConfig.relationship.relatedJoinTableName); - } - - return modelDependencies; -} diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts index 3d4109a25..41812471e 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/event-handler-props.ts @@ -54,7 +54,7 @@ import { import { getOnChangeValidationBlock } from './validation'; import { buildModelFieldObject } from './model-fields'; import { isModelDataType, shouldWrapInArrayField } from './render-checkers'; -import { extractModelAndKey } from './display-value'; +import { extractModelAndKeys, getMatchEveryModelFieldCallExpression } from './model-values'; export const buildMutationBindings = (form: StudioForm, primaryKey?: string) => { const { @@ -318,17 +318,21 @@ export const buildOnChangeStatement = ( ); }; -// onSelect={({ id }) => { -// setCurrentPrimaryAuthorValue( -// id -// ); -// setCurrentPrimaryAuthorDisplayValue(id); -// }} - /** - example: +examples: + + scalar: + onSelect={({ id }) => { + setCurrentPrimaryAuthorValue(id); + setCurrentPrimaryAuthorDisplayValue(id); + }} + + model: onSelect={({ id, label }) => { - setCurrentPrimaryAuthorValue(authorRecords.find((r) => r.id === id)); + setCurrentPrimaryAuthorValue( + primaryAuthorRecords.find((r) => Object.entries(JSON.parse(id)).every(([key, value]) => + r[key] === value))); + ); setCurrentPrimaryAuthorDisplayValue(label); }} */ @@ -339,14 +343,8 @@ export function buildOnSelect({ sanitizedFieldName: string; fieldConfig: FieldConfigMetadata; }): JsxAttribute { - const { model, key } = extractModelAndKey(fieldConfig.valueMappings); - if (!model || !key) { - throw new InvalidInputError(`Invalid value mappings`); - } - const labelString = 'label'; const idString = 'id'; - const recordString = 'r'; const props: BindingElement[] = [ factory.createBindingElement(undefined, undefined, factory.createIdentifier(idString), undefined), @@ -356,44 +354,19 @@ export function buildOnSelect({ let nextCurrentDisplayValue: Expression = factory.createIdentifier(idString); if (isModelDataType(fieldConfig)) { + const { model, keys } = extractModelAndKeys(fieldConfig.valueMappings); + if (!model || !keys || !keys.length) { + throw new InvalidInputError(`Invalid value mappings`); + } + props.push(factory.createBindingElement(undefined, undefined, factory.createIdentifier(labelString), undefined)); nextCurrentDisplayValue = factory.createIdentifier(labelString); - nextCurrentValue = factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(getRecordsName(model)), - factory.createIdentifier('find'), - ), - undefined, - [ - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier(recordString), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - factory.createBinaryExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(recordString), - factory.createIdentifier(key), - ), - factory.createToken(SyntaxKind.EqualsEqualsEqualsToken), - factory.createIdentifier(idString), - ), - ), - ], - ); + nextCurrentValue = getMatchEveryModelFieldCallExpression({ + recordsArrayName: getRecordsName(model), + JSONName: idString, + }); } const setStateExpressions: ExpressionStatement[] = [ diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts index 4eb219f42..91dde618e 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/form-state.ts @@ -599,80 +599,3 @@ export const buildResetValuesOnRecordUpdate = (recordName: string, linkedDataNam ), ); }; - -// e.g. const PostsIdSet = new Set(Posts.map(post => post.id)); -export const buildSelectedRecordsIdSet = (fieldConfigs: Record): Statement[] => { - const statements: Statement[] = []; - Object.entries(fieldConfigs).forEach((fieldConfig) => { - const [name, fieldConfigMetaData] = fieldConfig; - const fieldName = fieldConfigMetaData.sanitizedFieldName || name; - if (fieldConfigMetaData.relationship) { - const { relatedModelName } = fieldConfigMetaData.relationship; - statements.push( - factory.createVariableStatement( - undefined, - factory.createVariableDeclarationList( - [ - factory.createVariableDeclaration( - factory.createIdentifier(`${fieldName}IdSet`), - undefined, - undefined, - factory.createNewExpression(factory.createIdentifier('Set'), undefined, [ - factory.createConditionalExpression( - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('Array'), - factory.createIdentifier('isArray'), - ), - undefined, - [factory.createIdentifier(fieldName)], - ), - factory.createToken(SyntaxKind.QuestionToken), - factory.createCallExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(fieldName), - factory.createIdentifier('map'), - ), - undefined, - [ - factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier(lowerCaseFirst(relatedModelName)), - undefined, - undefined, - undefined, - ), - ], - undefined, - factory.createToken(SyntaxKind.EqualsGreaterThanToken), - factory.createPropertyAccessExpression( - factory.createIdentifier(lowerCaseFirst(relatedModelName)), - factory.createIdentifier('id'), - ), - ), - ], - ), - factory.createToken(SyntaxKind.ColonToken), - factory.createPropertyAccessChain( - factory.createIdentifier(fieldName), - factory.createToken(SyntaxKind.QuestionDotToken), - factory.createIdentifier('id'), - ), - ), - ]), - ), - ], - NodeFlags.Const, - ), - ), - ); - } - }); - return statements; -}; diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/index.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/index.ts index f4021da8b..37e7f18e7 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/index.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/index.ts @@ -21,13 +21,7 @@ export { buildMutationBindings, buildOverrideOnChangeStatement } from './event-h export { buildOverrideTypesBindings } from './type-helper'; -export { - buildResetValuesOnRecordUpdate, - buildSetStateFunction, - buildSelectedRecordsIdSet, - getLinkedDataName, - getPropName, -} from './form-state'; +export { buildResetValuesOnRecordUpdate, buildSetStateFunction, getLinkedDataName, getPropName } from './form-state'; export { buildValidations, runValidationTasksFunction } from './validation'; diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/invalid-variable-helpers.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/invalid-variable-helpers.ts index 1fc0560bd..6bbeade28 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/invalid-variable-helpers.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/invalid-variable-helpers.ts @@ -28,3 +28,7 @@ export function getElementAccessExpression(elementName: string, propertyName: st factory.createStringLiteral(propertyName), ); } + +export function getValidProperty(key: string) { + return isValidVariableName(key) ? factory.createIdentifier(key) : factory.createStringLiteral(key); +} diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/map-from-fieldConfigs.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/map-from-fieldConfigs.ts index 1db82baa6..b051a6fd9 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/map-from-fieldConfigs.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/map-from-fieldConfigs.ts @@ -15,8 +15,18 @@ */ import { FieldConfigMetadata } from '@aws-amplify/codegen-ui'; import { PropertyAssignment } from 'typescript'; -import { buildDisplayValueFunction, getDisplayValueObject, getModelsToImport } from './display-value'; -import { shouldImplementDisplayValueFunction, shouldWrapInArrayField } from './render-checkers'; +import { + buildDisplayValueFunction, + getDisplayValueObject, + getModelsToImport, + buildIDValueFunction, + getIDValueObject, +} from './model-values'; +import { + shouldImplementDisplayValueFunction, + shouldWrapInArrayField, + shouldImplementIDValueFunction, +} from './render-checkers'; import { buildValidationForField, buildValidations } from './validation'; /** @@ -28,6 +38,7 @@ export function mapFromFieldConfigs(fieldConfigs: Record { + return factory.createCallChain( + getElementAccessExpression(getIDValueObjectName, fieldName), + factory.createToken(SyntaxKind.QuestionDotToken), + undefined, + [factory.createIdentifier(recordString)], + ); +}; + +const getDisplayValueCallChain = ({ fieldName, recordString }: { fieldName: string; recordString: string }) => { + return factory.createCallChain( + getElementAccessExpression(getDisplayValueObjectName, fieldName), + factory.createToken(SyntaxKind.QuestionDotToken), + undefined, + [factory.createIdentifier(recordString)], + ); +}; + +/** + examples: + for model - + authorRecords + .map((r) => ({ + id: r?.id, + label: r?.id, + })) + */ +function getSuggestionsForRelationshipScalar({ modelName, key }: { modelName: string; key: string }): CallExpression { + const recordString = 'r'; + + return factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(getRecordsName(modelName)), + factory.createIdentifier('map'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(recordString), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createParenthesizedExpression( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment(factory.createIdentifier('id'), buildAccessChain([recordString, key])), + factory.createPropertyAssignment( + factory.createIdentifier('label'), + buildAccessChain([recordString, key]), + ), + ], + true, + ), + ), + ), + ], + ); +} + +/** + example: + for model - + authorRecords + .filter(r => !primaryAuthorSet.has(getIDValue.primaryAuthor?.(r)) + .map((r) => ({ + id: getIDValue['primaryAuthor]?.(r), + label: getDisplayValue['primaryAuthor']?.(r), + })) + */ +function getModelTypeSuggestions({ modelName, fieldName }: { modelName: string; fieldName: string }): CallExpression { + const recordString = 'r'; + + const labelExpression = getDisplayValueCallChain({ fieldName, recordString }); + + const filterOptionsExpression = factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(getRecordsName(modelName)), + factory.createIdentifier('filter'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(recordString), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(`${fieldName}IdSet`), + factory.createIdentifier('has'), + ), + undefined, + [getIDValueCallChain({ fieldName, recordString })], + ), + ), + ), + ], + ); + + return factory.createCallExpression( + factory.createPropertyAccessExpression(filterOptionsExpression, factory.createIdentifier('map')), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(recordString), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createParenthesizedExpression( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('id'), + getIDValueCallChain({ fieldName, recordString }), + ), + factory.createPropertyAssignment(factory.createIdentifier('label'), labelExpression), + ], + true, + ), + ), + ), + ], + ); +} + +export function extractModelAndKeys(valueMappings?: StudioFormValueMappings): { model?: string; keys?: string[] } { + let model: undefined | string; + let keys: undefined | string[]; + const bindingProperty = valueMappings?.bindingProperties && Object.values(valueMappings.bindingProperties)[0]; + if (bindingProperty && bindingProperty.type === 'Data') { + model = bindingProperty.bindingProperties.model; + const { values } = valueMappings; + values.forEach((v) => { + const { value } = v; + if (isBoundProperty(value) && value.bindingProperties.field) { + if (!keys) { + keys = []; + } + keys.push(value.bindingProperties.field); + } + }); + } + console.log('KEYSSS', keys); + return { model, keys }; +} + +/** + example: + options={authorRecords.map(r) => ({ + id: getIDValue['primaryAuthor]?.(r), + label: getDisplayValue['primaryAuthor']?.(r), + }))} + */ +export function getAutocompleteOptionsProp({ + fieldName, + fieldConfig, +}: { + fieldName: string; + fieldConfig: FieldConfigMetadata; +}): JsxAttribute { + let options: Expression | undefined; + + const { valueMappings } = fieldConfig; + + const { model, keys } = extractModelAndKeys(valueMappings); + if (model) { + if (isModelDataType(fieldConfig)) { + options = getModelTypeSuggestions({ + modelName: model, + fieldName, + }); + } else if (keys) { + options = getSuggestionsForRelationshipScalar({ modelName: model, key: keys[0] }); + } + } + + if (!options) { + throw new InvalidInputError(`Invalid value mappings on ${fieldName}`); + } + + return factory.createJsxAttribute( + factory.createIdentifier('options'), + factory.createJsxExpression(undefined, options), + ); +} + +// impure helper +/* eslint-disable no-param-reassign */ +function replaceProperty(prop: StudioFormInputFieldProperty, toReplace: string, replaceWith: string): void { + if (isBoundProperty(prop) && prop.bindingProperties.property === toReplace) { + prop.bindingProperties.property = replaceWith; + } + if (isConcatenatedProperty(prop)) { + prop.concat.forEach((subProp) => replaceProperty(subProp as StudioFormInputFieldProperty, toReplace, replaceWith)); + } +} +/* eslint-enable no-param-reassign */ + +export function getDisplayValueObject(displayValueFunctions: PropertyAssignment[]) { + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(getDisplayValueObjectName), + undefined, + undefined, + factory.createObjectLiteralExpression(displayValueFunctions, true), + ), + ], + NodeFlags.Const, + ), + ); +} + +export function getIDValueObject(idValueFunctions: PropertyAssignment[]) { + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(getIDValueObjectName), + undefined, + undefined, + factory.createObjectLiteralExpression(idValueFunctions, true), + ), + ], + NodeFlags.Const, + ), + ); +} + +function getDefaultDisplayValue({ + fieldConfig, + modelName, +}: { + fieldConfig: FieldConfigMetadata; + modelName: string; +}): BoundStudioComponentProperty | ConcatenatedStudioComponentProperty { + const { keys } = extractModelAndKeys(fieldConfig.valueMappings); + if (!keys || !keys.length) { + throw new InternalError(`Unable to find primary key(s) for ${modelName}`); + } + if (keys.length === 1) { + return { bindingProperties: { property: modelName, field: keys[0] } }; + } + + const concatArray: StudioComponentProperty[] = []; + + keys.forEach((key, index) => { + concatArray.push({ + bindingProperties: { property: modelName, field: key }, + }); + if (index !== keys.length - 1) { + concatArray.push({ + value: ' - ', + }); + } + }); + + return { concat: concatArray }; +} + +// CompositeBowl: (r) => JSON.stringify({ shape: r?.shape, size: r?.size }) +export function buildIDValueFunction(fieldName: string, fieldConfig: FieldConfigMetadata): PropertyAssignment { + const recordString = 'r'; + + const { keys } = extractModelAndKeys(fieldConfig.valueMappings); + if (!keys || !keys.length) { + throw new InternalError(`Unable to render IDValue function for ${fieldName}`); + } + + const idObjectProperties = keys.map((key) => + factory.createPropertyAssignment(getValidProperty(key), buildAccessChain([recordString, key])), + ); + + return factory.createPropertyAssignment( + getValidProperty(fieldName), + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(recordString), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('JSON'), factory.createIdentifier('stringify')), + undefined, + [factory.createObjectLiteralExpression(idObjectProperties, false)], + ), + ), + ); +} + +// examples: +// primaryAuthor: (r) => r?.name, +// compositePrimaryAuthor: (r) => r?.name + ' - ' + r?.birthYear +export function buildDisplayValueFunction(fieldName: string, fieldConfig: FieldConfigMetadata): PropertyAssignment { + const recordString = 'r'; + + let additionalStatements: VariableStatement[] = []; + + let renderedDisplayValue: Expression | undefined; + + if (isModelDataType(fieldConfig) && fieldConfig.valueMappings) { + const valueConfig = fieldConfig.valueMappings.values[0]; + if (valueConfig) { + const modelName = fieldConfig.dataType.model; + const displayValueProperty = valueConfig.displayValue || getDefaultDisplayValue({ fieldConfig, modelName }); + replaceProperty(displayValueProperty, modelName, recordString); + if (isConcatenatedProperty(displayValueProperty)) { + renderedDisplayValue = buildConcatExpression(displayValueProperty); + } else if (isBoundProperty(displayValueProperty)) { + renderedDisplayValue = buildBindingExpression(displayValueProperty); + } + } + } + + if (isEnumFieldType(fieldConfig.dataType) && fieldConfig.valueMappings && fieldConfig.isArray) { + const displayValueMapName = `enumDisplayValueMap`; + additionalStatements = [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(displayValueMapName), + undefined, + undefined, + factory.createObjectLiteralExpression( + fieldConfig.valueMappings.values.map((v) => { + let value = ''; + let displayValue = ''; + if (isFixedPropertyWithValue(v.value)) { + value = v.value.value.toString(); + } + if (v.displayValue && isFixedPropertyWithValue(v.displayValue)) { + displayValue = v.displayValue.value.toString(); + } + if (value === '') { + throw Error('Enum cannot have an empty value'); + } + return factory.createPropertyAssignment( + factory.createStringLiteral(value), + factory.createStringLiteral(displayValue ?? value), + ); + }), + + true, + ), + ), + ], + NodeFlags.Const, + ), + ), + ]; + renderedDisplayValue = factory.createElementAccessExpression( + factory.createIdentifier(displayValueMapName), + factory.createIdentifier(recordString), + ); + } + + if (!renderedDisplayValue) { + throw new InternalError(`Unable to render display value for ${fieldName}`); + } + + return factory.createPropertyAssignment( + getValidProperty(fieldName), + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(recordString), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + additionalStatements.length + ? factory.createBlock([...additionalStatements, factory.createReturnStatement(renderedDisplayValue)], false) + : renderedDisplayValue, + ), + ); +} + +export function getModelsToImport(fieldConfig: FieldConfigMetadata): string[] { + const modelDependencies: string[] = []; + if (fieldConfig.valueMappings && fieldConfig.valueMappings.bindingProperties) { + Object.values(fieldConfig.valueMappings.bindingProperties).forEach((prop) => { + if (prop.type === 'Data' && prop.bindingProperties.model) { + modelDependencies.push(prop.bindingProperties.model); + } + }); + } + + // Import join table model + if (fieldConfig.relationship?.type === 'HAS_MANY' && fieldConfig.relationship.relatedJoinTableName) { + modelDependencies.push(fieldConfig.relationship.relatedJoinTableName); + } + + return modelDependencies; +} + +/** + compositeVetRecords.find((r) => + Object.entries(JSON.parse(id)).every(([key, value]) => r[key] === value) + ); + */ +export function getMatchEveryModelFieldCallExpression({ + recordsArrayName, + JSONName, +}: { + recordsArrayName: string; + JSONName: string; +}): CallExpression { + const recordString = 'r'; + const keyString = 'key'; + const valueString = 'value'; + return factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(recordsArrayName), + factory.createIdentifier('find'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(recordString), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Object'), + factory.createIdentifier('entries'), + ), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('JSON'), + factory.createIdentifier('parse'), + ), + undefined, + [factory.createIdentifier(JSONName)], + ), + ], + ), + factory.createIdentifier('every'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createArrayBindingPattern([ + factory.createBindingElement(undefined, undefined, factory.createIdentifier('key'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('value'), undefined), + ]), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBinaryExpression( + factory.createElementAccessExpression( + factory.createIdentifier(recordString), + factory.createIdentifier(keyString), + ), + factory.createToken(SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier(valueString), + ), + ), + ], + ), + ), + ], + ); +} + +/** + const CompositeBowlIdSet = new Set( + Array.isArray(CompositeBowl) + ? CompositeBowl.map((r) => getIDValue.CompositeBowl?.(r)) + : getIDValue.CompositeBowl?.(CompositeBowl) + ); + */ +export const buildSelectedRecordsIdSet = (fieldConfigs: Record): Statement[] => { + const statements: Statement[] = []; + Object.entries(fieldConfigs).forEach((fieldConfig) => { + const [name, fieldConfigMetaData] = fieldConfig; + const fieldName = fieldConfigMetaData.sanitizedFieldName || name; + const recordString = 'r'; + if (fieldConfigMetaData.relationship && isModelDataType(fieldConfigMetaData)) { + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(`${fieldName}IdSet`), + undefined, + undefined, + factory.createNewExpression(factory.createIdentifier('Set'), undefined, [ + factory.createConditionalExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Array'), + factory.createIdentifier('isArray'), + ), + undefined, + [factory.createIdentifier(fieldName)], + ), + factory.createToken(SyntaxKind.QuestionToken), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(fieldName), + factory.createIdentifier('map'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier(recordString), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + getIDValueCallChain({ fieldName, recordString }), + ), + ], + ), + factory.createToken(SyntaxKind.ColonToken), + getIDValueCallChain({ fieldName, recordString: fieldName }), + ), + ]), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + }); + return statements; +}; 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 085b513cf..094acd326 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 @@ -25,7 +25,7 @@ import { buildBaseCollectionVariableStatement } from '../../react-studio-templat import { ImportCollection, ImportSource } from '../../imports'; import { lowerCaseFirst } from '../../helpers'; import { isManyToManyRelationship } from './map-from-fieldConfigs'; -import { extractModelAndKey } from './display-value'; +import { extractModelAndKeys } from './model-values'; import { isModelDataType } from './render-checkers'; export const buildRelationshipQuery = ( @@ -66,11 +66,12 @@ export const buildManyToManyRelationshipDataStoreStatements = ( const dataToUnlinkMap = `${lowerCaseFirst(fieldName)}ToUnLinkMap`; const updatedMap = `${lowerCaseFirst(fieldName)}Map`; const originalMap = `${linkedDataName}Map`; - const { key } = extractModelAndKey(fieldConfigMetaData.valueMappings); - if (!key) { - throw new InternalError(`Could not identify primary key for ${relatedModelName}`); + const { keys } = extractModelAndKeys(fieldConfigMetaData.valueMappings); + if (!keys) { + throw new InternalError(`Could not identify primary key(s) for ${relatedModelName}`); } - const relatedModelPrimaryKey = key; + // TODO: update for composite + const relatedModelPrimaryKey = keys[0]; return [ factory.createVariableStatement( @@ -1164,11 +1165,12 @@ export const buildHasManyRelationshipDataStoreStatements = ( const dataToUnLink = `${lowerCaseFirst(fieldName)}ToUnLink`; const dataToLinkSet = `${lowerCaseFirst(fieldName)}Set`; const linkedDataSet = `${linkedDataName}Set`; - const { key } = extractModelAndKey(fieldConfigMetaData.valueMappings); - if (!key) { - throw new InternalError(`Could not identify primary key for ${relatedModelName}`); + const { keys } = extractModelAndKeys(fieldConfigMetaData.valueMappings); + if (!keys) { + throw new InternalError(`Could not identify primary key(s) for ${relatedModelName}`); } - const relatedModelPrimaryKey = key; + // TODO: update for composite keys + const relatedModelPrimaryKey = keys[0]; if (dataStoreActionType === 'update') { return [ factory.createVariableStatement( diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts index f46838d71..cf73684c6 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-array-field.ts @@ -27,7 +27,7 @@ import { } from './form-state'; import { buildOverrideOnChangeStatement } from './event-handler-props'; import { isModelDataType, shouldImplementDisplayValueFunction } from './render-checkers'; -import { getDisplayValueObjectName } from './display-value'; +import { getDisplayValueObjectName } from './model-values'; import { getElementAccessExpression } from './invalid-variable-helpers'; function getOnChangeAttribute({ diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-checkers.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-checkers.ts index 68f4bb398..cffaba05e 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-checkers.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper/render-checkers.ts @@ -26,3 +26,7 @@ export const isModelDataType = ( export const shouldImplementDisplayValueFunction = (config: FieldConfigMetadata): boolean => { return isModelDataType(config) || (isEnumFieldType(config.dataType) && shouldWrapInArrayField(config)); }; + +export const shouldImplementIDValueFunction = (config: FieldConfigMetadata): boolean => { + return !!(isModelDataType(config) && config.valueMappings); +}; 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 564040799..a20ae5938 100644 --- a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -80,7 +80,6 @@ import { } from './form-renderer-helper'; import { buildUseStateExpression, - buildSelectedRecordsIdSet, getCurrentDisplayValueName, getArrayChildRefName, getCurrentValueName, @@ -96,6 +95,7 @@ import { validationFunctionType, validationResponseType, } from './form-renderer-helper/type-helper'; +import { buildSelectedRecordsIdSet } from './form-renderer-helper/model-values'; type RenderComponentOnlyResponse = { compText: string; @@ -452,8 +452,6 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< statements.push(...getUseStateHooks(formMetadata.fieldConfigs)); - statements.push(...buildSelectedRecordsIdSet(formMetadata.fieldConfigs)); - statements.push(buildUseStateExpression('errors', factory.createObjectLiteralExpression())); let defaultValueVariableName: undefined | string; @@ -570,9 +568,14 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< } }); - const { validationsObject, dataTypesMap, displayValueObject, modelsToImport, usesArrayField } = mapFromFieldConfigs( - formMetadata.fieldConfigs, - ); + const { validationsObject, dataTypesMap, displayValueObject, idValueObject, modelsToImport, usesArrayField } = + mapFromFieldConfigs(formMetadata.fieldConfigs); + + if (idValueObject) { + statements.push(idValueObject); + } + + statements.push(...buildSelectedRecordsIdSet(formMetadata.fieldConfigs)); this.shouldRenderArrayField = usesArrayField; diff --git a/packages/codegen-ui/example-schemas/datastore/composite-relationships.json b/packages/codegen-ui/example-schemas/datastore/composite-relationships.json new file mode 100644 index 000000000..5c9069d2a --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/composite-relationships.json @@ -0,0 +1,554 @@ +{ + "models": { + "CompositeDog": { + "name": "CompositeDog", + "fields": { + "name": { + "name": "name", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "description": { + "name": "description", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "CompositeBowl": { + "name": "CompositeBowl", + "isArray": false, + "type": { + "model": "CompositeBowl" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "HAS_ONE", + "associatedWith": [ + "shape", + "size" + ], + "targetNames": [ + "compositeDogCompositeBowlShape", + "compositeDogCompositeBowlSize" + ] + } + }, + "CompositeOwner": { + "name": "CompositeOwner", + "isArray": false, + "type": { + "model": "CompositeOwner" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetNames": [ + "compositeDogCompositeOwnerLastName", + "compositeDogCompositeOwnerFirstName" + ] + } + }, + "CompositeToys": { + "name": "CompositeToys", + "isArray": true, + "type": { + "model": "CompositeToy" + }, + "isRequired": false, + "attributes": [], + "isArrayNullable": true, + "association": { + "connectionType": "HAS_MANY", + "associatedWith": [ + "compositeDogCompositeToysName", + "compositeDogCompositeToysDescription" + ] + } + }, + "CompositeVets": { + "name": "CompositeVets", + "isArray": true, + "type": { + "model": "CompositeDogCompositeVet" + }, + "isRequired": false, + "attributes": [], + "isArrayNullable": true, + "association": { + "connectionType": "HAS_MANY", + "associatedWith": [ + "compositeDog" + ] + } + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "compositeDogCompositeBowlShape": { + "name": "compositeDogCompositeBowlShape", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + }, + "compositeDogCompositeBowlSize": { + "name": "compositeDogCompositeBowlSize", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "compositeDogCompositeOwnerLastName": { + "name": "compositeDogCompositeOwnerLastName", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + }, + "compositeDogCompositeOwnerFirstName": { + "name": "compositeDogCompositeOwnerFirstName", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + } + }, + "syncable": true, + "pluralName": "CompositeDogs", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "key", + "properties": { + "fields": [ + "name", + "description" + ] + } + } + ] + }, + "CompositeBowl": { + "name": "CompositeBowl", + "fields": { + "shape": { + "name": "shape", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "size": { + "name": "size", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "CompositeBowls", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "key", + "properties": { + "fields": [ + "shape", + "size" + ] + } + } + ] + }, + "CompositeOwner": { + "name": "CompositeOwner", + "fields": { + "lastName": { + "name": "lastName", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "firstName": { + "name": "firstName", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "CompositeDog": { + "name": "CompositeDog", + "isArray": false, + "type": { + "model": "CompositeDog" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "HAS_ONE", + "associatedWith": [ + "CompositeOwner" + ], + "targetNames": [ + "compositeOwnerCompositeDogName", + "compositeOwnerCompositeDogDescription" + ] + } + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "compositeOwnerCompositeDogName": { + "name": "compositeOwnerCompositeDogName", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + }, + "compositeOwnerCompositeDogDescription": { + "name": "compositeOwnerCompositeDogDescription", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + } + }, + "syncable": true, + "pluralName": "CompositeOwners", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "key", + "properties": { + "fields": [ + "lastName", + "firstName" + ] + } + } + ] + }, + "CompositeToy": { + "name": "CompositeToy", + "fields": { + "kind": { + "name": "kind", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "color": { + "name": "color", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "compositeDogCompositeToysName": { + "name": "compositeDogCompositeToysName", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + }, + "compositeDogCompositeToysDescription": { + "name": "compositeDogCompositeToysDescription", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + } + }, + "syncable": true, + "pluralName": "CompositeToys", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "key", + "properties": { + "fields": [ + "kind", + "color" + ] + } + }, + { + "type": "key", + "properties": { + "name": "gsi-CompositeDog.CompositeToys", + "fields": [ + "compositeDogCompositeToysName", + "compositeDogCompositeToysDescription" + ] + } + } + ] + }, + "CompositeVet": { + "name": "CompositeVet", + "fields": { + "specialty": { + "name": "specialty", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "city": { + "name": "city", + "isArray": false, + "type": "String", + "isRequired": true, + "attributes": [] + }, + "CompositeDogs": { + "name": "CompositeDogs", + "isArray": true, + "type": { + "model": "CompositeDogCompositeVet" + }, + "isRequired": false, + "attributes": [], + "isArrayNullable": true, + "association": { + "connectionType": "HAS_MANY", + "associatedWith": [ + "compositeVet" + ] + } + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "CompositeVets", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "key", + "properties": { + "fields": [ + "specialty", + "city" + ] + } + } + ] + }, + "CompositeDogCompositeVet": { + "name": "CompositeDogCompositeVet", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "compositeDogName": { + "name": "compositeDogName", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + }, + "compositeDogdescription": { + "name": "compositeDogdescription", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "compositeVetSpecialty": { + "name": "compositeVetSpecialty", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + }, + "compositeVetcity": { + "name": "compositeVetcity", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "compositeDog": { + "name": "compositeDog", + "isArray": false, + "type": { + "model": "CompositeDog" + }, + "isRequired": true, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetNames": [ + "compositeDogName", + "compositeDogdescription" + ] + } + }, + "compositeVet": { + "name": "compositeVet", + "isArray": false, + "type": { + "model": "CompositeVet" + }, + "isRequired": true, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetNames": [ + "compositeVetSpecialty", + "compositeVetcity" + ] + } + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "CompositeDogCompositeVets", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "key", + "properties": { + "name": "byCompositeDog", + "fields": [ + "compositeDogName", + "compositeDogdescription" + ] + } + }, + { + "type": "key", + "properties": { + "name": "byCompositeVet", + "fields": [ + "compositeVetSpecialty", + "compositeVetcity" + ] + } + } + ] + } + }, + "enums": {}, + "nonModels": {}, + "codegenVersion": "3.3.2", + "version": "8f8e59ee8fb2e3ca4efda3aa25b0211f" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/composite-dog-datastore-update.json b/packages/codegen-ui/example-schemas/forms/composite-dog-datastore-update.json new file mode 100644 index 000000000..72f6304fa --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/composite-dog-datastore-update.json @@ -0,0 +1,12 @@ +{ + "name": "UpdateCompositeDogForm", + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "CompositeDog" + }, + "formActionType": "update", + "fields": {}, + "sectionalElements": {}, + "style": {}, + "cta": {} +} \ No newline at end of file diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts index b1c32cf57..f40e51a60 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts @@ -309,7 +309,6 @@ describe('getFormDefinitionInputElement', () => { props: { label: 'Label', isDisabled: true, placeholder: 'Please select an option' }, valueMappings: { values: [{ value: { value: 'value1' }, displayvalue: { value: 'displayValue1' } }], - bindingProperties: {}, }, defaultValue: 'value1', }); @@ -495,7 +494,6 @@ describe('getFormDefinitionInputElement', () => { props: { label: 'Label', name: 'MyFieldName' }, valueMappings: { values: [{ value: { value: 'value1' }, displayvalue: { value: 'displayValue1' } }], - bindingProperties: {}, }, }); }); @@ -642,21 +640,22 @@ describe('mergeValueMappings', () => { expect(mergedMappings.values.find((v) => 'value' in v.value && v.value.value === 'AUSTIN')).toBeUndefined(); }); - it('should merge base and override bindingProperties', () => { + it('should transfer bindingProperties', () => { expect( mergeValueMappings( { values: [{ value: { value: 'sdjoiflj' }, displayValue: { bindingProperties: { property: 'Dog' } } }], bindingProperties: { Dog: { type: 'Data', bindingProperties: { model: 'Dog' } }, - Person: { type: 'Data', bindingProperties: { model: 'Person' } }, }, }, - { values: [], bindingProperties: { Dog: { type: 'Data', bindingProperties: { model: 'MyDog' } } } }, + { + values: [{ value: { value: 'sdjoiflj' } }], + bindingProperties: { Dog: { type: 'Data', bindingProperties: { model: 'Dog' } } }, + }, ).bindingProperties, ).toStrictEqual({ - Person: { type: 'Data', bindingProperties: { model: 'Person' } }, - Dog: { type: 'Data', bindingProperties: { model: 'MyDog' } }, + Dog: { type: 'Data', bindingProperties: { model: 'Dog' } }, }); }); }); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts index 3f66e8f97..f0ec032c2 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts @@ -260,7 +260,7 @@ describe('mapModelFieldsConfigs', () => { value: 'ownerId', isArray: false, valueMappings: { - values: [{ value: { bindingProperties: { property: 'Owner', field: 'id' } } }], + values: [{ value: { bindingProperties: { property: 'Owner', field: 'ownerId' } } }], bindingProperties: { Owner: { type: 'Data', bindingProperties: { model: 'Owner' } } }, }, }, diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts index 5e7a49ea3..1b9a565fb 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts @@ -33,6 +33,22 @@ export function mergeValueMappings( base?: StudioFormValueMappings, override?: StudioFormValueMappings, ): StudioFormValueMappings { + // if model-based + if (base?.bindingProperties || override?.bindingProperties) { + const valueMappings = override || base; + const firstValue = valueMappings?.values[0]; + if (!firstValue) { + throw new InternalError(`No valueMapping found for model-bound field`); + } + firstValue.displayValue = override?.values?.[0]?.displayValue; + + return { + values: valueMappings.values, + bindingProperties: valueMappings.bindingProperties, + }; + } + + // if not model-based let values: StudioFormValueMappings['values'] = []; if (!base && override) { @@ -55,7 +71,6 @@ export function mergeValueMappings( return { values, - bindingProperties: { ...base?.bindingProperties, ...override?.bindingProperties }, }; } diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts index f533494dd..733b12df9 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts @@ -46,10 +46,12 @@ export function getFieldTypeMapKey(field: GenericDataField): FieldTypeMapKeys { } function getValueMappings({ + fieldName, field, enums, allModels, }: { + fieldName: string; field: GenericDataField; enums: GenericDataSchema['enums']; allModels: { [modelName: string]: GenericDataModel }; @@ -72,8 +74,14 @@ function getValueMappings({ // if relationship if (field.relationship) { const modelName = field.relationship.relatedModelName; + // if model, store all keys; else, store field as key + const keys = + typeof field.dataType === 'object' && 'model' in field.dataType ? allModels[modelName].primaryKeys : [fieldName]; + const values: StudioFormValueMappings['values'] = keys.map((key) => ({ + value: { bindingProperties: { property: modelName, field: key } }, + })); return { - values: [{ value: { bindingProperties: { property: modelName, field: allModels[modelName].primaryKeys[0] } } }], + values, bindingProperties: { [modelName]: { type: 'Data', bindingProperties: { model: modelName } } }, }; } @@ -126,7 +134,7 @@ export function getFieldConfigFromModelField({ config.relationship = field.relationship; } - const valueMappings = getValueMappings({ field, enums: dataSchema.enums, allModels: dataSchema.models }); + const valueMappings = getValueMappings({ fieldName, field, enums: dataSchema.enums, allModels: dataSchema.models }); if (valueMappings) { config.inputType.valueMappings = valueMappings; }