diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap index b31377788..e20575171 100644 --- a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap @@ -9,8 +9,8 @@ import { getOverrideProps, useDataStoreCreateAction, } from \\"@aws-amplify/ui-react/internal\\"; -import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; import { Customer } from \\"../models\\"; +import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type CreateCustomerButtonProps = React.PropsWithChildren< Partial & { @@ -52,8 +52,8 @@ import { getOverrideProps, useDataStoreDeleteAction, } from \\"@aws-amplify/ui-react/internal\\"; -import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; import { Customer } from \\"../models\\"; +import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type DeleteCustomerButtonProps = React.PropsWithChildren< Partial & { @@ -95,8 +95,8 @@ import { getOverrideProps, useDataStoreUpdateAction, } from \\"@aws-amplify/ui-react/internal\\"; -import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; import { Customer } from \\"../models\\"; +import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type UpdateCustomerButtonProps = React.PropsWithChildren< Partial & { @@ -339,7 +339,6 @@ exports[`amplify render tests actions with conditional in parameters 1`] = ` Object { "componentText": "/* eslint-disable */ import React from \\"react\\"; -import { User } from \\"../models\\"; import { EscapeHatchProps, createDataStorePredicate, @@ -347,6 +346,7 @@ import { useDataStoreBinding, useStateMutationAction, } from \\"@aws-amplify/ui-react/internal\\"; +import { User } from \\"../models\\"; import { Button, Flex, FlexProps, Text } from \\"@aws-amplify/ui-react\\"; export type ConditionalInMutationProps = React.PropsWithChildren< @@ -502,8 +502,8 @@ import { useAuth, useDataStoreCreateAction, } from \\"@aws-amplify/ui-react/internal\\"; -import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; import { Customer } from \\"../models\\"; +import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type ComponentWithAuthEventBindingProps = React.PropsWithChildren< Partial & { @@ -540,6 +540,12 @@ export default function ComponentWithAuthEventBinding( exports[`amplify render tests collection should render collection with data binding 1`] = ` "/* eslint-disable */ import React from \\"react\\"; +import { + EscapeHatchProps, + createDataStorePredicate, + getOverrideProps, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; import { User, UserPreferences } from \\"../models\\"; import { Button, @@ -547,12 +553,6 @@ import { CollectionProps, Flex, } from \\"@aws-amplify/ui-react\\"; -import { - EscapeHatchProps, - createDataStorePredicate, - getOverrideProps, - useDataStoreBinding, -} from \\"@aws-amplify/ui-react/internal\\"; export type CollectionOfCustomButtonsProps = React.PropsWithChildren< Partial> & { @@ -641,13 +641,6 @@ exports[`amplify render tests collection should render collection with data bind Object { "componentText": "/* eslint-disable */ import React from \\"react\\"; -import { User, UserPreferences } from \\"../models\\"; -import { - Button, - Collection, - CollectionProps, - Flex, -} from \\"@aws-amplify/ui-react\\"; import { EscapeHatchProps, createDataStorePredicate, @@ -655,6 +648,13 @@ import { useDataStoreBinding, } from \\"@aws-amplify/ui-react/internal\\"; import { SortDirection, SortPredicate } from \\"@aws-amplify/datastore\\"; +import { User, UserPreferences } from \\"../models\\"; +import { + Button, + Collection, + CollectionProps, + Flex, +} from \\"@aws-amplify/ui-react\\"; export type CollectionOfCustomButtonsProps = React.PropsWithChildren< Partial> & { @@ -750,6 +750,12 @@ export default function CollectionOfCustomButtons( exports[`amplify render tests collection should render collection with data binding if binding name is items 1`] = ` "/* eslint-disable */ import React from \\"react\\"; +import { + EscapeHatchProps, + createDataStorePredicate, + getOverrideProps, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; import { User, UserPreferences } from \\"../models\\"; import { Button, @@ -757,12 +763,6 @@ import { CollectionProps, Flex, } from \\"@aws-amplify/ui-react\\"; -import { - EscapeHatchProps, - createDataStorePredicate, - getOverrideProps, - useDataStoreBinding, -} from \\"@aws-amplify/ui-react/internal\\"; export type CollectionOfCustomButtonsProps = React.PropsWithChildren< Partial> & { @@ -850,14 +850,14 @@ export default function CollectionOfCustomButtons( exports[`amplify render tests collection should render collection with data binding with no predicate 1`] = ` "/* eslint-disable */ import React from \\"react\\"; -import ListingCard from \\"./ListingCard\\"; +import { UntitledModel } from \\"../models\\"; import { EscapeHatchProps, getOverrideProps, useDataStoreBinding, } from \\"@aws-amplify/ui-react/internal\\"; +import ListingCard from \\"./ListingCard\\"; import { Collection, CollectionProps } from \\"@aws-amplify/ui-react\\"; -import { UntitledModel } from \\"../models\\"; export type ListingCardCollectionProps = React.PropsWithChildren< Partial> & { @@ -4845,13 +4845,13 @@ exports[`amplify render tests default value should render collection default val Object { "componentText": "/* eslint-disable */ import React from \\"react\\"; -import { Collection, CollectionProps, Text } from \\"@aws-amplify/ui-react\\"; +import { User } from \\"../models\\"; import { EscapeHatchProps, getOverrideProps, useDataStoreBinding, } from \\"@aws-amplify/ui-react/internal\\"; -import { User } from \\"../models\\"; +import { Collection, CollectionProps, Text } from \\"@aws-amplify/ui-react\\"; export type CollectionDefaultValueProps = React.PropsWithChildren< Partial> & { @@ -4881,8 +4881,8 @@ export default function CollectionDefaultValue( > {(item, index) => ( )} @@ -5330,8 +5330,8 @@ import { useDataStoreUpdateAction, useStateMutationAction, } from \\"@aws-amplify/ui-react/internal\\"; -import { Button, Flex, FlexProps, TextField } from \\"@aws-amplify/ui-react\\"; import { Customer } from \\"../models\\"; +import { Button, Flex, FlexProps, TextField } from \\"@aws-amplify/ui-react\\"; export type MyFormProps = React.PropsWithChildren< Partial & { @@ -5540,7 +5540,7 @@ export default function StepperControlledElement( props: StepperControlledElementProps ): React.ReactElement { const { overrides, ...rest } = props; - const [inputValue, setInputValue] = useStateMutationAction(undefined); + const [inputValue, setInputValue] = useStateMutationAction(0); return ( /* @ts-ignore: TS2322 */ { setRadioGroupFieldInputValue(event.target.value); @@ -6265,7 +6263,6 @@ export default function TwoWayBindings( > extends ComponentWithChildrenRendererBase< TPropIn, @@ -47,7 +46,6 @@ export class ReactComponentWithChildrenRenderer extends ComponentWithCh protected parent?: StudioNode, ) { super(component, parent); - this.mapSyntheticProps(); addBindingPropertiesImports(component, importCollection); } @@ -194,26 +192,4 @@ export class ReactComponentWithChildrenRenderer extends ComponentWithCh attributes.push(attr); } - - /* Some additional props are added to Amplify primitives in Studio. These "sythetic" props are mapped to real props - * on the primitives. - * - * Example: Text prop label is mapped to to Text prop Children - * - * This is done so that nonadvanced users of Studio do not need to interact with props that might confuse them. - */ - private mapSyntheticProps() { - // properties.children will take precedent over mapped children prop - if (this.component.properties.children === undefined) { - const childrenPropMapping = PrimitiveChildrenPropMapping[Primitive[this.component.componentType as Primitive]]; - - if (childrenPropMapping !== undefined) { - const mappedChildrenProp = this.component.properties[childrenPropMapping]; - if (mappedChildrenProp !== undefined) { - this.component.properties.children = mappedChildrenProp; - delete this.component.properties[childrenPropMapping]; - } - } - } - } } diff --git a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts index 7e4e8110b..66d02bfbe 100644 --- a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts @@ -23,6 +23,7 @@ import { isStudioComponentWithCollectionProperties, isStudioComponentWithVariants, StudioComponent, + StudioComponentChild, StudioComponentPredicate, StudioComponentSort, StudioComponentVariant, @@ -111,6 +112,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer this.componentMetadata.stateReferences = mapSyntheticStateReferences(this.componentMetadata); this.mapSyntheticPropsForVariants(); + this.mapSyntheticProps(); // TODO: throw warnings on invalid config combinations. i.e. CommonJS + JSX } @@ -136,6 +138,9 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer @handleCodegenErrors renderComponentOnly() { + // buildVariableStatements must be called before renderJsx + // so that some properties can be removed from opening element + const variableStatements = this.buildVariableStatements(this.component); const jsx = this.renderJsx(this.component); const { printer, file } = buildPrinter(this.fileName, this.renderConfig); @@ -151,6 +156,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer const wrappedFunction = this.renderFunctionWrapper( this.component.name ?? StudioRendererConstants.unknownName, + variableStatements, jsx, false, ); @@ -168,10 +174,14 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + // buildVariableStatements must be called before renderJsx + // so that some properties can be removed from opening element + const variableStatements = this.buildVariableStatements(this.component); const jsx = this.renderJsx(this.component); const wrappedFunction = this.renderFunctionWrapper( this.component.name ?? StudioRendererConstants.unknownName, + variableStatements, jsx, true, ); @@ -212,23 +222,25 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer renderFunctionWrapper( componentName: string, + variableStatements: Statement[], jsx: JsxElement | JsxFragment | JsxSelfClosingElement, renderExport: boolean, ): FunctionDeclaration { const componentPropType = getComponentPropName(componentName); - const codeBlockContent = this.buildVariableStatements(this.component); - const jsxStatement = factory.createParenthesizedExpression( - this.renderConfig.script !== ScriptKind.TSX - ? jsx - : /* add ts-ignore comment above jsx statement. Generated props are incompatible with amplify-ui props */ - addSyntheticLeadingComment( - factory.createParenthesizedExpression(jsx), - SyntaxKind.MultiLineCommentTrivia, - ' @ts-ignore: TS2322 ', - true, - ), + const jsxStatement = factory.createReturnStatement( + factory.createParenthesizedExpression( + this.renderConfig.script !== ScriptKind.TSX + ? jsx + : /* add ts-ignore comment above jsx statement. Generated props are incompatible with amplify-ui props */ + addSyntheticLeadingComment( + factory.createParenthesizedExpression(jsx), + SyntaxKind.MultiLineCommentTrivia, + ' @ts-ignore: TS2322 ', + true, + ), + ), ); - codeBlockContent.push(factory.createReturnStatement(jsxStatement)); + const codeBlockContent = variableStatements.concat([jsxStatement]); const modifiers: Modifier[] = renderExport ? [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DefaultKeyword)] : []; @@ -1136,5 +1148,35 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer }); } + /* Some additional props are added to Amplify primitives in Studio. These "sythetic" props are mapped to real props + * on the primitives. + * + * Example: Text prop label is mapped to to Text prop Children + * + * This is done so that nonadvanced users of Studio do not need to interact with props that might confuse them. + */ + private mapSyntheticProps() { + function mapSyntheticPropsForComponent(component: StudioComponent | StudioComponentChild) { + // properties.children will take precedent over mapped children prop + if (component.properties.children === undefined) { + const childrenPropMapping = PrimitiveChildrenPropMapping[Primitive[component.componentType as Primitive]]; + + if (childrenPropMapping !== undefined) { + const mappedChildrenProp = component.properties[childrenPropMapping]; + if (mappedChildrenProp !== undefined) { + component.properties.children = mappedChildrenProp; // eslint-disable-line no-param-reassign + delete component.properties[childrenPropMapping]; // eslint-disable-line no-param-reassign + } + } + } + + if (component.children !== undefined) { + component.children.forEach(mapSyntheticPropsForComponent); + } + } + + mapSyntheticPropsForComponent(this.component); + } + abstract renderJsx(component: StudioComponent): JsxElement | JsxFragment | JsxSelfClosingElement; } diff --git a/packages/codegen-ui-react/lib/workflow/mutation.ts b/packages/codegen-ui-react/lib/workflow/mutation.ts index 23c34d551..5808ee980 100644 --- a/packages/codegen-ui-react/lib/workflow/mutation.ts +++ b/packages/codegen-ui-react/lib/workflow/mutation.ts @@ -50,6 +50,18 @@ const genericEventToReactEventImplementationOverrides: PrimitiveLevelPropConfigu [Primitive.SwitchField]: { [StudioGenericEvent.change]: toggleBooleanStateCallback }, }; +const PrimitiveDefaultValuePropMapping: PrimitiveLevelPropConfiguration = new Proxy( + { + [Primitive.CheckboxField]: { checked: 'defaultChecked' }, + [Primitive.SwitchField]: { isChecked: 'defaultChecked' }, + }, + { + get(target, name, ...args) { + return name in target ? Reflect.get(target, name, ...args) : { value: 'defaultValue' }; + }, + }, +); + export function mapSyntheticStateReferences(componentMetadata: ComponentMetadata) { return componentMetadata.stateReferences.map((stateReference) => { const { componentName, property } = stateReference; @@ -229,7 +241,7 @@ export function buildStateStatements( undefined, undefined, factory.createCallExpression(factory.createIdentifier('useStateMutationAction'), undefined, [ - getStateDefaultValue(component, componentMetadata, stateReference), + getStateInitialValue(component, componentMetadata, stateReference), ]), ), ], @@ -239,7 +251,7 @@ export function buildStateStatements( }); } -export function getStateDefaultValue( +export function getStateInitialValue( component: StudioComponent, componentMetadata: ComponentMetadata, stateReference: StateStudioComponentProperty, @@ -248,33 +260,34 @@ export function getStateDefaultValue( const referencedComponent = getComponentFromComponentTree(component, componentName); const componentProperty = referencedComponent.properties[property]; - // does not work for custom components wrapping form components - if ( - componentProperty === undefined && - PrimitiveDefaultPropertyValue[referencedComponent.componentType as Primitive] - ) { + if (componentProperty === undefined) { + const defaultPropMapping = PrimitiveDefaultValuePropMapping[referencedComponent.componentType as Primitive]; + if (property in defaultPropMapping && referencedComponent.properties[defaultPropMapping[property]]) { + const defaultProp = referencedComponent.properties[defaultPropMapping[property]]; + // eslint-disable-next-line no-param-reassign + delete referencedComponent.properties[defaultPropMapping[property]]; + return propertyToExpression(componentMetadata, defaultProp); + } + return propertyToExpression(componentMetadata, getDefaultForComponentAndProperty(referencedComponent, property)); } + return propertyToExpression(componentMetadata, componentProperty); } export function getDefaultForComponentAndProperty( component: StudioComponent | StudioComponentChild, property: string, -): StudioComponentProperty { +): StudioComponentProperty | undefined { const { componentType } = component; const componentDefault = PrimitiveDefaultPropertyValue[componentType as Primitive]; if (componentDefault && property in componentDefault) { // if component has defaultValue use defaultValue as initial state value - if (property === 'value' && component.properties.defaultValue) { - // TODO: remove defaultValue becuase coponents canot have value and defaultValue - return component.properties.defaultValue; - } return componentDefault[property]; } - // use empty string a fallback default - return { value: '', type: 'string' }; + // use empty string as fallback default + return undefined; } export type MutationReferences = {