Add back derived properties

This commit is contained in:
Hetu Nandu 2020-01-17 09:28:26 +00:00
parent 17b642d085
commit cc50beb0a0
18 changed files with 410 additions and 104 deletions

View File

@ -82,6 +82,7 @@
"source-map-explorer": "^2.1.1", "source-map-explorer": "^2.1.1",
"styled-components": "^4.1.3", "styled-components": "^4.1.3",
"tinycolor2": "^1.4.1", "tinycolor2": "^1.4.1",
"toposort": "^2.0.2",
"ts-loader": "^6.0.4", "ts-loader": "^6.0.4",
"typescript": "^3.6.3", "typescript": "^3.6.3",
"unescape-js": "^1.1.4" "unescape-js": "^1.1.4"
@ -123,6 +124,7 @@
"@types/react-select": "^3.0.5", "@types/react-select": "^3.0.5",
"@types/react-tabs": "^2.3.1", "@types/react-tabs": "^2.3.1",
"@types/redux-form": "^8.1.9", "@types/redux-form": "^8.1.9",
"@types/toposort": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0", "@typescript-eslint/parser": "^2.0.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",

View File

@ -1,6 +1,6 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
// TODO (hetu): Remove useless escapes and re-enable the above lint rule // TODO (hetu): Remove useless escapes and re-enable the above lint rule
export type NamePathBindingMap = Record<string, string>; export type NamePathBindingMap = Record<string, string>;
export const DATA_BIND_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g; export const DATA_BIND_REGEX = /{{([\s\S]*?)}}/g;
export const AUTOCOMPLETE_MATCH_REGEX = /{{\s*.*?\s*}}/g; export const AUTOCOMPLETE_MATCH_REGEX = /{{\s*.*?\s*}}/g;
/* eslint-enable no-useless-escape */ /* eslint-enable no-useless-escape */

View File

@ -11,14 +11,14 @@ export default class RealmExecutor implements JSExecutor {
constructor() { constructor() {
this.rootRealm = Realm.makeRootRealm(); this.rootRealm = Realm.makeRootRealm();
this.createSafeFunction = this.rootRealm.evaluate(` this.createSafeFunction = this.rootRealm.evaluate(`
(function createSafeFunction(unsafeFn) { (function createSafeFunction(unsafeFn) {
return function safeFn(...args) { return function safeFn(...args) {
unsafeFn(...args); unsafeFn(...args);
} }
}) })
`); `);
this.createSafeObject = this.rootRealm.evaluate(` this.createSafeObject = this.rootRealm.evaluate(`
(function creaetSafeObject(unsafeObject) { (function creaetSafeObject(unsafeObject) {
return JSON.parse(JSON.stringify(unsafeObject)); return JSON.parse(JSON.stringify(unsafeObject));
}) })
`); `);
@ -44,7 +44,7 @@ export default class RealmExecutor implements JSExecutor {
try { try {
result = this.rootRealm.evaluate(sourceText, safeData); result = this.rootRealm.evaluate(sourceText, safeData);
} catch (e) { } catch (e) {
//TODO(Satbir): Return an object with an error message. console.error(`Error: "${e.message}" when evaluating {{${sourceText}}}`);
} }
return this.convertToMainScope(result); return this.convertToMainScope(result);
} }

View File

@ -52,10 +52,7 @@ import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms"; import { API_EDITOR_FORM_NAME } from "constants/forms";
import { executeAction } from "actions/widgetActions"; import { executeAction } from "actions/widgetActions";
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton"; import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
import { import { getParsedDataTree } from "selectors/nameBindingsWithDataSelector";
getNameBindingsWithData,
NameBindingsWithData,
} from "selectors/nameBindingsWithDataSelector";
import { transformRestAction } from "transformers/RestActionTransformer"; import { transformRestAction } from "transformers/RestActionTransformer";
export const getAction = ( export const getAction = (
@ -83,8 +80,8 @@ const createActionErrorResponse = (
}); });
export function* evaluateDynamicBoundValueSaga(path: string): any { export function* evaluateDynamicBoundValueSaga(path: string): any {
const nameBindingsWithData = yield select(getNameBindingsWithData); const tree = yield select(getParsedDataTree);
return getDynamicValue(`{{${path}}}`, nameBindingsWithData); return getDynamicValue(`{{${path}}}`, tree);
} }
export function* getActionParams(jsonPathKeys: string[] | undefined) { export function* getActionParams(jsonPathKeys: string[] | undefined) {
@ -106,12 +103,10 @@ export function* getActionParams(jsonPathKeys: string[] | undefined) {
} }
function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) { function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) {
const nameBindingsWithData: NameBindingsWithData = yield select( const tree = yield select(getParsedDataTree);
getNameBindingsWithData,
);
const result = JSExecutionManagerSingleton.evaluateSync( const result = JSExecutionManagerSingleton.evaluateSync(
jsAction.jsFunction, jsAction.jsFunction,
nameBindingsWithData, tree,
); );
yield put({ yield put({

View File

@ -7,7 +7,7 @@ import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigRe
import { WidgetCardProps } from "widgets/BaseWidget"; import { WidgetCardProps } from "widgets/BaseWidget";
import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer"; import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer";
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; import { getEvaluatedDataTree, getParsedTree } from "utils/DynamicBindingUtils";
import { getDataTree } from "./entitiesSelector"; import { getDataTree } from "./entitiesSelector";
import { import {
FlattenedWidgetProps, FlattenedWidgetProps,
@ -18,9 +18,11 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer";
import { OccupiedSpace } from "constants/editorConstants"; import { OccupiedSpace } from "constants/editorConstants";
import { WidgetTypes } from "constants/WidgetConstants"; import { WidgetTypes } from "constants/WidgetConstants";
import { import {
getNameBindingsWithData,
NameBindingsWithData, NameBindingsWithData,
getNameBindingsWithData,
getParsedDataTree,
} from "./nameBindingsWithDataSelector"; } from "./nameBindingsWithDataSelector";
import _ from "lodash";
const getEditorState = (state: AppState) => state.ui.editor; const getEditorState = (state: AppState) => state.ui.editor;
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
@ -116,15 +118,14 @@ export const getWidgetCards = createSelector(
export const getValidatedDynamicProps = createSelector( export const getValidatedDynamicProps = createSelector(
getDataTree, getDataTree,
getNameBindingsWithData, getParsedDataTree,
(entities: DataTree, nameBindingsWithData: NameBindingsWithData) => { (entities: DataTree, tree) => {
const widgets = { ...entities.canvasWidgets }; const widgets = { ...entities.canvasWidgets };
Object.keys(widgets).forEach(widgetKey => { Object.keys(widgets).forEach(widgetKey => {
widgets[widgetKey] = enhanceWithDynamicValuesAndValidations( const evaluatedWidget = _.find(tree, { widgetId: widgetKey });
widgets[widgetKey], if (evaluatedWidget) {
nameBindingsWithData, widgets[widgetKey] = evaluatedWidget;
true, }
);
}); });
return widgets; return widgets;
}, },

View File

@ -4,37 +4,42 @@ import { createSelector } from "reselect";
import { getActions, getDataTree } from "./entitiesSelector"; import { getActions, getDataTree } from "./entitiesSelector";
import { ActionDataState } from "reducers/entityReducers/actionsReducer"; import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import createCachedSelector from "re-reselect"; import createCachedSelector from "re-reselect";
import { getEvaluatedDataTree } from "utils/DynamicBindingUtils";
export type NameBindingsWithData = Record<string, object>; export type NameBindingsWithData = Record<string, object>;
export const getNameBindingsWithData = createSelector( export const getNameBindingsWithData = createSelector(
getDataTree, getDataTree,
(dataTree: DataTree): NameBindingsWithData => { (dataTree: DataTree): NameBindingsWithData => {
const nameBindingsWithData: Record<string, object> = {}; const nameBindingsWithData: Record<string, object> = {};
Object.keys(dataTree.nameBindings).forEach(key => { Object.keys(dataTree.nameBindings).forEach(key => {
const nameBindings = dataTree.nameBindings[key]; const nameBindings = dataTree.nameBindings[key];
const evaluatedValue = JSONPath({ nameBindingsWithData[key] = JSONPath({
path: nameBindings, path: nameBindings,
json: dataTree, json: dataTree,
})[0]; })[0];
if (evaluatedValue && key !== "undefined") {
nameBindingsWithData[key] = evaluatedValue;
}
}); });
return nameBindingsWithData; return nameBindingsWithData;
}, },
); );
export const getParsedDataTree = createSelector(
getNameBindingsWithData,
(namedBindings: NameBindingsWithData) => {
return getEvaluatedDataTree(namedBindings, true);
},
);
// For autocomplete. Use actions cached responses if // For autocomplete. Use actions cached responses if
// there isn't a response already // there isn't a response already
export const getNameBindingsForAutocomplete = createCachedSelector( export const getNameBindingsForAutocomplete = createCachedSelector(
getNameBindingsWithData, getParsedDataTree,
getActions, getActions,
(namedBindings: NameBindingsWithData, actions: ActionDataState["data"]) => { (dataTree: NameBindingsWithData, actions: ActionDataState["data"]) => {
const cachedResponses: Record<string, any> = {}; const cachedResponses: Record<string, any> = {};
if (actions && actions.length) { if (actions && actions.length) {
actions.forEach(action => { actions.forEach(action => {
if (!(action.name in namedBindings) && action.cacheResponse) { if (!(action.name in dataTree) && action.cacheResponse) {
try { try {
cachedResponses[action.name] = JSON.parse(action.cacheResponse); cachedResponses[action.name] = JSON.parse(action.cacheResponse);
} catch (e) { } catch (e) {
@ -43,6 +48,6 @@ export const getNameBindingsForAutocomplete = createCachedSelector(
} }
}); });
} }
return { ...namedBindings, ...cachedResponses }; return { ...dataTree, ...cachedResponses };
}, },
)((state: AppState) => state.entities.actions.data.length); )((state: AppState) => state.entities.actions.data.length);

View File

@ -4,12 +4,16 @@ import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer"
import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer"; import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer"; import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer";
import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; import {
enhanceWidgetWithValidations,
getEvaluatedDataTree,
} from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import { import {
getNameBindingsWithData,
NameBindingsWithData, NameBindingsWithData,
getNameBindingsWithData,
} from "./nameBindingsWithDataSelector"; } from "./nameBindingsWithDataSelector";
import _ from "lodash";
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
state.ui.propertyPane; state.ui.propertyPane;
@ -41,14 +45,23 @@ export const getWidgetPropsWithValidations = createSelector(
getNameBindingsWithData, getNameBindingsWithData,
( (
widget: WidgetProps | undefined, widget: WidgetProps | undefined,
nameBindigsWithData: NameBindingsWithData, nameBindingsWithData: NameBindingsWithData,
) => { ) => {
if (!widget) return undefined; if (!widget) return undefined;
return enhanceWithDynamicValuesAndValidations( const tree = getEvaluatedDataTree(nameBindingsWithData, false);
widget, const evaluatedWidget = _.find(tree, { widgetId: widget.widgetId });
nameBindigsWithData, const validations = enhanceWidgetWithValidations(
false, evaluatedWidget as WidgetProps,
); );
if (validations) {
const { invalidProps, validationMessages } = validations;
return {
...widget,
invalidProps,
validationMessages,
};
}
return widget;
}, },
); );

View File

@ -0,0 +1,21 @@
import WidgetFactory from "./WidgetFactory";
import { WidgetType } from "constants/WidgetConstants";
export class DerivedPropFactory {
static getDerivedPropertiesOfWidgetType(
widgetType: WidgetType,
widgetName: string,
): any {
const derivedPropertyMap = WidgetFactory.getWidgetDerivedPropertiesMap(
widgetType,
);
const derivedProps: any = {};
Object.keys(derivedPropertyMap).forEach(propertyName => {
derivedProps[propertyName] = derivedPropertyMap[propertyName].replace(
/this./g,
`${widgetName}.`,
);
});
return derivedProps;
}
}

View File

@ -3,8 +3,9 @@ import { WidgetProps } from "widgets/BaseWidget";
import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import { DATA_BIND_REGEX } from "constants/BindingsConstants";
import ValidationFactory from "./ValidationFactory"; import ValidationFactory from "./ValidationFactory";
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton"; import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
import unescapeJS from "unescape-js"; import unescapeJS from "unescape-js";
import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
import toposort from "toposort";
export const isDynamicValue = (value: string): boolean => export const isDynamicValue = (value: string): boolean =>
DATA_BIND_REGEX.test(value); DATA_BIND_REGEX.test(value);
@ -51,11 +52,34 @@ export function parseDynamicString(dynamicString: string): string[] {
return parsedDynamicValues; return parsedDynamicValues;
} }
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 };
}
}, {});
};
export const getDynamicBindings = ( export const getDynamicBindings = (
dynamicString: string, dynamicString: string,
): { bindings: string[]; paths: string[] } => { ): { bindings: string[]; paths: string[] } => {
if (!dynamicString) return { bindings: [], paths: [] };
const sanitisedString = dynamicString.trim();
// Get the {{binding}} bound values // Get the {{binding}} bound values
const bindings = parseDynamicString(dynamicString); const bindings = parseDynamicString(sanitisedString);
// Get the "binding" path values // Get the "binding" path values
const paths = bindings.map(binding => { const paths = bindings.map(binding => {
const length = binding.length; const length = binding.length;
@ -105,22 +129,7 @@ export const getDynamicValue = (
// Get the Data Tree value of those "binding "paths // Get the Data Tree value of those "binding "paths
const values = paths.map((p, i) => { const values = paths.map((p, i) => {
if (p) { if (p) {
const value = evaluateDynamicBoundValue(data, p); return evaluateDynamicBoundValue(data, p);
// Check if the result is a dynamic value, if so get the value again
if (isDynamicValue(value)) {
// Check for the paths of this dynamic value
const { paths } = getDynamicBindings(value);
// If it is the same as it came in, log an error
// and return the same value back
if (paths.length === 1 && paths[0] === p) {
console.error("Binding not correct");
return value;
}
// Evaluate the value again
return getDynamicValue(value, data);
} else {
return value;
}
} else { } else {
return bindings[i]; return bindings[i];
} }
@ -134,34 +143,161 @@ export const getDynamicValue = (
return undefined; return undefined;
}; };
export const enhanceWithDynamicValuesAndValidations = ( export const enhanceWidgetWithValidations = (
widget: WidgetProps, widget: WidgetProps,
nameBindingsWithData: NameBindingsWithData,
replaceWithParsed: boolean,
): WidgetProps => { ): WidgetProps => {
if (!widget) return widget; if (!widget) return widget;
const properties = { ...widget }; const properties = { ...widget };
const invalidProps: Record<string, boolean> = {}; const invalidProps: Record<string, boolean> = {};
const validationMessages: Record<string, string> = {}; const validationMessages: Record<string, string> = {};
Object.keys(properties).forEach((property: string) => {
Object.keys(widget).forEach((property: string) => { const value = properties[property];
let value = widget[property];
// Check for dynamic bindings
if (widget.dynamicBindings && property in widget.dynamicBindings) {
value = getDynamicValue(value, nameBindingsWithData);
}
// Pass it through validation and parse // Pass it through validation and parse
const { const { isValid, message } = ValidationFactory.validateWidgetProperty(
isValid, widget.type,
parsed, property,
message, value,
} = ValidationFactory.validateWidgetProperty(widget.type, property, value); );
// Store all invalid props // Store all invalid props
if (!isValid) invalidProps[property] = true; if (!isValid) invalidProps[property] = true;
// Store validation Messages // Store validation Messages
if (message) validationMessages[property] = message; if (message) validationMessages[property] = message;
// Replace if flag is turned on
if (replaceWithParsed) properties[property] = parsed;
}); });
return { ...properties, invalidProps, validationMessages }; return {
...properties,
invalidProps,
validationMessages,
};
}; };
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;
}
}

View File

@ -8,7 +8,11 @@ jest.mock("jsExecution/RealmExecutor", () => {
return { execute: mockExecute, registerLibrary: mockRegisterLibrary }; return { execute: mockExecute, registerLibrary: mockRegisterLibrary };
}); });
}); });
import { getDynamicValue, parseDynamicString } from "./DynamicBindingUtils"; import {
dependencySortedEvaluateDataTree,
getDynamicValue,
parseDynamicString,
} from "./DynamicBindingUtils";
import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector"; import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
import { AppState, DataTree } from "reducers"; import { AppState, DataTree } from "reducers";
@ -121,3 +125,43 @@ it("Parse the dynamic string", () => {
expect(value).toEqual(actualValue); expect(value).toEqual(actualValue);
}); });
it("evaluates the data tree", () => {
const input = {
widget1: {
displayValue: "{{widget2.computedProperty}}",
},
widget2: {
computedProperty: "{{ widget2.data[widget2.index] }}",
data: "{{ apiData.node }}",
index: 2,
},
apiData: {
node: ["wrong value", "still wrong", "correct"],
},
};
const dynamicBindings = [
["widget1.displayValue", "widget2.computedProperty"],
["widget2.computedProperty", "widget2.data"],
["widget2.computedProperty", "widget2.index"],
["widget2.data", "apiData.node"],
];
const output = {
widget1: {
displayValue: "correct",
},
widget2: {
computedProperty: "correct",
data: ["wrong value", "still wrong", "correct"],
index: 2,
},
apiData: {
node: ["wrong value", "still wrong", "correct"],
},
};
const result = dependencySortedEvaluateDataTree(input, dynamicBindings);
expect(result).toEqual(output);
});

View File

@ -6,20 +6,33 @@ import {
} from "widgets/BaseWidget"; } from "widgets/BaseWidget";
import { WidgetPropertyValidationType } from "./ValidationFactory"; import { WidgetPropertyValidationType } from "./ValidationFactory";
type WidgetDerivedPropertyType = any;
export type DerivedPropertiesMap = Record<string, string>;
class WidgetFactory { class WidgetFactory {
static widgetMap: Map<WidgetType, WidgetBuilder<WidgetProps>> = new Map(); static widgetMap: Map<WidgetType, WidgetBuilder<WidgetProps>> = new Map();
static widgetPropValidationMap: Map< static widgetPropValidationMap: Map<
WidgetType, WidgetType,
WidgetPropertyValidationType WidgetPropertyValidationType
> = new Map(); > = new Map();
static widgetDerivedPropertiesGetterMap: Map<
WidgetType,
WidgetDerivedPropertyType
> = new Map();
static derivedPropertiesMap: Map<
WidgetType,
DerivedPropertiesMap
> = new Map();
static registerWidgetBuilder( static registerWidgetBuilder(
widgetType: WidgetType, widgetType: WidgetType,
widgetBuilder: WidgetBuilder<WidgetProps>, widgetBuilder: WidgetBuilder<WidgetProps>,
widgetPropertyValidation: WidgetPropertyValidationType, widgetPropertyValidation: WidgetPropertyValidationType,
derivedPropertiesMap: DerivedPropertiesMap,
) { ) {
this.widgetMap.set(widgetType, widgetBuilder); this.widgetMap.set(widgetType, widgetBuilder);
this.widgetPropValidationMap.set(widgetType, widgetPropertyValidation); this.widgetPropValidationMap.set(widgetType, widgetPropertyValidation);
this.derivedPropertiesMap.set(widgetType, derivedPropertiesMap);
} }
static createWidget( static createWidget(
@ -60,6 +73,17 @@ class WidgetFactory {
} }
return map; return map;
} }
static getWidgetDerivedPropertiesMap(
widgetType: WidgetType,
): DerivedPropertiesMap {
const map = this.derivedPropertiesMap.get(widgetType);
if (!map) {
console.error("Widget type validation is not defined");
return {};
}
return map;
}
} }
export interface WidgetCreationException { export interface WidgetCreationException {

View File

@ -14,7 +14,7 @@ import {
WidgetOperations, WidgetOperations,
WidgetOperation, WidgetOperation,
} from "widgets/BaseWidget"; } from "widgets/BaseWidget";
import { WidgetType, RenderModes } from "constants/WidgetConstants"; import { WidgetType } from "constants/WidgetConstants";
import { generateReactKey } from "utils/generators"; import { generateReactKey } from "utils/generators";
import { import {
GridDefaults, GridDefaults,
@ -25,6 +25,7 @@ import {
} from "constants/WidgetConstants"; } from "constants/WidgetConstants";
import { snapToGrid } from "./helpers"; import { snapToGrid } from "./helpers";
import { OccupiedSpace } from "constants/editorConstants"; import { OccupiedSpace } from "constants/editorConstants";
import { DerivedPropFactory } from "utils/DerivedPropertiesFactory";
export type WidgetOperationParams = { export type WidgetOperationParams = {
operation: WidgetOperation; operation: WidgetOperation;
@ -289,17 +290,26 @@ export const generateWidgetProps = (
children: [], children: [],
}; };
} }
const derivedProperties = DerivedPropFactory.getDerivedPropertiesOfWidgetType(
type,
widgetName,
);
const dynamicBindings: Record<string, true> = {};
Object.keys(derivedProperties).forEach(prop => {
dynamicBindings[prop] = true;
});
return { return {
...widgetConfig, ...widgetConfig,
type, type,
widgetName: widgetName, widgetName,
isVisible: true, isVisible: true,
isLoading: false, isLoading: false,
parentColumnSpace, parentColumnSpace,
parentRowSpace, parentRowSpace,
renderMode: RenderModes.CANVAS, dynamicBindings,
...sizes, ...sizes,
...others, ...others,
...derivedProperties,
}; };
} else { } else {
if (parent) { if (parent) {

View File

@ -32,6 +32,7 @@ class WidgetBuilderRegistry {
}, },
}, },
ContainerWidget.getPropertyValidationMap(), ContainerWidget.getPropertyValidationMap(),
ContainerWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -42,6 +43,7 @@ class WidgetBuilderRegistry {
}, },
}, },
TextWidget.getPropertyValidationMap(), TextWidget.getPropertyValidationMap(),
TextWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -52,6 +54,7 @@ class WidgetBuilderRegistry {
}, },
}, },
ButtonWidget.getPropertyValidationMap(), ButtonWidget.getPropertyValidationMap(),
ButtonWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -62,6 +65,7 @@ class WidgetBuilderRegistry {
}, },
}, },
SpinnerWidget.getPropertyValidationMap(), SpinnerWidget.getPropertyValidationMap(),
SpinnerWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -72,6 +76,7 @@ class WidgetBuilderRegistry {
}, },
}, },
InputWidget.getPropertyValidationMap(), InputWidget.getPropertyValidationMap(),
InputWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -82,6 +87,7 @@ class WidgetBuilderRegistry {
}, },
}, },
CheckboxWidget.getPropertyValidationMap(), CheckboxWidget.getPropertyValidationMap(),
CheckboxWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -92,6 +98,7 @@ class WidgetBuilderRegistry {
}, },
}, },
DropdownWidget.getPropertyValidationMap(), DropdownWidget.getPropertyValidationMap(),
DropdownWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -102,6 +109,7 @@ class WidgetBuilderRegistry {
}, },
}, },
RadioGroupWidget.getPropertyValidationMap(), RadioGroupWidget.getPropertyValidationMap(),
RadioGroupWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
@ -112,6 +120,7 @@ class WidgetBuilderRegistry {
}, },
}, },
ImageWidget.getPropertyValidationMap(), ImageWidget.getPropertyValidationMap(),
ImageWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
"TABLE_WIDGET", "TABLE_WIDGET",
@ -121,6 +130,7 @@ class WidgetBuilderRegistry {
}, },
}, },
TableWidget.getPropertyValidationMap(), TableWidget.getPropertyValidationMap(),
TableWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
"FILE_PICKER_WIDGET", "FILE_PICKER_WIDGET",
@ -130,6 +140,7 @@ class WidgetBuilderRegistry {
}, },
}, },
FilePickerWidget.getPropertyValidationMap(), FilePickerWidget.getPropertyValidationMap(),
FilePickerWidget.getDerivedPropertiesMap(),
); );
WidgetFactory.registerWidgetBuilder( WidgetFactory.registerWidgetBuilder(
"DATE_PICKER_WIDGET", "DATE_PICKER_WIDGET",
@ -139,6 +150,7 @@ class WidgetBuilderRegistry {
}, },
}, },
DatePickerWidget.getPropertyValidationMap(), DatePickerWidget.getPropertyValidationMap(),
DatePickerWidget.getDerivedPropertiesMap(),
); );
} }
} }

View File

@ -28,6 +28,7 @@ import { PositionTypes } from "constants/WidgetConstants";
import ErrorBoundary from "components/editorComponents/ErrorBoundry"; import ErrorBoundary from "components/editorComponents/ErrorBoundry";
import { WidgetPropertyValidationType } from "utils/ValidationFactory"; import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { DerivedPropertiesMap } from "utils/WidgetFactory";
/*** /***
* BaseWidget * BaseWidget
* *
@ -63,6 +64,10 @@ abstract class BaseWidget<
return {}; return {};
} }
static getDerivedPropertiesMap(): DerivedPropertiesMap {
return {};
}
/** /**
* Widget abstraction to register the widget type * Widget abstraction to register the widget type
* ```javascript * ```javascript

View File

@ -7,6 +7,10 @@ import _ from "lodash";
import { WidgetPropertyValidationType } from "utils/ValidationFactory"; import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation";
export interface DropDownDerivedProps {
selectedOption?: DropdownOption;
selectedOptionArr?: DropdownOption[];
}
class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> { class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType { static getPropertyValidationMap(): WidgetPropertyValidationType {
return { return {
@ -18,6 +22,23 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
selectedIndexArr: VALIDATION_TYPES.ARRAY, selectedIndexArr: VALIDATION_TYPES.ARRAY,
}; };
} }
static getDerivedPropertiesMap() {
return {
selectedOption: `{{
this.selectionType === 'SINGLE_SELECT'
? this.options[this.selectedIndex]
: undefined
}}`,
selectedOptionArr: `{{
const options = this.options || [];
this.selectionType === "MULTI_SELECT"
? options.filter((opt, index) =>
_.includes(this.selectedIndexArr, index),
)
: undefined
}}`,
};
}
getPageView() { getPageView() {
return ( return (
<DropDownComponent <DropDownComponent

View File

@ -14,6 +14,12 @@ class RadioGroupWidget extends BaseWidget<RadioGroupWidgetProps, WidgetState> {
selectedOptionValue: VALIDATION_TYPES.TEXT, selectedOptionValue: VALIDATION_TYPES.TEXT,
}; };
} }
static getDerivedPropertiesMap() {
return {
selectedOption:
"{{_.find(this.options, { value: this.selectedOptionValue })}}",
};
}
getPageView() { getPageView() {
return ( return (
<RadioGroupComponent <RadioGroupComponent

View File

@ -2,7 +2,7 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants"; import { WidgetType } from "constants/WidgetConstants";
import { ActionPayload, TableAction } from "constants/ActionConstants"; import { ActionPayload, TableAction } from "constants/ActionConstants";
import _, { forIn } from "lodash"; import { forIn } from "lodash";
import TableComponent from "components/designSystems/syncfusion/TableComponent"; import TableComponent from "components/designSystems/syncfusion/TableComponent";
import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation";
@ -32,7 +32,12 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
nextPageKey: VALIDATION_TYPES.TEXT, nextPageKey: VALIDATION_TYPES.TEXT,
prevPageKey: VALIDATION_TYPES.TEXT, prevPageKey: VALIDATION_TYPES.TEXT,
label: VALIDATION_TYPES.TEXT, label: VALIDATION_TYPES.TEXT,
selectedRow: VALIDATION_TYPES.OBJECT, selectedRowIndex: VALIDATION_TYPES.NUMBER,
};
}
static getDerivedPropertiesMap() {
return {
selectedRow: "{{this.tableData[this.selectedRowIndex]}}",
}; };
} }
@ -46,40 +51,36 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
isLoading={this.props.isLoading} isLoading={this.props.isLoading}
height={this.state.componentHeight} height={this.state.componentHeight}
width={this.state.componentWidth} width={this.state.componentWidth}
selectedRowIndex={ selectedRowIndex={this.props.selectedRowIndex}
this.props.selectedRow && this.props.selectedRow.rowIndex
}
disableDrag={(disable: boolean) => { disableDrag={(disable: boolean) => {
this.disableDrag(disable); this.disableDrag(disable);
}} }}
onRowClick={(rowData: object, index: number) => { onRowClick={(rowData: object, index: number) => {
const { onRowSelected } = this.props; const { onRowSelected } = this.props;
this.updateSelectedRowProperty(rowData, index); this.updateSelectedRowProperty(index);
super.executeAction(onRowSelected); super.executeAction(onRowSelected);
}} }}
></TableComponent> />
); );
} }
componentDidUpdate(prevProps: TableWidgetProps) { // componentDidUpdate(prevProps: TableWidgetProps) {
super.componentDidUpdate(prevProps); // super.componentDidUpdate(prevProps);
if ( // if (
!_.isEqual(prevProps.tableData, this.props.tableData) && // !_.isEqual(prevProps.tableData, this.props.tableData) &&
prevProps.selectedRow // prevProps.selectedRow
) { // ) {
this.updateSelectedRowProperty( // this.updateSelectedRowProperty(
this.props.tableData[prevProps.selectedRow.rowIndex], // this.props.tableData[prevProps.selectedRow.rowIndex],
prevProps.selectedRow.rowIndex, // prevProps.selectedRow.rowIndex,
); // );
} // }
} // }
updateSelectedRowProperty(rowData: object, index: number) { updateSelectedRowProperty(index: number) {
const { widgetId } = this.props; const { widgetId } = this.props;
this.updateWidgetProperty(widgetId, "selectedRow", { this.updateWidgetProperty(widgetId, "selectedRowIndex", index);
...rowData,
rowIndex: index,
});
} }
getWidgetType(): WidgetType { getWidgetType(): WidgetType {
@ -102,7 +103,7 @@ export interface TableWidgetProps extends WidgetProps {
recordActions?: TableAction[]; recordActions?: TableAction[];
onPageChange?: ActionPayload[]; onPageChange?: ActionPayload[];
onRowSelected?: ActionPayload[]; onRowSelected?: ActionPayload[];
selectedRow?: SelectedRow; selectedRowIndex?: number;
} }
export default TableWidget; export default TableWidget;

View File

@ -2569,6 +2569,11 @@
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf" resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf"
integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw== integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw==
"@types/toposort@^2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/toposort/-/toposort-2.0.3.tgz#dc490842b77c3e910c8d727ff0bdb2fb124cb41b"
integrity sha512-jRtyvEu0Na/sy0oIxBW0f6wPQjidgVqlmCTJVHEGTNEUdL1f0YSvdPzHY7nX7MUWAZS6zcAa0KkqofHjy/xDZQ==
"@types/uglify-js@*": "@types/uglify-js@*":
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
@ -14649,6 +14654,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0: tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"