fix: Updated drop down control memo usage (#11218)

* Stopped props drilling of eval state

* Connect drop down to redux state

* Added extra check to formcontrol memo function

* Reduced modification of section at top

* Stopped mutating the initial state

* Created selector to get dynamic fetched values

* <refactor> Added comments and refactors

- Added key to the ES fragment
- Cleaned drop down component from redundant code
- Added comments

* <refactor> Removed test files

- Removed testing JSON configs

* <fix> Added null check for form eval output

- Added check to prevent null evalOutput in forms

* <chore> Removed console error

- Removed console error which is causing the vercel builds to fail
This commit is contained in:
Ayush Pahwa 2022-02-26 22:41:38 +05:30 committed by GitHub
parent a2240c7107
commit fe2d625f5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 123 additions and 143 deletions

View File

@ -1,10 +1,7 @@
import { Component } from "react"; import { Component } from "react";
import { ControlType } from "constants/PropertyControlConstants"; import { ControlType } from "constants/PropertyControlConstants";
import { InputType } from "components/constants"; import { InputType } from "components/constants";
import { import { ConditonalObject } from "reducers/evaluationReducers/formEvaluationReducer";
ConditonalObject,
DynamicValues,
} from "reducers/evaluationReducers/formEvaluationReducer";
import { DropdownOption } from "components/ads/Dropdown"; import { DropdownOption } from "components/ads/Dropdown";
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
abstract class BaseControl<P extends ControlProps, S = {}> extends Component< abstract class BaseControl<P extends ControlProps, S = {}> extends Component<
@ -77,7 +74,6 @@ export interface ControlData {
identifier?: string; identifier?: string;
sectionName?: string; sectionName?: string;
disabled?: boolean; disabled?: boolean;
dynamicFetchedValues?: DynamicValues; // Object that holds the output of the dynamic fetched values
} }
export type FormConfig = Omit<ControlData, "configProperty"> & { export type FormConfig = Omit<ControlData, "configProperty"> & {
configProperty?: string; configProperty?: string;

View File

@ -9,7 +9,9 @@ import {
WrappedFieldInputProps, WrappedFieldInputProps,
WrappedFieldMetaProps, WrappedFieldMetaProps,
} from "redux-form"; } from "redux-form";
import { DynamicValues } from "reducers/evaluationReducers/formEvaluationReducer"; import { connect } from "react-redux";
import { AppState } from "reducers";
import { getDynamicFetchedValues } from "selectors/formSelectors";
const DropdownSelect = styled.div` const DropdownSelect = styled.div`
font-size: 14px; font-size: 14px;
@ -27,23 +29,12 @@ class DropDownControl extends BaseControl<DropDownControlProps> {
width = this.props.customStyles.width; width = this.props.customStyles.width;
} }
// Options will be set dynamically if the config has fetchOptionsConditionally set to true
let options = this.props.options;
let isLoading = false;
if (
this.props.fetchOptionsCondtionally &&
!!this.props.dynamicFetchedValues
) {
options = this.props.dynamicFetchedValues.data;
isLoading = this.props.dynamicFetchedValues.isLoading;
}
return ( return (
<DropdownSelect data-cy={this.props.configProperty} style={{ width }}> <DropdownSelect data-cy={this.props.configProperty} style={{ width }}>
<Field <Field
component={renderDropdown} component={renderDropdown}
name={this.props.configProperty} name={this.props.configProperty}
props={{ ...this.props, width, isLoading, options }} // Passing options and isLoading in props allows the component to get the updated values props={{ ...this.props, width }}
type={this.props?.isMultiSelect ? "select-multiple" : undefined} type={this.props?.isMultiSelect ? "select-multiple" : undefined}
/> />
</DropdownSelect> </DropdownSelect>
@ -55,39 +46,38 @@ class DropDownControl extends BaseControl<DropDownControlProps> {
} }
} }
function renderDropdown(props: { function renderDropdown(
input?: WrappedFieldInputProps; props: {
meta?: WrappedFieldMetaProps; input?: WrappedFieldInputProps;
props: DropDownControlProps; meta?: Partial<WrappedFieldMetaProps>;
width: string; width: string;
formName: string; } & DropDownControlProps,
isLoading?: boolean; ): JSX.Element {
options: DropdownOption[];
disabled?: boolean;
}): JSX.Element {
let selectedValue = props.input?.value; let selectedValue = props.input?.value;
if (_.isUndefined(props.input?.value)) { if (_.isUndefined(props.input?.value)) {
selectedValue = props?.props?.initialValue; selectedValue = props?.initialValue;
}
let options: DropdownOption[] = [];
let selectedOption = {};
if (typeof props.options === "object" && Array.isArray(props.options)) {
options = props.options;
selectedOption =
options.find(
(option: DropdownOption) => option.value === selectedValue,
) || {};
} }
const selectedOption =
props.options.find(
(option: DropdownOption) => option.value === selectedValue,
) || {};
return ( return (
<Dropdown <Dropdown
boundary="window" boundary="window"
disabled={props.disabled} disabled={props.disabled}
dontUsePortal={false} dontUsePortal={false}
dropdownMaxHeight="250px" dropdownMaxHeight="250px"
errorMsg={props.props?.errorText}
helperText={props.props?.info}
isLoading={props.isLoading} isLoading={props.isLoading}
isMultiSelect={props?.props?.isMultiSelect} isMultiSelect={props?.isMultiSelect}
onSelect={props.input?.onChange} onSelect={props.input?.onChange}
optionWidth={props.width} optionWidth={props.width}
options={props.options} options={options}
placeholder={props.props?.placeholderText} placeholder={props?.placeholderText}
selected={selectedOption} selected={selectedOption}
showLabelOnly showLabelOnly
width={props.width} width={props.width}
@ -103,7 +93,36 @@ export interface DropDownControlProps extends ControlProps {
isMultiSelect?: boolean; isMultiSelect?: boolean;
isSearchable?: boolean; isSearchable?: boolean;
fetchOptionsCondtionally?: boolean; fetchOptionsCondtionally?: boolean;
dynamicFetchedValues?: DynamicValues; isLoading: boolean;
} }
export default DropDownControl; const mapStateToProps = (
state: AppState,
ownProps: DropDownControlProps,
): { isLoading: boolean; options: DropdownOption[] } => {
// Added default options to prevent error when options is undefined
let isLoading = false;
let options: DropdownOption[] = ownProps.fetchOptionsCondtionally
? []
: ownProps.options;
try {
if (ownProps.fetchOptionsCondtionally) {
const dynamicFetchedValues = getDynamicFetchedValues(
state,
ownProps.configProperty,
);
isLoading = dynamicFetchedValues.isLoading;
options = dynamicFetchedValues.data;
}
return { isLoading, options };
} catch (e) {
return {
isLoading,
options,
};
}
};
// Connecting this componenet to the state to allow for dynamic fetching of options to be updated.
export default connect(mapStateToProps)(DropDownControl);

View File

@ -1,11 +1,9 @@
import React from "react"; import React from "react";
import FormControl from "pages/Editor/FormControl"; import FormControl from "pages/Editor/FormControl";
import styled from "styled-components"; import styled from "styled-components";
import FormLabel from "components/editorComponents/FormLabel";
import { ControlProps } from "./BaseControl"; import { ControlProps } from "./BaseControl";
import { Colors } from "constants/Colors"; import { Colors } from "constants/Colors";
import Icon, { IconSize } from "components/ads/Icon"; import Icon, { IconSize } from "components/ads/Icon";
import { getBindingOrConfigPathsForEntitySelectorControl } from "entities/Action/actionProperties";
import { allowedControlTypes } from "components/formControls/utils"; import { allowedControlTypes } from "components/formControls/utils";
const dropDownFieldConfig: any = { const dropDownFieldConfig: any = {
@ -39,15 +37,6 @@ const EntitySelectorContainer = styled.div`
justify-content: space-between; justify-content: space-between;
`; `;
export const StyledBottomLabel = styled(FormLabel)`
margin-top: 5px;
margin-left: 5px;
font-weight: 400;
font-size: 12px;
color: ${Colors.GREY_7};
line-height: 16px;
`;
function EntitySelectorComponent(props: any) { function EntitySelectorComponent(props: any) {
const { configProperty, schema } = props; const { configProperty, schema } = props;
@ -61,25 +50,20 @@ function EntitySelectorComponent(props: any) {
}; };
return ( return (
<EntitySelectorContainer> <EntitySelectorContainer key={`ES_${configProperty}`}>
{schema && {schema &&
schema.length > 0 && schema.length > 0 &&
schema.map((singleSchema: any, index: number) => { schema.map((singleSchema: any, index: number) => {
const columnPath = getBindingOrConfigPathsForEntitySelectorControl(
configProperty,
index,
);
return ( return (
allowedControlTypes.includes(singleSchema.controlType) && ( allowedControlTypes.includes(singleSchema.controlType) && (
<> <React.Fragment key={`ES_FRAG_${singleSchema.configProperty}`}>
{singleSchema.controlType === "DROP_DOWN" ? ( {singleSchema.controlType === "DROP_DOWN" ? (
<FormControl <FormControl
config={{ config={{
...dropDownFieldConfig, ...dropDownFieldConfig,
...singleSchema, ...singleSchema,
customStyles, customStyles,
configProperty: columnPath, key: `ES_${singleSchema.configProperty}`,
key: columnPath,
}} }}
formName={props.formName} formName={props.formName}
/> />
@ -89,19 +73,19 @@ function EntitySelectorComponent(props: any) {
...inputFieldConfig, ...inputFieldConfig,
...singleSchema, ...singleSchema,
customStyles, customStyles,
configProperty: columnPath, key: `ES_${singleSchema.configProperty}`,
key: columnPath,
}} }}
formName={props.formName} formName={props.formName}
/> />
)} )}
{index < schema.length - 1 && ( {index < schema.length - 1 && (
<CenteredIcon <CenteredIcon
key={`ES_ICON_${configProperty}`}
name="double-arrow-right" name="double-arrow-right"
size={IconSize.SMALL} size={IconSize.SMALL}
/> />
)} )}
</> </React.Fragment>
) )
); );
})} })}
@ -109,6 +93,8 @@ function EntitySelectorComponent(props: any) {
); );
} }
// This is a wrapper component that just encapsulated the children dropdown and dynamic text
// components & changes their appearance
export default function EntitySelectorControl( export default function EntitySelectorControl(
props: EntitySelectorControlProps, props: EntitySelectorControlProps,
) { ) {
@ -122,7 +108,7 @@ export default function EntitySelectorControl(
<EntitySelectorComponent <EntitySelectorComponent
configProperty={configProperty} configProperty={configProperty}
formName={formName} formName={formName}
key={configProperty} key={`ES_PARENT_${configProperty}`}
name={configProperty} name={configProperty}
schema={schema} schema={schema}
/> />

View File

@ -51,7 +51,7 @@ function FormControl(props: FormControlProps) {
props.formName, props.formName,
props?.multipleConfig, props?.multipleConfig,
), ),
[], [props],
); );
if (hidden) return null; if (hidden) return null;
@ -133,7 +133,13 @@ function FormConfig(props: FormConfigProps) {
); );
} }
export default memo(FormControl); // Updated the memo function to allow for disabled props to be compared
export default memo(FormControl, (prevProps, nextProps) => {
return (
prevProps === nextProps &&
prevProps.config.disabled === nextProps.config.disabled
);
});
function renderFormConfigTop(props: { config: ControlProps }) { function renderFormConfigTop(props: { config: ControlProps }) {
const { const {

View File

@ -80,7 +80,6 @@ import Spinner from "components/ads/Spinner";
import { import {
ConditionalOutput, ConditionalOutput,
FormEvalOutput, FormEvalOutput,
DynamicValues,
} from "reducers/evaluationReducers/formEvaluationReducer"; } from "reducers/evaluationReducers/formEvaluationReducer";
const QueryFormContainer = styled.form` const QueryFormContainer = styled.form`
@ -598,6 +597,7 @@ export function EditorJSONtoForm(props: Props) {
} }
}; };
// Extract the output of conditionals attached to the form from the state
const extractConditionalOutput = (section: any): ConditionalOutput => { const extractConditionalOutput = (section: any): ConditionalOutput => {
let conditionalOutput: ConditionalOutput = {}; let conditionalOutput: ConditionalOutput = {};
if ( if (
@ -649,67 +649,35 @@ export function EditorJSONtoForm(props: Props) {
}; };
// Function to modify the section config based on the output of evaluations // Function to modify the section config based on the output of evaluations
const modifySectionConfig = ( const modifySectionConfig = (section: any, enabled: boolean): any => {
section: any,
enabled: boolean,
dynamicFetchedValues: DynamicValues | undefined,
): any => {
if (!enabled) { if (!enabled) {
section.disabled = true; section.disabled = true;
} else { } else {
section.disabled = false; section.disabled = false;
} }
if (!!dynamicFetchedValues) {
section.dynamicFetchedValues = dynamicFetchedValues;
}
return section; return section;
}; };
// Function to extract the object for dynamicValues if it is there in the evaluation state
const extractDynamicValuesIfPresent = (
conditionalOutput: ConditionalOutput,
) => {
// By default, the section is enabled. This is to allow for the case where no conditional is provided.
// The evaluation state disables the section if the condition is not met. (Checkout formEval.ts)
let dynamicFetchedValues: DynamicValues | undefined;
if (conditionalOutput.hasOwnProperty("fetchDynamicValues")) {
dynamicFetchedValues = conditionalOutput.fetchDynamicValues;
}
return dynamicFetchedValues;
};
// Render function to render the V2 of form editor type (UQI) // Render function to render the V2 of form editor type (UQI)
// Section argument is a nested config object, this function recursively renders the UI based on the config // Section argument is a nested config object, this function recursively renders the UI based on the config
const renderEachConfigV2 = (formName: string, section: any, idx: number) => { const renderEachConfigV2 = (formName: string, section: any, idx: number) => {
let enabled = true; let enabled = true;
let dynamicFetchedValues: DynamicValues | undefined;
if (!!section) { if (!!section) {
// If the section is a nested component, recursively check for conditional statements
if ("schema" in section && section.schema.length > 0) { if ("schema" in section && section.schema.length > 0) {
section.schema.forEach((subSection: any, index: number) => { section.schema.forEach((subSection: any) => {
const configPropertyOfSubSection = `${
section.configProperty
}.column_${index + 1}`;
const conditionalOutput = extractConditionalOutput({ const conditionalOutput = extractConditionalOutput({
...subSection, ...subSection,
configProperty: configPropertyOfSubSection,
}); });
enabled = checkIfSectionIsEnabled(conditionalOutput); enabled = checkIfSectionIsEnabled(conditionalOutput);
dynamicFetchedValues = extractDynamicValuesIfPresent( subSection = modifySectionConfig(subSection, enabled);
conditionalOutput,
);
subSection = modifySectionConfig(
subSection,
enabled,
dynamicFetchedValues,
);
}); });
} }
// If the component is not allowed to render, return null // If the component is not allowed to render, return null
const conditionalOutput = extractConditionalOutput(section); const conditionalOutput = extractConditionalOutput(section);
if (!checkIfSectionCanRender(conditionalOutput)) return null; if (!checkIfSectionCanRender(conditionalOutput)) return null;
enabled = checkIfSectionIsEnabled(conditionalOutput); enabled = checkIfSectionIsEnabled(conditionalOutput);
dynamicFetchedValues = extractDynamicValuesIfPresent(conditionalOutput);
} }
if (section.hasOwnProperty("controlType")) { if (section.hasOwnProperty("controlType")) {
// If component is type section, render it's children // If component is type section, render it's children
@ -723,11 +691,7 @@ export function EditorJSONtoForm(props: Props) {
} }
try { try {
const { configProperty } = section; const { configProperty } = section;
const modifiedSection = modifySectionConfig( const modifiedSection = modifySectionConfig(section, enabled);
section,
enabled,
dynamicFetchedValues,
);
return ( return (
<FieldWrapper key={`${configProperty}_${idx}`}> <FieldWrapper key={`${configProperty}_${idx}`}>
<FormControl config={modifiedSection} formName={formName} /> <FormControl config={modifiedSection} formName={formName} />

View File

@ -87,16 +87,18 @@ function* setFormEvaluationSagaAsync(
// Once all the actions are done, extract the actions that need to be fetched dynamically // Once all the actions are done, extract the actions that need to be fetched dynamically
const formId = action.payload.formId; const formId = action.payload.formId;
const evalOutput = workerResponse[formId]; const evalOutput = workerResponse[formId];
const queueOfValuesToBeFetched = extractQueueOfValuesToBeFetched( if (!!evalOutput && typeof evalOutput === "object") {
evalOutput, const queueOfValuesToBeFetched = extractQueueOfValuesToBeFetched(
); evalOutput,
// Pass the queue to the saga to fetch the dynamic values );
yield call( // Pass the queue to the saga to fetch the dynamic values
fetchDynamicValuesSaga, yield call(
queueOfValuesToBeFetched, fetchDynamicValuesSaga,
formId, queueOfValuesToBeFetched,
evalOutput, formId,
); evalOutput,
);
}
} }
} catch (e) { } catch (e) {
log.error(e); log.error(e);
@ -112,11 +114,10 @@ function* fetchDynamicValuesSaga(
evalOutput: FormEvalOutput, evalOutput: FormEvalOutput,
) { ) {
for (const key of Object.keys(queueOfValuesToBeFetched)) { for (const key of Object.keys(queueOfValuesToBeFetched)) {
evalOutput = yield call( evalOutput[key].fetchDynamicValues = yield call(
fetchDynamicValueSaga, fetchDynamicValueSaga,
queueOfValuesToBeFetched[key], queueOfValuesToBeFetched[key],
key, Object.assign({}, evalOutput[key].fetchDynamicValues as DynamicValues),
evalOutput,
); );
} }
// Set the values to the state once all values are fetched // Set the values to the state once all values are fetched
@ -128,34 +129,31 @@ function* fetchDynamicValuesSaga(
function* fetchDynamicValueSaga( function* fetchDynamicValueSaga(
value: ConditionalOutput, value: ConditionalOutput,
key: string, dynamicFetchedValues: DynamicValues,
evalOutput: FormEvalOutput,
) { ) {
try { try {
const { config } = value.fetchDynamicValues as DynamicValues; const { config } = value.fetchDynamicValues as DynamicValues;
const { url } = config; const { url } = config;
(evalOutput[key].fetchDynamicValues as DynamicValues).hasStarted = true; dynamicFetchedValues.hasStarted = true;
// Call the API to fetch the dynamic values // Call the API to fetch the dynamic values
const response = yield call(PluginsApi.fetchDynamicFormValues, url); const response = yield call(PluginsApi.fetchDynamicFormValues, url);
(evalOutput[key].fetchDynamicValues as DynamicValues).isLoading = false; dynamicFetchedValues.isLoading = false;
if (!!response && response instanceof Array) { if (!!response && response instanceof Array) {
(evalOutput[key].fetchDynamicValues as DynamicValues).data = response; dynamicFetchedValues.data = response;
(evalOutput[key] dynamicFetchedValues.hasFetchFailed = false;
.fetchDynamicValues as DynamicValues).hasFetchFailed = false;
} else { } else {
(evalOutput[key] dynamicFetchedValues.hasFetchFailed = true;
.fetchDynamicValues as DynamicValues).hasFetchFailed = true; dynamicFetchedValues.data = [];
(evalOutput[key].fetchDynamicValues as DynamicValues).data = [];
} }
} catch (e) { } catch (e) {
log.error(e); log.error(e);
(evalOutput[key].fetchDynamicValues as DynamicValues).hasFetchFailed = true; dynamicFetchedValues.hasFetchFailed = true;
(evalOutput[key].fetchDynamicValues as DynamicValues).isLoading = false; dynamicFetchedValues.isLoading = false;
(evalOutput[key].fetchDynamicValues as DynamicValues).data = []; dynamicFetchedValues.data = [];
} }
return evalOutput; return dynamicFetchedValues;
} }
function* formEvaluationChangeListenerSaga() { function* formEvaluationChangeListenerSaga() {

View File

@ -1,13 +1,17 @@
import { getFormValues, isValid, getFormInitialValues } from "redux-form"; import { getFormValues, isValid, getFormInitialValues } from "redux-form";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { ActionData } from "reducers/entityReducers/actionsReducer"; import { ActionData } from "reducers/entityReducers/actionsReducer";
import { FormEvaluationState } from "reducers/evaluationReducers/formEvaluationReducer"; import {
DynamicValues,
FormEvaluationState,
} from "reducers/evaluationReducers/formEvaluationReducer";
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import _ from "lodash"; import { replace } from "lodash";
import { getDataTree } from "./dataTreeSelectors"; import { getDataTree } from "./dataTreeSelectors";
import { DataTree } from "entities/DataTree/dataTreeFactory"; import { DataTree } from "entities/DataTree/dataTreeFactory";
import { Action } from "entities/Action"; import { Action } from "entities/Action";
import { EvaluationError } from "utils/DynamicBindingUtils"; import { EvaluationError } from "utils/DynamicBindingUtils";
import { getActionIdFromURL } from "pages/Editor/Explorer/helpers";
type GetFormData = ( type GetFormData = (
state: AppState, state: AppState,
@ -30,6 +34,16 @@ export const getApiName = (state: AppState, id: string) => {
export const getFormEvaluationState = (state: AppState): FormEvaluationState => export const getFormEvaluationState = (state: AppState): FormEvaluationState =>
state.evaluations.formEvaluation; state.evaluations.formEvaluation;
// Selector to return the fetched values of the form components, only called for components that
// have the fetchOptionsDynamically option set to true
export const getDynamicFetchedValues = (
state: AppState,
configProperty: string,
): DynamicValues =>
state.evaluations.formEvaluation[getActionIdFromURL() as string][
configProperty
].fetchDynamicValues as DynamicValues;
type ConfigErrorProps = { configProperty: string; formName: string }; type ConfigErrorProps = { configProperty: string; formName: string };
export const getConfigErrors = createSelector( export const getConfigErrors = createSelector(
@ -53,7 +67,7 @@ export const getConfigErrors = createSelector(
const actionError = action && action?.__evaluation__?.errors; const actionError = action && action?.__evaluation__?.errors;
// get the configProperty for this form control and format it to resemble the format used in the action details errors object. // get the configProperty for this form control and format it to resemble the format used in the action details errors object.
const formattedConfig = _.replace( const formattedConfig = replace(
configProperty, configProperty,
"actionConfiguration", "actionConfiguration",
"config", "config",

View File

@ -85,11 +85,8 @@ const generateInitialEvalState = (formConfig: FormConfig) => {
); );
if ("schema" in formConfig && !!formConfig.schema) if ("schema" in formConfig && !!formConfig.schema)
formConfig.schema.forEach((config: FormConfig, index: number) => formConfig.schema.forEach((config: FormConfig) =>
generateInitialEvalState({ generateInitialEvalState({ ...config }),
...config,
configProperty: `${formConfig.configProperty}.column_${index + 1}`,
}),
); );
}; };