diff --git a/app/client/src/utils/WidgetFactory.tsx b/app/client/src/utils/WidgetFactory.tsx index 52bf48649d..3c02049e09 100644 --- a/app/client/src/utils/WidgetFactory.tsx +++ b/app/client/src/utils/WidgetFactory.tsx @@ -121,6 +121,7 @@ class WidgetFactory { WidgetType, readonly PropertyPaneConfig[] > = new Map(); + static loadingProperties: Map> = new Map(); static widgetConfigMap: Map< WidgetType, @@ -134,6 +135,7 @@ class WidgetFactory { defaultPropertiesMap: Record, metaPropertiesMap: Record, propertyPaneConfig?: PropertyPaneConfig[], + loadingProperties?: Array, ) { if (!this.widgetTypes[widgetType]) { this.widgetTypes[widgetType] = widgetType; @@ -141,6 +143,8 @@ class WidgetFactory { this.derivedPropertiesMap.set(widgetType, derivedPropertiesMap); this.defaultPropertiesMap.set(widgetType, defaultPropertiesMap); this.metaPropertiesMap.set(widgetType, metaPropertiesMap); + loadingProperties && + this.loadingProperties.set(widgetType, loadingProperties); if (propertyPaneConfig) { const validatedPropertyPaneConfig = validatePropertyPaneConfig( @@ -235,6 +239,10 @@ class WidgetFactory { return map; } + static getLoadingProperties(type: WidgetType): Array | undefined { + return this.loadingProperties.get(type); + } + static getWidgetTypeConfigMap(): WidgetTypeConfigMap { const typeConfigMap: WidgetTypeConfigMap = {}; WidgetFactory.getWidgetTypes().forEach((type) => { diff --git a/app/client/src/utils/WidgetLoadingStateUtils.test.ts b/app/client/src/utils/WidgetLoadingStateUtils.test.ts index 52995d3b5a..8830caa806 100644 --- a/app/client/src/utils/WidgetLoadingStateUtils.test.ts +++ b/app/client/src/utils/WidgetLoadingStateUtils.test.ts @@ -7,9 +7,10 @@ import { } from "entities/DataTree/dataTreeFactory"; import { findLoadingEntities, - getEntityDependants, + getEntityDependantPaths, groupAndFilterDependantsMap, } from "utils/WidgetLoadingStateUtils"; +import WidgetFactory from "./WidgetFactory"; const JS_object_tree: DataTreeJSAction = { pluginType: PluginType.JS, @@ -68,14 +69,61 @@ const Query_tree: DataTreeAction = { isLoading: false, }; +const Api_tree: DataTreeAction = { + data: {}, + actionId: "", + config: {}, + pluginType: PluginType.API, + pluginId: "", + name: "", + run: {}, + clear: {}, + dynamicBindingPathList: [], + bindingPaths: {}, + ENTITY_TYPE: ENTITY_TYPE.ACTION, + dependencyMap: {}, + logBlackList: {}, + datasourceUrl: "", + responseMeta: { + isExecutionSuccess: true, + }, + isLoading: false, +}; + +const Table_tree: DataTreeWidget = { + ENTITY_TYPE: ENTITY_TYPE.WIDGET, + bindingPaths: {}, + triggerPaths: {}, + validationPaths: {}, + logBlackList: {}, + propertyOverrideDependency: {}, + overridingPropertyPaths: {}, + privateWidgets: {}, + widgetId: "", + type: "TABLE_WIDGET", + widgetName: "", + renderMode: "CANVAS", + version: 0, + parentColumnSpace: 0, + parentRowSpace: 0, + leftColumn: 0, + rightColumn: 0, + topRow: 0, + bottomRow: 0, + isLoading: false, + animateLoading: true, +}; + 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" }, + Table1: { ...Table_tree, name: "Table1" }, Query1: { ...Query_tree, name: "Query1" }, Query2: { ...Query_tree, name: "Query2" }, Query3: { ...Query_tree, name: "Query3" }, + Api1: { ...Api_tree, name: "Api1" }, }; describe("Widget loading state utils", () => { @@ -116,6 +164,22 @@ describe("Widget loading state utils", () => { ], }; + beforeAll(() => { + // mock WidgetFactory.getLoadingProperties + const loadingPropertiesMap = new Map(); + loadingPropertiesMap.set("TABLE_WIDGET", [/.tableData$/]); + + jest + .spyOn(WidgetFactory, "getLoadingProperties") + .mockImplementation((widgetType) => + loadingPropertiesMap.get(widgetType), + ); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + // Select1.options -> JS_file.func1 -> Query1.data it("handles linear dependencies", () => { const loadingEntites = findLoadingEntities( @@ -247,6 +311,20 @@ describe("Widget loading state utils", () => { }); expect(loadingEntites).toStrictEqual(new Set(["Select2"])); }); + + it("includes loading properties", () => { + const loadingEntites = findLoadingEntities(["Api1"], baseDataTree, { + "Api1.data": ["Table1.tableData"], + }); + expect(loadingEntites).toStrictEqual(new Set(["Table1"])); + }); + + it("ignores non-loading properties", () => { + const loadingEntites = findLoadingEntities(["Api1"], baseDataTree, { + "Api1.run": ["Table1.primaryColumns.action.onClick"], + }); + expect(loadingEntites).toStrictEqual(new Set()); + }); }); describe("groupAndFilterDependantsMap", () => { @@ -327,10 +405,10 @@ describe("Widget loading state utils", () => { }); }); - describe("getEntityDependants", () => { + describe("getEntityDependantPaths", () => { // Select1.options -> JS_file.func1 -> Query1.data it("handles simple dependency", () => { - const dependants = getEntityDependants( + const dependants = getEntityDependantPaths( ["Query1"], { Query1: { @@ -342,16 +420,15 @@ describe("Widget loading state utils", () => { }, new Set(), ); - expect(dependants).toStrictEqual({ - names: new Set(["JS_file", "Select1"]), - fullPaths: new Set(["JS_file.func1", "Select1.options"]), - }); + expect(dependants).toStrictEqual( + 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( + const dependants = getEntityDependantPaths( ["Query1"], { Query1: { @@ -364,19 +441,18 @@ describe("Widget loading state utils", () => { }, new Set(), ); - expect(dependants).toStrictEqual({ - names: new Set(["JS_file", "Select1", "Select2"]), - fullPaths: new Set([ + expect(dependants).toStrictEqual( + new Set([ "JS_file.func1", "Select1.options", "JS_file.func2", "Select2.options", ]), - }); + ); }); it("handles specific entity paths", () => { - const dependants = getEntityDependants( + const dependants = getEntityDependantPaths( ["JS_file.func2"], // specific path { Query1: { @@ -392,15 +468,12 @@ describe("Widget loading state utils", () => { }, new Set(), ); - expect(dependants).toStrictEqual({ - names: new Set(["Select2"]), - fullPaths: new Set(["Select2.options"]), - }); + expect(dependants).toStrictEqual(new Set(["Select2.options"])); }); // Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data it("handles JS self-dependencies", () => { - const dependants = getEntityDependants( + const dependants = getEntityDependantPaths( ["Query1"], { Query1: { @@ -413,19 +486,14 @@ describe("Widget loading state utils", () => { }, new Set(), ); - expect(dependants).toStrictEqual({ - names: new Set(["JS_file", "Select1"]), - fullPaths: new Set([ - "JS_file.internalFunc", - "JS_file.func1", - "Select1.options", - ]), - }); + expect(dependants).toStrictEqual( + 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( + const dependants = getEntityDependantPaths( ["Query1"], { Query1: { @@ -439,15 +507,14 @@ describe("Widget loading state utils", () => { }, new Set(), ); - expect(dependants).toStrictEqual({ - names: new Set(["JS_file", "Select1"]), - fullPaths: new Set([ + expect(dependants).toStrictEqual( + new Set([ "JS_file.internalFunc1", "JS_file.internalFunc2", "JS_file.func", "Select1.options", ]), - }); + ); }); /* Select1.options -> JS.func1 -> Query1.data, @@ -458,7 +525,7 @@ describe("Widget loading state utils", () => { Only Select2 should be listed, not Select1. */ it("handles selective dependencies in same JS file", () => { - const dependants = getEntityDependants( + const dependants = getEntityDependantPaths( ["Query2"], { Query1: { @@ -474,10 +541,9 @@ describe("Widget loading state utils", () => { }, new Set(), ); - expect(dependants).toStrictEqual({ - names: new Set(["JS_file", "Select2"]), - fullPaths: new Set(["JS_file.func2", "Select2.options"]), - }); + expect(dependants).toStrictEqual( + new Set(["JS_file.func2", "Select2.options"]), + ); }); }); }); diff --git a/app/client/src/utils/WidgetLoadingStateUtils.ts b/app/client/src/utils/WidgetLoadingStateUtils.ts index 1e314421d9..d93bda3f92 100644 --- a/app/client/src/utils/WidgetLoadingStateUtils.ts +++ b/app/client/src/utils/WidgetLoadingStateUtils.ts @@ -2,6 +2,7 @@ import { DataTree } from "entities/DataTree/dataTreeFactory"; import { get, set } from "lodash"; import { isJSObject } from "workers/evaluationUtils"; import { DependencyMap } from "./DynamicBindingUtils"; +import WidgetFactory from "./WidgetFactory"; type GroupedDependencyMap = Record; @@ -57,14 +58,13 @@ export const groupAndFilterDependantsMap = ( 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 = ( +// get entity paths that depend on a given list of entites +// e.g. widget paths that depend on a list of actions +export const getEntityDependantPaths = ( fullEntityPaths: string[], allEntitiesDependantsmap: GroupedDependencyMap, visitedPaths: Set, -): { names: Set; fullPaths: Set } => { - const dependantEntityNames = new Set(); +): Set => { const dependantEntityFullPaths = new Set(); fullEntityPaths.forEach((fullEntityPath) => { @@ -87,25 +87,20 @@ export const getEntityDependants = ( // 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( + const childDependants = getEntityDependantPaths( [dependantPath], allEntitiesDependantsmap, visitedPaths, ); - childDependants.names.forEach((childDependantName) => { - dependantEntityNames.add(childDependantName); - }); - childDependants.fullPaths.forEach((childDependantPath) => { + childDependants.forEach((childDependantPath) => { dependantEntityFullPaths.add(childDependantPath); }); }); @@ -113,7 +108,7 @@ export const getEntityDependants = ( ); }); - return { names: dependantEntityNames, fullPaths: dependantEntityFullPaths }; + return dependantEntityFullPaths; }; export const findLoadingEntities = ( @@ -125,15 +120,29 @@ export const findLoadingEntities = ( inverseMap, dataTree, ); - const loadingEntitiesDetails = getEntityDependants( + const loadingEntityPaths = getEntityDependantPaths( isLoadingActions, entitiesDependantsMap, new Set(), ); - - // check animateLoading is active on current widgets and set const filteredLoadingEntityNames = new Set(); - loadingEntitiesDetails.names.forEach((entityName) => { + + loadingEntityPaths.forEach((entityPath) => { + const entityPathArray = entityPath.split("."); + const entityName = entityPathArray[0]; + const widgetType = get(dataTree, [entityName, "type"]); + const loadingProperties = WidgetFactory.getLoadingProperties(widgetType); + + // check if propertyPath is listed in widgetConfig + if ( + entityPathArray.length > 1 && + loadingProperties && + !loadingProperties.find((propRegExp) => propRegExp.test(entityPath)) + ) { + return; + } + + // check animateLoading is active on current widgets and set get(dataTree, [entityName, "animateLoading"]) === true && filteredLoadingEntityNames.add(entityName); }); diff --git a/app/client/src/utils/WidgetRegisterHelpers.tsx b/app/client/src/utils/WidgetRegisterHelpers.tsx index 68f8b69985..6fddc07089 100644 --- a/app/client/src/utils/WidgetRegisterHelpers.tsx +++ b/app/client/src/utils/WidgetRegisterHelpers.tsx @@ -26,6 +26,7 @@ export interface WidgetConfiguration { default: Record; meta: Record; derived: DerivedPropertiesMap; + loadingProperties?: Array; }; } @@ -55,6 +56,7 @@ export const registerWidget = (Widget: any, config: WidgetConfiguration) => { config.properties.default, config.properties.meta, config.properties.config, + config.properties.loadingProperties, ); configureWidget(config); }; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index fa133715ea..7b30aca4ca 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -75,6 +75,20 @@ abstract class BaseWidget< return {}; } + /** + * getLoadingProperties returns a list of regexp's used to specify bindingPaths, + * which can set the isLoading prop of the widget. + * When: + * 1. the path is bound to an action (API/Query) + * 2. the action is currently in-progress + * + * if undefined, all paths can set the isLoading state + * if empty array, no paths can set the isLoading state + */ + static getLoadingProperties(): Array | undefined { + return; + } + /** * Widget abstraction to register the widget type * ```javascript diff --git a/app/client/src/widgets/TableWidget/index.ts b/app/client/src/widgets/TableWidget/index.ts index a13859de81..fc41a41ffe 100644 --- a/app/client/src/widgets/TableWidget/index.ts +++ b/app/client/src/widgets/TableWidget/index.ts @@ -180,6 +180,7 @@ export const CONFIG = { default: Widget.getDefaultPropertiesMap(), meta: Widget.getMetaPropertiesMap(), config: Widget.getPropertyPaneConfig(), + loadingProperties: Widget.getLoadingProperties(), }, }; diff --git a/app/client/src/widgets/TableWidget/widget/index.tsx b/app/client/src/widgets/TableWidget/widget/index.tsx index b7d10ed642..03e769fb92 100644 --- a/app/client/src/widgets/TableWidget/widget/index.tsx +++ b/app/client/src/widgets/TableWidget/widget/index.tsx @@ -109,6 +109,10 @@ class TableWidget extends BaseWidget { }; } + static getLoadingProperties(): Array | undefined { + return [/.tableData$/]; + } + getTableColumns = () => { let columns: ReactTableColumnProps[] = []; const hiddenColumns: ReactTableColumnProps[] = []; diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts index 041c42e212..1362f34d01 100644 --- a/app/client/src/workers/DataTreeEvaluator.ts +++ b/app/client/src/workers/DataTreeEvaluator.ts @@ -519,12 +519,22 @@ export default class DataTreeEvaluator { if (isWidget(entity)) { // Adding the dynamic triggers in the dependency list as they need linting whenever updated - // we don't make it dependent on anything else - if (entity.dynamicTriggerPathList) { - Object.values(entity.dynamicTriggerPathList).forEach(({ key }) => { - dependencies[`${entityName}.${key}`] = []; + // To keep linting in trigger fields in sync, nodes they depend on need to be added to their dependencies + const dynamicTriggerPathlist = entity.dynamicTriggerPathList; + + if (dynamicTriggerPathlist && dynamicTriggerPathlist.length) { + dynamicTriggerPathlist.forEach((dynamicPath) => { + const propertyPath = dynamicPath.key; + const unevalPropValue = _.get(entity, propertyPath); + const { jsSnippets } = getDynamicBindings(unevalPropValue); + const existingDeps = + dependencies[`${entityName}.${propertyPath}`] || []; + dependencies[`${entityName}.${propertyPath}`] = existingDeps.concat( + jsSnippets.filter((jsSnippet) => !!jsSnippet), + ); }); } + const widgetDependencies = addWidgetPropertyDependencies({ entity, entityName, @@ -1333,7 +1343,10 @@ export default class DataTreeEvaluator { entity, entityPropertyPath, ); - if (isABindingPath) { + const isATriggerPath = + isWidget(entity) && + isPathADynamicTrigger(entity, entityPropertyPath); + if (isABindingPath || isATriggerPath) { didUpdateDependencyMap = true; const { jsSnippets } = getDynamicBindings(