diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index 216aae3c56..673f0b1282 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -3,6 +3,8 @@ import { ReduxActionTypes, ReduxAction, ReduxActionErrorTypes, + EvaluationReduxAction, + ReduxActionWithoutPayload, } from "constants/ReduxActionConstants"; import { Action } from "entities/Action"; import { batchAction } from "actions/batchActions"; @@ -27,10 +29,12 @@ export type FetchActionsPayload = { export const fetchActions = ( applicationId: string, -): ReduxAction => { + postEvalActions: Array | ReduxActionWithoutPayload>, +): EvaluationReduxAction => { return { type: ReduxActionTypes.FETCH_ACTIONS_INIT, payload: { applicationId }, + postEvalActions, }; }; diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index da38f73e31..e164e1ba9f 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -3,6 +3,7 @@ import { EvaluationReduxAction, ReduxAction, ReduxActionTypes, + ReduxActionWithoutPayload, UpdateCanvasPayload, } from "constants/ReduxActionConstants"; import AnalyticsUtil from "utils/AnalyticsUtil"; @@ -46,18 +47,14 @@ export const fetchPublishedPage = (pageId: string, bustCache = false) => ({ }, }); -export const fetchPageSuccess = ( - postEvalActions: ReduxAction[], -): EvaluationReduxAction => { +export const fetchPageSuccess = (): ReduxActionWithoutPayload => { return { type: ReduxActionTypes.FETCH_PAGE_SUCCESS, - payload: {}, - postEvalActions, }; }; export const fetchPublishedPageSuccess = ( - postEvalActions: ReduxAction[], + postEvalActions: Array | ReduxActionWithoutPayload>, ): EvaluationReduxAction => ({ type: ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS, postEvalActions, diff --git a/app/client/src/actions/pluginActions.ts b/app/client/src/actions/pluginActions.ts index 6b0830bd5a..573853e2bc 100644 --- a/app/client/src/actions/pluginActions.ts +++ b/app/client/src/actions/pluginActions.ts @@ -4,6 +4,7 @@ import { ReduxActionWithoutPayload, } from "constants/ReduxActionConstants"; import { PluginFormPayload } from "api/PluginApi"; +import { DependencyMap } from "utils/DynamicBindingUtils"; export const fetchPlugins = (): ReduxActionWithoutPayload => ({ type: ReduxActionTypes.FETCH_PLUGINS_REQUEST, @@ -17,6 +18,7 @@ export type PluginFormsPayload = { formConfigs: Record; editorConfigs: Record; settingConfigs: Record; + dependencies: Record; }; export const fetchPluginFormConfigsSuccess = ( diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index 814f7a6987..03e1f59548 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -2,11 +2,11 @@ import { ReduxActionTypes, ReduxAction, ReduxActionErrorTypes, + ReduxActionWithoutPayload, } from "constants/ReduxActionConstants"; import { ExecuteActionPayload, ExecuteErrorPayload, - PageAction, } from "constants/AppsmithActionConstants/ActionConstants"; import { BatchAction, batchAction } from "actions/batchActions"; import PerformanceTracker, { @@ -30,11 +30,8 @@ export const executeActionError = ( }; }; -export const executePageLoadActions = ( - payload: PageAction[][], -): ReduxAction => ({ +export const executePageLoadActions = (): ReduxActionWithoutPayload => ({ type: ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS, - payload, }); export const disableDragAction = ( diff --git a/app/client/src/api/PluginApi.ts b/app/client/src/api/PluginApi.ts index 2a14da0364..938b7bff26 100644 --- a/app/client/src/api/PluginApi.ts +++ b/app/client/src/api/PluginApi.ts @@ -2,6 +2,7 @@ import Api from "api/Api"; import { AxiosPromise } from "axios"; import { GenericApiResponse } from "api/ApiResponses"; import { PluginType } from "entities/Action"; +import { DependencyMap } from "utils/DynamicBindingUtils"; export interface Plugin { id: string; @@ -21,6 +22,7 @@ export interface PluginFormPayload { form: any[]; editor: any[]; setting: any[]; + dependencies: DependencyMap; } class PluginsApi extends Api { diff --git a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx index b505fff205..42d98b73a0 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx @@ -8,6 +8,9 @@ import { theme } from "constants/DefaultTheme"; import { Placement } from "popper.js"; import ScrollIndicator from "components/ads/ScrollIndicator"; import DebugButton from "components/editorComponents/Debugger/DebugCTA"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; +import Tooltip from "components/ads/Tooltip"; +import { Classes } from "@blueprintjs/core"; const Wrapper = styled.div` position: relative; @@ -112,6 +115,7 @@ interface Props { error?: string; useValidationMessage?: boolean; hideEvaluatedValue?: boolean; + evaluationSubstitutionType?: EvaluationSubstitutionType; } interface PopoverContentProps { @@ -124,12 +128,56 @@ interface PopoverContentProps { onMouseEnter: () => void; onMouseLeave: () => void; hideEvaluatedValue?: boolean; + preparedStatementViewer: boolean; } +const PreparedStatementViewerContainer = styled.span` + .${Classes.POPOVER_TARGET} { + display: inline-block; + } +`; + +const PreparedStatementParameter = styled.span` + cursor: pointer; + text-decoration: underline; + color: #333; +`; + +type PreparedStatementValue = { + value: string; + parameters: Record; +}; +export const PreparedStatementViewer = (props: { + evaluatedValue: PreparedStatementValue; +}) => { + const { value, parameters } = props.evaluatedValue; + const stringSegments = value.split(/\$\d/); + const $params = [...value.matchAll(/\$\d/g)].map((matches) => matches[0]); + const paramsWithTooltips = $params.map((param) => ( + {parameters[param]}} key={param}> + + {param} + + + )); + + return ( + + {stringSegments.map((segment, index) => ( + + {segment} + {paramsWithTooltips[index]} + + ))} + + ); +}; + export const CurrentValueViewer = (props: { theme: EditorTheme; evaluatedValue: any; hideLabel?: boolean; + preparedStatementViewer?: boolean; }) => { const currentValueWrapperRef = React.createRef(); const codeWrapperRef = React.createRef(); @@ -145,19 +193,31 @@ export const CurrentValueViewer = (props: { _.isObject(props.evaluatedValue) || Array.isArray(props.evaluatedValue) ) { - const reactJsonProps = { - theme: props.theme === EditorTheme.DARK ? "summerfruit" : "rjv-default", - name: null, - enableClipboard: false, - displayObjectSize: false, - displayDataTypes: false, - style: { - fontSize: "12px", - }, - collapsed: 2, - collapseStringsAfterLength: 20, - }; - content = ; + if (props.preparedStatementViewer) { + content = ( + + + + + ); + } else { + const reactJsonProps = { + theme: + props.theme === EditorTheme.DARK ? "summerfruit" : "rjv-default", + name: null, + enableClipboard: false, + displayObjectSize: false, + displayDataTypes: false, + style: { + fontSize: "12px", + }, + collapsed: 2, + collapseStringsAfterLength: 20, + }; + content = ; + } } else { content = ( @@ -222,6 +282,7 @@ const PopoverContent = (props: PopoverContentProps) => { )} @@ -246,7 +307,7 @@ const EvaluatedValuePopup = (props: Props) => { { hasError={props.hasError} theme={props.theme} hideEvaluatedValue={props.hideEvaluatedValue} + preparedStatementViewer={ + props.evaluationSubstitutionType + ? props.evaluationSubstitutionType === + EvaluationSubstitutionType.PARAMETER + : false + } onMouseLeave={() => { setContentHovered(false); }} diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index 56a6e2ce14..594896c10d 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -16,7 +16,10 @@ import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors"; import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup"; import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; import _ from "lodash"; -import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { + DataTree, + EvaluationSubstitutionType, +} from "entities/DataTree/dataTreeFactory"; import { Skin } from "constants/DefaultTheme"; import AnalyticsUtil from "utils/AnalyticsUtil"; import "components/editorComponents/CodeEditor/modes"; @@ -84,6 +87,7 @@ export type EditorStyleProps = { hoverInteraction?: boolean; fill?: boolean; useValidationMessage?: boolean; + evaluationSubstitutionType?: EvaluationSubstitutionType; }; export type EditorProps = EditorStyleProps & @@ -352,6 +356,7 @@ class CodeEditor extends Component { fill, useValidationMessage, hideEvaluatedValue, + evaluationSubstitutionType, } = this.props; const hasError = !!(meta && meta.error); let evaluated = evaluatedValue; @@ -398,6 +403,7 @@ class CodeEditor extends Component { error={meta?.error} useValidationMessage={useValidationMessage} hideEvaluatedValue={hideEvaluatedValue} + evaluationSubstitutionType={evaluationSubstitutionType} >
{props.children}
; @@ -33,6 +34,7 @@ describe("DynamicTextFieldControl", () => { id={"test"} isValid={true} pluginId="123" + evaluationSubstitutionType={EvaluationSubstitutionType.TEMPLATE} /> , { diff --git a/app/client/src/components/formControls/DynamicTextFieldControl.tsx b/app/client/src/components/formControls/DynamicTextFieldControl.tsx index bc764a9efe..0cf2b06ff6 100644 --- a/app/client/src/components/formControls/DynamicTextFieldControl.tsx +++ b/app/client/src/components/formControls/DynamicTextFieldControl.tsx @@ -22,6 +22,7 @@ import { getQueryParams, } from "utils/AppsmithUtils"; import { actionPathFromName } from "components/formControls/utils"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; const Wrapper = styled.div` .dynamic-text-field { @@ -64,6 +65,7 @@ class DynamicTextControl extends BaseControl< placeholderText, actionName, configProperty, + evaluationSubstitutionType, } = this.props; const dataTreePath = actionPathFromName(actionName, configProperty); const isNewQuery = @@ -104,6 +106,7 @@ class DynamicTextControl extends BaseControl< mode={mode} tabBehaviour={TabBehaviour.INDENT} placeholder={placeholderText} + evaluationSubstitutionType={evaluationSubstitutionType} /> )} @@ -117,6 +120,7 @@ export interface DynamicTextFieldProps extends ControlProps { pluginId: string; responseType: string; placeholderText?: string; + evaluationSubstitutionType: EvaluationSubstitutionType; } const mapStateToProps = (state: AppState, props: DynamicTextFieldProps) => { diff --git a/app/client/src/constants/ApiEditorConstants.ts b/app/client/src/constants/ApiEditorConstants.ts index 85efc7b576..c142a5ca63 100644 --- a/app/client/src/constants/ApiEditorConstants.ts +++ b/app/client/src/constants/ApiEditorConstants.ts @@ -33,6 +33,13 @@ export const DEFAULT_API_ACTION_CONFIG: ApiActionConfig = { httpMethod: HTTP_METHODS[0], headers: EMPTY_KEY_VALUE_PAIRS.slice(), queryParameters: EMPTY_KEY_VALUE_PAIRS.slice(), + body: "", + pluginSpecifiedTemplates: [ + { + // JSON smart substitution + value: false, + }, + ], }; export const DEFAULT_PROVIDER_OPTION = "Business Software"; diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx index 0bcc540960..a4534ff617 100644 --- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx +++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx @@ -5,6 +5,7 @@ import queryActionSettingsConfig from "constants/AppsmithActionConstants/formCon import apiActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/ApiSettingsConfig"; import apiActionEditorConfig from "constants/AppsmithActionConstants/formConfig/ApiEditorConfigs"; import saasActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/GoogleSheetsSettingsConfig"; +import apiActionDependencyConfig from "constants/AppsmithActionConstants/formConfig/ApiDependencyConfigs"; export type ExecuteActionPayloadEvent = { type: EventType; @@ -123,3 +124,12 @@ export const defaultActionEditorConfigs: Record = { [PluginType.DB]: [], [PluginType.SAAS]: [], }; + +export const defaultActionDependenciesConfig: Record< + PluginType, + Record +> = { + [PluginType.API]: apiActionDependencyConfig, + [PluginType.DB]: {}, + [PluginType.SAAS]: {}, +}; diff --git a/app/client/src/constants/AppsmithActionConstants/formConfig/ApiDependencyConfigs.ts b/app/client/src/constants/AppsmithActionConstants/formConfig/ApiDependencyConfigs.ts index a80725410f..85ee182931 100644 --- a/app/client/src/constants/AppsmithActionConstants/formConfig/ApiDependencyConfigs.ts +++ b/app/client/src/constants/AppsmithActionConstants/formConfig/ApiDependencyConfigs.ts @@ -1,9 +1,5 @@ -export default [ - { - dependencies: { - "actionConfiguration.body": [ - "actionConfiguration.pluginSpecifiedTemplates[0].value", - ], - }, - }, -]; +export default { + "actionConfiguration.body": [ + "actionConfiguration.pluginSpecifiedTemplates[0].value", + ], +}; diff --git a/app/client/src/constants/AppsmithActionConstants/formConfig/ApiEditorConfigs.ts b/app/client/src/constants/AppsmithActionConstants/formConfig/ApiEditorConfigs.ts index 0692c6b9e2..4f16a95228 100644 --- a/app/client/src/constants/AppsmithActionConstants/formConfig/ApiEditorConfigs.ts +++ b/app/client/src/constants/AppsmithActionConstants/formConfig/ApiEditorConfigs.ts @@ -12,6 +12,23 @@ export default [ label: "Body", configProperty: "actionConfiguration.body", controlType: "QUERY_DYNAMIC_INPUT_TEXT", + evaluationSubstitutionType: "SMART_SUBSTITUTE", + hidden: { + path: "actionConfiguration.pluginSpecifiedTemplates[0].value", + comparison: "EQUALS", + value: false, + }, + }, + { + label: "Body", + configProperty: "actionConfiguration.body", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + evaluationSubstitutionType: "TEMPLATE", + hidden: { + path: "actionConfiguration.pluginSpecifiedTemplates[0].value", + comparison: "EQUALS", + value: true, + }, }, { label: "Query Parameters", diff --git a/app/client/src/constants/BindingsConstants.ts b/app/client/src/constants/BindingsConstants.ts index 99724a6345..325bb59e7e 100644 --- a/app/client/src/constants/BindingsConstants.ts +++ b/app/client/src/constants/BindingsConstants.ts @@ -1,3 +1,4 @@ export const DATA_BIND_REGEX = /{{([\s\S]*?)}}/; export const DATA_BIND_REGEX_GLOBAL = /{{([\s\S]*?)}}/g; export const AUTOCOMPLETE_MATCH_REGEX = /{{\s*.*?\s*}}/g; +export const QUOTED_BINDING_REGEX = /["']({{[\s\S]*?}})["']/g; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index fea86bfb34..da7cbca874 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -497,7 +497,7 @@ export interface ReduxActionWithCallbacks extends ReduxAction { } export interface EvaluationReduxAction extends ReduxAction { - postEvalActions?: ReduxAction[]; + postEvalActions?: Array | ReduxActionWithoutPayload>; } export interface PromisePayload { diff --git a/app/client/src/entities/Action/actionProperties.test.ts b/app/client/src/entities/Action/actionProperties.test.ts index 69aea75368..9b058deab4 100644 --- a/app/client/src/entities/Action/actionProperties.test.ts +++ b/app/client/src/entities/Action/actionProperties.test.ts @@ -1,5 +1,6 @@ import { Action, PluginType } from "entities/Action/index"; import { getBindingPathsOfAction } from "entities/Action/actionProperties"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; const DEFAULT_ACTION: Action = { actionConfiguration: {}, @@ -24,9 +25,9 @@ describe("getBindingPathsOfAction", () => { it("returns default list of no config is sent", () => { const response = getBindingPathsOfAction(DEFAULT_ACTION, undefined); expect(response).toStrictEqual({ - data: true, - isLoading: true, - config: true, + data: EvaluationSubstitutionType.TEMPLATE, + isLoading: EvaluationSubstitutionType.TEMPLATE, + config: EvaluationSubstitutionType.TEMPLATE, }); }); @@ -46,6 +47,18 @@ describe("getBindingPathsOfAction", () => { configProperty: "actionConfiguration.body2", controlType: "QUERY_DYNAMIC_INPUT_TEXT", }, + { + label: "", + configProperty: "actionConfiguration.field1", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + evaluationSubstitutionType: "SMART_SUBSTITUTE", + }, + { + label: "", + configProperty: "actionConfiguration.field2", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + evaluationSubstitutionType: "PARAMETER", + }, ], }, ]; @@ -54,15 +67,19 @@ describe("getBindingPathsOfAction", () => { actionConfiguration: { body: "basic action", body2: "another body", + field1: "test", + field2: "anotherTest", }, }; const response = getBindingPathsOfAction(basicAction, config); expect(response).toStrictEqual({ - data: true, - isLoading: true, - "config.body": true, - "config.body2": true, + data: EvaluationSubstitutionType.TEMPLATE, + isLoading: EvaluationSubstitutionType.TEMPLATE, + "config.body": EvaluationSubstitutionType.TEMPLATE, + "config.body2": EvaluationSubstitutionType.TEMPLATE, + "config.field1": EvaluationSubstitutionType.SMART_SUBSTITUTE, + "config.field2": EvaluationSubstitutionType.PARAMETER, }); }); @@ -122,12 +139,12 @@ describe("getBindingPathsOfAction", () => { // @ts-ignore const response = getBindingPathsOfAction(basicAction, config); expect(response).toStrictEqual({ - data: true, - isLoading: true, - "config.params[0].key": true, - "config.params[0].value": true, - "config.params[1].key": true, - "config.params[1].value": true, + data: EvaluationSubstitutionType.TEMPLATE, + isLoading: EvaluationSubstitutionType.TEMPLATE, + "config.params[0].key": EvaluationSubstitutionType.TEMPLATE, + "config.params[0].value": EvaluationSubstitutionType.TEMPLATE, + "config.params[1].key": EvaluationSubstitutionType.TEMPLATE, + "config.params[1].value": EvaluationSubstitutionType.TEMPLATE, }); }); @@ -177,10 +194,76 @@ describe("getBindingPathsOfAction", () => { // @ts-ignore const response = getBindingPathsOfAction(basicAction, config); expect(response).toStrictEqual({ - data: true, - isLoading: true, - "config.key": true, - "config.value": true, + data: EvaluationSubstitutionType.TEMPLATE, + isLoading: EvaluationSubstitutionType.TEMPLATE, + "config.key": EvaluationSubstitutionType.TEMPLATE, + "config.value": EvaluationSubstitutionType.TEMPLATE, + }); + }); + + it("checks for hidden field and returns bindingPaths accordingly", () => { + const config = [ + { + sectionName: "", + id: 1, + children: [ + { + label: "", + configProperty: "actionConfiguration.body", + controlType: "QUERY_DYNAMIC_TEXT", + }, + { + label: "", + configProperty: "actionConfiguration.body2", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + hidden: { + path: "actionConfiguration.template.setting", + comparison: "EQUALS", + value: false, + }, + }, + { + label: "", + configProperty: "actionConfiguration.field1", + controlType: "QUERY_DYNAMIC_INPUT_TEXT", + evaluationSubstitutionType: "SMART_SUBSTITUTE", + hidden: { + path: "actionConfiguration.template.setting", + comparison: "EQUALS", + value: true, + }, + }, + ], + }, + ]; + const basicAction = { + ...DEFAULT_ACTION, + actionConfiguration: { + body: "basic action", + body2: "another body", + field1: "alternate body", + template: { + setting: false, + }, + }, + }; + + const response = getBindingPathsOfAction(basicAction, config); + expect(response).toStrictEqual({ + data: EvaluationSubstitutionType.TEMPLATE, + isLoading: EvaluationSubstitutionType.TEMPLATE, + "config.body": EvaluationSubstitutionType.TEMPLATE, + "config.field1": EvaluationSubstitutionType.SMART_SUBSTITUTE, + }); + + basicAction.actionConfiguration.template.setting = true; + + const response2 = getBindingPathsOfAction(basicAction, config); + expect(response2).toStrictEqual({ + data: EvaluationSubstitutionType.TEMPLATE, + isLoading: EvaluationSubstitutionType.TEMPLATE, + "config.body": EvaluationSubstitutionType.TEMPLATE, + "config.body2": EvaluationSubstitutionType.TEMPLATE, }); }); }); diff --git a/app/client/src/entities/Action/actionProperties.ts b/app/client/src/entities/Action/actionProperties.ts index 568e49b5dc..cbda958366 100644 --- a/app/client/src/entities/Action/actionProperties.ts +++ b/app/client/src/entities/Action/actionProperties.ts @@ -1,32 +1,46 @@ import { Action } from "entities/Action/index"; import _ from "lodash"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; +import { isHidden } from "components/formControls/utils"; const dynamicFields = ["QUERY_DYNAMIC_TEXT", "QUERY_DYNAMIC_INPUT_TEXT"]; +const getCorrectEvaluationSubstitutionType = (substitutionType?: string) => { + if (substitutionType) { + if (substitutionType === EvaluationSubstitutionType.SMART_SUBSTITUTE) { + return EvaluationSubstitutionType.SMART_SUBSTITUTE; + } else if (substitutionType === EvaluationSubstitutionType.PARAMETER) { + return EvaluationSubstitutionType.PARAMETER; + } + } + return EvaluationSubstitutionType.TEMPLATE; +}; + export const getBindingPathsOfAction = ( action: Action, formConfig?: any[], -): Record => { - const bindingPaths: Record = { - data: true, - isLoading: true, +): Record => { + const bindingPaths: Record = { + data: EvaluationSubstitutionType.TEMPLATE, + isLoading: EvaluationSubstitutionType.TEMPLATE, }; if (!formConfig) { return { ...bindingPaths, - config: true, + config: EvaluationSubstitutionType.TEMPLATE, }; } const recursiveFindBindingPaths = (formConfig: any) => { if (formConfig.children) { formConfig.children.forEach(recursiveFindBindingPaths); } else { - const configPath = formConfig.configProperty.replace( - "actionConfiguration.", - "config.", - ); + const configPath = getDataTreeActionConfigPath(formConfig.configProperty); if (dynamicFields.includes(formConfig.controlType)) { - bindingPaths[configPath] = true; + if (!isHidden(action, formConfig.hidden)) { + bindingPaths[configPath] = getCorrectEvaluationSubstitutionType( + formConfig.evaluationSubstitutionType, + ); + } } if (formConfig.controlType === "ARRAY_FIELD") { const actionValue = _.get(action, formConfig.configProperty); @@ -38,7 +52,11 @@ export const getBindingPathsOfAction = ( dynamicFields.includes(schemaField.controlType) ) { const arrayConfigPath = `${configPath}[${i}].${schemaField.key}`; - bindingPaths[arrayConfigPath] = true; + bindingPaths[ + arrayConfigPath + ] = getCorrectEvaluationSubstitutionType( + formConfig.evaluationSubstitutionType, + ); } }); } @@ -51,3 +69,6 @@ export const getBindingPathsOfAction = ( return bindingPaths; }; + +export const getDataTreeActionConfigPath = (propertyPath: string) => + propertyPath.replace("actionConfiguration.", "config."); diff --git a/app/client/src/entities/Action/index.ts b/app/client/src/entities/Action/index.ts index 090db222b1..e2a8bc342c 100644 --- a/app/client/src/entities/Action/index.ts +++ b/app/client/src/entities/Action/index.ts @@ -17,6 +17,7 @@ export enum PaginationType { export interface ActionConfig { timeoutInMillisecond?: number; paginationType?: PaginationType; + pluginSpecifiedTemplates?: Array<{ key?: string; value?: unknown }>; } export interface ActionProvider { diff --git a/app/client/src/entities/DataTree/dataTreeAction.ts b/app/client/src/entities/DataTree/dataTreeAction.ts index 1f68a517a4..e10f9ba481 100644 --- a/app/client/src/entities/DataTree/dataTreeAction.ts +++ b/app/client/src/entities/DataTree/dataTreeAction.ts @@ -1,11 +1,15 @@ -import { DynamicPath } from "utils/DynamicBindingUtils"; +import { DependencyMap, DynamicPath } from "utils/DynamicBindingUtils"; import { DataTreeAction, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { ActionData } from "reducers/entityReducers/actionsReducer"; -import { getBindingPathsOfAction } from "entities/Action/actionProperties"; +import { + getBindingPathsOfAction, + getDataTreeActionConfigPath, +} from "entities/Action/actionProperties"; export const generateDataTreeAction = ( action: ActionData, editorConfig: any[], + dependencyConfig: DependencyMap = {}, ): DataTreeAction => { let dynamicBindingPathList: DynamicPath[] = []; // update paths @@ -18,6 +22,12 @@ export const generateDataTreeAction = ( key: `config.${d.key}`, })); } + const dependencyMap: DependencyMap = {}; + Object.entries(dependencyConfig).forEach(([dependent, dependencies]) => { + dependencyMap[getDataTreeActionConfigPath(dependent)] = dependencies.map( + getDataTreeActionConfigPath, + ); + }); return { run: {}, actionId: action.config.id, @@ -29,5 +39,6 @@ export const generateDataTreeAction = ( ENTITY_TYPE: ENTITY_TYPE.ACTION, isLoading: action.isLoading, bindingPaths: getBindingPathsOfAction(action.config, editorConfig), + dependencyMap, }; }; diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index 5c6dee3737..eb071d4bca 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -9,7 +9,7 @@ import { MetaState } from "reducers/entityReducers/metaReducer"; import { PageListPayload } from "constants/ReduxActionConstants"; import { ActionConfig, PluginType } from "entities/Action"; import { AppDataState } from "reducers/entityReducers/appReducer"; -import { DynamicPath } from "utils/DynamicBindingUtils"; +import { DependencyMap, DynamicPath } from "utils/DynamicBindingUtils"; import { generateDataTreeAction } from "entities/DataTree/dataTreeAction"; import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; @@ -36,6 +36,12 @@ export type RunActionPayload = { params: Record | string; }; +export enum EvaluationSubstitutionType { + TEMPLATE = "TEMPLATE", + PARAMETER = "PARAMETER", + SMART_SUBSTITUTE = "SMART_SUBSTITUTE", +} + export interface DataTreeAction extends Omit { data: ActionResponse["body"]; actionId: string; @@ -46,12 +52,13 @@ export interface DataTreeAction extends Omit { | ActionDispatcher | Record; dynamicBindingPathList: DynamicPath[]; - bindingPaths: Record; + bindingPaths: Record; ENTITY_TYPE: ENTITY_TYPE.ACTION; + dependencyMap: DependencyMap; } export interface DataTreeWidget extends WidgetProps { - bindingPaths: Record; + bindingPaths: Record; triggerPaths: Record; validationPaths: Record; ENTITY_TYPE: ENTITY_TYPE.WIDGET; @@ -79,6 +86,7 @@ export type DataTree = { type DataTreeSeed = { actions: ActionDataState; editorConfigs: Record; + pluginDependencyConfig: Record; widgets: CanvasWidgetsReduxState; widgetsMeta: MetaState; pageList: PageListPayload; @@ -93,13 +101,16 @@ export class DataTreeFactory { pageList, appData, editorConfigs, + pluginDependencyConfig, }: DataTreeSeed): DataTree { const dataTree: DataTree = {}; actions.forEach((action) => { const editorConfig = editorConfigs[action.config.pluginId]; + const dependencyConfig = pluginDependencyConfig[action.config.pluginId]; dataTree[action.config.name] = generateDataTreeAction( action, editorConfig, + dependencyConfig, ); }); Object.values(widgets).forEach((widget) => { diff --git a/app/client/src/entities/DataTree/dataTreeWidget.test.ts b/app/client/src/entities/DataTree/dataTreeWidget.test.ts index 23ab042a17..5ee2df1219 100644 --- a/app/client/src/entities/DataTree/dataTreeWidget.test.ts +++ b/app/client/src/entities/DataTree/dataTreeWidget.test.ts @@ -1,6 +1,10 @@ import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget"; -import { DataTreeWidget, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { + DataTreeWidget, + ENTITY_TYPE, + EvaluationSubstitutionType, +} from "entities/DataTree/dataTreeFactory"; import { RenderModes, WidgetTypes } from "constants/WidgetConstants"; import WidgetFactory from "utils/WidgetFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; @@ -181,19 +185,19 @@ describe("generateDataTreeWidget", () => { const expected: DataTreeWidget = { bindingPaths: { - defaultText: true, - errorMessage: true, - isDirty: true, - isDisabled: true, - isFocused: true, - isRequired: true, - isValid: true, - isVisible: true, - placeholderText: true, - regex: true, - resetOnSubmit: true, - text: true, - value: true, + defaultText: EvaluationSubstitutionType.TEMPLATE, + errorMessage: EvaluationSubstitutionType.TEMPLATE, + isDirty: EvaluationSubstitutionType.TEMPLATE, + isDisabled: EvaluationSubstitutionType.TEMPLATE, + isFocused: EvaluationSubstitutionType.TEMPLATE, + isRequired: EvaluationSubstitutionType.TEMPLATE, + isValid: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + placeholderText: EvaluationSubstitutionType.TEMPLATE, + regex: EvaluationSubstitutionType.TEMPLATE, + resetOnSubmit: EvaluationSubstitutionType.TEMPLATE, + text: EvaluationSubstitutionType.TEMPLATE, + value: EvaluationSubstitutionType.TEMPLATE, }, triggerPaths: { onSubmit: true, diff --git a/app/client/src/entities/DataTree/dataTreeWidget.ts b/app/client/src/entities/DataTree/dataTreeWidget.ts index a0dacf901f..fa77cfa3fb 100644 --- a/app/client/src/entities/DataTree/dataTreeWidget.ts +++ b/app/client/src/entities/DataTree/dataTreeWidget.ts @@ -19,26 +19,10 @@ export const generateDataTreeWidget = ( const propertyPaneConfigs = WidgetFactory.getWidgetPropertyPaneConfig( widget.type, ); - const { - bindingPaths, - triggerPaths, - validationPaths, - } = getAllPathsFromPropertyConfig( - widget, - propertyPaneConfigs, - Object.fromEntries( - Object.keys(derivedPropertyMap).map((key) => [key, true]), - ), - ); - Object.keys(defaultMetaProps).forEach((defaultPath) => { - bindingPaths[defaultPath] = true; - }); const derivedProps: any = {}; const dynamicBindingPathList = getEntityDynamicBindingPathList(widget); dynamicBindingPathList.forEach((dynamicPath) => { const propertyPath = dynamicPath.key; - // Add any dynamically generated dynamic bindings in the binding paths - bindingPaths[propertyPath] = true; const propertyValue = _.get(widget, propertyPath); if (_.isObject(propertyValue)) { // Stringify this because composite controls may have bindings in the sub controls @@ -54,7 +38,6 @@ export const generateDataTreeWidget = ( dynamicBindingPathList.push({ key: propertyName, }); - bindingPaths[propertyName] = true; }); const unInitializedDefaultProps: Record = {}; Object.values(defaultProps).forEach((propertyName) => { @@ -62,6 +45,16 @@ export const generateDataTreeWidget = ( unInitializedDefaultProps[propertyName] = undefined; } }); + const { + bindingPaths, + triggerPaths, + validationPaths, + } = getAllPathsFromPropertyConfig(widget, propertyPaneConfigs, { + ...derivedPropertyMap, + ...defaultMetaProps, + ...unInitializedDefaultProps, + ..._.keyBy(dynamicBindingPathList, "key"), + }); return { ...widget, ...defaultMetaProps, diff --git a/app/client/src/entities/Widget/utils.test.ts b/app/client/src/entities/Widget/utils.test.ts index bab8f09a93..286dde8a7a 100644 --- a/app/client/src/entities/Widget/utils.test.ts +++ b/app/client/src/entities/Widget/utils.test.ts @@ -2,6 +2,7 @@ import { getAllPathsFromPropertyConfig } from "./utils"; import { RenderModes, WidgetTypes } from "../../constants/WidgetConstants"; import tablePropertyPaneConfig from "widgets/TableWidget/TablePropertyPaneConfig"; import chartPorpertyConfig from "widgets/ChartWidget/propertyConfig"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; describe("getAllPathsFromPropertyConfig", () => { it("works as expected for table widget", () => { @@ -112,31 +113,47 @@ describe("getAllPathsFromPropertyConfig", () => { const expected = { bindingPaths: { - selectedRow: true, - selectedRows: true, - tableData: true, - defaultSearchText: true, - defaultSelectedRow: true, - isVisible: true, - "primaryColumns.name.computedValue": true, - "primaryColumns.name.horizontalAlignment": true, - "primaryColumns.name.verticalAlignment": true, - "primaryColumns.name.textSize": true, - "primaryColumns.name.fontStyle": true, - "primaryColumns.name.textColor": true, - "primaryColumns.name.cellBackground": true, - "primaryColumns.createdAt.inputFormat": true, - "primaryColumns.createdAt.outputFormat": true, - "primaryColumns.createdAt.computedValue": true, - "primaryColumns.createdAt.horizontalAlignment": true, - "primaryColumns.createdAt.verticalAlignment": true, - "primaryColumns.createdAt.textSize": true, - "primaryColumns.createdAt.fontStyle": true, - "primaryColumns.createdAt.textColor": true, - "primaryColumns.createdAt.cellBackground": true, - "primaryColumns.status.buttonLabel": true, - "primaryColumns.status.buttonStyle": true, - "primaryColumns.status.buttonLabelColor": true, + selectedRow: EvaluationSubstitutionType.TEMPLATE, + selectedRows: EvaluationSubstitutionType.TEMPLATE, + tableData: EvaluationSubstitutionType.TEMPLATE, + defaultSearchText: EvaluationSubstitutionType.TEMPLATE, + defaultSelectedRow: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.computedValue": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.horizontalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.verticalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.textSize": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.fontStyle": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.textColor": EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.name.cellBackground": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.inputFormat": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.outputFormat": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.computedValue": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.horizontalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.verticalAlignment": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.textSize": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.fontStyle": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.textColor": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.createdAt.cellBackground": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.buttonLabel": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.buttonStyle": + EvaluationSubstitutionType.TEMPLATE, + "primaryColumns.status.buttonLabelColor": + EvaluationSubstitutionType.TEMPLATE, }, triggerPaths: { onRowSelected: true, @@ -197,13 +214,13 @@ describe("getAllPathsFromPropertyConfig", () => { const expected = { bindingPaths: { - chartType: true, - chartName: true, - "chartData[0].seriesName": true, - "chartData[0].data": true, - xAxisName: true, - yAxisName: true, - isVisible: true, + chartType: EvaluationSubstitutionType.TEMPLATE, + chartName: EvaluationSubstitutionType.TEMPLATE, + "chartData[0].seriesName": EvaluationSubstitutionType.TEMPLATE, + "chartData[0].data": EvaluationSubstitutionType.TEMPLATE, + xAxisName: EvaluationSubstitutionType.TEMPLATE, + yAxisName: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, }, triggerPaths: { onDataPointClick: true, diff --git a/app/client/src/entities/Widget/utils.ts b/app/client/src/entities/Widget/utils.ts index 84ae7d552a..538218fd3d 100644 --- a/app/client/src/entities/Widget/utils.ts +++ b/app/client/src/entities/Widget/utils.ts @@ -3,17 +3,22 @@ import { PropertyPaneConfig } from "constants/PropertyControlConstants"; import { get } from "lodash"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; export const getAllPathsFromPropertyConfig = ( widget: WidgetProps, widgetConfig: readonly PropertyPaneConfig[], - derivedProperties: Record, + defaultProperties: Record, ): { - bindingPaths: Record; + bindingPaths: Record; triggerPaths: Record; validationPaths: Record; } => { - const bindingPaths: Record = derivedProperties; + const bindingPaths: Record = {}; + Object.keys(defaultProperties).forEach( + (property) => + (bindingPaths[property] = EvaluationSubstitutionType.TEMPLATE), + ); const triggerPaths: Record = {}; const validationPaths: Record = {}; widgetConfig.forEach((config) => { @@ -29,7 +34,8 @@ export const getAllPathsFromPropertyConfig = ( controlConfig.isBindProperty && !controlConfig.isTriggerProperty ) { - bindingPaths[controlConfig.propertyName] = true; + bindingPaths[controlConfig.propertyName] = + EvaluationSubstitutionType.TEMPLATE; if (controlConfig.validation) { validationPaths[controlConfig.propertyName] = controlConfig.validation; @@ -72,7 +78,8 @@ export const getAllPathsFromPropertyConfig = ( panelColumnControlConfig.isBindProperty && !panelColumnControlConfig.isTriggerProperty ) { - bindingPaths[panelPropertyPath] = true; + bindingPaths[panelPropertyPath] = + EvaluationSubstitutionType.TEMPLATE; if (panelColumnControlConfig.validation) { validationPaths[panelPropertyPath] = panelColumnControlConfig.validation; @@ -107,7 +114,8 @@ export const getAllPathsFromPropertyConfig = ( childPropertyConfig.isBindProperty && !childPropertyConfig.isTriggerProperty ) { - bindingPaths[childArrayPropertyPath] = true; + bindingPaths[childArrayPropertyPath] = + EvaluationSubstitutionType.TEMPLATE; if (childPropertyConfig.validation) { validationPaths[childArrayPropertyPath] = childPropertyConfig.validation; @@ -131,13 +139,13 @@ export const getAllPathsFromPropertyConfig = ( }; export const nextAvailableRowInContainer = ( - parenContainertId: string, + parentContainerId: string, canvasWidgets: { [widgetId: string]: FlattenedWidgetProps }, ) => { return ( Object.values(canvasWidgets).reduce( (prev: number, next: any) => - next?.parentId === parenContainertId && next.bottomRow > prev + next?.parentId === parentContainerId && next.bottomRow > prev ? next.bottomRow : prev, 0, diff --git a/app/client/src/reducers/entityReducers/pluginsReducer.ts b/app/client/src/reducers/entityReducers/pluginsReducer.ts index 1538ccd310..5c8a414a51 100644 --- a/app/client/src/reducers/entityReducers/pluginsReducer.ts +++ b/app/client/src/reducers/entityReducers/pluginsReducer.ts @@ -9,6 +9,7 @@ import { PluginFormPayloadWithId, PluginFormsPayload, } from "actions/pluginActions"; +import { DependencyMap } from "utils/DynamicBindingUtils"; export interface PluginDataState { list: Plugin[]; @@ -16,6 +17,7 @@ export interface PluginDataState { formConfigs: Record; editorConfigs: Record; settingConfigs: Record; + dependencies: Record; } const initialState: PluginDataState = { @@ -24,6 +26,7 @@ const initialState: PluginDataState = { formConfigs: {}, editorConfigs: {}, settingConfigs: {}, + dependencies: {}, }; const pluginsReducer = createReducer(initialState, { diff --git a/app/client/src/reducers/uiReducers/editorReducer.tsx b/app/client/src/reducers/uiReducers/editorReducer.tsx index 22ac72b8f4..f4dfa33458 100644 --- a/app/client/src/reducers/uiReducers/editorReducer.tsx +++ b/app/client/src/reducers/uiReducers/editorReducer.tsx @@ -6,6 +6,7 @@ import { ReduxActionErrorTypes, } from "constants/ReduxActionConstants"; import moment from "moment"; +import { PageAction } from "constants/AppsmithActionConstants/ActionConstants"; const initialState: EditorReduxState = { initialized: false, @@ -106,6 +107,7 @@ const editorReducer = createReducer(initialState, { pageWidgetId, currentApplicationId, currentPageId, + pageActions, } = action.payload; state.loadingStates.publishing = false; state.loadingStates.publishingError = false; @@ -116,6 +118,7 @@ const editorReducer = createReducer(initialState, { pageWidgetId, currentApplicationId, currentPageId, + pageActions, }; }, [ReduxActionTypes.CLONE_PAGE_INIT]: (state: EditorReduxState) => { @@ -171,6 +174,7 @@ export interface EditorReduxState { currentLayoutId?: string; currentPageName?: string; currentPageId?: string; + pageActions?: PageAction[][]; loadingStates: { saving: boolean; savingError: boolean; diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 20c9d1f119..06148a2194 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -30,6 +30,7 @@ import { executeAction, executeActionError } from "actions/widgetActions"; import { getCurrentApplicationId, getCurrentPageId, + getLayoutOnLoadActions, getPageList, } from "selectors/editorSelectors"; import _, { get, isString } from "lodash"; @@ -979,9 +980,9 @@ function* executePageLoadAction(pageAction: PageAction) { } } -function* executePageLoadActionsSaga(action: ReduxAction) { +function* executePageLoadActionsSaga() { try { - const pageActions = action.payload; + const pageActions: PageAction[][] = yield select(getLayoutOnLoadActions); const actionCount = _.flatten(pageActions).length; PerformanceTracker.startAsyncTracking( PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS, diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 43113e54ab..7cd345d7ba 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -1,4 +1,5 @@ import { + EvaluationReduxAction, ReduxAction, ReduxActionErrorTypes, ReduxActionTypes, @@ -158,7 +159,9 @@ export function* createActionSaga( } } -export function* fetchActionsSaga(action: ReduxAction) { +export function* fetchActionsSaga( + action: EvaluationReduxAction, +) { const { applicationId } = action.payload; PerformanceTracker.startAsyncTracking( PerformanceTransactionName.FETCH_ACTIONS_API, @@ -173,6 +176,7 @@ export function* fetchActionsSaga(action: ReduxAction) { yield put({ type: ReduxActionTypes.FETCH_ACTIONS_SUCCESS, payload: response.data, + postEvalActions: action.postEvalActions, }); PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.FETCH_ACTIONS_API, diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index c5e07540cc..e32f657bf6 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -67,7 +67,6 @@ import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages"; import { checkCurrentStep } from "./OnboardingSagas"; import { OnboardingStep } from "constants/OnboardingConstants"; import { getIndextoUpdate } from "utils/ApiPaneUtils"; -import { changeQuery } from "actions/queryPaneActions"; function* syncApiParamsSaga( actionPayload: ReduxActionWithMeta, @@ -208,29 +207,10 @@ function* initializeExtraFormDataSaga() { const { extraformData } = state.ui.apiPane; const formData = yield select(getFormData, API_EDITOR_FORM_NAME); const { values } = formData; - const headers = get( - values, - "actionConfiguration.headers", - DEFAULT_API_ACTION_CONFIG.headers, - ); + const headers = get(values, "actionConfiguration.headers"); - const queryParameters = get( - values, - "actionConfiguration.queryParameters", - [], - ); if (!extraformData[values.id]) { - yield put( - change(API_EDITOR_FORM_NAME, "actionConfiguration.headers", headers), - ); - if (queryParameters.length === 0) - yield put( - change( - API_EDITOR_FORM_NAME, - "actionConfiguration.queryParameters", - DEFAULT_API_ACTION_CONFIG.queryParameters, - ), - ); + yield call(setHeaderFormat, values.id, headers); } } @@ -270,6 +250,42 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) { PerformanceTracker.stopTracking(); } +function* setHeaderFormat(apiId: string, headers?: Property[]) { + let displayFormat; + + if (headers) { + const contentType = headers.find( + (header: any) => + header && + header.key && + header.key.toLowerCase() === CONTENT_TYPE_HEADER_KEY, + ); + + if ( + contentType && + contentType.value && + POST_BODY_FORMATS.includes(contentType.value) + ) { + displayFormat = { + label: contentType.value, + value: contentType.value, + }; + } else { + displayFormat = POST_BODY_FORMAT_OPTIONS[3]; + } + } + + yield put({ + type: ReduxActionTypes.SET_EXTRA_FORMDATA, + payload: { + id: apiId, + values: { + displayFormat, + }, + }, + }); +} + function* updateFormFields( actionPayload: ReduxActionWithMeta, ) { @@ -318,35 +334,7 @@ function* updateFormFields( "actionConfiguration.headers", ); const apiId = get(values, "id"); - let displayFormat; - - if (actionConfigurationHeaders) { - const contentType = actionConfigurationHeaders.find( - (header: any) => - header && - header.key && - header.key.toLowerCase() === CONTENT_TYPE_HEADER_KEY, - ); - - if (contentType && POST_BODY_FORMATS.includes(contentType.value)) { - displayFormat = { - label: contentType.value, - value: contentType.value, - }; - } else { - displayFormat = POST_BODY_FORMAT_OPTIONS[3]; - } - } - - yield put({ - type: ReduxActionTypes.SET_EXTRA_FORMDATA, - payload: { - id: apiId, - values: { - displayFormat, - }, - }, - }); + yield call(setHeaderFormat, apiId, actionConfigurationHeaders); } } @@ -562,12 +550,6 @@ function* handleApiNameChangeFailureSaga( yield put(change(API_EDITOR_FORM_NAME, "name", action.payload.oldName)); } -function* updateFormValues(action: ReduxAction<{ data: Action }>) { - if (action.payload.data.pluginType === PluginType.API) { - yield call(changeApiSaga, changeQuery(action.payload.data.id)); - } -} - export default function* root() { yield all([ takeEvery(ReduxActionTypes.API_PANE_CHANGE_API, changeApiSaga), @@ -597,7 +579,6 @@ export default function* root() { ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE, handleUpdateBodyContentType, ), - takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, updateFormValues), // Intercepting the redux-form change actionType takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga), diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 4dccf11c6e..7aef086af0 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -12,6 +12,7 @@ import { ReduxAction, ReduxActionErrorTypes, ReduxActionTypes, + ReduxActionWithoutPayload, } from "constants/ReduxActionConstants"; import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors"; import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory"; @@ -109,13 +110,17 @@ const evalErrorHandler = (errors: EvalError[]) => { }); }; -function* postEvalActionDispatcher(actions: ReduxAction[]) { +function* postEvalActionDispatcher( + actions: Array | ReduxActionWithoutPayload>, +) { for (const action of actions) { yield put(action); } } -function* evaluateTreeSaga(postEvalActions?: ReduxAction[]) { +function* evaluateTreeSaga( + postEvalActions?: Array | ReduxActionWithoutPayload>, +) { const unevalTree = yield select(getUnevaluatedDataTree); log.debug({ unevalTree }); PerformanceTracker.startAsyncTracking( diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 353dec7f36..92cf151453 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -13,6 +13,7 @@ import { ReduxAction, ReduxActionErrorTypes, ReduxActionTypes, + ReduxActionWithoutPayload, } from "constants/ReduxActionConstants"; import { ERROR_CODES } from "constants/ApiConstants"; @@ -43,6 +44,41 @@ import { resetEditorSuccess } from "actions/initActions"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; +import { executePageLoadActions } from "actions/widgetActions"; + +function* failFastApiCalls( + triggerActions: Array | ReduxActionWithoutPayload>, + successActions: string[], + failureActions: string[], +) { + const triggerEffects = []; + for (const triggerAction of triggerActions) { + triggerEffects.push(put(triggerAction)); + } + const successEffects = []; + for (const successAction of successActions) { + successEffects.push(take(successAction)); + } + yield all(triggerEffects); + const effectRaceResult = yield race({ + success: all(successEffects), + failure: take(failureActions), + }); + if (effectRaceResult.failure) { + yield put({ + type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, + payload: { + code: get( + effectRaceResult, + "failure.payload.error.code", + ERROR_CODES.SERVER_ERROR, + ), + }, + }); + return false; + } + return true; +} function* initializeEditorSaga( initializeEditorAction: ReduxAction, @@ -55,97 +91,61 @@ function* initializeEditorSaga( yield put(setAppMode(APP_MODE.EDIT)); yield put(updateAppPersistentStore(getPersistentAppStore(applicationId))); yield put({ type: ReduxActionTypes.START_EVALUATION }); - yield all([ - put(fetchPageList(applicationId, APP_MODE.EDIT)), - put(fetchActions(applicationId)), - put(fetchPage(pageId)), - put(fetchApplication(applicationId, APP_MODE.EDIT)), - ]); - yield put(restoreRecentEntitiesRequest(applicationId)); - - const resultOfPrimaryCalls = yield race({ - success: all([ - take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), - take(ReduxActionTypes.FETCH_PAGE_SUCCESS), - take(ReduxActionTypes.FETCH_APPLICATION_SUCCESS), - take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS), - ]), - failure: take([ + const applicationAndLayoutCalls = yield failFastApiCalls( + [ + fetchPageList(applicationId, APP_MODE.EDIT), + fetchPage(pageId), + fetchApplication(applicationId, APP_MODE.EDIT), + ], + [ + ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS, + ReduxActionTypes.FETCH_PAGE_SUCCESS, + ReduxActionTypes.FETCH_APPLICATION_SUCCESS, + ], + [ ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR, ReduxActionErrorTypes.FETCH_PAGE_ERROR, ReduxActionErrorTypes.FETCH_APPLICATION_ERROR, - ReduxActionErrorTypes.FETCH_ACTIONS_ERROR, - ]), - }); + ], + ); + if (!applicationAndLayoutCalls) return; - if (resultOfPrimaryCalls.failure) { - yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, - payload: { - code: get( - resultOfPrimaryCalls, - "failure.payload.error.code", - ERROR_CODES.SERVER_ERROR, - ), - }, - }); - return; - } - - yield all([put(fetchPlugins()), put(fetchDatasources())]); - - const resultOfSecondaryCalls = yield race({ - success: all([ - take(ReduxActionTypes.FETCH_PLUGINS_SUCCESS), - take(ReduxActionTypes.FETCH_DATASOURCES_SUCCESS), - ]), - failure: take([ + const pluginsAndDatasourcesCalls = yield failFastApiCalls( + [fetchPlugins(), fetchDatasources()], + [ + ReduxActionTypes.FETCH_PLUGINS_SUCCESS, + ReduxActionTypes.FETCH_DATASOURCES_SUCCESS, + ], + [ ReduxActionErrorTypes.FETCH_PLUGINS_ERROR, ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR, - ]), - }); + ], + ); + if (!pluginsAndDatasourcesCalls) return; - if (resultOfSecondaryCalls.failure) { - yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, - payload: { - code: get( - resultOfSecondaryCalls, - "failure.payload.error.code", - ERROR_CODES.SERVER_ERROR, - ), - }, - }); - return; - } + const pluginFormCall = yield failFastApiCalls( + [fetchPluginFormConfigs()], + [ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS], + [ReduxActionErrorTypes.FETCH_PLUGIN_FORM_CONFIGS_ERROR], + ); + if (!pluginFormCall) return; - yield put(fetchPluginFormConfigs()); + const actionsCall = yield failFastApiCalls( + [fetchActions(applicationId, [executePageLoadActions()])], + [ReduxActionTypes.FETCH_ACTIONS_SUCCESS], + [ReduxActionErrorTypes.FETCH_ACTIONS_ERROR], + ); - const resultOfPluginFormsCall = yield race({ - success: take(ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS), - failure: take(ReduxActionErrorTypes.FETCH_PLUGIN_FORM_CONFIGS_ERROR), - }); - - if (resultOfPluginFormsCall.failure) { - yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, - payload: { - code: get( - resultOfPluginFormsCall, - "failure.payload.error.code", - ERROR_CODES.SERVER_ERROR, - ), - }, - }); - return; - } + if (!actionsCall) return; const currentApplication = yield select(getCurrentApplication); const appName = currentApplication ? currentApplication.name : ""; const appId = currentApplication ? currentApplication.id : ""; + yield put(restoreRecentEntitiesRequest(applicationId)); + AnalyticsUtil.logEvent("EDITOR_OPEN", { appId: appId, appName: appName, diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index 7e41f72dbc..b99f3d048f 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -195,12 +195,7 @@ export function* fetchPageSaga( // set current page yield put(updateCurrentPage(id)); // dispatch fetch page success - yield put( - fetchPageSuccess([ - // Execute page load actions after evaluation of fetch page - executePageLoadActions(canvasWidgetsPayload.pageActions), - ]), - ); + yield put(fetchPageSuccess()); yield put({ type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE, @@ -264,7 +259,7 @@ export function* fetchPublishedPageSaga( yield put( fetchPublishedPageSuccess( // Execute page load actions post published page eval - [executePageLoadActions(canvasWidgetsPayload.pageActions)], + [executePageLoadActions()], ), ); PerformanceTracker.stopAsyncTracking( diff --git a/app/client/src/sagas/PluginSagas.ts b/app/client/src/sagas/PluginSagas.ts index 0ed227fbbd..66d250fa3c 100644 --- a/app/client/src/sagas/PluginSagas.ts +++ b/app/client/src/sagas/PluginSagas.ts @@ -19,6 +19,7 @@ import { fetchPluginFormConfigSuccess, } from "actions/pluginActions"; import { + defaultActionDependenciesConfig, defaultActionEditorConfigs, defaultActionSettings, } from "constants/AppsmithActionConstants/ActionConstants"; @@ -26,6 +27,7 @@ import { GenericApiResponse } from "api/ApiResponses"; import PluginApi from "api/PluginApi"; import log from "loglevel"; import { PluginType } from "entities/Action"; +import { DependencyMap } from "utils/DynamicBindingUtils"; function* fetchPluginsSaga() { try { @@ -77,20 +79,30 @@ function* fetchPluginFormConfigsSaga() { const formConfigs: Record = {}; const editorConfigs: Record = {}; const settingConfigs: Record = {}; + const dependencies: Record = {}; Array.from(pluginIdFormsToFetch).forEach((pluginId, index) => { const plugin = plugins.find((plugin) => plugin.id === pluginId); + // Datasource form always use server's copy formConfigs[pluginId] = pluginFormData[index].form; + // Action editor form if not available use default if (plugin && !pluginFormData[index].editor) { editorConfigs[pluginId] = defaultActionEditorConfigs[plugin.type]; } else { editorConfigs[pluginId] = pluginFormData[index].editor; } + // Action settings form if not available use default if (plugin && !pluginFormData[index].setting) { settingConfigs[pluginId] = defaultActionSettings[plugin.type]; } else { settingConfigs[pluginId] = pluginFormData[index].setting; } + // Action dependencies config if not available use default + if (plugin && !pluginFormData[index].dependencies) { + dependencies[pluginId] = defaultActionDependenciesConfig[plugin.type]; + } else { + dependencies[pluginId] = pluginFormData[index].dependencies; + } }); yield put( @@ -98,6 +110,7 @@ function* fetchPluginFormConfigsSaga() { formConfigs, editorConfigs, settingConfigs, + dependencies, }), ); } catch (error) { @@ -125,6 +138,10 @@ export function* checkAndGetPluginFormConfigsSaga(pluginId: string) { formConfigResponse.data.editor = defaultActionEditorConfigs[plugin.type]; } + if (!formConfigResponse.data.dependencies) { + formConfigResponse.data.dependencies = + defaultActionDependenciesConfig[plugin.type]; + } yield put( fetchPluginFormConfigSuccess({ id: pluginId, diff --git a/app/client/src/sagas/ProvidersSaga.ts b/app/client/src/sagas/ProvidersSaga.ts index 9379049236..397f411347 100644 --- a/app/client/src/sagas/ProvidersSaga.ts +++ b/app/client/src/sagas/ProvidersSaga.ts @@ -104,7 +104,7 @@ export function* addApiToPageSaga( }); const applicationId = yield select(getCurrentApplicationId); - yield put(fetchActions(applicationId)); + yield put(fetchActions(applicationId, [])); } } catch (error) { yield put({ diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index 09c056a3a9..acdeb053e4 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -27,7 +27,7 @@ import { getPluginTemplates, getPlugin, } from "selectors/entitiesSelector"; -import { Action, PluginType, QueryAction } from "entities/Action"; +import { PluginType, QueryAction } from "entities/Action"; import { setActionProperty } from "actions/actionActions"; import { getQueryParams } from "utils/AppsmithUtils"; import { isEmpty, merge } from "lodash"; @@ -37,7 +37,6 @@ import { Toaster } from "components/ads/Toast"; import { Datasource } from "entities/Datasource"; import _ from "lodash"; import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages"; -import { changeQuery } from "actions/queryPaneActions"; function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { const { id } = actionPayload.payload; @@ -214,12 +213,6 @@ function* handleNameChangeFailureSaga( yield put(change(QUERY_EDITOR_FORM_NAME, "name", action.payload.oldName)); } -function* updateFormValues(action: ReduxAction<{ data: Action }>) { - if (action.payload.data.pluginType === PluginType.DB) { - yield call(changeQuerySaga, changeQuery(action.payload.data.id)); - } -} - export default function* root() { yield all([ takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleQueryCreatedSaga), @@ -237,7 +230,6 @@ export default function* root() { ReduxActionErrorTypes.SAVE_ACTION_NAME_ERROR, handleNameChangeFailureSaga, ), - takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, updateFormValues), // Intercepting the redux-form change actionType takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga), diff --git a/app/client/src/selectors/dataTreeSelectors.ts b/app/client/src/selectors/dataTreeSelectors.ts index 9b4f6cf231..6ad8a33ddc 100644 --- a/app/client/src/selectors/dataTreeSelectors.ts +++ b/app/client/src/selectors/dataTreeSelectors.ts @@ -2,6 +2,7 @@ import { createSelector } from "reselect"; import { getActionsForCurrentPage, getAppData, + getPluginDependencyConfig, getPluginEditorConfigs, } from "./entitiesSelector"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; @@ -18,7 +19,16 @@ export const getUnevaluatedDataTree = createSelector( getPageList, getAppData, getPluginEditorConfigs, - (actions, widgets, widgetsMeta, pageListPayload, appData, editorConfigs) => { + getPluginDependencyConfig, + ( + actions, + widgets, + widgetsMeta, + pageListPayload, + appData, + editorConfigs, + pluginDependencyConfig, + ) => { const pageList = pageListPayload || []; return DataTreeFactory.create({ actions, @@ -27,6 +37,7 @@ export const getUnevaluatedDataTree = createSelector( pageList, appData, editorConfigs, + pluginDependencyConfig, }); }, ); diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 89d8e8b400..632f88e28e 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -63,6 +63,9 @@ export const getPageSavingError = (state: AppState) => { return state.ui.editor.loadingStates.savingError; }; +export const getLayoutOnLoadActions = (state: AppState) => + state.ui.editor.pageActions || []; + export const getIsPublishingApplication = (state: AppState) => state.ui.editor.loadingStates.publishing; diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 3f427c6716..cbcfcdf01f 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -129,6 +129,9 @@ export const getPluginByPackageName = (state: AppState, name: string) => export const getPluginEditorConfigs = (state: AppState) => state.entities.plugins.editorConfigs; +export const getPluginDependencyConfig = (state: AppState) => + state.entities.plugins.dependencies; + export const getPluginSettingConfigs = (state: AppState, pluginId: string) => state.entities.plugins.settingConfigs[pluginId]; diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts index bff6e9a8a5..54fd467836 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts @@ -2,7 +2,11 @@ import { generateTypeDef, dataTreeTypeDefCreator, } from "utils/autocomplete/dataTreeTypeDefCreator"; -import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { + DataTree, + ENTITY_TYPE, + EvaluationSubstitutionType, +} from "entities/DataTree/dataTreeFactory"; import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; import { WidgetTypes } from "../../constants/WidgetConstants"; @@ -26,7 +30,7 @@ describe("dataTreeTypeDefCreator", () => { isLoading: false, version: 1, bindingPaths: { - defaultText: true, + defaultText: EvaluationSubstitutionType.TEMPLATE, }, triggerPaths: { onTextChange: true, diff --git a/app/client/src/utils/canvasStructureHelpers.test.ts b/app/client/src/utils/canvasStructureHelpers.test.ts index 8f6a2a9a41..ebd0c3a85e 100644 --- a/app/client/src/utils/canvasStructureHelpers.test.ts +++ b/app/client/src/utils/canvasStructureHelpers.test.ts @@ -81,272 +81,6 @@ const simpleDSL: any = { ], }; -const newDSL: any = { - widgetName: "MainContainer", - backgroundColor: "none", - rightColumn: 1224, - snapColumns: 16, - detachFromLayout: true, - widgetId: "0", - topRow: 0, - bottomRow: 4320, - containerStyle: "none", - snapRows: 33, - parentRowSpace: 1, - type: "CANVAS_WIDGET", - canExtend: true, - dynamicBindingPathList: [], - version: 6, - minHeight: 1292, - parentColumnSpace: 1, - leftColumn: 0, - children: [ - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button16", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 43, - bottomRow: 50, - parentId: "0", - widgetId: "77rkwd5hm7", - dynamicTriggerPathList: [{ key: "onClick" }], - onClick: "{{showModal('Modal1')}}", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button17", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 51, - bottomRow: 58, - parentId: "0", - widgetId: "atvf7cgber", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button20", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 59, - bottomRow: 66, - parentId: "0", - widgetId: "c09qn063tc", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button21", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 67, - bottomRow: 74, - parentId: "0", - widgetId: "cu7873x1s6", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button22", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 75, - bottomRow: 82, - parentId: "0", - widgetId: "qgxdk87yiw", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button23", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 83, - bottomRow: 90, - parentId: "0", - widgetId: "oeu2eud3q4", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button24", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 91, - bottomRow: 98, - parentId: "0", - widgetId: "11sgnzdckq", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button25", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 0, - rightColumn: 10, - topRow: 99, - bottomRow: 106, - parentId: "0", - widgetId: "rs2c4g4g0o", - }, - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button13", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 34.5, - parentRowSpace: 40, - leftColumn: 7, - rightColumn: 9, - topRow: 7, - bottomRow: 8, - parentId: "0", - widgetId: "iwsi8fleku", - dynamicTriggerPathList: [{ key: "onClick" }], - onClick: "{{showModal('Modal1')}}", - }, - { - isVisible: true, - shouldScrollContents: false, - widgetName: "Tabs1", - tabs: - '[{"id":"tab2","widgetId":"377zsl4rgg","label":"Tab 2"},{"id":"tab1","widgetId":"9augj62fwd","label":"Tab 1"}]', - shouldShowTabs: true, - defaultTab: "Tab 1", - blueprint: { operations: [{ type: "MODIFY_PROPS" }] }, - type: "TABS_WIDGET", - isLoading: false, - parentColumnSpace: 74, - parentRowSpace: 40, - leftColumn: 3, - rightColumn: 11, - topRow: 11, - bottomRow: 18, - parentId: "0", - widgetId: "g3s5k86c8v", - children: [ - { - type: "CANVAS_WIDGET", - tabId: "tab2", - tabName: "Tab 2", - widgetId: "377zsl4rgg", - parentId: "g3s5k86c8v", - detachFromLayout: true, - children: [], - parentRowSpace: 1, - parentColumnSpace: 1, - leftColumn: 0, - rightColumn: 592, - topRow: 0, - bottomRow: 280, - isLoading: false, - widgetName: "Canvas1", - renderMode: "CANVAS", - }, - { - type: "CANVAS_WIDGET", - tabId: "tab1", - tabName: "Tab 1", - widgetId: "9augj62fwd", - parentId: "g3s5k86c8v", - detachFromLayout: true, - children: [ - { - isVisible: true, - text: "Submit", - buttonStyle: "PRIMARY_BUTTON", - widgetName: "Button26", - isDisabled: false, - isDefaultClickDisabled: true, - type: "BUTTON_WIDGET", - isLoading: false, - parentColumnSpace: 34.5, - parentRowSpace: 40, - leftColumn: 2, - rightColumn: 4, - topRow: 1, - bottomRow: 2, - parentId: "9augj62fwd", - widgetId: "o87mpa118i", - }, - ], - parentRowSpace: 1, - parentColumnSpace: 1, - leftColumn: 0, - rightColumn: 592, - topRow: 0, - bottomRow: 280, - isLoading: false, - widgetName: "Canvas1", - renderMode: "CANVAS", - }, - ], - dynamicBindingPathList: [{ key: "selectedTab" }], - }, - ], -}; describe("Immutable Canvas structures", () => { it("generates the same object if it is run with the same dsl", () => { const nextState = compareAndGenerateImmutableCanvasStructure( @@ -356,15 +90,6 @@ describe("Immutable Canvas structures", () => { expect(nextState).toBe(canvasStructure); }); - it("calculates 100 simple diffs in less than 30ms", () => { - const start = performance.now(); - for (let i = 0; i < 100; i++) { - compareAndGenerateImmutableCanvasStructure(canvasStructure, newDSL); - } - console.log("Time taken for 100 runs: ", performance.now() - start, "ms"); - const timeTaken = performance.now() - start; - expect(timeTaken).toBeLessThanOrEqual(100); - }); it("updates the diff appropriately", () => { const dsl: any = { widgetId: "x", diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts index 9ffb3812de..cbcfacabc1 100644 --- a/app/client/src/workers/DataTreeEvaluator.ts +++ b/app/client/src/workers/DataTreeEvaluator.ts @@ -1,6 +1,5 @@ import { DependencyMap, - EntityWithBindings, EvalError, EvalErrorTypes, getDynamicBindings, @@ -17,6 +16,7 @@ import { DataTreeObjectEntity, DataTreeWidget, ENTITY_TYPE, + EvaluationSubstitutionType, } from "entities/DataTree/dataTreeFactory"; import { addDependantsOfNestedPropertyPaths, @@ -43,6 +43,7 @@ import { } from "constants/AppsmithActionConstants/ActionConstants"; import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import evaluate, { EvalResult } from "workers/evaluate"; +import { substituteDynamicBindingWithValues } from "workers/evaluationSubstitution"; export default class DataTreeEvaluator { dependencyMap: DependencyMap = {}; @@ -308,7 +309,6 @@ export default class DataTreeEvaluator { ), ); }); - // TODO make this run only for widgets and not actions dependencyMap = makeParentsDependOnChildren(dependencyMap); return dependencyMap; } @@ -331,15 +331,26 @@ export default class DataTreeEvaluator { ); }); } - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { + if (isWidget(entity)) { // Set default property dependency const defaultProperties = this.widgetConfigMap[entity.type] .defaultProperties; - Object.keys(defaultProperties).forEach((property) => { - dependencies[`${entityName}.${property}`] = [ - `${entityName}.${defaultProperties[property]}`, - ]; - }); + Object.entries(defaultProperties).forEach( + ([property, defaultPropertyPath]) => { + dependencies[`${entityName}.${property}`] = [ + `${entityName}.${defaultPropertyPath}`, + ]; + }, + ); + } + if (isAction(entity)) { + Object.entries(entity.dependencyMap).forEach( + ([dependent, entityDependencies]) => { + dependencies[`${entityName}.${dependent}`] = entityDependencies.map( + (propertyPath) => `${entityName}.${propertyPath}`, + ); + }, + ); } return dependencies; } @@ -356,7 +367,9 @@ export default class DataTreeEvaluator { const { entityName, propertyPath } = getEntityNameAndPropertyPath( fullPropertyPath, ); - const entity: DataTreeEntity = currentTree[entityName]; + const entity = currentTree[entityName] as + | DataTreeWidget + | DataTreeAction; const unEvalPropertyValue = _.get( currentTree as any, fullPropertyPath, @@ -368,10 +381,15 @@ export default class DataTreeEvaluator { const requiresEval = isABindingPath && isDynamicValue(unEvalPropertyValue); if (requiresEval) { + const evaluationSubstitutionType = + entity.bindingPaths[propertyPath] || + EvaluationSubstitutionType.TEMPLATE; try { - evalPropertyValue = this.evaluateDynamicProperty( - currentTree, + evalPropertyValue = this.getDynamicValue( unEvalPropertyValue, + currentTree, + evaluationSubstitutionType, + false, ); } catch (e) { this.errors.push({ @@ -512,6 +530,7 @@ export default class DataTreeEvaluator { getDynamicValue( dynamicBinding: string, data: DataTree, + evaluationSubstitutionType: EvaluationSubstitutionType, returnTriggers: boolean, callBackData?: Array, ) { @@ -540,10 +559,15 @@ export default class DataTreeEvaluator { } }); - // if it is just one binding, no need to create template string + // if it is just one binding, return that directly if (stringSegments.length === 1) return values[0]; - // else return a string template with bindings - return createDynamicValueString(dynamicBinding, stringSegments, values); + // else return a combined value according to the evaluation type + return substituteDynamicBindingWithValues( + dynamicBinding, + stringSegments, + values, + evaluationSubstitutionType, + ); } return undefined; } @@ -569,13 +593,6 @@ export default class DataTreeEvaluator { } } - evaluateDynamicProperty( - currentTree: DataTree, - unEvalPropertyValue: any, - ): any { - return this.getDynamicValue(unEvalPropertyValue, currentTree, false); - } - validateAndParseWidgetProperty( fullPropertyPath: string, widget: DataTreeWidget, @@ -590,6 +607,7 @@ export default class DataTreeEvaluator { const { triggers } = this.getDynamicValue( unEvalPropertyValue, currentTree, + EvaluationSubstitutionType.TEMPLATE, true, undefined, ); @@ -695,21 +713,37 @@ export default class DataTreeEvaluator { if (entityType !== "noop") { switch (dataTreeDiff.event) { case DataTreeDiffEvent.NEW: { - // If a new widget was added, add all the internal bindings for this widget to the global dependency map + // If a new entity/property was added, add all the internal bindings for this entity to the global dependency map if ( - isWidget(entity) && + (isWidget(entity) || isAction(entity)) && !this.isDynamicLeaf( unEvalDataTree, dataTreeDiff.payload.propertyPath, ) ) { - const widgetDependencyMap: DependencyMap = this.listEntityDependencies( - entity as DataTreeWidget, + const entityDependencyMap: DependencyMap = this.listEntityDependencies( + entity, entityName, ); - if (Object.keys(widgetDependencyMap).length) { + if (Object.keys(entityDependencyMap).length) { didUpdateDependencyMap = true; - Object.assign(this.dependencyMap, widgetDependencyMap); + // The entity might already have some dependencies, + // so we just want to update those + Object.entries(entityDependencyMap).forEach( + ([entityDependent, entityDependencies]) => { + if (this.dependencyMap[entityDependent]) { + this.dependencyMap[ + entityDependent + ] = this.dependencyMap[entityDependent].concat( + entityDependencies, + ); + } else { + this.dependencyMap[ + entityDependent + ] = entityDependencies; + } + }, + ); } } // Either a new entity or a new property path has been added. Go through existing dynamic bindings and @@ -735,14 +769,14 @@ export default class DataTreeEvaluator { removedPaths.push(dataTreeDiff.payload.propertyPath); // If an existing widget was deleted, remove all the bindings from the global dependency map if ( - isWidget(entity) && + (isWidget(entity) || isAction(entity)) && dataTreeDiff.payload.propertyPath === entityName ) { - const widgetBindings = this.listEntityDependencies( + const entityDependencies = this.listEntityDependencies( entity, entityName, ); - Object.keys(widgetBindings).forEach((widgetDep) => { + Object.keys(entityDependencies).forEach((widgetDep) => { didUpdateDependencyMap = true; delete this.dependencyMap[widgetDep]; }); @@ -783,23 +817,22 @@ export default class DataTreeEvaluator { } case DataTreeDiffEvent.EDIT: { - // We only care about dependencies for a widget. This is because in case a dependency of an action changes, - // that shouldn't trigger an evaluation. - // Also for a widget, we only care if the difference is in dynamic bindings since static values do not need + // We only care if the difference is in dynamic bindings since static values do not need // an evaluation. if ( - (entityType === ENTITY_TYPE.WIDGET || - entityType === ENTITY_TYPE.ACTION) && + (isWidget(entity) || isAction(entity)) && typeof dataTreeDiff.payload.value === "string" ) { - const entity: EntityWithBindings = unEvalDataTree[ + const entity: DataTreeAction | DataTreeWidget = unEvalDataTree[ entityName - ] as EntityWithBindings; + ] as DataTreeAction | DataTreeWidget; + const fullPropertyPath = dataTreeDiff.payload.propertyPath; + const entityPropertyPath = fullPropertyPath.substring( + fullPropertyPath.indexOf(".") + 1, + ); const isABindingPath = isPathADynamicBinding( entity, - dataTreeDiff.payload.propertyPath.substring( - dataTreeDiff.payload.propertyPath.indexOf(".") + 1, - ), + entityPropertyPath, ); if (isABindingPath) { didUpdateDependencyMap = true; @@ -813,15 +846,30 @@ export default class DataTreeEvaluator { // We found a new dynamic binding for this property path. We update the dependency map by overwriting the // dependencies for this property path with the newly found dependencies if (correctSnippets.length) { - this.dependencyMap[ - dataTreeDiff.payload.propertyPath - ] = correctSnippets; + this.dependencyMap[fullPropertyPath] = correctSnippets; } else { // The dependency on this property path has been removed. Delete this property path from the global // dependency map - delete this.dependencyMap[ - dataTreeDiff.payload.propertyPath - ]; + delete this.dependencyMap[fullPropertyPath]; + } + if (isAction(entity)) { + // Actions have a defined dependency map that should always be maintained + if (entityPropertyPath in entity.dependencyMap) { + const entityDependenciesName = entity.dependencyMap[ + entityPropertyPath + ].map((dep) => `${entityName}.${dep}`); + if (fullPropertyPath in this.dependencyMap) { + this.dependencyMap[ + fullPropertyPath + ] = this.dependencyMap[fullPropertyPath].concat( + entityDependenciesName, + ); + } else { + this.dependencyMap[ + fullPropertyPath + ] = entityDependenciesName; + } + } } } } @@ -838,9 +886,11 @@ export default class DataTreeEvaluator { if (didUpdateDependencyMap) { // TODO Optimise Object.keys(this.dependencyMap).forEach((key) => { - this.dependencyMap[key] = _.flatten( - this.dependencyMap[key].map((path) => - extractReferencesFromBinding(path, this.allKeys), + this.dependencyMap[key] = _.uniq( + _.flatten( + this.dependencyMap[key].map((path) => + extractReferencesFromBinding(path, this.allKeys), + ), ), ); }); @@ -1003,6 +1053,7 @@ export default class DataTreeEvaluator { evaluatedExecutionParams = this.getDynamicValue( `{{${JSON.stringify(executionParams)}}}`, this.evalTree, + EvaluationSubstitutionType.TEMPLATE, false, ); } @@ -1021,6 +1072,7 @@ export default class DataTreeEvaluator { this.getDynamicValue( `{{${binding}}}`, dataTreeWithExecutionParams, + EvaluationSubstitutionType.TEMPLATE, false, ), ); @@ -1070,31 +1122,6 @@ const extractReferencesFromBinding = ( // referencing DATA_BIND_REGEX fails for the value "{{Table1.tableData[Table1.selectedRowIndex]}}" if you run it multiple times and don't recreate const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value); -// For creating a final value where bindings could be in a template format -const createDynamicValueString = ( - binding: string, - subBindings: string[], - subValues: string[], -): string => { - // Replace the string with the data tree values - let finalValue = binding; - subBindings.forEach((b, i) => { - let value = subValues[i]; - if (Array.isArray(value) || _.isObject(value)) { - value = JSON.stringify(value); - } - try { - if (JSON.parse(value)) { - value = value.replace(/\\([\s\S])|(")/g, "\\$1$2"); - } - } catch (e) { - // do nothing - } - finalValue = finalValue.replace(b, value); - }); - return finalValue; -}; - function isValidEntity(entity: DataTreeEntity): entity is DataTreeObjectEntity { if (!_.isObject(entity)) { // ERRORS.push({ diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts index bfda076995..abffdb4d63 100644 --- a/app/client/src/workers/evaluation.test.ts +++ b/app/client/src/workers/evaluation.test.ts @@ -2,6 +2,7 @@ import { DataTreeAction, DataTreeWidget, ENTITY_TYPE, + EvaluationSubstitutionType, } from "../entities/DataTree/dataTreeFactory"; import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; import { RenderModes, WidgetTypes } from "../constants/WidgetConstants"; @@ -238,9 +239,10 @@ const BASE_ACTION: DataTreeAction = { data: {}, ENTITY_TYPE: ENTITY_TYPE.ACTION, bindingPaths: { - isLoading: true, - data: true, + isLoading: EvaluationSubstitutionType.TEMPLATE, + data: EvaluationSubstitutionType.TEMPLATE, }, + dependencyMap: {}, }; describe("DataTreeEvaluator", () => { @@ -251,7 +253,10 @@ describe("DataTreeEvaluator", () => { text: "Label", type: WidgetTypes.TEXT_WIDGET, bindingPaths: { - text: true, + text: EvaluationSubstitutionType.TEMPLATE, + }, + validationPaths: { + text: VALIDATION_TYPES.TEXT, }, }, Text2: { @@ -261,7 +266,7 @@ describe("DataTreeEvaluator", () => { dynamicBindingPathList: [{ key: "text" }], type: WidgetTypes.TEXT_WIDGET, bindingPaths: { - text: true, + text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { text: VALIDATION_TYPES.TEXT, @@ -274,7 +279,7 @@ describe("DataTreeEvaluator", () => { dynamicBindingPathList: [{ key: "text" }], type: WidgetTypes.TEXT_WIDGET, bindingPaths: { - text: true, + text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { text: VALIDATION_TYPES.TEXT, @@ -294,18 +299,18 @@ describe("DataTreeEvaluator", () => { ], type: WidgetTypes.DROP_DOWN_WIDGET, bindingPaths: { - options: true, - defaultOptionValue: true, - isRequired: true, - isVisible: true, - isDisabled: true, - isValid: true, - selectedOption: true, - selectedOptionArr: true, - selectedIndex: true, - selectedIndexArr: true, - value: true, - selectedOptionValues: true, + options: EvaluationSubstitutionType.TEMPLATE, + defaultOptionValue: EvaluationSubstitutionType.TEMPLATE, + isRequired: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + isDisabled: EvaluationSubstitutionType.TEMPLATE, + isValid: EvaluationSubstitutionType.TEMPLATE, + selectedOption: EvaluationSubstitutionType.TEMPLATE, + selectedOptionArr: EvaluationSubstitutionType.TEMPLATE, + selectedIndex: EvaluationSubstitutionType.TEMPLATE, + selectedIndexArr: EvaluationSubstitutionType.TEMPLATE, + value: EvaluationSubstitutionType.TEMPLATE, + selectedOptionValues: EvaluationSubstitutionType.TEMPLATE, }, }, Table1: { @@ -314,9 +319,9 @@ describe("DataTreeEvaluator", () => { dynamicBindingPathList: [{ key: "tableData" }], type: WidgetTypes.TABLE_WIDGET, bindingPaths: { - tableData: true, - selectedRow: true, - selectedRows: true, + tableData: EvaluationSubstitutionType.TEMPLATE, + selectedRow: EvaluationSubstitutionType.TEMPLATE, + selectedRows: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { tableData: VALIDATION_TYPES.TABLE_DATA, @@ -328,7 +333,7 @@ describe("DataTreeEvaluator", () => { dynamicBindingPathList: [{ key: "text" }], type: WidgetTypes.TEXT_WIDGET, bindingPaths: { - text: true, + text: EvaluationSubstitutionType.TEMPLATE, }, validationPaths: { text: VALIDATION_TYPES.TEXT, @@ -431,10 +436,10 @@ describe("DataTreeEvaluator", () => { widgetName: "Input1", type: WidgetTypes.INPUT_WIDGET, bindingPaths: { - defaultText: true, - isValid: true, - value: true, - text: true, + defaultText: EvaluationSubstitutionType.TEMPLATE, + isValid: EvaluationSubstitutionType.TEMPLATE, + value: EvaluationSubstitutionType.TEMPLATE, + text: EvaluationSubstitutionType.TEMPLATE, }, }, }; @@ -460,18 +465,18 @@ describe("DataTreeEvaluator", () => { ], type: WidgetTypes.DROP_DOWN_WIDGET, bindingPaths: { - options: true, - defaultOptionValue: true, - isRequired: true, - isVisible: true, - isDisabled: true, - isValid: true, - selectedOption: true, - selectedOptionArr: true, - selectedIndex: true, - selectedIndexArr: true, - value: true, - selectedOptionValues: true, + options: EvaluationSubstitutionType.TEMPLATE, + defaultOptionValue: EvaluationSubstitutionType.TEMPLATE, + isRequired: EvaluationSubstitutionType.TEMPLATE, + isVisible: EvaluationSubstitutionType.TEMPLATE, + isDisabled: EvaluationSubstitutionType.TEMPLATE, + isValid: EvaluationSubstitutionType.TEMPLATE, + selectedOption: EvaluationSubstitutionType.TEMPLATE, + selectedOptionArr: EvaluationSubstitutionType.TEMPLATE, + selectedIndex: EvaluationSubstitutionType.TEMPLATE, + selectedIndexArr: EvaluationSubstitutionType.TEMPLATE, + value: EvaluationSubstitutionType.TEMPLATE, + selectedOptionValues: EvaluationSubstitutionType.TEMPLATE, }, }, }; @@ -604,4 +609,86 @@ describe("DataTreeEvaluator", () => { "Text4.text": ["Table1.selectedRow.test"], }); }); + + it("Honors predefined action dependencyMap", () => { + const updatedTree1 = { + ...unEvalTree, + Text1: { + ...BASE_WIDGET, + text: "Test", + }, + Api2: { + ...BASE_ACTION, + dependencyMap: { + "config.body": ["config.pluginSpecifiedTemplates[0].value"], + }, + bindingPaths: { + ...BASE_ACTION.bindingPaths, + "config.body": EvaluationSubstitutionType.TEMPLATE, + }, + config: { + ...BASE_ACTION.config, + body: "", + pluginSpecifiedTemplates: [ + { + value: false, + }, + ], + }, + }, + }; + evaluator.updateDataTree(updatedTree1); + expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ + "Api2.config.pluginSpecifiedTemplates[0].value", + ]); + const updatedTree2 = { + ...updatedTree1, + Api2: { + ...updatedTree1.Api2, + dynamicBindingPathList: [ + { + key: "config.body", + }, + ], + config: { + ...updatedTree1.Api2.config, + body: "{ 'name': {{ Text1.text }} }", + }, + }, + }; + const evaluatedDataTree2 = evaluator.updateDataTree(updatedTree2); + expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ + "Text1.text", + "Api2.config.pluginSpecifiedTemplates[0].value", + ]); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(evaluatedDataTree2.Api2.config.body).toBe("{ 'name': Test }"); + const updatedTree3 = { + ...updatedTree2, + Api2: { + ...updatedTree2.Api2, + bindingPaths: { + ...updatedTree2.Api2.bindingPaths, + "config.body": EvaluationSubstitutionType.SMART_SUBSTITUTE, + }, + config: { + ...updatedTree2.Api2.config, + pluginSpecifiedTemplates: [ + { + value: true, + }, + ], + }, + }, + }; + const evaluatedDataTree3 = evaluator.updateDataTree(updatedTree3); + expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([ + "Text1.text", + "Api2.config.pluginSpecifiedTemplates[0].value", + ]); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(evaluatedDataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }"); + }); }); diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts index 85a7399224..467acce726 100644 --- a/app/client/src/workers/evaluation.worker.ts +++ b/app/client/src/workers/evaluation.worker.ts @@ -1,4 +1,7 @@ -import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { + DataTree, + EvaluationSubstitutionType, +} from "entities/DataTree/dataTreeFactory"; import { DependencyMap, EVAL_WORKER_ACTIONS, @@ -109,6 +112,7 @@ ctx.addEventListener( const triggers = dataTreeEvaluator.getDynamicValue( dynamicTrigger, evalTree, + EvaluationSubstitutionType.TEMPLATE, true, callbackData, ); diff --git a/app/client/src/workers/evaluationSubstitution.test.ts b/app/client/src/workers/evaluationSubstitution.test.ts new file mode 100644 index 0000000000..7c94f46eab --- /dev/null +++ b/app/client/src/workers/evaluationSubstitution.test.ts @@ -0,0 +1,339 @@ +import { substituteDynamicBindingWithValues } from "workers/evaluationSubstitution"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; + +describe("substituteDynamicBindingWithValues", () => { + describe("template substitution", () => { + it("substitutes strings values", () => { + const binding = "Hello {{name}}"; + const subBindings = ["Hello ", "{{name}}"]; + const subValues = ["Hello ", "Tester"]; + const expected = "Hello Tester"; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.TEMPLATE, + ); + + expect(result).toBe(expected); + }); + + it("substitute number values", () => { + const binding = "My age is {{age}}"; + const subBindings = ["My age is ", "{{age}}"]; + const subValues = ["My age is ", 16]; + const expected = "My age is 16"; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.TEMPLATE, + ); + + expect(result).toBe(expected); + }); + + it("substitute objects/ arrays values", () => { + const binding = "Response was {{response}}"; + const subBindings = ["Response was ", "{{response}}"]; + const subValues = ["Response was ", { message: "Unauthorised user" }]; + const expected = 'Response was {\\"message\\":\\"Unauthorised user\\"}'; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.TEMPLATE, + ); + + expect(result).toBe(expected); + }); + + it("substitute multiple values values", () => { + const binding = + "My name is {{name}}. My age is {{age}}. Response: {{response}}"; + const subBindings = [ + "My name is ", + "{{name}}", + ". My age is ", + "{{age}}", + ". Response: ", + "{{response}}", + ]; + const subValues = [ + "My name is ", + "Tester", + ". My age is ", + 16, + ". Response: ", + { message: "Unauthorised user" }, + ]; + const expected = + 'My name is Tester. My age is 16. Response: {\\"message\\":\\"Unauthorised user\\"}'; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.TEMPLATE, + ); + + expect(result).toBe(expected); + }); + }); + describe("parameter substitution", () => { + it("replaces bindings with $variables", () => { + const binding = "SELECT * from {{tableName}} LIMIT {{limit}}"; + const subBindings = [ + "SELECT * from ", + "{{tableName}}", + " LIMIT ", + "{{limit}}", + ]; + const subValues = ["SELECT * from ", "users", " LIMIT ", 10]; + const expected = { + value: "SELECT * from $1 LIMIT $2", + parameters: { + $1: "users", + $2: 10, + }, + }; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.PARAMETER, + ); + + expect(result).toHaveProperty("value"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(result.value).toBe(expected.value); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(result.parameters).toStrictEqual(expected.parameters); + }); + + it("removed quotes around bindings", () => { + const binding = + 'SELECT * from users WHERE lastname = "{{lastname}}" LIMIT {{limit}}'; + const subBindings = [ + 'SELECT * from users WHERE lastname = "', + "{{lastname}}", + '" LIMIT ', + "{{limit}}", + ]; + const subValues = [ + 'SELECT * from users WHERE lastname = "', + "Smith", + '" LIMIT ', + 10, + ]; + const expected = { + value: "SELECT * from users WHERE lastname = $1 LIMIT $2", + parameters: { + $1: "Smith", + $2: 10, + }, + }; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.PARAMETER, + ); + + expect(result).toHaveProperty("value"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(result.value).toBe(expected.value); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(result.parameters).toStrictEqual(expected.parameters); + }); + + it("stringifies objects and arrays", () => { + const binding = "SELECT * from {{testObject}} WHERE {{testArray}}"; + const subBindings = [ + "SELECT * from ", + "{{testObject}}", + " WHERE ", + "{{testArray}}", + ]; + const subValues = [ + "SELECT * from ", + { name: "tester" }, + " WHERE ", + [42, "meaning", false], + ]; + const expected = { + value: "SELECT * from $1 WHERE $2", + parameters: { + $1: `{\n \"name\": \"tester\"\n}`, + $2: `[\n 42,\n \"meaning\",\n false\n]`, + }, + }; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.PARAMETER, + ); + + expect(result).toHaveProperty("value"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(result.value).toBe(expected.value); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(result.parameters).toStrictEqual(expected.parameters); + }); + }); + describe("smart substitution", () => { + it("substitutes strings, numbers, boolean, undefined, null values correctly", () => { + const binding = `{ + "name": {{name}}, + "age": {{age}}, + "isHuman": {{isHuman}}, + "wrongBinding": {{wrongBinding}}, + "emptyBinding": {{emptyBinding}}, + }`; + const subBindings = [ + '{\n "name": ', + "{{name}}", + ',\n "age": ', + "{{age}}", + ',\n "isHuman": ', + "{{isHuman}}", + ',\n "wrongBinding": ', + "{{wrongBinding}}", + ',\n "emptyBinding": ', + "{{emptyBinding}}", + ",\n }", + ]; + const subValues = [ + '{\n "name": ', + "Tester", + ',\n "age": ', + 42, + ',\n "isHuman": ', + false, + ',\n "wrongBinding": ', + undefined, + ',\n "emptyBinding": ', + null, + ",\n }", + ]; + const expected = `{ + "name": "Tester", + "age": 42, + "isHuman": false, + "wrongBinding": undefined, + "emptyBinding": null, + }`; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.SMART_SUBSTITUTE, + ); + + expect(result).toBe(expected); + }); + + it("substitute objects/ arrays values", () => { + const binding = `{\n "data": {{formData}}\n}`; + const subBindings = ["{\n data: ", "{{formData}}", "\n}"]; + const subValues = [ + '{\n "data": ', + { + name: "Tester", + age: 42, + isHuman: false, + wrongBinding: undefined, + emptyBinding: null, + }, + "\n}", + ]; + const expected = + '{\n "data": {\n "name": "Tester",\n "age": 42,\n "isHuman": false,\n "emptyBinding": null\n}\n}'; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.SMART_SUBSTITUTE, + ); + + expect(result).toBe(expected); + }); + + it("substitute correctly when quotes are surrounding the binding", () => { + const binding = `{ + "name": "{{name}}", + "age": "{{age}}", + isHuman: {{isHuman}}, + "wrongBinding": {{wrongBinding}}, + "emptyBinding": "{{emptyBinding}}", + }`; + const subBindings = [ + '{\n "name": "', + "{{name}}", + '",\n "age": "', + "{{age}}", + '",\n isHuman: ', + "{{isHuman}}", + ',\n "wrongBinding": ', + "{{wrongBinding}}", + ',\n "emptyBinding": "', + "{{emptyBinding}}", + '",\n }', + ]; + const subValues = [ + '{\n "name": "', + "Tester", + '",\n "age": "', + 42, + '",\n isHuman: ', + false, + ',\n "wrongBinding": ', + undefined, + ',\n "emptyBinding": "', + null, + '",\n }', + ]; + const expected = `{ + "name": "Tester", + "age": 42, + isHuman: false, + "wrongBinding": undefined, + "emptyBinding": null, + }`; + debugger; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.SMART_SUBSTITUTE, + ); + + expect(result).toBe(expected); + }); + + it("escapes strings before substitution", () => { + const binding = `{\n "paragraph": {{paragraph}},\n}`; + const subBindings = ['{\n "paragraph": ', "{{paragraph}}", ",\n}"]; + const subValues = [ + '{\n "paragraph": ', + `This is a \f string \b with \n many different " characters that are not \n all. these \r\t`, + ",\n}", + ]; + const expected = `{\n "paragraph": "This is a \\f string \\b with \\n many different \\" characters that are not \\n all. these \\r\\t",\n}`; + const result = substituteDynamicBindingWithValues( + binding, + subBindings, + subValues, + EvaluationSubstitutionType.SMART_SUBSTITUTE, + ); + + expect(result).toBe(expected); + }); + }); +}); diff --git a/app/client/src/workers/evaluationSubstitution.ts b/app/client/src/workers/evaluationSubstitution.ts new file mode 100644 index 0000000000..477cc180d0 --- /dev/null +++ b/app/client/src/workers/evaluationSubstitution.ts @@ -0,0 +1,147 @@ +import { getType, Types } from "utils/TypeHelpers"; +import _ from "lodash"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; +import { isDynamicValue } from "utils/DynamicBindingUtils"; +import { QUOTED_BINDING_REGEX } from "constants/BindingsConstants"; + +const filterBindingSegmentsAndRemoveQuotes = ( + binding: string, + subSegments: string[], + subSegmentValues: unknown[], +) => { + const bindingStrippedQuotes = binding.replace( + QUOTED_BINDING_REGEX, + (original, firstGroup) => { + return firstGroup; + }, + ); + const subBindings: string[] = []; + const subValues: unknown[] = []; + subSegments.forEach((segment, i) => { + if (isDynamicValue(segment)) { + subBindings.push(segment); + subValues.push(subSegmentValues[i]); + } + }); + return { binding: bindingStrippedQuotes, subBindings, subValues }; +}; + +export const smartSubstituteDynamicValues = ( + originalBinding: string, + subSegments: string[], + subSegmentValues: unknown[], +): string => { + const { + binding, + subValues, + subBindings, + } = filterBindingSegmentsAndRemoveQuotes( + originalBinding, + subSegments, + subSegmentValues, + ); + let finalBinding = binding; + subBindings.forEach((b, i) => { + const value = subValues[i]; + switch (getType(value)) { + case Types.NUMBER: + case Types.BOOLEAN: + case Types.NULL: + case Types.UNDEFINED: + // Direct substitution + finalBinding = finalBinding.replace(b, `${value}`); + break; + case Types.STRING: + // Add quotes to a string + // JSON.stringify string to escape any unsupported characters + finalBinding = finalBinding.replace(b, `${JSON.stringify(value)}`); + break; + case Types.ARRAY: + case Types.OBJECT: + // Stringify and substitute + finalBinding = finalBinding.replace(b, JSON.stringify(value, null, 2)); + break; + } + }); + return finalBinding; +}; + +export const parameterSubstituteDynamicValues = ( + originalBinding: string, + subSegments: string[], + subSegmentValues: unknown[], +) => { + const { + binding, + subValues, + subBindings, + } = filterBindingSegmentsAndRemoveQuotes( + originalBinding, + subSegments, + subSegmentValues, + ); + let finalBinding = binding; + const parameters: Record = {}; + subBindings.forEach((b, i) => { + // Replace binding with $1, $2; + const key = `$${i + 1}`; + finalBinding = finalBinding.replace(b, key); + parameters[key] = + typeof subValues[i] === "object" + ? JSON.stringify(subValues[i], null, 2) + : subValues[i]; + }); + return { value: finalBinding, parameters }; +}; +// For creating a final value where bindings could be in a template format +export const templateSubstituteDynamicValues = ( + binding: string, + subBindings: string[], + subValues: unknown[], +): string => { + // Replace the string with the data tree values + let finalValue = binding; + subBindings.forEach((b, i) => { + let value = subValues[i]; + if (Array.isArray(value) || _.isObject(value)) { + value = JSON.stringify(value); + } + try { + if (typeof value === "string" && JSON.parse(value)) { + value = value.replace(/\\([\s\S])|(")/g, "\\$1$2"); + } + } catch (e) { + // do nothing + } + finalValue = finalValue.replace(b, `${value}`); + }); + return finalValue; +}; + +export const substituteDynamicBindingWithValues = ( + binding: string, + subSegments: string[], + subSegmentValues: unknown[], + evaluationSubstitutionType: EvaluationSubstitutionType, +): string | { value: string; parameters: Record } => { + switch (evaluationSubstitutionType) { + case EvaluationSubstitutionType.TEMPLATE: + return templateSubstituteDynamicValues( + binding, + subSegments, + subSegmentValues, + ); + case EvaluationSubstitutionType.SMART_SUBSTITUTE: + return smartSubstituteDynamicValues( + binding, + subSegments, + subSegmentValues, + ); + case EvaluationSubstitutionType.PARAMETER: + return parameterSubstituteDynamicValues( + binding, + subSegments, + subSegmentValues, + ); + } +}; diff --git a/app/client/src/workers/evaluationUtils.test.ts b/app/client/src/workers/evaluationUtils.test.ts index a5a9e2ef73..05127e1bc1 100644 --- a/app/client/src/workers/evaluationUtils.test.ts +++ b/app/client/src/workers/evaluationUtils.test.ts @@ -56,6 +56,7 @@ describe("Add functions", () => { isLoading: false, run: {}, ENTITY_TYPE: ENTITY_TYPE.ACTION, + dependencyMap: {}, }, }; const dataTreeWithFunctions = addFunctions(dataTree);