Skip to content

Commit

Permalink
feat: adding many to many support for form creation
Browse files Browse the repository at this point in the history
  • Loading branch information
zchenwei committed Nov 17, 2022
1 parent dafe587 commit e7852c3
Show file tree
Hide file tree
Showing 18 changed files with 652 additions and 91 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ describe('amplify form renderer tests', () => {
expect(declaration).toMatchSnapshot();
});

it('should generate a create form with manyToMany relationship', () => {
const { componentText, declaration } = generateWithAmplifyFormRenderer(
'forms/tag-datastore-create',
'datastore/tag-post',
);
// check nested model is imported
expect(componentText).toContain('import { Post, Tag, TagPost } from "../models";');

// check binding call is generated
expect(componentText).toContain('const postRecords = useDataStoreBinding({');

// check custom display value is set
expect(componentText).toContain('Posts: (record) => record?.title');

expect(componentText).toMatchSnapshot();
expect(declaration).toMatchSnapshot();
});

it('should render form with a two inputs in row', () => {
const { componentText, declaration } = generateWithAmplifyFormRenderer(
'forms/post-datastore-create-row',
Expand Down
10 changes: 9 additions & 1 deletion packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ export default class FormRenderer extends ReactComponentRenderer<BaseComponentPr
} = this.form;
const importedModelName = this.importCollection.getMappedAlias(ImportSource.LOCAL_MODELS, dataTypeName);

const { formMetadata } = this.componentMetadata;
if (!formMetadata) {
throw new Error(`Form Metadata is missing from form: ${this.component.name}`);
}
const hasManyFieldConfigs = Object.entries(formMetadata.fieldConfigs).filter(
([, fieldConfigMetaData]) => fieldConfigMetaData.relationship?.type === 'HAS_MANY',
);

const onSubmitIdentifier = factory.createIdentifier('onSubmit');

if (dataSourceType === 'Custom') {
Expand Down Expand Up @@ -128,7 +136,7 @@ export default class FormRenderer extends ReactComponentRenderer<BaseComponentPr
factory.createTryStatement(
factory.createBlock(
[
...buildDataStoreExpression(formActionType, importedModelName),
...buildDataStoreExpression(formActionType, importedModelName, hasManyFieldConfigs),
// call onSuccess hook if it exists
factory.createIfStatement(
factory.createIdentifier('onSuccess'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import { StudioComponent, StudioComponentChild, FormMetadata, isValidVariableNam
import { factory, SyntaxKind, JsxAttribute } from 'typescript';
import { buildComponentSpecificAttributes } from './static-props';
import { renderValueAttribute, renderDefaultValueAttribute, isControlledComponent } from './value-props';
import { buildOnChangeStatement, buildOnBlurStatement, buildOnSelect } from './event-handler-props';
import {
buildOnChangeStatement,
buildOnClearStatement,
buildOnBlurStatement,
buildOnSelect,
} from './event-handler-props';
import { getArrayChildRefName, resetValuesName } from './form-state';
import { shouldWrapInArrayField } from './render-checkers';
import { getAutocompleteOptionsProp } from './display-value';
Expand Down Expand Up @@ -71,9 +76,11 @@ export const addFormAttributes = (component: StudioComponent | StudioComponentCh
attributes.push(valueAttribute);
}

// TODO: Allow for other relationship types once valueMappings available
if (fieldConfig.componentType === 'Autocomplete' && fieldConfig.relationship) {
attributes.push(getAutocompleteOptionsProp({ fieldName: componentName, fieldConfig }));
attributes.push(buildOnSelect({ sanitizedFieldName: renderedVariableName, fieldConfig }));
attributes.push(buildOnClearStatement(componentName, fieldConfig));
}

if (formMetadata.formActionType === 'update' && !fieldConfig.isArray && !isControlledComponent(componentType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FieldConfigMetadata } from '@aws-amplify/codegen-ui/lib/types';
import { factory, NodeFlags, SyntaxKind, Expression } from 'typescript';
import { lowerCaseFirst } from '../../helpers';
import { getDisplayValueObjectName } from './display-value';
import { getSetNameIdentifier } from './form-state';
import { buildManyToManyRelationshipCreateStatements } from './relationship';

export const buildDataStoreExpression = (dataStoreActionType: 'update' | 'create', importedModelName: string) => {
export const buildDataStoreExpression = (
dataStoreActionType: 'update' | 'create',
importedModelName: string,
hasManyFieldConfigs: [string, FieldConfigMetadata][],
) => {
if (dataStoreActionType === 'update') {
return [
factory.createVariableStatement(
Expand Down Expand Up @@ -101,6 +107,25 @@ export const buildDataStoreExpression = (dataStoreActionType: 'update' | 'create
),
];
}

// TODO: Many to Many update action, this condition dataStoreActionType === 'create' can be moved inside the functon
if (hasManyFieldConfigs.length > 0 && dataStoreActionType === 'create') {
return hasManyFieldConfigs
.map((hasManyFieldConfig) => {
const [, fieldConfigMetaData] = hasManyFieldConfig;
if (
fieldConfigMetaData.relationship?.type === 'HAS_MANY' &&
fieldConfigMetaData.relationship.relatedJoinTableName
) {
return buildManyToManyRelationshipCreateStatements(importedModelName, hasManyFieldConfig);
}
return [];
})
.reduce((statements, statement) => {
return [...statements, ...statement];
}, []);
}

return [
factory.createExpressionStatement(
factory.createAwaitExpression(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,5 +266,10 @@ export function getModelsToImport(fieldConfig: FieldConfigMetadata): string[] {
});
}

// Import join table model
if (fieldConfig.relationship?.type === 'HAS_MANY' && fieldConfig.relationship.relatedJoinTableName) {
modelDependencies.push(fieldConfig.relationship.relatedJoinTableName);
}

return modelDependencies;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ import {
getCurrentDisplayValueName,
getCurrentValueIdentifier,
getCurrentValueName,
getDefaultValueExpression,
getRecordsName,
getSetNameIdentifier,
setFieldState,
setStateExpression,
} from './form-state';
Expand Down Expand Up @@ -130,6 +132,43 @@ export function buildOnBlurStatement(fieldName: string, fieldConfig: FieldConfig
);
}

/**
* e.g.
* onClear={() => {
* setCurrentTeamDisplayValue('');
* }}
*/
export function buildOnClearStatement(fieldName: string, fieldConfig: FieldConfigMetadata) {
const { componentType, dataType } = fieldConfig;
const renderedFieldName = fieldConfig.sanitizedFieldName || fieldName;

return factory.createJsxAttribute(
factory.createIdentifier('onClear'),
factory.createJsxExpression(
undefined,
factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
factory.createToken(SyntaxKind.EqualsGreaterThanToken),
factory.createBlock(
[
factory.createExpressionStatement(
factory.createCallExpression(
getSetNameIdentifier(getCurrentDisplayValueName(renderedFieldName)),
undefined,
[getDefaultValueExpression(fieldName, componentType, dataType, false, true)],
),
),
],
true,
),
),
),
);
}

/**
* if the onChange variable is defined it will send the current state of the fields into the function
* the function expects all fields in return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,11 @@ export const getDefaultValueExpression = (
componentType: string,
dataType?: DataFieldDataType,
isArray?: boolean,
isDisplayValue?: boolean,
): Expression => {
const componentTypeToDefaultValueMap: { [key: string]: Expression } = {
Autocomplete: isDisplayValue ? factory.createStringLiteral('') : factory.createIdentifier('undefined'),
TextField: factory.createStringLiteral(''),
ToggleButton: factory.createFalse(),
SwitchField: factory.createFalse(),
StepperField: factory.createNumericLiteral(0),
Expand Down Expand Up @@ -296,11 +299,15 @@ export const resetStateFunction = (fieldConfigs: Record<string, FieldConfigMetad
}

if (isModelDataType(fieldConfig)) {
acc.push(setStateExpression(getCurrentDisplayValueName(renderedName), factory.createIdentifier('undefined')));
acc.push(
setStateExpression(
getCurrentDisplayValueName(renderedName),
getDefaultValueExpression(name, componentType, dataType, false, true),
),
);

stateNames.add(stateName);
}
stateNames.add(stateName);
}
return acc;
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { factory } from 'typescript';
import { GenericDataRelationshipType } from '@aws-amplify/codegen-ui';
import { factory, NodeFlags, SyntaxKind } from 'typescript';
import { FieldConfigMetadata, GenericDataRelationshipType, HasManyRelationshipType } from '@aws-amplify/codegen-ui';
import { getRecordsName } from './form-state';
import { buildBaseCollectionVariableStatement } from '../../react-studio-template-renderer-helper';

Expand All @@ -32,3 +32,132 @@ export const buildRelationshipQuery = (relationship: GenericDataRelationshipType
]),
);
};

export const buildManyToManyRelationshipCreateStatements = (
modelName: string,
hasManyFieldConfig: [string, FieldConfigMetadata],
) => {
const [fieldName, fieldConfigMetaData] = hasManyFieldConfig;
const { relatedModelField, relatedJoinFieldName, relatedJoinTableName } =
fieldConfigMetaData.relationship as HasManyRelationshipType;
return [
factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
factory.createIdentifier(relatedModelField),
undefined,
undefined,
factory.createAwaitExpression(
factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier('DataStore'),
factory.createIdentifier('save'),
),
undefined,
[
factory.createNewExpression(factory.createIdentifier(modelName), undefined, [
factory.createIdentifier('modelFields'),
]),
],
),
),
),
],
NodeFlags.Const,
),
),
factory.createExpressionStatement(
factory.createAwaitExpression(
factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('Promise'), factory.createIdentifier('all')),
undefined,
[
factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier(fieldName),
factory.createIdentifier('reduce'),
),
undefined,
[
factory.createArrowFunction(
undefined,
undefined,
[
factory.createParameterDeclaration(
undefined,
undefined,
undefined,
factory.createIdentifier('promises'),
undefined,
undefined,
undefined,
),
factory.createParameterDeclaration(
undefined,
undefined,
undefined,
factory.createIdentifier(relatedJoinFieldName as string),
undefined,
undefined,
undefined,
),
],
undefined,
factory.createToken(SyntaxKind.EqualsGreaterThanToken),
factory.createBlock(
[
factory.createExpressionStatement(
factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier('promises'),
factory.createIdentifier('push'),
),
undefined,
[
factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier('DataStore'),
factory.createIdentifier('save'),
),
undefined,
[
factory.createNewExpression(
factory.createIdentifier(relatedJoinTableName as string),
undefined,
[
factory.createObjectLiteralExpression(
[
factory.createShorthandPropertyAssignment(
factory.createIdentifier(relatedModelField),
undefined,
),
factory.createShorthandPropertyAssignment(
factory.createIdentifier(relatedJoinFieldName as string),
undefined,
),
],
true,
),
],
),
],
),
],
),
),
factory.createReturnStatement(factory.createIdentifier('promises')),
],
true,
),
),
factory.createArrayLiteralExpression([], false),
],
),
],
),
),
),
];
};
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function getOnChangeAttribute({
factory.createCallExpression(
factory.createIdentifier(`set${capitalizeFirstLetter(getCurrentDisplayValueName(renderedFieldName))}`),
undefined,
[getDefaultValueExpression(fieldName, componentType, dataType)],
[getDefaultValueExpression(fieldName, componentType, dataType, false, true)],
),
),
);
Expand Down Expand Up @@ -212,7 +212,7 @@ export const renderArrayFieldComponent = (
let setFieldValueIdentifier = setStateName;

if (isModelDataType(fieldConfig)) {
setFieldValueIdentifier = factory.createIdentifier(getCurrentDisplayValueName(renderedFieldName));
setFieldValueIdentifier = getSetNameIdentifier(getCurrentDisplayValueName(renderedFieldName));
props.push(
factory.createJsxAttribute(
factory.createIdentifier('getBadgeText'),
Expand All @@ -235,7 +235,10 @@ export const renderArrayFieldComponent = (
),
factory.createJsxAttribute(
factory.createIdentifier('defaultFieldValue'),
factory.createJsxExpression(undefined, getDefaultValueExpression(fieldName, componentType, dataType)),
factory.createJsxExpression(
undefined,
getDefaultValueExpression(fieldName, componentType, dataType, false, true),
),
),
);

Expand Down
Loading

0 comments on commit e7852c3

Please sign in to comment.