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 273be168..240a4d68 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 @@ -40445,6 +40445,515 @@ export default function UpdateOwnerForm(props: UpdateOwnerFormProps): React.Reac " `; +exports[`amplify form renderer tests datastore form tests should 1:1 relationships without types file path - Create amplify js v6 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 { Owner, Dog as Dog0 } from \\"../models\\"; +import { + fetchByPath, + getOverrideProps, + useDataStoreBinding, + validateField, +} from \\"./utils\\"; +import { DataStore } from \\"aws-amplify/datastore\\"; +function ArrayField({ + items = [], + onChange, + label, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, + defaultFieldValue, + lengthLimit, + getBadgeText, + runValidationTasks, + errorMessage, +}) { + const labelElement = {label}; + const { + tokens: { + components: { + fieldmessages: { error: errorStyles }, + }, + }, + } = useTheme(); + const [selectedBadgeIndex, setSelectedBadgeIndex] = React.useState(); + const [isEditing, setIsEditing] = React.useState(); + React.useEffect(() => { + if (isEditing) { + inputFieldRef?.current?.focus(); + } + }, [isEditing]); + const removeItem = async (removeIndex) => { + const newItems = items.filter((value, index) => index !== removeIndex); + await onChange(newItems); + setSelectedBadgeIndex(undefined); + }; + const addItem = async () => { + const { hasError } = runValidationTasks(); + if ( + currentFieldValue !== undefined && + currentFieldValue !== null && + currentFieldValue !== \\"\\" && + !hasError + ) { + const newItems = [...items]; + if (selectedBadgeIndex !== undefined) { + newItems[selectedBadgeIndex] = currentFieldValue; + setSelectedBadgeIndex(undefined); + } else { + newItems.push(currentFieldValue); + } + await onChange(newItems); + setIsEditing(false); + } + }; + const arraySection = ( + + {!!items?.length && ( + + {items.map((value, index) => { + return ( + { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + setIsEditing(true); + }} + > + {getBadgeText ? getBadgeText(value) : value.toString()} + { + event.stopPropagation(); + removeItem(index); + }} + /> + + ); + })} + + )} + + + ); + if (lengthLimit !== undefined && items.length >= lengthLimit && !isEditing) { + return ( + + {labelElement} + {arraySection} + + ); + } + return ( + + {labelElement} + {isEditing && children} + {!isEditing ? ( + <> + + {errorMessage && hasError && ( + + {errorMessage} + + )} + + ) : ( + + {(currentFieldValue || isEditing) && ( + + )} + + + )} + {arraySection} + + ); +} +export default function CreateOwnerForm(props) { + const { + clearOnSuccess = true, + onSuccess, + onError, + onSubmit, + onValidate, + onChange, + overrides, + ...rest + } = props; + const initialValues = { + name: \\"\\", + Dog: undefined, + }; + const [name, setName] = React.useState(initialValues.name); + const [Dog, setDog] = React.useState(initialValues.Dog); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(initialValues.name); + setDog(initialValues.Dog); + setCurrentDogValue(undefined); + setCurrentDogDisplayValue(\\"\\"); + setErrors({}); + }; + const [currentDogDisplayValue, setCurrentDogDisplayValue] = + React.useState(\\"\\"); + const [currentDogValue, setCurrentDogValue] = React.useState(undefined); + const DogRef = React.createRef(); + const getIDValue = { + Dog: (r) => JSON.stringify({ id: r?.id }), + }; + const DogIdSet = new Set( + Array.isArray(Dog) + ? Dog.map((r) => getIDValue.Dog?.(r)) + : getIDValue.Dog?.(Dog) + ); + const dogRecords = useDataStoreBinding({ + type: \\"collection\\", + model: Dog0, + }).items; + const getDisplayValue = { + Dog: (r) => \`\${r?.name ? r?.name + \\" - \\" : \\"\\"}\${r?.id}\`, + }; + const validations = { + name: [{ type: \\"Required\\" }], + Dog: [], + }; + const runValidationTasks = async ( + fieldName, + currentValue, + getDisplayValue + ) => { + const value = + currentValue && getDisplayValue + ? getDisplayValue(currentValue) + : currentValue; + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + name, + Dog, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks( + fieldName, + item, + getDisplayValue[fieldName] + ) + ) + ); + return promises; + } + promises.push( + runValidationTasks( + fieldName, + modelFields[fieldName], + getDisplayValue[fieldName] + ) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + if (onSubmit) { + modelFields = onSubmit(modelFields); + } + try { + Object.entries(modelFields).forEach(([key, value]) => { + if (typeof value === \\"string\\" && value === \\"\\") { + modelFields[key] = null; + } + }); + const owner = await DataStore.save(new Owner(modelFields)); + const promises = []; + const dogToLink = modelFields.Dog; + if (dogToLink) { + promises.push( + DataStore.save( + Dog0.copyOf(dogToLink, (updated) => { + updated.owner = owner; + }) + ) + ); + const ownerToUnlink = await dogToLink.owner; + if (ownerToUnlink) { + promises.push( + DataStore.save( + Owner.copyOf(ownerToUnlink, (updated) => { + updated.Dog = undefined; + updated.ownerDogId = undefined; + }) + ) + ); + } + } + await Promise.all(promises); + if (onSuccess) { + onSuccess(modelFields); + } + if (clearOnSuccess) { + resetStateValues(); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...getOverrideProps(overrides, \\"CreateOwnerForm\\")} + {...rest} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + Dog, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + > + { + let value = items[0]; + if (onChange) { + const modelFields = { + name, + Dog: value, + }; + const result = onChange(modelFields); + value = result?.Dog ?? value; + } + setDog(value); + setCurrentDogValue(undefined); + setCurrentDogDisplayValue(\\"\\"); + }} + currentFieldValue={currentDogValue} + label={\\"Dog\\"} + items={Dog ? [Dog] : []} + hasError={errors?.Dog?.hasError} + runValidationTasks={async () => + await runValidationTasks(\\"Dog\\", currentDogValue) + } + errorMessage={errors?.Dog?.errorMessage} + getBadgeText={getDisplayValue.Dog} + setFieldValue={(model) => { + setCurrentDogDisplayValue(model ? getDisplayValue.Dog(model) : \\"\\"); + setCurrentDogValue(model); + }} + inputFieldRef={DogRef} + defaultFieldValue={\\"\\"} + > + !DogIdSet.has(getIDValue.Dog?.(r))) + .map((r) => ({ + id: getIDValue.Dog?.(r), + label: getDisplayValue.Dog?.(r), + }))} + onSelect={({ id, label }) => { + setCurrentDogValue( + dogRecords.find((r) => + Object.entries(JSON.parse(id)).every( + ([key, value]) => r[key] === value + ) + ) + ); + setCurrentDogDisplayValue(label); + runValidationTasks(\\"Dog\\", label); + }} + onClear={() => { + setCurrentDogDisplayValue(\\"\\"); + }} + onChange={(e) => { + let { value } = e.target; + if (errors.Dog?.hasError) { + runValidationTasks(\\"Dog\\", value); + } + setCurrentDogDisplayValue(value); + setCurrentDogValue(undefined); + }} + onBlur={() => runValidationTasks(\\"Dog\\", currentDogDisplayValue)} + errorMessage={errors.Dog?.errorMessage} + hasError={errors.Dog?.hasError} + ref={DogRef} + labelHidden={true} + {...getOverrideProps(overrides, \\"Dog\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should 1:1 relationships without types file path - Create amplify js v6 2`] = ` +"import * as React from \\"react\\"; +import { AutocompleteProps, GridProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +import { Dog as Dog0 } from \\"../models\\"; +export declare type EscapeHatchProps = { + [elementHierarchy: string]: Record; +} | null; +export declare type VariantValues = { + [key: string]: string; +}; +export declare type Variant = { + variantValues: VariantValues; + overrides: EscapeHatchProps; +}; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type CreateOwnerFormInputValues = { + name?: string; + Dog?: Dog0; +}; +export declare type CreateOwnerFormValidationValues = { + name?: ValidationFunction; + Dog?: ValidationFunction; +}; +export declare type PrimitiveOverrideProps = Partial & React.DOMAttributes; +export declare type CreateOwnerFormOverridesProps = { + CreateOwnerFormGrid?: PrimitiveOverrideProps; + name?: PrimitiveOverrideProps; + Dog?: PrimitiveOverrideProps; +} & EscapeHatchProps; +export declare type CreateOwnerFormProps = React.PropsWithChildren<{ + overrides?: CreateOwnerFormOverridesProps | undefined | null; +} & { + clearOnSuccess?: boolean; + onSubmit?: (fields: CreateOwnerFormInputValues) => CreateOwnerFormInputValues; + onSuccess?: (fields: CreateOwnerFormInputValues) => void; + onError?: (fields: CreateOwnerFormInputValues, errorMessage: string) => void; + onChange?: (fields: CreateOwnerFormInputValues) => CreateOwnerFormInputValues; + onValidate?: CreateOwnerFormValidationValues; +} & React.CSSProperties>; +export default function CreateOwnerForm(props: CreateOwnerFormProps): React.ReactElement; +" +`; + exports[`amplify form renderer tests datastore form tests should generate a create form 1`] = ` "/* eslint-disable */ import * as React from \\"react\\"; diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts index 03200eee..11401015 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 @@ -383,6 +383,19 @@ describe('amplify form renderer tests', () => { expect(importCollection).toBeDefined(); }); + it('should 1:1 relationships without types file path - Create amplify js v6', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/owner-dog-create', + 'datastore/dog-owner-required', + { ...defaultCLIRenderConfig, dependencies: { 'aws-amplify': '^6.0.0' } }, + { isNonModelSupported: true, isRelationshipSupported: true }, + ); + + expect(componentText).toContain('import { DataStore } from "aws-amplify/datastore";'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + describe('custom form tests', () => { it('should render a custom backed create form', () => { const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/post-custom-create', undefined); diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts index d45a4d60..7686f862 100644 --- a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts @@ -34,7 +34,7 @@ import { } from 'typescript'; import { ReactComponentRenderer } from '../react-component-renderer'; import { buildFormLayoutProperties, buildOpeningElementProperties } from '../react-component-render-helper'; -import { ImportCollection, ImportSource } from '../imports'; +import { ImportCollection, ImportSource, ImportValue } from '../imports'; import { buildExpression } from '../forms'; import { onSubmitValidationRun, buildModelFieldObject } from '../forms/form-renderer-helper'; import { hasTokenReference } from '../utils/forms/layout-helpers'; @@ -43,7 +43,8 @@ import { isModelDataType } from '../forms/form-renderer-helper/render-checkers'; import { replaceEmptyStringStatement } from '../forms/form-renderer-helper/cta-props'; import { ReactRenderConfig } from '../react-render-config'; import { defaultRenderConfig } from '../react-studio-template-renderer-helper'; -import { getAmplifyJSAPIImport } from '../helpers/amplify-js-versioning'; +import { getAmplifyJSAPIImport, getAmplifyJSVersionToRender } from '../helpers/amplify-js-versioning'; +import { AMPLIFY_JS_V6 } from '../utils/constants'; export default class FormRenderer extends ReactComponentRenderer { constructor( @@ -75,7 +76,11 @@ export default class FormRenderer extends ReactComponentRenderer