2019-11-06 06:35:15 +00:00
|
|
|
import _ from "lodash";
|
2019-11-25 05:07:27 +00:00
|
|
|
import { WidgetProps } from "widgets/BaseWidget";
|
2020-01-02 13:36:35 +00:00
|
|
|
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
|
2019-11-19 12:44:58 +00:00
|
|
|
import ValidationFactory from "./ValidationFactory";
|
2019-11-28 03:56:44 +00:00
|
|
|
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
|
2020-01-08 10:50:49 +00:00
|
|
|
import unescapeJS from "unescape-js";
|
2020-01-17 09:28:26 +00:00
|
|
|
import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
|
|
|
|
|
import toposort from "toposort";
|
2019-12-06 13:16:08 +00:00
|
|
|
|
2019-11-14 09:28:51 +00:00
|
|
|
export const isDynamicValue = (value: string): boolean =>
|
2019-12-02 07:37:33 +00:00
|
|
|
DATA_BIND_REGEX.test(value);
|
2019-11-28 03:56:44 +00:00
|
|
|
|
|
|
|
|
//{{}}{{}}}
|
2019-12-02 09:51:18 +00:00
|
|
|
export function parseDynamicString(dynamicString: string): string[] {
|
2019-11-28 03:56:44 +00:00
|
|
|
let parsedDynamicValues = [];
|
|
|
|
|
const indexOfDoubleParanStart = dynamicString.indexOf("{{");
|
|
|
|
|
if (indexOfDoubleParanStart === -1) {
|
|
|
|
|
return [dynamicString];
|
|
|
|
|
}
|
|
|
|
|
//{{}}{{}}}
|
|
|
|
|
const firstString = dynamicString.substring(0, indexOfDoubleParanStart);
|
|
|
|
|
firstString && parsedDynamicValues.push(firstString);
|
|
|
|
|
let rest = dynamicString.substring(
|
|
|
|
|
indexOfDoubleParanStart,
|
|
|
|
|
dynamicString.length,
|
|
|
|
|
);
|
|
|
|
|
//{{}}{{}}}
|
|
|
|
|
let sum = 0;
|
|
|
|
|
for (let i = 0; i <= rest.length - 1; i++) {
|
|
|
|
|
const char = rest[i];
|
|
|
|
|
const prevChar = rest[i - 1];
|
|
|
|
|
|
|
|
|
|
if (char === "{") {
|
|
|
|
|
sum++;
|
|
|
|
|
} else if (char === "}") {
|
|
|
|
|
sum--;
|
|
|
|
|
if (prevChar === "}" && sum === 0) {
|
|
|
|
|
parsedDynamicValues.push(rest.substring(0, i + 1));
|
|
|
|
|
rest = rest.substring(i + 1, rest.length);
|
|
|
|
|
if (rest) {
|
|
|
|
|
parsedDynamicValues = parsedDynamicValues.concat(
|
|
|
|
|
parseDynamicString(rest),
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (sum !== 0 && dynamicString !== "") {
|
|
|
|
|
return [dynamicString];
|
|
|
|
|
}
|
|
|
|
|
return parsedDynamicValues;
|
|
|
|
|
}
|
2019-11-14 09:28:51 +00:00
|
|
|
|
2020-01-17 09:28:26 +00:00
|
|
|
const getAllPaths = (
|
|
|
|
|
tree: Record<string, any>,
|
|
|
|
|
prefix = "",
|
|
|
|
|
): Record<string, true> => {
|
|
|
|
|
return Object.keys(tree).reduce((res: Record<string, true>, el): Record<
|
|
|
|
|
string,
|
|
|
|
|
true
|
|
|
|
|
> => {
|
|
|
|
|
if (Array.isArray(tree[el])) {
|
|
|
|
|
const key = `${prefix}${el}`;
|
|
|
|
|
return { ...res, [key]: true };
|
|
|
|
|
} else if (typeof tree[el] === "object" && tree[el] !== null) {
|
|
|
|
|
const key = `${prefix}${el}`;
|
|
|
|
|
return { ...res, [key]: true, ...getAllPaths(tree[el], `${key}.`) };
|
|
|
|
|
} else {
|
|
|
|
|
const key = `${prefix}${el}`;
|
|
|
|
|
return { ...res, [key]: true };
|
|
|
|
|
}
|
|
|
|
|
}, {});
|
|
|
|
|
};
|
|
|
|
|
|
2019-11-14 09:28:51 +00:00
|
|
|
export const getDynamicBindings = (
|
|
|
|
|
dynamicString: string,
|
|
|
|
|
): { bindings: string[]; paths: string[] } => {
|
2020-01-17 09:28:26 +00:00
|
|
|
if (!dynamicString) return { bindings: [], paths: [] };
|
|
|
|
|
const sanitisedString = dynamicString.trim();
|
2019-11-14 09:28:51 +00:00
|
|
|
// Get the {{binding}} bound values
|
2020-01-17 09:28:26 +00:00
|
|
|
const bindings = parseDynamicString(sanitisedString);
|
2019-11-14 09:28:51 +00:00
|
|
|
// Get the "binding" path values
|
2019-11-28 03:56:44 +00:00
|
|
|
const paths = bindings.map(binding => {
|
|
|
|
|
const length = binding.length;
|
2019-12-02 07:37:33 +00:00
|
|
|
const matches = binding.match(DATA_BIND_REGEX);
|
2019-11-28 03:56:44 +00:00
|
|
|
if (matches) {
|
|
|
|
|
return binding.substring(2, length - 2);
|
|
|
|
|
}
|
2019-11-14 09:28:51 +00:00
|
|
|
return "";
|
|
|
|
|
});
|
|
|
|
|
return { bindings, paths };
|
|
|
|
|
};
|
|
|
|
|
|
2019-11-06 06:35:15 +00:00
|
|
|
// Paths are expected to have "{name}.{path}" signature
|
2019-12-02 07:36:20 +00:00
|
|
|
export const evaluateDynamicBoundValue = (
|
2019-11-28 03:56:44 +00:00
|
|
|
data: NameBindingsWithData,
|
2019-11-06 06:35:15 +00:00
|
|
|
path: string,
|
2019-11-14 09:28:51 +00:00
|
|
|
): any => {
|
2020-01-08 10:50:49 +00:00
|
|
|
const unescapedInput = unescapeJS(path);
|
|
|
|
|
return JSExecutionManagerSingleton.evaluateSync(unescapedInput, data);
|
2019-11-14 09:28:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// For creating a final value where bindings could be in a template format
|
|
|
|
|
export const createDynamicValueString = (
|
|
|
|
|
binding: string,
|
|
|
|
|
subBindings: string[],
|
|
|
|
|
subValues: string[],
|
|
|
|
|
): string => {
|
|
|
|
|
// Replace the string with the data tree values
|
|
|
|
|
let finalValue = binding;
|
|
|
|
|
subBindings.forEach((b, i) => {
|
|
|
|
|
let value = subValues[i];
|
|
|
|
|
if (Array.isArray(value) || _.isObject(value)) {
|
|
|
|
|
value = JSON.stringify(value);
|
|
|
|
|
}
|
|
|
|
|
finalValue = finalValue.replace(b, value);
|
|
|
|
|
});
|
|
|
|
|
return finalValue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getDynamicValue = (
|
|
|
|
|
dynamicBinding: string,
|
2019-11-28 03:56:44 +00:00
|
|
|
data: NameBindingsWithData,
|
2019-11-14 09:28:51 +00:00
|
|
|
): any => {
|
|
|
|
|
// Get the {{binding}} bound values
|
|
|
|
|
const { bindings, paths } = getDynamicBindings(dynamicBinding);
|
|
|
|
|
if (bindings.length) {
|
|
|
|
|
// Get the Data Tree value of those "binding "paths
|
2019-11-28 03:56:44 +00:00
|
|
|
const values = paths.map((p, i) => {
|
2019-12-30 07:39:53 +00:00
|
|
|
if (p) {
|
2020-01-17 09:28:26 +00:00
|
|
|
return evaluateDynamicBoundValue(data, p);
|
2019-12-30 07:39:53 +00:00
|
|
|
} else {
|
|
|
|
|
return bindings[i];
|
|
|
|
|
}
|
2019-11-28 03:56:44 +00:00
|
|
|
});
|
|
|
|
|
|
2019-11-14 09:28:51 +00:00
|
|
|
// if it is just one binding, no need to create template string
|
|
|
|
|
if (bindings.length === 1) return values[0];
|
|
|
|
|
// else return a string template with bindings
|
|
|
|
|
return createDynamicValueString(dynamicBinding, bindings, values);
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
2019-11-06 06:35:15 +00:00
|
|
|
};
|
|
|
|
|
|
2020-01-17 09:28:26 +00:00
|
|
|
export const enhanceWidgetWithValidations = (
|
2019-11-19 12:44:58 +00:00
|
|
|
widget: WidgetProps,
|
|
|
|
|
): WidgetProps => {
|
|
|
|
|
if (!widget) return widget;
|
|
|
|
|
const properties = { ...widget };
|
|
|
|
|
const invalidProps: Record<string, boolean> = {};
|
2019-12-10 13:30:16 +00:00
|
|
|
const validationMessages: Record<string, string> = {};
|
2020-01-17 09:28:26 +00:00
|
|
|
Object.keys(properties).forEach((property: string) => {
|
|
|
|
|
const value = properties[property];
|
2019-11-22 13:12:39 +00:00
|
|
|
// Pass it through validation and parse
|
2020-01-17 09:28:26 +00:00
|
|
|
const { isValid, message } = ValidationFactory.validateWidgetProperty(
|
|
|
|
|
widget.type,
|
|
|
|
|
property,
|
|
|
|
|
value,
|
|
|
|
|
);
|
2019-11-22 13:12:39 +00:00
|
|
|
// Store all invalid props
|
|
|
|
|
if (!isValid) invalidProps[property] = true;
|
2019-12-10 13:30:16 +00:00
|
|
|
// Store validation Messages
|
|
|
|
|
if (message) validationMessages[property] = message;
|
2019-11-19 12:44:58 +00:00
|
|
|
});
|
2020-01-17 09:28:26 +00:00
|
|
|
return {
|
|
|
|
|
...properties,
|
|
|
|
|
invalidProps,
|
|
|
|
|
validationMessages,
|
|
|
|
|
};
|
2019-11-06 06:35:15 +00:00
|
|
|
};
|
2020-01-17 09:28:26 +00:00
|
|
|
|
|
|
|
|
export const getParsedTree = (tree: any) => {
|
|
|
|
|
return Object.keys(tree).reduce((tree, entityKey: string) => {
|
|
|
|
|
const entity = tree[entityKey];
|
|
|
|
|
if (entity && entity.type) {
|
|
|
|
|
const parsedEntity = { ...entity };
|
|
|
|
|
Object.keys(entity).forEach((property: string) => {
|
|
|
|
|
const value = entity[property];
|
|
|
|
|
// Pass it through parse
|
|
|
|
|
const { parsed } = ValidationFactory.validateWidgetProperty(
|
|
|
|
|
entity.type,
|
|
|
|
|
property,
|
|
|
|
|
value,
|
|
|
|
|
);
|
|
|
|
|
parsedEntity[property] = parsed;
|
|
|
|
|
});
|
|
|
|
|
return { ...tree, [entityKey]: parsedEntity };
|
|
|
|
|
}
|
|
|
|
|
return tree;
|
|
|
|
|
}, tree);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getEvaluatedDataTree = (
|
|
|
|
|
dataTree: NameBindingsWithData,
|
|
|
|
|
parseValues: boolean,
|
|
|
|
|
) => {
|
|
|
|
|
const dynamicDependencyMap = createDependencyTree(dataTree);
|
|
|
|
|
const evaluatedTree = dependencySortedEvaluateDataTree(
|
|
|
|
|
dataTree,
|
|
|
|
|
dynamicDependencyMap,
|
|
|
|
|
parseValues,
|
|
|
|
|
);
|
|
|
|
|
if (parseValues) {
|
|
|
|
|
return getParsedTree(evaluatedTree);
|
|
|
|
|
} else {
|
|
|
|
|
return evaluatedTree;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type DynamicDependencyMap = Record<string, Array<string>>;
|
|
|
|
|
export const createDependencyTree = (
|
|
|
|
|
dataTree: NameBindingsWithData,
|
|
|
|
|
): Array<[string, string]> => {
|
|
|
|
|
const dependencyMap: DynamicDependencyMap = {};
|
|
|
|
|
const allKeys = getAllPaths(dataTree);
|
|
|
|
|
Object.keys(dataTree).forEach(entityKey => {
|
|
|
|
|
const entity = dataTree[entityKey] as WidgetProps;
|
|
|
|
|
if (entity && entity.dynamicBindings) {
|
|
|
|
|
Object.keys(entity.dynamicBindings).forEach(prop => {
|
|
|
|
|
const { paths } = getDynamicBindings(entity[prop]);
|
|
|
|
|
dependencyMap[`${entityKey}.${prop}`] = paths.filter(p => !!p);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
Object.keys(dependencyMap).forEach(key => {
|
|
|
|
|
dependencyMap[key] = _.flatten(
|
|
|
|
|
dependencyMap[key].map(path => calculateSubDependencies(path, allKeys)),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
const dependencyTree: Array<[string, string]> = [];
|
|
|
|
|
Object.keys(dependencyMap).forEach((key: string) => {
|
|
|
|
|
dependencyMap[key].forEach(dep => dependencyTree.push([key, dep]));
|
|
|
|
|
});
|
|
|
|
|
return dependencyTree;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calculateSubDependencies = (
|
|
|
|
|
path: string,
|
|
|
|
|
all: Record<string, true>,
|
|
|
|
|
): Array<string> => {
|
|
|
|
|
const subDeps: Array<string> = [];
|
|
|
|
|
const identifiers = path.match(/[a-zA-Z_$][a-zA-Z_$0-9.]*/g) || [path];
|
|
|
|
|
identifiers.forEach((identifier: string) => {
|
|
|
|
|
if (identifier in all) {
|
|
|
|
|
subDeps.push(identifier);
|
|
|
|
|
} else {
|
|
|
|
|
const subIdentifiers =
|
|
|
|
|
identifier.match(/[a-zA-Z_$][a-zA-Z_$0-9]*/g) || [];
|
|
|
|
|
let current = "";
|
|
|
|
|
for (let i = 0; i < subIdentifiers.length; i++) {
|
|
|
|
|
const key = `${current}${current ? "." : ""}${subIdentifiers[i]}`;
|
|
|
|
|
if (key in all) {
|
|
|
|
|
current = key;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (current) subDeps.push(current);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return subDeps;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function dependencySortedEvaluateDataTree(
|
|
|
|
|
dataTree: NameBindingsWithData,
|
|
|
|
|
dependencyTree: Array<[string, string]>,
|
|
|
|
|
parseValues: boolean,
|
|
|
|
|
) {
|
|
|
|
|
const tree = JSON.parse(JSON.stringify(dataTree));
|
|
|
|
|
try {
|
|
|
|
|
// sort dependencies
|
|
|
|
|
const sortedDependencies = toposort(dependencyTree).reverse();
|
|
|
|
|
// evaluate and replace values
|
|
|
|
|
return sortedDependencies.reduce(
|
|
|
|
|
(currentTree: NameBindingsWithData, path: string) => {
|
|
|
|
|
const binding = _.get(currentTree as any, path);
|
|
|
|
|
const widgetType = _.get(
|
|
|
|
|
currentTree as any,
|
|
|
|
|
`${path.split(".")[0]}.type`,
|
|
|
|
|
null,
|
|
|
|
|
);
|
|
|
|
|
let result = binding;
|
|
|
|
|
if (isDynamicValue(binding)) {
|
|
|
|
|
result = getDynamicValue(binding, currentTree);
|
|
|
|
|
}
|
|
|
|
|
if (widgetType && parseValues) {
|
|
|
|
|
const { parsed } = ValidationFactory.validateWidgetProperty(
|
|
|
|
|
widgetType,
|
|
|
|
|
`${path.split(".")[1]}`,
|
|
|
|
|
result,
|
|
|
|
|
);
|
|
|
|
|
result = parsed;
|
|
|
|
|
}
|
|
|
|
|
return _.set(currentTree, path, result);
|
|
|
|
|
},
|
|
|
|
|
tree,
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
return tree;
|
|
|
|
|
}
|
|
|
|
|
}
|