diff --git a/app/client/src/entities/Action/actionProperties.test.ts b/app/client/src/entities/Action/actionProperties.test.ts index 1857e3350c..deb8ac72a7 100644 --- a/app/client/src/entities/Action/actionProperties.test.ts +++ b/app/client/src/entities/Action/actionProperties.test.ts @@ -1,5 +1,5 @@ import { Action, PluginType } from "entities/Action/index"; -import { getBindingPathsOfAction } from "entities/Action/actionProperties"; +import { getBindingAndReactivePathsOfAction } from "entities/Action/actionProperties"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; const DEFAULT_ACTION: Action = { @@ -18,12 +18,16 @@ const DEFAULT_ACTION: Action = { organizationId: "", pageId: "", pluginId: "", + messages: [], pluginType: PluginType.DB, }; -describe("getBindingPathsOfAction", () => { +describe("getReactivePathsOfAction", () => { it("returns default list of no config is sent", () => { - const response = getBindingPathsOfAction(DEFAULT_ACTION, undefined); + const response = getBindingAndReactivePathsOfAction( + DEFAULT_ACTION, + undefined, + ).reactivePaths; expect(response).toStrictEqual({ data: EvaluationSubstitutionType.TEMPLATE, isLoading: EvaluationSubstitutionType.TEMPLATE, @@ -73,7 +77,8 @@ describe("getBindingPathsOfAction", () => { }, }; - const response = getBindingPathsOfAction(basicAction, config); + const response = getBindingAndReactivePathsOfAction(basicAction, config) + .reactivePaths; expect(response).toStrictEqual({ data: EvaluationSubstitutionType.TEMPLATE, isLoading: EvaluationSubstitutionType.TEMPLATE, @@ -139,7 +144,8 @@ describe("getBindingPathsOfAction", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const response = getBindingPathsOfAction(basicAction, config); + const response = getBindingAndReactivePathsOfAction(basicAction, config) + .reactivePaths; expect(response).toStrictEqual({ data: EvaluationSubstitutionType.TEMPLATE, isLoading: EvaluationSubstitutionType.TEMPLATE, @@ -195,7 +201,8 @@ describe("getBindingPathsOfAction", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const response = getBindingPathsOfAction(basicAction, config); + const response = getBindingAndReactivePathsOfAction(basicAction, config) + .reactivePaths; expect(response).toStrictEqual({ data: EvaluationSubstitutionType.TEMPLATE, isLoading: EvaluationSubstitutionType.TEMPLATE, @@ -205,7 +212,7 @@ describe("getBindingPathsOfAction", () => { }); }); - it("checks for hidden field and returns bindingPaths accordingly", () => { + it("checks for hidden field and returns reactivePaths accordingly", () => { const config = [ { sectionName: "", @@ -252,7 +259,8 @@ describe("getBindingPathsOfAction", () => { }, }; - const response = getBindingPathsOfAction(basicAction, config); + const response = getBindingAndReactivePathsOfAction(basicAction, config) + .reactivePaths; expect(response).toStrictEqual({ data: EvaluationSubstitutionType.TEMPLATE, isLoading: EvaluationSubstitutionType.TEMPLATE, @@ -263,7 +271,8 @@ describe("getBindingPathsOfAction", () => { basicAction.actionConfiguration.template.setting = true; - const response2 = getBindingPathsOfAction(basicAction, config); + const response2 = getBindingAndReactivePathsOfAction(basicAction, config) + .reactivePaths; expect(response2).toStrictEqual({ data: EvaluationSubstitutionType.TEMPLATE, isLoading: EvaluationSubstitutionType.TEMPLATE, @@ -272,4 +281,63 @@ describe("getBindingPathsOfAction", () => { "config.body2": EvaluationSubstitutionType.TEMPLATE, }); }); + + it.only("returns default list of no config is sent", () => { + const response = getBindingAndReactivePathsOfAction( + DEFAULT_ACTION, + undefined, + ).bindingPaths; + expect(response).toStrictEqual({}); + }); + + it.only("returns correct values for basic config", () => { + const config = [ + { + sectionName: "", + id: 1, + children: [ + { + label: "", + configProperty: "actionConfiguration.body", + controlType: "QUERY_DYNAMIC_TEXT", + }, + { + label: "", + configProperty: "actionConfiguration.body2", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + }, + { + label: "", + configProperty: "actionConfiguration.field1", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + evaluationSubstitutionType: "SMART_SUBSTITUTE", + }, + { + label: "", + configProperty: "actionConfiguration.field2", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + evaluationSubstitutionType: "PARAMETER", + }, + ], + }, + ]; + const basicAction = { + ...DEFAULT_ACTION, + actionConfiguration: { + body: "basic action", + body2: "another body", + field1: "test", + field2: "anotherTest", + }, + }; + + const response = getBindingAndReactivePathsOfAction(basicAction, config) + .bindingPaths; + expect(response).toStrictEqual({ + "config.body": EvaluationSubstitutionType.TEMPLATE, + "config.body2": EvaluationSubstitutionType.TEMPLATE, + "config.field1": EvaluationSubstitutionType.SMART_SUBSTITUTE, + "config.field2": EvaluationSubstitutionType.PARAMETER, + }); + }); }); diff --git a/app/client/src/entities/Action/actionProperties.ts b/app/client/src/entities/Action/actionProperties.ts index dbb46719db..e2e8f18f44 100644 --- a/app/client/src/entities/Action/actionProperties.ts +++ b/app/client/src/entities/Action/actionProperties.ts @@ -12,9 +12,15 @@ import { WhereClauseSubComponent, allowedControlTypes, } from "components/formControls/utils"; +import formControlTypes from "utils/formControl/formControlTypes"; -const dynamicFields = ["QUERY_DYNAMIC_TEXT", "QUERY_DYNAMIC_INPUT_TEXT"]; +const dynamicFields = [ + formControlTypes.QUERY_DYNAMIC_TEXT, + formControlTypes.QUERY_DYNAMIC_INPUT_TEXT, +]; +type ReactivePaths = Record; +type BindingPaths = ReactivePaths; const getCorrectEvaluationSubstitutionType = (substitutionType?: string) => { if (substitutionType) { if (substitutionType === EvaluationSubstitutionType.SMART_SUBSTITUTE) { @@ -26,20 +32,25 @@ const getCorrectEvaluationSubstitutionType = (substitutionType?: string) => { return EvaluationSubstitutionType.TEMPLATE; }; -export const getBindingPathsOfAction = ( +export const getBindingAndReactivePathsOfAction = ( action: Action, formConfig?: any[], -): Record => { - const bindingPaths: Record = { +): { reactivePaths: ReactivePaths; bindingPaths: BindingPaths } => { + let reactivePaths: ReactivePaths = { data: EvaluationSubstitutionType.TEMPLATE, isLoading: EvaluationSubstitutionType.TEMPLATE, datasourceUrl: EvaluationSubstitutionType.TEMPLATE, }; + const bindingPaths: BindingPaths = {}; if (!formConfig) { - return { - ...bindingPaths, + reactivePaths = { + ...reactivePaths, config: EvaluationSubstitutionType.TEMPLATE, }; + return { + reactivePaths, + bindingPaths, + }; } const recursiveFindBindingPaths = (formConfig: any) => { if (formConfig.children) { @@ -61,7 +72,7 @@ export const getBindingPathsOfAction = ( bindingPaths[configPath] = getCorrectEvaluationSubstitutionType( alternateViewTypeInputConfig.evaluationSubstitutionType, ); - } else if (formConfig.controlType === "ARRAY_FIELD") { + } else if (formConfig.controlType === formControlTypes.ARRAY_FIELD) { let actionValue = _.get(action, formConfig.configProperty); if (Array.isArray(actionValue)) { actionValue = actionValue.filter((val) => val); @@ -81,7 +92,7 @@ export const getBindingPathsOfAction = ( }); } } - } else if (formConfig.controlType === "WHERE_CLAUSE") { + } else if (formConfig.controlType === formControlTypes.WHERE_CLAUSE) { const recursiveFindBindingPathsForWhereClause = ( newConfigPath: string, actionValue: any, @@ -138,7 +149,7 @@ export const getBindingPathsOfAction = ( recursiveFindBindingPathsForWhereClause(childrenPath, value); }); } - } else if (formConfig.controlType === "PAGINATION") { + } else if (formConfig.controlType === formControlTypes.PAGINATION) { const limitPath = getBindingOrConfigPathsForPaginationControl( PaginationSubComponent.Offset, configPath, @@ -153,7 +164,7 @@ export const getBindingPathsOfAction = ( bindingPaths[offsetPath] = getCorrectEvaluationSubstitutionType( formConfig.evaluationSubstitutionType, ); - } else if (formConfig.controlType === "SORTING") { + } else if (formConfig.controlType === formControlTypes.SORTING) { const actionValue = _.get(action, formConfig.configProperty); if (Array.isArray(actionValue)) { actionValue.forEach((fieldConfig: any, index: number) => { @@ -175,7 +186,7 @@ export const getBindingPathsOfAction = ( ); }); } - } else if (formConfig.controlType === "ENTITY_SELECTOR") { + } else if (formConfig.controlType === formControlTypes.ENTITY_SELECTOR) { if (Array.isArray(formConfig.schema)) { formConfig.schema.forEach((schemaField: any, index: number) => { if (allowedControlTypes.includes(schemaField.controlType)) { @@ -193,7 +204,11 @@ export const getBindingPathsOfAction = ( } }; formConfig.forEach(recursiveFindBindingPaths); - return bindingPaths; + reactivePaths = { + ...reactivePaths, + ...bindingPaths, + }; + return { reactivePaths, bindingPaths }; }; export const getBindingOrConfigPathsForSortingControl = ( diff --git a/app/client/src/entities/DataTree/dataTreeAction.ts b/app/client/src/entities/DataTree/dataTreeAction.ts index ab528cae64..874dfa0032 100644 --- a/app/client/src/entities/DataTree/dataTreeAction.ts +++ b/app/client/src/entities/DataTree/dataTreeAction.ts @@ -2,7 +2,7 @@ import { DependencyMap, DynamicPath } from "utils/DynamicBindingUtils"; import { DataTreeAction, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { ActionData } from "reducers/entityReducers/actionsReducer"; import { - getBindingPathsOfAction, + getBindingAndReactivePathsOfAction, getDataTreeActionConfigPath, } from "entities/Action/actionProperties"; @@ -38,6 +38,11 @@ export const generateDataTreeAction = ( getDataTreeActionConfigPath, ); }); + + const { bindingPaths, reactivePaths } = getBindingAndReactivePathsOfAction( + action.config, + editorConfig, + ); return { run: {}, clear: {}, @@ -55,7 +60,8 @@ export const generateDataTreeAction = ( }, ENTITY_TYPE: ENTITY_TYPE.ACTION, isLoading: action.isLoading, - bindingPaths: getBindingPathsOfAction(action.config, editorConfig), + bindingPaths, + reactivePaths, dependencyMap, logBlackList: {}, datasourceUrl, diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index e0faba69d7..9315ea9bff 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -60,6 +60,7 @@ export interface DataTreeAction | Record; dynamicBindingPathList: DynamicPath[]; bindingPaths: Record; + reactivePaths: Record; ENTITY_TYPE: ENTITY_TYPE.ACTION; dependencyMap: DependencyMap; logBlackList: Record; @@ -75,6 +76,7 @@ export interface DataTreeJSAction { meta: Record; dynamicBindingPathList: DynamicPath[]; bindingPaths: Record; + reactivePaths: Record; variables: Array; dependencyMap: DependencyMap; } @@ -106,6 +108,7 @@ export type PropertyOverrideDependency = Record< export interface DataTreeWidget extends WidgetProps { bindingPaths: Record; + reactivePaths: Record; triggerPaths: Record; validationPaths: Record; ENTITY_TYPE: ENTITY_TYPE.WIDGET; diff --git a/app/client/src/entities/DataTree/dataTreeJSAction.ts b/app/client/src/entities/DataTree/dataTreeJSAction.ts index 886cbfa111..e9bc72401a 100644 --- a/app/client/src/entities/DataTree/dataTreeJSAction.ts +++ b/app/client/src/entities/DataTree/dataTreeJSAction.ts @@ -7,6 +7,8 @@ import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import { DependencyMap } from "utils/DynamicBindingUtils"; +const reg = /this\./g; + export const generateDataTreeJSAction = ( js: JSCollectionData, ): DataTreeJSAction => { @@ -17,7 +19,7 @@ export const generateDataTreeJSAction = ( const variables = js.config.variables; const listVariables: Array = []; dynamicBindingPathList.push({ key: "body" }); - const reg = /this\./g; + const removeThisReference = js.config.body.replace(reg, `${js.config.name}.`); bindingPaths["body"] = EvaluationSubstitutionType.SMART_SUBSTITUTE; @@ -56,7 +58,8 @@ export const generateDataTreeJSAction = ( ENTITY_TYPE: ENTITY_TYPE.JSACTION, body: removeThisReference, meta: meta, - bindingPaths: bindingPaths, + bindingPaths: bindingPaths, // As all js object function referred to as action is user javascript code, we add them as binding paths. + reactivePaths: { ...bindingPaths }, dynamicBindingPathList: dynamicBindingPathList, variables: listVariables, dependencyMap: dependencyMap, diff --git a/app/client/src/entities/DataTree/dataTreeWidget.test.ts b/app/client/src/entities/DataTree/dataTreeWidget.test.ts index 2acddfd7c8..a4fb85e407 100644 --- a/app/client/src/entities/DataTree/dataTreeWidget.test.ts +++ b/app/client/src/entities/DataTree/dataTreeWidget.test.ts @@ -196,19 +196,24 @@ describe("generateDataTreeWidget", () => { isDirty: true, }); + const bindingPaths = { + defaultText: EvaluationSubstitutionType.TEMPLATE, + placeholderText: EvaluationSubstitutionType.TEMPLATE, + regex: EvaluationSubstitutionType.TEMPLATE, + resetOnSubmit: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + isRequired: EvaluationSubstitutionType.TEMPLATE, + isDisabled: EvaluationSubstitutionType.TEMPLATE, + errorMessage: EvaluationSubstitutionType.TEMPLATE, + }; + const expected: DataTreeWidget = { - bindingPaths: { - defaultText: EvaluationSubstitutionType.TEMPLATE, - errorMessage: EvaluationSubstitutionType.TEMPLATE, + bindingPaths, + reactivePaths: { + ...bindingPaths, isDirty: EvaluationSubstitutionType.TEMPLATE, - isDisabled: EvaluationSubstitutionType.TEMPLATE, isFocused: EvaluationSubstitutionType.TEMPLATE, - isRequired: EvaluationSubstitutionType.TEMPLATE, isValid: EvaluationSubstitutionType.TEMPLATE, - isVisible: EvaluationSubstitutionType.TEMPLATE, - placeholderText: EvaluationSubstitutionType.TEMPLATE, - regex: EvaluationSubstitutionType.TEMPLATE, - resetOnSubmit: EvaluationSubstitutionType.TEMPLATE, text: EvaluationSubstitutionType.TEMPLATE, value: EvaluationSubstitutionType.TEMPLATE, "meta.text": EvaluationSubstitutionType.TEMPLATE, diff --git a/app/client/src/entities/DataTree/dataTreeWidget.ts b/app/client/src/entities/DataTree/dataTreeWidget.ts index efffed0843..93e24c86dc 100644 --- a/app/client/src/entities/DataTree/dataTreeWidget.ts +++ b/app/client/src/entities/DataTree/dataTreeWidget.ts @@ -106,6 +106,7 @@ export const generateDataTreeWidget = ( const { bindingPaths, + reactivePaths, triggerPaths, validationPaths, } = getAllPathsFromPropertyConfig(widget, propertyPaneConfigs, { @@ -152,6 +153,7 @@ export const generateDataTreeWidget = ( propertyOverrideDependency, overridingPropertyPaths, bindingPaths, + reactivePaths, triggerPaths, validationPaths, ENTITY_TYPE: ENTITY_TYPE.WIDGET, diff --git a/app/client/src/entities/Widget/utils.test.ts b/app/client/src/entities/Widget/utils.test.ts index 94641abb80..e80e21085b 100644 --- a/app/client/src/entities/Widget/utils.test.ts +++ b/app/client/src/entities/Widget/utils.test.ts @@ -117,8 +117,61 @@ describe("getAllPathsFromPropertyConfig", () => { }; const config = tablePropertyPaneConfig; + const bindingPaths = { + tableData: EvaluationSubstitutionType.SMART_SUBSTITUTE, + defaultSearchText: EvaluationSubstitutionType.TEMPLATE, + defaultSelectedRow: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + isSortable: EvaluationSubstitutionType.TEMPLATE, + animateLoading: EvaluationSubstitutionType.TEMPLATE, + primaryColumnId: EvaluationSubstitutionType.TEMPLATE, + compactMode: EvaluationSubstitutionType.TEMPLATE, + isVisibleDownload: EvaluationSubstitutionType.TEMPLATE, + isVisibleFilters: EvaluationSubstitutionType.TEMPLATE, + isVisiblePagination: EvaluationSubstitutionType.TEMPLATE, + isVisibleSearch: EvaluationSubstitutionType.TEMPLATE, + delimiter: EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.computedValue": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.horizontalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.verticalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.textSize": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.fontStyle": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.textColor": EvaluationSubstitutionType.TEMPLATE, + // "primaryColumns.name.isVisible": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.isCellVisible": EvaluationSubstitutionType.TEMPLATE, + + "primaryColumns.name.cellBackground": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.inputFormat": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.outputFormat": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.computedValue": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.isCellVisible": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.horizontalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.verticalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.textSize": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.fontStyle": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.textColor": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.cellBackground": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.buttonLabel": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.buttonColor": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.isDisabled": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.buttonVariant": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.isCellVisible": + EvaluationSubstitutionType.TEMPLATE, + }; const expected = { - bindingPaths: { + bindingPaths, + reactivePaths: { + ...bindingPaths, selectedRow: EvaluationSubstitutionType.TEMPLATE, selectedRows: EvaluationSubstitutionType.TEMPLATE, tableData: EvaluationSubstitutionType.SMART_SUBSTITUTE, @@ -171,8 +224,6 @@ describe("getAllPathsFromPropertyConfig", () => { EvaluationSubstitutionType.TEMPLATE, "primaryColumns.status.buttonLabel": EvaluationSubstitutionType.TEMPLATE, - "primaryColumns.status.buttonVariant": - EvaluationSubstitutionType.TEMPLATE, "primaryColumns.status.buttonColor": EvaluationSubstitutionType.TEMPLATE, "primaryColumns.status.isDisabled": EvaluationSubstitutionType.TEMPLATE, @@ -508,18 +559,21 @@ describe("getAllPathsFromPropertyConfig", () => { }; const config = chartPorpertyConfig; + const bindingPaths = { + chartType: EvaluationSubstitutionType.TEMPLATE, + chartName: EvaluationSubstitutionType.TEMPLATE, + "chartData.random-id.seriesName": EvaluationSubstitutionType.TEMPLATE, + "chartData.random-id.data": EvaluationSubstitutionType.SMART_SUBSTITUTE, + xAxisName: EvaluationSubstitutionType.TEMPLATE, + yAxisName: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + animateLoading: EvaluationSubstitutionType.TEMPLATE, + setAdaptiveYMin: EvaluationSubstitutionType.TEMPLATE, + }; + const expected = { - bindingPaths: { - chartType: EvaluationSubstitutionType.TEMPLATE, - chartName: EvaluationSubstitutionType.TEMPLATE, - "chartData.random-id.seriesName": EvaluationSubstitutionType.TEMPLATE, - "chartData.random-id.data": EvaluationSubstitutionType.SMART_SUBSTITUTE, - xAxisName: EvaluationSubstitutionType.TEMPLATE, - yAxisName: EvaluationSubstitutionType.TEMPLATE, - isVisible: EvaluationSubstitutionType.TEMPLATE, - animateLoading: EvaluationSubstitutionType.TEMPLATE, - setAdaptiveYMin: EvaluationSubstitutionType.TEMPLATE, - }, + bindingPaths, + reactivePaths: { ...bindingPaths }, triggerPaths: { onDataPointClick: true, }, diff --git a/app/client/src/entities/Widget/utils.ts b/app/client/src/entities/Widget/utils.ts index be9c2274aa..f7696aaf32 100644 --- a/app/client/src/entities/Widget/utils.ts +++ b/app/client/src/entities/Widget/utils.ts @@ -10,10 +10,21 @@ import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; /** * @typedef {Object} Paths * @property {Object} configBindingPaths - The Binding Path + * @property {Object} configReactivePaths - The Dynamic Property Path * @property {Object} configTriggerPaths - The Trigger Path * @property {Object} configValidationPaths - The Validation Path */ +/** + * All widget's property or paths where user can write javaScript bindings using mustache syntax are called bindingPaths. + * Widget's meta and derived paths aren't binding paths as user can't add or remove binding for those value. + */ +type BindingPaths = Record; +/** + * Binding paths and non-binding paths of widget/action together form reactivePaths. + */ +type ReactivePaths = Record; + /** * This function gets the binding validation and trigger paths from a config * @param config @@ -24,14 +35,14 @@ const checkPathsInConfig = ( config: any, path: string, ): { - configBindingPaths: Record; + configBindingPaths: BindingPaths; + configReactivePaths: ReactivePaths; configTriggerPaths: Record; configValidationPaths: Record; } => { - const configBindingPaths: Record = {}; + const configBindingPaths: BindingPaths = {}; const configTriggerPaths: Record = {}; const configValidationPaths: Record = {}; - // Purely a Binding Path if (config.isBindProperty && !config.isTriggerProperty) { configBindingPaths[path] = @@ -42,7 +53,12 @@ const checkPathsInConfig = ( } else if (config.isBindProperty && config.isTriggerProperty) { configTriggerPaths[path] = true; } - return { configBindingPaths, configTriggerPaths, configValidationPaths }; + return { + configBindingPaths, + configReactivePaths: configBindingPaths, // All bindingPaths are reactivePaths. + configTriggerPaths, + configValidationPaths, + }; }; // "originalWidget" param here always contains the complete widget props @@ -55,7 +71,9 @@ const childHasPanelConfig = ( ) => { const panelPropertyPath = config.propertyName; const widgetPanelPropertyValues = get(widget, panelPropertyPath); - let bindingPaths: Record = {}; + + let bindingPaths: BindingPaths = {}; + let reactivePaths: ReactivePaths = {}; let triggerPaths: Record = {}; let validationPaths: Record = {}; if (widgetPanelPropertyValues) { @@ -86,13 +104,21 @@ const childHasPanelConfig = ( if (!isControlHidden) { const { configBindingPaths, + configReactivePaths, configTriggerPaths, configValidationPaths, } = checkPathsInConfig( panelColumnControlConfig, panelPropertyConfigPath, ); - bindingPaths = { ...configBindingPaths, ...bindingPaths }; + bindingPaths = { + ...configBindingPaths, + ...bindingPaths, + }; + reactivePaths = { + ...configReactivePaths, + ...reactivePaths, + }; triggerPaths = { ...configTriggerPaths, ...triggerPaths }; validationPaths = { ...configValidationPaths, @@ -102,6 +128,7 @@ const childHasPanelConfig = ( if (panelColumnControlConfig.panelConfig) { const { bindingPaths: panelBindingPaths, + reactivePaths: panelReactivePaths, triggerPaths: panelTriggerPaths, validationPaths: panelValidationPaths, } = childHasPanelConfig( @@ -110,7 +137,14 @@ const childHasPanelConfig = ( panelPropertyConfigPath, originalWidget, ); - bindingPaths = { ...panelBindingPaths, ...bindingPaths }; + bindingPaths = { + ...panelBindingPaths, + ...bindingPaths, + }; + reactivePaths = { + ...panelReactivePaths, + ...reactivePaths, + }; triggerPaths = { ...panelTriggerPaths, ...triggerPaths }; validationPaths = { ...panelValidationPaths, @@ -126,7 +160,7 @@ const childHasPanelConfig = ( ); } - return { bindingPaths, triggerPaths, validationPaths }; + return { reactivePaths, triggerPaths, validationPaths, bindingPaths }; }; export const getAllPathsFromPropertyConfig = ( @@ -134,15 +168,16 @@ export const getAllPathsFromPropertyConfig = ( widgetConfig: readonly PropertyPaneConfig[], defaultProperties: Record, ): { - bindingPaths: Record; + bindingPaths: BindingPaths; + reactivePaths: ReactivePaths; triggerPaths: Record; validationPaths: Record; } => { - let bindingPaths: Record = {}; - Object.keys(defaultProperties).forEach( - (property) => - (bindingPaths[property] = EvaluationSubstitutionType.TEMPLATE), - ); + let bindingPaths: BindingPaths = {}; + let reactivePaths: ReactivePaths = {}; + Object.keys(defaultProperties).forEach((property) => { + reactivePaths[property] = EvaluationSubstitutionType.TEMPLATE; + }); let triggerPaths: Record = {}; let validationPaths: Record = {}; @@ -158,11 +193,19 @@ export const getAllPathsFromPropertyConfig = ( const path = controlConfig.propertyName; const { configBindingPaths, + configReactivePaths, configTriggerPaths, configValidationPaths, } = checkPathsInConfig(controlConfig, path); + bindingPaths = { + ...bindingPaths, + ...configBindingPaths, + }; // Update default path configs with the ones in the property config - bindingPaths = { ...bindingPaths, ...configBindingPaths }; + reactivePaths = { + ...reactivePaths, + ...configReactivePaths, + }; triggerPaths = { ...triggerPaths, ...configTriggerPaths }; validationPaths = { ...validationPaths, ...configValidationPaths }; } @@ -174,7 +217,14 @@ export const getAllPathsFromPropertyConfig = ( basePath, widget, ); - bindingPaths = { ...bindingPaths, ...resultingPaths.bindingPaths }; + bindingPaths = { + ...bindingPaths, + ...resultingPaths.bindingPaths, + }; + reactivePaths = { + ...reactivePaths, + ...resultingPaths.reactivePaths, + }; triggerPaths = { ...triggerPaths, ...resultingPaths.triggerPaths }; validationPaths = { ...validationPaths, @@ -195,13 +245,21 @@ export const getAllPathsFromPropertyConfig = ( const childArrayPropertyPath = `${objectIndexPropertyPath}.${childPropertyConfig.propertyName}`; const { configBindingPaths, + configReactivePaths, configTriggerPaths, configValidationPaths, } = checkPathsInConfig( childPropertyConfig, childArrayPropertyPath, ); - bindingPaths = { ...bindingPaths, ...configBindingPaths }; + bindingPaths = { + ...bindingPaths, + ...configBindingPaths, + }; + reactivePaths = { + ...reactivePaths, + ...configReactivePaths, + }; triggerPaths = { ...triggerPaths, ...configTriggerPaths }; validationPaths = { ...validationPaths, @@ -215,7 +273,7 @@ export const getAllPathsFromPropertyConfig = ( } }); - return { bindingPaths, triggerPaths, validationPaths }; + return { reactivePaths, triggerPaths, validationPaths, bindingPaths }; }; /** diff --git a/app/client/src/pages/Editor/FormControl.tsx b/app/client/src/pages/Editor/FormControl.tsx index 71d928bc3e..67ac485ff8 100644 --- a/app/client/src/pages/Editor/FormControl.tsx +++ b/app/client/src/pages/Editor/FormControl.tsx @@ -7,7 +7,7 @@ import { } from "components/formControls/utils"; import { useSelector, shallowEqual } from "react-redux"; import { getFormValues } from "redux-form"; -import FormControlFactory from "utils/FormControlFactory"; +import FormControlFactory from "utils/formControl/FormControlFactory"; import { AppState } from "reducers"; import { Action } from "entities/Action"; diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 0e60eb95ec..b61392005c 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -448,6 +448,7 @@ const createLoadingWidget = ( type: WidgetTypes.SKELETON_WIDGET, ENTITY_TYPE: ENTITY_TYPE.WIDGET, bindingPaths: {}, + reactivePaths: {}, triggerPaths: {}, validationPaths: {}, logBlackList: {}, diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index fee71c9757..152b231267 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -6,7 +6,7 @@ import { import { getAppsmithConfigs } from "@appsmith/configs"; import * as Sentry from "@sentry/react"; import AnalyticsUtil from "./AnalyticsUtil"; -import FormControlRegistry from "./FormControlRegistry"; +import FormControlRegistry from "./formControl/FormControlRegistry"; import { Property } from "api/ActionAPI"; import _ from "lodash"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; diff --git a/app/client/src/utils/DynamicBindingUtils.test.ts b/app/client/src/utils/DynamicBindingUtils.test.ts index d792438116..e875eb1f0c 100644 --- a/app/client/src/utils/DynamicBindingUtils.test.ts +++ b/app/client/src/utils/DynamicBindingUtils.test.ts @@ -1,5 +1,6 @@ import { Action, PluginType } from "entities/Action"; import _ from "lodash"; +import { getPropertyPath } from "./DynamicBindingUtils"; import { EVAL_VALUE_PATH, getDynamicBindingsChangesSaga, @@ -156,6 +157,23 @@ describe("DynamicBindingPathlist", () => { }); }); +describe("getPropertyPath function", () => { + it("test getPropertyPath", () => { + const testCases = [ + ["Table1.searchText", "searchText"], + ["Table1.selectedRow", "selectedRow"], + ["Table1.meta.searchText", "meta.searchText"], + ["Table1", "Table1"], + ["Table1.", ""], + ]; + + testCases.forEach(([input, expectedResult]) => { + const actualResult = getPropertyPath(input); + expect(actualResult).toStrictEqual(expectedResult); + }); + }); +}); + describe("getNestedEvalPath", () => { it("returns valid nested path", () => { const actualUnpopulatedNestedPath = getEvalValuePath( diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 0e751bad6c..8991b7382b 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -239,6 +239,15 @@ export const isPathADynamicBinding = ( } return false; }; +/** + * Get property path from full property path + * Input: "Table1.meta.searchText" => Output: "meta.searchText" + * @param {string} fullPropertyPath + * @return {*} + */ +export const getPropertyPath = (fullPropertyPath: string) => { + return fullPropertyPath.substring(fullPropertyPath.indexOf(".") + 1); +}; export const getWidgetDynamicTriggerPathList = ( widget: WidgetProps, diff --git a/app/client/src/utils/WidgetLoadingStateUtils.test.ts b/app/client/src/utils/WidgetLoadingStateUtils.test.ts index 52995d3b5a..0ef1781b90 100644 --- a/app/client/src/utils/WidgetLoadingStateUtils.test.ts +++ b/app/client/src/utils/WidgetLoadingStateUtils.test.ts @@ -19,6 +19,7 @@ const JS_object_tree: DataTreeJSAction = { meta: {}, dynamicBindingPathList: [], bindingPaths: {}, + reactivePaths: {}, variables: [], dependencyMap: {}, }; @@ -26,6 +27,7 @@ const JS_object_tree: DataTreeJSAction = { const Select_tree: DataTreeWidget = { ENTITY_TYPE: ENTITY_TYPE.WIDGET, bindingPaths: {}, + reactivePaths: {}, triggerPaths: {}, validationPaths: {}, logBlackList: {}, @@ -58,6 +60,7 @@ const Query_tree: DataTreeAction = { clear: {}, dynamicBindingPathList: [], bindingPaths: {}, + reactivePaths: {}, ENTITY_TYPE: ENTITY_TYPE.ACTION, dependencyMap: {}, logBlackList: {}, diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts index ba648195bd..b0799edfb3 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts @@ -31,6 +31,9 @@ describe("dataTreeTypeDefCreator", () => { bindingPaths: { defaultText: EvaluationSubstitutionType.TEMPLATE, }, + reactivePaths: { + defaultText: EvaluationSubstitutionType.TEMPLATE, + }, triggerPaths: { onTextChange: true, }, diff --git a/app/client/src/utils/FormControlFactory.tsx b/app/client/src/utils/formControl/FormControlFactory.tsx similarity index 100% rename from app/client/src/utils/FormControlFactory.tsx rename to app/client/src/utils/formControl/FormControlFactory.tsx diff --git a/app/client/src/utils/FormControlRegistry.tsx b/app/client/src/utils/formControl/FormControlRegistry.tsx similarity index 64% rename from app/client/src/utils/FormControlRegistry.tsx rename to app/client/src/utils/formControl/FormControlRegistry.tsx index d4fd124293..86b35b0892 100644 --- a/app/client/src/utils/FormControlRegistry.tsx +++ b/app/client/src/utils/formControl/FormControlRegistry.tsx @@ -45,50 +45,54 @@ import SortingControl, { import EntitySelectorControl, { EntitySelectorControlProps, } from "components/formControls/EntitySelectorControl"; +import formControlTypes from "./formControlTypes"; /** * NOTE: If you are adding a component that uses FormControl - * then add logic for creating bindingPaths in recursiveFindBindingPaths() at entities/Action/actionProperties.ts + * then add logic for creating reactivePaths in recursiveFindReactivePaths() at entities/Action/actionProperties.ts */ class FormControlRegistry { static registerFormControlBuilders() { - FormControlFactory.registerControlBuilder("INPUT_TEXT", { + FormControlFactory.registerControlBuilder(formControlTypes.INPUT_TEXT, { buildPropertyControl(controlProps: InputControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("FIXED_KEY_INPUT", { - buildPropertyControl( - controlProps: FixedKeyInputControlProps, - ): JSX.Element { - //TODO: may not be in use - return ; + FormControlFactory.registerControlBuilder( + formControlTypes.FIXED_KEY_INPUT, + { + buildPropertyControl( + controlProps: FixedKeyInputControlProps, + ): JSX.Element { + //TODO: may not be in use + return ; + }, }, - }); - FormControlFactory.registerControlBuilder("DROP_DOWN", { + ); + FormControlFactory.registerControlBuilder(formControlTypes.DROP_DOWN, { buildPropertyControl(controlProps: DropDownControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("SWITCH", { + FormControlFactory.registerControlBuilder(formControlTypes.SWITCH, { buildPropertyControl(controlProps: SwitchControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("KEYVALUE_ARRAY", { + FormControlFactory.registerControlBuilder(formControlTypes.KEYVALUE_ARRAY, { buildPropertyControl( controlProps: KeyValueArrayControlProps, ): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("FILE_PICKER", { + FormControlFactory.registerControlBuilder(formControlTypes.FILE_PICKER, { buildPropertyControl(controlProps: FilePickerControlProps): JSX.Element { //used by redshift datasource return ; }, }); - FormControlFactory.registerControlBuilder("KEY_VAL_INPUT", { + FormControlFactory.registerControlBuilder(formControlTypes.KEY_VAL_INPUT, { //TODO: may not be in use, replace it with KeyValueArrayControl buildPropertyControl( controlProps: KeyValueInputControlProps, @@ -96,58 +100,67 @@ class FormControlRegistry { return ; }, }); - FormControlFactory.registerControlBuilder("QUERY_DYNAMIC_TEXT", { - buildPropertyControl(controlProps: DynamicTextFieldProps): JSX.Element { - return ; + FormControlFactory.registerControlBuilder( + formControlTypes.QUERY_DYNAMIC_TEXT, + { + buildPropertyControl(controlProps: DynamicTextFieldProps): JSX.Element { + return ; + }, }, - }); - FormControlFactory.registerControlBuilder("QUERY_DYNAMIC_INPUT_TEXT", { - buildPropertyControl( - controlProps: DynamicInputControlProps, - ): JSX.Element { - return ; + ); + FormControlFactory.registerControlBuilder( + formControlTypes.QUERY_DYNAMIC_INPUT_TEXT, + { + buildPropertyControl( + controlProps: DynamicInputControlProps, + ): JSX.Element { + return ; + }, }, - }); - FormControlFactory.registerControlBuilder("CHECKBOX", { + ); + FormControlFactory.registerControlBuilder(formControlTypes.CHECKBOX, { buildPropertyControl(controlProps: CheckboxControlProps): JSX.Element { //used in API datasource form only return ; }, }); - FormControlFactory.registerControlBuilder("NUMBER_INPUT", { + FormControlFactory.registerControlBuilder(formControlTypes.NUMBER_INPUT, { buildPropertyControl(controlProps: InputControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("ARRAY_FIELD", { + FormControlFactory.registerControlBuilder(formControlTypes.ARRAY_FIELD, { buildPropertyControl(controlProps: FieldArrayControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("WHERE_CLAUSE", { + FormControlFactory.registerControlBuilder(formControlTypes.WHERE_CLAUSE, { buildPropertyControl(controlProps: WhereClauseControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("ENTITY_SELECTOR", { - buildPropertyControl( - controlProps: EntitySelectorControlProps, - ): JSX.Element { - return ; + FormControlFactory.registerControlBuilder( + formControlTypes.ENTITY_SELECTOR, + { + buildPropertyControl( + controlProps: EntitySelectorControlProps, + ): JSX.Element { + return ; + }, }, - }); + ); - FormControlFactory.registerControlBuilder("PAGINATION", { + FormControlFactory.registerControlBuilder(formControlTypes.PAGINATION, { buildPropertyControl(controlProps: PaginationControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("SORTING", { + FormControlFactory.registerControlBuilder(formControlTypes.SORTING, { buildPropertyControl(controlProps: SortingControlProps): JSX.Element { return ; }, }); - FormControlFactory.registerControlBuilder("PROJECTION", { + FormControlFactory.registerControlBuilder(formControlTypes.PROJECTION, { buildPropertyControl(controlProps: DropDownControlProps): JSX.Element { return ( Tree) => { } return { ...mapped }; }; + +/** + * This function sorts the object's value which is array of string. + * + * @param {Record>} data + * @return {*} + */ +export const sortObjectWithArray = (data: Record>) => { + Object.entries(data).map(([key, value]) => { + data[key] = value.sort(); + }); + return data; +}; diff --git a/app/client/src/workers/Actions.test.ts b/app/client/src/workers/Actions.test.ts index d1e95ba75f..2c83b2bebc 100644 --- a/app/client/src/workers/Actions.test.ts +++ b/app/client/src/workers/Actions.test.ts @@ -16,6 +16,7 @@ describe("Add functions", () => { dynamicBindingPathList: [], name: "action1", bindingPaths: {}, + reactivePaths: {}, isLoading: false, run: {}, clear: {}, diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator/DataTreeEvaluator.ts similarity index 98% rename from app/client/src/workers/DataTreeEvaluator.ts rename to app/client/src/workers/DataTreeEvaluator/DataTreeEvaluator.ts index 362f7ee754..03fa6120a9 100644 --- a/app/client/src/workers/DataTreeEvaluator.ts +++ b/app/client/src/workers/DataTreeEvaluator/DataTreeEvaluator.ts @@ -7,6 +7,7 @@ import { getEntityDynamicBindingPathList, getEvalErrorPath, getEvalValuePath, + getPropertyPath, isChildPropertyPath, isPathADynamicBinding, isPathADynamicTrigger, @@ -76,7 +77,7 @@ import { JSUpdate } from "utils/JSPaneUtils"; import { addWidgetPropertyDependencies, overrideWidgetProperties, -} from "./evaluationUtils"; +} from "../evaluationUtils"; import { ActionValidationConfigMap, ValidationConfig, @@ -292,19 +293,21 @@ export default class DataTreeEvaluator { translateDiffEventToDataTreeDiffEvent(diff, localUnEvalTree), ), ); - this.logs.push({ differences, translatedDiffs }); + this.logs.push({ + differences, + translatedDiffs, + }); const diffCheckTimeStop = performance.now(); // Check if dependencies have changed const updateDependenciesStart = performance.now(); - this.logs.push({ differences: clone(differences), translatedDiffs }); - // Find all the paths that have changed as part of the difference and update the // global dependency map if an existing dynamic binding has now become legal const { dependenciesOfRemovedPaths, removedPaths, } = this.updateDependencyMap(translatedDiffs, localUnEvalTree); + const updateDependenciesStop = performance.now(); this.applyDifferencesToEvalTree(differences); @@ -560,8 +563,8 @@ export default class DataTreeEvaluator { } if (isJSAction(entity)) { // making functions dependent on their function body entities - if (entity.bindingPaths) { - Object.keys(entity.bindingPaths).forEach((propertyPath) => { + if (entity.reactivePaths) { + Object.keys(entity.reactivePaths).forEach((propertyPath) => { const existingDeps = dependencies[`${entityName}.${propertyPath}`] || []; const unevalPropValue = _.get(entity, propertyPath).toString(); @@ -613,14 +616,14 @@ export default class DataTreeEvaluator { fullPropertyPath, ); - const isABindingPath = + const isADynamicBindingPath = (isAction(entity) || isWidget(entity) || isJSAction(entity)) && isPathADynamicBinding(entity, propertyPath); const isATriggerPath = isWidget(entity) && isPathADynamicTrigger(entity, propertyPath); let evalPropertyValue; const requiresEval = - isABindingPath && + isADynamicBindingPath && !isATriggerPath && (isDynamicValue(unEvalPropertyValue) || isJSAction(entity)); if (propertyPath) { @@ -628,7 +631,7 @@ export default class DataTreeEvaluator { } if (requiresEval) { const evaluationSubstitutionType = - entity.bindingPaths[propertyPath] || + entity.reactivePaths[propertyPath] || EvaluationSubstitutionType.TEMPLATE; const contextData: EvaluateContext = {}; @@ -1330,14 +1333,12 @@ export default class DataTreeEvaluator { | DataTreeWidget | DataTreeJSAction; const fullPropertyPath = dataTreeDiff.payload.propertyPath; - const entityPropertyPath = fullPropertyPath.substring( - fullPropertyPath.indexOf(".") + 1, - ); - const isABindingPath = isPathADynamicBinding( + const entityPropertyPath = getPropertyPath(fullPropertyPath); + const isADynamicBindingPath = isPathADynamicBinding( entity, entityPropertyPath, ); - if (isABindingPath) { + if (isADynamicBindingPath) { didUpdateDependencyMap = true; const { jsSnippets } = getDynamicBindings( @@ -1381,11 +1382,12 @@ export default class DataTreeEvaluator { } } } - // If the whole binding was removed then the value - // at this path would be "". - // In this case if the path exists in the dependency map - // remove it. - else if (fullPropertyPath in this.dependencyMap) { + // If the whole binding was removed, then the value at this path would be a string without any bindings. + // In this case, if the path exists in the dependency map and is a bindingPath, then remove it. + else if ( + entity.bindingPaths[entityPropertyPath] && + fullPropertyPath in this.dependencyMap + ) { didUpdateDependencyMap = true; delete this.dependencyMap[fullPropertyPath]; } @@ -1501,7 +1503,7 @@ export default class DataTreeEvaluator { ); } const parentPropertyPath = convertPathToString(d.path); - Object.keys(entity.bindingPaths).forEach((relativePath) => { + Object.keys(entity.reactivePaths).forEach((relativePath) => { const childPropertyPath = `${entityName}.${relativePath}`; // Check if relative path has dynamic binding if ( diff --git a/app/client/src/workers/DataTreeEvaluator.test.ts b/app/client/src/workers/DataTreeEvaluator/test/DataTreeEvaluator.test.ts similarity index 52% rename from app/client/src/workers/DataTreeEvaluator.test.ts rename to app/client/src/workers/DataTreeEvaluator/test/DataTreeEvaluator.test.ts index 77c158e0cf..fb70be1234 100644 --- a/app/client/src/workers/DataTreeEvaluator.test.ts +++ b/app/client/src/workers/DataTreeEvaluator/test/DataTreeEvaluator.test.ts @@ -1,10 +1,27 @@ -import DataTreeEvaluator from "./DataTreeEvaluator"; +import DataTreeEvaluator from "../DataTreeEvaluator"; +import { unEvalTree } from "./mockUnEvalTree"; +import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { DataTreeDiff } from "workers/evaluationUtils"; +import { ALL_WIDGETS_AND_CONFIG } from "utils/WidgetRegistry"; + +const widgetConfigMap = {}; +ALL_WIDGETS_AND_CONFIG.map(([, config]) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + if (config.type && config.properties) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + widgetConfigMap[config.type] = { + defaultProperties: config.properties.default, + derivedProperties: config.properties.derived, + metaProperties: config.properties.meta, + }; + } +}); + +const dataTreeEvaluator = new DataTreeEvaluator(widgetConfigMap); describe("DataTreeEvaluator", () => { - let dataTreeEvaluator: DataTreeEvaluator; - beforeAll(() => { - dataTreeEvaluator = new DataTreeEvaluator({}); - }); describe("evaluateActionBindings", () => { it("handles this.params.property", () => { const result = dataTreeEvaluator.evaluateActionBindings( @@ -106,4 +123,67 @@ describe("DataTreeEvaluator", () => { ]); }); }); + + describe("test updateDependencyMap", () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + dataTreeEvaluator.createFirstTree(unEvalTree as DataTree); + }); + + it("initial dependencyMap computation", () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + dataTreeEvaluator.updateDataTree(unEvalTree as DataTree); + + expect(dataTreeEvaluator.dependencyMap).toStrictEqual({ + "Button2.text": ["Button1.text"], + Button2: ["Button2.text"], + Button1: ["Button1.text"], + }); + }); + + it(`When empty binding is modified from {{Button1.text}} to {{""}}`, () => { + const translatedDiffs = [ + { + payload: { + propertyPath: "Button2.text", + value: '{{""}}', + }, + event: "EDIT", + }, + ]; + dataTreeEvaluator.updateDependencyMap( + translatedDiffs as Array, + dataTreeEvaluator.oldUnEvalTree, + ); + + expect(dataTreeEvaluator.dependencyMap).toStrictEqual({ + "Button2.text": [], + Button2: ["Button2.text"], + Button1: ["Button1.text"], + }); + }); + + it(`When binding is removed`, () => { + const translatedDiffs = [ + { + payload: { + propertyPath: "Button2.text", + value: "abc", + }, + event: "EDIT", + }, + ]; + dataTreeEvaluator.updateDependencyMap( + translatedDiffs as Array, + dataTreeEvaluator.oldUnEvalTree, + ); + + expect(dataTreeEvaluator.dependencyMap).toStrictEqual({ + Button2: ["Button2.text"], + Button1: ["Button1.text"], + }); + }); + }); }); diff --git a/app/client/src/workers/DataTreeEvaluator/test/mockUnEvalTree.ts b/app/client/src/workers/DataTreeEvaluator/test/mockUnEvalTree.ts new file mode 100644 index 0000000000..42e3130b1c --- /dev/null +++ b/app/client/src/workers/DataTreeEvaluator/test/mockUnEvalTree.ts @@ -0,0 +1,293 @@ +export const unEvalTree = { + MainContainer: { + widgetName: "MainContainer", + backgroundColor: "none", + rightColumn: 2220, + snapColumns: 64, + detachFromLayout: true, + widgetId: "0", + topRow: 0, + bottomRow: 640, + containerStyle: "none", + snapRows: 113, + parentRowSpace: 1, + type: "CANVAS_WIDGET", + canExtend: true, + version: 52, + minHeight: 620, + parentColumnSpace: 1, + dynamicBindingPathList: [], + leftColumn: 0, + children: ["j9dpft2lpu", "l0yem4eh6l"], + defaultProps: {}, + defaultMetaProps: [], + logBlackList: {}, + meta: {}, + propertyOverrideDependency: {}, + overridingPropertyPaths: {}, + reactivePaths: {}, + triggerPaths: {}, + validationPaths: {}, + ENTITY_TYPE: "WIDGET", + privateWidgets: {}, + }, + Button1: { + widgetName: "Button1", + buttonColor: "#03B365", + displayName: "Button", + iconSVG: "/static/media/icon.cca02633.svg", + topRow: 15, + bottomRow: 19, + parentRowSpace: 10, + type: "BUTTON_WIDGET", + hideCard: false, + animateLoading: true, + parentColumnSpace: 26.421875, + dynamicTriggerPathList: [], + leftColumn: 20, + dynamicBindingPathList: [], + text: "button1", + isDisabled: false, + key: "r6h8y6dc8i", + rightColumn: 36, + isDefaultClickDisabled: true, + widgetId: "j9dpft2lpu", + isVisible: true, + recaptchaType: "V3", + version: 1, + parentId: "0", + renderMode: "CANVAS", + isLoading: false, + buttonVariant: "PRIMARY", + placement: "CENTER", + defaultProps: {}, + defaultMetaProps: ["recaptchaToken"], + logBlackList: {}, + meta: {}, + propertyOverrideDependency: {}, + overridingPropertyPaths: {}, + reactivePaths: { + recaptchaToken: "TEMPLATE", + text: "TEMPLATE", + tooltip: "TEMPLATE", + googleRecaptchaKey: "TEMPLATE", + recaptchaType: "TEMPLATE", + isVisible: "TEMPLATE", + isDisabled: "TEMPLATE", + animateLoading: "TEMPLATE", + buttonVariant: "TEMPLATE", + placement: "TEMPLATE", + }, + triggerPaths: { + onClick: true, + }, + validationPaths: { + text: { + type: "TEXT", + }, + tooltip: { + type: "TEXT", + }, + googleRecaptchaKey: { + type: "TEXT", + }, + recaptchaType: { + type: "TEXT", + params: { + allowedValues: ["V3", "V2"], + default: "V3", + }, + }, + isVisible: { + type: "BOOLEAN", + }, + isDisabled: { + type: "BOOLEAN", + }, + animateLoading: { + type: "BOOLEAN", + }, + buttonVariant: { + type: "TEXT", + params: { + allowedValues: ["PRIMARY", "SECONDARY", "TERTIARY"], + default: "PRIMARY", + }, + }, + placement: { + type: "TEXT", + params: { + allowedValues: ["START", "BETWEEN", "CENTER"], + default: "CENTER", + }, + }, + }, + ENTITY_TYPE: "WIDGET", + privateWidgets: {}, + }, + Button2: { + widgetName: "Button2", + buttonColor: "#03B365", + displayName: "Button", + iconSVG: "/static/media/icon.cca02633.svg", + topRow: 25, + bottomRow: 29, + parentRowSpace: 10, + type: "BUTTON_WIDGET", + hideCard: false, + animateLoading: true, + parentColumnSpace: 26.421875, + dynamicTriggerPathList: [], + leftColumn: 20, + dynamicBindingPathList: [ + { + key: "text", + }, + ], + text: "{{Button1.text}}", + isDisabled: false, + key: "r6h8y6dc8i", + rightColumn: 36, + isDefaultClickDisabled: true, + widgetId: "l0yem4eh6l", + isVisible: true, + recaptchaType: "V3", + version: 1, + parentId: "0", + renderMode: "CANVAS", + isLoading: false, + buttonVariant: "PRIMARY", + placement: "CENTER", + defaultProps: {}, + defaultMetaProps: ["recaptchaToken"], + logBlackList: {}, + meta: {}, + propertyOverrideDependency: {}, + overridingPropertyPaths: {}, + reactivePaths: { + recaptchaToken: "TEMPLATE", + text: "TEMPLATE", + tooltip: "TEMPLATE", + googleRecaptchaKey: "TEMPLATE", + recaptchaType: "TEMPLATE", + isVisible: "TEMPLATE", + isDisabled: "TEMPLATE", + animateLoading: "TEMPLATE", + buttonVariant: "TEMPLATE", + placement: "TEMPLATE", + }, + triggerPaths: { + onClick: true, + }, + validationPaths: { + text: { + type: "TEXT", + }, + tooltip: { + type: "TEXT", + }, + googleRecaptchaKey: { + type: "TEXT", + }, + recaptchaType: { + type: "TEXT", + params: { + allowedValues: ["V3", "V2"], + default: "V3", + }, + }, + isVisible: { + type: "BOOLEAN", + }, + isDisabled: { + type: "BOOLEAN", + }, + animateLoading: { + type: "BOOLEAN", + }, + buttonVariant: { + type: "TEXT", + params: { + allowedValues: ["PRIMARY", "SECONDARY", "TERTIARY"], + default: "PRIMARY", + }, + }, + placement: { + type: "TEXT", + params: { + allowedValues: ["START", "BETWEEN", "CENTER"], + default: "CENTER", + }, + }, + }, + ENTITY_TYPE: "WIDGET", + privateWidgets: {}, + }, + pageList: [ + { + pageName: "Page1", + pageId: "6200d1a2b5bfc0392b959cae", + isDefault: true, + isHidden: false, + }, + { + pageName: "Page2", + pageId: "621e22cf2b75295c1c165fa6", + isDefault: false, + isHidden: false, + }, + { + pageName: "Page3", + pageId: "6220c268c48234070f8ac65a", + isDefault: false, + isHidden: false, + }, + ], + appsmith: { + user: { + email: "rathod@appsmith.com", + organizationIds: [ + "6218a61972ccd9145ec78c57", + "621913df0276eb01d22fec44", + "60caf8edb1e47a1315f0c48f", + "609114fe05c4d35a9f6cbbf2", + ], + username: "rathod@appsmith.com", + name: "Rishabh", + commentOnboardingState: "RESOLVED", + role: "engineer", + useCase: "personal project", + enableTelemetry: false, + emptyInstance: false, + accountNonExpired: true, + accountNonLocked: true, + credentialsNonExpired: true, + isAnonymous: false, + isEnabled: true, + isSuperUser: false, + isConfigurable: true, + }, + URL: { + fullPath: + "https://dev.appsmith.com/applications/6200d1a2b5bfc0392b959cab/pages/6220c268c48234070f8ac65a/edit?a=b", + host: "dev.appsmith.com", + hostname: "dev.appsmith.com", + queryParams: { + a: "b", + }, + protocol: "https:", + pathname: + "/applications/6200d1a2b5bfc0392b959cab/pages/6220c268c48234070f8ac65a/edit", + port: "", + hash: "", + }, + store: { + textColor: "#DF7E65", + }, + geolocation: { + canBeRequested: true, + }, + mode: "EDIT", + ENTITY_TYPE: "APPSMITH", + }, +}; diff --git a/app/client/src/workers/evaluate.test.ts b/app/client/src/workers/evaluate.test.ts index 19819a4793..33fcfc7b5c 100644 --- a/app/client/src/workers/evaluate.test.ts +++ b/app/client/src/workers/evaluate.test.ts @@ -27,6 +27,7 @@ describe("evaluateSync", () => { text: "value", ENTITY_TYPE: ENTITY_TYPE.WIDGET, bindingPaths: {}, + reactivePaths: {}, triggerPaths: {}, validationPaths: {}, logBlackList: {}, diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts index 07981660f4..de185c5529 100644 --- a/app/client/src/workers/evaluation.test.ts +++ b/app/client/src/workers/evaluation.test.ts @@ -7,23 +7,11 @@ import { import { WidgetTypeConfigMap } from "utils/WidgetFactory"; import { RenderModes } from "constants/WidgetConstants"; import { PluginType } from "entities/Action"; -import DataTreeEvaluator from "workers/DataTreeEvaluator"; +import DataTreeEvaluator from "workers/DataTreeEvaluator/DataTreeEvaluator"; import { ValidationTypes } from "constants/WidgetValidation"; import WidgetFactory from "utils/WidgetFactory"; import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget"; - -/** - * This function sorts the object's value which is array of string. - * - * @param {Record>} data - * @return {*} - */ -const sortObject = (data: Record>) => { - Object.entries(data).map(([key, value]) => { - data[key] = value.sort(); - }); - return data; -}; +import { sortObjectWithArray } from "../utils/treeUtils"; const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = { CONTAINER_WIDGET: { @@ -230,6 +218,7 @@ const BASE_WIDGET: DataTreeWidget = { parentId: "0", version: 1, bindingPaths: {}, + reactivePaths: {}, triggerPaths: {}, validationPaths: {}, ENTITY_TYPE: ENTITY_TYPE.WIDGET, @@ -254,7 +243,8 @@ const BASE_ACTION: DataTreeAction = { data: {}, responseMeta: { isExecutionSuccess: false }, ENTITY_TYPE: ENTITY_TYPE.ACTION, - bindingPaths: { + bindingPaths: {}, + reactivePaths: { isLoading: EvaluationSubstitutionType.TEMPLATE, data: EvaluationSubstitutionType.TEMPLATE, }, @@ -334,7 +324,7 @@ describe("DataTreeEvaluator", () => { defaultText: "Default value", widgetName: "Input1", type: "INPUT_WIDGET_V2", - bindingPaths: { + reactivePaths: { defaultText: EvaluationSubstitutionType.TEMPLATE, isValid: EvaluationSubstitutionType.TEMPLATE, value: EvaluationSubstitutionType.TEMPLATE, @@ -406,7 +396,7 @@ describe("DataTreeEvaluator", () => { text: "{{Table1.selectedRow.test}}", dynamicBindingPathList: [{ key: "text" }], type: "TEXT_WIDGET", - bindingPaths: { + reactivePaths: { text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { @@ -424,7 +414,7 @@ describe("DataTreeEvaluator", () => { expect(evaluation).toHaveProperty("Text2.text", "Label"); expect(evaluation).toHaveProperty("Text3.text", "Label"); - expect(sortObject(dependencyMap)).toStrictEqual(dependencyMap); + expect(sortObjectWithArray(dependencyMap)).toStrictEqual(dependencyMap); }); it("Evaluates a value change in update run", () => { @@ -455,7 +445,9 @@ describe("DataTreeEvaluator", () => { expect(dataTree).toHaveProperty("Text2.text", "Label"); expect(dataTree).toHaveProperty("Text3.text", "Label 3"); - expect(sortObject(updatedDependencyMap)).toStrictEqual(dependencyMap); + expect(sortObjectWithArray(updatedDependencyMap)).toStrictEqual( + dependencyMap, + ); }); it("Overrides with default value", () => { @@ -470,6 +462,13 @@ describe("DataTreeEvaluator", () => { }); it("Evaluates for value changes in nested diff paths", () => { + const bindingPaths = { + options: EvaluationSubstitutionType.TEMPLATE, + defaultOptionValue: EvaluationSubstitutionType.TEMPLATE, + isRequired: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + isDisabled: EvaluationSubstitutionType.TEMPLATE, + }; const updatedUnEvalTree = { ...unEvalTree, Dropdown2: { @@ -485,12 +484,9 @@ describe("DataTreeEvaluator", () => { }, ], type: "SELECT_WIDGET", - bindingPaths: { - options: EvaluationSubstitutionType.TEMPLATE, - defaultOptionValue: EvaluationSubstitutionType.TEMPLATE, - isRequired: EvaluationSubstitutionType.TEMPLATE, - isVisible: EvaluationSubstitutionType.TEMPLATE, - isDisabled: EvaluationSubstitutionType.TEMPLATE, + bindingPaths, + reactivePaths: { + ...bindingPaths, isValid: EvaluationSubstitutionType.TEMPLATE, selectedOption: EvaluationSubstitutionType.TEMPLATE, selectedOptionValue: EvaluationSubstitutionType.TEMPLATE, @@ -533,7 +529,7 @@ describe("DataTreeEvaluator", () => { }, ]); - expect(sortObject(updatedDependencyMap)).toStrictEqual({ + expect(sortObjectWithArray(updatedDependencyMap)).toStrictEqual({ Api1: ["Api1.data"], ...dependencyMap, "Table1.tableData": ["Api1.data", "Text1.text"], @@ -579,7 +575,7 @@ describe("DataTreeEvaluator", () => { }, ]); expect(dataTree).toHaveProperty("Text4.text", "Hey"); - expect(sortObject(updatedDependencyMap)).toStrictEqual({ + expect(sortObjectWithArray(updatedDependencyMap)).toStrictEqual({ Api1: ["Api1.data"], ...dependencyMap, "Table1.tableData": ["Api1.data", "Text1.text"], @@ -599,8 +595,8 @@ describe("DataTreeEvaluator", () => { dependencyMap: { "config.body": ["config.pluginSpecifiedTemplates[0].value"], }, - bindingPaths: { - ...BASE_ACTION.bindingPaths, + reactivePaths: { + ...BASE_ACTION.reactivePaths, "config.body": EvaluationSubstitutionType.TEMPLATE, }, config: { @@ -646,8 +642,8 @@ describe("DataTreeEvaluator", () => { ...updatedTree2, Api2: { ...updatedTree2.Api2, - bindingPaths: { - ...updatedTree2.Api2.bindingPaths, + reactivePaths: { + ...updatedTree2.Api2.reactivePaths, "config.body": EvaluationSubstitutionType.SMART_SUBSTITUTE, }, config: { diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts index e21718ffd5..6eb99cd30f 100644 --- a/app/client/src/workers/evaluation.worker.ts +++ b/app/client/src/workers/evaluation.worker.ts @@ -14,7 +14,7 @@ import { removeFunctions, validateWidgetProperty, } from "./evaluationUtils"; -import DataTreeEvaluator from "workers/DataTreeEvaluator"; +import DataTreeEvaluator from "workers/DataTreeEvaluator/DataTreeEvaluator"; import ReplayEntity from "entities/Replay"; import evaluate, { evaluateAsync, diff --git a/app/client/src/workers/evaluationUtils.test.ts b/app/client/src/workers/evaluationUtils.test.ts index d88d91a807..67c15c675e 100644 --- a/app/client/src/workers/evaluationUtils.test.ts +++ b/app/client/src/workers/evaluationUtils.test.ts @@ -41,6 +41,7 @@ const BASE_WIDGET: DataTreeWidget = { parentId: "0", version: 1, bindingPaths: {}, + reactivePaths: {}, triggerPaths: {}, validationPaths: {}, ENTITY_TYPE: ENTITY_TYPE.WIDGET, @@ -55,7 +56,7 @@ const testDataTree: Record = { widgetName: "Text1", text: "Label", type: "TEXT_WIDGET", - bindingPaths: { + reactivePaths: { text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { @@ -68,7 +69,7 @@ const testDataTree: Record = { text: "{{Text1.text}}", dynamicBindingPathList: [{ key: "text" }], type: "TEXT_WIDGET", - bindingPaths: { + reactivePaths: { text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { @@ -81,7 +82,7 @@ const testDataTree: Record = { text: "{{Text1.text}}", dynamicBindingPathList: [{ key: "text" }], type: "TEXT_WIDGET", - bindingPaths: { + reactivePaths: { text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { @@ -94,7 +95,7 @@ const testDataTree: Record = { text: "{{Text1.text}}", dynamicBindingPathList: [{ key: "text" }], type: "TEXT_WIDGET", - bindingPaths: { + reactivePaths: { text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { @@ -198,7 +199,7 @@ describe("privateWidgets", () => { widgetName: "Text1", text: "Label", type: "TEXT_WIDGET", - bindingPaths: { + reactivePaths: { text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { @@ -212,7 +213,7 @@ describe("privateWidgets", () => { text: "{{Text1.text}}", dynamicBindingPathList: [{ key: "text" }], type: "TEXT_WIDGET", - bindingPaths: { + reactivePaths: { text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { diff --git a/app/client/src/workers/evaluationUtils.ts b/app/client/src/workers/evaluationUtils.ts index 502b615b8b..345c3268b4 100644 --- a/app/client/src/workers/evaluationUtils.ts +++ b/app/client/src/workers/evaluationUtils.ts @@ -96,7 +96,7 @@ export function getEntityNameAndPropertyPath( //these paths are not required to go through evaluate tree as these are internal properties const ignorePathsForEvalRegex = - ".(bindingPaths|triggerPaths|validationPaths|dynamicBindingPathList)"; + ".(reactivePaths|bindingPaths|triggerPaths|validationPaths|dynamicBindingPathList)"; //match if paths are part of ignorePathsForEvalRegex const isUninterestingChangeForDependencyUpdate = (path: string) => { @@ -602,7 +602,7 @@ export const isDynamicLeaf = (unEvalTree: DataTree, propertyPath: string) => { return false; const relativePropertyPath = convertPathToString(propPathEls); return ( - relativePropertyPath in entity.bindingPaths || + relativePropertyPath in entity.reactivePaths || (isWidget(entity) && relativePropertyPath in entity.triggerPaths) ); }; @@ -642,14 +642,15 @@ export const updateJSCollectionInDataTree = ( ); } } else { - const bindingPaths = jsCollection.bindingPaths; - bindingPaths[action.name] = EvaluationSubstitutionType.SMART_SUBSTITUTE; - bindingPaths[`${action.name}.data`] = + const reactivePaths = jsCollection.reactivePaths; + reactivePaths[action.name] = + EvaluationSubstitutionType.SMART_SUBSTITUTE; + reactivePaths[`${action.name}.data`] = EvaluationSubstitutionType.TEMPLATE; _.set( modifiedDataTree, - `${jsCollection.name}.bindingPaths`, - bindingPaths, + `${jsCollection.name}.reactivePaths`, + reactivePaths, ); const dynamicBindingPathList = jsCollection.dynamicBindingPathList; dynamicBindingPathList.push({ key: action.name }); @@ -697,12 +698,12 @@ export const updateJSCollectionInDataTree = ( (js: ParsedJSSubAction) => js.name === preAction, ); if (!existed) { - const bindingPaths = jsCollection.bindingPaths; - delete bindingPaths[preAction]; + const reactivePaths = jsCollection.reactivePaths; + delete reactivePaths[preAction]; _.set( modifiedDataTree, - `${jsCollection.name}.bindingPaths`, - bindingPaths, + `${jsCollection.name}.reactivePaths`, + reactivePaths, ); let dynamicBindingPathList = jsCollection.dynamicBindingPathList; dynamicBindingPathList = dynamicBindingPathList.filter( @@ -796,12 +797,12 @@ export const removeFunctionsAndVariableJSCollection = ( } //remove functions let dynamicBindingPathList = entity.dynamicBindingPathList; - const bindingPaths = entity.bindingPaths; + const reactivePaths = entity.reactivePaths; const meta = entity.meta; let dependencyMap = entity.dependencyMap["body"]; for (let i = 0; i < functionsList.length; i++) { const actionName = functionsList[i]; - delete bindingPaths[actionName]; + delete reactivePaths[actionName]; delete meta[actionName]; delete modifiedDataTree[`${entity.name}`][`${actionName}`]; dynamicBindingPathList = dynamicBindingPathList.filter( @@ -809,7 +810,7 @@ export const removeFunctionsAndVariableJSCollection = ( ); dependencyMap = dependencyMap.filter((item: any) => item !== actionName); } - _.set(modifiedDataTree, `${entity.name}.bindingPaths`, bindingPaths); + _.set(modifiedDataTree, `${entity.name}.reactivePaths`, reactivePaths); _.set( modifiedDataTree, `${entity.name}.dynamicBindingPathList`,