diff --git a/app/client/src/utils/WidgetFactory.tsx b/app/client/src/utils/WidgetFactory.tsx index f2fbbc7ecd..459efac818 100644 --- a/app/client/src/utils/WidgetFactory.tsx +++ b/app/client/src/utils/WidgetFactory.tsx @@ -48,6 +48,7 @@ class WidgetFactory { WidgetType, readonly PropertyPaneConfig[] > = new Map(); + static loadingProperties: Map> = new Map(); static widgetConfigMap: Map< WidgetType, @@ -64,6 +65,7 @@ class WidgetFactory { propertyPaneContentConfig?: PropertyPaneConfig[], propertyPaneStyleConfig?: PropertyPaneConfig[], features?: WidgetFeatures, + loadingProperties?: Array, ) { if (!this.widgetTypes[widgetType]) { this.widgetTypes[widgetType] = widgetType; @@ -71,6 +73,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 enhancedPropertyPaneConfig = enhancePropertyPaneConfig( @@ -244,6 +248,10 @@ class WidgetFactory { }); return typeConfigMap; } + + static getLoadingProperties(type: WidgetType): Array | undefined { + return this.loadingProperties.get(type); + } } export type WidgetTypeConfigMap = Record< diff --git a/app/client/src/utils/WidgetLoadingStateUtils.test.ts b/app/client/src/utils/WidgetLoadingStateUtils.test.ts index 96add00169..2e4937176c 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, @@ -72,14 +73,64 @@ 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, + reactivePaths: {}, +}; + +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, + reactivePaths: {}, + meta: {}, +}; + 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", () => { @@ -120,6 +171,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( @@ -251,6 +318,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", () => { @@ -331,10 +412,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: { @@ -346,16 +427,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: { @@ -368,19 +448,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: { @@ -396,15 +475,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: { @@ -417,19 +493,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: { @@ -443,15 +514,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, @@ -462,7 +532,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: { @@ -478,10 +548,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..3436145702 100644 --- a/app/client/src/utils/WidgetLoadingStateUtils.ts +++ b/app/client/src/utils/WidgetLoadingStateUtils.ts @@ -1,7 +1,8 @@ import { DataTree } from "entities/DataTree/dataTreeFactory"; import { get, set } from "lodash"; -import { isJSObject } from "workers/evaluationUtils"; +import { isJSObject, isWidget } from "workers/evaluationUtils"; import { DependencyMap } from "./DynamicBindingUtils"; +import WidgetFactory from "./WidgetFactory"; type GroupedDependencyMap = Record; @@ -57,13 +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 } => { +): Set => { const dependantEntityNames = new Set(); const dependantEntityFullPaths = new Set(); @@ -97,15 +98,13 @@ export const getEntityDependants = ( 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,11 +112,11 @@ export const getEntityDependants = ( ); }); - return { names: dependantEntityNames, fullPaths: dependantEntityFullPaths }; + return dependantEntityFullPaths; }; export const findLoadingEntities = ( - isLoadingActions: string[], + loadingActions: string[], dataTree: DataTree, inverseMap: DependencyMap, ): Set => { @@ -125,15 +124,33 @@ export const findLoadingEntities = ( inverseMap, dataTree, ); - const loadingEntitiesDetails = getEntityDependants( - isLoadingActions, + const loadingEntityPaths = getEntityDependantPaths( + loadingActions, 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 widget = get(dataTree, [entityName]); + if (isWidget(widget)) { + const loadingProperties = WidgetFactory.getLoadingProperties(widget.type); + + // check if propertyPath is listed in widgetConfig + if ( + entityPathArray.length > 1 && + loadingProperties && + !loadingProperties.some((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 71e3f75465..e3fbcd1dd1 100644 --- a/app/client/src/utils/WidgetRegisterHelpers.tsx +++ b/app/client/src/utils/WidgetRegisterHelpers.tsx @@ -44,6 +44,7 @@ export const registerWidget = (Widget: any, config: WidgetConfiguration) => { config.properties.contentConfig, config.properties.styleConfig, config.features, + config.properties.loadingProperties, ); configureWidget(config); }; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index be02526017..b9482f8602 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -77,6 +77,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/TableWidgetV2/index.ts b/app/client/src/widgets/TableWidgetV2/index.ts index 0856f8385c..a0de3ee218 100644 --- a/app/client/src/widgets/TableWidgetV2/index.ts +++ b/app/client/src/widgets/TableWidgetV2/index.ts @@ -229,6 +229,7 @@ export const CONFIG = { config: Widget.getPropertyPaneConfig(), contentConfig: Widget.getPropertyPaneContentConfig(), styleConfig: Widget.getPropertyPaneStyleConfig(), + loadingProperties: Widget.getLoadingProperties(), }, }; diff --git a/app/client/src/widgets/TableWidgetV2/widget/index.tsx b/app/client/src/widgets/TableWidgetV2/widget/index.tsx index e7d42e2ff6..501e3218ab 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/widget/index.tsx @@ -140,6 +140,10 @@ class TableWidgetV2 extends BaseWidget { }; } + static getLoadingProperties(): Array | undefined { + return [/\.tableData$/]; + } + /* * Function to get the table columns with appropriate render functions * based on columnType diff --git a/app/client/src/widgets/constants.ts b/app/client/src/widgets/constants.ts index ad657dc085..a32422565f 100644 --- a/app/client/src/widgets/constants.ts +++ b/app/client/src/widgets/constants.ts @@ -26,6 +26,7 @@ export interface WidgetConfiguration { default: Record; meta: Record; derived: DerivedPropertiesMap; + loadingProperties?: Array; }; }