> Pull Request Template > > Use this template to quickly create a well written pull request. Delete all quotes before creating the pull request. > ## Description There are multiple refactors and split for query module's creator flow changes which involves module input -- it's a new entity introduced as part of modules project #### PR fixes following issue(s) Fixes # (issue number) Part of https://app.zenhub.com/workspaces/modules-pod-63e0d668a7fea03850c89c6f/issues/gh/appsmithorg/appsmith/27352 #### Type of change - Chore (housekeeping or task changes that don't impact user perception) > > ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] JUnit - [ ] 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: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
526 lines
15 KiB
TypeScript
526 lines
15 KiB
TypeScript
import { PluginType } from "entities/Action";
|
|
import type {
|
|
WidgetEntity,
|
|
ActionEntity,
|
|
JSActionEntity,
|
|
} from "@appsmith/entities/DataTree/types";
|
|
import { ENTITY_TYPE_VALUE } from "entities/DataTree/dataTreeFactory";
|
|
import {
|
|
findLoadingEntities,
|
|
getEntityDependantPaths,
|
|
groupAndFilterDependantsMap,
|
|
} from "utils/WidgetLoadingStateUtils";
|
|
import WidgetFactory from "../WidgetProvider/factory";
|
|
|
|
const JS_object_tree: JSActionEntity = {
|
|
pluginType: PluginType.JS,
|
|
name: "",
|
|
ENTITY_TYPE: ENTITY_TYPE_VALUE.JSACTION,
|
|
body: "",
|
|
meta: {},
|
|
dynamicBindingPathList: [],
|
|
bindingPaths: {},
|
|
reactivePaths: {},
|
|
variables: [],
|
|
dependencyMap: {},
|
|
actionId: "",
|
|
};
|
|
|
|
// @ts-expect-error: meta property not provided
|
|
const Select_tree: WidgetEntity = {
|
|
ENTITY_TYPE: ENTITY_TYPE_VALUE.WIDGET,
|
|
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: ActionEntity = {
|
|
data: {},
|
|
actionId: "",
|
|
config: {},
|
|
run: {},
|
|
clear: {},
|
|
ENTITY_TYPE: ENTITY_TYPE_VALUE.ACTION,
|
|
datasourceUrl: "",
|
|
responseMeta: {
|
|
isExecutionSuccess: true,
|
|
},
|
|
isLoading: false,
|
|
};
|
|
|
|
const Api_tree: ActionEntity = {
|
|
data: {},
|
|
actionId: "",
|
|
config: {},
|
|
run: {},
|
|
clear: {},
|
|
ENTITY_TYPE: ENTITY_TYPE_VALUE.ACTION,
|
|
datasourceUrl: "",
|
|
responseMeta: {
|
|
isExecutionSuccess: true,
|
|
},
|
|
isLoading: false,
|
|
};
|
|
|
|
const Table_tree: WidgetEntity = {
|
|
ENTITY_TYPE: ENTITY_TYPE_VALUE.WIDGET,
|
|
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,
|
|
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", () => {
|
|
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",
|
|
],
|
|
};
|
|
|
|
beforeAll(() => {
|
|
// mock WidgetFactory.getLoadingProperties
|
|
const loadingPropertiesMap = new Map<string, RegExp[]>();
|
|
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(
|
|
["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"]));
|
|
});
|
|
|
|
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", () => {
|
|
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("getEntityDependantPaths", () => {
|
|
// Select1.options -> JS_file.func1 -> Query1.data
|
|
it("handles simple dependency", () => {
|
|
const dependants = getEntityDependantPaths(
|
|
["Query1"],
|
|
{
|
|
Query1: {
|
|
"Query1.data": ["JS_file.func1"],
|
|
},
|
|
JS_file: {
|
|
"JS_file.func1": ["Select1.options"],
|
|
},
|
|
},
|
|
new Set<string>(),
|
|
);
|
|
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 = getEntityDependantPaths(
|
|
["Query1"],
|
|
{
|
|
Query1: {
|
|
"Query1.data": ["JS_file.func1", "JS_file.func2"],
|
|
},
|
|
JS_file: {
|
|
"JS_file.func1": ["Select1.options"],
|
|
"JS_file.func2": ["Select2.options"],
|
|
},
|
|
},
|
|
new Set<string>(),
|
|
);
|
|
expect(dependants).toStrictEqual(
|
|
new Set([
|
|
"JS_file.func1",
|
|
"Select1.options",
|
|
"JS_file.func2",
|
|
"Select2.options",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("handles specific entity paths", () => {
|
|
const dependants = getEntityDependantPaths(
|
|
["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<string>(),
|
|
);
|
|
expect(dependants).toStrictEqual(new Set(["Select2.options"]));
|
|
});
|
|
|
|
// Select1.options -> JS_file.func1 -> JS_file.internalFunc -> Query1.data
|
|
it("handles JS self-dependencies", () => {
|
|
const dependants = getEntityDependantPaths(
|
|
["Query1"],
|
|
{
|
|
Query1: {
|
|
"Query1.data": ["JS_file.internalFunc"],
|
|
},
|
|
JS_file: {
|
|
"JS_file.internalFunc": ["JS_file.func1"],
|
|
"JS_file.func1": ["Select1.options"],
|
|
},
|
|
},
|
|
new Set<string>(),
|
|
);
|
|
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 = getEntityDependantPaths(
|
|
["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<string>(),
|
|
);
|
|
expect(dependants).toStrictEqual(
|
|
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 = getEntityDependantPaths(
|
|
["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<string>(),
|
|
);
|
|
expect(dependants).toStrictEqual(
|
|
new Set(["JS_file.func2", "Select2.options"]),
|
|
);
|
|
});
|
|
});
|
|
});
|