From 5231d307b8cb232f1d987c4ce38f71c33e764e57 Mon Sep 17 00:00:00 2001 From: Favour Ohanekwu Date: Fri, 13 Jan 2023 19:29:03 +0100 Subject: [PATCH] feat: meta rehydration (#19683) The introduction of MetaWidgets in https://github.com/appsmithorg/appsmith/pull/15839 introduces a scenario where widgets are newly added to the unevalTree but already have defined meta values. These previously defined meta values have higher priority and should not get overridden by default values. Fixes https://github.com/appsmithorg/appsmith/issues/16926 ## Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) ## How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Provide instructions, so we can reproduce. > Please also list any relevant details for your test configuration. > Delete anything that is not important - Manual - Jest - Cypress ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --- app/client/src/actions/metaActions.ts | 11 ++++ .../src/ce/constants/ReduxActionConstants.tsx | 1 + .../Evaluation/evaluationUtils.test.ts | 4 ++ .../ce/workers/Evaluation/evaluationUtils.ts | 59 ++++++++++++++++++- .../src/entities/DataTree/dataTreeWidget.ts | 8 ++- app/client/src/entities/DataTree/types.ts | 9 +-- app/client/src/entities/DataTree/utils.ts | 16 +++++ .../entityReducers/metaReducer/index.ts | 12 ++++ app/client/src/sagas/EvaluationsSaga.ts | 10 +++- .../Evaluation/__tests__/evaluation.test.ts | 49 ++++++++++++--- .../workers/Evaluation/handlers/evalTree.ts | 12 +++- .../Evaluation/handlers/evalTrigger.ts | 2 + app/client/src/workers/Evaluation/types.ts | 1 + .../workers/common/DataTreeEvaluator/index.ts | 48 ++++++++++++--- .../workers/common/DataTreeEvaluator/test.ts | 30 ++++++++++ .../src/workers/common/DependencyMap/test.ts | 2 + 16 files changed, 246 insertions(+), 28 deletions(-) diff --git a/app/client/src/actions/metaActions.ts b/app/client/src/actions/metaActions.ts index a97a86e0f7..d604518e19 100644 --- a/app/client/src/actions/metaActions.ts +++ b/app/client/src/actions/metaActions.ts @@ -87,3 +87,14 @@ export const syncUpdateWidgetMetaProperty = ( }, }; }; + +export const resetWidgetsMetaState = ( + widgetIdsToClear: string[], +): ReduxAction<{ widgetIdsToClear: string[] }> => { + return { + type: ReduxActionTypes.RESET_WIDGETS_META_STATE, + payload: { + widgetIdsToClear, + }, + }; +}; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 5ff049dd09..65a59f222f 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -394,6 +394,7 @@ export const ReduxActionTypes = { RESET_CHILDREN_WIDGET_META: "RESET_CHILDREN_WIDGET_META", RESET_WIDGET_META: "RESET_WIDGET_META", RESET_WIDGET_META_EVALUATED: "RESET_WIDGET_META_EVALUATED", + RESET_WIDGETS_META_STATE: "RESET_WIDGETS_META_STATE", UPDATE_WIDGET_NAME_INIT: "UPDATE_WIDGET_NAME_INIT", UPDATE_WIDGET_NAME_SUCCESS: "UPDATE_WIDGET_NAME_SUCCESS", FETCH_ACTIONS_FOR_PAGE_INIT: "FETCH_ACTIONS_FOR_PAGE_INIT", diff --git a/app/client/src/ce/workers/Evaluation/evaluationUtils.test.ts b/app/client/src/ce/workers/Evaluation/evaluationUtils.test.ts index 0d16abac7b..632d4c5087 100644 --- a/app/client/src/ce/workers/Evaluation/evaluationUtils.test.ts +++ b/app/client/src/ce/workers/Evaluation/evaluationUtils.test.ts @@ -622,6 +622,7 @@ describe("5. overrideWidgetProperties", () => { propertyPath: "defaultText", value: "abcde", evalMetaUpdates, + isNewWidget: false, }); expect(overwriteObj).toStrictEqual(undefined); @@ -656,6 +657,7 @@ describe("5. overrideWidgetProperties", () => { propertyPath: "meta.text", value: "abcdefg", evalMetaUpdates, + isNewWidget: false, }); expect(overwriteObj).toStrictEqual(undefined); @@ -699,6 +701,7 @@ describe("5. overrideWidgetProperties", () => { propertyPath: "defaultSelectedRow", value: [0, 1], evalMetaUpdates, + isNewWidget: false, }); expect(overwriteObj).toStrictEqual(undefined); @@ -732,6 +735,7 @@ describe("5. overrideWidgetProperties", () => { propertyPath: "meta.selectedRowIndex", value: 0, evalMetaUpdates, + isNewWidget: false, }); expect(overwriteObj).toStrictEqual(undefined); diff --git a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts index 165e4f6ead..b3c9e75f94 100644 --- a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts +++ b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts @@ -18,7 +18,7 @@ import { DataTreeJSAction, } from "entities/DataTree/dataTreeFactory"; -import _, { get, set } from "lodash"; +import _, { find, get, isEmpty, set } from "lodash"; import { WidgetTypeConfigMap } from "utils/WidgetFactory"; import { PluginType } from "entities/Action"; import { klona } from "klona/full"; @@ -737,15 +737,30 @@ export const overrideWidgetProperties = (params: { value: unknown; currentTree: DataTree; evalMetaUpdates: EvalMetaUpdates; + isNewWidget: boolean; }) => { - const { currentTree, entity, evalMetaUpdates, propertyPath, value } = params; + const { + currentTree, + entity, + evalMetaUpdates, + isNewWidget, + propertyPath, + value, + } = params; const clonedValue = klona(value); if (propertyPath in entity.overridingPropertyPaths) { const overridingPropertyPaths = entity.overridingPropertyPaths[propertyPath]; + const pathsNotToOverride = widgetPathsNotToOverride( + isNewWidget, + entity, + propertyPath, + ); + overridingPropertyPaths.forEach((overriddenPropertyPath) => { const overriddenPropertyPathArray = overriddenPropertyPath.split("."); + if (pathsNotToOverride.includes(overriddenPropertyPath)) return; _.set( currentTree, [entity.widgetName, ...overriddenPropertyPathArray], @@ -805,3 +820,43 @@ export const isATriggerPath = ( ) => { return isWidget(entity) && isPathDynamicTrigger(entity, propertyPath); }; + +// Checks if entity newly got added to the unevalTree +export const isNewEntity = (updates: DataTreeDiff[], entityName: string) => { + return !!find(updates, { + event: DataTreeDiffEvent.NEW, + payload: { propertyPath: entityName }, + }); +}; + +export const widgetPathsNotToOverride = ( + isNewWidget: boolean, + entity: DataTreeWidget, + propertyPath: string, +) => { + let pathsNotToOverride: string[] = []; + const overriddenPropertyPaths = entity.overridingPropertyPaths[propertyPath]; + + // To tell whether a widget has pre-existing meta values (although newly added), we stringify its meta object to get rid of undefined values + // An empty parsedMetaObj implies that the widget has no pre-existing meta values. + // MetaWidgets can have pre-existing meta values + const parsedMetaObj = JSON.parse(JSON.stringify(entity.meta)); + + if (isNewWidget && !isEmpty(parsedMetaObj)) { + const overriddenMetaPaths = overriddenPropertyPaths.filter( + (path) => path.split(".")[0] === "meta", + ); + // If widget is newly added but has pre-existing meta values, this meta values take precedence and should not be overridden + pathsNotToOverride = [...overriddenMetaPaths]; + // paths which these meta values override should also not get overridden + overriddenMetaPaths.forEach((path) => { + if (entity.overridingPropertyPaths.hasOwnProperty(path)) { + pathsNotToOverride = [ + ...pathsNotToOverride, + ...entity.overridingPropertyPaths[path], + ]; + } + }); + } + return pathsNotToOverride; +}; diff --git a/app/client/src/entities/DataTree/dataTreeWidget.ts b/app/client/src/entities/DataTree/dataTreeWidget.ts index a2e41a7ec0..e076bed41e 100644 --- a/app/client/src/entities/DataTree/dataTreeWidget.ts +++ b/app/client/src/entities/DataTree/dataTreeWidget.ts @@ -78,6 +78,7 @@ const generateDataTreeWidgetWithoutMeta = ( blockedDerivedProps[propertyName] = true; }); + // Map of properties that can both be overridden by meta and default values const overridingMetaPropsMap: Record = {}; Object.entries(defaultProps).forEach( @@ -214,11 +215,12 @@ export const generateDataTreeWidget = ( } = generateDataTreeWidgetWithoutMetaMemoized(widget); const overridingMetaProps: Record = {}; - // overridingMetaProps has all meta property value either from metaReducer or default set by widget whose dependent property also has default property. - Object.entries(defaultMetaProps).forEach(([key, value]) => { + // overridingMetaProps maps properties that can be overriden by either default values or meta changes to initial values. + // initial value is set to metaProps value or undefined (set to undefined to ensure that its path is present in the unevalTree). + Object.entries(defaultMetaProps).forEach(([key]) => { if (overridingMetaPropsMap[key]) { overridingMetaProps[key] = - key in widgetMetaProps ? widgetMetaProps[key] : value; + key in widgetMetaProps ? widgetMetaProps[key] : undefined; } }); diff --git a/app/client/src/entities/DataTree/types.ts b/app/client/src/entities/DataTree/types.ts index 18ff257a11..76d4437a8a 100644 --- a/app/client/src/entities/DataTree/types.ts +++ b/app/client/src/entities/DataTree/types.ts @@ -102,15 +102,16 @@ export enum OverridingPropertyType { META = "META", DEFAULT = "DEFAULT", } +export interface overrideDependency { + DEFAULT: string; + META: string; +} /** * Map of property name as key and value as object with defaultPropertyName and metaPropertyName which it depends on. */ export type PropertyOverrideDependency = Record< string, - { - DEFAULT: string | undefined; - META: string | undefined; - } + Partial >; export type WidgetConfig = { diff --git a/app/client/src/entities/DataTree/utils.ts b/app/client/src/entities/DataTree/utils.ts index 4989849aaa..a87ff6d88b 100644 --- a/app/client/src/entities/DataTree/utils.ts +++ b/app/client/src/entities/DataTree/utils.ts @@ -1,3 +1,4 @@ +import { DataTreeWidget } from "./dataTreeFactory"; import { PropertyOverrideDependency, OverridingPropertyPaths, @@ -57,3 +58,18 @@ export const setOverridingProperty = ({ overridingPropertyPaths[defaultPropertyName].push(overridingPropertyKey); } }; + +export const isMetaWidgetTemplate = (widget: DataTreeWidget) => { + return !!widget.siblingMetaWidgets; +}; + +export const isWidgetDefaultPropertyPath = ( + widget: DataTreeWidget, + propertyPath: string, +) => { + for (const property of Object.keys(widget.propertyOverrideDependency)) { + const overrideDependency = widget.propertyOverrideDependency[property]; + if (overrideDependency.DEFAULT === propertyPath) return true; + } + return false; +}; diff --git a/app/client/src/reducers/entityReducers/metaReducer/index.ts b/app/client/src/reducers/entityReducers/metaReducer/index.ts index 018fac23b0..a07ee37b3b 100644 --- a/app/client/src/reducers/entityReducers/metaReducer/index.ts +++ b/app/client/src/reducers/entityReducers/metaReducer/index.ts @@ -108,6 +108,18 @@ export const metaReducer = createReducer(initialState, { } return state; }, + [ReduxActionTypes.RESET_WIDGETS_META_STATE]: ( + state: MetaState, + action: ReduxAction<{ widgetIdsToClear: string[] }>, + ) => { + const next = { ...state }; + for (const metaWidgetId of action.payload.widgetIdsToClear) { + if (metaWidgetId && next[metaWidgetId]) { + delete next[metaWidgetId]; + } + } + return next; + }, [ReduxActionTypes.FETCH_PAGE_SUCCESS]: () => { return initialState; }, diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 8b361c28ef..107451724f 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -58,7 +58,7 @@ import { import { JSAction } from "entities/JSCollection"; import { getAppMode } from "selectors/applicationSelectors"; import { APP_MODE } from "entities/App"; -import { get, isUndefined } from "lodash"; +import { difference, get, isEmpty, isUndefined } from "lodash"; import { setEvaluatedArgument, setEvaluatedSnippet, @@ -94,7 +94,7 @@ import { Channel } from "redux-saga"; import { FormEvaluationState } from "reducers/evaluationReducers/formEvaluationReducer"; import { FormEvalActionPayload } from "./FormEvaluationSaga"; import { getSelectedAppTheme } from "selectors/appThemingSelectors"; -import { updateMetaState } from "actions/metaActions"; +import { resetWidgetsMetaState, updateMetaState } from "actions/metaActions"; import { getAllActionValidationConfig } from "selectors/entitiesSelector"; import { DataTree, @@ -184,6 +184,7 @@ export function* evaluateTreeSaga( userLogs, unEvalUpdates, isCreateFirstTree = false, + staleMetaIds, } = workerResponse; PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, @@ -194,6 +195,11 @@ export function* evaluateTreeSaga( const oldDataTree: DataTree = yield select(getDataTree); const updates = diff(oldDataTree, dataTree) || []; + // Replace empty object below with list of current metaWidgets present in the viewport + const hiddenStaleMetaIds = difference(staleMetaIds, Object.keys({})); + if (!isEmpty(hiddenStaleMetaIds)) { + yield put(resetWidgetsMetaState(hiddenStaleMetaIds)); + } yield put(setEvaluatedTree(updates)); PerformanceTracker.stopAsyncTracking( diff --git a/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts b/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts index aed6242a87..eab575c4af 100644 --- a/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts +++ b/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts @@ -441,9 +441,13 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = evaluator.setupUpdateTree(createUnEvalTreeForEval(updatedUnEvalTree)); - - evaluator.evalAndValidateSubTree(evalOrder, nonDynamicFieldValidationOrder); + evaluator.evalAndValidateSubTree( + evalOrder, + nonDynamicFieldValidationOrder, + unEvalUpdates, + ); const dataTree = evaluator.evalTree; expect(dataTree).toHaveProperty("Text2.text", "Hey there"); expect(dataTree).toHaveProperty("Text3.text", "Hey there"); @@ -460,8 +464,13 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = evaluator.setupUpdateTree(createUnEvalTreeForEval(updatedUnEvalTree)); - evaluator.evalAndValidateSubTree(evalOrder, nonDynamicFieldValidationOrder); + evaluator.evalAndValidateSubTree( + evalOrder, + nonDynamicFieldValidationOrder, + unEvalUpdates, + ); const dataTree = evaluator.evalTree; const updatedDependencyMap = evaluator.dependencyMap; @@ -482,8 +491,13 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = evaluator.setupUpdateTree(createUnEvalTreeForEval(updatedUnEvalTree)); - evaluator.evalAndValidateSubTree(evalOrder, nonDynamicFieldValidationOrder); + evaluator.evalAndValidateSubTree( + evalOrder, + nonDynamicFieldValidationOrder, + unEvalUpdates, + ); const dataTree = evaluator.evalTree; expect(dataTree).toHaveProperty("Input1.text", "Default value"); }); @@ -526,8 +540,13 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = evaluator.setupUpdateTree(createUnEvalTreeForEval(updatedUnEvalTree)); - evaluator.evalAndValidateSubTree(evalOrder, nonDynamicFieldValidationOrder); + evaluator.evalAndValidateSubTree( + evalOrder, + nonDynamicFieldValidationOrder, + unEvalUpdates, + ); const dataTree = evaluator.evalTree; expect(dataTree).toHaveProperty("Dropdown2.options.0.label", "newValue"); }); @@ -551,8 +570,13 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = evaluator.setupUpdateTree(createUnEvalTreeForEval(updatedUnEvalTree)); - evaluator.evalAndValidateSubTree(evalOrder, nonDynamicFieldValidationOrder); + evaluator.evalAndValidateSubTree( + evalOrder, + nonDynamicFieldValidationOrder, + unEvalUpdates, + ); const dataTree = evaluator.evalTree; const updatedDependencyMap = evaluator.dependencyMap; expect(dataTree).toHaveProperty("Table1.tableData", [ @@ -601,8 +625,13 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = evaluator.setupUpdateTree(createUnEvalTreeForEval(updatedUnEvalTree)); - evaluator.evalAndValidateSubTree(evalOrder, nonDynamicFieldValidationOrder); + evaluator.evalAndValidateSubTree( + evalOrder, + nonDynamicFieldValidationOrder, + unEvalUpdates, + ); const dataTree = evaluator.evalTree; const updatedDependencyMap = evaluator.dependencyMap; expect(dataTree).toHaveProperty("Table1.tableData", [ @@ -654,12 +683,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder2, + unEvalUpdates, } = evaluator.setupUpdateTree( createUnEvalTreeForEval((updatedTree1 as unknown) as UnEvalTree), ); evaluator.evalAndValidateSubTree( evalOrder, nonDynamicFieldValidationOrder2, + unEvalUpdates, ); expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ "Api2.config.pluginSpecifiedTemplates[0].value", @@ -683,12 +714,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder: newEvalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates: unEvalUpdates2, } = evaluator.setupUpdateTree( createUnEvalTreeForEval((updatedTree2 as unknown) as UnEvalTree), ); evaluator.evalAndValidateSubTree( newEvalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates2, ); const dataTree = evaluator.evalTree; expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ @@ -718,12 +751,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder: newEvalOrder2, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder3, + unEvalUpdates: unEvalUpdates3, } = evaluator.setupUpdateTree( createUnEvalTreeForEval((updatedTree3 as unknown) as UnEvalTree), ); evaluator.evalAndValidateSubTree( newEvalOrder2, nonDynamicFieldValidationOrder3, + unEvalUpdates3, ); const dataTree3 = evaluator.evalTree; expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ diff --git a/app/client/src/workers/Evaluation/handlers/evalTree.ts b/app/client/src/workers/Evaluation/handlers/evalTree.ts index 53837ee208..cae83729d0 100644 --- a/app/client/src/workers/Evaluation/handlers/evalTree.ts +++ b/app/client/src/workers/Evaluation/handlers/evalTree.ts @@ -44,6 +44,7 @@ export default function(request: EvalWorkerSyncRequest) { let userLogs: UserLogObject[] = []; let dependencies: DependencyMap = {}; let evalMetaUpdates: EvalMetaUpdates = []; + let staleMetaIds: string[] = []; const { allActionValidationConfig, @@ -86,6 +87,7 @@ export default function(request: EvalWorkerSyncRequest) { dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, { evalProps: dataTreeEvaluator.evalProps, }); + staleMetaIds = dataTreeResponse.staleMetaIds; } else if (dataTreeEvaluator.hasCyclicalDependency || forceEvaluation) { if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) { //allActionValidationConfigs may not be set in dataTreeEvaluatior. Therefore, set it explicitly via setter method @@ -125,6 +127,7 @@ export default function(request: EvalWorkerSyncRequest) { dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, { evalProps: dataTreeEvaluator.evalProps, }); + staleMetaIds = dataTreeResponse.staleMetaIds; } else { if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) { dataTreeEvaluator.setAllActionValidationConfig( @@ -156,6 +159,7 @@ export default function(request: EvalWorkerSyncRequest) { const updateResponse = dataTreeEvaluator.evalAndValidateSubTree( evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, ); dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, { evalProps: dataTreeEvaluator.evalProps, @@ -163,6 +167,7 @@ export default function(request: EvalWorkerSyncRequest) { evalMetaUpdates = JSON.parse( JSON.stringify(updateResponse.evalMetaUpdates), ); + staleMetaIds = updateResponse.staleMetaIds; } dataTreeEvaluator = dataTreeEvaluator as DataTreeEvaluator; dependencies = dataTreeEvaluator.inverseDependencyMap; @@ -200,7 +205,7 @@ export default function(request: EvalWorkerSyncRequest) { unEvalUpdates = []; } - return { + const evalTreeResponse: EvalTreeResponseData = { dataTree, dependencies, errors, @@ -211,7 +216,10 @@ export default function(request: EvalWorkerSyncRequest) { userLogs, unEvalUpdates, isCreateFirstTree, - } as EvalTreeResponseData; + staleMetaIds, + }; + + return evalTreeResponse; } export function clearCache() { diff --git a/app/client/src/workers/Evaluation/handlers/evalTrigger.ts b/app/client/src/workers/Evaluation/handlers/evalTrigger.ts index 7d49f0345a..1b9d487dd3 100644 --- a/app/client/src/workers/Evaluation/handlers/evalTrigger.ts +++ b/app/client/src/workers/Evaluation/handlers/evalTrigger.ts @@ -19,10 +19,12 @@ export default async function(request: EvalWorkerASyncRequest) { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree(unEvalTree); dataTreeEvaluator.evalAndValidateSubTree( evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, ); const evalTree = dataTreeEvaluator.evalTree; const resolvedFunctions = dataTreeEvaluator.resolvedFunctions; diff --git a/app/client/src/workers/Evaluation/types.ts b/app/client/src/workers/Evaluation/types.ts index 57692e5c66..9105e45038 100644 --- a/app/client/src/workers/Evaluation/types.ts +++ b/app/client/src/workers/Evaluation/types.ts @@ -45,4 +45,5 @@ export interface EvalTreeResponseData { userLogs: UserLogObject[]; unEvalUpdates: DataTreeDiff[]; isCreateFirstTree: boolean; + staleMetaIds: string[]; } diff --git a/app/client/src/workers/common/DataTreeEvaluator/index.ts b/app/client/src/workers/common/DataTreeEvaluator/index.ts index d2a0012951..94ae0c4fb6 100644 --- a/app/client/src/workers/common/DataTreeEvaluator/index.ts +++ b/app/client/src/workers/common/DataTreeEvaluator/index.ts @@ -40,6 +40,7 @@ import { overrideWidgetProperties, getAllPaths, isValidEntity, + isNewEntity, } from "@appsmith/workers/Evaluation/evaluationUtils"; import { difference, @@ -99,6 +100,10 @@ import { validateAndParseWidgetProperty, } from "./validationUtils"; import { errorModifier } from "workers/Evaluation/errorModifier"; +import { + isMetaWidgetTemplate, + isWidgetDefaultPropertyPath, +} from "entities/DataTree/utils"; type SortedDependencies = Array; export type EvalProps = { @@ -288,10 +293,11 @@ export default class DataTreeEvaluator { evalAndValidateFirstTree(): { evalTree: DataTree; evalMetaUpdates: EvalMetaUpdates; + staleMetaIds: string[]; } { const evaluationStartTime = performance.now(); // Evaluate - const { evalMetaUpdates, evaluatedTree } = this.evaluateTree( + const { evalMetaUpdates, evaluatedTree, staleMetaIds } = this.evaluateTree( this.oldUnEvalTree, this.resolvedFunctions, this.sortedDependencies, @@ -322,6 +328,7 @@ export default class DataTreeEvaluator { return { evalTree: this.getEvalTree(), evalMetaUpdates, + staleMetaIds, }; } @@ -522,18 +529,21 @@ export default class DataTreeEvaluator { evalAndValidateSubTree( evaluationOrder: string[], nonDynamicFieldValidationOrder: string[], + unevalUpdates: DataTreeDiff[], ): { evalMetaUpdates: EvalMetaUpdates; + staleMetaIds: string[]; } { const evaluationStartTime = performance.now(); const { evalMetaUpdates, evaluatedTree: newEvalTree, + staleMetaIds, } = this.evaluateTree( this.evalTree, this.resolvedFunctions, evaluationOrder, - { skipRevalidation: false }, + { skipRevalidation: false, isFirstTree: false, unevalUpdates }, ); const evaluationEndTime = performance.now(); const reValidateStartTime = performance.now(); @@ -553,6 +563,7 @@ export default class DataTreeEvaluator { this.logs.push({ timeTakenForEvalAndValidateSubTree }); return { evalMetaUpdates, + staleMetaIds, }; } @@ -656,16 +667,21 @@ export default class DataTreeEvaluator { oldUnevalTree: DataTree, resolvedFunctions: Record, sortedDependencies: Array, - option = { skipRevalidation: true }, + options: { + skipRevalidation: boolean; + isFirstTree: boolean; + unevalUpdates: DataTreeDiff[]; + } = { skipRevalidation: true, isFirstTree: true, unevalUpdates: [] }, ): { evaluatedTree: DataTree; evalMetaUpdates: EvalMetaUpdates; + staleMetaIds: string[]; } { const tree = klona(oldUnevalTree); - errorModifier.updateAsyncFunctions(tree); - const evalMetaUpdates: EvalMetaUpdates = []; + const { isFirstTree, skipRevalidation, unevalUpdates } = options; + let staleMetaIds: string[] = []; try { const evaluatedTree = sortedDependencies.reduce( (currentTree: DataTree, fullPropertyPath: string) => { @@ -725,6 +741,8 @@ export default class DataTreeEvaluator { evalPropertyValue = unEvalPropertyValue; } if (isWidget(entity) && !isATriggerPath) { + const isNewWidget = + isFirstTree || isNewEntity(unevalUpdates, entityName); if (propertyPath) { const parsedValue = validateAndParseWidgetProperty({ fullPropertyPath, @@ -743,15 +761,19 @@ export default class DataTreeEvaluator { parsedValue, propertyPath, evalPropertyValue, + isNewWidget, }); - if (!option.skipRevalidation) { + if (!skipRevalidation) { this.reValidateWidgetDependentProperty({ fullPropertyPath, widget: entity, currentTree, }); } + staleMetaIds = staleMetaIds.concat( + this.getStaleMetaStateIds(entity, propertyPath), + ); return currentTree; } @@ -818,13 +840,13 @@ export default class DataTreeEvaluator { }, tree, ); - return { evaluatedTree, evalMetaUpdates }; + return { evaluatedTree, evalMetaUpdates, staleMetaIds }; } catch (error) { this.errors.push({ type: EvalErrorTypes.EVAL_TREE_ERROR, message: (error as Error).message, }); - return { evaluatedTree: tree, evalMetaUpdates }; + return { evaluatedTree: tree, evalMetaUpdates, staleMetaIds }; } } @@ -1073,6 +1095,7 @@ export default class DataTreeEvaluator { evalMetaUpdates, evalPropertyValue, fullPropertyPath, + isNewWidget, parsedValue, propertyPath, }: { @@ -1080,6 +1103,7 @@ export default class DataTreeEvaluator { entity: DataTreeWidget; evalMetaUpdates: EvalMetaUpdates; fullPropertyPath: string; + isNewWidget: boolean; parsedValue: unknown; propertyPath: string; evalPropertyValue: unknown; @@ -1090,6 +1114,7 @@ export default class DataTreeEvaluator { value: parsedValue, currentTree, evalMetaUpdates, + isNewWidget, }); if (overwriteObj && overwriteObj.overwriteParsedValue) { @@ -1370,6 +1395,13 @@ export default class DataTreeEvaluator { ); }); } + // When a default value changes, meta values of metaWidgets not present in the unevalTree becomes stale + getStaleMetaStateIds(entity: DataTreeWidget, propertyPath: string) { + return isWidgetDefaultPropertyPath(entity, propertyPath) && + isMetaWidgetTemplate(entity) + ? (entity.siblingMetaWidgets as string[]) + : []; + } clearErrors() { this.errors = []; diff --git a/app/client/src/workers/common/DataTreeEvaluator/test.ts b/app/client/src/workers/common/DataTreeEvaluator/test.ts index a2de526969..475056cb2d 100644 --- a/app/client/src/workers/common/DataTreeEvaluator/test.ts +++ b/app/client/src/workers/common/DataTreeEvaluator/test.ts @@ -146,12 +146,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree( (unEvalTree as unknown) as DataTree, ); dataTreeEvaluator.evalAndValidateSubTree( evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, ); expect(dataTreeEvaluator.dependencyMap).toStrictEqual({ @@ -240,12 +242,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder1, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree( arrayAccessorCyclicDependency.apiSuccessUnEvalTree, ); dataTreeEvaluator.evalAndValidateSubTree( evalOrder, nonDynamicFieldValidationOrder1, + unEvalUpdates, ); expect(dataTreeEvaluator.dependencyMap["Api1"]).toStrictEqual([ "Api1.data", @@ -264,12 +268,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder2, + unEvalUpdates: unEvalUpdates2, } = dataTreeEvaluator.setupUpdateTree( arrayAccessorCyclicDependency.apiFailureUnEvalTree, ); dataTreeEvaluator.evalAndValidateSubTree( order, nonDynamicFieldValidationOrder2, + unEvalUpdates2, ); expect(dataTreeEvaluator.dependencyMap["Api1"]).toStrictEqual([ @@ -293,24 +299,28 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order1, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder3, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree( arrayAccessorCyclicDependency.apiSuccessUnEvalTree, ); dataTreeEvaluator.evalAndValidateSubTree( order1, nonDynamicFieldValidationOrder3, + unEvalUpdates, ); // success: response -> [{...}, {...}] const { evalOrder: order2, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder4, + unEvalUpdates: unEvalUpdates2, } = dataTreeEvaluator.setupUpdateTree( arrayAccessorCyclicDependency.apiSuccessUnEvalTree2, ); dataTreeEvaluator.evalAndValidateSubTree( order2, nonDynamicFieldValidationOrder4, + unEvalUpdates2, ); expect(dataTreeEvaluator.dependencyMap["Api1"]).toStrictEqual([ @@ -333,12 +343,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder5, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree( nestedArrayAccessorCyclicDependency.apiSuccessUnEvalTree, ); dataTreeEvaluator.evalAndValidateSubTree( order, nonDynamicFieldValidationOrder5, + unEvalUpdates, ); expect(dataTreeEvaluator.dependencyMap["Api1"]).toStrictEqual([ "Api1.data", @@ -360,12 +372,14 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order1, nonDynamicFieldValidationOrder, + unEvalUpdates: unEvalUpdates2, } = dataTreeEvaluator.setupUpdateTree( nestedArrayAccessorCyclicDependency.apiFailureUnEvalTree, ); dataTreeEvaluator.evalAndValidateSubTree( order1, nonDynamicFieldValidationOrder, + unEvalUpdates2, ); expect(dataTreeEvaluator.dependencyMap["Api1"]).toStrictEqual([ "Api1.data", @@ -391,24 +405,28 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order, nonDynamicFieldValidationOrder, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree( nestedArrayAccessorCyclicDependency.apiSuccessUnEvalTree, ); dataTreeEvaluator.evalAndValidateSubTree( order, nonDynamicFieldValidationOrder, + unEvalUpdates, ); // success: response -> [ [{...}, {...}, {...}], [{...}, {...}, {...}] ] const { evalOrder: order1, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder2, + unEvalUpdates: unEvalUpdates2, } = dataTreeEvaluator.setupUpdateTree( nestedArrayAccessorCyclicDependency.apiSuccessUnEvalTree2, ); dataTreeEvaluator.evalAndValidateSubTree( order1, nonDynamicFieldValidationOrder2, + unEvalUpdates2, ); expect(dataTreeEvaluator.dependencyMap["Api1"]).toStrictEqual([ @@ -430,24 +448,28 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder2, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree( nestedArrayAccessorCyclicDependency.apiSuccessUnEvalTree, ); dataTreeEvaluator.evalAndValidateSubTree( order, nonDynamicFieldValidationOrder2, + unEvalUpdates, ); // success: response -> [ [{...}, {...}, {...}], [{...}, {...}, {...}], [] ] const { evalOrder: order1, nonDynamicFieldValidationOrder, + unEvalUpdates: unEvalUpdates2, } = dataTreeEvaluator.setupUpdateTree( nestedArrayAccessorCyclicDependency.apiSuccessUnEvalTree3, ); dataTreeEvaluator.evalAndValidateSubTree( order1, nonDynamicFieldValidationOrder, + unEvalUpdates2, ); expect(dataTreeEvaluator.dependencyMap["Api1"]).toStrictEqual([ "Api1.data", @@ -487,10 +509,12 @@ describe("DataTreeEvaluator", () => { const { evalOrder, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder2, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree(newUnEvalTree); dataTreeEvaluator.evalAndValidateSubTree( evalOrder, nonDynamicFieldValidationOrder2, + unEvalUpdates, ); expect(dataTreeEvaluator.triggerFieldDependencyMap).toEqual({ "Button3.onClick": ["Api1.run", "Button2.text"], @@ -503,10 +527,12 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order1, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder3, + unEvalUpdates: unEvalUpdates2, } = dataTreeEvaluator.setupUpdateTree(newUnEvalTree); dataTreeEvaluator.evalAndValidateSubTree( order1, nonDynamicFieldValidationOrder3, + unEvalUpdates2, ); expect(dataTreeEvaluator.triggerFieldDependencyMap).toEqual({ "Button3.onClick": ["Api1.run", "Button2.text", "Api2.run"], @@ -521,10 +547,12 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order2, nonDynamicFieldValidationOrder, + unEvalUpdates: unEvalUpdates3, } = dataTreeEvaluator.setupUpdateTree(newUnEvalTree); dataTreeEvaluator.evalAndValidateSubTree( order2, nonDynamicFieldValidationOrder, + unEvalUpdates3, ); // delete Button2 @@ -532,10 +560,12 @@ describe("DataTreeEvaluator", () => { const { evalOrder: order3, nonDynamicFieldValidationOrder: nonDynamicFieldValidationOrder4, + unEvalUpdates: unEvalUpdates4, } = dataTreeEvaluator.setupUpdateTree(newUnEvalTree); dataTreeEvaluator.evalAndValidateSubTree( order3, nonDynamicFieldValidationOrder4, + unEvalUpdates4, ); expect(dataTreeEvaluator.triggerFieldDependencyMap).toEqual({ diff --git a/app/client/src/workers/common/DependencyMap/test.ts b/app/client/src/workers/common/DependencyMap/test.ts index e3758586c0..2f7e9cf23b 100644 --- a/app/client/src/workers/common/DependencyMap/test.ts +++ b/app/client/src/workers/common/DependencyMap/test.ts @@ -54,10 +54,12 @@ describe("test validationDependencyMap", () => { const { evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, } = dataTreeEvaluator.setupUpdateTree((unEvalTree as unknown) as DataTree); dataTreeEvaluator.evalAndValidateSubTree( evalOrder, nonDynamicFieldValidationOrder, + unEvalUpdates, ); expect(dataTreeEvaluator.validationDependencyMap).toStrictEqual({});