diff --git a/app/client/public/index.html b/app/client/public/index.html
index f44efa34ec..0492727c0b 100755
--- a/app/client/public/index.html
+++ b/app/client/public/index.html
@@ -3,6 +3,7 @@
+
diff --git a/app/client/public/shims/realms-shim.umd.min.js b/app/client/public/shims/realms-shim.umd.min.js
new file mode 100644
index 0000000000..2b74c0b9cc
--- /dev/null
+++ b/app/client/public/shims/realms-shim.umd.min.js
@@ -0,0 +1,10 @@
+(function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):(a=a||self,a.Realm=b())})(this,function(){'use strict';function a(a,b=void 0){const c=`please report internal shim error: ${a}`;console.error(c),b&&(console.error(`${b}`),console.error(`${b.stack}`));debugger;throw c}function b(b,c){b||a(c)}function c(a){let b=`'use strict'; (${a})`;return b=b.replace(/\(0,\s*_[0-9a-fA-F]{3}\u200D\.e\)/g,"(0, eval)"),b=b.replace(/_[0-9a-fA-F]{3}\u200D\.g\./g,""),b=b.replace(/cov_[^+]+\+\+[;,]/g,""),b}function d(a,b){const{callAndWrapError:c}=a,{initRootRealm:d,initCompartment:e,getRealmGlobal:f,realmEvaluate:g}=b,{create:h,defineProperties:i}=Object;class j{constructor(){throw new TypeError("Realm is not a constructor")}static makeRootRealm(b={}){const e=h(j.prototype);return c(d,[a,e,b]),e}static makeCompartment(b={}){const d=h(j.prototype);return c(e,[a,d,b]),d}get global(){return c(f,[this])}evaluate(a,b,d={}){return c(g,[this,a,b,d])}}return i(j,{toString:{value:()=>"function Realm() { [shim code] }",writable:!1,enumerable:!1,configurable:!0}}),i(j.prototype,{toString:{value:()=>"[object Realm]",writable:!1,enumerable:!1,configurable:!0}}),j}function e(a){function c(c,e,f,g){for(const h of c){const c=G(a,h);c&&(b("value"in c,`unexpected accessor on global property: ${h}`),d[h]={value:c.value,writable:e,enumerable:f,configurable:g})}}const d={};return c(V,!1,!1,!1),c(W,!1,!1,!1),c(X,!0,!1,!0),d}function f(){function a(a){if(a===void 0||null===a)throw new TypeError(`can't convert undefined or null to object`);return Object(a)}function b(a){return"symbol"==typeof a?a:`${a}`}function c(a,b){if("function"!=typeof a)throw TypeError(`invalid ${b} usage`);return a}const{defineProperty:d,defineProperties:e,getOwnPropertyDescriptor:f,getPrototypeOf:g,prototype:h}=Object;try{(0,h.__lookupGetter__)("x")}catch(a){return}e(h,{__defineGetter__:{value:function(b,e){const f=a(this);d(f,b,{get:c(e,"getter"),enumerable:!0,configurable:!0})}},__defineSetter__:{value:function(b,e){const f=a(this);d(f,b,{set:c(e,"setter"),enumerable:!0,configurable:!0})}},__lookupGetter__:{value:function(c){let d=a(this);c=b(c);let e;for(;d&&!(e=f(d,c));)d=g(d);return e&&e.get}},__lookupSetter__:{value:function(c){let d=a(this);c=b(c);let e;for(;d&&!(e=f(d,c));)d=g(d);return e&&e.set}}})}function g(){function a(a,e){let f;try{f=(0,eval)(e)}catch(a){if(a instanceof SyntaxError)return;throw a}const g=c(f),h=function(){throw new TypeError("Not available")};b(h,{name:{value:a}}),b(g,{constructor:{value:h}}),b(h,{prototype:{value:g}}),h!==Function.prototype.constructor&&d(h,Function.prototype.constructor)}const{defineProperties:b,getPrototypeOf:c,setPrototypeOf:d}=Object;a("Function","(function(){})"),a("GeneratorFunction","(function*(){})"),a("AsyncFunction","(async function(){})"),a("AsyncGeneratorFunction","(async function*(){})")}function h(){const a=new Function("try {return this===global}catch(e){return false}")();if(!a)return;const b=require("vm"),c=b.runInNewContext(Z);return c}function i(){if("undefined"!=typeof document){const a=document.createElement("iframe");a.style.display="none",document.body.appendChild(a);const b=a.contentWindow.eval(Y);return b}}function j(a,b=[]){const c=e(a),d=a.eval,f=a.Function,g=d(B)();return E({unsafeGlobal:a,sharedGlobalDescs:c,unsafeEval:d,unsafeFunction:f,callAndWrapError:g,allShims:b})}function k(a){const b=$(),c=j(b,a),{unsafeEval:d}=c;return d(_)(),d(aa)(),c}function l(a,b={}){const c=I(a),d=P(c,c=>{if(c in b)return!1;if("eval"===c||ca.has(c)||!T(ba,c))return!1;const d=G(a,c);return!1===d.configurable&&!1===d.writable&&O(d,"value")});return d}function m(a){const b=a.search(ha);if(-1!==b){const c=a.slice(0,b).split("\n").length;throw new SyntaxError(`possible html comment syntax rejected around line ${c}`)}}function n(a){const b=a.search(ia);if(-1!==b){const c=a.slice(0,b).split("\n").length;throw new SyntaxError(`possible import expression rejected around line ${c}`)}}function o(a){const b=a.search(ja);if(-1!==b){const c=a.slice(0,b).split("\n").length;throw new SyntaxError(`possible direct eval expression rejected around line ${c}`)}}function p(a){m(a),n(a),o(a)}function q(a){return 0===a.length?"":`const {${R(a,",")}} = this;`}function r(a,b){const{unsafeFunction:c}=a,d=q(b);return c(`
+ with (arguments[0]) {
+ ${d}
+ return function() {
+ 'use strict';
+ return eval(arguments[0]);
+ };
+ }
+ `)}function s(b,c,d,e){const{unsafeEval:f}=b,g=f(ga);return function(h={},i={}){const j=i.transforms||[],k=S(j,d||[],[ka]);return function(d){let i={src:d,endowments:h};i=g(i,k);const j=l(c,i.endowments),m=l(i.endowments),n=S(j,m),o=r(b,n),p=f(da)(b,c,i.endowments,e),q=Proxy.revocable({},p),s=q.proxy,t=L(o,c,[s]);p.useUnsafeEvaluator=!0;let u;try{return L(t,c,[i.src])}catch(a){throw u=a,a}finally{p.useUnsafeEvaluator&&(q.revoke(),a("handler did not revoke useUnsafeEvaluator",u))}}}}function t(a,c){const{unsafeEval:d,unsafeFunction:e}=a,f=d(ea)(a,c);return b(J(f).constructor!==Function,"hide Function"),b(J(f).constructor!==e,"hide unsafeFunction"),f}function u(a){return(b,c,d={})=>a(c,d)(b)}function v(a,c){const{unsafeGlobal:d,unsafeEval:e,unsafeFunction:f}=a,g=e(fa)(a,function(...a){const b=`${Q(a)||""}`;let e=`${R(a,",")}`;if(!T(/^[\w\s,]*$/,e))throw new SyntaxError("shim limitation: Function arg must be simple ASCII identifiers, possibly separated by commas: no default values, pattern matches, or non-ASCII parameter names");if(new f(b),U(e,")"))throw new d.SyntaxError("shim limitation: Function arg string contains parenthesis");0(c,...d)=>b(a,c,d),d=c(Map.prototype.get),e=c(Set.prototype.has),f=new Map([["EvalError",EvalError],["RangeError",RangeError],["ReferenceError",ReferenceError],["SyntaxError",SyntaxError],["TypeError",TypeError],["URIError",URIError]]),g=new Set([EvalError.prototype,RangeError.prototype,ReferenceError.prototype,SyntaxError.prototype,TypeError.prototype,URIError.prototype,Error.prototype]);return function(c,h){try{return b(c,void 0,h)}catch(b){if(Object(b)!==b)throw b;if(e(g,a(b)))throw b;let c,h,i;try{c=`${b.name}`,h=`${b.message}`,i=`${b.stack||h}`}catch(a){throw new Error("unknown error")}const j=d(f,c)||Error;try{throw new j(h)}catch(a){throw a.stack=i,a}}}}),{assign:C,create:D,freeze:E,defineProperties:F,getOwnPropertyDescriptor:G,getOwnPropertyDescriptors:H,getOwnPropertyNames:I,getPrototypeOf:J,setPrototypeOf:K}=Object,{apply:L,ownKeys:M}=Reflect,N=a=>(b,...c)=>L(a,b,c),O=N(Object.prototype.hasOwnProperty),P=N(Array.prototype.filter),Q=N(Array.prototype.pop),R=N(Array.prototype.join),S=N(Array.prototype.concat),T=N(RegExp.prototype.test),U=N(String.prototype.includes),V=["Infinity","NaN","undefined"],W=["isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","Array","ArrayBuffer","Boolean","DataView","EvalError","Float32Array","Float64Array","Int8Array","Int16Array","Int32Array","Map","Number","Object","RangeError","ReferenceError","Set","String","Symbol","SyntaxError","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","URIError","WeakMap","WeakSet","JSON","Math","Reflect","escape","unescape"],X=["Date","Error","Promise","Proxy","RegExp","Intl"],Y="'use strict'; this",Z=`(0, eval)("'use strict'; this")`,$=()=>{const a=i(),b=h();if(!a&&!b||a&&b)throw new Error("unexpected platform, unable to create Realm");return a||b},_=c(f),aa=c(g),ba=/^[a-zA-Z_$][\w$]*$/,ca=new Set(["await","break","case","catch","class","const","continue","debugger","default","delete","do","else","export","extends","finally","for","function","if","import","in","instanceof","new","return","super","switch","this","throw","try","typeof","var","void","while","with","yield","let","static","enum","implements","package","protected","interface","private","public","await","null","true","false","this","arguments"]),da=c(function(a,b,c={},d=!1){const{unsafeGlobal:e,unsafeEval:f}=a,{freeze:g,getOwnPropertyDescriptor:h}=Object,{get:i,set:j}=Reflect,k=new Proxy(g({}),{get(a,b){throw new TypeError(`unexpected scope handler trap called: ${b+""}`)}});return{__proto__:k,useUnsafeEvaluator:!1,get(a,d){return"symbol"==typeof d?void 0:"eval"===d&&!0===this.useUnsafeEvaluator?(this.useUnsafeEvaluator=!1,f):d in c?i(c,d,b):i(b,d)},set(a,d,e){if(d in c){const a=h(c,d);return"value"in a?j(c,d,e):j(c,d,e,b)}return j(b,d,e)},has(a,f){return!!d||!!("eval"===f||f in c||f in b||f in e)},getPrototypeOf(){return null}}}),ea=c(function(a,b){const{callAndWrapError:c}=a,{defineProperties:d}=Object,e={eval(){return c(b,arguments)}}.eval;return d(e,{toString:{value:()=>`function ${"eval"}() { [shim code] }`,writable:!1,enumerable:!1,configurable:!0}}),e}),fa=c(function(a,b){const{callAndWrapError:c,unsafeFunction:d}=a,{defineProperties:e}=Object,f=function(){return c(b,arguments)};return e(f,{prototype:{value:d.prototype},toString:{value:()=>"function Function() { [shim code] }",writable:!1,enumerable:!1,configurable:!0}}),f}),ga=c(function(a,b){const{create:c,getOwnPropertyDescriptors:d}=Object,{apply:e}=Reflect,f=(a=>(b,...c)=>e(a,b,c))(Array.prototype.reduce);return a={src:`${a.src}`,endowments:c(null,d(a.endowments))},a=f(b,(a,b)=>b.rewrite?b.rewrite(a):a,a),a={src:`${a.src}`,endowments:c(null,d(a.endowments))},a}),ha=/(?:)/,ia=/\bimport\s*(?:\(|\/[/*])/,ja=/\beval\s*(?:\(|\/[/*])/,ka={rewrite(a){return p(a.src),a}},la=new WeakMap,ma={initRootRealm:function(a,b,c){const{shims:d,transforms:e,sloppyGlobals:f}=c,g=S(a.allShims,d),h=k(g),{unsafeEval:i}=h,j=i(A)(h,ma);h.sharedGlobalDescs.Realm={value:j,writable:!0,configurable:!0};const l=z(h,e,f),{safeEvalWhichTakesEndowments:m}=l;for(const d of g)m(d);x(b,l)},initCompartment:function(a,b,c={}){const{transforms:d,sloppyGlobals:e}=c,f=z(a,d,e);x(b,f)},getRealmGlobal:function(a){const{safeGlobal:b}=w(a);return b},realmEvaluate:function(a,b,c={},d={}){const{safeEvalWhichTakesEndowments:e}=w(a);return e(b,c,d)}},na=function(){const a=eval,b=a(Y);return f(),g(),j(b)}(),oa=d(na,ma);return oa});
+//# sourceMappingURL=realms-shim.umd.min.js.map
diff --git a/app/client/src/constants/ActionConstants.tsx b/app/client/src/constants/ActionConstants.tsx
index fe3fd47b29..aad107e36d 100644
--- a/app/client/src/constants/ActionConstants.tsx
+++ b/app/client/src/constants/ActionConstants.tsx
@@ -27,7 +27,7 @@ export const PropertyPaneActionDropdownOptions: DropdownOption[] = [
// { label: "Run Query", value: "QUERY" },
];
-export interface ActionPayload {
+export interface BaseActionPayload {
actionId: string;
actionType: ActionType;
contextParams: Record;
@@ -35,34 +35,42 @@ export interface ActionPayload {
onError?: ActionPayload[];
}
+export type ActionPayload =
+ | NavigateActionPayload
+ | SetValueActionPayload
+ | ExecuteJSActionPayload
+ | DownloadDataActionPayload
+ | SetValueActionPayload;
+
export type NavigationType = "NEW_TAB" | "INLINE";
-export interface NavigateActionPayload extends ActionPayload {
+export interface NavigateActionPayload extends BaseActionPayload {
pageUrl: string;
navigationType: NavigationType;
}
-export interface ShowAlertActionPayload extends ActionPayload {
+export interface ShowAlertActionPayload extends BaseActionPayload {
header: string;
message: string;
alertType: AlertType;
intent: MessageIntent;
}
-export interface SetValueActionPayload extends ActionPayload {
+export interface SetValueActionPayload extends BaseActionPayload {
header: string;
message: string;
alertType: AlertType;
intent: MessageIntent;
}
-export interface ExecuteJSActionPayload extends ActionPayload {
+export interface ExecuteJSActionPayload extends BaseActionPayload {
jsFunctionId: string;
+ jsFunction: string;
}
export type DownloadFiletype = "CSV" | "XLS" | "JSON" | "TXT";
-export interface DownloadDataActionPayload extends ActionPayload {
+export interface DownloadDataActionPayload extends BaseActionPayload {
data: JSON;
fileName: string;
fileType: DownloadFiletype;
diff --git a/app/client/src/constants/BindingsConstants.ts b/app/client/src/constants/BindingsConstants.ts
index e3fc912a00..d1f091312c 100644
--- a/app/client/src/constants/BindingsConstants.ts
+++ b/app/client/src/constants/BindingsConstants.ts
@@ -2,5 +2,6 @@
// TODO (hetu): Remove useless escapes and re-enable the above lint rule
export type NamePathBindingMap = Record;
export const DATA_BIND_REGEX = /{{(\s*[\w\.\[\]\d]+\s*)}}/g;
+export const DATA_BIND_JS_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g;
export const DATA_PATH_REGEX = /[\w\.\[\]\d]+/;
/* eslint-enable no-useless-escape */
diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx
index 37e80ccfa5..3e5499550a 100644
--- a/app/client/src/constants/ReduxActionConstants.tsx
+++ b/app/client/src/constants/ReduxActionConstants.tsx
@@ -121,6 +121,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
FETCH_PAGE_LIST_ERROR: "FETCH_PAGE_LIST_ERROR",
FETCH_APPLICATION_LIST_ERROR: "FETCH_APPLICATION_LIST_ERROR",
CREATE_APPLICATION_ERROR: "CREATE_APPLICATION_ERROR",
+ SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD",
};
export const ReduxFormActionTypes: { [key: string]: string } = {
diff --git a/app/client/src/jsExecution/JSExecutionManagerSingleton.ts b/app/client/src/jsExecution/JSExecutionManagerSingleton.ts
new file mode 100644
index 0000000000..796bb9ae03
--- /dev/null
+++ b/app/client/src/jsExecution/JSExecutionManagerSingleton.ts
@@ -0,0 +1,51 @@
+import RealmExecutor from "./RealmExecutor";
+
+export type JSExecutorGlobal = Record;
+export interface JSExecutor {
+ execute: (src: string, data: JSExecutorGlobal) => string;
+ registerLibrary: (accessor: string, lib: any) => void;
+ unRegisterLibrary: (accessor: string) => void;
+}
+
+enum JSExecutorType {
+ REALM,
+}
+
+class JSExecutionManager {
+ currentExecutor: JSExecutor;
+ executors: Record;
+ registerLibrary(accessor: string, lib: any) {
+ Object.keys(this.executors).forEach(type => {
+ const executor = this.executors[(type as any) as JSExecutorType];
+ executor.registerLibrary(accessor, lib);
+ });
+ }
+ unRegisterLibrary(accessor: string) {
+ Object.keys(this.executors).forEach(type => {
+ const executor = this.executors[(type as any) as JSExecutorType];
+ executor.unRegisterLibrary(accessor);
+ });
+ }
+ switchExecutor(type: JSExecutorType) {
+ const executor = this.executors[type];
+ if (!executor) {
+ throw new Error("Executor does not exist");
+ }
+ this.currentExecutor = executor;
+ }
+ constructor() {
+ const realmExecutor = new RealmExecutor();
+ this.executors = {
+ [JSExecutorType.REALM]: realmExecutor,
+ };
+ this.currentExecutor = realmExecutor;
+
+ this.registerLibrary("_", window._);
+ }
+ evaluateSync(jsSrc: string, data: JSExecutorGlobal) {
+ return this.currentExecutor.execute(jsSrc, data);
+ }
+}
+const JSExecutionManagerSingleton = new JSExecutionManager();
+
+export default JSExecutionManagerSingleton;
diff --git a/app/client/src/jsExecution/RealmExecutor.ts b/app/client/src/jsExecution/RealmExecutor.ts
new file mode 100644
index 0000000000..0f3c53f637
--- /dev/null
+++ b/app/client/src/jsExecution/RealmExecutor.ts
@@ -0,0 +1,42 @@
+import { JSExecutorGlobal, JSExecutor } from "./JSExecutionManagerSingleton";
+declare let Realm: any;
+
+export default class RealmExecutor implements JSExecutor {
+ rootRealm: any;
+ creaetSafeObject: any;
+ extrinsics: any[] = [];
+ createSafeFunction: (unsafeFn: Function) => Function;
+
+ libraries: Record = {};
+ constructor() {
+ this.rootRealm = Realm.makeRootRealm();
+ this.createSafeFunction = this.rootRealm.evaluate(`
+ (function createSafeFunction(unsafeFn) {
+ return function safeFn(...args) {
+ unsafeFn(...args);
+ }
+ })
+ `);
+ this.creaetSafeObject = this.rootRealm.evaluate(`
+ (function creaetSafeObject(unsafeObject) {
+ return JSON.parse(JSON.stringify(unsafeObject));
+ })
+ `);
+ }
+ registerLibrary(accessor: string, lib: any) {
+ this.rootRealm.global[accessor] = lib;
+ }
+ unRegisterLibrary(accessor: string) {
+ this.rootRealm.global[accessor] = null;
+ }
+ execute(sourceText: string, data: JSExecutorGlobal) {
+ const safeData = this.creaetSafeObject(data);
+ let result;
+ try {
+ result = this.rootRealm.evaluate(sourceText, safeData);
+ } catch (e) {
+ result = `Error: ${e}`;
+ }
+ return result;
+ }
+}
diff --git a/app/client/src/reducers/entityReducers/index.tsx b/app/client/src/reducers/entityReducers/index.tsx
index 9e1a3a74b7..91609d1326 100644
--- a/app/client/src/reducers/entityReducers/index.tsx
+++ b/app/client/src/reducers/entityReducers/index.tsx
@@ -8,6 +8,7 @@ import propertyPaneConfigReducer from "./propertyPaneConfigReducer";
import datasourceReducer from "./datasourceReducer";
import bindingsReducer from "./bindingsReducer";
import pageListReducer from "./pageListReducer";
+import jsExecutionsReducer from "./jsExecutionsReducer";
const entityReducer = combineReducers({
canvasWidgets: canvasWidgetsReducer,
@@ -19,6 +20,7 @@ const entityReducer = combineReducers({
datasources: datasourceReducer,
nameBindings: bindingsReducer,
pageList: pageListReducer,
+ jsExecutions: jsExecutionsReducer,
});
export default entityReducer;
diff --git a/app/client/src/reducers/entityReducers/jsExecutionsReducer.ts b/app/client/src/reducers/entityReducers/jsExecutionsReducer.ts
new file mode 100644
index 0000000000..6ca6c85240
--- /dev/null
+++ b/app/client/src/reducers/entityReducers/jsExecutionsReducer.ts
@@ -0,0 +1,21 @@
+import { createReducer } from "../../utils/AppsmithUtils";
+import {
+ ReduxActionTypes,
+ ReduxAction,
+} from "../../constants/ReduxActionConstants";
+
+export type JSExecutionRecord = Record;
+const initialState: JSExecutionRecord = {};
+const jsExecutionsReducer = createReducer(initialState, {
+ [ReduxActionTypes.SAVE_JS_EXECUTION_RECORD]: (
+ state: JSExecutionRecord,
+ action: ReduxAction,
+ ) => {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ },
+});
+
+export default jsExecutionsReducer;
diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts
index fdd46b65b2..86e288286a 100644
--- a/app/client/src/sagas/ActionSagas.ts
+++ b/app/client/src/sagas/ActionSagas.ts
@@ -12,7 +12,11 @@ import {
takeEvery,
takeLatest,
} from "redux-saga/effects";
-import { ActionPayload, PageAction } from "constants/ActionConstants";
+import {
+ ActionPayload,
+ PageAction,
+ ExecuteJSActionPayload,
+} from "constants/ActionConstants";
import ActionAPI, {
ActionApiResponse,
ActionCreateUpdateResponse,
@@ -35,6 +39,7 @@ import {
extractDynamicBoundValue,
getDynamicBindings,
isDynamicValue,
+ NameBindingsWithData,
} from "utils/DynamicBindingUtils";
import { validateResponse } from "./ErrorSagas";
import { getDataTree } from "selectors/entitiesSelector";
@@ -44,6 +49,8 @@ import {
} from "constants/messages";
import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms";
+import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
+import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
export const getAction = (
state: AppState,
@@ -90,6 +97,23 @@ export function* getActionParams(jsonPathKeys: string[] | undefined) {
return mapToPropList(dynamicBindings);
}
+function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) {
+ const nameBindingsWithData: NameBindingsWithData = yield select(
+ getNameBindingsWithData,
+ );
+ const result = JSExecutionManagerSingleton.evaluateSync(
+ jsAction.jsFunction,
+ nameBindingsWithData,
+ );
+
+ yield put({
+ type: ReduxActionTypes.SAVE_JS_EXECUTION_RECORD,
+ payload: {
+ [jsAction.jsFunctionId]: result,
+ },
+ });
+}
+
export function* executeAPIQueryActionSaga(apiAction: ActionPayload) {
try {
const api: PageAction = yield select(getAction, apiAction.actionId);
@@ -193,6 +217,11 @@ export function* executeActionSaga(actionPayloads: ActionPayload[]): any {
return call(executeAPIQueryActionSaga, actionPayload);
case "QUERY":
return call(executeAPIQueryActionSaga, actionPayload);
+ case "JS_FUNCTION":
+ return call(
+ executeJSActionSaga,
+ actionPayload as ExecuteJSActionPayload,
+ );
default:
return undefined;
}
diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx
index 69fae442c6..32d2fc14e9 100644
--- a/app/client/src/selectors/editorSelectors.tsx
+++ b/app/client/src/selectors/editorSelectors.tsx
@@ -7,7 +7,10 @@ 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 {
+ enhanceWithDynamicValuesAndValidations,
+ NameBindingsWithData,
+} from "utils/DynamicBindingUtils";
import { getDataTree } from "./entitiesSelector";
import {
FlattenedWidgetProps,
@@ -17,6 +20,7 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer";
import { OccupiedSpace } from "constants/editorConstants";
import { WidgetTypes } from "constants/WidgetConstants";
+import { getNameBindingsWithData } from "./nameBindingsWithDataSelector";
const getEditorState = (state: AppState) => state.ui.editor;
const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig;
@@ -112,12 +116,13 @@ export const getWidgetCards = createSelector(
export const getValidatedDynamicProps = createSelector(
getDataTree,
- (entities: DataTree) => {
+ getNameBindingsWithData,
+ (entities: DataTree, nameBindingsWithData: NameBindingsWithData) => {
const widgets = { ...entities.canvasWidgets };
Object.keys(widgets).forEach(widgetKey => {
widgets[widgetKey] = enhanceWithDynamicValuesAndValidations(
widgets[widgetKey],
- entities,
+ nameBindingsWithData,
true,
);
});
diff --git a/app/client/src/selectors/nameBindingsWithDataSelector.ts b/app/client/src/selectors/nameBindingsWithDataSelector.ts
new file mode 100644
index 0000000000..55ae87f2d7
--- /dev/null
+++ b/app/client/src/selectors/nameBindingsWithDataSelector.ts
@@ -0,0 +1,24 @@
+import { DataTree } from "reducers";
+import { NameBindingsWithData } from "utils/DynamicBindingUtils";
+import { JSONPath } from "jsonpath-plus";
+import { createSelector } from "reselect";
+import { getDataTree } from "./entitiesSelector";
+
+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({
+ path: nameBindings,
+ json: dataTree,
+ })[0];
+ if (evaluatedValue && key !== "undefined") {
+ nameBindingsWithData[key] = evaluatedValue;
+ }
+ });
+
+ return nameBindingsWithData;
+ },
+);
diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx
index 300d4f1a89..9dc1489779 100644
--- a/app/client/src/selectors/propertyPaneSelectors.tsx
+++ b/app/client/src/selectors/propertyPaneSelectors.tsx
@@ -1,12 +1,15 @@
import { createSelector } from "reselect";
-import { AppState, DataTree } from "reducers";
+import { AppState } from "reducers";
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 { getDataTree } from "./entitiesSelector";
-import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils";
+import {
+ enhanceWithDynamicValuesAndValidations,
+ NameBindingsWithData,
+} from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget";
+import { getNameBindingsWithData } from "./nameBindingsWithDataSelector";
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
state.ui.propertyPane;
@@ -35,10 +38,17 @@ export const getCurrentWidgetProperties = createSelector(
export const getWidgetPropsWithValidations = createSelector(
getCurrentWidgetProperties,
- getDataTree,
- (widget: WidgetProps | undefined, dataTree: DataTree) => {
+ getNameBindingsWithData,
+ (
+ widget: WidgetProps | undefined,
+ nameBindigsWithData: NameBindingsWithData,
+ ) => {
if (!widget) return undefined;
- return enhanceWithDynamicValuesAndValidations(widget, dataTree, false);
+ return enhanceWithDynamicValuesAndValidations(
+ widget,
+ nameBindigsWithData,
+ false,
+ );
},
);
diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts
index a51797e43d..a1022f6eae 100644
--- a/app/client/src/utils/DynamicBindingUtils.ts
+++ b/app/client/src/utils/DynamicBindingUtils.ts
@@ -1,22 +1,67 @@
import _ from "lodash";
-import { DataTree } from "reducers";
-import { JSONPath } from "jsonpath-plus";
import { WidgetProps } from "widgets/BaseWidget";
-import { DATA_BIND_REGEX, DATA_PATH_REGEX } from "constants/BindingsConstants";
+import { DATA_BIND_JS_REGEX } from "constants/BindingsConstants";
import ValidationFactory from "./ValidationFactory";
+import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
+export type NameBindingsWithData = Record;
export const isDynamicValue = (value: string): boolean =>
- DATA_BIND_REGEX.test(value);
+ DATA_BIND_JS_REGEX.test(value);
+
+//{{}}{{}}}
+function parseDynamicString(dynamicString: string): string[] {
+ 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;
+}
export const getDynamicBindings = (
dynamicString: string,
): { bindings: string[]; paths: string[] } => {
// Get the {{binding}} bound values
- const bindings = dynamicString.match(DATA_BIND_REGEX) || [];
+ const bindings = parseDynamicString(dynamicString);
// Get the "binding" path values
- const paths = bindings.map(p => {
- const matches = p.match(DATA_PATH_REGEX);
- if (matches) return matches[0];
+ const paths = bindings.map(binding => {
+ const length = binding.length;
+ const matches = binding.match(DATA_BIND_JS_REGEX);
+ if (matches) {
+ return binding.substring(2, length - 2);
+ }
return "";
});
return { bindings, paths };
@@ -24,17 +69,10 @@ export const getDynamicBindings = (
// Paths are expected to have "{name}.{path}" signature
export const extractDynamicBoundValue = (
- dataTree: DataTree,
+ data: NameBindingsWithData,
path: string,
): any => {
- // Remove the name in the binding
- const splitPath = path.split(".");
- // Find the dataTree path of the name
- const bindingPath = dataTree.nameBindings[splitPath[0]];
- // Create the full path
- const fullPath = `${bindingPath}.${splitPath.slice(1).join(".")}`;
- // Search with JSONPath
- return JSONPath({ path: fullPath, json: dataTree })[0];
+ return JSExecutionManagerSingleton.evaluateSync(path, data);
};
// For creating a final value where bindings could be in a template format
@@ -57,13 +95,16 @@ export const createDynamicValueString = (
export const getDynamicValue = (
dynamicBinding: string,
- dataTree: DataTree,
+ data: NameBindingsWithData,
): any => {
// Get the {{binding}} bound values
const { bindings, paths } = getDynamicBindings(dynamicBinding);
if (bindings.length) {
// Get the Data Tree value of those "binding "paths
- const values = paths.map(p => extractDynamicBoundValue(dataTree, p));
+ const values = paths.map((p, i) => {
+ return p ? extractDynamicBoundValue(data, p) : bindings[i];
+ });
+
// 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
@@ -74,17 +115,19 @@ export const getDynamicValue = (
export const enhanceWithDynamicValuesAndValidations = (
widget: WidgetProps,
- entities: DataTree,
+ nameBindingsWithData: NameBindingsWithData,
replaceWithParsed: boolean,
): WidgetProps => {
if (!widget) return widget;
const properties = { ...widget };
const invalidProps: Record = {};
+ const t0 = performance.now();
+
Object.keys(widget).forEach((property: string) => {
let value = widget[property];
// Check for dynamic bindings
if (widget.dynamicBindings && property in widget.dynamicBindings) {
- value = getDynamicValue(value, entities);
+ value = getDynamicValue(value, nameBindingsWithData);
}
// Pass it through validation and parse
const { isValid, parsed } = ValidationFactory.validateWidgetProperty(
@@ -97,5 +140,14 @@ export const enhanceWithDynamicValuesAndValidations = (
// Replace if flag is turned on
if (replaceWithParsed) properties[property] = parsed;
});
+ const t1 = performance.now();
+ console.log(
+ "Evaluations for " +
+ widget.widgetName +
+ " took " +
+ (t1 - t0) +
+ " milliseconds.",
+ );
+ console.trace();
return { ...properties, invalidProps };
};
diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx
index f75fbe2816..5a4b0e52ab 100644
--- a/app/client/src/widgets/TableWidget.tsx
+++ b/app/client/src/widgets/TableWidget.tsx
@@ -2,7 +2,7 @@ import React from "react";
import _ from "lodash";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
-import { ActionPayload } from "constants/ActionConstants";
+import { ActionPayload, BaseActionPayload } from "constants/ActionConstants";
import { AutoResizer } from "react-base-table";
import "react-base-table/styles.css";
import { forIn } from "lodash";
@@ -94,7 +94,7 @@ class TableWidget extends BaseWidget {
export type PaginationType = "PAGES" | "INFINITE_SCROLL";
-export interface TableAction extends ActionPayload {
+export interface TableAction extends BaseActionPayload {
actionName: string;
}
diff --git a/app/client/typings/Realm/index.d.ts b/app/client/typings/Realm/index.d.ts
new file mode 100644
index 0000000000..ea78bb32e9
--- /dev/null
+++ b/app/client/typings/Realm/index.d.ts
@@ -0,0 +1,2 @@
+// import * as React from "react";
+declare module "Realm";