diff --git a/app/client/cypress/fixtures/executionParamsDsl.json b/app/client/cypress/fixtures/executionParamsDsl.json new file mode 100644 index 0000000000..32de9477c0 --- /dev/null +++ b/app/client/cypress/fixtures/executionParamsDsl.json @@ -0,0 +1,139 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1254, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "dynamicBindingPathList": [], + "version": 4, + "minHeight": 1292, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "backgroundColor": "#FFFFFF", + "widgetName": "Container3", + "type": "CONTAINER_WIDGET", + "containerStyle": "card", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 75.25, + "parentRowSpace": 38, + "dynamicBindingPathList": [], + "leftColumn": 0, + "rightColumn": 16, + "topRow": 0, + "bottomRow": 23, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [ + { + "backgroundColor": "transparent", + "widgetName": "8muuok24ny", + "type": "CANVAS_WIDGET", + "containerStyle": "none", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 1204, + "topRow": 0, + "bottomRow": 532, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [ + { + "isVisible": true, + "label": "Data", + "widgetName": "Table1", + "tableData": "", + "type": "TABLE_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.75, + "parentRowSpace": 38, + "leftColumn": 2, + "rightColumn": 10, + "topRow": 3, + "bottomRow": 10, + "parentId": "tyiwk4xuq0", + "widgetId": "5up3r2iuvs", + "dynamicBindingPathList": [] + }, + { + "widgetName": "StaticButton", + "rightColumn": 14, + "onClick": "", + "isDefaultClickDisabled": true, + "widgetId": "3p92qmlzfl", + "buttonStyle": "PRIMARY_BUTTON", + "topRow": 3, + "bottomRow": 4, + "parentRowSpace": 38, + "isVisible": true, + "type": "BUTTON_WIDGET", + "dynamicBindingPathList": [], + "parentId": "8muuok24ny", + "isLoading": false, + "parentColumnSpace": 71.75, + "leftColumn": 11, + "text": "Run Static", + "isDisabled": false + }, + { + "widgetName": "DynamicButton", + "rightColumn": 14, + "onClick": "", + "isDefaultClickDisabled": true, + "widgetId": "asdasdlnud", + "buttonStyle": "PRIMARY_BUTTON", + "topRow": 4, + "bottomRow": 5, + "parentRowSpace": 38, + "isVisible": true, + "type": "BUTTON_WIDGET", + "dynamicBindingPathList": [], + "parentId": "8muuok24ny", + "isLoading": false, + "parentColumnSpace": 71.75, + "leftColumn": 11, + "text": "Run Dynamic", + "isDisabled": false + }, + { + "isVisible": true, + "inputType": "TEXT", + "label": "Endpoint", + "widgetName": "EndpointInput", + "defaultText": "todos", + "type": "INPUT_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.75, + "parentRowSpace": 38, + "leftColumn": 9, + "rightColumn": 14, + "topRow": 1, + "bottomRow": 2, + "parentId": "0", + "widgetId": "ufr2ik3x1q" + } + ], + "widgetId": "tyiwk4xuq0", + "detachFromLayout": true, + "canExtend": false + } + ], + "widgetId": "3oe1ka7jon" + } + ] + } +} diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/Action_PageOnLoad_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/Action_PageOnLoad_spec.js similarity index 100% rename from app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/Action_PageOnLoad_spec.js rename to app/client/cypress/integration/Smoke_TestSuite/ActionExecution/Action_PageOnLoad_spec.js diff --git a/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/ExecutionParams_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/ExecutionParams_spec.js new file mode 100644 index 0000000000..091cece48b --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ActionExecution/ExecutionParams_spec.js @@ -0,0 +1,89 @@ +const dsl = require("../../../fixtures/executionParamsDsl.json"); +const publishPage = require("../../../locators/publishWidgetspage.json"); +const commonlocators = require("../../../locators/commonlocators.json"); + +describe("API Panel Test Functionality", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Will pass execution params", function() { + // Create the Api + cy.NavigateToAPI_Panel(); + cy.CreateAPI("MultiApi"); + cy.enterDatasourceAndPath( + "https://jsonplaceholder.typicode.com/", + "{{this.params.endpoint || 'posts'}}", + ); + cy.WaitAutoSave(); + // Run it + cy.RunAPI(); + + // Bind the table + cy.SearchEntityandOpen("Table1"); + cy.testJsontext("tabledata", "{{MultiApi.data"); + // Assert 'posts' data (default) + cy.readTabledataPublish("0", "2").then(cellData => { + expect(cellData).to.be.equal( + "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + ); + }); + + // Choose static button + cy.SearchEntityandOpen("StaticButton"); + // toggle js of onClick + cy.get(".t--property-control-onclick") + .find(".t--js-toggle") + .click({ force: true }); + // Bind with MultiApi with static value + cy.testJsontext( + "onclick", + "{{MultiApi.run(undefined, undefined, { endpoint: 'users", + ); + cy.get(commonlocators.editPropCrossButton).click(); + + // Choose dynamic button + cy.SearchEntityandOpen("DynamicButton"); + // toggle js of onClick + cy.get(".t--property-control-onclick") + .find(".t--js-toggle") + .click({ force: true }); + // Bind with MultiApi with dynamicValue value + cy.testJsontext( + "onclick", + "{{MultiApi.run(undefined, undefined, { endpoint: EndpointInput.text", + ); + + // Publish the app + cy.PublishtheApp(); + cy.wait("@postExecute"); + + // Assert on load data in table + cy.readTabledataPublish("0", "2").then(cellData => { + expect(cellData).to.be.equal( + "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + ); + }); + + // Click Static button + cy.get(publishPage.buttonWidget) + .first() + .click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + // Assert statically bound "users" data + cy.readTabledataPublish("1", "1").then(cellData => { + expect(cellData).to.be.equal("Ervin Howell"); + }); + + // Click dynamic button + cy.get(publishPage.buttonWidget) + .eq(1) + .click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + // Assert dynamically bound "todos" data + cy.readTabledataPublish("0", "2").then(cellData => { + expect(cellData).to.be.equal("delectus aut autem"); + }); + }); +}); diff --git a/app/client/src/constants/ActionConstants.tsx b/app/client/src/constants/ActionConstants.tsx index a421314b31..f34df4e257 100644 --- a/app/client/src/constants/ActionConstants.tsx +++ b/app/client/src/constants/ActionConstants.tsx @@ -72,3 +72,4 @@ export interface ExecuteErrorPayload { // Group 2 = path (/nested/path) // Group 3 = params (?param=123¶m2=12) export const urlGroupsRegexExp = /^(https?:\/{2}\S+?)(\/\S*?)(\?\S*)?$/; +export const EXECUTION_PARAM_KEY = "executionParams"; diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index f1f8115b04..29ea489338 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -1,5 +1,6 @@ import { WidgetProps } from "widgets/BaseWidget"; import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { EXECUTION_PARAM_KEY } from "./ActionConstants"; // Always add a validator function in ./Validators for these types export const VALIDATION_TYPES = { @@ -39,7 +40,7 @@ export type Validator = ( export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.SSSZ"; -export const JAVSCRIPT_KEYWORDS = { +export const JAVASCRIPT_KEYWORDS = { true: "true", await: "await", break: "break", @@ -87,3 +88,10 @@ export const JAVSCRIPT_KEYWORDS = { with: "with", yield: "yield", }; + +export const DATA_TREE_KEYWORDS = { + actionPaths: "actionPaths", + appsmith: "appsmith", + pageList: "pageList", + [EXECUTION_PARAM_KEY]: EXECUTION_PARAM_KEY, +}; diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 8423b1d3dc..8aa9728775 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -9,6 +9,7 @@ import { EventType, ExecuteActionPayload, ExecuteActionPayloadEvent, + EXECUTION_PARAM_KEY, PageAction, } from "constants/ActionConstants"; import * as log from "loglevel"; @@ -63,7 +64,7 @@ import { import { AppState } from "reducers"; import { mapToPropList } from "utils/AppsmithUtils"; import { validateResponse } from "sagas/ErrorSagas"; -import { ToastType, TypeOptions } from "react-toastify"; +import { TypeOptions } from "react-toastify"; import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants"; import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "constants/ApiConstants"; import { updateAppStore } from "actions/pageActions"; @@ -71,7 +72,7 @@ import { getAppStoreName } from "constants/AppConstants"; import downloadjs from "downloadjs"; import { getType, Types } from "utils/TypeHelpers"; import { Toaster } from "components/ads/Toast"; -import { Variant, ToastVariant } from "components/ads/common"; +import { Variant } from "components/ads/common"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; @@ -239,62 +240,35 @@ const isErrorResponse = (response: ActionApiResponse) => { return !response.data.isExecutionSuccess; }; -export function* evaluateDynamicBoundValueSaga(path: string): any { - return yield call(evaluateSingleValue, `{{${path}}}`); +export function* evaluateDynamicBoundValueSaga( + valueToEvaluate: string, + params?: Record, +): any { + return yield call(evaluateSingleValue, `{{${valueToEvaluate}}}`, params); } -const EXECUTION_PARAM_PATH = "this.params"; -const getExecutionParamPath = (key: string) => `${EXECUTION_PARAM_PATH}.${key}`; +const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g; export function* getActionParams( bindings: string[] | undefined, executionParams?: Record, ) { if (_.isNil(bindings)) return []; - let dataTreeBindings = bindings; + const evaluatedExecutionParams = yield evaluateDynamicBoundValueSaga( + JSON.stringify(executionParams), + ); - if (executionParams && Object.keys(executionParams).length) { - // List of params in the path format - const executionParamsPathList = Object.keys(executionParams).map( - getExecutionParamPath, - ); - const paramSearchRegex = new RegExp(executionParamsPathList.join("|"), "g"); - // Bindings with references to execution params - const executionBindings = bindings.filter(binding => - paramSearchRegex.test(binding), - ); + const bindingsForExecutionParams = bindings.map(binding => + binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY), + ); - // Replace references with values - const replacedBindings = executionBindings.map(binding => { - let replaced = binding; - const matches = binding.match(paramSearchRegex); - if (matches && matches.length) { - matches.forEach(match => { - // we add one for substring index to account for '.' - const paramKey = match.substring(EXECUTION_PARAM_PATH.length + 1); - let paramValue = executionParams[paramKey]; - if (paramValue) { - if (typeof paramValue === "object") { - paramValue = JSON.stringify(paramValue); - } - replaced = replaced.replace(match, paramValue); - } - }); - } - return replaced; - }); - // Replace binding with replaced bindings for evaluation - dataTreeBindings = dataTreeBindings.map(key => { - if (executionBindings.includes(key)) { - return replacedBindings[executionBindings.indexOf(key)]; - } - return key; - }); - } - // Evaluate all values const values: any = yield all( - dataTreeBindings.map((binding: string) => { - return call(evaluateDynamicBoundValueSaga, binding); + bindingsForExecutionParams.map((binding: string) => { + return call( + evaluateDynamicBoundValueSaga, + binding, + evaluatedExecutionParams, + ); }), ); // convert to object and transform non string values diff --git a/app/client/src/sagas/evaluationsSaga.ts b/app/client/src/sagas/evaluationsSaga.ts index 52d3cf0186..c7f7b3cbef 100644 --- a/app/client/src/sagas/evaluationsSaga.ts +++ b/app/client/src/sagas/evaluationsSaga.ts @@ -35,6 +35,7 @@ import PerformanceTracker, { import { Variant } from "components/ads/common"; import { Toaster } from "components/ads/Toast"; import * as Sentry from "@sentry/react"; +import { EXECUTION_PARAM_KEY } from "../constants/ActionConstants"; let evaluationWorker: Worker; let workerChannel: EventChannel; @@ -110,9 +111,13 @@ function* evaluateTreeSaga(postEvalActions?: ReduxAction[]) { } } -export function* evaluateSingleValue(binding: string) { +export function* evaluateSingleValue( + binding: string, + executionParams: Record = {}, +) { if (evaluationWorker) { const dataTree = yield select(getDataTree); + dataTree[EXECUTION_PARAM_KEY] = executionParams; evaluationWorker.postMessage({ action: EVAL_WORKER_ACTIONS.EVAL_SINGLE, dataTree, diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 41972dccef..30db336796 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -7,8 +7,6 @@ import { Action } from "entities/Action"; import moment from "moment-timezone"; import { WidgetProps } from "../widgets/BaseWidget"; -type StringTuple = [string, string]; - export const removeBindingsFromActionObject = (obj: Action) => { const string = JSON.stringify(obj); const withBindings = string.replace(DATA_BIND_REGEX_GLOBAL, "{{ }}"); diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index 72df7da24b..6fd8462963 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -1,5 +1,9 @@ import { GridDefaults } from "constants/WidgetConstants"; -import { JAVSCRIPT_KEYWORDS } from "constants/WidgetValidation"; +import { + DATA_TREE_KEYWORDS, + JAVASCRIPT_KEYWORDS, +} from "constants/WidgetValidation"; +import { GLOBAL_FUNCTIONS } from "./autocomplete/EntityDefinitions"; export const snapToGrid = ( columnWidth: number, rowHeight: number, @@ -165,7 +169,7 @@ export const convertArrayToSentence = (arr: string[]) => { }; /** - * checks if the name is conflciting with + * checks if the name is conflicting with * 1. API names, * 2. Queries name * 3. Javascript reserved names @@ -180,9 +184,10 @@ export const isNameValid = ( name: string, invalidNames: Record, ) => { - if (name in JAVSCRIPT_KEYWORDS || name in invalidNames) { - return false; - } - - return true; + return !( + name in JAVASCRIPT_KEYWORDS || + name in DATA_TREE_KEYWORDS || + name in GLOBAL_FUNCTIONS || + name in invalidNames + ); };