Introduce different evaluation types for different binding fields (#3834)

This commit is contained in:
Hetu Nandu 2021-04-26 11:11:32 +05:30 committed by GitHub
parent 05e935b0a0
commit c92eb0e5a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1300 additions and 686 deletions

View File

@ -3,6 +3,8 @@ import {
ReduxActionTypes, ReduxActionTypes,
ReduxAction, ReduxAction,
ReduxActionErrorTypes, ReduxActionErrorTypes,
EvaluationReduxAction,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { Action } from "entities/Action"; import { Action } from "entities/Action";
import { batchAction } from "actions/batchActions"; import { batchAction } from "actions/batchActions";
@ -27,10 +29,12 @@ export type FetchActionsPayload = {
export const fetchActions = ( export const fetchActions = (
applicationId: string, applicationId: string,
): ReduxAction<FetchActionsPayload> => { postEvalActions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
): EvaluationReduxAction<unknown> => {
return { return {
type: ReduxActionTypes.FETCH_ACTIONS_INIT, type: ReduxActionTypes.FETCH_ACTIONS_INIT,
payload: { applicationId }, payload: { applicationId },
postEvalActions,
}; };
}; };

View File

@ -3,6 +3,7 @@ import {
EvaluationReduxAction, EvaluationReduxAction,
ReduxAction, ReduxAction,
ReduxActionTypes, ReduxActionTypes,
ReduxActionWithoutPayload,
UpdateCanvasPayload, UpdateCanvasPayload,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
@ -46,18 +47,14 @@ export const fetchPublishedPage = (pageId: string, bustCache = false) => ({
}, },
}); });
export const fetchPageSuccess = ( export const fetchPageSuccess = (): ReduxActionWithoutPayload => {
postEvalActions: ReduxAction<unknown>[],
): EvaluationReduxAction<unknown> => {
return { return {
type: ReduxActionTypes.FETCH_PAGE_SUCCESS, type: ReduxActionTypes.FETCH_PAGE_SUCCESS,
payload: {},
postEvalActions,
}; };
}; };
export const fetchPublishedPageSuccess = ( export const fetchPublishedPageSuccess = (
postEvalActions: ReduxAction<unknown>[], postEvalActions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
): EvaluationReduxAction<undefined> => ({ ): EvaluationReduxAction<undefined> => ({
type: ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS, type: ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS,
postEvalActions, postEvalActions,

View File

@ -4,6 +4,7 @@ import {
ReduxActionWithoutPayload, ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { PluginFormPayload } from "api/PluginApi"; import { PluginFormPayload } from "api/PluginApi";
import { DependencyMap } from "utils/DynamicBindingUtils";
export const fetchPlugins = (): ReduxActionWithoutPayload => ({ export const fetchPlugins = (): ReduxActionWithoutPayload => ({
type: ReduxActionTypes.FETCH_PLUGINS_REQUEST, type: ReduxActionTypes.FETCH_PLUGINS_REQUEST,
@ -17,6 +18,7 @@ export type PluginFormsPayload = {
formConfigs: Record<string, any[]>; formConfigs: Record<string, any[]>;
editorConfigs: Record<string, any[]>; editorConfigs: Record<string, any[]>;
settingConfigs: Record<string, any[]>; settingConfigs: Record<string, any[]>;
dependencies: Record<string, DependencyMap>;
}; };
export const fetchPluginFormConfigsSuccess = ( export const fetchPluginFormConfigsSuccess = (

View File

@ -2,11 +2,11 @@ import {
ReduxActionTypes, ReduxActionTypes,
ReduxAction, ReduxAction,
ReduxActionErrorTypes, ReduxActionErrorTypes,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { import {
ExecuteActionPayload, ExecuteActionPayload,
ExecuteErrorPayload, ExecuteErrorPayload,
PageAction,
} from "constants/AppsmithActionConstants/ActionConstants"; } from "constants/AppsmithActionConstants/ActionConstants";
import { BatchAction, batchAction } from "actions/batchActions"; import { BatchAction, batchAction } from "actions/batchActions";
import PerformanceTracker, { import PerformanceTracker, {
@ -30,11 +30,8 @@ export const executeActionError = (
}; };
}; };
export const executePageLoadActions = ( export const executePageLoadActions = (): ReduxActionWithoutPayload => ({
payload: PageAction[][],
): ReduxAction<PageAction[][]> => ({
type: ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS, type: ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS,
payload,
}); });
export const disableDragAction = ( export const disableDragAction = (

View File

@ -2,6 +2,7 @@ import Api from "api/Api";
import { AxiosPromise } from "axios"; import { AxiosPromise } from "axios";
import { GenericApiResponse } from "api/ApiResponses"; import { GenericApiResponse } from "api/ApiResponses";
import { PluginType } from "entities/Action"; import { PluginType } from "entities/Action";
import { DependencyMap } from "utils/DynamicBindingUtils";
export interface Plugin { export interface Plugin {
id: string; id: string;
@ -21,6 +22,7 @@ export interface PluginFormPayload {
form: any[]; form: any[];
editor: any[]; editor: any[];
setting: any[]; setting: any[];
dependencies: DependencyMap;
} }
class PluginsApi extends Api { class PluginsApi extends Api {

View File

@ -8,6 +8,9 @@ import { theme } from "constants/DefaultTheme";
import { Placement } from "popper.js"; import { Placement } from "popper.js";
import ScrollIndicator from "components/ads/ScrollIndicator"; import ScrollIndicator from "components/ads/ScrollIndicator";
import DebugButton from "components/editorComponents/Debugger/DebugCTA"; 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` const Wrapper = styled.div`
position: relative; position: relative;
@ -112,6 +115,7 @@ interface Props {
error?: string; error?: string;
useValidationMessage?: boolean; useValidationMessage?: boolean;
hideEvaluatedValue?: boolean; hideEvaluatedValue?: boolean;
evaluationSubstitutionType?: EvaluationSubstitutionType;
} }
interface PopoverContentProps { interface PopoverContentProps {
@ -124,12 +128,56 @@ interface PopoverContentProps {
onMouseEnter: () => void; onMouseEnter: () => void;
onMouseLeave: () => void; onMouseLeave: () => void;
hideEvaluatedValue?: boolean; 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<string, number | string>;
};
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) => (
<Tooltip content={<span>{parameters[param]}</span>} key={param}>
<PreparedStatementParameter key={param}>
{param}
</PreparedStatementParameter>
</Tooltip>
));
return (
<PreparedStatementViewerContainer>
{stringSegments.map((segment, index) => (
<span key={segment}>
{segment}
{paramsWithTooltips[index]}
</span>
))}
</PreparedStatementViewerContainer>
);
};
export const CurrentValueViewer = (props: { export const CurrentValueViewer = (props: {
theme: EditorTheme; theme: EditorTheme;
evaluatedValue: any; evaluatedValue: any;
hideLabel?: boolean; hideLabel?: boolean;
preparedStatementViewer?: boolean;
}) => { }) => {
const currentValueWrapperRef = React.createRef<HTMLDivElement>(); const currentValueWrapperRef = React.createRef<HTMLDivElement>();
const codeWrapperRef = React.createRef<HTMLPreElement>(); const codeWrapperRef = React.createRef<HTMLPreElement>();
@ -145,19 +193,31 @@ export const CurrentValueViewer = (props: {
_.isObject(props.evaluatedValue) || _.isObject(props.evaluatedValue) ||
Array.isArray(props.evaluatedValue) Array.isArray(props.evaluatedValue)
) { ) {
const reactJsonProps = { if (props.preparedStatementViewer) {
theme: props.theme === EditorTheme.DARK ? "summerfruit" : "rjv-default", content = (
name: null, <CodeWrapper colorTheme={props.theme} ref={codeWrapperRef}>
enableClipboard: false, <PreparedStatementViewer
displayObjectSize: false, evaluatedValue={props.evaluatedValue as PreparedStatementValue}
displayDataTypes: false, />
style: { <ScrollIndicator containerRef={codeWrapperRef} />
fontSize: "12px", </CodeWrapper>
}, );
collapsed: 2, } else {
collapseStringsAfterLength: 20, const reactJsonProps = {
}; theme:
content = <ReactJson src={props.evaluatedValue} {...reactJsonProps} />; props.theme === EditorTheme.DARK ? "summerfruit" : "rjv-default",
name: null,
enableClipboard: false,
displayObjectSize: false,
displayDataTypes: false,
style: {
fontSize: "12px",
},
collapsed: 2,
collapseStringsAfterLength: 20,
};
content = <ReactJson src={props.evaluatedValue} {...reactJsonProps} />;
}
} else { } else {
content = ( content = (
<CodeWrapper colorTheme={props.theme} ref={codeWrapperRef}> <CodeWrapper colorTheme={props.theme} ref={codeWrapperRef}>
@ -222,6 +282,7 @@ const PopoverContent = (props: PopoverContentProps) => {
<CurrentValueViewer <CurrentValueViewer
theme={props.theme} theme={props.theme}
evaluatedValue={props.evaluatedValue} evaluatedValue={props.evaluatedValue}
preparedStatementViewer={props.preparedStatementViewer}
/> />
)} )}
</ContentWrapper> </ContentWrapper>
@ -246,7 +307,7 @@ const EvaluatedValuePopup = (props: Props) => {
<Popper <Popper
targetNode={wrapperRef.current || undefined} targetNode={wrapperRef.current || undefined}
isOpen isOpen
zIndex={15} zIndex={5}
placement={placement} placement={placement}
modifiers={{ modifiers={{
offset: { offset: {
@ -263,6 +324,12 @@ const EvaluatedValuePopup = (props: Props) => {
hasError={props.hasError} hasError={props.hasError}
theme={props.theme} theme={props.theme}
hideEvaluatedValue={props.hideEvaluatedValue} hideEvaluatedValue={props.hideEvaluatedValue}
preparedStatementViewer={
props.evaluationSubstitutionType
? props.evaluationSubstitutionType ===
EvaluationSubstitutionType.PARAMETER
: false
}
onMouseLeave={() => { onMouseLeave={() => {
setContentHovered(false); setContentHovered(false);
}} }}

View File

@ -16,7 +16,10 @@ import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup"; import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import _ from "lodash"; import _ from "lodash";
import { DataTree } from "entities/DataTree/dataTreeFactory"; import {
DataTree,
EvaluationSubstitutionType,
} from "entities/DataTree/dataTreeFactory";
import { Skin } from "constants/DefaultTheme"; import { Skin } from "constants/DefaultTheme";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import "components/editorComponents/CodeEditor/modes"; import "components/editorComponents/CodeEditor/modes";
@ -84,6 +87,7 @@ export type EditorStyleProps = {
hoverInteraction?: boolean; hoverInteraction?: boolean;
fill?: boolean; fill?: boolean;
useValidationMessage?: boolean; useValidationMessage?: boolean;
evaluationSubstitutionType?: EvaluationSubstitutionType;
}; };
export type EditorProps = EditorStyleProps & export type EditorProps = EditorStyleProps &
@ -352,6 +356,7 @@ class CodeEditor extends Component<Props, State> {
fill, fill,
useValidationMessage, useValidationMessage,
hideEvaluatedValue, hideEvaluatedValue,
evaluationSubstitutionType,
} = this.props; } = this.props;
const hasError = !!(meta && meta.error); const hasError = !!(meta && meta.error);
let evaluated = evaluatedValue; let evaluated = evaluatedValue;
@ -398,6 +403,7 @@ class CodeEditor extends Component<Props, State> {
error={meta?.error} error={meta?.error}
useValidationMessage={useValidationMessage} useValidationMessage={useValidationMessage}
hideEvaluatedValue={hideEvaluatedValue} hideEvaluatedValue={hideEvaluatedValue}
evaluationSubstitutionType={evaluationSubstitutionType}
> >
<EditorWrapper <EditorWrapper
editorTheme={this.props.theme} editorTheme={this.props.theme}

View File

@ -6,6 +6,7 @@ import { mockCodemirrorRender } from "test/__mocks__/CodeMirrorEditorMock";
import { PluginType } from "entities/Action"; import { PluginType } from "entities/Action";
import { waitFor } from "@testing-library/dom"; import { waitFor } from "@testing-library/dom";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
const TestForm = (props: any) => <div>{props.children}</div>; const TestForm = (props: any) => <div>{props.children}</div>;
@ -33,6 +34,7 @@ describe("DynamicTextFieldControl", () => {
id={"test"} id={"test"}
isValid={true} isValid={true}
pluginId="123" pluginId="123"
evaluationSubstitutionType={EvaluationSubstitutionType.TEMPLATE}
/> />
</ReduxFormDecorator>, </ReduxFormDecorator>,
{ {

View File

@ -22,6 +22,7 @@ import {
getQueryParams, getQueryParams,
} from "utils/AppsmithUtils"; } from "utils/AppsmithUtils";
import { actionPathFromName } from "components/formControls/utils"; import { actionPathFromName } from "components/formControls/utils";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
const Wrapper = styled.div` const Wrapper = styled.div`
.dynamic-text-field { .dynamic-text-field {
@ -64,6 +65,7 @@ class DynamicTextControl extends BaseControl<
placeholderText, placeholderText,
actionName, actionName,
configProperty, configProperty,
evaluationSubstitutionType,
} = this.props; } = this.props;
const dataTreePath = actionPathFromName(actionName, configProperty); const dataTreePath = actionPathFromName(actionName, configProperty);
const isNewQuery = const isNewQuery =
@ -104,6 +106,7 @@ class DynamicTextControl extends BaseControl<
mode={mode} mode={mode}
tabBehaviour={TabBehaviour.INDENT} tabBehaviour={TabBehaviour.INDENT}
placeholder={placeholderText} placeholder={placeholderText}
evaluationSubstitutionType={evaluationSubstitutionType}
/> />
)} )}
</Wrapper> </Wrapper>
@ -117,6 +120,7 @@ export interface DynamicTextFieldProps extends ControlProps {
pluginId: string; pluginId: string;
responseType: string; responseType: string;
placeholderText?: string; placeholderText?: string;
evaluationSubstitutionType: EvaluationSubstitutionType;
} }
const mapStateToProps = (state: AppState, props: DynamicTextFieldProps) => { const mapStateToProps = (state: AppState, props: DynamicTextFieldProps) => {

View File

@ -33,6 +33,13 @@ export const DEFAULT_API_ACTION_CONFIG: ApiActionConfig = {
httpMethod: HTTP_METHODS[0], httpMethod: HTTP_METHODS[0],
headers: EMPTY_KEY_VALUE_PAIRS.slice(), headers: EMPTY_KEY_VALUE_PAIRS.slice(),
queryParameters: 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"; export const DEFAULT_PROVIDER_OPTION = "Business Software";

View File

@ -5,6 +5,7 @@ import queryActionSettingsConfig from "constants/AppsmithActionConstants/formCon
import apiActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/ApiSettingsConfig"; import apiActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/ApiSettingsConfig";
import apiActionEditorConfig from "constants/AppsmithActionConstants/formConfig/ApiEditorConfigs"; import apiActionEditorConfig from "constants/AppsmithActionConstants/formConfig/ApiEditorConfigs";
import saasActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/GoogleSheetsSettingsConfig"; import saasActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/GoogleSheetsSettingsConfig";
import apiActionDependencyConfig from "constants/AppsmithActionConstants/formConfig/ApiDependencyConfigs";
export type ExecuteActionPayloadEvent = { export type ExecuteActionPayloadEvent = {
type: EventType; type: EventType;
@ -123,3 +124,12 @@ export const defaultActionEditorConfigs: Record<PluginType, any> = {
[PluginType.DB]: [], [PluginType.DB]: [],
[PluginType.SAAS]: [], [PluginType.SAAS]: [],
}; };
export const defaultActionDependenciesConfig: Record<
PluginType,
Record<string, string[]>
> = {
[PluginType.API]: apiActionDependencyConfig,
[PluginType.DB]: {},
[PluginType.SAAS]: {},
};

View File

@ -1,9 +1,5 @@
export default [ export default {
{ "actionConfiguration.body": [
dependencies: { "actionConfiguration.pluginSpecifiedTemplates[0].value",
"actionConfiguration.body": [ ],
"actionConfiguration.pluginSpecifiedTemplates[0].value", };
],
},
},
];

View File

@ -12,6 +12,23 @@ export default [
label: "Body", label: "Body",
configProperty: "actionConfiguration.body", configProperty: "actionConfiguration.body",
controlType: "QUERY_DYNAMIC_INPUT_TEXT", 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", label: "Query Parameters",

View File

@ -1,3 +1,4 @@
export const DATA_BIND_REGEX = /{{([\s\S]*?)}}/; export const DATA_BIND_REGEX = /{{([\s\S]*?)}}/;
export const DATA_BIND_REGEX_GLOBAL = /{{([\s\S]*?)}}/g; export const DATA_BIND_REGEX_GLOBAL = /{{([\s\S]*?)}}/g;
export const AUTOCOMPLETE_MATCH_REGEX = /{{\s*.*?\s*}}/g; export const AUTOCOMPLETE_MATCH_REGEX = /{{\s*.*?\s*}}/g;
export const QUOTED_BINDING_REGEX = /["']({{[\s\S]*?}})["']/g;

View File

@ -497,7 +497,7 @@ export interface ReduxActionWithCallbacks<T, S, E> extends ReduxAction<T> {
} }
export interface EvaluationReduxAction<T> extends ReduxAction<T> { export interface EvaluationReduxAction<T> extends ReduxAction<T> {
postEvalActions?: ReduxAction<any>[]; postEvalActions?: Array<ReduxAction<any> | ReduxActionWithoutPayload>;
} }
export interface PromisePayload { export interface PromisePayload {

View File

@ -1,5 +1,6 @@
import { Action, PluginType } from "entities/Action/index"; import { Action, PluginType } from "entities/Action/index";
import { getBindingPathsOfAction } from "entities/Action/actionProperties"; import { getBindingPathsOfAction } from "entities/Action/actionProperties";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
const DEFAULT_ACTION: Action = { const DEFAULT_ACTION: Action = {
actionConfiguration: {}, actionConfiguration: {},
@ -24,9 +25,9 @@ describe("getBindingPathsOfAction", () => {
it("returns default list of no config is sent", () => { it("returns default list of no config is sent", () => {
const response = getBindingPathsOfAction(DEFAULT_ACTION, undefined); const response = getBindingPathsOfAction(DEFAULT_ACTION, undefined);
expect(response).toStrictEqual({ expect(response).toStrictEqual({
data: true, data: EvaluationSubstitutionType.TEMPLATE,
isLoading: true, isLoading: EvaluationSubstitutionType.TEMPLATE,
config: true, config: EvaluationSubstitutionType.TEMPLATE,
}); });
}); });
@ -46,6 +47,18 @@ describe("getBindingPathsOfAction", () => {
configProperty: "actionConfiguration.body2", configProperty: "actionConfiguration.body2",
controlType: "QUERY_DYNAMIC_INPUT_TEXT", 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: { actionConfiguration: {
body: "basic action", body: "basic action",
body2: "another body", body2: "another body",
field1: "test",
field2: "anotherTest",
}, },
}; };
const response = getBindingPathsOfAction(basicAction, config); const response = getBindingPathsOfAction(basicAction, config);
expect(response).toStrictEqual({ expect(response).toStrictEqual({
data: true, data: EvaluationSubstitutionType.TEMPLATE,
isLoading: true, isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.body": true, "config.body": EvaluationSubstitutionType.TEMPLATE,
"config.body2": true, "config.body2": EvaluationSubstitutionType.TEMPLATE,
"config.field1": EvaluationSubstitutionType.SMART_SUBSTITUTE,
"config.field2": EvaluationSubstitutionType.PARAMETER,
}); });
}); });
@ -122,12 +139,12 @@ describe("getBindingPathsOfAction", () => {
// @ts-ignore // @ts-ignore
const response = getBindingPathsOfAction(basicAction, config); const response = getBindingPathsOfAction(basicAction, config);
expect(response).toStrictEqual({ expect(response).toStrictEqual({
data: true, data: EvaluationSubstitutionType.TEMPLATE,
isLoading: true, isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.params[0].key": true, "config.params[0].key": EvaluationSubstitutionType.TEMPLATE,
"config.params[0].value": true, "config.params[0].value": EvaluationSubstitutionType.TEMPLATE,
"config.params[1].key": true, "config.params[1].key": EvaluationSubstitutionType.TEMPLATE,
"config.params[1].value": true, "config.params[1].value": EvaluationSubstitutionType.TEMPLATE,
}); });
}); });
@ -177,10 +194,76 @@ describe("getBindingPathsOfAction", () => {
// @ts-ignore // @ts-ignore
const response = getBindingPathsOfAction(basicAction, config); const response = getBindingPathsOfAction(basicAction, config);
expect(response).toStrictEqual({ expect(response).toStrictEqual({
data: true, data: EvaluationSubstitutionType.TEMPLATE,
isLoading: true, isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.key": true, "config.key": EvaluationSubstitutionType.TEMPLATE,
"config.value": true, "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,
}); });
}); });
}); });

View File

@ -1,32 +1,46 @@
import { Action } from "entities/Action/index"; import { Action } from "entities/Action/index";
import _ from "lodash"; 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 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 = ( export const getBindingPathsOfAction = (
action: Action, action: Action,
formConfig?: any[], formConfig?: any[],
): Record<string, true> => { ): Record<string, EvaluationSubstitutionType> => {
const bindingPaths: Record<string, true> = { const bindingPaths: Record<string, EvaluationSubstitutionType> = {
data: true, data: EvaluationSubstitutionType.TEMPLATE,
isLoading: true, isLoading: EvaluationSubstitutionType.TEMPLATE,
}; };
if (!formConfig) { if (!formConfig) {
return { return {
...bindingPaths, ...bindingPaths,
config: true, config: EvaluationSubstitutionType.TEMPLATE,
}; };
} }
const recursiveFindBindingPaths = (formConfig: any) => { const recursiveFindBindingPaths = (formConfig: any) => {
if (formConfig.children) { if (formConfig.children) {
formConfig.children.forEach(recursiveFindBindingPaths); formConfig.children.forEach(recursiveFindBindingPaths);
} else { } else {
const configPath = formConfig.configProperty.replace( const configPath = getDataTreeActionConfigPath(formConfig.configProperty);
"actionConfiguration.",
"config.",
);
if (dynamicFields.includes(formConfig.controlType)) { if (dynamicFields.includes(formConfig.controlType)) {
bindingPaths[configPath] = true; if (!isHidden(action, formConfig.hidden)) {
bindingPaths[configPath] = getCorrectEvaluationSubstitutionType(
formConfig.evaluationSubstitutionType,
);
}
} }
if (formConfig.controlType === "ARRAY_FIELD") { if (formConfig.controlType === "ARRAY_FIELD") {
const actionValue = _.get(action, formConfig.configProperty); const actionValue = _.get(action, formConfig.configProperty);
@ -38,7 +52,11 @@ export const getBindingPathsOfAction = (
dynamicFields.includes(schemaField.controlType) dynamicFields.includes(schemaField.controlType)
) { ) {
const arrayConfigPath = `${configPath}[${i}].${schemaField.key}`; const arrayConfigPath = `${configPath}[${i}].${schemaField.key}`;
bindingPaths[arrayConfigPath] = true; bindingPaths[
arrayConfigPath
] = getCorrectEvaluationSubstitutionType(
formConfig.evaluationSubstitutionType,
);
} }
}); });
} }
@ -51,3 +69,6 @@ export const getBindingPathsOfAction = (
return bindingPaths; return bindingPaths;
}; };
export const getDataTreeActionConfigPath = (propertyPath: string) =>
propertyPath.replace("actionConfiguration.", "config.");

View File

@ -17,6 +17,7 @@ export enum PaginationType {
export interface ActionConfig { export interface ActionConfig {
timeoutInMillisecond?: number; timeoutInMillisecond?: number;
paginationType?: PaginationType; paginationType?: PaginationType;
pluginSpecifiedTemplates?: Array<{ key?: string; value?: unknown }>;
} }
export interface ActionProvider { export interface ActionProvider {

View File

@ -1,11 +1,15 @@
import { DynamicPath } from "utils/DynamicBindingUtils"; import { DependencyMap, DynamicPath } from "utils/DynamicBindingUtils";
import { DataTreeAction, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { DataTreeAction, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { ActionData } from "reducers/entityReducers/actionsReducer"; import { ActionData } from "reducers/entityReducers/actionsReducer";
import { getBindingPathsOfAction } from "entities/Action/actionProperties"; import {
getBindingPathsOfAction,
getDataTreeActionConfigPath,
} from "entities/Action/actionProperties";
export const generateDataTreeAction = ( export const generateDataTreeAction = (
action: ActionData, action: ActionData,
editorConfig: any[], editorConfig: any[],
dependencyConfig: DependencyMap = {},
): DataTreeAction => { ): DataTreeAction => {
let dynamicBindingPathList: DynamicPath[] = []; let dynamicBindingPathList: DynamicPath[] = [];
// update paths // update paths
@ -18,6 +22,12 @@ export const generateDataTreeAction = (
key: `config.${d.key}`, key: `config.${d.key}`,
})); }));
} }
const dependencyMap: DependencyMap = {};
Object.entries(dependencyConfig).forEach(([dependent, dependencies]) => {
dependencyMap[getDataTreeActionConfigPath(dependent)] = dependencies.map(
getDataTreeActionConfigPath,
);
});
return { return {
run: {}, run: {},
actionId: action.config.id, actionId: action.config.id,
@ -29,5 +39,6 @@ export const generateDataTreeAction = (
ENTITY_TYPE: ENTITY_TYPE.ACTION, ENTITY_TYPE: ENTITY_TYPE.ACTION,
isLoading: action.isLoading, isLoading: action.isLoading,
bindingPaths: getBindingPathsOfAction(action.config, editorConfig), bindingPaths: getBindingPathsOfAction(action.config, editorConfig),
dependencyMap,
}; };
}; };

View File

@ -9,7 +9,7 @@ import { MetaState } from "reducers/entityReducers/metaReducer";
import { PageListPayload } from "constants/ReduxActionConstants"; import { PageListPayload } from "constants/ReduxActionConstants";
import { ActionConfig, PluginType } from "entities/Action"; import { ActionConfig, PluginType } from "entities/Action";
import { AppDataState } from "reducers/entityReducers/appReducer"; import { AppDataState } from "reducers/entityReducers/appReducer";
import { DynamicPath } from "utils/DynamicBindingUtils"; import { DependencyMap, DynamicPath } from "utils/DynamicBindingUtils";
import { generateDataTreeAction } from "entities/DataTree/dataTreeAction"; import { generateDataTreeAction } from "entities/DataTree/dataTreeAction";
import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget"; import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget";
import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation";
@ -36,6 +36,12 @@ export type RunActionPayload = {
params: Record<string, any> | string; params: Record<string, any> | string;
}; };
export enum EvaluationSubstitutionType {
TEMPLATE = "TEMPLATE",
PARAMETER = "PARAMETER",
SMART_SUBSTITUTE = "SMART_SUBSTITUTE",
}
export interface DataTreeAction extends Omit<ActionData, "data" | "config"> { export interface DataTreeAction extends Omit<ActionData, "data" | "config"> {
data: ActionResponse["body"]; data: ActionResponse["body"];
actionId: string; actionId: string;
@ -46,12 +52,13 @@ export interface DataTreeAction extends Omit<ActionData, "data" | "config"> {
| ActionDispatcher<RunActionPayload, [string, string, string]> | ActionDispatcher<RunActionPayload, [string, string, string]>
| Record<string, any>; | Record<string, any>;
dynamicBindingPathList: DynamicPath[]; dynamicBindingPathList: DynamicPath[];
bindingPaths: Record<string, boolean>; bindingPaths: Record<string, EvaluationSubstitutionType>;
ENTITY_TYPE: ENTITY_TYPE.ACTION; ENTITY_TYPE: ENTITY_TYPE.ACTION;
dependencyMap: DependencyMap;
} }
export interface DataTreeWidget extends WidgetProps { export interface DataTreeWidget extends WidgetProps {
bindingPaths: Record<string, boolean>; bindingPaths: Record<string, EvaluationSubstitutionType>;
triggerPaths: Record<string, boolean>; triggerPaths: Record<string, boolean>;
validationPaths: Record<string, VALIDATION_TYPES>; validationPaths: Record<string, VALIDATION_TYPES>;
ENTITY_TYPE: ENTITY_TYPE.WIDGET; ENTITY_TYPE: ENTITY_TYPE.WIDGET;
@ -79,6 +86,7 @@ export type DataTree = {
type DataTreeSeed = { type DataTreeSeed = {
actions: ActionDataState; actions: ActionDataState;
editorConfigs: Record<string, any[]>; editorConfigs: Record<string, any[]>;
pluginDependencyConfig: Record<string, DependencyMap>;
widgets: CanvasWidgetsReduxState; widgets: CanvasWidgetsReduxState;
widgetsMeta: MetaState; widgetsMeta: MetaState;
pageList: PageListPayload; pageList: PageListPayload;
@ -93,13 +101,16 @@ export class DataTreeFactory {
pageList, pageList,
appData, appData,
editorConfigs, editorConfigs,
pluginDependencyConfig,
}: DataTreeSeed): DataTree { }: DataTreeSeed): DataTree {
const dataTree: DataTree = {}; const dataTree: DataTree = {};
actions.forEach((action) => { actions.forEach((action) => {
const editorConfig = editorConfigs[action.config.pluginId]; const editorConfig = editorConfigs[action.config.pluginId];
const dependencyConfig = pluginDependencyConfig[action.config.pluginId];
dataTree[action.config.name] = generateDataTreeAction( dataTree[action.config.name] = generateDataTreeAction(
action, action,
editorConfig, editorConfig,
dependencyConfig,
); );
}); });
Object.values(widgets).forEach((widget) => { Object.values(widgets).forEach((widget) => {

View File

@ -1,6 +1,10 @@
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget"; 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 { RenderModes, WidgetTypes } from "constants/WidgetConstants";
import WidgetFactory from "utils/WidgetFactory"; import WidgetFactory from "utils/WidgetFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation";
@ -181,19 +185,19 @@ describe("generateDataTreeWidget", () => {
const expected: DataTreeWidget = { const expected: DataTreeWidget = {
bindingPaths: { bindingPaths: {
defaultText: true, defaultText: EvaluationSubstitutionType.TEMPLATE,
errorMessage: true, errorMessage: EvaluationSubstitutionType.TEMPLATE,
isDirty: true, isDirty: EvaluationSubstitutionType.TEMPLATE,
isDisabled: true, isDisabled: EvaluationSubstitutionType.TEMPLATE,
isFocused: true, isFocused: EvaluationSubstitutionType.TEMPLATE,
isRequired: true, isRequired: EvaluationSubstitutionType.TEMPLATE,
isValid: true, isValid: EvaluationSubstitutionType.TEMPLATE,
isVisible: true, isVisible: EvaluationSubstitutionType.TEMPLATE,
placeholderText: true, placeholderText: EvaluationSubstitutionType.TEMPLATE,
regex: true, regex: EvaluationSubstitutionType.TEMPLATE,
resetOnSubmit: true, resetOnSubmit: EvaluationSubstitutionType.TEMPLATE,
text: true, text: EvaluationSubstitutionType.TEMPLATE,
value: true, value: EvaluationSubstitutionType.TEMPLATE,
}, },
triggerPaths: { triggerPaths: {
onSubmit: true, onSubmit: true,

View File

@ -19,26 +19,10 @@ export const generateDataTreeWidget = (
const propertyPaneConfigs = WidgetFactory.getWidgetPropertyPaneConfig( const propertyPaneConfigs = WidgetFactory.getWidgetPropertyPaneConfig(
widget.type, 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 derivedProps: any = {};
const dynamicBindingPathList = getEntityDynamicBindingPathList(widget); const dynamicBindingPathList = getEntityDynamicBindingPathList(widget);
dynamicBindingPathList.forEach((dynamicPath) => { dynamicBindingPathList.forEach((dynamicPath) => {
const propertyPath = dynamicPath.key; const propertyPath = dynamicPath.key;
// Add any dynamically generated dynamic bindings in the binding paths
bindingPaths[propertyPath] = true;
const propertyValue = _.get(widget, propertyPath); const propertyValue = _.get(widget, propertyPath);
if (_.isObject(propertyValue)) { if (_.isObject(propertyValue)) {
// Stringify this because composite controls may have bindings in the sub controls // Stringify this because composite controls may have bindings in the sub controls
@ -54,7 +38,6 @@ export const generateDataTreeWidget = (
dynamicBindingPathList.push({ dynamicBindingPathList.push({
key: propertyName, key: propertyName,
}); });
bindingPaths[propertyName] = true;
}); });
const unInitializedDefaultProps: Record<string, undefined> = {}; const unInitializedDefaultProps: Record<string, undefined> = {};
Object.values(defaultProps).forEach((propertyName) => { Object.values(defaultProps).forEach((propertyName) => {
@ -62,6 +45,16 @@ export const generateDataTreeWidget = (
unInitializedDefaultProps[propertyName] = undefined; unInitializedDefaultProps[propertyName] = undefined;
} }
}); });
const {
bindingPaths,
triggerPaths,
validationPaths,
} = getAllPathsFromPropertyConfig(widget, propertyPaneConfigs, {
...derivedPropertyMap,
...defaultMetaProps,
...unInitializedDefaultProps,
..._.keyBy(dynamicBindingPathList, "key"),
});
return { return {
...widget, ...widget,
...defaultMetaProps, ...defaultMetaProps,

View File

@ -2,6 +2,7 @@ import { getAllPathsFromPropertyConfig } from "./utils";
import { RenderModes, WidgetTypes } from "../../constants/WidgetConstants"; import { RenderModes, WidgetTypes } from "../../constants/WidgetConstants";
import tablePropertyPaneConfig from "widgets/TableWidget/TablePropertyPaneConfig"; import tablePropertyPaneConfig from "widgets/TableWidget/TablePropertyPaneConfig";
import chartPorpertyConfig from "widgets/ChartWidget/propertyConfig"; import chartPorpertyConfig from "widgets/ChartWidget/propertyConfig";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
describe("getAllPathsFromPropertyConfig", () => { describe("getAllPathsFromPropertyConfig", () => {
it("works as expected for table widget", () => { it("works as expected for table widget", () => {
@ -112,31 +113,47 @@ describe("getAllPathsFromPropertyConfig", () => {
const expected = { const expected = {
bindingPaths: { bindingPaths: {
selectedRow: true, selectedRow: EvaluationSubstitutionType.TEMPLATE,
selectedRows: true, selectedRows: EvaluationSubstitutionType.TEMPLATE,
tableData: true, tableData: EvaluationSubstitutionType.TEMPLATE,
defaultSearchText: true, defaultSearchText: EvaluationSubstitutionType.TEMPLATE,
defaultSelectedRow: true, defaultSelectedRow: EvaluationSubstitutionType.TEMPLATE,
isVisible: true, isVisible: EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.name.computedValue": true, "primaryColumns.name.computedValue":
"primaryColumns.name.horizontalAlignment": true, EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.name.verticalAlignment": true, "primaryColumns.name.horizontalAlignment":
"primaryColumns.name.textSize": true, EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.name.fontStyle": true, "primaryColumns.name.verticalAlignment":
"primaryColumns.name.textColor": true, EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.name.cellBackground": true, "primaryColumns.name.textSize": EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.createdAt.inputFormat": true, "primaryColumns.name.fontStyle": EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.createdAt.outputFormat": true, "primaryColumns.name.textColor": EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.createdAt.computedValue": true, "primaryColumns.name.cellBackground":
"primaryColumns.createdAt.horizontalAlignment": true, EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.createdAt.verticalAlignment": true, "primaryColumns.createdAt.inputFormat":
"primaryColumns.createdAt.textSize": true, EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.createdAt.fontStyle": true, "primaryColumns.createdAt.outputFormat":
"primaryColumns.createdAt.textColor": true, EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.createdAt.cellBackground": true, "primaryColumns.createdAt.computedValue":
"primaryColumns.status.buttonLabel": true, EvaluationSubstitutionType.TEMPLATE,
"primaryColumns.status.buttonStyle": true, "primaryColumns.createdAt.horizontalAlignment":
"primaryColumns.status.buttonLabelColor": true, 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: { triggerPaths: {
onRowSelected: true, onRowSelected: true,
@ -197,13 +214,13 @@ describe("getAllPathsFromPropertyConfig", () => {
const expected = { const expected = {
bindingPaths: { bindingPaths: {
chartType: true, chartType: EvaluationSubstitutionType.TEMPLATE,
chartName: true, chartName: EvaluationSubstitutionType.TEMPLATE,
"chartData[0].seriesName": true, "chartData[0].seriesName": EvaluationSubstitutionType.TEMPLATE,
"chartData[0].data": true, "chartData[0].data": EvaluationSubstitutionType.TEMPLATE,
xAxisName: true, xAxisName: EvaluationSubstitutionType.TEMPLATE,
yAxisName: true, yAxisName: EvaluationSubstitutionType.TEMPLATE,
isVisible: true, isVisible: EvaluationSubstitutionType.TEMPLATE,
}, },
triggerPaths: { triggerPaths: {
onDataPointClick: true, onDataPointClick: true,

View File

@ -3,17 +3,22 @@ import { PropertyPaneConfig } from "constants/PropertyControlConstants";
import { get } from "lodash"; import { get } from "lodash";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
export const getAllPathsFromPropertyConfig = ( export const getAllPathsFromPropertyConfig = (
widget: WidgetProps, widget: WidgetProps,
widgetConfig: readonly PropertyPaneConfig[], widgetConfig: readonly PropertyPaneConfig[],
derivedProperties: Record<string, true>, defaultProperties: Record<string, any>,
): { ): {
bindingPaths: Record<string, true>; bindingPaths: Record<string, EvaluationSubstitutionType>;
triggerPaths: Record<string, true>; triggerPaths: Record<string, true>;
validationPaths: Record<string, VALIDATION_TYPES>; validationPaths: Record<string, VALIDATION_TYPES>;
} => { } => {
const bindingPaths: Record<string, true> = derivedProperties; const bindingPaths: Record<string, EvaluationSubstitutionType> = {};
Object.keys(defaultProperties).forEach(
(property) =>
(bindingPaths[property] = EvaluationSubstitutionType.TEMPLATE),
);
const triggerPaths: Record<string, true> = {}; const triggerPaths: Record<string, true> = {};
const validationPaths: Record<any, VALIDATION_TYPES> = {}; const validationPaths: Record<any, VALIDATION_TYPES> = {};
widgetConfig.forEach((config) => { widgetConfig.forEach((config) => {
@ -29,7 +34,8 @@ export const getAllPathsFromPropertyConfig = (
controlConfig.isBindProperty && controlConfig.isBindProperty &&
!controlConfig.isTriggerProperty !controlConfig.isTriggerProperty
) { ) {
bindingPaths[controlConfig.propertyName] = true; bindingPaths[controlConfig.propertyName] =
EvaluationSubstitutionType.TEMPLATE;
if (controlConfig.validation) { if (controlConfig.validation) {
validationPaths[controlConfig.propertyName] = validationPaths[controlConfig.propertyName] =
controlConfig.validation; controlConfig.validation;
@ -72,7 +78,8 @@ export const getAllPathsFromPropertyConfig = (
panelColumnControlConfig.isBindProperty && panelColumnControlConfig.isBindProperty &&
!panelColumnControlConfig.isTriggerProperty !panelColumnControlConfig.isTriggerProperty
) { ) {
bindingPaths[panelPropertyPath] = true; bindingPaths[panelPropertyPath] =
EvaluationSubstitutionType.TEMPLATE;
if (panelColumnControlConfig.validation) { if (panelColumnControlConfig.validation) {
validationPaths[panelPropertyPath] = validationPaths[panelPropertyPath] =
panelColumnControlConfig.validation; panelColumnControlConfig.validation;
@ -107,7 +114,8 @@ export const getAllPathsFromPropertyConfig = (
childPropertyConfig.isBindProperty && childPropertyConfig.isBindProperty &&
!childPropertyConfig.isTriggerProperty !childPropertyConfig.isTriggerProperty
) { ) {
bindingPaths[childArrayPropertyPath] = true; bindingPaths[childArrayPropertyPath] =
EvaluationSubstitutionType.TEMPLATE;
if (childPropertyConfig.validation) { if (childPropertyConfig.validation) {
validationPaths[childArrayPropertyPath] = validationPaths[childArrayPropertyPath] =
childPropertyConfig.validation; childPropertyConfig.validation;
@ -131,13 +139,13 @@ export const getAllPathsFromPropertyConfig = (
}; };
export const nextAvailableRowInContainer = ( export const nextAvailableRowInContainer = (
parenContainertId: string, parentContainerId: string,
canvasWidgets: { [widgetId: string]: FlattenedWidgetProps }, canvasWidgets: { [widgetId: string]: FlattenedWidgetProps },
) => { ) => {
return ( return (
Object.values(canvasWidgets).reduce( Object.values(canvasWidgets).reduce(
(prev: number, next: any) => (prev: number, next: any) =>
next?.parentId === parenContainertId && next.bottomRow > prev next?.parentId === parentContainerId && next.bottomRow > prev
? next.bottomRow ? next.bottomRow
: prev, : prev,
0, 0,

View File

@ -9,6 +9,7 @@ import {
PluginFormPayloadWithId, PluginFormPayloadWithId,
PluginFormsPayload, PluginFormsPayload,
} from "actions/pluginActions"; } from "actions/pluginActions";
import { DependencyMap } from "utils/DynamicBindingUtils";
export interface PluginDataState { export interface PluginDataState {
list: Plugin[]; list: Plugin[];
@ -16,6 +17,7 @@ export interface PluginDataState {
formConfigs: Record<string, any[]>; formConfigs: Record<string, any[]>;
editorConfigs: Record<string, any[]>; editorConfigs: Record<string, any[]>;
settingConfigs: Record<string, any[]>; settingConfigs: Record<string, any[]>;
dependencies: Record<string, DependencyMap>;
} }
const initialState: PluginDataState = { const initialState: PluginDataState = {
@ -24,6 +26,7 @@ const initialState: PluginDataState = {
formConfigs: {}, formConfigs: {},
editorConfigs: {}, editorConfigs: {},
settingConfigs: {}, settingConfigs: {},
dependencies: {},
}; };
const pluginsReducer = createReducer(initialState, { const pluginsReducer = createReducer(initialState, {

View File

@ -6,6 +6,7 @@ import {
ReduxActionErrorTypes, ReduxActionErrorTypes,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import moment from "moment"; import moment from "moment";
import { PageAction } from "constants/AppsmithActionConstants/ActionConstants";
const initialState: EditorReduxState = { const initialState: EditorReduxState = {
initialized: false, initialized: false,
@ -106,6 +107,7 @@ const editorReducer = createReducer(initialState, {
pageWidgetId, pageWidgetId,
currentApplicationId, currentApplicationId,
currentPageId, currentPageId,
pageActions,
} = action.payload; } = action.payload;
state.loadingStates.publishing = false; state.loadingStates.publishing = false;
state.loadingStates.publishingError = false; state.loadingStates.publishingError = false;
@ -116,6 +118,7 @@ const editorReducer = createReducer(initialState, {
pageWidgetId, pageWidgetId,
currentApplicationId, currentApplicationId,
currentPageId, currentPageId,
pageActions,
}; };
}, },
[ReduxActionTypes.CLONE_PAGE_INIT]: (state: EditorReduxState) => { [ReduxActionTypes.CLONE_PAGE_INIT]: (state: EditorReduxState) => {
@ -171,6 +174,7 @@ export interface EditorReduxState {
currentLayoutId?: string; currentLayoutId?: string;
currentPageName?: string; currentPageName?: string;
currentPageId?: string; currentPageId?: string;
pageActions?: PageAction[][];
loadingStates: { loadingStates: {
saving: boolean; saving: boolean;
savingError: boolean; savingError: boolean;

View File

@ -30,6 +30,7 @@ import { executeAction, executeActionError } from "actions/widgetActions";
import { import {
getCurrentApplicationId, getCurrentApplicationId,
getCurrentPageId, getCurrentPageId,
getLayoutOnLoadActions,
getPageList, getPageList,
} from "selectors/editorSelectors"; } from "selectors/editorSelectors";
import _, { get, isString } from "lodash"; import _, { get, isString } from "lodash";
@ -979,9 +980,9 @@ function* executePageLoadAction(pageAction: PageAction) {
} }
} }
function* executePageLoadActionsSaga(action: ReduxAction<PageAction[][]>) { function* executePageLoadActionsSaga() {
try { try {
const pageActions = action.payload; const pageActions: PageAction[][] = yield select(getLayoutOnLoadActions);
const actionCount = _.flatten(pageActions).length; const actionCount = _.flatten(pageActions).length;
PerformanceTracker.startAsyncTracking( PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS, PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS,

View File

@ -1,4 +1,5 @@
import { import {
EvaluationReduxAction,
ReduxAction, ReduxAction,
ReduxActionErrorTypes, ReduxActionErrorTypes,
ReduxActionTypes, ReduxActionTypes,
@ -158,7 +159,9 @@ export function* createActionSaga(
} }
} }
export function* fetchActionsSaga(action: ReduxAction<FetchActionsPayload>) { export function* fetchActionsSaga(
action: EvaluationReduxAction<FetchActionsPayload>,
) {
const { applicationId } = action.payload; const { applicationId } = action.payload;
PerformanceTracker.startAsyncTracking( PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.FETCH_ACTIONS_API, PerformanceTransactionName.FETCH_ACTIONS_API,
@ -173,6 +176,7 @@ export function* fetchActionsSaga(action: ReduxAction<FetchActionsPayload>) {
yield put({ yield put({
type: ReduxActionTypes.FETCH_ACTIONS_SUCCESS, type: ReduxActionTypes.FETCH_ACTIONS_SUCCESS,
payload: response.data, payload: response.data,
postEvalActions: action.postEvalActions,
}); });
PerformanceTracker.stopAsyncTracking( PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_ACTIONS_API, PerformanceTransactionName.FETCH_ACTIONS_API,

View File

@ -67,7 +67,6 @@ import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages";
import { checkCurrentStep } from "./OnboardingSagas"; import { checkCurrentStep } from "./OnboardingSagas";
import { OnboardingStep } from "constants/OnboardingConstants"; import { OnboardingStep } from "constants/OnboardingConstants";
import { getIndextoUpdate } from "utils/ApiPaneUtils"; import { getIndextoUpdate } from "utils/ApiPaneUtils";
import { changeQuery } from "actions/queryPaneActions";
function* syncApiParamsSaga( function* syncApiParamsSaga(
actionPayload: ReduxActionWithMeta<string, { field: string }>, actionPayload: ReduxActionWithMeta<string, { field: string }>,
@ -208,29 +207,10 @@ function* initializeExtraFormDataSaga() {
const { extraformData } = state.ui.apiPane; const { extraformData } = state.ui.apiPane;
const formData = yield select(getFormData, API_EDITOR_FORM_NAME); const formData = yield select(getFormData, API_EDITOR_FORM_NAME);
const { values } = formData; const { values } = formData;
const headers = get( const headers = get(values, "actionConfiguration.headers");
values,
"actionConfiguration.headers",
DEFAULT_API_ACTION_CONFIG.headers,
);
const queryParameters = get(
values,
"actionConfiguration.queryParameters",
[],
);
if (!extraformData[values.id]) { if (!extraformData[values.id]) {
yield put( yield call(setHeaderFormat, values.id, headers);
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,
),
);
} }
} }
@ -270,6 +250,42 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) {
PerformanceTracker.stopTracking(); 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( function* updateFormFields(
actionPayload: ReduxActionWithMeta<string, { field: string }>, actionPayload: ReduxActionWithMeta<string, { field: string }>,
) { ) {
@ -318,35 +334,7 @@ function* updateFormFields(
"actionConfiguration.headers", "actionConfiguration.headers",
); );
const apiId = get(values, "id"); const apiId = get(values, "id");
let displayFormat; yield call(setHeaderFormat, apiId, actionConfigurationHeaders);
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,
},
},
});
} }
} }
@ -562,12 +550,6 @@ function* handleApiNameChangeFailureSaga(
yield put(change(API_EDITOR_FORM_NAME, "name", action.payload.oldName)); 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() { export default function* root() {
yield all([ yield all([
takeEvery(ReduxActionTypes.API_PANE_CHANGE_API, changeApiSaga), takeEvery(ReduxActionTypes.API_PANE_CHANGE_API, changeApiSaga),
@ -597,7 +579,6 @@ export default function* root() {
ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE, ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE,
handleUpdateBodyContentType, handleUpdateBodyContentType,
), ),
takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, updateFormValues),
// Intercepting the redux-form change actionType // Intercepting the redux-form change actionType
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),

View File

@ -12,6 +12,7 @@ import {
ReduxAction, ReduxAction,
ReduxActionErrorTypes, ReduxActionErrorTypes,
ReduxActionTypes, ReduxActionTypes,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors"; import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";
import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory"; import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory";
@ -109,13 +110,17 @@ const evalErrorHandler = (errors: EvalError[]) => {
}); });
}; };
function* postEvalActionDispatcher(actions: ReduxAction<unknown>[]) { function* postEvalActionDispatcher(
actions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
) {
for (const action of actions) { for (const action of actions) {
yield put(action); yield put(action);
} }
} }
function* evaluateTreeSaga(postEvalActions?: ReduxAction<unknown>[]) { function* evaluateTreeSaga(
postEvalActions?: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
) {
const unevalTree = yield select(getUnevaluatedDataTree); const unevalTree = yield select(getUnevaluatedDataTree);
log.debug({ unevalTree }); log.debug({ unevalTree });
PerformanceTracker.startAsyncTracking( PerformanceTracker.startAsyncTracking(

View File

@ -13,6 +13,7 @@ import {
ReduxAction, ReduxAction,
ReduxActionErrorTypes, ReduxActionErrorTypes,
ReduxActionTypes, ReduxActionTypes,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { ERROR_CODES } from "constants/ApiConstants"; import { ERROR_CODES } from "constants/ApiConstants";
@ -43,6 +44,41 @@ import { resetEditorSuccess } from "actions/initActions";
import PerformanceTracker, { import PerformanceTracker, {
PerformanceTransactionName, PerformanceTransactionName,
} from "utils/PerformanceTracker"; } from "utils/PerformanceTracker";
import { executePageLoadActions } from "actions/widgetActions";
function* failFastApiCalls(
triggerActions: Array<ReduxAction<unknown> | 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( function* initializeEditorSaga(
initializeEditorAction: ReduxAction<InitializeEditorPayload>, initializeEditorAction: ReduxAction<InitializeEditorPayload>,
@ -55,97 +91,61 @@ function* initializeEditorSaga(
yield put(setAppMode(APP_MODE.EDIT)); yield put(setAppMode(APP_MODE.EDIT));
yield put(updateAppPersistentStore(getPersistentAppStore(applicationId))); yield put(updateAppPersistentStore(getPersistentAppStore(applicationId)));
yield put({ type: ReduxActionTypes.START_EVALUATION }); 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 applicationAndLayoutCalls = yield failFastApiCalls(
[
const resultOfPrimaryCalls = yield race({ fetchPageList(applicationId, APP_MODE.EDIT),
success: all([ fetchPage(pageId),
take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), fetchApplication(applicationId, APP_MODE.EDIT),
take(ReduxActionTypes.FETCH_PAGE_SUCCESS), ],
take(ReduxActionTypes.FETCH_APPLICATION_SUCCESS), [
take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS), ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS,
]), ReduxActionTypes.FETCH_PAGE_SUCCESS,
failure: take([ ReduxActionTypes.FETCH_APPLICATION_SUCCESS,
],
[
ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR, ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR,
ReduxActionErrorTypes.FETCH_PAGE_ERROR, ReduxActionErrorTypes.FETCH_PAGE_ERROR,
ReduxActionErrorTypes.FETCH_APPLICATION_ERROR, ReduxActionErrorTypes.FETCH_APPLICATION_ERROR,
ReduxActionErrorTypes.FETCH_ACTIONS_ERROR, ],
]), );
}); if (!applicationAndLayoutCalls) return;
if (resultOfPrimaryCalls.failure) { const pluginsAndDatasourcesCalls = yield failFastApiCalls(
yield put({ [fetchPlugins(), fetchDatasources()],
type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, [
payload: { ReduxActionTypes.FETCH_PLUGINS_SUCCESS,
code: get( ReduxActionTypes.FETCH_DATASOURCES_SUCCESS,
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([
ReduxActionErrorTypes.FETCH_PLUGINS_ERROR, ReduxActionErrorTypes.FETCH_PLUGINS_ERROR,
ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR, ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR,
]), ],
}); );
if (!pluginsAndDatasourcesCalls) return;
if (resultOfSecondaryCalls.failure) { const pluginFormCall = yield failFastApiCalls(
yield put({ [fetchPluginFormConfigs()],
type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, [ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS],
payload: { [ReduxActionErrorTypes.FETCH_PLUGIN_FORM_CONFIGS_ERROR],
code: get( );
resultOfSecondaryCalls, if (!pluginFormCall) return;
"failure.payload.error.code",
ERROR_CODES.SERVER_ERROR,
),
},
});
return;
}
yield put(fetchPluginFormConfigs()); const actionsCall = yield failFastApiCalls(
[fetchActions(applicationId, [executePageLoadActions()])],
[ReduxActionTypes.FETCH_ACTIONS_SUCCESS],
[ReduxActionErrorTypes.FETCH_ACTIONS_ERROR],
);
const resultOfPluginFormsCall = yield race({ if (!actionsCall) return;
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;
}
const currentApplication = yield select(getCurrentApplication); const currentApplication = yield select(getCurrentApplication);
const appName = currentApplication ? currentApplication.name : ""; const appName = currentApplication ? currentApplication.name : "";
const appId = currentApplication ? currentApplication.id : ""; const appId = currentApplication ? currentApplication.id : "";
yield put(restoreRecentEntitiesRequest(applicationId));
AnalyticsUtil.logEvent("EDITOR_OPEN", { AnalyticsUtil.logEvent("EDITOR_OPEN", {
appId: appId, appId: appId,
appName: appName, appName: appName,

View File

@ -195,12 +195,7 @@ export function* fetchPageSaga(
// set current page // set current page
yield put(updateCurrentPage(id)); yield put(updateCurrentPage(id));
// dispatch fetch page success // dispatch fetch page success
yield put( yield put(fetchPageSuccess());
fetchPageSuccess([
// Execute page load actions after evaluation of fetch page
executePageLoadActions(canvasWidgetsPayload.pageActions),
]),
);
yield put({ yield put({
type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE, type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE,
@ -264,7 +259,7 @@ export function* fetchPublishedPageSaga(
yield put( yield put(
fetchPublishedPageSuccess( fetchPublishedPageSuccess(
// Execute page load actions post published page eval // Execute page load actions post published page eval
[executePageLoadActions(canvasWidgetsPayload.pageActions)], [executePageLoadActions()],
), ),
); );
PerformanceTracker.stopAsyncTracking( PerformanceTracker.stopAsyncTracking(

View File

@ -19,6 +19,7 @@ import {
fetchPluginFormConfigSuccess, fetchPluginFormConfigSuccess,
} from "actions/pluginActions"; } from "actions/pluginActions";
import { import {
defaultActionDependenciesConfig,
defaultActionEditorConfigs, defaultActionEditorConfigs,
defaultActionSettings, defaultActionSettings,
} from "constants/AppsmithActionConstants/ActionConstants"; } from "constants/AppsmithActionConstants/ActionConstants";
@ -26,6 +27,7 @@ import { GenericApiResponse } from "api/ApiResponses";
import PluginApi from "api/PluginApi"; import PluginApi from "api/PluginApi";
import log from "loglevel"; import log from "loglevel";
import { PluginType } from "entities/Action"; import { PluginType } from "entities/Action";
import { DependencyMap } from "utils/DynamicBindingUtils";
function* fetchPluginsSaga() { function* fetchPluginsSaga() {
try { try {
@ -77,20 +79,30 @@ function* fetchPluginFormConfigsSaga() {
const formConfigs: Record<string, any[]> = {}; const formConfigs: Record<string, any[]> = {};
const editorConfigs: Record<string, any[]> = {}; const editorConfigs: Record<string, any[]> = {};
const settingConfigs: Record<string, any[]> = {}; const settingConfigs: Record<string, any[]> = {};
const dependencies: Record<string, DependencyMap> = {};
Array.from(pluginIdFormsToFetch).forEach((pluginId, index) => { Array.from(pluginIdFormsToFetch).forEach((pluginId, index) => {
const plugin = plugins.find((plugin) => plugin.id === pluginId); const plugin = plugins.find((plugin) => plugin.id === pluginId);
// Datasource form always use server's copy
formConfigs[pluginId] = pluginFormData[index].form; formConfigs[pluginId] = pluginFormData[index].form;
// Action editor form if not available use default
if (plugin && !pluginFormData[index].editor) { if (plugin && !pluginFormData[index].editor) {
editorConfigs[pluginId] = defaultActionEditorConfigs[plugin.type]; editorConfigs[pluginId] = defaultActionEditorConfigs[plugin.type];
} else { } else {
editorConfigs[pluginId] = pluginFormData[index].editor; editorConfigs[pluginId] = pluginFormData[index].editor;
} }
// Action settings form if not available use default
if (plugin && !pluginFormData[index].setting) { if (plugin && !pluginFormData[index].setting) {
settingConfigs[pluginId] = defaultActionSettings[plugin.type]; settingConfigs[pluginId] = defaultActionSettings[plugin.type];
} else { } else {
settingConfigs[pluginId] = pluginFormData[index].setting; 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( yield put(
@ -98,6 +110,7 @@ function* fetchPluginFormConfigsSaga() {
formConfigs, formConfigs,
editorConfigs, editorConfigs,
settingConfigs, settingConfigs,
dependencies,
}), }),
); );
} catch (error) { } catch (error) {
@ -125,6 +138,10 @@ export function* checkAndGetPluginFormConfigsSaga(pluginId: string) {
formConfigResponse.data.editor = formConfigResponse.data.editor =
defaultActionEditorConfigs[plugin.type]; defaultActionEditorConfigs[plugin.type];
} }
if (!formConfigResponse.data.dependencies) {
formConfigResponse.data.dependencies =
defaultActionDependenciesConfig[plugin.type];
}
yield put( yield put(
fetchPluginFormConfigSuccess({ fetchPluginFormConfigSuccess({
id: pluginId, id: pluginId,

View File

@ -104,7 +104,7 @@ export function* addApiToPageSaga(
}); });
const applicationId = yield select(getCurrentApplicationId); const applicationId = yield select(getCurrentApplicationId);
yield put(fetchActions(applicationId)); yield put(fetchActions(applicationId, []));
} }
} catch (error) { } catch (error) {
yield put({ yield put({

View File

@ -27,7 +27,7 @@ import {
getPluginTemplates, getPluginTemplates,
getPlugin, getPlugin,
} from "selectors/entitiesSelector"; } from "selectors/entitiesSelector";
import { Action, PluginType, QueryAction } from "entities/Action"; import { PluginType, QueryAction } from "entities/Action";
import { setActionProperty } from "actions/actionActions"; import { setActionProperty } from "actions/actionActions";
import { getQueryParams } from "utils/AppsmithUtils"; import { getQueryParams } from "utils/AppsmithUtils";
import { isEmpty, merge } from "lodash"; import { isEmpty, merge } from "lodash";
@ -37,7 +37,6 @@ import { Toaster } from "components/ads/Toast";
import { Datasource } from "entities/Datasource"; import { Datasource } from "entities/Datasource";
import _ from "lodash"; import _ from "lodash";
import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages"; import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages";
import { changeQuery } from "actions/queryPaneActions";
function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload; const { id } = actionPayload.payload;
@ -214,12 +213,6 @@ function* handleNameChangeFailureSaga(
yield put(change(QUERY_EDITOR_FORM_NAME, "name", action.payload.oldName)); 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() { export default function* root() {
yield all([ yield all([
takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleQueryCreatedSaga), takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleQueryCreatedSaga),
@ -237,7 +230,6 @@ export default function* root() {
ReduxActionErrorTypes.SAVE_ACTION_NAME_ERROR, ReduxActionErrorTypes.SAVE_ACTION_NAME_ERROR,
handleNameChangeFailureSaga, handleNameChangeFailureSaga,
), ),
takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, updateFormValues),
// Intercepting the redux-form change actionType // Intercepting the redux-form change actionType
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),

View File

@ -2,6 +2,7 @@ import { createSelector } from "reselect";
import { import {
getActionsForCurrentPage, getActionsForCurrentPage,
getAppData, getAppData,
getPluginDependencyConfig,
getPluginEditorConfigs, getPluginEditorConfigs,
} from "./entitiesSelector"; } from "./entitiesSelector";
import { ActionDataState } from "reducers/entityReducers/actionsReducer"; import { ActionDataState } from "reducers/entityReducers/actionsReducer";
@ -18,7 +19,16 @@ export const getUnevaluatedDataTree = createSelector(
getPageList, getPageList,
getAppData, getAppData,
getPluginEditorConfigs, getPluginEditorConfigs,
(actions, widgets, widgetsMeta, pageListPayload, appData, editorConfigs) => { getPluginDependencyConfig,
(
actions,
widgets,
widgetsMeta,
pageListPayload,
appData,
editorConfigs,
pluginDependencyConfig,
) => {
const pageList = pageListPayload || []; const pageList = pageListPayload || [];
return DataTreeFactory.create({ return DataTreeFactory.create({
actions, actions,
@ -27,6 +37,7 @@ export const getUnevaluatedDataTree = createSelector(
pageList, pageList,
appData, appData,
editorConfigs, editorConfigs,
pluginDependencyConfig,
}); });
}, },
); );

View File

@ -63,6 +63,9 @@ export const getPageSavingError = (state: AppState) => {
return state.ui.editor.loadingStates.savingError; return state.ui.editor.loadingStates.savingError;
}; };
export const getLayoutOnLoadActions = (state: AppState) =>
state.ui.editor.pageActions || [];
export const getIsPublishingApplication = (state: AppState) => export const getIsPublishingApplication = (state: AppState) =>
state.ui.editor.loadingStates.publishing; state.ui.editor.loadingStates.publishing;

View File

@ -129,6 +129,9 @@ export const getPluginByPackageName = (state: AppState, name: string) =>
export const getPluginEditorConfigs = (state: AppState) => export const getPluginEditorConfigs = (state: AppState) =>
state.entities.plugins.editorConfigs; state.entities.plugins.editorConfigs;
export const getPluginDependencyConfig = (state: AppState) =>
state.entities.plugins.dependencies;
export const getPluginSettingConfigs = (state: AppState, pluginId: string) => export const getPluginSettingConfigs = (state: AppState, pluginId: string) =>
state.entities.plugins.settingConfigs[pluginId]; state.entities.plugins.settingConfigs[pluginId];

View File

@ -2,7 +2,11 @@ import {
generateTypeDef, generateTypeDef,
dataTreeTypeDefCreator, dataTreeTypeDefCreator,
} from "utils/autocomplete/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 { entityDefinitions } from "utils/autocomplete/EntityDefinitions";
import { WidgetTypes } from "../../constants/WidgetConstants"; import { WidgetTypes } from "../../constants/WidgetConstants";
@ -26,7 +30,7 @@ describe("dataTreeTypeDefCreator", () => {
isLoading: false, isLoading: false,
version: 1, version: 1,
bindingPaths: { bindingPaths: {
defaultText: true, defaultText: EvaluationSubstitutionType.TEMPLATE,
}, },
triggerPaths: { triggerPaths: {
onTextChange: true, onTextChange: true,

View File

@ -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", () => { describe("Immutable Canvas structures", () => {
it("generates the same object if it is run with the same dsl", () => { it("generates the same object if it is run with the same dsl", () => {
const nextState = compareAndGenerateImmutableCanvasStructure( const nextState = compareAndGenerateImmutableCanvasStructure(
@ -356,15 +90,6 @@ describe("Immutable Canvas structures", () => {
expect(nextState).toBe(canvasStructure); 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", () => { it("updates the diff appropriately", () => {
const dsl: any = { const dsl: any = {
widgetId: "x", widgetId: "x",

View File

@ -1,6 +1,5 @@
import { import {
DependencyMap, DependencyMap,
EntityWithBindings,
EvalError, EvalError,
EvalErrorTypes, EvalErrorTypes,
getDynamicBindings, getDynamicBindings,
@ -17,6 +16,7 @@ import {
DataTreeObjectEntity, DataTreeObjectEntity,
DataTreeWidget, DataTreeWidget,
ENTITY_TYPE, ENTITY_TYPE,
EvaluationSubstitutionType,
} from "entities/DataTree/dataTreeFactory"; } from "entities/DataTree/dataTreeFactory";
import { import {
addDependantsOfNestedPropertyPaths, addDependantsOfNestedPropertyPaths,
@ -43,6 +43,7 @@ import {
} from "constants/AppsmithActionConstants/ActionConstants"; } from "constants/AppsmithActionConstants/ActionConstants";
import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import { DATA_BIND_REGEX } from "constants/BindingsConstants";
import evaluate, { EvalResult } from "workers/evaluate"; import evaluate, { EvalResult } from "workers/evaluate";
import { substituteDynamicBindingWithValues } from "workers/evaluationSubstitution";
export default class DataTreeEvaluator { export default class DataTreeEvaluator {
dependencyMap: DependencyMap = {}; dependencyMap: DependencyMap = {};
@ -308,7 +309,6 @@ export default class DataTreeEvaluator {
), ),
); );
}); });
// TODO make this run only for widgets and not actions
dependencyMap = makeParentsDependOnChildren(dependencyMap); dependencyMap = makeParentsDependOnChildren(dependencyMap);
return 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 // Set default property dependency
const defaultProperties = this.widgetConfigMap[entity.type] const defaultProperties = this.widgetConfigMap[entity.type]
.defaultProperties; .defaultProperties;
Object.keys(defaultProperties).forEach((property) => { Object.entries(defaultProperties).forEach(
dependencies[`${entityName}.${property}`] = [ ([property, defaultPropertyPath]) => {
`${entityName}.${defaultProperties[property]}`, 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; return dependencies;
} }
@ -356,7 +367,9 @@ export default class DataTreeEvaluator {
const { entityName, propertyPath } = getEntityNameAndPropertyPath( const { entityName, propertyPath } = getEntityNameAndPropertyPath(
fullPropertyPath, fullPropertyPath,
); );
const entity: DataTreeEntity = currentTree[entityName]; const entity = currentTree[entityName] as
| DataTreeWidget
| DataTreeAction;
const unEvalPropertyValue = _.get( const unEvalPropertyValue = _.get(
currentTree as any, currentTree as any,
fullPropertyPath, fullPropertyPath,
@ -368,10 +381,15 @@ export default class DataTreeEvaluator {
const requiresEval = const requiresEval =
isABindingPath && isDynamicValue(unEvalPropertyValue); isABindingPath && isDynamicValue(unEvalPropertyValue);
if (requiresEval) { if (requiresEval) {
const evaluationSubstitutionType =
entity.bindingPaths[propertyPath] ||
EvaluationSubstitutionType.TEMPLATE;
try { try {
evalPropertyValue = this.evaluateDynamicProperty( evalPropertyValue = this.getDynamicValue(
currentTree,
unEvalPropertyValue, unEvalPropertyValue,
currentTree,
evaluationSubstitutionType,
false,
); );
} catch (e) { } catch (e) {
this.errors.push({ this.errors.push({
@ -512,6 +530,7 @@ export default class DataTreeEvaluator {
getDynamicValue( getDynamicValue(
dynamicBinding: string, dynamicBinding: string,
data: DataTree, data: DataTree,
evaluationSubstitutionType: EvaluationSubstitutionType,
returnTriggers: boolean, returnTriggers: boolean,
callBackData?: Array<any>, callBackData?: Array<any>,
) { ) {
@ -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]; if (stringSegments.length === 1) return values[0];
// else return a string template with bindings // else return a combined value according to the evaluation type
return createDynamicValueString(dynamicBinding, stringSegments, values); return substituteDynamicBindingWithValues(
dynamicBinding,
stringSegments,
values,
evaluationSubstitutionType,
);
} }
return undefined; return undefined;
} }
@ -569,13 +593,6 @@ export default class DataTreeEvaluator {
} }
} }
evaluateDynamicProperty(
currentTree: DataTree,
unEvalPropertyValue: any,
): any {
return this.getDynamicValue(unEvalPropertyValue, currentTree, false);
}
validateAndParseWidgetProperty( validateAndParseWidgetProperty(
fullPropertyPath: string, fullPropertyPath: string,
widget: DataTreeWidget, widget: DataTreeWidget,
@ -590,6 +607,7 @@ export default class DataTreeEvaluator {
const { triggers } = this.getDynamicValue( const { triggers } = this.getDynamicValue(
unEvalPropertyValue, unEvalPropertyValue,
currentTree, currentTree,
EvaluationSubstitutionType.TEMPLATE,
true, true,
undefined, undefined,
); );
@ -695,21 +713,37 @@ export default class DataTreeEvaluator {
if (entityType !== "noop") { if (entityType !== "noop") {
switch (dataTreeDiff.event) { switch (dataTreeDiff.event) {
case DataTreeDiffEvent.NEW: { 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 ( if (
isWidget(entity) && (isWidget(entity) || isAction(entity)) &&
!this.isDynamicLeaf( !this.isDynamicLeaf(
unEvalDataTree, unEvalDataTree,
dataTreeDiff.payload.propertyPath, dataTreeDiff.payload.propertyPath,
) )
) { ) {
const widgetDependencyMap: DependencyMap = this.listEntityDependencies( const entityDependencyMap: DependencyMap = this.listEntityDependencies(
entity as DataTreeWidget, entity,
entityName, entityName,
); );
if (Object.keys(widgetDependencyMap).length) { if (Object.keys(entityDependencyMap).length) {
didUpdateDependencyMap = true; 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 // 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); removedPaths.push(dataTreeDiff.payload.propertyPath);
// If an existing widget was deleted, remove all the bindings from the global dependency map // If an existing widget was deleted, remove all the bindings from the global dependency map
if ( if (
isWidget(entity) && (isWidget(entity) || isAction(entity)) &&
dataTreeDiff.payload.propertyPath === entityName dataTreeDiff.payload.propertyPath === entityName
) { ) {
const widgetBindings = this.listEntityDependencies( const entityDependencies = this.listEntityDependencies(
entity, entity,
entityName, entityName,
); );
Object.keys(widgetBindings).forEach((widgetDep) => { Object.keys(entityDependencies).forEach((widgetDep) => {
didUpdateDependencyMap = true; didUpdateDependencyMap = true;
delete this.dependencyMap[widgetDep]; delete this.dependencyMap[widgetDep];
}); });
@ -783,23 +817,22 @@ export default class DataTreeEvaluator {
} }
case DataTreeDiffEvent.EDIT: { case DataTreeDiffEvent.EDIT: {
// We only care about dependencies for a widget. This is because in case a dependency of an action changes, // We only care if the difference is in dynamic bindings since static values do not need
// 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
// an evaluation. // an evaluation.
if ( if (
(entityType === ENTITY_TYPE.WIDGET || (isWidget(entity) || isAction(entity)) &&
entityType === ENTITY_TYPE.ACTION) &&
typeof dataTreeDiff.payload.value === "string" typeof dataTreeDiff.payload.value === "string"
) { ) {
const entity: EntityWithBindings = unEvalDataTree[ const entity: DataTreeAction | DataTreeWidget = unEvalDataTree[
entityName entityName
] as EntityWithBindings; ] as DataTreeAction | DataTreeWidget;
const fullPropertyPath = dataTreeDiff.payload.propertyPath;
const entityPropertyPath = fullPropertyPath.substring(
fullPropertyPath.indexOf(".") + 1,
);
const isABindingPath = isPathADynamicBinding( const isABindingPath = isPathADynamicBinding(
entity, entity,
dataTreeDiff.payload.propertyPath.substring( entityPropertyPath,
dataTreeDiff.payload.propertyPath.indexOf(".") + 1,
),
); );
if (isABindingPath) { if (isABindingPath) {
didUpdateDependencyMap = true; 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 // 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 // dependencies for this property path with the newly found dependencies
if (correctSnippets.length) { if (correctSnippets.length) {
this.dependencyMap[ this.dependencyMap[fullPropertyPath] = correctSnippets;
dataTreeDiff.payload.propertyPath
] = correctSnippets;
} else { } else {
// The dependency on this property path has been removed. Delete this property path from the global // The dependency on this property path has been removed. Delete this property path from the global
// dependency map // dependency map
delete this.dependencyMap[ delete this.dependencyMap[fullPropertyPath];
dataTreeDiff.payload.propertyPath }
]; 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) { if (didUpdateDependencyMap) {
// TODO Optimise // TODO Optimise
Object.keys(this.dependencyMap).forEach((key) => { Object.keys(this.dependencyMap).forEach((key) => {
this.dependencyMap[key] = _.flatten( this.dependencyMap[key] = _.uniq(
this.dependencyMap[key].map((path) => _.flatten(
extractReferencesFromBinding(path, this.allKeys), this.dependencyMap[key].map((path) =>
extractReferencesFromBinding(path, this.allKeys),
),
), ),
); );
}); });
@ -1003,6 +1053,7 @@ export default class DataTreeEvaluator {
evaluatedExecutionParams = this.getDynamicValue( evaluatedExecutionParams = this.getDynamicValue(
`{{${JSON.stringify(executionParams)}}}`, `{{${JSON.stringify(executionParams)}}}`,
this.evalTree, this.evalTree,
EvaluationSubstitutionType.TEMPLATE,
false, false,
); );
} }
@ -1021,6 +1072,7 @@ export default class DataTreeEvaluator {
this.getDynamicValue( this.getDynamicValue(
`{{${binding}}}`, `{{${binding}}}`,
dataTreeWithExecutionParams, dataTreeWithExecutionParams,
EvaluationSubstitutionType.TEMPLATE,
false, 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 // 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); 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 { function isValidEntity(entity: DataTreeEntity): entity is DataTreeObjectEntity {
if (!_.isObject(entity)) { if (!_.isObject(entity)) {
// ERRORS.push({ // ERRORS.push({

View File

@ -2,6 +2,7 @@ import {
DataTreeAction, DataTreeAction,
DataTreeWidget, DataTreeWidget,
ENTITY_TYPE, ENTITY_TYPE,
EvaluationSubstitutionType,
} from "../entities/DataTree/dataTreeFactory"; } from "../entities/DataTree/dataTreeFactory";
import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; import { WidgetTypeConfigMap } from "../utils/WidgetFactory";
import { RenderModes, WidgetTypes } from "../constants/WidgetConstants"; import { RenderModes, WidgetTypes } from "../constants/WidgetConstants";
@ -238,9 +239,10 @@ const BASE_ACTION: DataTreeAction = {
data: {}, data: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION, ENTITY_TYPE: ENTITY_TYPE.ACTION,
bindingPaths: { bindingPaths: {
isLoading: true, isLoading: EvaluationSubstitutionType.TEMPLATE,
data: true, data: EvaluationSubstitutionType.TEMPLATE,
}, },
dependencyMap: {},
}; };
describe("DataTreeEvaluator", () => { describe("DataTreeEvaluator", () => {
@ -251,7 +253,10 @@ describe("DataTreeEvaluator", () => {
text: "Label", text: "Label",
type: WidgetTypes.TEXT_WIDGET, type: WidgetTypes.TEXT_WIDGET,
bindingPaths: { bindingPaths: {
text: true, text: EvaluationSubstitutionType.TEMPLATE,
},
validationPaths: {
text: VALIDATION_TYPES.TEXT,
}, },
}, },
Text2: { Text2: {
@ -261,7 +266,7 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "text" }], dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET, type: WidgetTypes.TEXT_WIDGET,
bindingPaths: { bindingPaths: {
text: true, text: EvaluationSubstitutionType.TEMPLATE,
}, },
validationPaths: { validationPaths: {
text: VALIDATION_TYPES.TEXT, text: VALIDATION_TYPES.TEXT,
@ -274,7 +279,7 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "text" }], dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET, type: WidgetTypes.TEXT_WIDGET,
bindingPaths: { bindingPaths: {
text: true, text: EvaluationSubstitutionType.TEMPLATE,
}, },
validationPaths: { validationPaths: {
text: VALIDATION_TYPES.TEXT, text: VALIDATION_TYPES.TEXT,
@ -294,18 +299,18 @@ describe("DataTreeEvaluator", () => {
], ],
type: WidgetTypes.DROP_DOWN_WIDGET, type: WidgetTypes.DROP_DOWN_WIDGET,
bindingPaths: { bindingPaths: {
options: true, options: EvaluationSubstitutionType.TEMPLATE,
defaultOptionValue: true, defaultOptionValue: EvaluationSubstitutionType.TEMPLATE,
isRequired: true, isRequired: EvaluationSubstitutionType.TEMPLATE,
isVisible: true, isVisible: EvaluationSubstitutionType.TEMPLATE,
isDisabled: true, isDisabled: EvaluationSubstitutionType.TEMPLATE,
isValid: true, isValid: EvaluationSubstitutionType.TEMPLATE,
selectedOption: true, selectedOption: EvaluationSubstitutionType.TEMPLATE,
selectedOptionArr: true, selectedOptionArr: EvaluationSubstitutionType.TEMPLATE,
selectedIndex: true, selectedIndex: EvaluationSubstitutionType.TEMPLATE,
selectedIndexArr: true, selectedIndexArr: EvaluationSubstitutionType.TEMPLATE,
value: true, value: EvaluationSubstitutionType.TEMPLATE,
selectedOptionValues: true, selectedOptionValues: EvaluationSubstitutionType.TEMPLATE,
}, },
}, },
Table1: { Table1: {
@ -314,9 +319,9 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "tableData" }], dynamicBindingPathList: [{ key: "tableData" }],
type: WidgetTypes.TABLE_WIDGET, type: WidgetTypes.TABLE_WIDGET,
bindingPaths: { bindingPaths: {
tableData: true, tableData: EvaluationSubstitutionType.TEMPLATE,
selectedRow: true, selectedRow: EvaluationSubstitutionType.TEMPLATE,
selectedRows: true, selectedRows: EvaluationSubstitutionType.TEMPLATE,
}, },
validationPaths: { validationPaths: {
tableData: VALIDATION_TYPES.TABLE_DATA, tableData: VALIDATION_TYPES.TABLE_DATA,
@ -328,7 +333,7 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "text" }], dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET, type: WidgetTypes.TEXT_WIDGET,
bindingPaths: { bindingPaths: {
text: true, text: EvaluationSubstitutionType.TEMPLATE,
}, },
validationPaths: { validationPaths: {
text: VALIDATION_TYPES.TEXT, text: VALIDATION_TYPES.TEXT,
@ -431,10 +436,10 @@ describe("DataTreeEvaluator", () => {
widgetName: "Input1", widgetName: "Input1",
type: WidgetTypes.INPUT_WIDGET, type: WidgetTypes.INPUT_WIDGET,
bindingPaths: { bindingPaths: {
defaultText: true, defaultText: EvaluationSubstitutionType.TEMPLATE,
isValid: true, isValid: EvaluationSubstitutionType.TEMPLATE,
value: true, value: EvaluationSubstitutionType.TEMPLATE,
text: true, text: EvaluationSubstitutionType.TEMPLATE,
}, },
}, },
}; };
@ -460,18 +465,18 @@ describe("DataTreeEvaluator", () => {
], ],
type: WidgetTypes.DROP_DOWN_WIDGET, type: WidgetTypes.DROP_DOWN_WIDGET,
bindingPaths: { bindingPaths: {
options: true, options: EvaluationSubstitutionType.TEMPLATE,
defaultOptionValue: true, defaultOptionValue: EvaluationSubstitutionType.TEMPLATE,
isRequired: true, isRequired: EvaluationSubstitutionType.TEMPLATE,
isVisible: true, isVisible: EvaluationSubstitutionType.TEMPLATE,
isDisabled: true, isDisabled: EvaluationSubstitutionType.TEMPLATE,
isValid: true, isValid: EvaluationSubstitutionType.TEMPLATE,
selectedOption: true, selectedOption: EvaluationSubstitutionType.TEMPLATE,
selectedOptionArr: true, selectedOptionArr: EvaluationSubstitutionType.TEMPLATE,
selectedIndex: true, selectedIndex: EvaluationSubstitutionType.TEMPLATE,
selectedIndexArr: true, selectedIndexArr: EvaluationSubstitutionType.TEMPLATE,
value: true, value: EvaluationSubstitutionType.TEMPLATE,
selectedOptionValues: true, selectedOptionValues: EvaluationSubstitutionType.TEMPLATE,
}, },
}, },
}; };
@ -604,4 +609,86 @@ describe("DataTreeEvaluator", () => {
"Text4.text": ["Table1.selectedRow.test"], "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\" }");
});
}); });

View File

@ -1,4 +1,7 @@
import { DataTree } from "entities/DataTree/dataTreeFactory"; import {
DataTree,
EvaluationSubstitutionType,
} from "entities/DataTree/dataTreeFactory";
import { import {
DependencyMap, DependencyMap,
EVAL_WORKER_ACTIONS, EVAL_WORKER_ACTIONS,
@ -109,6 +112,7 @@ ctx.addEventListener(
const triggers = dataTreeEvaluator.getDynamicValue( const triggers = dataTreeEvaluator.getDynamicValue(
dynamicTrigger, dynamicTrigger,
evalTree, evalTree,
EvaluationSubstitutionType.TEMPLATE,
true, true,
callbackData, callbackData,
); );

View File

@ -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);
});
});
});

View File

@ -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<string, unknown> = {};
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<string, unknown> } => {
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,
);
}
};

View File

@ -56,6 +56,7 @@ describe("Add functions", () => {
isLoading: false, isLoading: false,
run: {}, run: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION, ENTITY_TYPE: ENTITY_TYPE.ACTION,
dependencyMap: {},
}, },
}; };
const dataTreeWithFunctions = addFunctions(dataTree); const dataTreeWithFunctions = addFunctions(dataTree);