Skip to content

Commit

Permalink
chore: differentiate reset for datastore update form (#670)
Browse files Browse the repository at this point in the history
Co-authored-by: Hein Jeong <heinje@amazon.com>
  • Loading branch information
hein-j and Hein Jeong authored Sep 22, 2022
1 parent 5c93079 commit 7fa7664
Show file tree
Hide file tree
Showing 9 changed files with 494 additions and 305 deletions.

Large diffs are not rendered by default.

49 changes: 19 additions & 30 deletions packages/codegen-ui-react/lib/forms/form-renderer-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
capitalizeFirstLetter,
getCurrentValueIdentifier,
getCurrentValueName,
getSetNameIdentifier,
resetValuesName,
setFieldState,
setStateExpression,
Expand Down Expand Up @@ -203,7 +204,7 @@ export const addFormAttributes = (component: StudioComponent | StudioComponentCh
);
}
}
if (componentName === 'ClearButton') {
if (componentName === 'ClearButton' || componentName === 'ResetButton') {
attributes.push(
factory.createJsxAttribute(
factory.createIdentifier('onClick'),
Expand Down Expand Up @@ -1112,33 +1113,6 @@ export const onSubmitValidationRun = [
),
];

export const ifRecordDefinedExpression = (dataTypeName: string, fieldConfigs: Record<string, FieldConfigMetadata>) => {
return factory.createIfStatement(
factory.createIdentifier('record'),
factory.createBlock(
[
factory.createExpressionStatement(
factory.createCallExpression(factory.createIdentifier(`set${dataTypeName}Record`), undefined, [
factory.createIdentifier('record'),
]),
),
...Object.keys(fieldConfigs).map((field) =>
factory.createExpressionStatement(
factory.createCallExpression(factory.createIdentifier(`set${capitalizeFirstLetter(field)}`), undefined, [
factory.createPropertyAccessExpression(
factory.createIdentifier('record'),
factory.createIdentifier(field),
),
]),
),
),
],
true,
),
undefined,
);
};

export const buildSetStateFunction = (fieldConfigs: Record<string, FieldConfigMetadata>) => {
const fieldSet = new Set<string>();
const expression = Object.keys(fieldConfigs).reduce<ExpressionStatement[]>((acc, field) => {
Expand All @@ -1161,7 +1135,18 @@ export const buildSetStateFunction = (fieldConfigs: Record<string, FieldConfigMe
return factory.createIfStatement(factory.createIdentifier('initialData'), factory.createBlock(expression, true));
};

export const buildUpdateDatastoreQuery = (dataTypeName: string, fieldConfigs: Record<string, FieldConfigMetadata>) => {
// ex. React.useEffect(resetStateValues, [bookRecord])
export const buildResetValuesOnRecordUpdate = (recordName: string) => {
return factory.createExpressionStatement(
factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('React'), factory.createIdentifier('useEffect')),
undefined,
[resetValuesName, factory.createArrayLiteralExpression([factory.createIdentifier(recordName)], false)],
),
);
};

export const buildUpdateDatastoreQuery = (dataTypeName: string, recordName: string) => {
// TODO: update this once cpk is supported in datastore
const pkQueryIdentifier = factory.createIdentifier('id');
return [
Expand Down Expand Up @@ -1210,7 +1195,11 @@ export const buildUpdateDatastoreQuery = (dataTypeName: string, fieldConfigs: Re
NodeFlags.Const,
),
),
ifRecordDefinedExpression(dataTypeName, fieldConfigs),
factory.createExpressionStatement(
factory.createCallExpression(getSetNameIdentifier(recordName), undefined, [
factory.createIdentifier('record'),
]),
),
],
true,
),
Expand Down
131 changes: 103 additions & 28 deletions packages/codegen-ui-react/lib/forms/form-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
SyntaxKind,
ObjectLiteralExpression,
CallExpression,
PropertyAssignment,
} from 'typescript';

export const getCurrentValueName = (fieldName: string) => `current${capitalizeFirstLetter(fieldName)}Value`;
Expand Down Expand Up @@ -72,60 +73,134 @@ export const getDefaultValueExpression = (
}
return factory.createIdentifier('undefined');
};
/* ex. const initialValues = {
name: undefined,
isChecked: false
}
*/
export const getInitialValues = (fieldConfigs: Record<string, FieldConfigMetadata>): Statement => {
const stateNames = new Set<string>();
const propertyAssignments = Object.entries(fieldConfigs).reduce<PropertyAssignment[]>(
(acc, [name, { dataType, componentType }]) => {
const stateName = name.split('.')[0];
if (!stateNames.has(stateName)) {
acc.push(
factory.createPropertyAssignment(
factory.createIdentifier(stateName),
getDefaultValueExpression(name, componentType, dataType),
),
);
stateNames.add(stateName);
}
return acc;
},
[],
);

return factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
factory.createIdentifier('initialValues'),
undefined,
undefined,
factory.createObjectLiteralExpression(propertyAssignments, true),
),
],
NodeFlags.Const,
),
);
};

/**
* iterates field configs to create useState hooks for each field
* populates the default values as undefined if it as a nested object, relationship model or nonModel
* the default is an empty object
* @param fieldConfigs
* @returns
*/
export const getUseStateHooks = (fieldConfigs: Record<string, FieldConfigMetadata>): Statement[] => {
const stateNames = new Set<string>();
return Object.entries(fieldConfigs).reduce<Statement[]>((acc, [name, { dataType, componentType }]) => {
return Object.keys(fieldConfigs).reduce<Statement[]>((acc, name) => {
const stateName = name.split('.')[0];
if (!stateNames.has(stateName)) {
acc.push(buildUseStateExpression(stateName, getDefaultValueExpression(name, componentType, dataType)));
acc.push(
buildUseStateExpression(
stateName,
factory.createPropertyAccessExpression(
factory.createIdentifier('initialValues'),
factory.createIdentifier(stateName),
),
),
);
stateNames.add(stateName);
}
return acc;
}, []);
};

/**
* function used by the onClear/onReset button cta
* 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
*
* ex.
* const resetStateValues = () => {
* setName('')
* setLastName('')
* setName(initialValues.name)
* setLastName(initialValues.lastName)
* ....
* };
*/
export const resetStateFunction = (fieldConfigs: Record<string, FieldConfigMetadata>) => {
export const resetStateFunction = (fieldConfigs: Record<string, FieldConfigMetadata>, recordName?: string) => {
const cleanValues = recordName ? 'cleanValues' : 'initialValues';

const stateNames = new Set<string>();
const setStateExpressions = Object.entries(fieldConfigs).reduce<Statement[]>(
(acc, [name, { dataType, componentType, isArray }]) => {
const stateName = name.split('.')[0];
if (!stateNames.has(stateName)) {
acc.push(setStateExpression(stateName, getDefaultValueExpression(name, componentType, dataType)));
if (isArray) {
acc.push(
setStateExpression(
getCurrentValueName(stateName),
getDefaultValueExpression(name, componentType, dataType),
),
);
}
stateNames.add(stateName);
const expressions = Object.entries(fieldConfigs).reduce<Statement[]>((acc, [name, { isArray }]) => {
const stateName = name.split('.')[0];
if (!stateNames.has(stateName)) {
acc.push(
setStateExpression(
stateName,
factory.createPropertyAccessExpression(
factory.createIdentifier(cleanValues),
factory.createIdentifier(stateName),
),
),
);
if (isArray) {
acc.push(setStateExpression(getCurrentValueName(stateName), factory.createStringLiteral('')));
}
return acc;
},
[],
);
stateNames.add(stateName);
}
return acc;
}, []);

// ex. const cleanValues = {...initialValues, ...bookRecord}
if (recordName) {
expressions.unshift(
factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
factory.createIdentifier('cleanValues'),
undefined,
undefined,
factory.createObjectLiteralExpression(
[
factory.createSpreadAssignment(factory.createIdentifier('initialValues')),
factory.createSpreadAssignment(factory.createIdentifier(recordName)),
],
false,
),
),
],
NodeFlags.Const,
),
),
);
}

// also reset the state of the errors
setStateExpressions.push(setStateExpression('errors', factory.createObjectLiteralExpression()));
expressions.push(setStateExpression('errors', factory.createObjectLiteralExpression()));
return factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
Expand All @@ -140,7 +215,7 @@ export const resetStateFunction = (fieldConfigs: Record<string, FieldConfigMetad
[],
undefined,
factory.createToken(SyntaxKind.EqualsGreaterThanToken),
factory.createBlock(setStateExpressions, true),
factory.createBlock(expressions, true),
),
),
],
Expand Down
46 changes: 31 additions & 15 deletions packages/codegen-ui-react/lib/forms/react-form-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,19 @@ import { RequiredKeys } from '../utils/type-utils';
import {
buildMutationBindings,
buildOverrideTypesBindings,
buildResetValuesOnRecordUpdate,
buildSetStateFunction,
buildUpdateDatastoreQuery,
buildValidations,
runValidationTasksFunction,
} from './form-renderer-helper';
import { buildUseStateExpression, getCurrentValueName, getUseStateHooks, resetStateFunction } from './form-state';
import {
buildUseStateExpression,
getCurrentValueName,
getInitialValues,
getUseStateHooks,
resetStateFunction,
} from './form-state';
import {
buildFormPropNode,
baseValidationConditionalType,
Expand Down Expand Up @@ -328,6 +335,8 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer<
formActionType,
} = this.component;
const lowerCaseDataTypeName = lowerCaseFirst(dataTypeName);
const lowerCaseDataTypeNameRecord = `${lowerCaseDataTypeName}Record`;
const isDataStoreUpdateForm = dataSourceType === 'DataStore' && formActionType === 'update';

if (!formMetadata) {
throw new Error(`Form Metadata is missing from form: ${this.component.name}`);
Expand Down Expand Up @@ -392,31 +401,38 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer<
);
}

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

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

statements.push(resetStateFunction(formMetadata.fieldConfigs));
statements.push(
resetStateFunction(formMetadata.fieldConfigs, isDataStoreUpdateForm ? lowerCaseDataTypeNameRecord : undefined),
);

if (isDataStoreUpdateForm) {
statements.push(
buildUseStateExpression(lowerCaseDataTypeNameRecord, factory.createIdentifier(lowerCaseDataTypeName)),
);
statements.push(
addUseEffectWrapper(
buildUpdateDatastoreQuery(dataTypeName, lowerCaseDataTypeNameRecord),
// TODO: change once cpk is supported in datastore
['id', lowerCaseDataTypeName],
),
);

statements.push(buildResetValuesOnRecordUpdate(lowerCaseDataTypeNameRecord));
}

this.importCollection.addMappedImport(ImportValue.VALIDATE_FIELD);
this.importCollection.addMappedImport(ImportValue.FETCH_BY_PATH);

// add model import for datastore type
if (dataSourceType === 'DataStore') {
this.importCollection.addImport(ImportSource.LOCAL_MODELS, dataTypeName);
if (formActionType === 'update') {
statements.push(
buildUseStateExpression(`${lowerCaseDataTypeName}Record`, factory.createIdentifier(lowerCaseDataTypeName)),
);
statements.push(
addUseEffectWrapper(
buildUpdateDatastoreQuery(dataTypeName, formMetadata.fieldConfigs),
// TODO: change once cpk is supported in datastore
['id', lowerCaseDataTypeName],
),
);
}
}

if (dataSourceType === 'Custom' && formActionType === 'update') {
statements.push(addUseEffectWrapper([buildSetStateFunction(formMetadata.fieldConfigs)], []));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
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 { mapButtons } from '../../../generate-form-definition/helpers/map-cta';

describe('mapButtons', () => {
it('correctly excludes from matrix but returns all configs', () => {
const buttonConfigs = mapButtons('create', { submit: { excluded: true } });

expect(buttonConfigs.buttonMatrix).toStrictEqual([['clear'], ['cancel']]);
expect(buttonConfigs.buttonConfigs.submit).toBeDefined();
expect(buttonConfigs.buttonConfigs.cancel).toBeDefined();
expect(buttonConfigs.buttonConfigs.clear).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function generateFormDefinition({

mapElements({ form, formDefinition, modelFieldsConfigs });

formDefinition.buttons = mapButtons(form.cta);
formDefinition.buttons = mapButtons(form.formActionType, form.cta);

return formDefinition;
}
Loading

0 comments on commit 7fa7664

Please sign in to comment.