From ae733ca2f021dfe6b2dc9f4fc3e94452244c91e4 Mon Sep 17 00:00:00 2001 From: Anand Srinivasan <66776129+eco-monk@users.noreply.github.com> Date: Tue, 8 Mar 2022 19:26:03 +0530 Subject: [PATCH] fix: unrelated widgets in loading state (#11370) * log loading actions * isLoading: check entity properties * rename variables * unit tests * add descriptions * clean test data * include JS_Object self-dependencies * add comment * fix basic tests * fix unit tests * updated unit tests * add test case * update comment * console - disable lint errors * code clean up * change folder * find loading entites as a method * updated unit tests * check if entity exists in dataTree * update test cases * getEntityDependants - add specific path test case --- app/client/src/sagas/WidgetLoadingSaga.ts | 107 +--- .../src/utils/WidgetLoadingStateUtils.test.ts | 483 ++++++++++++++++++ .../src/utils/WidgetLoadingStateUtils.ts | 142 +++++ app/client/src/workers/evaluationUtils.ts | 11 + 4 files changed, 664 insertions(+), 79 deletions(-) create mode 100644 app/client/src/utils/WidgetLoadingStateUtils.test.ts create mode 100644 app/client/src/utils/WidgetLoadingStateUtils.ts diff --git a/app/client/src/sagas/WidgetLoadingSaga.ts b/app/client/src/sagas/WidgetLoadingSaga.ts index 66b15c750b..21befafc81 100644 --- a/app/client/src/sagas/WidgetLoadingSaga.ts +++ b/app/client/src/sagas/WidgetLoadingSaga.ts @@ -1,73 +1,22 @@ -import { DependencyMap } from "../utils/DynamicBindingUtils"; +import { DependencyMap } from "utils/DynamicBindingUtils"; import { call, fork, put, select, take } from "redux-saga/effects"; import { getEvaluationInverseDependencyMap, getDataTree, } from "../selectors/dataTreeSelectors"; -import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { DataTree } from "entities/DataTree/dataTreeFactory"; import { getActions } from "../selectors/entitiesSelector"; -import { ActionData } from "../reducers/entityReducers/actionsReducer"; +import { + ActionData, + ActionDataState, +} from "../reducers/entityReducers/actionsReducer"; import { ReduxActionErrorTypes, ReduxActionTypes, } from "../constants/ReduxActionConstants"; import log from "loglevel"; import * as Sentry from "@sentry/react"; -import { get } from "lodash"; - -const createEntityDependencyMap = (dependencyMap: DependencyMap) => { - const entityDepMap: DependencyMap = {}; - Object.entries(dependencyMap).forEach(([dependant, dependencies]) => { - const entityDependant = dependant.split(".")[0]; - const existing = entityDepMap[entityDependant] || []; - entityDepMap[entityDependant] = existing.concat( - dependencies - .map((dep) => { - const value = dep.split(".")[0]; - if (value !== entityDependant) { - return value; - } - return undefined; - }) - .filter((value) => typeof value === "string") as string[], - ); - }); - return entityDepMap; -}; - -const getEntityDependencies = ( - entityNames: string[], - inverseMap: DependencyMap, - visited: Set, -): Set => { - const dependantsEntities: Set = new Set(); - entityNames.forEach((entityName) => { - if (entityName in inverseMap) { - inverseMap[entityName].forEach((dependency) => { - const dependantEntityName = dependency.split(".")[0]; - // Example: For a dependency chain that looks like Dropdown1.selectedOptionValue -> Table1.tableData -> Text1.text -> Dropdown1.options - // Here we're operating on - // Dropdown1 -> Table1 -> Text1 -> Dropdown1 - // It looks like a circle, but isn't - // So we need to mark the visited nodes and avoid infinite recursion in case we've already visited a node once. - if (visited.has(dependantEntityName)) { - return; - } - visited.add(dependantEntityName); - dependantsEntities.add(dependantEntityName); - const childDependencies = getEntityDependencies( - Array.from(dependantsEntities), - inverseMap, - visited, - ); - childDependencies.forEach((entityName) => { - dependantsEntities.add(entityName); - }); - }); - } - }); - return dependantsEntities; -}; +import { findLoadingEntities } from "utils/WidgetLoadingStateUtils"; const ACTION_EXECUTION_REDUX_ACTIONS = [ // Actions @@ -81,33 +30,33 @@ const ACTION_EXECUTION_REDUX_ACTIONS = [ ]; function* setWidgetsLoadingSaga() { - const inverseMap = yield select(getEvaluationInverseDependencyMap); - const entityDependencyMap = createEntityDependencyMap(inverseMap); - const actions = yield select(getActions); + const actions: ActionDataState = yield select(getActions); const isLoadingActions: string[] = actions .filter((action: ActionData) => action.isLoading) .map((action: ActionData) => action.config.name); - const loadingEntities = getEntityDependencies( - isLoadingActions, - entityDependencyMap, - new Set(), - ); + if (isLoadingActions.length === 0) { + yield put({ + type: ReduxActionTypes.SET_LOADING_ENTITIES, + payload: new Set(), + }); + } else { + const inverseMap: DependencyMap = yield select( + getEvaluationInverseDependencyMap, + ); + const dataTree: DataTree = yield select(getDataTree); - // get all widgets evaluted data - const dataTree: DataTree = yield select(getDataTree); - // check animateLoading is active on current widgets and set - Object.entries(dataTree).forEach(([entityName, entity]) => { - if ("ENTITY_TYPE" in entity && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) - if (get(dataTree, [entityName, "animateLoading"]) === false) { - loadingEntities.delete(entityName); - } - }); + const loadingEntities = findLoadingEntities( + isLoadingActions, + dataTree, + inverseMap, + ); - yield put({ - type: ReduxActionTypes.SET_LOADING_ENTITIES, - payload: loadingEntities, - }); + yield put({ + type: ReduxActionTypes.SET_LOADING_ENTITIES, + payload: loadingEntities, + }); + } } function* actionExecutionChangeListenerSaga() { diff --git a/app/client/src/utils/WidgetLoadingStateUtils.test.ts b/app/client/src/utils/WidgetLoadingStateUtils.test.ts new file mode 100644 index 0000000000..52995d3b5a --- /dev/null +++ b/app/client/src/utils/WidgetLoadingStateUtils.test.ts @@ -0,0 +1,483 @@ +import { PluginType } from "entities/Action"; +import { + DataTreeAction, + DataTreeJSAction, + DataTreeWidget, + ENTITY_TYPE, +} from "entities/DataTree/dataTreeFactory"; +import { + findLoadingEntities, + getEntityDependants, + groupAndFilterDependantsMap, +} from "utils/WidgetLoadingStateUtils"; + +const JS_object_tree: DataTreeJSAction = { + pluginType: PluginType.JS, + name: "", + ENTITY_TYPE: ENTITY_TYPE.JSACTION, + body: "", + meta: {}, + dynamicBindingPathList: [], + bindingPaths: {}, + variables: [], + dependencyMap: {}, +}; + +const Select_tree: DataTreeWidget = { + ENTITY_TYPE: ENTITY_TYPE.WIDGET, + bindingPaths: {}, + triggerPaths: {}, + validationPaths: {}, + logBlackList: {}, + propertyOverrideDependency: {}, + overridingPropertyPaths: {}, + privateWidgets: {}, + widgetId: "", + type: "", + widgetName: "", + renderMode: "CANVAS", + version: 0, + parentColumnSpace: 0, + parentRowSpace: 0, + leftColumn: 0, + rightColumn: 0, + topRow: 0, + bottomRow: 0, + isLoading: false, + animateLoading: true, +}; + +const Query_tree: DataTreeAction = { + data: {}, + actionId: "", + config: {}, + pluginType: PluginType.DB, + pluginId: "", + name: "", + run: {}, + clear: {}, + dynamicBindingPathList: [], + bindingPaths: {}, + ENTITY_TYPE: ENTITY_TYPE.ACTION, + dependencyMap: {}, + logBlackList: {}, + datasourceUrl: "", + responseMeta: { + isExecutionSuccess: true, + }, + isLoading: false, +}; + +const baseDataTree = { + JS_file: { ...JS_object_tree, name: "JS_file" }, + Select1: { ...Select_tree, name: "Select1" }, + Select2: { ...Select_tree, name: "Select2" }, + Select3: { ...Select_tree, name: "Select3" }, + Query1: { ...Query_tree, name: "Query1" }, + Query2: { ...Query_tree, name: "Query2" }, + Query3: { ...Query_tree, name: "Query3" }, +}; + +describe("Widget loading state utils", () => { + describe("findLoadingEntites", () => { + // Select1.options -> JS_file.func1 -> Query1.data + // Select2.options -> JS_file.func2 -> Query2.data + // JS_file.func3 -> Query3.data + const baseInverseMap = { + "Query1.config": ["Query1"], + "Query1.config.body": ["Query1.config"], + "Query1.data": ["JS_file.func1", "Query1"], + + "Query2.config": ["Query2"], + "Query2.config.body": ["Query2.config"], + "Query2.data": ["JS_file.func2", "Query2"], + + "Query3.config": ["Query3"], + "Query3.config.body": ["Query3.config"], + "Query3.data": ["JS_file.func3"], + + "JS_file.func1": ["Select1.options"], + "JS_file.func2": ["Select2.options"], + + "Select1.options": [ + "Select1.selectedOptionValue", + "Select1.selectedOptionLabel", + "Select1", + ], + "Select2.options": [ + "Select2.selectedOptionValue", + "Select2.selectedOptionLabel", + "Select2", + ], + "Select3.options": [ + "Select3.selectedOptionValue", + "Select3.selectedOptionLabel", + "Select3", + ], + }; + + // Select1.options -> JS_file.func1 -> Query1.data + it("handles linear dependencies", () => { + const loadingEntites = findLoadingEntities( + ["Query1"], + baseDataTree, + baseInverseMap, + ); + expect(loadingEntites).toStrictEqual(new Set(["Select1"])); + }); + + // Select1.options -> JS_file.func1 -> Query1.data + // Select2.options -> JS_file.func2 -> Query2.data + // Select3.options -> none + it("handles multiple dependencies", () => { + const loadingEntites = findLoadingEntities( + ["Query1", "Query2", "Query3"], + baseDataTree, + baseInverseMap, + ); + expect(loadingEntites).toStrictEqual(new Set(["Select1", "Select2"])); + }); + + // none -> Query3.data + it("handles no dependencies", () => { + const loadingEntites = findLoadingEntities( + ["Query3"], + baseDataTree, + baseInverseMap, + ); + expect(loadingEntites).toStrictEqual(new Set([])); + }); + + // JS_file.func1 -> Query1.run + // Select1.options -> Query1.data + it("handles Query.run and Query.data dependency", () => { + const loadingEntites = findLoadingEntities(["Query1"], baseDataTree, { + "Query1.config": ["Query1"], + "Query1.config.body": ["Query1.config"], + "Query1.run": ["JS_file.func1"], + "Query1.data": ["Select1.options", "Query1"], + + "JS_file.func1": [], + + "Select1.options": [ + "Select1.selectedOptionValue", + "Select1.selectedOptionLabel", + "Select1", + ], + }); + expect(loadingEntites).toStrictEqual(new Set(["Select1"])); + }); + + // Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data + it("handles nested JS dependencies within same file", () => { + const loadingEntites = findLoadingEntities(["Query1"], baseDataTree, { + "Query1.config": ["Query1"], + "Query1.config.body": ["Query1.config"], + "Query1.data": ["JS_file.internalFunc", "Query1"], + + "JS_file.internalFunc": ["JS_file.func1"], + "JS_file.func1": ["Select1.options"], + + "Select1.options": [ + "Select1.selectedOptionValue", + "Select1.selectedOptionLabel", + "Select1", + ], + }); + expect(loadingEntites).toStrictEqual(new Set(["Select1"])); + }); + + // Select1.options -> JS_file1.func1 -> JS_file2.internalFunc -> Query1.data + it("handles nested JS dependencies between files", () => { + const loadingEntites = findLoadingEntities( + ["Query1"], + { + ...baseDataTree, + JS_file1: { ...JS_object_tree, name: "JS_file1" }, + JS_file2: { ...JS_object_tree, name: "JS_file2" }, + }, + { + "Query1.config": ["Query1"], + "Query1.config.body": ["Query1.config"], + "Query1.data": ["JS_file2.internalFunc", "Query1"], + + "JS_file2.internalFunc": ["JS_file1.func1"], + "JS_file1.func1": ["Select1.options"], + + "Select1.options": [ + "Select1.selectedOptionValue", + "Select1.selectedOptionLabel", + "Select1", + ], + }, + ); + expect(loadingEntites).toStrictEqual(new Set(["Select1"])); + }); + + /* Select1.options -> JS.func1 -> Query1.data, + Select2.options -> Query2.data, + JS.func2 -> Query2.run + + When Query2 is called. + Only Select2 should be listed, not Select1. + */ + it("handles selective dependencies in same JS file", () => { + const loadingEntites = findLoadingEntities(["Query2"], baseDataTree, { + "Query1.config": ["Query1"], + "Query1.config.body": ["Query1.config"], + "Query1.data": ["JS_file.func1"], + + "Query2.config": ["Query2"], + "Query2.config.body": ["Query2.config"], + "Query2.data": ["JS_file.func2"], + + "JS_file.func1": ["Select1.options"], + "JS_file.func2": ["Select2.options"], + + "Select1.options": [ + "Select1.selectedOptionValue", + "Select1.selectedOptionLabel", + "Select1", + ], + "Select2.options": [ + "Select2.selectedOptionValue", + "Select2.selectedOptionLabel", + "Select2", + ], + }); + expect(loadingEntites).toStrictEqual(new Set(["Select2"])); + }); + }); + + describe("groupAndFilterDependantsMap", () => { + it("groups entities and filters self-dependencies", () => { + const groupedDependantsMap = groupAndFilterDependantsMap( + { + "Query1.config": ["Query1"], + "Query1.config.body": ["Query1.config"], + "Query1.data": ["JS_file.func1", "Query1"], // dependant + + "Query2.config": ["Query2"], + "Query2.config.body": ["Query2.config"], + "Query2.run": ["Query2", "JS_file.func2"], // dependant + "Query2.data": ["Query2", "Select2.options"], // dependant + + "Query3.config": ["Query3"], + "Query3.config.body": ["Query3.config"], + + "JS_file.func1": ["Select1.options"], // dependant + + "Select1.options": [ + "Select1.selectedOptionValue", + "Select1.selectedOptionLabel", + "Select1", + ], + "Select2.options": [ + "Select2.selectedOptionValue", + "Select2.selectedOptionLabel", + "Select2", + ], + }, + baseDataTree, + ); + expect(groupedDependantsMap).toStrictEqual({ + Query1: { "Query1.data": ["JS_file.func1"] }, + Query2: { + "Query2.run": ["JS_file.func2"], + "Query2.data": ["Select2.options"], + }, + JS_file: { + "JS_file.func1": ["Select1.options"], + }, + }); + }); + + it("includes JS object's self dependencies", () => { + const groupedDependantsMap = groupAndFilterDependantsMap( + { + "JS_file.func1": ["Select1.options"], // dependant + "JS_file.internalFunc": ["JS_file.func1"], // self-dependant JsObject + }, + baseDataTree, + ); + expect(groupedDependantsMap).toStrictEqual({ + JS_file: { + "JS_file.func1": ["Select1.options"], + "JS_file.internalFunc": ["JS_file.func1"], + }, + }); + }); + + it("includes JS object's nested self dependencies", () => { + const groupedDependantsMap = groupAndFilterDependantsMap( + { + "JS_file.func1": ["Select1.options"], // dependant + "JS_file.internalFunc2": ["JS_file.func1"], // self-dependant JsObject + "JS_file.internalFunc1": ["JS_file.internalFunc2"], // self-dependant JsObject + }, + baseDataTree, + ); + expect(groupedDependantsMap).toStrictEqual({ + JS_file: { + "JS_file.func1": ["Select1.options"], + "JS_file.internalFunc2": ["JS_file.func1"], + "JS_file.internalFunc1": ["JS_file.internalFunc2"], + }, + }); + }); + }); + + describe("getEntityDependants", () => { + // Select1.options -> JS_file.func1 -> Query1.data + it("handles simple dependency", () => { + const dependants = getEntityDependants( + ["Query1"], + { + Query1: { + "Query1.data": ["JS_file.func1"], + }, + JS_file: { + "JS_file.func1": ["Select1.options"], + }, + }, + new Set(), + ); + expect(dependants).toStrictEqual({ + names: new Set(["JS_file", "Select1"]), + fullPaths: new Set(["JS_file.func1", "Select1.options"]), + }); + }); + + // Select1.options -> JS_file.func1 -> Query1.data + // Select2.options -> JS_file.func2 -> Query1.data + it("handles multiple dependencies", () => { + const dependants = getEntityDependants( + ["Query1"], + { + Query1: { + "Query1.data": ["JS_file.func1", "JS_file.func2"], + }, + JS_file: { + "JS_file.func1": ["Select1.options"], + "JS_file.func2": ["Select2.options"], + }, + }, + new Set(), + ); + expect(dependants).toStrictEqual({ + names: new Set(["JS_file", "Select1", "Select2"]), + fullPaths: new Set([ + "JS_file.func1", + "Select1.options", + "JS_file.func2", + "Select2.options", + ]), + }); + }); + + it("handles specific entity paths", () => { + const dependants = getEntityDependants( + ["JS_file.func2"], // specific path + { + Query1: { + "Query1.data": ["JS_file.func1"], + }, + Query2: { + "Query2.data": ["JS_file.func2"], + }, + JS_file: { + "JS_file.func1": ["Select1.options"], + "JS_file.func2": ["Select2.options"], + }, + }, + new Set(), + ); + expect(dependants).toStrictEqual({ + names: new Set(["Select2"]), + fullPaths: new Set(["Select2.options"]), + }); + }); + + // Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data + it("handles JS self-dependencies", () => { + const dependants = getEntityDependants( + ["Query1"], + { + Query1: { + "Query1.data": ["JS_file.internalFunc"], + }, + JS_file: { + "JS_file.internalFunc": ["JS_file.func1"], + "JS_file.func1": ["Select1.options"], + }, + }, + new Set(), + ); + expect(dependants).toStrictEqual({ + names: new Set(["JS_file", "Select1"]), + fullPaths: new Set([ + "JS_file.internalFunc", + "JS_file.func1", + "Select1.options", + ]), + }); + }); + + // Select1.options -> JS_file.func -> JS_file.internalFunc1 -> JS_file.internalFunc2 -> Query1.data + it("handles nested JS self-dependencies", () => { + const dependants = getEntityDependants( + ["Query1"], + { + Query1: { + "Query1.data": ["JS_file.internalFunc2"], + }, + JS_file: { + "JS_file.internalFunc2": ["JS_file.internalFunc1"], + "JS_file.internalFunc1": ["JS_file.func"], + "JS_file.func": ["Select1.options"], + }, + }, + new Set(), + ); + expect(dependants).toStrictEqual({ + names: new Set(["JS_file", "Select1"]), + fullPaths: new Set([ + "JS_file.internalFunc1", + "JS_file.internalFunc2", + "JS_file.func", + "Select1.options", + ]), + }); + }); + + /* Select1.options -> JS.func1 -> Query1.data, + Select2.options -> Query2.data, + JS.func2 -> Query2.run + + When Query2 is called. + Only Select2 should be listed, not Select1. + */ + it("handles selective dependencies in same JS file", () => { + const dependants = getEntityDependants( + ["Query2"], + { + Query1: { + "Query1.data": ["JS_file.func1"], + }, + Query2: { + "Query2.data": ["JS_file.func2"], + }, + JS_file: { + "JS_file.func1": ["Select1.options"], + "JS_file.func2": ["Select2.options"], + }, + }, + new Set(), + ); + expect(dependants).toStrictEqual({ + names: new Set(["JS_file", "Select2"]), + fullPaths: new Set(["JS_file.func2", "Select2.options"]), + }); + }); + }); +}); diff --git a/app/client/src/utils/WidgetLoadingStateUtils.ts b/app/client/src/utils/WidgetLoadingStateUtils.ts new file mode 100644 index 0000000000..1e314421d9 --- /dev/null +++ b/app/client/src/utils/WidgetLoadingStateUtils.ts @@ -0,0 +1,142 @@ +import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { get, set } from "lodash"; +import { isJSObject } from "workers/evaluationUtils"; +import { DependencyMap } from "./DynamicBindingUtils"; + +type GroupedDependencyMap = Record; + +// group dependants by entity and filter self-dependencies +// because, we're only interested in entities that depend on other entitites +// filter exception: JS_OBJECT's, when a function depends on another function within the same object +export const groupAndFilterDependantsMap = ( + inverseMap: DependencyMap, + dataTree: DataTree, +): GroupedDependencyMap => { + const entitiesDepMap: GroupedDependencyMap = {}; + + Object.entries(inverseMap).forEach(([fullDependencyPath, dependants]) => { + const dependencyEntityName = fullDependencyPath.split(".")[0]; + const dataTreeEntity = dataTree[dependencyEntityName]; + if (!dataTreeEntity) return; + const isJS_Object = isJSObject(dataTreeEntity); + + const entityDependantsMap = entitiesDepMap[dependencyEntityName] || {}; + let entityPathDependants = entityDependantsMap[fullDependencyPath] || []; + + entityPathDependants = entityPathDependants.concat( + isJS_Object + ? /* include self-dependent properties for JsObjects + e.g. { + "JsObject.internalFunc": [ "JsObject.fun1", "JsObject" ] + } + When fun1 calls internalfunc within it's body. + Will keep "JsObject.fun1" and filter "JsObject". + */ + dependants.filter((dep) => dep !== dependencyEntityName) + : /* filter self-dependent properties for everything else + e.g. { + Select1.selectedOptionValue: [ + 'Select1.isValid', 'Select1' + ] + } + Will remove both 'Select1.isValid', 'Select1'. + */ + dependants.filter( + (dep) => dep.split(".")[0] !== dependencyEntityName, + ), + ); + + if (!(entityPathDependants.length > 0)) return; + set( + entitiesDepMap, + [dependencyEntityName, fullDependencyPath], + entityPathDependants, + ); + }); + + return entitiesDepMap; +}; + +// get entities that depend on a given list of entites +// e.g. widgets that depend on a list of actions +export const getEntityDependants = ( + fullEntityPaths: string[], + allEntitiesDependantsmap: GroupedDependencyMap, + visitedPaths: Set, +): { names: Set; fullPaths: Set } => { + const dependantEntityNames = new Set(); + const dependantEntityFullPaths = new Set(); + + fullEntityPaths.forEach((fullEntityPath) => { + const entityPathArray = fullEntityPath.split("."); + const entityName = entityPathArray[0]; + if (!(entityName in allEntitiesDependantsmap)) return; + const entityDependantsMap = allEntitiesDependantsmap[entityName]; + + // goes through properties of an entity + Object.entries(entityDependantsMap).forEach( + ([fullDependencyPath, dependants]) => { + // skip other properties, when searching for a specific entityPath + // e.g. Entity.prop1 should not go through dependants of Entity.prop2 + if ( + entityPathArray.length > 1 && + fullDependencyPath !== fullEntityPath + ) { + return; + } + + // goes through dependants of a property + dependants.forEach((dependantPath) => { + const dependantEntityName = dependantPath.split(".")[0]; + // Marking visited paths to avoid infinite recursion. + if (visitedPaths.has(dependantPath)) { + return; + } + visitedPaths.add(dependantPath); + + dependantEntityNames.add(dependantEntityName); + dependantEntityFullPaths.add(dependantPath); + + const childDependants = getEntityDependants( + [dependantPath], + allEntitiesDependantsmap, + visitedPaths, + ); + childDependants.names.forEach((childDependantName) => { + dependantEntityNames.add(childDependantName); + }); + childDependants.fullPaths.forEach((childDependantPath) => { + dependantEntityFullPaths.add(childDependantPath); + }); + }); + }, + ); + }); + + return { names: dependantEntityNames, fullPaths: dependantEntityFullPaths }; +}; + +export const findLoadingEntities = ( + isLoadingActions: string[], + dataTree: DataTree, + inverseMap: DependencyMap, +): Set => { + const entitiesDependantsMap = groupAndFilterDependantsMap( + inverseMap, + dataTree, + ); + const loadingEntitiesDetails = getEntityDependants( + isLoadingActions, + entitiesDependantsMap, + new Set(), + ); + + // check animateLoading is active on current widgets and set + const filteredLoadingEntityNames = new Set(); + loadingEntitiesDetails.names.forEach((entityName) => { + get(dataTree, [entityName, "animateLoading"]) === true && + filteredLoadingEntityNames.add(entityName); + }); + + return filteredLoadingEntityNames; +}; diff --git a/app/client/src/workers/evaluationUtils.ts b/app/client/src/workers/evaluationUtils.ts index 08de06d1bc..1a48082ed6 100644 --- a/app/client/src/workers/evaluationUtils.ts +++ b/app/client/src/workers/evaluationUtils.ts @@ -27,6 +27,7 @@ import { ValidationConfig } from "constants/PropertyControlConstants"; import { Severity } from "entities/AppsmithConsole"; import { ParsedBody, ParsedJSSubAction } from "utils/JSPaneUtils"; import { Variable } from "entities/JSCollection"; +import { PluginType } from "entities/Action"; const clone = require("rfdc/default"); import { warn as logWarn } from "loglevel"; @@ -285,6 +286,16 @@ export function isJSAction(entity: DataTreeEntity): entity is DataTreeJSAction { ); } +export function isJSObject(entity: DataTreeEntity): entity is DataTreeJSAction { + return ( + typeof entity === "object" && + "ENTITY_TYPE" in entity && + entity.ENTITY_TYPE === ENTITY_TYPE.JSACTION && + "pluginType" in entity && + entity.pluginType === PluginType.JS + ); +} + // We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing // Check issue https://github.com/appsmithorg/appsmith/issues/719 export const removeFunctions = (value: any) => {