Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate Storagefield for form builder #976

Merged
merged 7 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ exports[`Primitives SliderField 1`] = `"<SliderField {...getOverrideProps(overri

exports[`Primitives StepperField 1`] = `"<StepperField {...getOverrideProps(overrides, \\"MyStepperField\\")} {...rest}></StepperField>"`;

exports[`Primitives StorageField 1`] = `"<StorageField {...getOverrideProps(overrides, \\"MyStorageField\\")} {...rest}></StorageField>"`;

exports[`Primitives SwitchField 1`] = `"<SwitchField {...getOverrideProps(overrides, \\"MySwitchField\\")} {...rest}></SwitchField>"`;

exports[`Primitives TabItem 1`] = `"<TabItem {...getOverrideProps(overrides, \\"MyTabItem\\")} {...rest}></TabItem>"`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,35 @@ describe('amplify form renderer tests', () => {
});
});

describe('forms with StorageField tests', () => {
it('should render a create form with StorageField', () => {
const { componentText } = generateWithAmplifyFormRenderer(
'forms/product-datastore-create',
'datastore/product',
undefined,
);
expect(componentText).toMatchSnapshot();
});

it.only('should render a update form with StorageField on non-array field', () => {
const { componentText } = generateWithAmplifyFormRenderer(
'forms/product-datastore-update-non-array',
'datastore/product-non-array',
undefined,
);
expect(componentText).toMatchSnapshot();
});

it('should render a update form with StorageField', () => {
const { componentText } = generateWithAmplifyFormRenderer(
'forms/product-datastore-update',
'datastore/product',
undefined,
);
expect(componentText).toMatchSnapshot();
});
});

it('should render form for child of bidirectional 1:m when field defined on parent', () => {
const { componentText, declaration } = generateWithAmplifyFormRenderer(
'forms/car-datastore-update',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
VisuallyHiddenProps,
TextProps,
} from '@aws-amplify/ui-react';
import { StorageManagerProps } from '@aws-amplify/ui-react-storage';
import { HTMLProps } from 'react';
import { Primitive } from '../primitive';
import CustomComponentRenderer from './customComponent';
Expand Down Expand Up @@ -366,6 +367,14 @@ export class AmplifyFormRenderer extends ReactFormTemplateRenderer {
parent,
).renderElement(renderChildren);

case Primitive.StorageField:
return new ReactComponentRenderer<StorageManagerProps>(
formComponent,
this.componentMetadata,
this.importCollection,
parent,
).renderElement(renderChildren);

case Primitive.TabItem:
return new ReactComponentRenderer<TabItemProps>(
formComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,23 +267,32 @@ export const buildOverrideOnChangeStatement = (
function getOnValueChangeProp(fieldType: string): string {
const map: { [key: string]: string } = {
StepperField: 'onStepChange',
StorageField: 'onFileSuccess',
};

return map[fieldType] ?? 'onChange';
}

function getCallbackVarName(fieldType: string): string {
const map: { [key: string]: string } = {
StorageField: 'files',
};

return map[fieldType] ?? 'e';
}

export const buildOnChangeStatement = (
component: StudioComponent | StudioComponentChild,
fieldConfigs: Record<string, FieldConfigMetadata>,
) => {
const { name: fieldName, componentType: fieldType } = component;
const fieldConfig = fieldConfigs[fieldName];
const { dataType, sanitizedFieldName, studioFormComponentType } = fieldConfig;
const { dataType, sanitizedFieldName, studioFormComponentType, isArray } = fieldConfig;
const renderedFieldName = sanitizedFieldName || fieldName;

// build statements that handle new value
const handleChangeStatements: Statement[] = [
...buildTargetVariable(studioFormComponentType || fieldType, renderedFieldName, dataType),
...buildTargetVariable(studioFormComponentType || fieldType, renderedFieldName, dataType, isArray),
];

if (!shouldWrapInArrayField(fieldConfig)) {
Expand Down Expand Up @@ -321,7 +330,7 @@ export const buildOnChangeStatement = (
undefined,
undefined,
undefined,
factory.createIdentifier('e'),
factory.createIdentifier(getCallbackVarName(fieldType)),
undefined,
undefined,
undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,48 @@ const expressionMap = {
),
};

// array: files.map(({ s3Key }) => s3Key)
// non-array: files?.[0]?.s3Key;
export function extractKeyByMapping(array: string, key: string, isArray?: boolean) {
if (isArray) {
return factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier(array), factory.createIdentifier('map')),
undefined,
[
factory.createArrowFunction(
undefined,
undefined,
[
factory.createParameterDeclaration(
undefined,
undefined,
undefined,
factory.createObjectBindingPattern([
factory.createBindingElement(undefined, undefined, factory.createIdentifier(key), undefined),
]),
undefined,
undefined,
undefined,
),
],
undefined,
factory.createToken(SyntaxKind.EqualsGreaterThanToken),
factory.createIdentifier(key),
),
],
);
}
return factory.createPropertyAccessChain(
factory.createElementAccessChain(
factory.createIdentifier(array),
factory.createToken(SyntaxKind.QuestionDotToken),
factory.createNumericLiteral('0'),
),
factory.createToken(SyntaxKind.QuestionDotToken),
factory.createIdentifier('s3Key'),
);
}

// default variable statement
const setVariableStatement = (lhs: Identifier, assignment: Expression) =>
factory.createVariableStatement(
Expand All @@ -54,7 +96,12 @@ const setVariableStatement = (lhs: Identifier, assignment: Expression) =>
),
);

export const buildTargetVariable = (fieldType: string, fieldName: string, dataType?: DataFieldDataType) => {
export const buildTargetVariable = (
fieldType: string,
fieldName: string,
dataType?: DataFieldDataType,
isArray?: boolean,
) => {
const fieldTypeToExpressionMap: {
[fieldType: string]: { expression: Expression; identifier: Identifier };
} = {
Expand All @@ -78,6 +125,10 @@ export const buildTargetVariable = (fieldType: string, fieldName: string, dataTy
expression: factory.createPrefixUnaryExpression(SyntaxKind.ExclamationToken, factory.createIdentifier(fieldName)),
identifier: expressionMap.value,
},
StorageField: {
expression: extractKeyByMapping('files', 's3Key', isArray),
identifier: expressionMap.value,
},
};

let expression: Expression = fieldTypeToExpressionMap[fieldType]?.expression ?? expressionMap.eTarget;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
getSetNameIdentifier,
buildUseStateExpression,
getControlledComponentDefaultValue,
buildUseRefExpression,
} from '../../helpers';
import { getElementAccessExpression } from './invalid-variable-helpers';
import { shouldWrapInArrayField } from './render-checkers';
Expand Down Expand Up @@ -260,6 +261,21 @@ export const getUseStateHooks = (fieldConfigs: Record<string, FieldConfigMetadat
}, []);
};

/**
* iterates field configs to create useRef hooks for fields that need refs.
* @param fieldConfigs
* @returns
*/
export const getUseRefHooks = (fieldConfigs: Record<string, FieldConfigMetadata>): Statement[] => {
return Object.entries(fieldConfigs)
.filter(([, { componentType }]) => {
return componentType === 'StorageField';
})
.map(([name]) => {
return buildUseRefExpression(`${name}Ref`, factory.createArrayLiteralExpression([], false));
});
};

/**
* function used by the Clear/ Reset button
* it's a reset type but we also need to clear the state of the input fields as well
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/
import { FieldConfigMetadata } from '@aws-amplify/codegen-ui';

export const shouldWrapInArrayField = (config: FieldConfigMetadata): boolean => config.isArray || !!config.relationship;
export const shouldWrapInArrayField = (config: FieldConfigMetadata): boolean =>
(config.isArray || !!config.relationship) && config.componentType !== 'StorageField';

export const isModelDataType = (
config: FieldConfigMetadata,
Expand Down
3 changes: 3 additions & 0 deletions packages/codegen-ui-react/lib/forms/react-form-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import {
getUseStateHooks,
resetStateFunction,
getCanUnlinkModelName,
getUseRefHooks,
} from './form-renderer-helper/form-state';
import { shouldWrapInArrayField } from './form-renderer-helper/render-checkers';
import {
Expand Down Expand Up @@ -462,6 +463,8 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer<

statements.push(...getUseStateHooks(formMetadata.fieldConfigs));

statements.push(...getUseRefHooks(formMetadata.fieldConfigs));

statements.push(buildUseStateExpression('errors', factory.createObjectLiteralExpression()));

let defaultValueVariableName: undefined | string;
Expand Down
24 changes: 24 additions & 0 deletions packages/codegen-ui-react/lib/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,30 @@ export const buildUseStateExpression = (name: string, defaultValue: Expression):
);
};

export const buildUseRefExpression = (name: string, defaultValue?: Expression): Statement => {
return factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
factory.createIdentifier(name),
undefined,
undefined,
factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier('React'),
factory.createIdentifier('useRef'),
),
undefined,
defaultValue ? [defaultValue] : undefined,
),
),
],
NodeFlags.Const,
),
);
};

/**
* Create statement to declare and initialized a const.
*
Expand Down
1 change: 1 addition & 0 deletions packages/codegen-ui-react/lib/imports/import-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum ImportSource {
UI_REACT = '@aws-amplify/ui-react',
UI_REACT_INTERNAL = '@aws-amplify/ui-react/internal',
AMPLIFY_DATASTORE = '@aws-amplify/datastore',
REACT_STORAGE = '@aws-amplify/ui-react-storage',
LOCAL_MODELS = '../models',
LOCAL_SCHEMA = '../models/schema',
UTILS = './utils',
Expand Down
2 changes: 2 additions & 0 deletions packages/codegen-ui-react/lib/primitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export enum Primitive {
SliderField = 'SliderField',
StepperField = 'StepperField',
SwitchField = 'SwitchField',
StorageField = 'StorageField',
Table = 'Table',
TableBody = 'TableBody',
TableCell = 'TableCell',
Expand Down Expand Up @@ -84,6 +85,7 @@ export const PrimitivesWithChangeEvent: Set<Primitive> = new Set([
Primitive.SelectField,
Primitive.SliderField,
Primitive.StepperField,
Primitive.StorageField,
Primitive.SwitchField,
Primitive.TextAreaField,
Primitive.TextField,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,14 @@ export function buildFixedLiteralExpression(
const { value, type } = prop;
switch (typeof value) {
case 'number':
return factory.createNumericLiteral(value, undefined);
return factory.createNumericLiteral(value as number, undefined);
case 'boolean':
return value ? factory.createTrue() : factory.createFalse();
case 'string':
return fixedPropertyWithTypeToLiteral(value, type);
case 'object':
if (type !== 'object') {
bombguy marked this conversation as resolved.
Show resolved Hide resolved
return fixedPropertyWithTypeToLiteral(value as string, type);
}
if (value instanceof Date) {
throw new Error('Date object is not currently supported for fixed literal expression.');
}
Expand Down
50 changes: 32 additions & 18 deletions packages/codegen-ui-react/lib/react-component-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { ImportCollection, ImportSource, ImportValue } from './imports';
import { addFormAttributes } from './forms';
import { shouldWrapInArrayField, renderArrayFieldComponent, getDecoratedLabel } from './forms/form-renderer-helper';
import { getIsRequiredValue } from './forms/form-renderer-helper/label-decorator';
import { renderStorageFieldComponent } from './utils/forms/storage-field-component';

export class ReactComponentRenderer<TPropIn> extends ComponentRendererBase<
TPropIn,
Expand All @@ -66,37 +67,50 @@ export class ReactComponentRenderer<TPropIn> extends ComponentRendererBase<
renderChildren: ((children: StudioComponentChild[]) => JsxChild[]) | undefined = undefined,
): JsxElement | JsxSelfClosingElement {
const children = this.component.children ?? [];
const { fieldConfigs, labelDecorator } = this.componentMetadata.formMetadata || {};

const element = factory.createJsxElement(
this.renderOpeningElement(),
renderChildren && !hasChildrenProp(this.component.properties) ? renderChildren(children) : [],
factory.createJsxClosingElement(factory.createIdentifier(this.component.componentType)),
);

this.importCollection.addImport(ImportSource.UI_REACT, this.component.componentType);

const { fieldConfigs, labelDecorator } = this.componentMetadata.formMetadata || {};

// Add ArrayField wrapper to element if Array type
if (
fieldConfigs &&
fieldConfigs[this.component.name] &&
shouldWrapInArrayField(fieldConfigs[this.component.name])
) {
this.importCollection.addImport(ImportSource.UI_REACT, 'Icon');
this.importCollection.addImport(ImportSource.UI_REACT, 'Badge');
this.importCollection.addImport(ImportSource.UI_REACT, 'ScrollView');
this.importCollection.addImport(ImportSource.UI_REACT, 'Divider');
this.importCollection.addImport(ImportSource.UI_REACT, 'Text');
this.importCollection.addImport(ImportSource.UI_REACT, 'useTheme');
if (this.component.componentType !== 'StorageField') {
this.importCollection.addImport(ImportSource.UI_REACT, this.component.componentType);
}

if (fieldConfigs && fieldConfigs[this.component.name]) {
const isRequired = getIsRequiredValue(this.component.properties.isRequired);
let label = '';
if (typeof this.component.properties.label === 'object' && 'value' in this.component.properties.label) {
label = this.component.properties.label.value.toString() ?? '';
}

const isRequired = getIsRequiredValue(this.component.properties.isRequired);
return renderArrayFieldComponent(this.component.name, label, fieldConfigs, element, labelDecorator, isRequired);
if (this.component.componentType === 'StorageField') {
this.importCollection.addImport(ImportSource.REACT_STORAGE, 'StorageManager');
this.importCollection.addImport(ImportSource.UI_REACT_INTERNAL, 'Field');

return renderStorageFieldComponent(
this.component,
this.componentMetadata,
label,
fieldConfigs,
labelDecorator,
isRequired,
);
}

if (shouldWrapInArrayField(fieldConfigs[this.component.name])) {
// Add ArrayField wrapper to element if Array type
this.importCollection.addImport(ImportSource.UI_REACT, 'Icon');
this.importCollection.addImport(ImportSource.UI_REACT, 'Badge');
this.importCollection.addImport(ImportSource.UI_REACT, 'ScrollView');
this.importCollection.addImport(ImportSource.UI_REACT, 'Divider');
this.importCollection.addImport(ImportSource.UI_REACT, 'Text');
this.importCollection.addImport(ImportSource.UI_REACT, 'useTheme');

return renderArrayFieldComponent(this.component.name, label, fieldConfigs, element, labelDecorator, isRequired);
}
}

return element;
Expand Down
Loading