From cc50beb0a0cbeda4c065b97c0900ea4e2a14a75d Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Fri, 17 Jan 2020 09:28:26 +0000 Subject: [PATCH] Add back derived properties --- app/client/package.json | 2 + app/client/src/constants/BindingsConstants.ts | 2 +- app/client/src/jsExecution/RealmExecutor.ts | 6 +- app/client/src/sagas/ActionSagas.ts | 15 +- app/client/src/selectors/editorSelectors.tsx | 19 +- .../selectors/nameBindingsWithDataSelector.ts | 23 +- .../src/selectors/propertyPaneSelectors.tsx | 27 ++- .../src/utils/DerivedPropertiesFactory.ts | 21 ++ app/client/src/utils/DynamicBindingUtils.ts | 208 +++++++++++++++--- .../src/utils/DynamicBindingsUtil.test.ts | 46 +++- app/client/src/utils/WidgetFactory.tsx | 24 ++ app/client/src/utils/WidgetPropsUtils.tsx | 16 +- app/client/src/utils/WidgetRegistry.tsx | 12 + app/client/src/widgets/BaseWidget.tsx | 5 + app/client/src/widgets/DropdownWidget.tsx | 21 ++ app/client/src/widgets/RadioGroupWidget.tsx | 6 + app/client/src/widgets/TableWidget.tsx | 51 ++--- app/client/yarn.lock | 10 + 18 files changed, 410 insertions(+), 104 deletions(-) create mode 100644 app/client/src/utils/DerivedPropertiesFactory.ts diff --git a/app/client/package.json b/app/client/package.json index 6119e0785d..284d9a418b 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -82,6 +82,7 @@ "source-map-explorer": "^2.1.1", "styled-components": "^4.1.3", "tinycolor2": "^1.4.1", + "toposort": "^2.0.2", "ts-loader": "^6.0.4", "typescript": "^3.6.3", "unescape-js": "^1.1.4" @@ -123,6 +124,7 @@ "@types/react-select": "^3.0.5", "@types/react-tabs": "^2.3.1", "@types/redux-form": "^8.1.9", + "@types/toposort": "^2.0.3", "@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/parser": "^2.0.0", "babel-loader": "^8.0.6", diff --git a/app/client/src/constants/BindingsConstants.ts b/app/client/src/constants/BindingsConstants.ts index 9dbc0ff7c3..464cc0d043 100644 --- a/app/client/src/constants/BindingsConstants.ts +++ b/app/client/src/constants/BindingsConstants.ts @@ -1,6 +1,6 @@ /* eslint-disable no-useless-escape */ // TODO (hetu): Remove useless escapes and re-enable the above lint rule export type NamePathBindingMap = Record; -export const DATA_BIND_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g; +export const DATA_BIND_REGEX = /{{([\s\S]*?)}}/g; export const AUTOCOMPLETE_MATCH_REGEX = /{{\s*.*?\s*}}/g; /* eslint-enable no-useless-escape */ diff --git a/app/client/src/jsExecution/RealmExecutor.ts b/app/client/src/jsExecution/RealmExecutor.ts index fc397eee9f..fb45ad968f 100644 --- a/app/client/src/jsExecution/RealmExecutor.ts +++ b/app/client/src/jsExecution/RealmExecutor.ts @@ -11,14 +11,14 @@ export default class RealmExecutor implements JSExecutor { constructor() { this.rootRealm = Realm.makeRootRealm(); this.createSafeFunction = this.rootRealm.evaluate(` - (function createSafeFunction(unsafeFn) { + (function createSafeFunction(unsafeFn) { return function safeFn(...args) { unsafeFn(...args); } }) `); this.createSafeObject = this.rootRealm.evaluate(` - (function creaetSafeObject(unsafeObject) { + (function creaetSafeObject(unsafeObject) { return JSON.parse(JSON.stringify(unsafeObject)); }) `); @@ -44,7 +44,7 @@ export default class RealmExecutor implements JSExecutor { try { result = this.rootRealm.evaluate(sourceText, safeData); } catch (e) { - //TODO(Satbir): Return an object with an error message. + console.error(`Error: "${e.message}" when evaluating {{${sourceText}}}`); } return this.convertToMainScope(result); } diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 4ec76cb007..cd9f7cf297 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -52,10 +52,7 @@ import { getFormData } from "selectors/formSelectors"; import { API_EDITOR_FORM_NAME } from "constants/forms"; import { executeAction } from "actions/widgetActions"; import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton"; -import { - getNameBindingsWithData, - NameBindingsWithData, -} from "selectors/nameBindingsWithDataSelector"; +import { getParsedDataTree } from "selectors/nameBindingsWithDataSelector"; import { transformRestAction } from "transformers/RestActionTransformer"; export const getAction = ( @@ -83,8 +80,8 @@ const createActionErrorResponse = ( }); export function* evaluateDynamicBoundValueSaga(path: string): any { - const nameBindingsWithData = yield select(getNameBindingsWithData); - return getDynamicValue(`{{${path}}}`, nameBindingsWithData); + const tree = yield select(getParsedDataTree); + return getDynamicValue(`{{${path}}}`, tree); } export function* getActionParams(jsonPathKeys: string[] | undefined) { @@ -106,12 +103,10 @@ export function* getActionParams(jsonPathKeys: string[] | undefined) { } function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) { - const nameBindingsWithData: NameBindingsWithData = yield select( - getNameBindingsWithData, - ); + const tree = yield select(getParsedDataTree); const result = JSExecutionManagerSingleton.evaluateSync( jsAction.jsFunction, - nameBindingsWithData, + tree, ); yield put({ diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index fdc4067eb7..f7aa8a8706 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -7,7 +7,7 @@ import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigRe import { WidgetCardProps } from "widgets/BaseWidget"; import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; -import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; +import { getEvaluatedDataTree, getParsedTree } from "utils/DynamicBindingUtils"; import { getDataTree } from "./entitiesSelector"; import { FlattenedWidgetProps, @@ -18,9 +18,11 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer"; import { OccupiedSpace } from "constants/editorConstants"; import { WidgetTypes } from "constants/WidgetConstants"; import { - getNameBindingsWithData, NameBindingsWithData, + getNameBindingsWithData, + getParsedDataTree, } from "./nameBindingsWithDataSelector"; +import _ from "lodash"; const getEditorState = (state: AppState) => state.ui.editor; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; @@ -116,15 +118,14 @@ export const getWidgetCards = createSelector( export const getValidatedDynamicProps = createSelector( getDataTree, - getNameBindingsWithData, - (entities: DataTree, nameBindingsWithData: NameBindingsWithData) => { + getParsedDataTree, + (entities: DataTree, tree) => { const widgets = { ...entities.canvasWidgets }; Object.keys(widgets).forEach(widgetKey => { - widgets[widgetKey] = enhanceWithDynamicValuesAndValidations( - widgets[widgetKey], - nameBindingsWithData, - true, - ); + const evaluatedWidget = _.find(tree, { widgetId: widgetKey }); + if (evaluatedWidget) { + widgets[widgetKey] = evaluatedWidget; + } }); return widgets; }, diff --git a/app/client/src/selectors/nameBindingsWithDataSelector.ts b/app/client/src/selectors/nameBindingsWithDataSelector.ts index a416b90bd4..051e0f3e19 100644 --- a/app/client/src/selectors/nameBindingsWithDataSelector.ts +++ b/app/client/src/selectors/nameBindingsWithDataSelector.ts @@ -4,37 +4,42 @@ import { createSelector } from "reselect"; import { getActions, getDataTree } from "./entitiesSelector"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; import createCachedSelector from "re-reselect"; +import { getEvaluatedDataTree } from "utils/DynamicBindingUtils"; export type NameBindingsWithData = Record; + export const getNameBindingsWithData = createSelector( getDataTree, (dataTree: DataTree): NameBindingsWithData => { const nameBindingsWithData: Record = {}; Object.keys(dataTree.nameBindings).forEach(key => { const nameBindings = dataTree.nameBindings[key]; - const evaluatedValue = JSONPath({ + nameBindingsWithData[key] = JSONPath({ path: nameBindings, json: dataTree, })[0]; - if (evaluatedValue && key !== "undefined") { - nameBindingsWithData[key] = evaluatedValue; - } }); - return nameBindingsWithData; }, ); +export const getParsedDataTree = createSelector( + getNameBindingsWithData, + (namedBindings: NameBindingsWithData) => { + return getEvaluatedDataTree(namedBindings, true); + }, +); + // For autocomplete. Use actions cached responses if // there isn't a response already export const getNameBindingsForAutocomplete = createCachedSelector( - getNameBindingsWithData, + getParsedDataTree, getActions, - (namedBindings: NameBindingsWithData, actions: ActionDataState["data"]) => { + (dataTree: NameBindingsWithData, actions: ActionDataState["data"]) => { const cachedResponses: Record = {}; if (actions && actions.length) { actions.forEach(action => { - if (!(action.name in namedBindings) && action.cacheResponse) { + if (!(action.name in dataTree) && action.cacheResponse) { try { cachedResponses[action.name] = JSON.parse(action.cacheResponse); } catch (e) { @@ -43,6 +48,6 @@ export const getNameBindingsForAutocomplete = createCachedSelector( } }); } - return { ...namedBindings, ...cachedResponses }; + return { ...dataTree, ...cachedResponses }; }, )((state: AppState) => state.entities.actions.data.length); diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index f264e1642f..85b5876416 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -4,12 +4,16 @@ import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer" import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer"; -import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; +import { + enhanceWidgetWithValidations, + getEvaluatedDataTree, +} from "utils/DynamicBindingUtils"; import { WidgetProps } from "widgets/BaseWidget"; import { - getNameBindingsWithData, NameBindingsWithData, + getNameBindingsWithData, } from "./nameBindingsWithDataSelector"; +import _ from "lodash"; const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => state.ui.propertyPane; @@ -41,14 +45,23 @@ export const getWidgetPropsWithValidations = createSelector( getNameBindingsWithData, ( widget: WidgetProps | undefined, - nameBindigsWithData: NameBindingsWithData, + nameBindingsWithData: NameBindingsWithData, ) => { if (!widget) return undefined; - return enhanceWithDynamicValuesAndValidations( - widget, - nameBindigsWithData, - false, + const tree = getEvaluatedDataTree(nameBindingsWithData, false); + const evaluatedWidget = _.find(tree, { widgetId: widget.widgetId }); + const validations = enhanceWidgetWithValidations( + evaluatedWidget as WidgetProps, ); + if (validations) { + const { invalidProps, validationMessages } = validations; + return { + ...widget, + invalidProps, + validationMessages, + }; + } + return widget; }, ); diff --git a/app/client/src/utils/DerivedPropertiesFactory.ts b/app/client/src/utils/DerivedPropertiesFactory.ts new file mode 100644 index 0000000000..c513e578b2 --- /dev/null +++ b/app/client/src/utils/DerivedPropertiesFactory.ts @@ -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; + } +} diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index b365337093..59069b5dae 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -3,8 +3,9 @@ import { WidgetProps } from "widgets/BaseWidget"; import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import ValidationFactory from "./ValidationFactory"; import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton"; -import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector"; import unescapeJS from "unescape-js"; +import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector"; +import toposort from "toposort"; export const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value); @@ -51,11 +52,34 @@ export function parseDynamicString(dynamicString: string): string[] { return parsedDynamicValues; } +const getAllPaths = ( + tree: Record, + prefix = "", +): Record => { + return Object.keys(tree).reduce((res: Record, 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 = ( dynamicString: string, ): { bindings: string[]; paths: string[] } => { + if (!dynamicString) return { bindings: [], paths: [] }; + const sanitisedString = dynamicString.trim(); // Get the {{binding}} bound values - const bindings = parseDynamicString(dynamicString); + const bindings = parseDynamicString(sanitisedString); // Get the "binding" path values const paths = bindings.map(binding => { const length = binding.length; @@ -105,22 +129,7 @@ export const getDynamicValue = ( // Get the Data Tree value of those "binding "paths const values = paths.map((p, i) => { if (p) { - const value = 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; - } + return evaluateDynamicBoundValue(data, p); } else { return bindings[i]; } @@ -134,34 +143,161 @@ export const getDynamicValue = ( return undefined; }; -export const enhanceWithDynamicValuesAndValidations = ( +export const enhanceWidgetWithValidations = ( widget: WidgetProps, - nameBindingsWithData: NameBindingsWithData, - replaceWithParsed: boolean, ): WidgetProps => { if (!widget) return widget; const properties = { ...widget }; const invalidProps: Record = {}; const validationMessages: Record = {}; - - Object.keys(widget).forEach((property: string) => { - let value = widget[property]; - // Check for dynamic bindings - if (widget.dynamicBindings && property in widget.dynamicBindings) { - value = getDynamicValue(value, nameBindingsWithData); - } + Object.keys(properties).forEach((property: string) => { + const value = properties[property]; // Pass it through validation and parse - const { - isValid, - parsed, - message, - } = ValidationFactory.validateWidgetProperty(widget.type, property, value); + const { isValid, message } = ValidationFactory.validateWidgetProperty( + widget.type, + property, + value, + ); // Store all invalid props if (!isValid) invalidProps[property] = true; // Store validation Messages 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>; +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, +): Array => { + const subDeps: Array = []; + 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; + } +} diff --git a/app/client/src/utils/DynamicBindingsUtil.test.ts b/app/client/src/utils/DynamicBindingsUtil.test.ts index 4c5e6c46c5..d646b2bf8e 100644 --- a/app/client/src/utils/DynamicBindingsUtil.test.ts +++ b/app/client/src/utils/DynamicBindingsUtil.test.ts @@ -8,7 +8,11 @@ jest.mock("jsExecution/RealmExecutor", () => { return { execute: mockExecute, registerLibrary: mockRegisterLibrary }; }); }); -import { getDynamicValue, parseDynamicString } from "./DynamicBindingUtils"; +import { + dependencySortedEvaluateDataTree, + getDynamicValue, + parseDynamicString, +} from "./DynamicBindingUtils"; import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector"; import { AppState, DataTree } from "reducers"; @@ -121,3 +125,43 @@ it("Parse the dynamic string", () => { 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); +}); diff --git a/app/client/src/utils/WidgetFactory.tsx b/app/client/src/utils/WidgetFactory.tsx index a9080fd8fc..d659a5c3df 100644 --- a/app/client/src/utils/WidgetFactory.tsx +++ b/app/client/src/utils/WidgetFactory.tsx @@ -6,20 +6,33 @@ import { } from "widgets/BaseWidget"; import { WidgetPropertyValidationType } from "./ValidationFactory"; +type WidgetDerivedPropertyType = any; +export type DerivedPropertiesMap = Record; + class WidgetFactory { static widgetMap: Map> = new Map(); static widgetPropValidationMap: Map< WidgetType, WidgetPropertyValidationType > = new Map(); + static widgetDerivedPropertiesGetterMap: Map< + WidgetType, + WidgetDerivedPropertyType + > = new Map(); + static derivedPropertiesMap: Map< + WidgetType, + DerivedPropertiesMap + > = new Map(); static registerWidgetBuilder( widgetType: WidgetType, widgetBuilder: WidgetBuilder, widgetPropertyValidation: WidgetPropertyValidationType, + derivedPropertiesMap: DerivedPropertiesMap, ) { this.widgetMap.set(widgetType, widgetBuilder); this.widgetPropValidationMap.set(widgetType, widgetPropertyValidation); + this.derivedPropertiesMap.set(widgetType, derivedPropertiesMap); } static createWidget( @@ -60,6 +73,17 @@ class WidgetFactory { } 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 { diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 479ae95f81..83baed55e7 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -14,7 +14,7 @@ import { WidgetOperations, WidgetOperation, } from "widgets/BaseWidget"; -import { WidgetType, RenderModes } from "constants/WidgetConstants"; +import { WidgetType } from "constants/WidgetConstants"; import { generateReactKey } from "utils/generators"; import { GridDefaults, @@ -25,6 +25,7 @@ import { } from "constants/WidgetConstants"; import { snapToGrid } from "./helpers"; import { OccupiedSpace } from "constants/editorConstants"; +import { DerivedPropFactory } from "utils/DerivedPropertiesFactory"; export type WidgetOperationParams = { operation: WidgetOperation; @@ -289,17 +290,26 @@ export const generateWidgetProps = ( children: [], }; } + const derivedProperties = DerivedPropFactory.getDerivedPropertiesOfWidgetType( + type, + widgetName, + ); + const dynamicBindings: Record = {}; + Object.keys(derivedProperties).forEach(prop => { + dynamicBindings[prop] = true; + }); return { ...widgetConfig, type, - widgetName: widgetName, + widgetName, isVisible: true, isLoading: false, parentColumnSpace, parentRowSpace, - renderMode: RenderModes.CANVAS, + dynamicBindings, ...sizes, ...others, + ...derivedProperties, }; } else { if (parent) { diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx index 7d067a66c8..ee61961a00 100644 --- a/app/client/src/utils/WidgetRegistry.tsx +++ b/app/client/src/utils/WidgetRegistry.tsx @@ -32,6 +32,7 @@ class WidgetBuilderRegistry { }, }, ContainerWidget.getPropertyValidationMap(), + ContainerWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -42,6 +43,7 @@ class WidgetBuilderRegistry { }, }, TextWidget.getPropertyValidationMap(), + TextWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -52,6 +54,7 @@ class WidgetBuilderRegistry { }, }, ButtonWidget.getPropertyValidationMap(), + ButtonWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -62,6 +65,7 @@ class WidgetBuilderRegistry { }, }, SpinnerWidget.getPropertyValidationMap(), + SpinnerWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -72,6 +76,7 @@ class WidgetBuilderRegistry { }, }, InputWidget.getPropertyValidationMap(), + InputWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -82,6 +87,7 @@ class WidgetBuilderRegistry { }, }, CheckboxWidget.getPropertyValidationMap(), + CheckboxWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -92,6 +98,7 @@ class WidgetBuilderRegistry { }, }, DropdownWidget.getPropertyValidationMap(), + DropdownWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -102,6 +109,7 @@ class WidgetBuilderRegistry { }, }, RadioGroupWidget.getPropertyValidationMap(), + RadioGroupWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( @@ -112,6 +120,7 @@ class WidgetBuilderRegistry { }, }, ImageWidget.getPropertyValidationMap(), + ImageWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( "TABLE_WIDGET", @@ -121,6 +130,7 @@ class WidgetBuilderRegistry { }, }, TableWidget.getPropertyValidationMap(), + TableWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( "FILE_PICKER_WIDGET", @@ -130,6 +140,7 @@ class WidgetBuilderRegistry { }, }, FilePickerWidget.getPropertyValidationMap(), + FilePickerWidget.getDerivedPropertiesMap(), ); WidgetFactory.registerWidgetBuilder( "DATE_PICKER_WIDGET", @@ -139,6 +150,7 @@ class WidgetBuilderRegistry { }, }, DatePickerWidget.getPropertyValidationMap(), + DatePickerWidget.getDerivedPropertiesMap(), ); } } diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index de59ee14e4..149fc385d1 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -28,6 +28,7 @@ import { PositionTypes } from "constants/WidgetConstants"; import ErrorBoundary from "components/editorComponents/ErrorBoundry"; import { WidgetPropertyValidationType } from "utils/ValidationFactory"; +import { DerivedPropertiesMap } from "utils/WidgetFactory"; /*** * BaseWidget * @@ -63,6 +64,10 @@ abstract class BaseWidget< return {}; } + static getDerivedPropertiesMap(): DerivedPropertiesMap { + return {}; + } + /** * Widget abstraction to register the widget type * ```javascript diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx index 58bef6ae01..fba9858a53 100644 --- a/app/client/src/widgets/DropdownWidget.tsx +++ b/app/client/src/widgets/DropdownWidget.tsx @@ -7,6 +7,10 @@ import _ from "lodash"; import { WidgetPropertyValidationType } from "utils/ValidationFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; +export interface DropDownDerivedProps { + selectedOption?: DropdownOption; + selectedOptionArr?: DropdownOption[]; +} class DropdownWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { return { @@ -18,6 +22,23 @@ class DropdownWidget extends BaseWidget { 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() { return ( { selectedOptionValue: VALIDATION_TYPES.TEXT, }; } + static getDerivedPropertiesMap() { + return { + selectedOption: + "{{_.find(this.options, { value: this.selectedOptionValue })}}", + }; + } getPageView() { return ( { nextPageKey: VALIDATION_TYPES.TEXT, prevPageKey: 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 { isLoading={this.props.isLoading} height={this.state.componentHeight} width={this.state.componentWidth} - selectedRowIndex={ - this.props.selectedRow && this.props.selectedRow.rowIndex - } + selectedRowIndex={this.props.selectedRowIndex} disableDrag={(disable: boolean) => { this.disableDrag(disable); }} onRowClick={(rowData: object, index: number) => { const { onRowSelected } = this.props; - this.updateSelectedRowProperty(rowData, index); + this.updateSelectedRowProperty(index); + super.executeAction(onRowSelected); }} - > + /> ); } - componentDidUpdate(prevProps: TableWidgetProps) { - super.componentDidUpdate(prevProps); - if ( - !_.isEqual(prevProps.tableData, this.props.tableData) && - prevProps.selectedRow - ) { - this.updateSelectedRowProperty( - this.props.tableData[prevProps.selectedRow.rowIndex], - prevProps.selectedRow.rowIndex, - ); - } - } + // componentDidUpdate(prevProps: TableWidgetProps) { + // super.componentDidUpdate(prevProps); + // if ( + // !_.isEqual(prevProps.tableData, this.props.tableData) && + // prevProps.selectedRow + // ) { + // this.updateSelectedRowProperty( + // this.props.tableData[prevProps.selectedRow.rowIndex], + // prevProps.selectedRow.rowIndex, + // ); + // } + // } - updateSelectedRowProperty(rowData: object, index: number) { + updateSelectedRowProperty(index: number) { const { widgetId } = this.props; - this.updateWidgetProperty(widgetId, "selectedRow", { - ...rowData, - rowIndex: index, - }); + this.updateWidgetProperty(widgetId, "selectedRowIndex", index); } getWidgetType(): WidgetType { @@ -102,7 +103,7 @@ export interface TableWidgetProps extends WidgetProps { recordActions?: TableAction[]; onPageChange?: ActionPayload[]; onRowSelected?: ActionPayload[]; - selectedRow?: SelectedRow; + selectedRowIndex?: number; } export default TableWidget; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 2f5f74e5ea..b53b3dbacd 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -2569,6 +2569,11 @@ resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf" 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@*": version "3.0.4" 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" 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: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"