diff --git a/app/client/src/actions/metaActions.ts b/app/client/src/actions/metaActions.ts index 2de6ff500c..fea4484f39 100644 --- a/app/client/src/actions/metaActions.ts +++ b/app/client/src/actions/metaActions.ts @@ -1,9 +1,7 @@ import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; import { BatchAction, batchAction } from "actions/batchActions"; +import { Diff } from "deep-diff"; import { DataTree } from "entities/DataTree/dataTreeFactory"; -import { isWidget } from "../workers/evaluationUtils"; -import { MetaState } from "../reducers/entityReducers/metaReducer"; -import isEmpty from "lodash/isEmpty"; export interface UpdateWidgetMetaPropertyPayload { widgetId: string; @@ -47,18 +45,15 @@ export const resetChildrenMetaProperty = ( }; }; -export const updateMetaState = (evaluatedDataTree: DataTree) => { - const updatedWidgetMetaState: MetaState = {}; - Object.values(evaluatedDataTree).forEach((entity) => { - if (isWidget(entity) && entity.widgetId && !isEmpty(entity.meta)) { - updatedWidgetMetaState[entity.widgetId] = entity.meta; - } - }); - +export const updateMetaState = ( + updates: Diff[], + updatedDataTree: DataTree, +) => { return { type: ReduxActionTypes.UPDATE_META_STATE, payload: { - updatedWidgetMetaState, + updates, + updatedDataTree, }, }; }; diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx index f598f1ee3f..4f6d93df8c 100644 --- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx +++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx @@ -118,7 +118,8 @@ export interface ExecuteErrorPayload extends ErrorActionPayload { export const urlGroupsRegexExp = /^(https?:\/{2}\S+?)(\/[\s\S]*?)?(\?(?![^{]*})[\s\S]*)?$/; export const EXECUTION_PARAM_KEY = "executionParams"; -export const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g; +export const EXECUTION_PARAM_REFERENCE_REGEX = /this.params|this\?.params/g; +export const THIS_DOT_PARAMS_KEY = "params"; export const RESP_HEADER_DATATYPE = "X-APPSMITH-DATATYPE"; export const API_REQUEST_HEADERS: APIHeaders = { diff --git a/app/client/src/reducers/entityReducers/metaReducer.ts b/app/client/src/reducers/entityReducers/metaReducer.ts index 9facbe6fae..e33ba32066 100644 --- a/app/client/src/reducers/entityReducers/metaReducer.ts +++ b/app/client/src/reducers/entityReducers/metaReducer.ts @@ -1,13 +1,16 @@ -import { set, cloneDeep } from "lodash"; +import { set, cloneDeep, get } from "lodash"; import { createReducer } from "utils/AppsmithUtils"; import { UpdateWidgetMetaPropertyPayload } from "actions/metaActions"; -import isObject from "lodash/isObject"; -import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; + import { ReduxActionTypes, ReduxAction, WidgetReduxActionTypes, } from "constants/ReduxActionConstants"; +import { Diff } from "deep-diff"; +import produce from "immer"; +import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { isWidget } from "../../workers/evaluationUtils"; export type MetaState = Record>; @@ -17,24 +20,41 @@ export const metaReducer = createReducer(initialState, { [ReduxActionTypes.UPDATE_META_STATE]: ( state: MetaState, action: ReduxAction<{ - updatedWidgetMetaState: Record; + updates: Diff[]; + updatedDataTree: DataTree; }>, ) => { - // if metaObject is updated in dataTree we also update meta values, to keep meta state in sync. - const newMetaState = cloneDeep(state); - const { updatedWidgetMetaState } = action.payload; + const { updatedDataTree, updates } = action.payload; - Object.entries(updatedWidgetMetaState).forEach( - ([entityWidgetId, entityMetaState]) => { - if (isObject(newMetaState[entityWidgetId])) { - Object.keys(newMetaState[entityWidgetId]).forEach((key) => { - if (key in entityMetaState) { - newMetaState[entityWidgetId][key] = entityMetaState[key]; + // if metaObject is updated in dataTree we also update meta values, to keep meta state in sync. + const newMetaState = produce(state, (draftMetaState) => { + if (updates.length) { + updates.forEach((update) => { + // if meta field is updated in the dataTree then update metaReducer values. + if ( + update.kind === "E" && + update.path?.length && + update.path?.length > 1 && + update.path[1] === "meta" + ) { + // path eg: Input1.meta.defaultText + const entity = get(updatedDataTree, update.path[0]); + const metaPropertyPath = update.path.slice(2); + if ( + isWidget(entity) && + entity.widgetId && + metaPropertyPath.length + ) { + set( + draftMetaState, + [entity.widgetId, ...metaPropertyPath], + update.rhs, + ); } - }); - } - }, - ); + } + }); + } + }); return newMetaState; }, [ReduxActionTypes.SET_META_PROP]: ( diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 8ff1dbf042..0029be622d 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -140,7 +140,7 @@ function* evaluateTreeSaga( PerformanceTransactionName.SET_EVALUATED_TREE, ); - yield put(updateMetaState(dataTree)); + yield put(updateMetaState(updates, dataTree)); const updatedDataTree = yield select(getDataTree); log.debug({ jsUpdates: jsUpdates }); diff --git a/app/client/src/utils/DSLMigrations.ts b/app/client/src/utils/DSLMigrations.ts index 7663bcc193..6c2848200d 100644 --- a/app/client/src/utils/DSLMigrations.ts +++ b/app/client/src/utils/DSLMigrations.ts @@ -25,7 +25,6 @@ import { migrateTableSanitizeColumnKeys, isSortableMigration, migrateTableWidgetIconButtonVariant, - migrateTableWidgetNumericColumnName, } from "./migrations/TableWidget"; import { migrateTextStyleFromTextWidget } from "./migrations/TextWidgetReplaceTextStyle"; import { DATA_BIND_REGEX_GLOBAL } from "constants/BindingsConstants"; @@ -1039,7 +1038,10 @@ export const transformDSL = ( } if (currentDSL.version === 50) { - currentDSL = migrateTableWidgetNumericColumnName(currentDSL); + /* + * We're skipping this to fix a bad table migration - migrateTableWidgetNumericColumnName + * it overwrites the computedValue of the table columns + */ currentDSL.version = LATEST_PAGE_VERSION; } diff --git a/app/client/src/utils/migrations/TableWidget.ts b/app/client/src/utils/migrations/TableWidget.ts index fb41c29533..6f3d7db200 100644 --- a/app/client/src/utils/migrations/TableWidget.ts +++ b/app/client/src/utils/migrations/TableWidget.ts @@ -490,6 +490,9 @@ export const migrateTableWidgetIconButtonVariant = (currentDSL: DSLWidget) => { return currentDSL; }; +/* + * DO NOT USE THIS. it overwrites conputedValues of the Table Columns + */ export const migrateTableWidgetNumericColumnName = (currentDSL: DSLWidget) => { currentDSL.children = currentDSL.children?.map((child: WidgetProps) => { if (child.type === "TABLE_WIDGET") { diff --git a/app/client/src/workers/DataTreeEvaluator.test.ts b/app/client/src/workers/DataTreeEvaluator.test.ts new file mode 100644 index 0000000000..77c158e0cf --- /dev/null +++ b/app/client/src/workers/DataTreeEvaluator.test.ts @@ -0,0 +1,109 @@ +import DataTreeEvaluator from "./DataTreeEvaluator"; + +describe("DataTreeEvaluator", () => { + let dataTreeEvaluator: DataTreeEvaluator; + beforeAll(() => { + dataTreeEvaluator = new DataTreeEvaluator({}); + }); + describe("evaluateActionBindings", () => { + it("handles this.params.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(function() { return this.params.property })()", + "(() => { return this.params.property })()", + 'this.params.property || "default value"', + 'this.params.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles this?.params.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(() => { return this?.params.property })()", + "(function() { return this?.params.property })()", + 'this?.params.property || "default value"', + 'this?.params.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles this?.params?.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(() => { return this?.params?.property })()", + "(function() { return this?.params?.property })()", + 'this?.params?.property || "default value"', + 'this?.params?.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles executionParams.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(function() { return executionParams.property })()", + "(() => { return executionParams.property })()", + 'executionParams.property || "default value"', + 'executionParams.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles executionParams?.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(function() { return executionParams?.property })()", + "(() => { return executionParams?.property })()", + 'executionParams?.property || "default value"', + 'executionParams?.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + }); +}); diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts index 51313b71c5..22a57ce6d8 100644 --- a/app/client/src/workers/DataTreeEvaluator.ts +++ b/app/client/src/workers/DataTreeEvaluator.ts @@ -55,11 +55,13 @@ import toposort from "toposort"; import { EXECUTION_PARAM_KEY, EXECUTION_PARAM_REFERENCE_REGEX, + THIS_DOT_PARAMS_KEY, } from "constants/AppsmithActionConstants/ActionConstants"; import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import evaluateSync, { createGlobalData, EvalResult, + EvaluateContext, EvaluationScriptType, getScriptToEval, evaluateAsync, @@ -609,12 +611,19 @@ export default class DataTreeEvaluator { entity.bindingPaths[propertyPath] || EvaluationSubstitutionType.TEMPLATE; + const contextData: EvaluateContext = {}; + if (isAction(entity)) { + contextData.thisContext = { + params: {}, + }; + } try { evalPropertyValue = this.getDynamicValue( unEvalPropertyValue, currentTree, resolvedFunctions, evaluationSubstitutionType, + contextData, undefined, fullPropertyPath, ); @@ -764,6 +773,7 @@ export default class DataTreeEvaluator { data: DataTree, resolvedFunctions: Record, evaluationSubstitutionType: EvaluationSubstitutionType, + contextData?: EvaluateContext, callBackData?: Array, fullPropertyPath?: string, ) { @@ -792,6 +802,7 @@ export default class DataTreeEvaluator { toBeSentForEval, data, resolvedFunctions, + contextData, callBackData, ); if (fullPropertyPath && result.errors.length) { @@ -864,10 +875,17 @@ export default class DataTreeEvaluator { js: string, data: DataTree, resolvedFunctions: Record, + contextData?: EvaluateContext, callbackData?: Array, ): EvalResult { try { - return evaluateSync(js, data, resolvedFunctions, callbackData); + return evaluateSync( + js, + data, + resolvedFunctions, + contextData, + callbackData, + ); } catch (e) { return { result: undefined, @@ -1555,24 +1573,26 @@ export default class DataTreeEvaluator { ); } - // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility) - const bindingsForExecutionParams: string[] = bindings.map( - (binding: string) => - binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY), - ); - - const dataTreeWithExecutionParams = Object.assign({}, this.evalTree, { - [EXECUTION_PARAM_KEY]: evaluatedExecutionParams, - }); - - return bindingsForExecutionParams.map((binding) => - this.getDynamicValue( - `{{${binding}}}`, - dataTreeWithExecutionParams, + return bindings.map((binding) => { + // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility) + // also helps with dealing with IIFE which are normal functions (not arrow) + // because normal functions won't retain 'this' context (when executed elsewhere) + const replacedBinding = binding.replace( + EXECUTION_PARAM_REFERENCE_REGEX, + EXECUTION_PARAM_KEY, + ); + return this.getDynamicValue( + `{{${replacedBinding}}}`, + this.evalTree, this.resolvedFunctions, EvaluationSubstitutionType.TEMPLATE, - ), - ); + // params can be accessed via "this.params" or "executionParams" + { + thisContext: { [THIS_DOT_PARAMS_KEY]: evaluatedExecutionParams }, + globalContext: { [EXECUTION_PARAM_KEY]: evaluatedExecutionParams }, + }, + ); + }); } clearErrors() { diff --git a/app/client/src/workers/evaluate.test.ts b/app/client/src/workers/evaluate.test.ts index a34cbcf116..3e1963b772 100644 --- a/app/client/src/workers/evaluate.test.ts +++ b/app/client/src/workers/evaluate.test.ts @@ -30,6 +30,9 @@ describe("evaluateSync", () => { triggerPaths: {}, validationPaths: {}, logBlackList: {}, + overridingPropertyPaths: {}, + privateWidgets: {}, + propertyOverrideDependency: {}, }; const dataTree: DataTree = { Input1: widget, @@ -75,7 +78,7 @@ describe("evaluateSync", () => { const result = wrongJS return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "wrongJS", @@ -89,7 +92,7 @@ describe("evaluateSync", () => { const result = wrongJS return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "wrongJS", @@ -108,7 +111,7 @@ describe("evaluateSync", () => { const result = {}.map() return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "{}.map()", @@ -135,7 +138,7 @@ describe("evaluateSync", () => { const result = setTimeout(() => {}, 100) return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "setTimeout(() => {}, 100)", @@ -151,7 +154,7 @@ describe("evaluateSync", () => { it("evaluates functions with callback data", () => { const js = "(arg1, arg2) => arg1.value + arg2"; const callbackData = [{ value: "test" }, "1"]; - const response = evaluate(js, dataTree, {}, callbackData); + const response = evaluate(js, dataTree, {}, {}, callbackData); expect(response.result).toBe("test1"); }); it("handles EXPRESSIONS with new lines", () => { @@ -165,22 +168,38 @@ describe("evaluateSync", () => { }); it("handles TRIGGERS with new lines", () => { let js = "\n"; - let response = evaluate(js, dataTree, {}, undefined); + let response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); js = "\n\n\n"; - response = evaluate(js, dataTree, {}, undefined); + response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); }); it("handles ANONYMOUS_FUNCTION with new lines", () => { let js = "\n"; - let response = evaluate(js, dataTree, {}, undefined); + let response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); js = "\n\n\n"; - response = evaluate(js, dataTree, {}, undefined); + response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); }); + it("has access to this context", () => { + const js = "this.contextVariable"; + const thisContext = { contextVariable: "test" }; + const response = evaluate(js, dataTree, {}, { thisContext }); + expect(response.result).toBe("test"); + // there should not be any error when accessing "this" variables + expect(response.errors).toHaveLength(0); + }); + + it("has access to additional global context", () => { + const js = "contextVariable"; + const globalContext = { contextVariable: "test" }; + const response = evaluate(js, dataTree, {}, { globalContext }); + expect(response.result).toBe("test"); + expect(response.errors).toHaveLength(0); + }); }); describe("evaluateAsync", () => { @@ -256,7 +275,7 @@ describe("isFunctionAsync", () => { if (typeof testFunc === "string") { testFunc = eval(testFunc); } - const actual = isFunctionAsync(testFunc, {}); + const actual = isFunctionAsync(testFunc, {}, {}); expect(actual).toBe(testCase.expected); } }); diff --git a/app/client/src/workers/evaluate.ts b/app/client/src/workers/evaluate.ts index 521080e987..ed023a6b1f 100644 --- a/app/client/src/workers/evaluate.ts +++ b/app/client/src/workers/evaluate.ts @@ -34,12 +34,12 @@ export const EvaluationScripts: Record = { const result = ${ScriptTemplate} return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, [EvaluationScriptType.ANONYMOUS_FUNCTION]: ` function callback (script) { const userFunction = script; - const result = userFunction?.apply(self, ARGUMENTS); + const result = userFunction?.apply(THIS_CONTEXT, ARGUMENTS); return result; } callback(${ScriptTemplate}) @@ -49,7 +49,7 @@ export const EvaluationScripts: Record = { const result = await ${ScriptTemplate}; return result; } - closedFunction(); + closedFunction.call(THIS_CONTEXT); `, }; @@ -102,6 +102,18 @@ export const createGlobalData = ( const GLOBAL_DATA: Record = {}; ///// Adding callback data GLOBAL_DATA.ARGUMENTS = evalArguments; + //// Adding contextual data not part of data tree + GLOBAL_DATA.THIS_CONTEXT = {}; + if (context) { + if (context.thisContext) { + GLOBAL_DATA.THIS_CONTEXT = context.thisContext; + } + if (context.globalContext) { + Object.entries(context.globalContext).forEach(([key, value]) => { + GLOBAL_DATA[key] = value; + }); + } + } //// Add internal functions to dataTree; const dataTreeWithFunctions = enhanceDataTreeWithFunctions( dataTree, @@ -134,9 +146,13 @@ export function sanitizeScript(js: string) { } /** Define a context just for this script + * thisContext will define it on the `this` + * globalContext will define it globally * requestId is used for completing promises */ export type EvaluateContext = { + thisContext?: Record; + globalContext?: Record; requestId?: string; }; @@ -172,6 +188,7 @@ export default function evaluateSync( userScript: string, dataTree: DataTree, resolvedFunctions: Record, + context?: EvaluateContext, evalArguments?: Array, ): EvalResult { return (function() { @@ -181,7 +198,7 @@ export default function evaluateSync( const GLOBAL_DATA: Record = createGlobalData( dataTree, resolvedFunctions, - undefined, + context, evalArguments, ); GLOBAL_DATA.ALLOW_ASYNC = false; diff --git a/app/client/src/workers/evaluationUtils.ts b/app/client/src/workers/evaluationUtils.ts index 2808024ef8..f79d83eb31 100644 --- a/app/client/src/workers/evaluationUtils.ts +++ b/app/client/src/workers/evaluationUtils.ts @@ -805,10 +805,11 @@ export const overrideWidgetProperties = ( const overridingPropertyPaths = entity.overridingPropertyPaths[propertyPath]; - overridingPropertyPaths.forEach((overriddenPropertyKey) => { + overridingPropertyPaths.forEach((overriddenPropertyPath) => { + const overriddenPropertyPathArray = overriddenPropertyPath.split("."); _.set( currentTree, - `${entity.widgetName}.${overriddenPropertyKey}`, + [entity.widgetName, ...overriddenPropertyPathArray], clonedValue, ); }); @@ -824,9 +825,10 @@ export const overrideWidgetProperties = ( const defaultValue = entity[propertyOverridingKeyMap.DEFAULT]; const clonedDefaultValue = cloneDeep(defaultValue); if (defaultValue !== undefined) { + const propertyPathArray = propertyPath.split("."); _.set( currentTree, - `${entity.widgetName}.${propertyPath}`, + [entity.widgetName, ...propertyPathArray], clonedDefaultValue, ); return { diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts index b0fa3f7e6a..2ac004b312 100644 --- a/app/client/src/workers/validations.ts +++ b/app/client/src/workers/validations.ts @@ -803,7 +803,7 @@ export const VALIDATORS: Record = { }; if (config.params?.fnString && isString(config.params?.fnString)) { try { - const { result } = evaluate(config.params.fnString, {}, {}, [ + const { result } = evaluate(config.params.fnString, {}, {}, undefined, [ value, props, _,