We are changing how we maintain meta values in our architecture, Earlier metaHOC component state was used to store meta values that were debounced and pushed in batch to metaReducer and trigger evaluation. We remove the metaHOC state and directly update values to metaReducer and trigger evaluation separately via a debouched function.
541 lines
14 KiB
TypeScript
541 lines
14 KiB
TypeScript
import { DependencyMap } from "utils/DynamicBindingUtils";
|
|
import { RenderModes } from "constants/WidgetConstants";
|
|
import { ValidationTypes } from "constants/WidgetValidation";
|
|
import {
|
|
DataTreeWidget,
|
|
ENTITY_TYPE,
|
|
EvaluationSubstitutionType,
|
|
PrivateWidgets,
|
|
} from "entities/DataTree/dataTreeFactory";
|
|
import {
|
|
DataTreeDiff,
|
|
DataTreeDiffEvent,
|
|
getAllPaths,
|
|
getAllPrivateWidgetsInDataTree,
|
|
getDataTreeWithoutPrivateWidgets,
|
|
isPrivateEntityPath,
|
|
makeParentsDependOnChildren,
|
|
translateDiffEventToDataTreeDiffEvent,
|
|
} from "./evaluationUtils";
|
|
import { warn as logWarn } from "loglevel";
|
|
import { Diff } from "deep-diff";
|
|
import { flatten } from "lodash";
|
|
import { overrideWidgetProperties } from "./evaluationUtils";
|
|
import { DataTree } from "entities/DataTree/dataTreeFactory";
|
|
import { EvalMetaUpdates } from "./DataTreeEvaluator/types";
|
|
import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget";
|
|
import TableWidget, { CONFIG as TableWidgetConfig } from "widgets/TableWidget";
|
|
import InputWidget, {
|
|
CONFIG as InputWidgetV2Config,
|
|
} from "widgets/InputWidgetV2";
|
|
import { registerWidget } from "utils/WidgetRegisterHelpers";
|
|
|
|
// to check if logWarn was called.
|
|
// use jest.unmock, if the mock needs to be removed.
|
|
jest.mock("loglevel");
|
|
|
|
const BASE_WIDGET: DataTreeWidget = {
|
|
logBlackList: {},
|
|
widgetId: "randomID",
|
|
widgetName: "randomWidgetName",
|
|
bottomRow: 0,
|
|
isLoading: false,
|
|
leftColumn: 0,
|
|
parentColumnSpace: 0,
|
|
parentRowSpace: 0,
|
|
renderMode: RenderModes.CANVAS,
|
|
rightColumn: 0,
|
|
topRow: 0,
|
|
type: "SKELETON_WIDGET",
|
|
parentId: "0",
|
|
version: 1,
|
|
bindingPaths: {},
|
|
reactivePaths: {},
|
|
triggerPaths: {},
|
|
validationPaths: {},
|
|
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
|
|
privateWidgets: {},
|
|
propertyOverrideDependency: {},
|
|
overridingPropertyPaths: {},
|
|
meta: {},
|
|
};
|
|
|
|
const testDataTree: Record<string, DataTreeWidget> = {
|
|
Text1: {
|
|
...BASE_WIDGET,
|
|
widgetName: "Text1",
|
|
text: "Label",
|
|
type: "TEXT_WIDGET",
|
|
reactivePaths: {
|
|
text: EvaluationSubstitutionType.TEMPLATE,
|
|
},
|
|
validationPaths: {
|
|
text: { type: ValidationTypes.TEXT },
|
|
},
|
|
},
|
|
Text2: {
|
|
...BASE_WIDGET,
|
|
widgetName: "Text2",
|
|
text: "{{Text1.text}}",
|
|
dynamicBindingPathList: [{ key: "text" }],
|
|
type: "TEXT_WIDGET",
|
|
reactivePaths: {
|
|
text: EvaluationSubstitutionType.TEMPLATE,
|
|
},
|
|
validationPaths: {
|
|
text: { type: ValidationTypes.TEXT },
|
|
},
|
|
},
|
|
Text3: {
|
|
...BASE_WIDGET,
|
|
widgetName: "Text3",
|
|
text: "{{Text1.text}}",
|
|
dynamicBindingPathList: [{ key: "text" }],
|
|
type: "TEXT_WIDGET",
|
|
reactivePaths: {
|
|
text: EvaluationSubstitutionType.TEMPLATE,
|
|
},
|
|
validationPaths: {
|
|
text: { type: ValidationTypes.TEXT },
|
|
},
|
|
},
|
|
Text4: {
|
|
...BASE_WIDGET,
|
|
widgetName: "Text4",
|
|
text: "{{Text1.text}}",
|
|
dynamicBindingPathList: [{ key: "text" }],
|
|
type: "TEXT_WIDGET",
|
|
reactivePaths: {
|
|
text: EvaluationSubstitutionType.TEMPLATE,
|
|
},
|
|
validationPaths: {
|
|
text: { type: ValidationTypes.TEXT },
|
|
},
|
|
},
|
|
|
|
List1: {
|
|
...BASE_WIDGET,
|
|
privateWidgets: {
|
|
Text2: true,
|
|
},
|
|
},
|
|
List2: {
|
|
...BASE_WIDGET,
|
|
privateWidgets: {
|
|
Text3: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
describe("Correctly handle paths", () => {
|
|
it("getsAllPaths", () => {
|
|
const myTree = {
|
|
WidgetName: {
|
|
1: "yo",
|
|
name: "WidgetName",
|
|
objectProperty: {
|
|
childObjectProperty: [
|
|
"1",
|
|
1,
|
|
{
|
|
key: "value",
|
|
2: 1,
|
|
},
|
|
["1", "2"],
|
|
],
|
|
},
|
|
},
|
|
};
|
|
const result = {
|
|
WidgetName: true,
|
|
"WidgetName.1": true,
|
|
"WidgetName.name": true,
|
|
"WidgetName.objectProperty": true,
|
|
"WidgetName.objectProperty.childObjectProperty": true,
|
|
"WidgetName.objectProperty.childObjectProperty[0]": true,
|
|
"WidgetName.objectProperty.childObjectProperty[1]": true,
|
|
"WidgetName.objectProperty.childObjectProperty[2]": true,
|
|
"WidgetName.objectProperty.childObjectProperty[2].key": true,
|
|
"WidgetName.objectProperty.childObjectProperty[2].2": true,
|
|
"WidgetName.objectProperty.childObjectProperty[3]": true,
|
|
"WidgetName.objectProperty.childObjectProperty[3][0]": true,
|
|
"WidgetName.objectProperty.childObjectProperty[3][1]": true,
|
|
};
|
|
|
|
const actual = getAllPaths(myTree);
|
|
expect(actual).toStrictEqual(result);
|
|
});
|
|
});
|
|
|
|
describe("privateWidgets", () => {
|
|
it("correctly checks if path is a PrivateEntityPath", () => {
|
|
const privateWidgets: PrivateWidgets = {
|
|
Button1: true,
|
|
Image1: true,
|
|
Button2: true,
|
|
Image2: true,
|
|
};
|
|
|
|
expect(
|
|
isPrivateEntityPath(privateWidgets, "List1.template.Button1.text"),
|
|
).toBeFalsy();
|
|
expect(isPrivateEntityPath(privateWidgets, "Button1.text")).toBeTruthy();
|
|
expect(
|
|
isPrivateEntityPath(privateWidgets, "List2.template.Image2.data"),
|
|
).toBeFalsy();
|
|
expect(isPrivateEntityPath(privateWidgets, "Image2.data")).toBeTruthy();
|
|
});
|
|
|
|
it("Returns list of all privateWidgets", () => {
|
|
const expectedPrivateWidgetsList = {
|
|
Text2: true,
|
|
Text3: true,
|
|
};
|
|
|
|
const actualPrivateWidgetsList = getAllPrivateWidgetsInDataTree(
|
|
testDataTree,
|
|
);
|
|
|
|
expect(expectedPrivateWidgetsList).toStrictEqual(actualPrivateWidgetsList);
|
|
});
|
|
|
|
it("Returns data tree without privateWidgets", () => {
|
|
const expectedDataTreeWithoutPrivateWidgets: Record<
|
|
string,
|
|
DataTreeWidget
|
|
> = {
|
|
Text1: {
|
|
...BASE_WIDGET,
|
|
widgetName: "Text1",
|
|
text: "Label",
|
|
type: "TEXT_WIDGET",
|
|
reactivePaths: {
|
|
text: EvaluationSubstitutionType.TEMPLATE,
|
|
},
|
|
validationPaths: {
|
|
text: { type: ValidationTypes.TEXT },
|
|
},
|
|
},
|
|
|
|
Text4: {
|
|
...BASE_WIDGET,
|
|
widgetName: "Text4",
|
|
text: "{{Text1.text}}",
|
|
dynamicBindingPathList: [{ key: "text" }],
|
|
type: "TEXT_WIDGET",
|
|
reactivePaths: {
|
|
text: EvaluationSubstitutionType.TEMPLATE,
|
|
},
|
|
validationPaths: {
|
|
text: { type: ValidationTypes.TEXT },
|
|
},
|
|
},
|
|
|
|
List1: {
|
|
...BASE_WIDGET,
|
|
privateWidgets: {
|
|
Text2: true,
|
|
},
|
|
},
|
|
List2: {
|
|
...BASE_WIDGET,
|
|
privateWidgets: {
|
|
Text3: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
const actualDataTreeWithoutPrivateWidgets = getDataTreeWithoutPrivateWidgets(
|
|
testDataTree,
|
|
);
|
|
|
|
expect(expectedDataTreeWithoutPrivateWidgets).toStrictEqual(
|
|
actualDataTreeWithoutPrivateWidgets,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("makeParentsDependOnChildren", () => {
|
|
it("makes parent properties depend on child properties", () => {
|
|
let depMap: DependencyMap = {
|
|
Widget1: [],
|
|
"Widget1.defaultText": [],
|
|
"Widget1.defaultText.abc": [],
|
|
};
|
|
const allkeys: Record<string, true> = {
|
|
Widget1: true,
|
|
"Widget1.defaultText": true,
|
|
"Widget1.defaultText.abc": true,
|
|
};
|
|
depMap = makeParentsDependOnChildren(depMap, allkeys);
|
|
expect(depMap).toStrictEqual({
|
|
Widget1: ["Widget1.defaultText"],
|
|
"Widget1.defaultText": ["Widget1.defaultText.abc"],
|
|
"Widget1.defaultText.abc": [],
|
|
});
|
|
});
|
|
|
|
it("logs warning for child properties not listed in allKeys", () => {
|
|
const depMap: DependencyMap = {
|
|
Widget1: [],
|
|
"Widget1.defaultText": [],
|
|
};
|
|
const allkeys: Record<string, true> = {
|
|
Widget1: true,
|
|
};
|
|
makeParentsDependOnChildren(depMap, allkeys);
|
|
expect(logWarn).toBeCalledWith(
|
|
"makeParentsDependOnChild - Widget1.defaultText is not present in dataTree.",
|
|
"This might result in a cyclic dependency.",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("translateDiffEvent", () => {
|
|
it("noop when diff path does not exist", () => {
|
|
const noDiffPath: Diff<any, any> = {
|
|
kind: "E",
|
|
lhs: undefined,
|
|
rhs: undefined,
|
|
};
|
|
const result = translateDiffEventToDataTreeDiffEvent(noDiffPath, {});
|
|
expect(result).toStrictEqual({
|
|
payload: {
|
|
propertyPath: "",
|
|
value: "",
|
|
},
|
|
event: DataTreeDiffEvent.NOOP,
|
|
});
|
|
});
|
|
it("translates new and delete events", () => {
|
|
const diffs: Diff<any, any>[] = [
|
|
{
|
|
kind: "N",
|
|
path: ["Widget1"],
|
|
rhs: {},
|
|
},
|
|
{
|
|
kind: "N",
|
|
path: ["Widget1", "name"],
|
|
rhs: "Widget1",
|
|
},
|
|
{
|
|
kind: "D",
|
|
path: ["Widget1"],
|
|
lhs: {},
|
|
},
|
|
{
|
|
kind: "D",
|
|
path: ["Widget1", "name"],
|
|
lhs: "Widget1",
|
|
},
|
|
{
|
|
kind: "E",
|
|
path: ["Widget2", "name"],
|
|
rhs: "test",
|
|
lhs: "test2",
|
|
},
|
|
];
|
|
|
|
const expectedTranslations: DataTreeDiff[] = [
|
|
{
|
|
payload: {
|
|
propertyPath: "Widget1",
|
|
},
|
|
event: DataTreeDiffEvent.NEW,
|
|
},
|
|
{
|
|
payload: {
|
|
propertyPath: "Widget1.name",
|
|
},
|
|
event: DataTreeDiffEvent.NEW,
|
|
},
|
|
{
|
|
payload: {
|
|
propertyPath: "Widget1",
|
|
},
|
|
event: DataTreeDiffEvent.DELETE,
|
|
},
|
|
{
|
|
payload: {
|
|
propertyPath: "Widget1.name",
|
|
},
|
|
event: DataTreeDiffEvent.DELETE,
|
|
},
|
|
{
|
|
payload: {
|
|
propertyPath: "",
|
|
value: "",
|
|
},
|
|
event: DataTreeDiffEvent.NOOP,
|
|
},
|
|
];
|
|
|
|
const actualTranslations = flatten(
|
|
diffs.map((diff) => translateDiffEventToDataTreeDiffEvent(diff, {})),
|
|
);
|
|
|
|
expect(expectedTranslations).toStrictEqual(actualTranslations);
|
|
});
|
|
|
|
it("properly categorises the edit events", () => {
|
|
const diffs: Diff<any, any>[] = [
|
|
{
|
|
kind: "E",
|
|
path: ["Widget2", "name"],
|
|
rhs: "test",
|
|
lhs: "test2",
|
|
},
|
|
];
|
|
|
|
const expectedTranslations: DataTreeDiff[] = [
|
|
{
|
|
payload: {
|
|
propertyPath: "",
|
|
value: "",
|
|
},
|
|
event: DataTreeDiffEvent.NOOP,
|
|
},
|
|
];
|
|
|
|
const actualTranslations = flatten(
|
|
diffs.map((diff) => translateDiffEventToDataTreeDiffEvent(diff, {})),
|
|
);
|
|
|
|
expect(expectedTranslations).toStrictEqual(actualTranslations);
|
|
});
|
|
});
|
|
|
|
describe("overrideWidgetProperties", () => {
|
|
beforeAll(() => {
|
|
registerWidget(TableWidget, TableWidgetConfig);
|
|
registerWidget(InputWidget, InputWidgetV2Config);
|
|
});
|
|
|
|
describe("1. Input widget ", () => {
|
|
const currentTree: DataTree = {};
|
|
beforeAll(() => {
|
|
const inputWidgetDataTree = generateDataTreeWidget(
|
|
{
|
|
type: InputWidget.getWidgetType(),
|
|
widgetId: "egwwwfgab",
|
|
widgetName: "Input1",
|
|
children: [],
|
|
},
|
|
{},
|
|
);
|
|
currentTree["Input1"] = inputWidgetDataTree;
|
|
});
|
|
// When default text is re-evaluated it will override values of meta.text and text in InputWidget
|
|
it("1. defaultText updating meta.text and text", () => {
|
|
const evalMetaUpdates: EvalMetaUpdates = [];
|
|
const overwriteObj = overrideWidgetProperties({
|
|
currentTree,
|
|
entity: currentTree.Input1 as DataTreeWidget,
|
|
propertyPath: "defaultText",
|
|
value: "abcde",
|
|
evalMetaUpdates,
|
|
});
|
|
|
|
expect(overwriteObj).toStrictEqual(undefined);
|
|
|
|
expect(evalMetaUpdates).toStrictEqual([
|
|
{
|
|
widgetId: currentTree.Input1.widgetId,
|
|
metaPropertyPath: ["inputText"],
|
|
value: "abcde",
|
|
},
|
|
{
|
|
widgetId: currentTree.Input1.widgetId,
|
|
metaPropertyPath: ["text"],
|
|
value: "abcde",
|
|
},
|
|
]);
|
|
|
|
expect(currentTree.Input1.meta).toStrictEqual({
|
|
text: "abcde",
|
|
inputText: "abcde",
|
|
});
|
|
});
|
|
// When meta.text is re-evaluated it will override values text in InputWidget
|
|
it("2. meta.text updating text", () => {
|
|
const evalMetaUpdates: EvalMetaUpdates = [];
|
|
const overwriteObj = overrideWidgetProperties({
|
|
currentTree,
|
|
entity: currentTree.Input1 as DataTreeWidget,
|
|
propertyPath: "meta.text",
|
|
value: "abcdefg",
|
|
evalMetaUpdates,
|
|
});
|
|
|
|
expect(overwriteObj).toStrictEqual(undefined);
|
|
|
|
expect(evalMetaUpdates).toStrictEqual([]);
|
|
|
|
expect(currentTree.Input1.text).toStrictEqual("abcdefg");
|
|
});
|
|
});
|
|
|
|
describe("2. Table widget ", () => {
|
|
const currentTree: DataTree = {};
|
|
beforeAll(() => {
|
|
const tableWidgetDataTree = generateDataTreeWidget(
|
|
{
|
|
type: TableWidget.getWidgetType(),
|
|
widgetId: "random",
|
|
widgetName: "Table1",
|
|
children: [],
|
|
},
|
|
{},
|
|
);
|
|
currentTree["Table1"] = tableWidgetDataTree;
|
|
});
|
|
// When default defaultSelectedRow is re-evaluated it will override values of meta.selectedRowIndices, selectedRowIndices, meta.selectedRowIndex & selectedRowIndex.
|
|
it("1. On change of defaultSelectedRow ", () => {
|
|
const evalMetaUpdates: EvalMetaUpdates = [];
|
|
const overwriteObj = overrideWidgetProperties({
|
|
currentTree,
|
|
entity: currentTree.Table1 as DataTreeWidget,
|
|
propertyPath: "defaultSelectedRow",
|
|
value: [0, 1],
|
|
evalMetaUpdates,
|
|
});
|
|
|
|
expect(overwriteObj).toStrictEqual(undefined);
|
|
|
|
expect(evalMetaUpdates).toStrictEqual([
|
|
{
|
|
widgetId: currentTree.Table1.widgetId,
|
|
metaPropertyPath: ["selectedRowIndex"],
|
|
value: [0, 1],
|
|
},
|
|
{
|
|
widgetId: currentTree.Table1.widgetId,
|
|
metaPropertyPath: ["selectedRowIndices"],
|
|
value: [0, 1],
|
|
},
|
|
]);
|
|
|
|
expect(currentTree.Table1.meta.selectedRowIndex).toStrictEqual([0, 1]);
|
|
expect(currentTree.Table1.meta.selectedRowIndices).toStrictEqual([0, 1]);
|
|
});
|
|
// When meta.selectedRowIndex is re-evaluated it will override values selectedRowIndex
|
|
it("2. meta.selectedRowIndex updating selectedRowIndex", () => {
|
|
const evalMetaUpdates: EvalMetaUpdates = [];
|
|
const overwriteObj = overrideWidgetProperties({
|
|
currentTree,
|
|
entity: currentTree.Table1 as DataTreeWidget,
|
|
propertyPath: "meta.selectedRowIndex",
|
|
value: 0,
|
|
evalMetaUpdates,
|
|
});
|
|
|
|
expect(overwriteObj).toStrictEqual(undefined);
|
|
|
|
expect(evalMetaUpdates).toStrictEqual([]);
|
|
|
|
expect(currentTree.Table1.selectedRowIndex).toStrictEqual(0);
|
|
});
|
|
});
|
|
});
|