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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,6 +33,13 @@ export const DEFAULT_API_ACTION_CONFIG: ApiActionConfig = {
httpMethod: HTTP_METHODS[0],
headers: EMPTY_KEY_VALUE_PAIRS.slice(),
queryParameters: EMPTY_KEY_VALUE_PAIRS.slice(),
body: "",
pluginSpecifiedTemplates: [
{
// JSON smart substitution
value: false,
},
],
};
export const DEFAULT_PROVIDER_OPTION = "Business Software";

View File

@ -5,6 +5,7 @@ import queryActionSettingsConfig from "constants/AppsmithActionConstants/formCon
import apiActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/ApiSettingsConfig";
import apiActionEditorConfig from "constants/AppsmithActionConstants/formConfig/ApiEditorConfigs";
import saasActionSettingsConfig from "constants/AppsmithActionConstants/formConfig/GoogleSheetsSettingsConfig";
import apiActionDependencyConfig from "constants/AppsmithActionConstants/formConfig/ApiDependencyConfigs";
export type ExecuteActionPayloadEvent = {
type: EventType;
@ -123,3 +124,12 @@ export const defaultActionEditorConfigs: Record<PluginType, any> = {
[PluginType.DB]: [],
[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 [
{
dependencies: {
"actionConfiguration.body": [
"actionConfiguration.pluginSpecifiedTemplates[0].value",
],
},
},
];
export default {
"actionConfiguration.body": [
"actionConfiguration.pluginSpecifiedTemplates[0].value",
],
};

View File

@ -12,6 +12,23 @@ export default [
label: "Body",
configProperty: "actionConfiguration.body",
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
evaluationSubstitutionType: "SMART_SUBSTITUTE",
hidden: {
path: "actionConfiguration.pluginSpecifiedTemplates[0].value",
comparison: "EQUALS",
value: false,
},
},
{
label: "Body",
configProperty: "actionConfiguration.body",
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
evaluationSubstitutionType: "TEMPLATE",
hidden: {
path: "actionConfiguration.pluginSpecifiedTemplates[0].value",
comparison: "EQUALS",
value: true,
},
},
{
label: "Query Parameters",

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { Action, PluginType } from "entities/Action/index";
import { getBindingPathsOfAction } from "entities/Action/actionProperties";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
const DEFAULT_ACTION: Action = {
actionConfiguration: {},
@ -24,9 +25,9 @@ describe("getBindingPathsOfAction", () => {
it("returns default list of no config is sent", () => {
const response = getBindingPathsOfAction(DEFAULT_ACTION, undefined);
expect(response).toStrictEqual({
data: true,
isLoading: true,
config: true,
data: EvaluationSubstitutionType.TEMPLATE,
isLoading: EvaluationSubstitutionType.TEMPLATE,
config: EvaluationSubstitutionType.TEMPLATE,
});
});
@ -46,6 +47,18 @@ describe("getBindingPathsOfAction", () => {
configProperty: "actionConfiguration.body2",
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
},
{
label: "",
configProperty: "actionConfiguration.field1",
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
evaluationSubstitutionType: "SMART_SUBSTITUTE",
},
{
label: "",
configProperty: "actionConfiguration.field2",
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
evaluationSubstitutionType: "PARAMETER",
},
],
},
];
@ -54,15 +67,19 @@ describe("getBindingPathsOfAction", () => {
actionConfiguration: {
body: "basic action",
body2: "another body",
field1: "test",
field2: "anotherTest",
},
};
const response = getBindingPathsOfAction(basicAction, config);
expect(response).toStrictEqual({
data: true,
isLoading: true,
"config.body": true,
"config.body2": true,
data: EvaluationSubstitutionType.TEMPLATE,
isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.body": EvaluationSubstitutionType.TEMPLATE,
"config.body2": EvaluationSubstitutionType.TEMPLATE,
"config.field1": EvaluationSubstitutionType.SMART_SUBSTITUTE,
"config.field2": EvaluationSubstitutionType.PARAMETER,
});
});
@ -122,12 +139,12 @@ describe("getBindingPathsOfAction", () => {
// @ts-ignore
const response = getBindingPathsOfAction(basicAction, config);
expect(response).toStrictEqual({
data: true,
isLoading: true,
"config.params[0].key": true,
"config.params[0].value": true,
"config.params[1].key": true,
"config.params[1].value": true,
data: EvaluationSubstitutionType.TEMPLATE,
isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.params[0].key": EvaluationSubstitutionType.TEMPLATE,
"config.params[0].value": EvaluationSubstitutionType.TEMPLATE,
"config.params[1].key": EvaluationSubstitutionType.TEMPLATE,
"config.params[1].value": EvaluationSubstitutionType.TEMPLATE,
});
});
@ -177,10 +194,76 @@ describe("getBindingPathsOfAction", () => {
// @ts-ignore
const response = getBindingPathsOfAction(basicAction, config);
expect(response).toStrictEqual({
data: true,
isLoading: true,
"config.key": true,
"config.value": true,
data: EvaluationSubstitutionType.TEMPLATE,
isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.key": EvaluationSubstitutionType.TEMPLATE,
"config.value": EvaluationSubstitutionType.TEMPLATE,
});
});
it("checks for hidden field and returns bindingPaths accordingly", () => {
const config = [
{
sectionName: "",
id: 1,
children: [
{
label: "",
configProperty: "actionConfiguration.body",
controlType: "QUERY_DYNAMIC_TEXT",
},
{
label: "",
configProperty: "actionConfiguration.body2",
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
hidden: {
path: "actionConfiguration.template.setting",
comparison: "EQUALS",
value: false,
},
},
{
label: "",
configProperty: "actionConfiguration.field1",
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
evaluationSubstitutionType: "SMART_SUBSTITUTE",
hidden: {
path: "actionConfiguration.template.setting",
comparison: "EQUALS",
value: true,
},
},
],
},
];
const basicAction = {
...DEFAULT_ACTION,
actionConfiguration: {
body: "basic action",
body2: "another body",
field1: "alternate body",
template: {
setting: false,
},
},
};
const response = getBindingPathsOfAction(basicAction, config);
expect(response).toStrictEqual({
data: EvaluationSubstitutionType.TEMPLATE,
isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.body": EvaluationSubstitutionType.TEMPLATE,
"config.field1": EvaluationSubstitutionType.SMART_SUBSTITUTE,
});
basicAction.actionConfiguration.template.setting = true;
const response2 = getBindingPathsOfAction(basicAction, config);
expect(response2).toStrictEqual({
data: EvaluationSubstitutionType.TEMPLATE,
isLoading: EvaluationSubstitutionType.TEMPLATE,
"config.body": EvaluationSubstitutionType.TEMPLATE,
"config.body2": EvaluationSubstitutionType.TEMPLATE,
});
});
});

View File

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

View File

@ -17,6 +17,7 @@ export enum PaginationType {
export interface ActionConfig {
timeoutInMillisecond?: number;
paginationType?: PaginationType;
pluginSpecifiedTemplates?: Array<{ key?: string; value?: unknown }>;
}
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 { ActionData } from "reducers/entityReducers/actionsReducer";
import { getBindingPathsOfAction } from "entities/Action/actionProperties";
import {
getBindingPathsOfAction,
getDataTreeActionConfigPath,
} from "entities/Action/actionProperties";
export const generateDataTreeAction = (
action: ActionData,
editorConfig: any[],
dependencyConfig: DependencyMap = {},
): DataTreeAction => {
let dynamicBindingPathList: DynamicPath[] = [];
// update paths
@ -18,6 +22,12 @@ export const generateDataTreeAction = (
key: `config.${d.key}`,
}));
}
const dependencyMap: DependencyMap = {};
Object.entries(dependencyConfig).forEach(([dependent, dependencies]) => {
dependencyMap[getDataTreeActionConfigPath(dependent)] = dependencies.map(
getDataTreeActionConfigPath,
);
});
return {
run: {},
actionId: action.config.id,
@ -29,5 +39,6 @@ export const generateDataTreeAction = (
ENTITY_TYPE: ENTITY_TYPE.ACTION,
isLoading: action.isLoading,
bindingPaths: getBindingPathsOfAction(action.config, editorConfig),
dependencyMap,
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -67,7 +67,6 @@ import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages";
import { checkCurrentStep } from "./OnboardingSagas";
import { OnboardingStep } from "constants/OnboardingConstants";
import { getIndextoUpdate } from "utils/ApiPaneUtils";
import { changeQuery } from "actions/queryPaneActions";
function* syncApiParamsSaga(
actionPayload: ReduxActionWithMeta<string, { field: string }>,
@ -208,29 +207,10 @@ function* initializeExtraFormDataSaga() {
const { extraformData } = state.ui.apiPane;
const formData = yield select(getFormData, API_EDITOR_FORM_NAME);
const { values } = formData;
const headers = get(
values,
"actionConfiguration.headers",
DEFAULT_API_ACTION_CONFIG.headers,
);
const headers = get(values, "actionConfiguration.headers");
const queryParameters = get(
values,
"actionConfiguration.queryParameters",
[],
);
if (!extraformData[values.id]) {
yield put(
change(API_EDITOR_FORM_NAME, "actionConfiguration.headers", headers),
);
if (queryParameters.length === 0)
yield put(
change(
API_EDITOR_FORM_NAME,
"actionConfiguration.queryParameters",
DEFAULT_API_ACTION_CONFIG.queryParameters,
),
);
yield call(setHeaderFormat, values.id, headers);
}
}
@ -270,6 +250,42 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) {
PerformanceTracker.stopTracking();
}
function* setHeaderFormat(apiId: string, headers?: Property[]) {
let displayFormat;
if (headers) {
const contentType = headers.find(
(header: any) =>
header &&
header.key &&
header.key.toLowerCase() === CONTENT_TYPE_HEADER_KEY,
);
if (
contentType &&
contentType.value &&
POST_BODY_FORMATS.includes(contentType.value)
) {
displayFormat = {
label: contentType.value,
value: contentType.value,
};
} else {
displayFormat = POST_BODY_FORMAT_OPTIONS[3];
}
}
yield put({
type: ReduxActionTypes.SET_EXTRA_FORMDATA,
payload: {
id: apiId,
values: {
displayFormat,
},
},
});
}
function* updateFormFields(
actionPayload: ReduxActionWithMeta<string, { field: string }>,
) {
@ -318,35 +334,7 @@ function* updateFormFields(
"actionConfiguration.headers",
);
const apiId = get(values, "id");
let displayFormat;
if (actionConfigurationHeaders) {
const contentType = actionConfigurationHeaders.find(
(header: any) =>
header &&
header.key &&
header.key.toLowerCase() === CONTENT_TYPE_HEADER_KEY,
);
if (contentType && POST_BODY_FORMATS.includes(contentType.value)) {
displayFormat = {
label: contentType.value,
value: contentType.value,
};
} else {
displayFormat = POST_BODY_FORMAT_OPTIONS[3];
}
}
yield put({
type: ReduxActionTypes.SET_EXTRA_FORMDATA,
payload: {
id: apiId,
values: {
displayFormat,
},
},
});
yield call(setHeaderFormat, apiId, actionConfigurationHeaders);
}
}
@ -562,12 +550,6 @@ function* handleApiNameChangeFailureSaga(
yield put(change(API_EDITOR_FORM_NAME, "name", action.payload.oldName));
}
function* updateFormValues(action: ReduxAction<{ data: Action }>) {
if (action.payload.data.pluginType === PluginType.API) {
yield call(changeApiSaga, changeQuery(action.payload.data.id));
}
}
export default function* root() {
yield all([
takeEvery(ReduxActionTypes.API_PANE_CHANGE_API, changeApiSaga),
@ -597,7 +579,6 @@ export default function* root() {
ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE,
handleUpdateBodyContentType,
),
takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, updateFormValues),
// Intercepting the redux-form change actionType
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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", () => {
it("generates the same object if it is run with the same dsl", () => {
const nextState = compareAndGenerateImmutableCanvasStructure(
@ -356,15 +90,6 @@ describe("Immutable Canvas structures", () => {
expect(nextState).toBe(canvasStructure);
});
it("calculates 100 simple diffs in less than 30ms", () => {
const start = performance.now();
for (let i = 0; i < 100; i++) {
compareAndGenerateImmutableCanvasStructure(canvasStructure, newDSL);
}
console.log("Time taken for 100 runs: ", performance.now() - start, "ms");
const timeTaken = performance.now() - start;
expect(timeTaken).toBeLessThanOrEqual(100);
});
it("updates the diff appropriately", () => {
const dsl: any = {
widgetId: "x",

View File

@ -1,6 +1,5 @@
import {
DependencyMap,
EntityWithBindings,
EvalError,
EvalErrorTypes,
getDynamicBindings,
@ -17,6 +16,7 @@ import {
DataTreeObjectEntity,
DataTreeWidget,
ENTITY_TYPE,
EvaluationSubstitutionType,
} from "entities/DataTree/dataTreeFactory";
import {
addDependantsOfNestedPropertyPaths,
@ -43,6 +43,7 @@ import {
} from "constants/AppsmithActionConstants/ActionConstants";
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
import evaluate, { EvalResult } from "workers/evaluate";
import { substituteDynamicBindingWithValues } from "workers/evaluationSubstitution";
export default class DataTreeEvaluator {
dependencyMap: DependencyMap = {};
@ -308,7 +309,6 @@ export default class DataTreeEvaluator {
),
);
});
// TODO make this run only for widgets and not actions
dependencyMap = makeParentsDependOnChildren(dependencyMap);
return dependencyMap;
}
@ -331,15 +331,26 @@ export default class DataTreeEvaluator {
);
});
}
if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) {
if (isWidget(entity)) {
// Set default property dependency
const defaultProperties = this.widgetConfigMap[entity.type]
.defaultProperties;
Object.keys(defaultProperties).forEach((property) => {
dependencies[`${entityName}.${property}`] = [
`${entityName}.${defaultProperties[property]}`,
];
});
Object.entries(defaultProperties).forEach(
([property, defaultPropertyPath]) => {
dependencies[`${entityName}.${property}`] = [
`${entityName}.${defaultPropertyPath}`,
];
},
);
}
if (isAction(entity)) {
Object.entries(entity.dependencyMap).forEach(
([dependent, entityDependencies]) => {
dependencies[`${entityName}.${dependent}`] = entityDependencies.map(
(propertyPath) => `${entityName}.${propertyPath}`,
);
},
);
}
return dependencies;
}
@ -356,7 +367,9 @@ export default class DataTreeEvaluator {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
fullPropertyPath,
);
const entity: DataTreeEntity = currentTree[entityName];
const entity = currentTree[entityName] as
| DataTreeWidget
| DataTreeAction;
const unEvalPropertyValue = _.get(
currentTree as any,
fullPropertyPath,
@ -368,10 +381,15 @@ export default class DataTreeEvaluator {
const requiresEval =
isABindingPath && isDynamicValue(unEvalPropertyValue);
if (requiresEval) {
const evaluationSubstitutionType =
entity.bindingPaths[propertyPath] ||
EvaluationSubstitutionType.TEMPLATE;
try {
evalPropertyValue = this.evaluateDynamicProperty(
currentTree,
evalPropertyValue = this.getDynamicValue(
unEvalPropertyValue,
currentTree,
evaluationSubstitutionType,
false,
);
} catch (e) {
this.errors.push({
@ -512,6 +530,7 @@ export default class DataTreeEvaluator {
getDynamicValue(
dynamicBinding: string,
data: DataTree,
evaluationSubstitutionType: EvaluationSubstitutionType,
returnTriggers: boolean,
callBackData?: Array<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];
// else return a string template with bindings
return createDynamicValueString(dynamicBinding, stringSegments, values);
// else return a combined value according to the evaluation type
return substituteDynamicBindingWithValues(
dynamicBinding,
stringSegments,
values,
evaluationSubstitutionType,
);
}
return undefined;
}
@ -569,13 +593,6 @@ export default class DataTreeEvaluator {
}
}
evaluateDynamicProperty(
currentTree: DataTree,
unEvalPropertyValue: any,
): any {
return this.getDynamicValue(unEvalPropertyValue, currentTree, false);
}
validateAndParseWidgetProperty(
fullPropertyPath: string,
widget: DataTreeWidget,
@ -590,6 +607,7 @@ export default class DataTreeEvaluator {
const { triggers } = this.getDynamicValue(
unEvalPropertyValue,
currentTree,
EvaluationSubstitutionType.TEMPLATE,
true,
undefined,
);
@ -695,21 +713,37 @@ export default class DataTreeEvaluator {
if (entityType !== "noop") {
switch (dataTreeDiff.event) {
case DataTreeDiffEvent.NEW: {
// If a new widget was added, add all the internal bindings for this widget to the global dependency map
// If a new entity/property was added, add all the internal bindings for this entity to the global dependency map
if (
isWidget(entity) &&
(isWidget(entity) || isAction(entity)) &&
!this.isDynamicLeaf(
unEvalDataTree,
dataTreeDiff.payload.propertyPath,
)
) {
const widgetDependencyMap: DependencyMap = this.listEntityDependencies(
entity as DataTreeWidget,
const entityDependencyMap: DependencyMap = this.listEntityDependencies(
entity,
entityName,
);
if (Object.keys(widgetDependencyMap).length) {
if (Object.keys(entityDependencyMap).length) {
didUpdateDependencyMap = true;
Object.assign(this.dependencyMap, widgetDependencyMap);
// The entity might already have some dependencies,
// so we just want to update those
Object.entries(entityDependencyMap).forEach(
([entityDependent, entityDependencies]) => {
if (this.dependencyMap[entityDependent]) {
this.dependencyMap[
entityDependent
] = this.dependencyMap[entityDependent].concat(
entityDependencies,
);
} else {
this.dependencyMap[
entityDependent
] = entityDependencies;
}
},
);
}
}
// Either a new entity or a new property path has been added. Go through existing dynamic bindings and
@ -735,14 +769,14 @@ export default class DataTreeEvaluator {
removedPaths.push(dataTreeDiff.payload.propertyPath);
// If an existing widget was deleted, remove all the bindings from the global dependency map
if (
isWidget(entity) &&
(isWidget(entity) || isAction(entity)) &&
dataTreeDiff.payload.propertyPath === entityName
) {
const widgetBindings = this.listEntityDependencies(
const entityDependencies = this.listEntityDependencies(
entity,
entityName,
);
Object.keys(widgetBindings).forEach((widgetDep) => {
Object.keys(entityDependencies).forEach((widgetDep) => {
didUpdateDependencyMap = true;
delete this.dependencyMap[widgetDep];
});
@ -783,23 +817,22 @@ export default class DataTreeEvaluator {
}
case DataTreeDiffEvent.EDIT: {
// We only care about dependencies for a widget. This is because in case a dependency of an action changes,
// that shouldn't trigger an evaluation.
// Also for a widget, we only care if the difference is in dynamic bindings since static values do not need
// We only care if the difference is in dynamic bindings since static values do not need
// an evaluation.
if (
(entityType === ENTITY_TYPE.WIDGET ||
entityType === ENTITY_TYPE.ACTION) &&
(isWidget(entity) || isAction(entity)) &&
typeof dataTreeDiff.payload.value === "string"
) {
const entity: EntityWithBindings = unEvalDataTree[
const entity: DataTreeAction | DataTreeWidget = unEvalDataTree[
entityName
] as EntityWithBindings;
] as DataTreeAction | DataTreeWidget;
const fullPropertyPath = dataTreeDiff.payload.propertyPath;
const entityPropertyPath = fullPropertyPath.substring(
fullPropertyPath.indexOf(".") + 1,
);
const isABindingPath = isPathADynamicBinding(
entity,
dataTreeDiff.payload.propertyPath.substring(
dataTreeDiff.payload.propertyPath.indexOf(".") + 1,
),
entityPropertyPath,
);
if (isABindingPath) {
didUpdateDependencyMap = true;
@ -813,15 +846,30 @@ export default class DataTreeEvaluator {
// We found a new dynamic binding for this property path. We update the dependency map by overwriting the
// dependencies for this property path with the newly found dependencies
if (correctSnippets.length) {
this.dependencyMap[
dataTreeDiff.payload.propertyPath
] = correctSnippets;
this.dependencyMap[fullPropertyPath] = correctSnippets;
} else {
// The dependency on this property path has been removed. Delete this property path from the global
// dependency map
delete this.dependencyMap[
dataTreeDiff.payload.propertyPath
];
delete this.dependencyMap[fullPropertyPath];
}
if (isAction(entity)) {
// Actions have a defined dependency map that should always be maintained
if (entityPropertyPath in entity.dependencyMap) {
const entityDependenciesName = entity.dependencyMap[
entityPropertyPath
].map((dep) => `${entityName}.${dep}`);
if (fullPropertyPath in this.dependencyMap) {
this.dependencyMap[
fullPropertyPath
] = this.dependencyMap[fullPropertyPath].concat(
entityDependenciesName,
);
} else {
this.dependencyMap[
fullPropertyPath
] = entityDependenciesName;
}
}
}
}
}
@ -838,9 +886,11 @@ export default class DataTreeEvaluator {
if (didUpdateDependencyMap) {
// TODO Optimise
Object.keys(this.dependencyMap).forEach((key) => {
this.dependencyMap[key] = _.flatten(
this.dependencyMap[key].map((path) =>
extractReferencesFromBinding(path, this.allKeys),
this.dependencyMap[key] = _.uniq(
_.flatten(
this.dependencyMap[key].map((path) =>
extractReferencesFromBinding(path, this.allKeys),
),
),
);
});
@ -1003,6 +1053,7 @@ export default class DataTreeEvaluator {
evaluatedExecutionParams = this.getDynamicValue(
`{{${JSON.stringify(executionParams)}}}`,
this.evalTree,
EvaluationSubstitutionType.TEMPLATE,
false,
);
}
@ -1021,6 +1072,7 @@ export default class DataTreeEvaluator {
this.getDynamicValue(
`{{${binding}}}`,
dataTreeWithExecutionParams,
EvaluationSubstitutionType.TEMPLATE,
false,
),
);
@ -1070,31 +1122,6 @@ const extractReferencesFromBinding = (
// referencing DATA_BIND_REGEX fails for the value "{{Table1.tableData[Table1.selectedRowIndex]}}" if you run it multiple times and don't recreate
const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value);
// For creating a final value where bindings could be in a template format
const createDynamicValueString = (
binding: string,
subBindings: string[],
subValues: string[],
): string => {
// Replace the string with the data tree values
let finalValue = binding;
subBindings.forEach((b, i) => {
let value = subValues[i];
if (Array.isArray(value) || _.isObject(value)) {
value = JSON.stringify(value);
}
try {
if (JSON.parse(value)) {
value = value.replace(/\\([\s\S])|(")/g, "\\$1$2");
}
} catch (e) {
// do nothing
}
finalValue = finalValue.replace(b, value);
});
return finalValue;
};
function isValidEntity(entity: DataTreeEntity): entity is DataTreeObjectEntity {
if (!_.isObject(entity)) {
// ERRORS.push({

View File

@ -2,6 +2,7 @@ import {
DataTreeAction,
DataTreeWidget,
ENTITY_TYPE,
EvaluationSubstitutionType,
} from "../entities/DataTree/dataTreeFactory";
import { WidgetTypeConfigMap } from "../utils/WidgetFactory";
import { RenderModes, WidgetTypes } from "../constants/WidgetConstants";
@ -238,9 +239,10 @@ const BASE_ACTION: DataTreeAction = {
data: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
bindingPaths: {
isLoading: true,
data: true,
isLoading: EvaluationSubstitutionType.TEMPLATE,
data: EvaluationSubstitutionType.TEMPLATE,
},
dependencyMap: {},
};
describe("DataTreeEvaluator", () => {
@ -251,7 +253,10 @@ describe("DataTreeEvaluator", () => {
text: "Label",
type: WidgetTypes.TEXT_WIDGET,
bindingPaths: {
text: true,
text: EvaluationSubstitutionType.TEMPLATE,
},
validationPaths: {
text: VALIDATION_TYPES.TEXT,
},
},
Text2: {
@ -261,7 +266,7 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET,
bindingPaths: {
text: true,
text: EvaluationSubstitutionType.TEMPLATE,
},
validationPaths: {
text: VALIDATION_TYPES.TEXT,
@ -274,7 +279,7 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET,
bindingPaths: {
text: true,
text: EvaluationSubstitutionType.TEMPLATE,
},
validationPaths: {
text: VALIDATION_TYPES.TEXT,
@ -294,18 +299,18 @@ describe("DataTreeEvaluator", () => {
],
type: WidgetTypes.DROP_DOWN_WIDGET,
bindingPaths: {
options: true,
defaultOptionValue: true,
isRequired: true,
isVisible: true,
isDisabled: true,
isValid: true,
selectedOption: true,
selectedOptionArr: true,
selectedIndex: true,
selectedIndexArr: true,
value: true,
selectedOptionValues: true,
options: EvaluationSubstitutionType.TEMPLATE,
defaultOptionValue: EvaluationSubstitutionType.TEMPLATE,
isRequired: EvaluationSubstitutionType.TEMPLATE,
isVisible: EvaluationSubstitutionType.TEMPLATE,
isDisabled: EvaluationSubstitutionType.TEMPLATE,
isValid: EvaluationSubstitutionType.TEMPLATE,
selectedOption: EvaluationSubstitutionType.TEMPLATE,
selectedOptionArr: EvaluationSubstitutionType.TEMPLATE,
selectedIndex: EvaluationSubstitutionType.TEMPLATE,
selectedIndexArr: EvaluationSubstitutionType.TEMPLATE,
value: EvaluationSubstitutionType.TEMPLATE,
selectedOptionValues: EvaluationSubstitutionType.TEMPLATE,
},
},
Table1: {
@ -314,9 +319,9 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "tableData" }],
type: WidgetTypes.TABLE_WIDGET,
bindingPaths: {
tableData: true,
selectedRow: true,
selectedRows: true,
tableData: EvaluationSubstitutionType.TEMPLATE,
selectedRow: EvaluationSubstitutionType.TEMPLATE,
selectedRows: EvaluationSubstitutionType.TEMPLATE,
},
validationPaths: {
tableData: VALIDATION_TYPES.TABLE_DATA,
@ -328,7 +333,7 @@ describe("DataTreeEvaluator", () => {
dynamicBindingPathList: [{ key: "text" }],
type: WidgetTypes.TEXT_WIDGET,
bindingPaths: {
text: true,
text: EvaluationSubstitutionType.TEMPLATE,
},
validationPaths: {
text: VALIDATION_TYPES.TEXT,
@ -431,10 +436,10 @@ describe("DataTreeEvaluator", () => {
widgetName: "Input1",
type: WidgetTypes.INPUT_WIDGET,
bindingPaths: {
defaultText: true,
isValid: true,
value: true,
text: true,
defaultText: EvaluationSubstitutionType.TEMPLATE,
isValid: EvaluationSubstitutionType.TEMPLATE,
value: EvaluationSubstitutionType.TEMPLATE,
text: EvaluationSubstitutionType.TEMPLATE,
},
},
};
@ -460,18 +465,18 @@ describe("DataTreeEvaluator", () => {
],
type: WidgetTypes.DROP_DOWN_WIDGET,
bindingPaths: {
options: true,
defaultOptionValue: true,
isRequired: true,
isVisible: true,
isDisabled: true,
isValid: true,
selectedOption: true,
selectedOptionArr: true,
selectedIndex: true,
selectedIndexArr: true,
value: true,
selectedOptionValues: true,
options: EvaluationSubstitutionType.TEMPLATE,
defaultOptionValue: EvaluationSubstitutionType.TEMPLATE,
isRequired: EvaluationSubstitutionType.TEMPLATE,
isVisible: EvaluationSubstitutionType.TEMPLATE,
isDisabled: EvaluationSubstitutionType.TEMPLATE,
isValid: EvaluationSubstitutionType.TEMPLATE,
selectedOption: EvaluationSubstitutionType.TEMPLATE,
selectedOptionArr: EvaluationSubstitutionType.TEMPLATE,
selectedIndex: EvaluationSubstitutionType.TEMPLATE,
selectedIndexArr: EvaluationSubstitutionType.TEMPLATE,
value: EvaluationSubstitutionType.TEMPLATE,
selectedOptionValues: EvaluationSubstitutionType.TEMPLATE,
},
},
};
@ -604,4 +609,86 @@ describe("DataTreeEvaluator", () => {
"Text4.text": ["Table1.selectedRow.test"],
});
});
it("Honors predefined action dependencyMap", () => {
const updatedTree1 = {
...unEvalTree,
Text1: {
...BASE_WIDGET,
text: "Test",
},
Api2: {
...BASE_ACTION,
dependencyMap: {
"config.body": ["config.pluginSpecifiedTemplates[0].value"],
},
bindingPaths: {
...BASE_ACTION.bindingPaths,
"config.body": EvaluationSubstitutionType.TEMPLATE,
},
config: {
...BASE_ACTION.config,
body: "",
pluginSpecifiedTemplates: [
{
value: false,
},
],
},
},
};
evaluator.updateDataTree(updatedTree1);
expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([
"Api2.config.pluginSpecifiedTemplates[0].value",
]);
const updatedTree2 = {
...updatedTree1,
Api2: {
...updatedTree1.Api2,
dynamicBindingPathList: [
{
key: "config.body",
},
],
config: {
...updatedTree1.Api2.config,
body: "{ 'name': {{ Text1.text }} }",
},
},
};
const evaluatedDataTree2 = evaluator.updateDataTree(updatedTree2);
expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([
"Text1.text",
"Api2.config.pluginSpecifiedTemplates[0].value",
]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(evaluatedDataTree2.Api2.config.body).toBe("{ 'name': Test }");
const updatedTree3 = {
...updatedTree2,
Api2: {
...updatedTree2.Api2,
bindingPaths: {
...updatedTree2.Api2.bindingPaths,
"config.body": EvaluationSubstitutionType.SMART_SUBSTITUTE,
},
config: {
...updatedTree2.Api2.config,
pluginSpecifiedTemplates: [
{
value: true,
},
],
},
},
};
const evaluatedDataTree3 = evaluator.updateDataTree(updatedTree3);
expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([
"Text1.text",
"Api2.config.pluginSpecifiedTemplates[0].value",
]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(evaluatedDataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }");
});
});

View File

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

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,
run: {},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
dependencyMap: {},
},
};
const dataTreeWithFunctions = addFunctions(dataTree);