-//
-// );
-// };
-
-type Props = ReduxStateProps &
- RouteComponentProps & { theme?: EditorTheme };
-
-export const EMPTY_RESPONSE: ActionResponse = {
- statusCode: "",
- duration: "",
- body: {},
- headers: {},
- request: {
- headers: {},
- body: {},
- httpMethod: "",
- url: "",
- },
- size: "",
-};
-
const TabbedViewWrapper = styled.div<{ isCentered: boolean }>`
height: calc(100% - 30px);
@@ -160,6 +95,12 @@ const Flex = styled.div`
`;
const NoResponseContainer = styled.div`
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
.${Classes.ICON} {
margin-right: 0px;
svg {
@@ -176,22 +117,43 @@ const NoResponseContainer = styled.div`
const FailedMessage = styled.div`
display: flex;
align-items: center;
- justify-content: space-between;
- width: 100%;
`;
-const ButtonContainer = styled.div`
+const ShowRequestText = styled.a`
display: flex;
- align-items: center;
- span {
- color: ${Colors.Galliano};
- cursor: pointer;
- }
- button {
- margin-left: ${(props) => props.theme.spaces[9]}px;
+ margin-left: ${(props) => props.theme.spaces[1] + 1}px;
+ .${Classes.ICON} {
+ margin-left: ${(props) => props.theme.spaces[1] + 1}px;
}
`;
+interface ReduxStateProps {
+ responses: Record;
+ isRunning: Record;
+}
+
+type Props = ReduxStateProps &
+ RouteComponentProps & { theme?: EditorTheme };
+
+export const EMPTY_RESPONSE: ActionResponse = {
+ statusCode: "",
+ duration: "",
+ body: {},
+ headers: {},
+ request: {
+ headers: {},
+ body: {},
+ httpMethod: "",
+ url: "",
+ },
+ size: "",
+};
+
+const StatusCodeText = styled(BaseText)<{ code: string }>`
+ color: ${(props) =>
+ props.code.startsWith("2") ? props.theme.colors.primaryOld : Colors.RED};
+`;
+
const ApiResponseView = (props: Props) => {
const {
match: {
@@ -219,38 +181,32 @@ const ApiResponseView = (props: Props) => {
key: "body",
title: "Response Body",
panelComponent: (
- <>
+
{hasFailed && !isRunning && requestDebugVisible && (
- {CHECK_REQUEST_BODY}
-
- {
- setRequestDebugVisible(false);
- }}
- >
- {DONT_SHOW_THIS_AGAIN}
+ {
+ setSelectedIndex(1);
+ }}
+ >
+
+ {SHOW_REQUEST}
-
+
+
}
+ variant={Variant.warning}
+ fill
+ closeButton
+ onClose={() => setRequestDebugVisible(false)}
/>
)}
- {_.isEmpty(response.body) ? (
+ {_.isEmpty(response.statusCode) ? (
Hit Run to get a Response
@@ -265,7 +221,7 @@ const ApiResponseView = (props: Props) => {
height={"100%"}
/>
)}
- >
+
),
},
{
@@ -287,7 +243,7 @@ const ApiResponseView = (props: Props) => {
];
return (
-
+
{isRunning && (
@@ -334,7 +290,7 @@ const ApiResponseView = (props: Props) => {
onSelect={setSelectedIndex}
/>
-
+
);
};
diff --git a/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx b/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx
index 5bcd3f3737..371b984b6f 100644
--- a/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx
+++ b/app/client/src/components/editorComponents/CodeEditor/BindingPrompt.tsx
@@ -1,6 +1,5 @@
import React, { useRef } from "react";
import styled from "styled-components";
-import { Colors } from "constants/Colors";
import { EditorTheme } from "./EditorConfig";
const Wrapper = styled.span<{
@@ -11,15 +10,11 @@ const Wrapper = styled.span<{
}>`
padding: ${(props) => (props.customMessage ? 6 : 8)}px;
font-size: 12px;
- color: #858282;
+ color: #ffffff;
box-shadow: 0px 12px 34px -6px rgba(0, 0, 0, 0.75);
border-radius: 0px;
background-color: ${(props) =>
- props.editorTheme === EditorTheme.DARK
- ? Colors.MINE_SHAFT
- : props.editorTheme === EditorTheme.LIGHT
- ? Colors.MERCURY
- : Colors.BLUE_CHARCOAL};
+ props.theme.colors.codeMirror.background.hoverState};
position: absolute;
bottom: ${(props) => -props.bottomOffset}px;
width: 100%;
@@ -29,8 +24,8 @@ const Wrapper = styled.span<{
`;
const CurlyBraces = styled.span`
- color: white;
- background-color: #f3672a;
+ color: ${(props) => props.theme.colors.codeMirror.background.hoverState};
+ background-color: #ffffff;
border-radius: 2px;
padding: 2px;
margin: 0px 2px;
diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
index 8af747b974..dce5b987ae 100644
--- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
@@ -45,6 +45,15 @@ export const HintStyles = createGlobalStyle<{
font-size: 12px;
line-height: 15px;
letter-spacing: -0.24px;
+ &:hover {
+ background: ${(props) =>
+ props.theme.colors.codeMirror.background.hoverState};
+ border-radius: 0px;
+ color: #fff;
+ &:after {
+ color: #fff;
+ }
+ }
}
.datasource-hint {
@@ -56,12 +65,6 @@ export const HintStyles = createGlobalStyle<{
overflow: hidden;
text-overflow: ellipsis;
}
-
- li.CodeMirror-hint-active {
- background: ${(props) =>
- props.theme.colors.codeMirror.background.hoverState};
- border-radius: 0px;
- }
.CodeMirror-Tern-completion {
padding-left: ${(props) => props.theme.spaces[11]}px !important;
&:hover{
@@ -132,6 +135,15 @@ export const HintStyles = createGlobalStyle<{
.CodeMirror-Tern-tooltip {
z-index: 20 !important;
}
+ li.CodeMirror-hint-active {
+ background: ${(props) =>
+ props.theme.colors.codeMirror.background.hoverState};
+ border-radius: 0px;
+ color: #fff;
+ &:after {
+ color: #fff;
+ }
+ }
.CodeMirror-Tern-hint-doc {
display: none;
&.visible {
@@ -176,7 +188,7 @@ const getBorderStyle = (
};
const editorBackground = (theme?: EditorTheme) => {
- let bg = "#FFFFFF";
+ let bg = "#FAFAFA";
switch (theme) {
case EditorTheme.DARK:
bg = "#1A191C";
@@ -269,7 +281,9 @@ export const EditorWrapper = styled.div<{
? `border-bottom: 1px solid ${Colors.MERCURY}`
: `border: 1px solid ${Colors.MERCURY}`};
background: ${(props) =>
- props.isFocused || props.fill ? Colors.MERCURY : Colors.WHITE};
+ props.isFocused || props.fill
+ ? Colors.MERCURY
+ : props.theme.colors.codeMirror.background.defaultState};
color: ${Colors.CHARCOAL};
& {
span.cm-operator {
@@ -289,7 +303,9 @@ export const EditorWrapper = styled.div<{
? `border-bottom: 1px solid ${Colors.NERO}`
: `border: 1px solid ${Colors.NERO}`};
background: ${(props) =>
- props.isFocused || props.fill ? Colors.NERO : Colors.BALTIC_SEA};
+ props.isFocused || props.fill
+ ? Colors.NERO
+ : props.theme.colors.codeMirror.background.defaultState};
color: ${Colors.LIGHT_GREY};
}
.cm-s-duotone-light .CodeMirror-linenumber,
diff --git a/app/client/src/components/editorComponents/LoadingOverlayScreen.tsx b/app/client/src/components/editorComponents/LoadingOverlayScreen.tsx
index b8db6a05d5..03bd62ea12 100644
--- a/app/client/src/components/editorComponents/LoadingOverlayScreen.tsx
+++ b/app/client/src/components/editorComponents/LoadingOverlayScreen.tsx
@@ -9,8 +9,8 @@ export default styled.div<{ theme?: EditorTheme }>`
left: 0;
background-color: ${(props) =>
props.theme === EditorTheme.DARK
- ? "rgba(0, 0, 0, 0.6)"
- : "rgba(255, 255, 255, 0.6)"};
+ ? "rgba(0, 0, 0, 0.8)"
+ : "rgba(255, 255, 255, 0.8)"};
pointer-events: none;
z-index: 10;
color: ${(props) =>
diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx
index 7dd75e5858..3dd3a6621d 100644
--- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx
+++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx
@@ -103,7 +103,7 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
- {props.fields.length && (
+ {props.fields.length > 0 && (
{props.fields.map((field: any, index: number) => {
const otherProps: Record = {};
@@ -195,20 +195,18 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => {
);
})}
- props.fields.push({ key: "", value: "" })}
- >
-
-
- Add more
-
-
)}
+ props.fields.push({ key: "", value: "" })}>
+
+
+ Add more
+
+
);
};
diff --git a/app/client/src/components/formControls/SwitchControl.tsx b/app/client/src/components/formControls/SwitchControl.tsx
index 4544fb4fd8..d35b3eb8fc 100644
--- a/app/client/src/components/formControls/SwitchControl.tsx
+++ b/app/client/src/components/formControls/SwitchControl.tsx
@@ -32,6 +32,15 @@ const Info = styled.div`
`;
export class SwitchField extends React.Component {
+ get value() {
+ const { input } = this.props;
+ if (typeof input.value !== "string") return !!input.value;
+ else {
+ if (input.value.toLocaleLowerCase() === "false") return false;
+ else return !!input.value;
+ }
+ }
+
render() {
const { label, isRequired, input, info } = this.props;
@@ -42,7 +51,7 @@ export class SwitchField extends React.Component {
{label} {isRequired && "*"}
input.onChange(value)}
large
/>
diff --git a/app/client/src/components/propertyControls/ActionSelectorControl.tsx b/app/client/src/components/propertyControls/ActionSelectorControl.tsx
index f2eda411a9..a2f1d18780 100644
--- a/app/client/src/components/propertyControls/ActionSelectorControl.tsx
+++ b/app/client/src/components/propertyControls/ActionSelectorControl.tsx
@@ -2,7 +2,6 @@ import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
// import DynamicActionCreator from "components/editorComponents/DynamicActionCreator";
import { ActionCreator } from "components/editorComponents/actioncreator/ActionCreator";
-import { ColumnProperties } from "components/designSystems/appsmith/TableComponent/Constants";
class ActionSelectorControl extends BaseControl {
handleValueUpdate = (newValue: string) => {
@@ -12,28 +11,14 @@ class ActionSelectorControl extends BaseControl {
render() {
const { propertyValue } = this.props;
- /* The following code is very specific to the table columns */
- const { widgetProperties } = this.props;
- let additionalAutoComplete = {};
- if (
- this.props.customJSControl &&
- this.props.customJSControl === "COMPUTE_VALUE"
- ) {
- const columns: ColumnProperties[] = widgetProperties.primaryColumns || [];
- const currentRow: { [key: string]: any } = {};
- for (let i = 0; i < columns.length; i++) {
- currentRow[columns[i].id] = undefined;
- }
- additionalAutoComplete = { currentRow };
- }
- /* EO specific code */
+
return (
);
}
diff --git a/app/client/src/components/propertyControls/BaseControl.tsx b/app/client/src/components/propertyControls/BaseControl.tsx
index 387604ab56..3d0ceba178 100644
--- a/app/client/src/components/propertyControls/BaseControl.tsx
+++ b/app/client/src/components/propertyControls/BaseControl.tsx
@@ -36,8 +36,10 @@ export interface ControlBuilder {
export interface ControlProps extends ControlData, ControlFunctions {
key?: string;
+ additionalAutoComplete?: Record>;
}
-export interface ControlData extends PropertyPaneControlConfig {
+export interface ControlData
+ extends Omit {
propertyValue?: any;
isValid: boolean;
errorMessage?: string;
diff --git a/app/client/src/constants/ActionConstants.tsx b/app/client/src/constants/ActionConstants.tsx
index 59a03159b4..bb4f5a4884 100644
--- a/app/client/src/constants/ActionConstants.tsx
+++ b/app/client/src/constants/ActionConstants.tsx
@@ -27,6 +27,7 @@ export enum EventType {
ON_ROW_SELECTED = "ON_ROW_SELECTED",
ON_SEARCH = "ON_SEARCH",
ON_CLICK = "ON_CLICK",
+ ON_DATA_POINT_CLICK = "ON_DATA_POINT_CLICK",
ON_FILES_SELECTED = "ON_FILES_SELECTED",
ON_HOVER = "ON_HOVER",
ON_TOGGLE = "ON_TOGGLE",
diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx
index 787253e95d..3d53682e0b 100644
--- a/app/client/src/constants/DefaultTheme.tsx
+++ b/app/client/src/constants/DefaultTheme.tsx
@@ -507,6 +507,7 @@ const darkShades = [
"#D4D4D4",
"#E9E9E9",
"#FFFFFF",
+ "#157A96",
] as const;
const lightShades = [
@@ -522,6 +523,7 @@ const lightShades = [
"#302D2D",
"#090707",
"#FFFFFF",
+ "#6A86CE",
] as const;
type ShadeColor = typeof darkShades[number] | typeof lightShades[number];
@@ -604,6 +606,11 @@ type ColorType = {
bg: ShadeColor;
icon: ShadeColor;
};
+ hovered: {
+ text: ShadeColor;
+ bg: ShadeColor;
+ icon: ShadeColor;
+ };
icon: ShadeColor;
};
toggle: {
@@ -672,6 +679,7 @@ type ColorType = {
normal: ShadeColor;
hover: ShadeColor;
border: ShadeColor;
+ countBg: ShadeColor;
};
settingHeading: ShadeColor;
table: {
@@ -793,6 +801,7 @@ type ColorType = {
text: ShadeColor;
dividerBg: ShadeColor;
iconHoverBg: ShadeColor;
+ tabBg: ShadeColor;
requestTree: {
bg: string;
header: {
@@ -1019,6 +1028,11 @@ export const dark: ColorType = {
bg: darkShades[4],
icon: darkShades[8],
},
+ hovered: {
+ text: darkShades[9],
+ bg: darkShades[10],
+ icon: darkShades[8],
+ },
icon: darkShades[6],
},
toggle: {
@@ -1087,6 +1101,7 @@ export const dark: ColorType = {
normal: darkShades[6],
hover: darkShades[7],
border: darkShades[3],
+ countBg: darkShades[4],
},
settingHeading: darkShades[9],
table: {
@@ -1205,6 +1220,7 @@ export const dark: ColorType = {
},
apiPane: {
bg: darkShades[0],
+ tabBg: lightShades[10],
text: darkShades[6],
dividerBg: darkShades[4],
iconHoverBg: darkShades[1],
@@ -1262,7 +1278,7 @@ export const dark: ColorType = {
codeMirror: {
background: {
defaultState: "#262626",
- hoverState: "#1A191C",
+ hoverState: darkShades[10],
},
text: "#FFFFFF",
dataType: {
@@ -1398,6 +1414,11 @@ export const light: ColorType = {
bg: lightShades[2],
icon: lightShades[8],
},
+ hovered: {
+ text: lightShades[11],
+ bg: lightShades[12],
+ icon: lightShades[8],
+ },
icon: lightShades[7],
},
toggle: {
@@ -1466,6 +1487,7 @@ export const light: ColorType = {
normal: lightShades[6],
hover: lightShades[10],
border: lightShades[3],
+ countBg: lightShades[3],
},
settingHeading: lightShades[9],
table: {
@@ -1583,7 +1605,8 @@ export const light: ColorType = {
border: "#E0DEDE",
},
apiPane: {
- bg: lightShades[11],
+ bg: lightShades[0],
+ tabBg: lightShades[11],
text: lightShades[6],
dividerBg: lightShades[3],
iconHoverBg: lightShades[1],
@@ -1640,8 +1663,8 @@ export const light: ColorType = {
},
codeMirror: {
background: {
- defaultState: "#EBEBEB",
- hoverState: "#FAFAFA",
+ defaultState: lightShades[0],
+ hoverState: lightShades[12],
},
text: "#090707",
dataType: {
diff --git a/app/client/src/constants/PropertyControlConstants.tsx b/app/client/src/constants/PropertyControlConstants.tsx
index 10e1e51aa1..0978a1809c 100644
--- a/app/client/src/constants/PropertyControlConstants.tsx
+++ b/app/client/src/constants/PropertyControlConstants.tsx
@@ -42,6 +42,9 @@ export type PropertyPaneControlConfig = {
hidden?: (props: any, propertyPath: string) => boolean;
isBindProperty: boolean;
isTriggerProperty: boolean;
+ additionalAutoComplete?: (
+ props: any,
+ ) => Record>;
};
export type PropertyPaneConfig =
diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts
index 18ac765667..9ca3f8cf96 100644
--- a/app/client/src/constants/messages.ts
+++ b/app/client/src/constants/messages.ts
@@ -159,7 +159,7 @@ export const LIGHTNING_MENU_API_CREATE_NEW = "Create new API";
export const LIGHTNING_MENU_OPTION_TEXT = "Plain Text";
export const LIGHTNING_MENU_OPTION_JS = "Write JS";
export const LIGHTNING_MENU_OPTION_HTML = "Write HTML";
-export const CHECK_REQUEST_BODY = "Please check request body to debug?";
+export const CHECK_REQUEST_BODY = "Please check request body to debug";
export const DONT_SHOW_THIS_AGAIN = "Don't show this again";
export const SHOW_REQUEST = "Show Request";
diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts
index e80ef9406f..612bb5f26d 100644
--- a/app/client/src/entities/DataTree/dataTreeFactory.ts
+++ b/app/client/src/entities/DataTree/dataTreeFactory.ts
@@ -122,6 +122,7 @@ export class DataTreeFactory {
bindingPaths: {
data: true,
isLoading: true,
+ config: true,
},
};
});
diff --git a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx
index 41f6de65fc..f5f4c77857 100644
--- a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx
+++ b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx
@@ -46,7 +46,6 @@ const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
color: white;
flex-direction: column;
.${Classes.TEXT} {
- max-width: 194px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -74,6 +73,10 @@ const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
width: 24px;
height: 24px;
}
+
+ & .current-app-name {
+ overflow: auto;
+ }
`;
const HeaderRow = styled.div<{ justify: string }>`
@@ -117,6 +120,11 @@ const HeaderRightItemContainer = styled.div`
height: 100%;
`;
+const PrimaryLogoLink = styled(Link)`
+ display: flex;
+ align-items: center;
+`;
+
type AppViewerHeaderProps = {
url?: string;
currentApplicationDetails?: ApplicationPayload;
@@ -183,9 +191,9 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
-
+
-
+
{currentApplicationDetails && (
diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx
index e8fb7c5a35..a93cf35ce2 100644
--- a/app/client/src/pages/Editor/APIEditor/Form.tsx
+++ b/app/client/src/pages/Editor/APIEditor/Form.tsx
@@ -177,6 +177,8 @@ interface APIFormProps {
actionName: string;
apiId: string;
apiName: string;
+ headersCount: number;
+ paramsCount: number;
}
type Props = APIFormProps & InjectedFormProps;
@@ -227,6 +229,8 @@ const ApiEditorForm: React.FC = (props: Props) => {
actionConfigurationBody,
httpMethodFromForm,
actionName,
+ headersCount,
+ paramsCount,
} = props;
const allowPostBody =
httpMethodFromForm && httpMethodFromForm !== HTTP_METHODS[0];
@@ -269,12 +273,11 @@ const ApiEditorForm: React.FC = (props: Props) => {
}
>
-
+
@@ -324,6 +327,7 @@ const ApiEditorForm: React.FC = (props: Props) => {
{
key: "headers",
title: "Headers",
+ count: headersCount,
panelComponent: (
{apiBindHelpSectionVisible && (
@@ -358,7 +362,6 @@ const ApiEditorForm: React.FC = (props: Props) => {
actionConfig={actionConfigurationHeaders}
placeholder="Value"
dataTreePath={`${actionName}.config.headers`}
- pushFields
/>
),
@@ -366,6 +369,7 @@ const ApiEditorForm: React.FC = (props: Props) => {
{
key: "params",
title: "Params",
+ count: paramsCount,
panelComponent: (
= (props: Props) => {
name="actionConfiguration.queryParameters"
label="Params"
dataTreePath={`${actionName}.config.queryParameters`}
- pushFields
/>
),
@@ -448,6 +451,11 @@ export default connect((state: AppState) => {
);
const apiId = selector(state, "id");
const actionName = getApiName(state, apiId) || "";
+ const headers = selector(state, "actionConfiguration.headers");
+ const headersCount = Array.isArray(headers) ? headers.length : 0;
+
+ const params = selector(state, "actionConfiguration.queryParameters");
+ const paramsCount = Array.isArray(params) ? params.length : 0;
return {
actionName,
@@ -455,6 +463,8 @@ export default connect((state: AppState) => {
httpMethodFromForm,
actionConfigurationBody,
actionConfigurationHeaders,
+ headersCount,
+ paramsCount,
};
})(
reduxForm({
diff --git a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx
index 5794c490be..dfba605669 100644
--- a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx
+++ b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx
@@ -21,6 +21,7 @@ import {
TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig";
import MultiSwitch from "components/ads/MultiSwitch";
+import { BodyFormData } from "entities/Action";
const PostBodyContainer = styled.div`
padding: 12px 0px 0px;
@@ -49,6 +50,8 @@ interface PostDataProps {
) => void;
dataTreePath: string;
theme?: EditorTheme;
+ bodyFormData?: BodyFormData[];
+ addBodyFormData: () => void;
}
type Props = PostDataProps;
@@ -61,6 +64,8 @@ const PostBodyData = (props: Props) => {
setDisplayFormat,
apiId,
dataTreePath,
+ bodyFormData,
+ addBodyFormData,
} = props;
return (
@@ -95,7 +100,6 @@ const PostBodyData = (props: Props) => {
name="actionConfiguration.bodyFormData"
dataTreePath={`${dataTreePath}.bodyFormData`}
label=""
- pushFields
theme={props.theme}
/>
);
@@ -123,29 +127,40 @@ const PostBodyData = (props: Props) => {
displayFormatObject &&
displayFormatObject.value === POST_BODY_FORMATS[3]
) {
+ // Dont update the content type header if raw has been selected
setDisplayFormat(apiId, POST_BODY_FORMAT_OPTIONS[3]);
return;
}
- const elementsIndex = actionConfigurationHeaders.findIndex(
+ const contentTypeHeaderIndex = actionConfigurationHeaders.findIndex(
(element: { key: string; value: string }) =>
element &&
element.key &&
element.key.trim().toLowerCase() === CONTENT_TYPE,
);
- if (elementsIndex >= 0 && displayFormatObject) {
- const updatedHeaders = [...actionConfigurationHeaders];
+ // If there is an existing header with content type, use that or
+ // create a new header
+ const indexToUpdate =
+ contentTypeHeaderIndex > -1
+ ? contentTypeHeaderIndex
+ : actionConfigurationHeaders.length;
- updatedHeaders[elementsIndex] = {
- ...updatedHeaders[elementsIndex],
- key: CONTENT_TYPE,
- value: displayFormatObject.value,
- };
+ const updatedHeaders = [...actionConfigurationHeaders];
- onDisplayFormatChange(updatedHeaders);
- } else {
- setDisplayFormat(apiId, POST_BODY_FORMAT_OPTIONS[3]);
+ updatedHeaders[indexToUpdate] = {
+ key: CONTENT_TYPE,
+ value: displayFormatObject.value,
+ };
+
+ onDisplayFormatChange(updatedHeaders);
+ if (
+ displayFormatObject &&
+ displayFormatObject.value === POST_BODY_FORMATS[1]
+ ) {
+ if (!bodyFormData) {
+ addBodyFormData();
+ }
}
}}
/>
@@ -167,6 +182,13 @@ const mapDispatchToProps = (dispatch: any) => ({
dispatch(
change(API_EDITOR_FORM_NAME, "actionConfiguration.headers", value),
),
+ addBodyFormData: () =>
+ dispatch(
+ change(API_EDITOR_FORM_NAME, "actionConfiguration.bodyFormData", [
+ { key: "", value: "" },
+ { key: "", value: "" },
+ ]),
+ ),
setDisplayFormat: (
id: string,
displayFormat: { label: string; value: string },
@@ -187,6 +209,7 @@ export default connect((state: AppState) => {
const apiId = selector(state, "id");
const extraFormData = state.ui.apiPane.extraformData[apiId] || {};
const headers = selector(state, "actionConfiguration.headers");
+ const bodyFormData = selector(state, "actionConfiguration.bodyFormData");
let contentType;
if (headers) {
contentType = headers.find(
@@ -200,5 +223,6 @@ export default connect((state: AppState) => {
extraFormData["displayFormat"] || POST_BODY_FORMAT_OPTIONS[3],
contentType,
apiId,
+ bodyFormData,
};
}, mapDispatchToProps)(PostBodyData);
diff --git a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx
index 9cd18a7127..d7b7665fe5 100644
--- a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx
+++ b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx
@@ -10,7 +10,6 @@ import { getDatasource } from "selectors/entitiesSelector";
import { useSelector, useDispatch } from "react-redux";
import { Datasource } from "entities/Datasource";
import { getDataSources } from "selectors/editorSelectors";
-import { getDataTree } from "selectors/dataTreeSelectors";
import { isNameValid } from "utils/helpers";
import { saveDatasourceName } from "actions/datasourceActions";
import { Spinner } from "@blueprintjs/core";
@@ -38,8 +37,8 @@ const FormTitle = (props: FormTitleProps) => {
| undefined = useSelector((state: AppState) =>
getDatasource(state, params.datasourceId),
);
+
const datasources: Datasource[] = useSelector(getDataSources);
- const evalTree = useSelector(getDataTree);
const [forceUpdate, setForceUpdate] = useState(false);
const dispatch = useDispatch();
const saveStatus: {
@@ -68,7 +67,7 @@ const FormTitle = (props: FormTitleProps) => {
datasourcesNames[datasource.name] = datasource;
});
- return !isNameValid(name, { ...datasourcesNames, ...evalTree });
+ return !isNameValid(name, { ...datasourcesNames });
},
[datasources, currentDatasource],
);
diff --git a/app/client/src/pages/Editor/EditableAppName.tsx b/app/client/src/pages/Editor/EditableAppName.tsx
index 8fa04d5a51..8b38549510 100644
--- a/app/client/src/pages/Editor/EditableAppName.tsx
+++ b/app/client/src/pages/Editor/EditableAppName.tsx
@@ -35,6 +35,8 @@ const Container = styled.div`
&&&& .${Classes.EDITABLE_TEXT_CONTENT} {
min-width: 0;
}
+ flex: 1;
+ overflow: auto;
`;
export default function EditableTextWrapper(props: EditableTextWrapperProps) {
diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx
index 0320c17dbf..142d409b52 100644
--- a/app/client/src/pages/Editor/EditorHeader.tsx
+++ b/app/client/src/pages/Editor/EditorHeader.tsx
@@ -50,6 +50,7 @@ import OnboardingIndicator from "components/editorComponents/Onboarding/Indicato
import { getThemeDetails, ThemeMode } from "selectors/themeSelectors";
const HeaderWrapper = styled(StyledHeader)`
+ width: 100%;
padding-right: 0;
padding-left: ${(props) => props.theme.spaces[7]}px;
background-color: ${(props) => props.theme.colors.header.background};
@@ -83,6 +84,7 @@ const HeaderWrapper = styled(StyledHeader)`
const HeaderSection = styled.div`
display: flex;
flex: 1;
+ overflow: auto;
align-items: center;
:nth-child(1) {
justify-content: flex-start;
@@ -211,7 +213,7 @@ export const EditorHeader = (props: EditorHeaderProps) => {
defaultValue={currentApplication.name || ""}
editInteractionKind={EditInteractionKind.SINGLE}
className="t--application-name editable-application-name"
- fill={false}
+ fill={true}
savingState={
isSavingName ? SavingState.STARTED : SavingState.NOT_STARTED
}
diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx
index 51ed09b906..c6dd959159 100644
--- a/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx
+++ b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx
@@ -45,7 +45,6 @@ export const ExplorerActionEntity = memo((props: ExplorerActionEntityProps) => {
icon={props.icon}
name={props.action.config.name}
action={switchToAction}
- isDefaultExpanded={props.active}
active={props.active}
entityId={props.action.config.id}
step={props.step}
diff --git a/app/client/src/pages/Editor/Explorer/Actions/MoreActionsMenu.tsx b/app/client/src/pages/Editor/Explorer/Actions/MoreActionsMenu.tsx
index 37ddca23a8..e8a416a779 100644
--- a/app/client/src/pages/Editor/Explorer/Actions/MoreActionsMenu.tsx
+++ b/app/client/src/pages/Editor/Explorer/Actions/MoreActionsMenu.tsx
@@ -82,6 +82,7 @@ export const MoreActionsMenu = (props: EntityContextMenuProps) => {
selectedValue=""
optionTree={[
{
+ icon: "duplicate",
value: "copy",
onSelect: noop,
label: "Copy to page",
@@ -93,6 +94,7 @@ export const MoreActionsMenu = (props: EntityContextMenuProps) => {
}),
},
{
+ icon: "swap-horizontal",
value: "move",
onSelect: noop,
label: "Move to page",
@@ -110,6 +112,7 @@ export const MoreActionsMenu = (props: EntityContextMenuProps) => {
: [{ value: "No Pages", onSelect: noop, label: "No Pages" }],
},
{
+ icon: "trash",
value: "delete",
onSelect: () => deleteActionFromPage(props.id, props.name),
label: "Delete",
diff --git a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
index fc874ddfbe..931ae33891 100644
--- a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
+++ b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
@@ -83,6 +83,7 @@ const EntityExplorer = (props: IPanelProps) => {
},
[openPanel, applicationId],
);
+
return (
diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx
index 97c331980e..1ab3fcdf8e 100644
--- a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx
+++ b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx
@@ -73,6 +73,7 @@ export const ExplorerPageEntity = memo((props: ExplorerPageEntityProps) => {
updateEntityName={updatePage}
contextMenu={contextMenu}
onNameEdit={resolveAsSpaceChar}
+ searchKeyword={props.searchKeyword}
>
{
className="group pages"
icon={pageGroupIcon}
isDefaultExpanded
- action={noop}
entityId="Pages"
step={props.step}
onCreate={createPageCallback}
diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts
index c0ae4634db..592363c27f 100644
--- a/app/client/src/pages/Editor/Explorer/hooks.ts
+++ b/app/client/src/pages/Editor/Explorer/hooks.ts
@@ -27,8 +27,9 @@ const findWidgets = (widgets: CanvasStructure, keyword: string) => {
),
);
}
- if (widgetNameMached || (widgets.children && widgets.children.length > 0))
+ if (widgetNameMached || (widgets.children && widgets.children.length > 0)) {
return widgets;
+ }
};
const findDataSources = (dataSources: Datasource[], keyword: string) => {
@@ -43,6 +44,7 @@ export const useFilteredDatasources = (searchKeyword?: string) => {
return state.entities.datasources.list;
});
const actions = useActions();
+ const pageIds = usePageIds(searchKeyword);
const datasources = useMemo(() => {
const datasourcesPageMap: Record = {};
@@ -67,11 +69,17 @@ export const useFilteredDatasources = (searchKeyword?: string) => {
return useMemo(() => {
if (searchKeyword) {
+ const start = performance.now();
const filteredDatasources = produce(datasources, (draft) => {
for (const [key, value] of Object.entries(draft)) {
- draft[key] = findDataSources(value, searchKeyword);
+ if (pageIds.includes(key)) {
+ draft[key] = value;
+ } else {
+ draft[key] = findDataSources(value, searchKeyword);
+ }
}
});
+ log.debug("Filtered datasources in:", performance.now() - start, "ms");
return filteredDatasources;
}
@@ -83,6 +91,7 @@ export const useActions = (searchKeyword?: string) => {
const reducerActions = useSelector(
(state: AppState) => state.entities.actions,
);
+ const pageIds = usePageIds(searchKeyword);
const actions = useMemo(() => {
return groupBy(reducerActions, "config.pageId");
@@ -93,17 +102,21 @@ export const useActions = (searchKeyword?: string) => {
const start = performance.now();
const filteredActions = produce(actions, (draft) => {
for (const [key, value] of Object.entries(draft)) {
- value.forEach((action, index) => {
- const searchMatches =
- action.config.name
- .toLowerCase()
- .indexOf(searchKeyword.toLowerCase()) > -1;
- if (searchMatches) {
- draft[key][index] = action;
- } else {
- delete draft[key][index];
- }
- });
+ if (pageIds.includes(key)) {
+ draft[key] = value;
+ } else {
+ value.forEach((action, index) => {
+ const searchMatches =
+ action.config.name
+ .toLowerCase()
+ .indexOf(searchKeyword.toLowerCase()) > -1;
+ if (searchMatches) {
+ draft[key][index] = action;
+ } else {
+ delete draft[key][index];
+ }
+ });
+ }
draft[key] = draft[key].filter(Boolean);
}
});
@@ -118,16 +131,22 @@ export const useWidgets = (searchKeyword?: string) => {
const pageCanvasStructures = useSelector(
(state: AppState) => state.ui.pageCanvasStructure,
);
+ const pageIds = usePageIds(searchKeyword);
+
return useMemo(() => {
if (searchKeyword && pageCanvasStructures) {
const start = performance.now();
const filteredDSLs = produce(pageCanvasStructures, (draft) => {
for (const [key, value] of Object.entries(draft)) {
- const filteredWidgets = findWidgets(
- value,
- searchKeyword.toLowerCase(),
- ) as WidgetProps;
- draft[key] = filteredWidgets;
+ if (pageIds.includes(key)) {
+ draft[key] = value;
+ } else {
+ const filteredWidgets = findWidgets(
+ value,
+ searchKeyword.toLowerCase(),
+ ) as WidgetProps;
+ draft[key] = filteredWidgets;
+ }
}
});
log.debug("Filtered widgets in: ", performance.now() - start, "ms");
@@ -137,6 +156,31 @@ export const useWidgets = (searchKeyword?: string) => {
}, [searchKeyword, pageCanvasStructures]);
};
+export const usePageIds = (searchKeyword?: string) => {
+ const pages = useSelector((state: AppState) => {
+ return state.entities.pageList.pages;
+ });
+
+ return useMemo(() => {
+ if (searchKeyword) {
+ const filteredPages = produce(pages, (draft) => {
+ draft.forEach((page, index) => {
+ const searchMatches =
+ page.pageName.toLowerCase().indexOf(searchKeyword.toLowerCase()) >
+ -1;
+ if (searchMatches) {
+ } else {
+ delete draft[index];
+ }
+ });
+ });
+
+ return filteredPages.map((page) => page.pageId);
+ }
+ return pages.map((page) => page.pageId);
+ }, [searchKeyword, pages]);
+};
+
export const useFilteredEntities = (
ref: MutableRefObject,
) => {
diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx
index 0b03bcd6da..b306728a7f 100644
--- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx
+++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx
@@ -172,8 +172,9 @@ const PropertyControl = memo((props: Props) => {
);
const { isValid, validationMessage } = getPropertyValidation(propertyName);
+ const { additionalAutoComplete, ...rest } = props;
const config = {
- ...props,
+ ...rest,
isValid,
propertyValue,
validationMessage,
@@ -246,6 +247,9 @@ const PropertyControl = memo((props: Props) => {
},
isDynamic,
props.customJSControl,
+ additionalAutoComplete
+ ? additionalAutoComplete(widgetProperties)
+ : undefined,
)}
diff --git a/app/client/src/pages/Editor/PropertyPane/index.tsx b/app/client/src/pages/Editor/PropertyPane/index.tsx
index 46b422f892..00c4e0cb2d 100644
--- a/app/client/src/pages/Editor/PropertyPane/index.tsx
+++ b/app/client/src/pages/Editor/PropertyPane/index.tsx
@@ -82,6 +82,7 @@ const PropertyPaneView = (
};
class PropertyPane extends Component {
+ private panelWrapperRef = React.createRef();
render() {
if (this.props.isVisible) {
log.debug("Property pane rendered");
@@ -109,11 +110,16 @@ class PropertyPane extends Component {
if (!widgetProperties) return ;
return (
{
e.stopPropagation();
}}
>
{
+ const parent = this.panelWrapperRef.current;
+ parent?.scrollTo(0, 0);
+ }}
initialPanel={{
component: PropertyPaneView,
props: {
diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts
index 47b224258d..55f5c6fb07 100644
--- a/app/client/src/sagas/EvaluationsSaga.ts
+++ b/app/client/src/sagas/EvaluationsSaga.ts
@@ -15,16 +15,16 @@ import {
} from "constants/ReduxActionConstants";
import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";
import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory";
-import { GracefulWorkerService } from "../utils/WorkerUtil";
+import { GracefulWorkerService } from "utils/WorkerUtil";
import Worker from "worker-loader!../workers/evaluation.worker";
import {
EVAL_WORKER_ACTIONS,
EvalError,
EvalErrorTypes,
-} from "../utils/DynamicBindingUtils";
+} from "utils/DynamicBindingUtils";
import log from "loglevel";
-import { WidgetType } from "../constants/WidgetConstants";
-import { WidgetProps } from "../widgets/BaseWidget";
+import { WidgetType } from "constants/WidgetConstants";
+import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, {
PerformanceTransactionName,
} from "../utils/PerformanceTracker";
@@ -41,22 +41,36 @@ const worker = new GracefulWorkerService(Worker);
const evalErrorHandler = (errors: EvalError[]) => {
if (!errors) return;
errors.forEach((error) => {
- if (error.type === EvalErrorTypes.DEPENDENCY_ERROR) {
- Toaster.show({
- text: error.message,
- variant: Variant.danger,
- });
+ switch (error.type) {
+ case EvalErrorTypes.DEPENDENCY_ERROR: {
+ Toaster.show({
+ text: error.message,
+ variant: Variant.danger,
+ });
+ break;
+ }
+ case EvalErrorTypes.EVAL_TREE_ERROR: {
+ Toaster.show({
+ text: "Unexpected error occurred while evaluating the app",
+ variant: Variant.danger,
+ });
+ break;
+ }
+ case EvalErrorTypes.BAD_UNEVAL_TREE_ERROR: {
+ Sentry.captureException(error);
+ break;
+ }
+ case EvalErrorTypes.EVAL_TRIGGER_ERROR: {
+ Toaster.show({
+ text: `Error occurred when executing trigger: ${error.message}`,
+ variant: Variant.danger,
+ });
+ break;
+ }
+ default: {
+ Sentry.captureException(error);
+ }
}
- if (error.type === EvalErrorTypes.EVAL_TREE_ERROR) {
- Toaster.show({
- text: "Unexpected error occurred while evaluating the app",
- variant: Variant.danger,
- });
- }
- if (error.type === EvalErrorTypes.BAD_UNEVAL_TREE_ERROR) {
- Sentry.captureException(error);
- }
- log.debug(error);
});
};
diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts
index c769bac4f4..bea8e6f4ac 100644
--- a/app/client/src/utils/DynamicBindingUtils.ts
+++ b/app/client/src/utils/DynamicBindingUtils.ts
@@ -5,7 +5,7 @@ import {
} from "constants/BindingsConstants";
import { Action } from "entities/Action";
import moment from "moment-timezone";
-import { WidgetProps } from "../widgets/BaseWidget";
+import { WidgetProps } from "widgets/BaseWidget";
import parser from "fast-xml-parser";
export type DependencyMap = Record>;
@@ -91,6 +91,7 @@ export enum EvalErrorTypes {
EVAL_ERROR = "EVAL_ERROR",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
BAD_UNEVAL_TREE_ERROR = "BAD_UNEVAL_TREE_ERROR",
+ EVAL_TRIGGER_ERROR = "EVAL_TRIGGER_ERROR",
}
export type EvalError = {
@@ -158,7 +159,7 @@ export interface WidgetEvaluatedProps {
evaluatedValues?: Record;
}
-interface EntityWithBindings {
+export interface EntityWithBindings {
dynamicBindingPathList?: DynamicPath[];
}
diff --git a/app/client/src/utils/PropertyControlFactory.tsx b/app/client/src/utils/PropertyControlFactory.tsx
index 5569796aa0..f997c8e5eb 100644
--- a/app/client/src/utils/PropertyControlFactory.tsx
+++ b/app/client/src/utils/PropertyControlFactory.tsx
@@ -21,6 +21,7 @@ class PropertyControlFactory {
controlFunctions: ControlFunctions,
preferEditor: boolean,
customEditor?: string,
+ additionalAutoComplete?: Record>,
): JSX.Element {
let controlBuilder = this.controlMap.get(controlData.controlType);
if (preferEditor) {
@@ -34,6 +35,7 @@ class PropertyControlFactory {
...controlFunctions,
key: controlData.id,
customJSControl: customEditor,
+ additionalAutoComplete,
};
const control = controlBuilder.buildPropertyControl(controlProps);
return control;
diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts
index ce0ea8b0ce..0dd1e5dc84 100644
--- a/app/client/src/utils/autocomplete/EntityDefinitions.ts
+++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts
@@ -171,6 +171,7 @@ export const entityDefinitions = {
chartData: "chartData",
xAxisName: "string",
yAxisName: "string",
+ selectedDataPoint: "chartDataPoint",
},
FORM_WIDGET: (widget: any) => ({
"!doc":
diff --git a/app/client/src/utils/migrations/TableWidget.ts b/app/client/src/utils/migrations/TableWidget.ts
index 6cc37279d0..6a85686a3a 100644
--- a/app/client/src/utils/migrations/TableWidget.ts
+++ b/app/client/src/utils/migrations/TableWidget.ts
@@ -101,7 +101,7 @@ export const tableWidgetPropertyPaneMigrations = (
? columnNameMap[accessor]
: accessor,
// Generate computed value
- computedValue: `{{${child.widgetName}.map((currentRow) => { return currentRow.${accessor}})}}`,
+ computedValue: `{{${child.widgetName}.tableData.map((currentRow) => { return currentRow.${accessor}})}}`,
};
// copy inputForma nd outputFormat for date column types
if (columnTypeMap && columnTypeMap[accessor]) {
@@ -135,6 +135,9 @@ export const tableWidgetPropertyPaneMigrations = (
onClick: action.dynamicTrigger,
computedValue: "",
};
+ dynamicTriggerPathList.push({
+ key: `primaryColumns.${columnPrefix}${index + 1}.onClick`,
+ });
updatedDerivedColumns[column.id] = column;
});
diff --git a/app/client/src/widgets/ChartWidget.tsx b/app/client/src/widgets/ChartWidget.tsx
index 16a21e5398..4fb325677e 100644
--- a/app/client/src/widgets/ChartWidget.tsx
+++ b/app/client/src/widgets/ChartWidget.tsx
@@ -3,9 +3,12 @@ import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { WidgetPropertyValidationType } from "utils/WidgetValidation";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
+import { TriggerPropertiesMap } from "utils/WidgetFactory";
import Skeleton from "components/utils/Skeleton";
import * as Sentry from "@sentry/react";
import { retryPromise } from "utils/AppsmithUtils";
+import { EventType } from "constants/ActionConstants";
+import withMeta, { WithMeta } from "./MetaHOC";
const ChartComponent = lazy(() =>
retryPromise(() =>
@@ -25,6 +28,17 @@ class ChartWidget extends BaseWidget {
chartData: VALIDATION_TYPES.CHART_DATA,
};
}
+ static getTriggerPropertyMap(): TriggerPropertiesMap {
+ return {
+ onDataPointClick: true,
+ };
+ }
+
+ static getMetaPropertiesMap(): Record {
+ return {
+ selectedDataPoint: undefined,
+ };
+ }
static getPropertyPaneConfig() {
return [
@@ -140,9 +154,36 @@ class ChartWidget extends BaseWidget {
},
],
},
+ {
+ sectionName: "Actions",
+ children: [
+ {
+ helpText: "Triggers an action when the chart data point is clicked",
+ propertyName: "onDataPointClick",
+ label: "onDataPointClick",
+ controlType: "ACTION_SELECTOR",
+ isJSConvertible: true,
+ isBindProperty: true,
+ isTriggerProperty: true,
+ },
+ ],
+ },
];
}
+ onDataPointClick = (selectedDataPoint: { x: any; y: any }) => {
+ this.props.updateWidgetMetaProperty(
+ "selectedDataPoint",
+ selectedDataPoint,
+ {
+ dynamicString: this.props.onDataPointClick,
+ event: {
+ type: EventType.ON_DATA_POINT_CLICK,
+ },
+ },
+ );
+ };
+
getPageView() {
return (
}>
@@ -155,6 +196,7 @@ class ChartWidget extends BaseWidget {
chartName={this.props.chartName}
chartData={this.props.chartData}
widgetId={this.props.widgetId}
+ onDataPointClick={this.onDataPointClick}
allowHorizontalScroll={this.props.allowHorizontalScroll}
/>
@@ -184,7 +226,7 @@ export interface ChartData {
data: ChartDataPoint[];
}
-export interface ChartWidgetProps extends WidgetProps {
+export interface ChartWidgetProps extends WidgetProps, WithMeta {
chartType: ChartType;
chartData: ChartData[];
xAxisName: string;
@@ -192,7 +234,9 @@ export interface ChartWidgetProps extends WidgetProps {
chartName: string;
isVisible?: boolean;
allowHorizontalScroll: boolean;
+ onDataPointClick?: string;
+ selectedDataPoint?: ChartDataPoint;
}
export default ChartWidget;
-export const ProfiledChartWidget = Sentry.withProfiler(ChartWidget);
+export const ProfiledChartWidget = Sentry.withProfiler(withMeta(ChartWidget));
diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx
index a543650226..10dd74251c 100644
--- a/app/client/src/widgets/DropdownWidget.tsx
+++ b/app/client/src/widgets/DropdownWidget.tsx
@@ -13,6 +13,7 @@ import { TriggerPropertiesMap } from "utils/WidgetFactory";
import { Intent as BlueprintIntent } from "@blueprintjs/core";
import * as Sentry from "@sentry/react";
import withMeta, { WithMeta } from "./MetaHOC";
+import { IconName } from "@blueprintjs/icons";
class DropdownWidget extends BaseWidget {
static getPropertyPaneConfig() {
@@ -260,7 +261,7 @@ export type SelectionType = "SINGLE_SELECT" | "MULTI_SELECT";
export interface DropdownOption {
label: string;
value: string;
- icon?: string;
+ icon?: IconName;
subText?: string;
id?: string;
onSelect?: (option: DropdownOption) => void;
diff --git a/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts b/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts
index 4bd40acb12..861791506b 100644
--- a/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts
+++ b/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts
@@ -500,7 +500,14 @@ export default [
propertyName: "onClick",
label: "onClick",
controlType: "ACTION_SELECTOR",
- customJSControl: "COMPUTE_VALUE",
+ additionalAutoComplete: (props: TableWidgetProps) => ({
+ currentRow: Object.assign(
+ {},
+ ...Object.keys(props.primaryColumns).map((key) => ({
+ [key]: "",
+ })),
+ ),
+ }),
isJSConvertible: true,
updateHook: updateDerivedColumnsHook,
isBindProperty: true,
diff --git a/app/client/src/widgets/TableWidget/index.tsx b/app/client/src/widgets/TableWidget/index.tsx
index 4ec130bf5c..84bd674b97 100644
--- a/app/client/src/widgets/TableWidget/index.tsx
+++ b/app/client/src/widgets/TableWidget/index.tsx
@@ -903,6 +903,7 @@ class TableWidget extends BaseWidget {
const modifiedAction = jsSnippets.reduce((prev: string, next: string) => {
return prev + `{{(currentRow) => { ${next} }}} `;
}, "");
+
super.executeAction({
dynamicString: modifiedAction,
event: {
diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts
new file mode 100644
index 0000000000..6fe0f04ffc
--- /dev/null
+++ b/app/client/src/workers/DataTreeEvaluator.ts
@@ -0,0 +1,1205 @@
+import {
+ DependencyMap,
+ EntityWithBindings,
+ EvalError,
+ EvalErrorTypes,
+ extraLibraries,
+ getDynamicBindings,
+ getEntityDynamicBindingPathList,
+ isChildPropertyPath,
+ isPathADynamicBinding,
+ isPathADynamicTrigger,
+ unsafeFunctionForEval,
+} from "utils/DynamicBindingUtils";
+import { WidgetTypeConfigMap } from "utils/WidgetFactory";
+import {
+ ActionDescription,
+ DataTree,
+ DataTreeAction,
+ DataTreeEntity,
+ DataTreeObjectEntity,
+ DataTreeWidget,
+ ENTITY_TYPE,
+} from "entities/DataTree/dataTreeFactory";
+import {
+ addDependantsOfNestedPropertyPaths,
+ addFunctions,
+ convertPathToString,
+ CrashingError,
+ DataTreeDiffEvent,
+ getAllPaths,
+ getImmediateParentsOfPropertyPaths,
+ getValidatedTree,
+ makeParentsDependOnChildren,
+ removeFunctions,
+ removeFunctionsFromDataTree,
+ translateDiffEventToDataTreeDiffEvent,
+ trimDependantChangePaths,
+ validateWidgetProperty,
+} from "workers/evaluationUtils";
+import _ from "lodash";
+import { applyChange, Diff, diff } from "deep-diff";
+import toposort from "toposort";
+import unescapeJS from "unescape-js";
+import equal from "fast-deep-equal/es6";
+import {
+ EXECUTION_PARAM_KEY,
+ EXECUTION_PARAM_REFERENCE_REGEX,
+} from "constants/ActionConstants";
+import { DATA_BIND_REGEX } from "constants/BindingsConstants";
+
+type EvalResult = {
+ result: any;
+ triggers?: ActionDescription[];
+};
+
+export default class DataTreeEvaluator {
+ dependencyMap: DependencyMap = {};
+ sortedDependencies: Array = [];
+ inverseDependencyMap: DependencyMap = {};
+ widgetConfigMap: WidgetTypeConfigMap = {};
+ evalTree: DataTree = {};
+ allKeys: Record = {};
+ oldUnEvalTree: DataTree = {};
+ errors: EvalError[] = [];
+ parsedValueCache: Map<
+ string,
+ {
+ value: any;
+ version: number;
+ }
+ > = new Map();
+ logs: any[] = [];
+
+ constructor(widgetConfigMap: WidgetTypeConfigMap) {
+ this.widgetConfigMap = widgetConfigMap;
+ }
+
+ createFirstTree(unEvalTree: DataTree) {
+ const totalStart = performance.now();
+ // Add functions to the tree
+ const withFunctions = addFunctions(unEvalTree);
+ // Create dependency map
+ const createDependencyStart = performance.now();
+ this.dependencyMap = this.createDependencyMap(withFunctions);
+ const createDependencyEnd = performance.now();
+ // Sort
+ const sortDependenciesStart = performance.now();
+ this.sortedDependencies = this.sortDependencies(this.dependencyMap);
+ const sortDependenciesEnd = performance.now();
+ // Inverse
+ this.inverseDependencyMap = this.getInverseDependencyTree();
+ // Evaluate
+ const evaluateStart = performance.now();
+ const evaluatedTree = this.evaluateTree(
+ withFunctions,
+ this.sortedDependencies,
+ );
+ const evaluateEnd = performance.now();
+ // Validate Widgets
+ const validateStart = performance.now();
+ const validated = getValidatedTree(this.widgetConfigMap, evaluatedTree);
+ const validateEnd = performance.now();
+ // Remove functions
+ this.evalTree = removeFunctionsFromDataTree(validated);
+ this.oldUnEvalTree = unEvalTree;
+ const totalEnd = performance.now();
+ const timeTakenForFirstTree = {
+ total: (totalEnd - totalStart).toFixed(2),
+ createDependencies: (createDependencyEnd - createDependencyStart).toFixed(
+ 2,
+ ),
+ sortDependencies: (sortDependenciesEnd - sortDependenciesStart).toFixed(
+ 2,
+ ),
+ evaluate: (evaluateEnd - evaluateStart).toFixed(2),
+ validate: (validateEnd - validateStart).toFixed(2),
+ dependencies: {
+ map: JSON.parse(JSON.stringify(this.dependencyMap)),
+ inverseMap: JSON.parse(JSON.stringify(this.inverseDependencyMap)),
+ sortedList: JSON.parse(JSON.stringify(this.sortedDependencies)),
+ },
+ };
+ this.logs.push({ timeTakenForFirstTree });
+ }
+
+ isDynamicLeaf(unEvalTree: DataTree, propertyPath: string) {
+ const [entityName, ...propPathEls] = _.toPath(propertyPath);
+ // Framework feature: Top level items are never leaves
+ if (entityName === propertyPath) return false;
+ // Ignore if this was a delete op
+ if (!(entityName in unEvalTree)) return false;
+
+ const entity = unEvalTree[entityName];
+ if (!isAction(entity) && !isWidget(entity)) return false;
+ const relativePropertyPath = convertPathToString(propPathEls);
+ return relativePropertyPath in entity.bindingPaths;
+ }
+
+ updateDataTree(unEvalTree: DataTree) {
+ const totalStart = performance.now();
+ // Add appsmith internal functions to the tree ex. navigateTo / showModal
+ const unEvalTreeWithFunctions = addFunctions(unEvalTree);
+ // Calculate diff
+ const diffCheckTimeStart = performance.now();
+ const differences = diff(this.oldUnEvalTree, unEvalTree) || [];
+ // Since eval tree is listening to possible events that dont cause differences
+ // We want to check if no diffs are present and bail out early
+ if (differences.length === 0) {
+ return this.evalTree;
+ }
+ const diffCheckTimeStop = performance.now();
+ // Check if dependencies have changed
+ const updateDependenciesStart = performance.now();
+
+ // Find all the paths that have changed as part of the difference and update the
+ // global dependency map if an existing dynamic binding has now become legal
+ const {
+ dependenciesOfRemovedPaths,
+ removedPaths,
+ } = this.updateDependencyMap(differences, unEvalTreeWithFunctions);
+ const updateDependenciesStop = performance.now();
+
+ const calculateSortOrderStart = performance.now();
+
+ const subTreeSortOrder: string[] = this.calculateSubTreeSortOrder(
+ differences,
+ dependenciesOfRemovedPaths,
+ removedPaths,
+ unEvalTree,
+ );
+
+ const calculateSortOrderStop = performance.now();
+
+ this.logs.push({
+ differences,
+ subTreeSortOrder,
+ sortedDependencies: this.sortedDependencies,
+ inverse: this.inverseDependencyMap,
+ updatedDependencyMap: this.dependencyMap,
+ });
+
+ // Evaluate
+ const evalStart = performance.now();
+ // We are setting all values from our uneval tree to the old eval tree we have
+ // this way we can get away with just evaluating the sort order and nothing else
+ subTreeSortOrder.forEach((propertyPath) => {
+ if (this.isDynamicLeaf(unEvalTree, propertyPath)) {
+ const unEvalPropValue = _.get(unEvalTree, propertyPath);
+ console.log("Setting", propertyPath);
+ _.set(this.evalTree, propertyPath, unEvalPropValue);
+ }
+ });
+
+ // Remove any deleted paths from the eval tree
+ removedPaths.forEach((removedPath) => {
+ _.unset(this.evalTree, removedPath);
+ });
+ const evaluatedTree = this.evaluateTree(this.evalTree, subTreeSortOrder);
+ const evalStop = performance.now();
+
+ // Remove functions
+ this.evalTree = removeFunctionsFromDataTree(evaluatedTree);
+ const totalEnd = performance.now();
+ // TODO: For some reason we are passing some reference which are getting mutated.
+ // Need to check why big api responses are getting split between two eval runs
+ this.oldUnEvalTree = unEvalTree;
+ const timeTakenForSubTreeEval = {
+ total: (totalEnd - totalStart).toFixed(2),
+ findDifferences: (diffCheckTimeStop - diffCheckTimeStart).toFixed(2),
+ updateDependencies: (
+ updateDependenciesStop - updateDependenciesStart
+ ).toFixed(2),
+ calculateSortOrder: (
+ calculateSortOrderStop - calculateSortOrderStart
+ ).toFixed(2),
+ evaluate: (evalStop - evalStart).toFixed(2),
+ };
+ this.logs.push({ timeTakenForSubTreeEval });
+ return this.evalTree;
+ }
+
+ getCompleteSortOrder(
+ changes: Array,
+ inverseMap: DependencyMap,
+ ): Array {
+ let finalSortOrder: Array = [];
+ let computeSortOrder = true;
+ // Initialize parents with the current sent of property paths that need to be evaluated
+ let parents = changes;
+ let subSortOrderArray: Array;
+ while (computeSortOrder) {
+ // Get all the nodes that would be impacted by the evaluation of the nodes in parents array in sorted order
+ subSortOrderArray = this.getEvaluationSortOrder(parents, inverseMap);
+
+ // Add all the sorted nodes in the final list
+ finalSortOrder = [...finalSortOrder, ...subSortOrderArray];
+
+ parents = getImmediateParentsOfPropertyPaths(subSortOrderArray);
+ // If we find parents of the property paths in the sorted array, we should continue finding all the nodes dependent
+ // on the parents
+ computeSortOrder = parents.length > 0;
+ }
+
+ // Remove duplicates from this list. Since we explicitly walk down the tree and implicitly (by fetching parents) walk
+ // up the tree, there are bound to be many duplicates.
+ const uniqueKeysInSortOrder = [...new Set(finalSortOrder)];
+
+ const sortOrderPropertyPaths = Array.from(uniqueKeysInSortOrder);
+
+ //Trim this list to now remove the property paths which are simply entity names
+ const finalSortOrderArray: Array = [];
+ sortOrderPropertyPaths.forEach((propertyPath) => {
+ const lastIndexOfDot = propertyPath.lastIndexOf(".");
+ // Only do this for property paths and not the entity themselves
+ if (lastIndexOfDot !== -1) {
+ finalSortOrderArray.push(propertyPath);
+ }
+ });
+
+ return finalSortOrderArray;
+ }
+
+ getEvaluationSortOrder(
+ changes: Array,
+ inverseMap: DependencyMap,
+ ): Array {
+ const sortOrder: Array = [...changes];
+ let iterator = 0;
+ while (iterator < sortOrder.length) {
+ // Find all the nodes who are to be evaluated when sortOrder[iterator] changes
+ const newNodes = inverseMap[sortOrder[iterator]];
+
+ // If we find more nodes that would be impacted by the evaluation of the node being investigated
+ // we add these to the sort order.
+ if (newNodes) {
+ newNodes.forEach((toBeEvaluatedNode) => {
+ // Only add the nodes if they haven't been already added for evaluation in the list. Since we are doing
+ // breadth first traversal, we should be safe in not changing the evaluation order and adding this now at this
+ // point instead of the previous index found.
+ if (!sortOrder.includes(toBeEvaluatedNode)) {
+ sortOrder.push(toBeEvaluatedNode);
+ }
+ });
+ }
+ iterator++;
+ }
+ return sortOrder;
+ }
+
+ // getValidationPaths(unevalDataTree: DataTree): Record> {
+ // const result: Record> = {};
+ // for (const key in unevalDataTree) {
+ // const entity = unevalDataTree[key];
+ // if (isAction(entity)) {
+ // // TODO: add the properties to a global map somewhere
+ // result[entity.name] = new Set(
+ // ["config", "isLoading", "data"].map((e) => `${entity.name}.${e}`),
+ // );
+ // } else if (isWidget(entity)) {
+ // if (!this.widgetConfigMap[entity.type])
+ // throw new CrashingError(
+ // `${entity.widgetName} has unrecognised entity type: ${entity.type}`,
+ // );
+ // const { validations } = this.widgetConfigMap[entity.type];
+ //
+ // result[entity.widgetName] = new Set(
+ // Object.keys(validations).map((e) => `${entity.widgetName}.${e}`),
+ // );
+ // }
+ // }
+ // return result;
+ // }
+
+ createDependencyMap(unEvalTree: DataTree): DependencyMap {
+ let dependencyMap: DependencyMap = {};
+ this.allKeys = getAllPaths(unEvalTree);
+ Object.keys(unEvalTree).forEach((entityName) => {
+ const entity = unEvalTree[entityName];
+ if (isAction(entity) || isWidget(entity)) {
+ const entityListedDependencies = this.listEntityDependencies(
+ entity,
+ entityName,
+ );
+ dependencyMap = { ...dependencyMap, ...entityListedDependencies };
+ }
+ });
+ Object.keys(dependencyMap).forEach((key) => {
+ dependencyMap[key] = _.flatten(
+ dependencyMap[key].map((path) =>
+ extractReferencesFromBinding(path, this.allKeys),
+ ),
+ );
+ });
+ // TODO make this run only for widgets and not actions
+ dependencyMap = makeParentsDependOnChildren(dependencyMap);
+ return dependencyMap;
+ }
+
+ listEntityDependencies(
+ entity: DataTreeWidget | DataTreeAction,
+ entityName: string,
+ ): DependencyMap {
+ const dependencies: DependencyMap = {};
+ const dynamicBindingPathList = getEntityDynamicBindingPathList(entity);
+ if (dynamicBindingPathList.length) {
+ dynamicBindingPathList.forEach((dynamicPath) => {
+ const propertyPath = dynamicPath.key;
+ const unevalPropValue = _.get(entity, propertyPath);
+ const { jsSnippets } = getDynamicBindings(unevalPropValue);
+ const existingDeps =
+ dependencies[`${entityName}.${propertyPath}`] || [];
+ dependencies[`${entityName}.${propertyPath}`] = existingDeps.concat(
+ jsSnippets.filter((jsSnippet) => !!jsSnippet),
+ );
+ });
+ }
+ if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) {
+ // Set default property dependency
+ const defaultProperties = this.widgetConfigMap[entity.type]
+ .defaultProperties;
+ Object.keys(defaultProperties).forEach((property) => {
+ dependencies[`${entityName}.${property}`] = [
+ `${entityName}.${defaultProperties[property]}`,
+ ];
+ });
+ }
+ return dependencies;
+ }
+
+ evaluateTree(
+ oldUnevalTree: DataTree,
+ sortedDependencies: Array,
+ ): DataTree {
+ const tree = _.cloneDeep(oldUnevalTree);
+ try {
+ return sortedDependencies.reduce(
+ (currentTree: DataTree, propertyPath: string) => {
+ this.logs.push(`evaluating ${propertyPath}`);
+ const entityName = propertyPath.split(".")[0];
+ const entity: DataTreeEntity = currentTree[entityName];
+ const unEvalPropertyValue = _.get(currentTree as any, propertyPath);
+ const isABindingPath =
+ (isAction(entity) || isWidget(entity)) &&
+ isPathADynamicBinding(
+ entity,
+ propertyPath.substring(propertyPath.indexOf(".") + 1),
+ );
+ let evalPropertyValue;
+ const requiresEval =
+ isABindingPath && isDynamicValue(unEvalPropertyValue);
+ if (requiresEval) {
+ try {
+ evalPropertyValue = this.evaluateDynamicProperty(
+ propertyPath,
+ currentTree,
+ unEvalPropertyValue,
+ );
+ } catch (e) {
+ this.errors.push({
+ type: EvalErrorTypes.EVAL_PROPERTY_ERROR,
+ message: e.message,
+ context: {
+ propertyPath,
+ },
+ });
+ evalPropertyValue = undefined;
+ }
+ } else {
+ evalPropertyValue = unEvalPropertyValue;
+ }
+ if (isWidget(entity)) {
+ const widgetEntity = entity;
+ // TODO fix for nested properties
+ // For nested properties like Table1.selectedRow.email
+ // The following line will calculated the property name to be selectedRow
+ // instead of selectedRow.email
+ const propertyName = propertyPath.split(".")[1];
+ if (propertyName) {
+ let parsedValue = this.validateAndParseWidgetProperty(
+ propertyPath,
+ widgetEntity,
+ currentTree,
+ evalPropertyValue,
+ unEvalPropertyValue,
+ );
+ const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type]
+ .defaultProperties;
+ const hasDefaultProperty = propertyName in defaultPropertyMap;
+ if (hasDefaultProperty) {
+ const defaultProperty = defaultPropertyMap[propertyName];
+ parsedValue = this.overwriteDefaultDependentProps(
+ defaultProperty,
+ parsedValue,
+ propertyPath,
+ widgetEntity,
+ );
+ }
+ return _.set(currentTree, propertyPath, parsedValue);
+ }
+ return _.set(currentTree, propertyPath, evalPropertyValue);
+ } else {
+ return _.set(currentTree, propertyPath, evalPropertyValue);
+ }
+ },
+ tree,
+ );
+ } catch (e) {
+ this.errors.push({
+ type: EvalErrorTypes.EVAL_TREE_ERROR,
+ message: e.message,
+ });
+ return tree;
+ }
+ }
+
+ sortDependencies(dependencyMap: DependencyMap): Array {
+ const dependencyTree: Array<[string, string]> = [];
+ Object.keys(dependencyMap).forEach((key: string) => {
+ if (dependencyMap[key].length) {
+ dependencyMap[key].forEach((dep) => dependencyTree.push([key, dep]));
+ } else {
+ // Set no dependency
+ dependencyTree.push([key, ""]);
+ }
+ });
+
+ try {
+ // sort dependencies and remove empty dependencies
+ return toposort(dependencyTree)
+ .reverse()
+ .filter((d) => !!d);
+ } catch (e) {
+ this.errors.push({
+ type: EvalErrorTypes.DEPENDENCY_ERROR,
+ message: e.message,
+ });
+ console.error("CYCLICAL DEPENDENCY MAP", dependencyMap);
+ throw new CrashingError(e.message);
+ }
+ }
+
+ getParsedValueCache(propertyPath: string) {
+ return (
+ this.parsedValueCache.get(propertyPath) || {
+ value: undefined,
+ version: 0,
+ }
+ );
+ }
+
+ clearPropertyCache(propertyPath: string) {
+ this.parsedValueCache.delete(propertyPath);
+ }
+
+ clearPropertyCacheOfWidget(widgetName: string) {
+ // TODO check if this loop mutating itself is safe
+ this.parsedValueCache.forEach((value, key) => {
+ const match = key.match(`${widgetName}.`);
+ if (match) {
+ this.parsedValueCache.delete(key);
+ }
+ });
+ }
+
+ clearAllCaches() {
+ this.parsedValueCache.clear();
+ this.clearErrors();
+ this.dependencyMap = {};
+ this.allKeys = {};
+ this.inverseDependencyMap = {};
+ this.sortedDependencies = [];
+ this.evalTree = {};
+ this.oldUnEvalTree = {};
+ }
+
+ getDynamicValue(
+ dynamicBinding: string,
+ data: DataTree,
+ returnTriggers: boolean,
+ callBackData?: Array,
+ ) {
+ // Get the {{binding}} bound values
+ const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding);
+ if (returnTriggers) {
+ const result = this.evaluateDynamicBoundValue(
+ data,
+ jsSnippets[0],
+ callBackData,
+ );
+ return result.triggers;
+ }
+ if (stringSegments.length) {
+ // Get the Data Tree value of those "binding "paths
+ const values = jsSnippets.map((jsSnippet, index) => {
+ if (jsSnippet) {
+ const result = this.evaluateDynamicBoundValue(
+ data,
+ jsSnippet,
+ callBackData,
+ );
+ return result.result;
+ } else {
+ return stringSegments[index];
+ }
+ });
+
+ // if it is just one binding, no need to create template string
+ if (stringSegments.length === 1) return values[0];
+ // else return a string template with bindings
+ return createDynamicValueString(dynamicBinding, stringSegments, values);
+ }
+ return undefined;
+ }
+
+ // Paths are expected to have "{name}.{path}" signature
+ // Also returns any action triggers found after evaluating value
+ evaluateDynamicBoundValue(
+ data: DataTree,
+ path: string,
+ callbackData?: Array,
+ ): EvalResult {
+ try {
+ const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, "");
+ return this.evaluate(unescapedJS, data, callbackData);
+ } catch (e) {
+ this.errors.push({
+ type: EvalErrorTypes.UNESCAPE_STRING_ERROR,
+ message: e.message,
+ context: {
+ path,
+ },
+ });
+ return { result: undefined, triggers: [] };
+ }
+ }
+
+ evaluate(js: string, data: DataTree, callbackData?: Array): EvalResult {
+ const scriptToEvaluate = `
+ function closedFunction () {
+ const result = ${js};
+ return { result, triggers: self.triggers }
+ }
+ closedFunction()
+ `;
+ const scriptWithCallback = `
+ function callback (script) {
+ const userFunction = script;
+ const result = userFunction.apply(self, CALLBACK_DATA);
+ return { result, triggers: self.triggers };
+ }
+ callback(${js});
+ `;
+ const script = callbackData ? scriptWithCallback : scriptToEvaluate;
+ try {
+ const { result, triggers } = (function() {
+ /**** Setting the eval context ****/
+ const GLOBAL_DATA: Record = {};
+ ///// Adding callback data
+ GLOBAL_DATA.CALLBACK_DATA = callbackData;
+ ///// Adding Data tree
+ Object.keys(data).forEach((datum) => {
+ GLOBAL_DATA[datum] = data[datum];
+ });
+ ///// Fixing action paths and capturing their execution response
+ if (data.actionPaths) {
+ GLOBAL_DATA.triggers = [];
+ const pusher = function(
+ this: DataTree,
+ action: any,
+ ...payload: any[]
+ ) {
+ const actionPayload = action(...payload);
+ GLOBAL_DATA.triggers.push(actionPayload);
+ };
+ GLOBAL_DATA.actionPaths.forEach((path: string) => {
+ const action = _.get(GLOBAL_DATA, path);
+ const entity = _.get(GLOBAL_DATA, path.split(".")[0]);
+ if (action) {
+ _.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity)));
+ }
+ });
+ }
+
+ // Set it to self
+ Object.keys(GLOBAL_DATA).forEach((key) => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: No types available
+ self[key] = GLOBAL_DATA[key];
+ });
+
+ ///// Adding extra libraries separately
+ extraLibraries.forEach((library) => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: No types available
+ self[library.accessor] = library.lib;
+ });
+
+ ///// Remove all unsafe functions
+ unsafeFunctionForEval.forEach((func) => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: No types available
+ self[func] = undefined;
+ });
+
+ const evalResult = eval(script);
+
+ // Remove it from self
+ // This is needed so that next eval can have a clean sheet
+ Object.keys(GLOBAL_DATA).forEach((key) => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore: No types available
+ delete self[key];
+ });
+
+ return evalResult;
+ })();
+ return { result, triggers };
+ } catch (e) {
+ this.errors.push({
+ type: EvalErrorTypes.EVAL_ERROR,
+ message: e.message,
+ context: {
+ binding: js,
+ },
+ });
+ return { result: undefined, triggers: [] };
+ }
+ }
+
+ evaluateDynamicProperty(
+ propertyPath: string,
+ currentTree: DataTree,
+ unEvalPropertyValue: any,
+ ): any {
+ return this.getDynamicValue(unEvalPropertyValue, currentTree, false);
+ }
+
+ validateAndParseWidgetProperty(
+ propertyPath: string,
+ widget: DataTreeWidget,
+ currentTree: DataTree,
+ evalPropertyValue: any,
+ unEvalPropertyValue: string,
+ ): any {
+ const entityPropertyName = _.drop(propertyPath.split(".")).join(".");
+ let valueToValidate = evalPropertyValue;
+ if (isPathADynamicTrigger(widget, propertyPath)) {
+ const { triggers } = this.getDynamicValue(
+ unEvalPropertyValue,
+ currentTree,
+ true,
+ undefined,
+ );
+ valueToValidate = triggers;
+ }
+ const { parsed, isValid, message, transformed } = validateWidgetProperty(
+ this.widgetConfigMap,
+ widget.type,
+ entityPropertyName,
+ valueToValidate,
+ widget,
+ currentTree,
+ );
+ const evaluatedValue = isValid
+ ? parsed
+ : _.isUndefined(transformed)
+ ? evalPropertyValue
+ : transformed;
+ const safeEvaluatedValue = removeFunctions(evaluatedValue);
+ _.set(widget, `evaluatedValues.${entityPropertyName}`, safeEvaluatedValue);
+ if (!isValid) {
+ _.set(widget, `invalidProps.${entityPropertyName}`, true);
+ _.set(widget, `validationMessages.${entityPropertyName}`, message);
+ } else {
+ _.set(widget, `invalidProps.${entityPropertyName}`, false);
+ _.set(widget, `validationMessages.${entityPropertyName}`, "");
+ }
+
+ if (isPathADynamicTrigger(widget, entityPropertyName)) {
+ return unEvalPropertyValue;
+ } else {
+ const parsedCache = this.getParsedValueCache(propertyPath);
+ if (!equal(parsedCache.value, parsed)) {
+ this.parsedValueCache.set(propertyPath, {
+ value: parsed,
+ version: Date.now(),
+ });
+ }
+ return parsed;
+ }
+ }
+
+ overwriteDefaultDependentProps(
+ defaultProperty: string,
+ propertyValue: any,
+ propertyPath: string,
+ entity: DataTreeWidget,
+ ) {
+ const defaultPropertyCache = this.getParsedValueCache(
+ `${entity.widgetName}.${defaultProperty}`,
+ );
+ const propertyCache = this.getParsedValueCache(propertyPath);
+ if (
+ propertyValue === undefined ||
+ propertyCache.version < defaultPropertyCache.version
+ ) {
+ return defaultPropertyCache.value;
+ }
+ return propertyValue;
+ }
+
+ updateDependencyMap(
+ differences: Array>,
+ unEvalDataTree: DataTree,
+ ): {
+ dependenciesOfRemovedPaths: Array;
+ removedPaths: Array;
+ } {
+ const diffCalcStart = performance.now();
+ let didUpdateDependencyMap = false;
+ const dependenciesOfRemovedPaths: Array = [];
+ const removedPaths: Array = [];
+
+ // This is needed for NEW and DELETE events below.
+ // In worst case, it tends to take ~12.5% of entire diffCalc (8 ms out of 67ms for 132 array of NEW)
+ // TODO: Optimise by only getting paths of changed node
+ this.allKeys = getAllPaths(unEvalDataTree);
+ // Transform the diff library events to Appsmith evaluator events
+ differences
+ .map(translateDiffEventToDataTreeDiffEvent)
+ .forEach((dataTreeDiff) => {
+ const entityName = dataTreeDiff.payload.propertyPath.split(".")[0];
+ let entity = unEvalDataTree[entityName];
+ if (dataTreeDiff.event === DataTreeDiffEvent.DELETE) {
+ entity = this.oldUnEvalTree[entityName];
+ }
+ const entityType = isValidEntity(entity) ? entity.ENTITY_TYPE : "noop";
+
+ 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 (
+ isWidget(entity) &&
+ !this.isDynamicLeaf(
+ unEvalDataTree,
+ dataTreeDiff.payload.propertyPath,
+ )
+ ) {
+ const widgetDependencyMap: DependencyMap = this.listEntityDependencies(
+ entity as DataTreeWidget,
+ entityName,
+ );
+ if (Object.keys(widgetDependencyMap).length) {
+ didUpdateDependencyMap = true;
+ Object.assign(this.dependencyMap, widgetDependencyMap);
+ }
+ }
+ // Either a new entity or a new property path has been added. Go through existing dynamic bindings and
+ // find out if a new dependency has to be created because the property path used in the binding just became
+ // eligible
+ const possibleReferencesInOldBindings: DependencyMap = this.getPropertyPathReferencesInExistingBindings(
+ unEvalDataTree,
+ dataTreeDiff.payload.propertyPath,
+ );
+ // We have found some bindings which are related to the new property path and hence should be added to the
+ // global dependency map
+ if (Object.keys(possibleReferencesInOldBindings).length) {
+ didUpdateDependencyMap = true;
+ Object.assign(
+ this.dependencyMap,
+ possibleReferencesInOldBindings,
+ );
+ }
+ break;
+ }
+ case DataTreeDiffEvent.DELETE: {
+ // Add to removedPaths as they have been deleted from the evalTree
+ removedPaths.push(dataTreeDiff.payload.propertyPath);
+ // If an existing widget was deleted, remove all the bindings from the global dependency map
+ if (
+ isWidget(entity) &&
+ dataTreeDiff.payload.propertyPath === entityName
+ ) {
+ const widgetBindings = this.listEntityDependencies(
+ entity,
+ entityName,
+ );
+ Object.keys(widgetBindings).forEach((widgetDep) => {
+ didUpdateDependencyMap = true;
+ delete this.dependencyMap[widgetDep];
+ });
+ }
+ // Either an existing entity or an existing property path has been deleted. Update the global dependency map
+ // by removing the bindings from the same.
+ Object.keys(this.dependencyMap).forEach((dependencyPath) => {
+ didUpdateDependencyMap = true;
+ if (
+ isChildPropertyPath(
+ dataTreeDiff.payload.propertyPath,
+ dependencyPath,
+ )
+ ) {
+ delete this.dependencyMap[dependencyPath];
+ } else {
+ const toRemove: Array = [];
+ this.dependencyMap[dependencyPath].forEach(
+ (dependantPath) => {
+ if (
+ isChildPropertyPath(
+ dataTreeDiff.payload.propertyPath,
+ dependantPath,
+ )
+ ) {
+ dependenciesOfRemovedPaths.push(dependencyPath);
+ toRemove.push(dependantPath);
+ }
+ },
+ );
+ this.dependencyMap[dependencyPath] = _.difference(
+ this.dependencyMap[dependencyPath],
+ toRemove,
+ );
+ }
+ });
+ break;
+ }
+
+ 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
+ // an evaluation.
+ if (
+ (entityType === ENTITY_TYPE.WIDGET ||
+ entityType === ENTITY_TYPE.ACTION) &&
+ typeof dataTreeDiff.payload.value === "string"
+ ) {
+ const entity: EntityWithBindings = unEvalDataTree[
+ entityName
+ ] as EntityWithBindings;
+ const isABindingPath = isPathADynamicBinding(
+ entity,
+ dataTreeDiff.payload.propertyPath.substring(
+ dataTreeDiff.payload.propertyPath.indexOf(".") + 1,
+ ),
+ );
+ if (isABindingPath) {
+ didUpdateDependencyMap = true;
+
+ const { jsSnippets } = getDynamicBindings(
+ dataTreeDiff.payload.value,
+ );
+ const correctSnippets = jsSnippets.filter(
+ (jsSnippet) => !!jsSnippet,
+ );
+ // 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;
+ } 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
+ ];
+ }
+ }
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+ });
+ const diffCalcEnd = performance.now();
+ const subDepCalcStart = performance.now();
+ 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 = makeParentsDependOnChildren(this.dependencyMap);
+ }
+ const subDepCalcEnd = performance.now();
+ const updateChangedDependenciesStart = performance.now();
+ // If the global dependency map has changed, re-calculate the sort order for all entities and the
+ // global inverse dependency map
+ if (didUpdateDependencyMap) {
+ // This is being called purely to test for new circular dependencies that might have been added
+ this.sortedDependencies = this.sortDependencies(this.dependencyMap);
+ this.inverseDependencyMap = this.getInverseDependencyTree();
+ }
+
+ const updateChangedDependenciesStop = performance.now();
+ this.logs.push({
+ diffCalcDeps: (diffCalcEnd - diffCalcStart).toFixed(2),
+ subDepCalc: (subDepCalcEnd - subDepCalcStart).toFixed(2),
+ updateChangedDependencies: (
+ updateChangedDependenciesStop - updateChangedDependenciesStart
+ ).toFixed(2),
+ });
+
+ return { dependenciesOfRemovedPaths, removedPaths };
+ }
+
+ calculateSubTreeSortOrder(
+ differences: Diff[],
+ dependenciesOfRemovedPaths: Array,
+ removedPaths: Array,
+ unEvalTree: DataTree,
+ ) {
+ const changePaths: Set = new Set(dependenciesOfRemovedPaths);
+ for (const d of differences) {
+ if (!Array.isArray(d.path) || d.path.length === 0) continue; // Null check for typescript
+ // Apply the changes into the evalTree so that it gets the latest changes
+ applyChange(this.evalTree, undefined, d);
+
+ changePaths.add(convertPathToString(d.path));
+ // If this is a property path change, simply add for evaluation and move on
+ if (!this.isDynamicLeaf(unEvalTree, convertPathToString(d.path))) {
+ // A parent level property has been added or deleted
+ /**
+ * We want to add all pre-existing dynamic and static bindings in dynamic paths of this entity to get evaluated and validated.
+ * Example:
+ * - Table1.tableData = {{Api1.data}}
+ * - Api1 gets created.
+ * - This function gets called with a diff {path:["Api1"]}
+ * We want to add `Api.data` to changedPaths so that `Table1.tableData` can be discovered below.
+ */
+ const entityName = d.path[0];
+ const entity = unEvalTree[entityName];
+ if (!entity) {
+ continue;
+ }
+ if (!isAction(entity) && !isWidget(entity)) {
+ continue;
+ }
+ if (isAction(entity)) {
+ // TODO create proper binding paths for actions
+ changePaths.add(convertPathToString(d.path));
+ continue;
+ }
+ const parentPropertyPath = convertPathToString(d.path);
+ Object.keys(entity.bindingPaths).forEach((relativePath) => {
+ const childPropertyPath = `${entityName}.${relativePath}`;
+ if (isChildPropertyPath(parentPropertyPath, childPropertyPath)) {
+ changePaths.add(childPropertyPath);
+ }
+ });
+ }
+ }
+
+ // If a nested property path has changed and someone (say x) is dependent on the parent of the said property,
+ // x must also be evaluated. For example, the following relationship exists in dependency map:
+ // < "Input1.defaultText" : ["Table1.selectedRow.email"] >
+ // If Table1.selectedRow has changed, then Input1.defaultText must also be evaluated because Table1.selectedRow.email
+ // is a nested property of Table1.selectedRow
+ const changePathsWithNestedDependants = addDependantsOfNestedPropertyPaths(
+ Array.from(changePaths),
+ this.inverseDependencyMap,
+ );
+
+ const trimmedChangedPaths = trimDependantChangePaths(
+ changePathsWithNestedDependants,
+ this.dependencyMap,
+ );
+
+ // Now that we have all the root nodes which have to be evaluated, recursively find all the other paths which
+ // would get impacted because they are dependent on the said root nodes and add them in order
+ const completeSortOrder = this.getCompleteSortOrder(
+ trimmedChangedPaths,
+ this.inverseDependencyMap,
+ );
+ // Remove any paths that do no exist in the data tree any more
+ return _.difference(completeSortOrder, removedPaths);
+ }
+
+ getInverseDependencyTree(): DependencyMap {
+ const inverseDag: DependencyMap = {};
+ this.sortedDependencies.forEach((propertyPath) => {
+ const incomingEdges: Array = this.dependencyMap[propertyPath];
+ if (incomingEdges) {
+ incomingEdges.forEach((edge) => {
+ const node = inverseDag[edge];
+ if (node) {
+ node.push(propertyPath);
+ } else {
+ inverseDag[edge] = [propertyPath];
+ }
+ });
+ }
+ });
+ return inverseDag;
+ }
+
+ // TODO: create the lookup dictionary once
+ // Response from listEntityDependencies only needs to change if the entity itself changed.
+ // Check if it is possible to make a flat structure with O(1) or at least O(m) lookup instead of O(n*m)
+ getPropertyPathReferencesInExistingBindings(
+ dataTree: DataTree,
+ propertyPath: string,
+ ) {
+ const possibleRefs: DependencyMap = {};
+ Object.keys(dataTree).forEach((entityName) => {
+ const entity = dataTree[entityName];
+ if (
+ isValidEntity(entity) &&
+ (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION ||
+ entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET)
+ ) {
+ const entityPropertyBindings = this.listEntityDependencies(
+ entity,
+ entityName,
+ );
+ Object.keys(entityPropertyBindings).forEach((path) => {
+ const propertyBindings = entityPropertyBindings[path];
+ const references = _.flatten(
+ propertyBindings.map((binding) =>
+ extractReferencesFromBinding(binding, this.allKeys),
+ ),
+ );
+ references.forEach((value) => {
+ if (isChildPropertyPath(propertyPath, value)) {
+ possibleRefs[path] = propertyBindings;
+ }
+ });
+ });
+ }
+ });
+ return possibleRefs;
+ }
+
+ evaluateActionBindings(
+ bindings: string[],
+ executionParams?: Record | string,
+ ) {
+ // We might get execution params as an object or as a string.
+ // If the user has added a proper object (valid case) it will be an object
+ // If they have not added any execution params or not an object
+ // it would be a string (invalid case)
+ let evaluatedExecutionParams: Record = {};
+ if (executionParams && _.isObject(executionParams)) {
+ evaluatedExecutionParams = this.getDynamicValue(
+ `{{${JSON.stringify(executionParams)}}}`,
+ this.evalTree,
+ false,
+ );
+ }
+
+ // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility)
+ const bindingsForExecutionParams: string[] = bindings.map(
+ (binding: string) =>
+ binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY),
+ );
+
+ const dataTreeWithExecutionParams = Object.assign({}, this.evalTree, {
+ [EXECUTION_PARAM_KEY]: evaluatedExecutionParams,
+ });
+
+ return bindingsForExecutionParams.map((binding) =>
+ this.getDynamicValue(
+ `{{${binding}}}`,
+ dataTreeWithExecutionParams,
+ false,
+ ),
+ );
+ }
+
+ clearErrors() {
+ this.errors = [];
+ }
+
+ clearLogs() {
+ this.logs = [];
+ }
+}
+
+const extractReferencesFromBinding = (
+ path: string,
+ all: Record,
+): Array => {
+ const subDeps: Array = [];
+ const identifiers = path.match(/[a-zA-Z_$][a-zA-Z_$0-9.\[\]]*/g) || [path];
+ identifiers.forEach((identifier: string) => {
+ // If the identifier exists directly, add it and return
+ if (all.hasOwnProperty(identifier)) {
+ subDeps.push(identifier);
+ return;
+ }
+ const subpaths = _.toPath(identifier);
+ let current = "";
+ // We want to keep going till we reach top level, but not add top level
+ // Eg: Input1.text should not depend on entire Table1 unless it explicitly asked for that.
+ // This is mainly to avoid a lot of unnecessary evals, if we feel this is wrong
+ // we can remove the length requirement and it will still work
+ while (subpaths.length > 1) {
+ current = convertPathToString(subpaths);
+ // We've found the dep, add it and return
+ if (all.hasOwnProperty(current)) {
+ subDeps.push(current);
+ return;
+ }
+ subpaths.pop();
+ }
+ });
+ return _.uniq(subDeps);
+};
+
+// TODO cryptic comment below. Dont know if we still need this. Duplicate function
+// 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({
+ // type: EvalErrorTypes.BAD_UNEVAL_TREE_ERROR,
+ // message: "Data tree entity is not an object",
+ // context: entity,
+ // });
+ return false;
+ }
+ return "ENTITY_TYPE" in entity;
+}
+
+function isWidget(entity: DataTreeEntity): entity is DataTreeWidget {
+ return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET;
+}
+
+function isAction(entity: DataTreeEntity): entity is DataTreeAction {
+ return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.ACTION;
+}
diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts
index 179835073d..00d3b4d9f6 100644
--- a/app/client/src/workers/evaluation.test.ts
+++ b/app/client/src/workers/evaluation.test.ts
@@ -1,4 +1,3 @@
-import { DataTreeEvaluator } from "./evaluation.worker";
import {
DataTreeAction,
DataTreeWidget,
@@ -7,6 +6,7 @@ import {
import { WidgetTypeConfigMap } from "../utils/WidgetFactory";
import { RenderModes, WidgetTypes } from "../constants/WidgetConstants";
import { PluginType } from "../entities/Action";
+import DataTreeEvaluator from "workers/DataTreeEvaluator";
const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = {
CONTAINER_WIDGET: {
diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts
index aaa1f30778..625d1b0fd4 100644
--- a/app/client/src/workers/evaluation.worker.ts
+++ b/app/client/src/workers/evaluation.worker.ts
@@ -1,62 +1,22 @@
-import {
- ActionDescription,
- DataTree,
- DataTreeAction,
- DataTreeEntity,
- DataTreeObjectEntity,
- DataTreeWidget,
- ENTITY_TYPE,
-} from "entities/DataTree/dataTreeFactory";
+import { DataTree } from "entities/DataTree/dataTreeFactory";
import {
DependencyMap,
EVAL_WORKER_ACTIONS,
EvalError,
EvalErrorTypes,
- extraLibraries,
- getDynamicBindings,
- getEntityDynamicBindingPathList,
- isChildPropertyPath,
- isPathADynamicBinding,
- isPathADynamicTrigger,
- unsafeFunctionForEval,
-} from "../utils/DynamicBindingUtils";
-import _ from "lodash";
-import { WidgetTypeConfigMap } from "../utils/WidgetFactory";
-import toposort from "toposort";
-import { DATA_BIND_REGEX } from "../constants/BindingsConstants";
-import equal from "fast-deep-equal/es6";
-import unescapeJS from "unescape-js";
-
-import { applyChange, diff, Diff } from "deep-diff";
+} from "utils/DynamicBindingUtils";
import {
- addDependantsOfNestedPropertyPaths,
- convertPathToString,
+ addFunctions,
CrashingError,
- DataTreeDiffEvent,
- getAllPaths,
- getImmediateParentsOfPropertyPaths,
getValidatedTree,
- makeParentsDependOnChildren,
removeFunctions,
- removeFunctionsFromDataTree,
- translateDiffEventToDataTreeDiffEvent,
- trimDependantChangePaths,
validateWidgetProperty,
} from "./evaluationUtils";
-import {
- EXECUTION_PARAM_KEY,
- EXECUTION_PARAM_REFERENCE_REGEX,
-} from "../constants/ActionConstants";
+import DataTreeEvaluator from "workers/DataTreeEvaluator";
const ctx: Worker = self as any;
let dataTreeEvaluator: DataTreeEvaluator | undefined;
-let LOGS: any[] = [];
-
-type EvalResult = {
- result: any;
- triggers?: ActionDescription[];
-};
//TODO: Create a more complete RPC setup in the subtree-eval branch.
function messageEventListener(
@@ -72,7 +32,6 @@ function messageEventListener(
responseData,
timeTaken: (endTime - startTime).toFixed(2),
});
- LOGS = [];
};
}
@@ -84,6 +43,7 @@ ctx.addEventListener(
const { widgetTypeConfigMap, unevalTree } = requestData;
let dataTree: DataTree = unevalTree;
let errors: EvalError[] = [];
+ let logs: any[] = [];
let dependencies: DependencyMap = {};
try {
if (!dataTreeEvaluator) {
@@ -100,9 +60,12 @@ ctx.addEventListener(
dependencies = dataTreeEvaluator.inverseDependencyMap;
errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors();
+ logs = dataTreeEvaluator.logs;
+ dataTreeEvaluator.clearLogs();
} catch (e) {
if (dataTreeEvaluator !== undefined) {
errors = dataTreeEvaluator.errors;
+ logs = dataTreeEvaluator.logs;
}
if (!(e instanceof CrashingError)) {
errors.push({
@@ -118,7 +81,7 @@ ctx.addEventListener(
dataTree,
dependencies,
errors,
- logs: LOGS,
+ logs,
};
}
case EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS: {
@@ -152,7 +115,17 @@ ctx.addEventListener(
callbackData,
);
const cleanTriggers = removeFunctions(triggers);
- const errors = dataTreeEvaluator.errors;
+ // Transforming eval errors into eval trigger errors. Since trigger
+ // errors occur less, we want to treat it separately
+ const errors = dataTreeEvaluator.errors.map((error) => {
+ if (error.type === EvalErrorTypes.EVAL_ERROR) {
+ return {
+ ...error,
+ type: EvalErrorTypes.EVAL_TRIGGER_ERROR,
+ };
+ }
+ return error;
+ });
dataTreeEvaluator.clearErrors();
return { triggers: cleanTriggers, errors };
}
@@ -200,1243 +173,3 @@ ctx.addEventListener(
}
}),
);
-
-export class DataTreeEvaluator {
- dependencyMap: DependencyMap = {};
- sortedDependencies: Array = [];
- inverseDependencyMap: DependencyMap = {};
- widgetConfigMap: WidgetTypeConfigMap = {};
- evalTree: DataTree = {};
- allKeys: Record = {};
- oldUnEvalTree: DataTree = {};
- errors: EvalError[] = [];
- parsedValueCache: Map<
- string,
- {
- value: any;
- version: number;
- }
- > = new Map();
-
- constructor(widgetConfigMap: WidgetTypeConfigMap) {
- this.widgetConfigMap = widgetConfigMap;
- }
-
- createFirstTree(unEvalTree: DataTree) {
- const totalStart = performance.now();
- // Add functions to the tree
- const withFunctions = addFunctions(unEvalTree);
- // Create dependency map
- const createDependencyStart = performance.now();
- this.dependencyMap = this.createDependencyMap(withFunctions);
- const createDependencyEnd = performance.now();
- // Sort
- const sortDependenciesStart = performance.now();
- this.sortedDependencies = this.sortDependencies(this.dependencyMap);
- const sortDependenciesEnd = performance.now();
- // Inverse
- this.inverseDependencyMap = this.getInverseDependencyTree();
- // Evaluate
- const evaluateStart = performance.now();
- const evaluatedTree = this.evaluateTree(
- withFunctions,
- this.sortedDependencies,
- );
- const evaluateEnd = performance.now();
- // Validate Widgets
- const validateStart = performance.now();
- const validated = getValidatedTree(this.widgetConfigMap, evaluatedTree);
- const validateEnd = performance.now();
- // Remove functions
- this.evalTree = removeFunctionsFromDataTree(validated);
- this.oldUnEvalTree = unEvalTree;
- const totalEnd = performance.now();
- const timeTakenForFirstTree = {
- total: (totalEnd - totalStart).toFixed(2),
- createDependencies: (createDependencyEnd - createDependencyStart).toFixed(
- 2,
- ),
- sortDependencies: (sortDependenciesEnd - sortDependenciesStart).toFixed(
- 2,
- ),
- evaluate: (evaluateEnd - evaluateStart).toFixed(2),
- validate: (validateEnd - validateStart).toFixed(2),
- dependencies: {
- map: JSON.parse(JSON.stringify(this.dependencyMap)),
- inverseMap: JSON.parse(JSON.stringify(this.inverseDependencyMap)),
- sortedList: JSON.parse(JSON.stringify(this.sortedDependencies)),
- },
- };
- LOGS.push({ timeTakenForFirstTree });
- }
-
- isDynamicLeaf(unEvalTree: DataTree, propertyPath: string) {
- const [entityName, ...propPathEls] = _.toPath(propertyPath);
- // Framework feature: Top level items are never leaves
- if (entityName === propertyPath) return false;
- // Ignore if this was a delete op
- if (!(entityName in unEvalTree)) return false;
-
- const entity = unEvalTree[entityName];
- if (!isAction(entity) && !isWidget(entity)) return false;
- const relativePropertyPath = convertPathToString(propPathEls);
- return relativePropertyPath in entity.bindingPaths;
- }
-
- updateDataTree(unEvalTree: DataTree) {
- const totalStart = performance.now();
- // Add appsmith internal functions to the tree ex. navigateTo / showModal
- const unEvalTreeWithFunctions = addFunctions(unEvalTree);
- // Calculate diff
- const diffCheckTimeStart = performance.now();
- const differences = diff(this.oldUnEvalTree, unEvalTree) || [];
- // Since eval tree is listening to possible events that dont cause differences
- // We want to check if no diffs are present and bail out early
- if (differences.length === 0) {
- return this.evalTree;
- }
- const diffCheckTimeStop = performance.now();
- // Check if dependencies have changed
- const updateDependenciesStart = performance.now();
-
- // Find all the paths that have changed as part of the difference and update the
- // global dependency map if an existing dynamic binding has now become legal
- const {
- dependenciesOfRemovedPaths,
- removedPaths,
- } = this.updateDependencyMap(differences, unEvalTreeWithFunctions);
- const updateDependenciesStop = performance.now();
-
- const calculateSortOrderStart = performance.now();
-
- const subTreeSortOrder: string[] = this.calculateSubTreeSortOrder(
- differences,
- dependenciesOfRemovedPaths,
- removedPaths,
- unEvalTree,
- );
-
- const calculateSortOrderStop = performance.now();
-
- LOGS.push({
- differences,
- subTreeSortOrder,
- sortedDependencies: this.sortedDependencies,
- inverse: this.inverseDependencyMap,
- updatedDependencyMap: this.dependencyMap,
- });
-
- // Evaluate
- const evalStart = performance.now();
- // We are setting all values from our uneval tree to the old eval tree we have
- // this way we can get away with just evaluating the sort order and nothing else
- subTreeSortOrder.forEach((propertyPath) => {
- if (this.isDynamicLeaf(unEvalTree, propertyPath)) {
- const unEvalPropValue = _.get(unEvalTree, propertyPath);
- _.set(this.evalTree, propertyPath, unEvalPropValue);
- }
- });
-
- // Remove any deleted paths from the eval tree
- removedPaths.forEach((removedPath) => {
- _.unset(this.evalTree, removedPath);
- });
- const evaluatedTree = this.evaluateTree(this.evalTree, subTreeSortOrder);
- const evalStop = performance.now();
-
- // Remove functions
- this.evalTree = removeFunctionsFromDataTree(evaluatedTree);
- const totalEnd = performance.now();
- // TODO: For some reason we are passing some reference which are getting mutated.
- // Need to check why big api responses are getting split between two eval runs
- this.oldUnEvalTree = unEvalTree;
- const timeTakenForSubTreeEval = {
- total: (totalEnd - totalStart).toFixed(2),
- findDifferences: (diffCheckTimeStop - diffCheckTimeStart).toFixed(2),
- updateDependencies: (
- updateDependenciesStop - updateDependenciesStart
- ).toFixed(2),
- calculateSortOrder: (
- calculateSortOrderStop - calculateSortOrderStart
- ).toFixed(2),
- evaluate: (evalStop - evalStart).toFixed(2),
- };
- LOGS.push({ timeTakenForSubTreeEval });
- return this.evalTree;
- }
-
- getCompleteSortOrder(
- changes: Array,
- inverseMap: DependencyMap,
- ): Array {
- let finalSortOrder: Array = [];
- let computeSortOrder = true;
- // Initialize parents with the current sent of property paths that need to be evaluated
- let parents = changes;
- let subSortOrderArray: Array;
- while (computeSortOrder) {
- // Get all the nodes that would be impacted by the evaluation of the nodes in parents array in sorted order
- subSortOrderArray = this.getEvaluationSortOrder(parents, inverseMap);
-
- // Add all the sorted nodes in the final list
- finalSortOrder = [...finalSortOrder, ...subSortOrderArray];
-
- parents = getImmediateParentsOfPropertyPaths(subSortOrderArray);
- // If we find parents of the property paths in the sorted array, we should continue finding all the nodes dependent
- // on the parents
- computeSortOrder = parents.length > 0;
- }
-
- // Remove duplicates from this list. Since we explicitly walk down the tree and implicitly (by fetching parents) walk
- // up the tree, there are bound to be many duplicates.
- const uniqueKeysInSortOrder = [...new Set(finalSortOrder)];
-
- const sortOrderPropertyPaths = Array.from(uniqueKeysInSortOrder);
-
- //Trim this list to now remove the property paths which are simply entity names
- const finalSortOrderArray: Array = [];
- sortOrderPropertyPaths.forEach((propertyPath) => {
- const lastIndexOfDot = propertyPath.lastIndexOf(".");
- // Only do this for property paths and not the entity themselves
- if (lastIndexOfDot !== -1) {
- finalSortOrderArray.push(propertyPath);
- }
- });
-
- return finalSortOrderArray;
- }
-
- getEvaluationSortOrder(
- changes: Array,
- inverseMap: DependencyMap,
- ): Array {
- const sortOrder: Array = [...changes];
- let iterator = 0;
- while (iterator < sortOrder.length) {
- // Find all the nodes who are to be evaluated when sortOrder[iterator] changes
- const newNodes = inverseMap[sortOrder[iterator]];
-
- // If we find more nodes that would be impacted by the evaluation of the node being investigated
- // we add these to the sort order.
- if (newNodes) {
- newNodes.forEach((toBeEvaluatedNode) => {
- // Only add the nodes if they haven't been already added for evaluation in the list. Since we are doing
- // breadth first traversal, we should be safe in not changing the evaluation order and adding this now at this
- // point instead of the previous index found.
- if (!sortOrder.includes(toBeEvaluatedNode)) {
- sortOrder.push(toBeEvaluatedNode);
- }
- });
- }
- iterator++;
- }
- return sortOrder;
- }
-
- // getValidationPaths(unevalDataTree: DataTree): Record> {
- // const result: Record> = {};
- // for (const key in unevalDataTree) {
- // const entity = unevalDataTree[key];
- // if (isAction(entity)) {
- // // TODO: add the properties to a global map somewhere
- // result[entity.name] = new Set(
- // ["config", "isLoading", "data"].map((e) => `${entity.name}.${e}`),
- // );
- // } else if (isWidget(entity)) {
- // if (!this.widgetConfigMap[entity.type])
- // throw new CrashingError(
- // `${entity.widgetName} has unrecognised entity type: ${entity.type}`,
- // );
- // const { validations } = this.widgetConfigMap[entity.type];
- //
- // result[entity.widgetName] = new Set(
- // Object.keys(validations).map((e) => `${entity.widgetName}.${e}`),
- // );
- // }
- // }
- // return result;
- // }
-
- createDependencyMap(unEvalTree: DataTree): DependencyMap {
- let dependencyMap: DependencyMap = {};
- this.allKeys = getAllPaths(unEvalTree);
- Object.keys(unEvalTree).forEach((entityName) => {
- const entity = unEvalTree[entityName];
- if (isAction(entity) || isWidget(entity)) {
- const entityListedDependencies = this.listEntityDependencies(
- entity,
- entityName,
- );
- dependencyMap = { ...dependencyMap, ...entityListedDependencies };
- }
- });
- Object.keys(dependencyMap).forEach((key) => {
- dependencyMap[key] = _.flatten(
- dependencyMap[key].map((path) =>
- extractReferencesFromBinding(path, this.allKeys),
- ),
- );
- });
- // TODO make this run only for widgets and not actions
- dependencyMap = makeParentsDependOnChildren(dependencyMap);
- return dependencyMap;
- }
-
- listEntityDependencies(
- entity: DataTreeWidget | DataTreeAction,
- entityName: string,
- ): DependencyMap {
- const dependencies: DependencyMap = {};
- const dynamicBindingPathList = getEntityDynamicBindingPathList(entity);
- if (dynamicBindingPathList.length) {
- dynamicBindingPathList.forEach((dynamicPath) => {
- const propertyPath = dynamicPath.key;
- const unevalPropValue = _.get(entity, propertyPath);
- const { jsSnippets } = getDynamicBindings(unevalPropValue);
- const existingDeps =
- dependencies[`${entityName}.${propertyPath}`] || [];
- dependencies[`${entityName}.${propertyPath}`] = existingDeps.concat(
- jsSnippets.filter((jsSnippet) => !!jsSnippet),
- );
- });
- }
- if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) {
- // Set default property dependency
- const defaultProperties = this.widgetConfigMap[entity.type]
- .defaultProperties;
- Object.keys(defaultProperties).forEach((property) => {
- dependencies[`${entityName}.${property}`] = [
- `${entityName}.${defaultProperties[property]}`,
- ];
- });
- }
- return dependencies;
- }
-
- evaluateTree(
- oldUnevalTree: DataTree,
- sortedDependencies: Array,
- ): DataTree {
- const tree = _.cloneDeep(oldUnevalTree);
- try {
- return sortedDependencies.reduce(
- (currentTree: DataTree, propertyPath: string) => {
- LOGS.push(`evaluating ${propertyPath}`);
- const entityName = propertyPath.split(".")[0];
- const entity: DataTreeEntity = currentTree[entityName];
- const unEvalPropertyValue = _.get(currentTree as any, propertyPath);
- const isABindingPath =
- (isAction(entity) || isWidget(entity)) &&
- isPathADynamicBinding(
- entity,
- propertyPath.substring(propertyPath.indexOf(".") + 1),
- );
- let evalPropertyValue;
- const requiresEval =
- isABindingPath && isDynamicValue(unEvalPropertyValue);
- if (requiresEval) {
- try {
- evalPropertyValue = this.evaluateDynamicProperty(
- propertyPath,
- currentTree,
- unEvalPropertyValue,
- );
- } catch (e) {
- this.errors.push({
- type: EvalErrorTypes.EVAL_PROPERTY_ERROR,
- message: e.message,
- context: {
- propertyPath,
- },
- });
- evalPropertyValue = undefined;
- }
- } else {
- evalPropertyValue = unEvalPropertyValue;
- }
- if (isWidget(entity)) {
- const widgetEntity = entity;
- // TODO fix for nested properties
- // For nested properties like Table1.selectedRow.email
- // The following line will calculated the property name to be selectedRow
- // instead of selectedRow.email
- const propertyName = propertyPath.split(".")[1];
- if (propertyName) {
- let parsedValue = this.validateAndParseWidgetProperty(
- propertyPath,
- widgetEntity,
- currentTree,
- evalPropertyValue,
- unEvalPropertyValue,
- );
- const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type]
- .defaultProperties;
- const hasDefaultProperty = propertyName in defaultPropertyMap;
- if (hasDefaultProperty) {
- const defaultProperty = defaultPropertyMap[propertyName];
- parsedValue = this.overwriteDefaultDependentProps(
- defaultProperty,
- parsedValue,
- propertyPath,
- widgetEntity,
- );
- }
- return _.set(currentTree, propertyPath, parsedValue);
- }
- return _.set(currentTree, propertyPath, evalPropertyValue);
- } else {
- return _.set(currentTree, propertyPath, evalPropertyValue);
- }
- },
- tree,
- );
- } catch (e) {
- this.errors.push({
- type: EvalErrorTypes.EVAL_TREE_ERROR,
- message: e.message,
- });
- return tree;
- }
- }
-
- sortDependencies(dependencyMap: DependencyMap): Array {
- const dependencyTree: Array<[string, string]> = [];
- Object.keys(dependencyMap).forEach((key: string) => {
- if (dependencyMap[key].length) {
- dependencyMap[key].forEach((dep) => dependencyTree.push([key, dep]));
- } else {
- // Set no dependency
- dependencyTree.push([key, ""]);
- }
- });
-
- try {
- // sort dependencies and remove empty dependencies
- return toposort(dependencyTree)
- .reverse()
- .filter((d) => !!d);
- } catch (e) {
- this.errors.push({
- type: EvalErrorTypes.DEPENDENCY_ERROR,
- message: e.message,
- });
- console.error("CYCLICAL DEPENDENCY MAP", dependencyMap);
- throw new CrashingError(e.message);
- }
- }
-
- getParsedValueCache(propertyPath: string) {
- return (
- this.parsedValueCache.get(propertyPath) || {
- value: undefined,
- version: 0,
- }
- );
- }
-
- clearPropertyCache(propertyPath: string) {
- this.parsedValueCache.delete(propertyPath);
- }
-
- clearPropertyCacheOfWidget(widgetName: string) {
- // TODO check if this loop mutating itself is safe
- this.parsedValueCache.forEach((value, key) => {
- const match = key.match(`${widgetName}.`);
- if (match) {
- this.parsedValueCache.delete(key);
- }
- });
- }
-
- clearAllCaches() {
- this.parsedValueCache.clear();
- this.clearErrors();
- this.dependencyMap = {};
- this.allKeys = {};
- this.inverseDependencyMap = {};
- this.sortedDependencies = [];
- this.evalTree = {};
- this.oldUnEvalTree = {};
- }
-
- getDynamicValue(
- dynamicBinding: string,
- data: DataTree,
- returnTriggers: boolean,
- callBackData?: Array,
- ) {
- // Get the {{binding}} bound values
- const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding);
- if (returnTriggers) {
- const result = this.evaluateDynamicBoundValue(
- data,
- jsSnippets[0],
- callBackData,
- );
- return result.triggers;
- }
- if (stringSegments.length) {
- // Get the Data Tree value of those "binding "paths
- const values = jsSnippets.map((jsSnippet, index) => {
- if (jsSnippet) {
- const result = this.evaluateDynamicBoundValue(
- data,
- jsSnippet,
- callBackData,
- );
- return result.result;
- } else {
- return stringSegments[index];
- }
- });
-
- // if it is just one binding, no need to create template string
- if (stringSegments.length === 1) return values[0];
- // else return a string template with bindings
- return createDynamicValueString(dynamicBinding, stringSegments, values);
- }
- return undefined;
- }
-
- // Paths are expected to have "{name}.{path}" signature
- // Also returns any action triggers found after evaluating value
- evaluateDynamicBoundValue(
- data: DataTree,
- path: string,
- callbackData?: Array,
- ): EvalResult {
- try {
- const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, "");
- return this.evaluate(unescapedJS, data, callbackData);
- } catch (e) {
- this.errors.push({
- type: EvalErrorTypes.UNESCAPE_STRING_ERROR,
- message: e.message,
- context: {
- path,
- },
- });
- return { result: undefined, triggers: [] };
- }
- }
-
- evaluate(js: string, data: DataTree, callbackData?: Array): EvalResult {
- const scriptToEvaluate = `
- function closedFunction () {
- const result = ${js};
- return { result, triggers: self.triggers }
- }
- closedFunction()
- `;
- const scriptWithCallback = `
- function callback (script) {
- const userFunction = script;
- const result = userFunction.apply(self, CALLBACK_DATA);
- return { result, triggers: self.triggers };
- }
- callback(${js});
- `;
- const script = callbackData ? scriptWithCallback : scriptToEvaluate;
- try {
- const { result, triggers } = (function() {
- /**** Setting the eval context ****/
- const GLOBAL_DATA: Record = {};
- ///// Adding callback data
- GLOBAL_DATA.CALLBACK_DATA = callbackData;
- ///// Adding Data tree
- Object.keys(data).forEach((datum) => {
- GLOBAL_DATA[datum] = data[datum];
- });
- ///// Fixing action paths and capturing their execution response
- if (data.actionPaths) {
- GLOBAL_DATA.triggers = [];
- const pusher = function(
- this: DataTree,
- action: any,
- ...payload: any[]
- ) {
- const actionPayload = action(...payload);
- GLOBAL_DATA.triggers.push(actionPayload);
- };
- GLOBAL_DATA.actionPaths.forEach((path: string) => {
- const action = _.get(GLOBAL_DATA, path);
- const entity = _.get(GLOBAL_DATA, path.split(".")[0]);
- if (action) {
- _.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity)));
- }
- });
- }
-
- // Set it to self
- Object.keys(GLOBAL_DATA).forEach((key) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore: No types available
- self[key] = GLOBAL_DATA[key];
- });
-
- ///// Adding extra libraries separately
- extraLibraries.forEach((library) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore: No types available
- self[library.accessor] = library.lib;
- });
-
- ///// Remove all unsafe functions
- unsafeFunctionForEval.forEach((func) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore: No types available
- self[func] = undefined;
- });
-
- const evalResult = eval(script);
-
- // Remove it from self
- // This is needed so that next eval can have a clean sheet
- Object.keys(GLOBAL_DATA).forEach((key) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore: No types available
- delete self[key];
- });
-
- return evalResult;
- })();
- return { result, triggers };
- } catch (e) {
- this.errors.push({
- type: EvalErrorTypes.EVAL_ERROR,
- message: e.message,
- context: {
- binding: js,
- },
- });
- return { result: undefined, triggers: [] };
- }
- }
-
- evaluateDynamicProperty(
- propertyPath: string,
- currentTree: DataTree,
- unEvalPropertyValue: any,
- ): any {
- return this.getDynamicValue(unEvalPropertyValue, currentTree, false);
- }
-
- validateAndParseWidgetProperty(
- propertyPath: string,
- widget: DataTreeWidget,
- currentTree: DataTree,
- evalPropertyValue: any,
- unEvalPropertyValue: string,
- ): any {
- const entityPropertyName = _.drop(propertyPath.split(".")).join(".");
- let valueToValidate = evalPropertyValue;
- if (isPathADynamicTrigger(widget, propertyPath)) {
- const { triggers } = this.getDynamicValue(
- unEvalPropertyValue,
- currentTree,
- true,
- undefined,
- );
- valueToValidate = triggers;
- }
- const { parsed, isValid, message, transformed } = validateWidgetProperty(
- this.widgetConfigMap,
- widget.type,
- entityPropertyName,
- valueToValidate,
- widget,
- currentTree,
- );
- const evaluatedValue = isValid
- ? parsed
- : _.isUndefined(transformed)
- ? evalPropertyValue
- : transformed;
- const safeEvaluatedValue = removeFunctions(evaluatedValue);
- _.set(widget, `evaluatedValues.${entityPropertyName}`, safeEvaluatedValue);
- if (!isValid) {
- _.set(widget, `invalidProps.${entityPropertyName}`, true);
- _.set(widget, `validationMessages.${entityPropertyName}`, message);
- } else {
- _.set(widget, `invalidProps.${entityPropertyName}`, false);
- _.set(widget, `validationMessages.${entityPropertyName}`, "");
- }
-
- if (isPathADynamicTrigger(widget, entityPropertyName)) {
- return unEvalPropertyValue;
- } else {
- const parsedCache = this.getParsedValueCache(propertyPath);
- if (!equal(parsedCache.value, parsed)) {
- this.parsedValueCache.set(propertyPath, {
- value: parsed,
- version: Date.now(),
- });
- }
- return parsed;
- }
- }
-
- overwriteDefaultDependentProps(
- defaultProperty: string,
- propertyValue: any,
- propertyPath: string,
- entity: DataTreeWidget,
- ) {
- const defaultPropertyCache = this.getParsedValueCache(
- `${entity.widgetName}.${defaultProperty}`,
- );
- const propertyCache = this.getParsedValueCache(propertyPath);
- if (
- propertyValue === undefined ||
- propertyCache.version < defaultPropertyCache.version
- ) {
- return defaultPropertyCache.value;
- }
- return propertyValue;
- }
-
- updateDependencyMap(
- differences: Array>,
- unEvalDataTree: DataTree,
- ): {
- dependenciesOfRemovedPaths: Array;
- removedPaths: Array;
- } {
- const diffCalcStart = performance.now();
- let didUpdateDependencyMap = false;
- const dependenciesOfRemovedPaths: Array = [];
- const removedPaths: Array = [];
-
- // This is needed for NEW and DELETE events below.
- // In worst case, it tends to take ~12.5% of entire diffCalc (8 ms out of 67ms for 132 array of NEW)
- // TODO: Optimise by only getting paths of changed node
- this.allKeys = getAllPaths(unEvalDataTree);
- // Transform the diff library events to Appsmith evaluator events
- differences
- .map(translateDiffEventToDataTreeDiffEvent)
- .forEach((dataTreeDiff) => {
- const entityName = dataTreeDiff.payload.propertyPath.split(".")[0];
- let entity = unEvalDataTree[entityName];
- if (dataTreeDiff.event === DataTreeDiffEvent.DELETE) {
- entity = this.oldUnEvalTree[entityName];
- }
- const entityType = isValidEntity(entity) ? entity.ENTITY_TYPE : "noop";
-
- 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 (
- isWidget(entity) &&
- !this.isDynamicLeaf(
- unEvalDataTree,
- dataTreeDiff.payload.propertyPath,
- )
- ) {
- const widgetDependencyMap: DependencyMap = this.listEntityDependencies(
- entity as DataTreeWidget,
- entityName,
- );
- if (Object.keys(widgetDependencyMap).length) {
- didUpdateDependencyMap = true;
- Object.assign(this.dependencyMap, widgetDependencyMap);
- }
- }
- // Either a new entity or a new property path has been added. Go through existing dynamic bindings and
- // find out if a new dependency has to be created because the property path used in the binding just became
- // eligible
- const possibleReferencesInOldBindings: DependencyMap = this.getPropertyPathReferencesInExistingBindings(
- unEvalDataTree,
- dataTreeDiff.payload.propertyPath,
- );
- // We have found some bindings which are related to the new property path and hence should be added to the
- // global dependency map
- if (Object.keys(possibleReferencesInOldBindings).length) {
- didUpdateDependencyMap = true;
- Object.assign(
- this.dependencyMap,
- possibleReferencesInOldBindings,
- );
- }
- break;
- }
- case DataTreeDiffEvent.DELETE: {
- // Add to removedPaths as they have been deleted from the evalTree
- removedPaths.push(dataTreeDiff.payload.propertyPath);
- // If an existing widget was deleted, remove all the bindings from the global dependency map
- if (
- isWidget(entity) &&
- dataTreeDiff.payload.propertyPath === entityName
- ) {
- const widgetBindings = this.listEntityDependencies(
- entity,
- entityName,
- );
- Object.keys(widgetBindings).forEach((widgetDep) => {
- didUpdateDependencyMap = true;
- delete this.dependencyMap[widgetDep];
- });
- }
- // Either an existing entity or an existing property path has been deleted. Update the global dependency map
- // by removing the bindings from the same.
- Object.keys(this.dependencyMap).forEach((dependencyPath) => {
- didUpdateDependencyMap = true;
- if (
- isChildPropertyPath(
- dataTreeDiff.payload.propertyPath,
- dependencyPath,
- )
- ) {
- delete this.dependencyMap[dependencyPath];
- } else {
- const toRemove: Array = [];
- this.dependencyMap[dependencyPath].forEach(
- (dependantPath) => {
- if (
- isChildPropertyPath(
- dataTreeDiff.payload.propertyPath,
- dependantPath,
- )
- ) {
- dependenciesOfRemovedPaths.push(dependencyPath);
- toRemove.push(dependantPath);
- }
- },
- );
- this.dependencyMap[dependencyPath] = _.difference(
- this.dependencyMap[dependencyPath],
- toRemove,
- );
- }
- });
- break;
- }
-
- 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
- // an evaluation.
- if (
- entityType === ENTITY_TYPE.WIDGET &&
- typeof dataTreeDiff.payload.value === "string"
- ) {
- const entity: DataTreeWidget = unEvalDataTree[
- entityName
- ] as DataTreeWidget;
- const isABindingPath = isPathADynamicBinding(
- entity,
- dataTreeDiff.payload.propertyPath.substring(
- dataTreeDiff.payload.propertyPath.indexOf(".") + 1,
- ),
- );
- if (isABindingPath) {
- didUpdateDependencyMap = true;
-
- const { jsSnippets } = getDynamicBindings(
- dataTreeDiff.payload.value,
- );
- const correctSnippets = jsSnippets.filter(
- (jsSnippet) => !!jsSnippet,
- );
- // 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;
- } 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
- ];
- }
- }
- }
- break;
- }
- default: {
- break;
- }
- }
- }
- });
- const diffCalcEnd = performance.now();
- const subDepCalcStart = performance.now();
- 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 = makeParentsDependOnChildren(this.dependencyMap);
- }
- const subDepCalcEnd = performance.now();
- const updateChangedDependenciesStart = performance.now();
- // If the global dependency map has changed, re-calculate the sort order for all entities and the
- // global inverse dependency map
- if (didUpdateDependencyMap) {
- // This is being called purely to test for new circular dependencies that might have been added
- this.sortedDependencies = this.sortDependencies(this.dependencyMap);
- this.inverseDependencyMap = this.getInverseDependencyTree();
- }
-
- const updateChangedDependenciesStop = performance.now();
- LOGS.push({
- diffCalcDeps: (diffCalcEnd - diffCalcStart).toFixed(2),
- subDepCalc: (subDepCalcEnd - subDepCalcStart).toFixed(2),
- updateChangedDependencies: (
- updateChangedDependenciesStop - updateChangedDependenciesStart
- ).toFixed(2),
- });
-
- return { dependenciesOfRemovedPaths, removedPaths };
- }
-
- calculateSubTreeSortOrder(
- differences: Diff[],
- dependenciesOfRemovedPaths: Array,
- removedPaths: Array,
- unEvalTree: DataTree,
- ) {
- const changePaths: Set = new Set(dependenciesOfRemovedPaths);
- for (const d of differences) {
- if (!Array.isArray(d.path) || d.path.length === 0) continue; // Null check for typescript
- // Apply the changes into the evalTree so that it gets the latest changes
- applyChange(this.evalTree, undefined, d);
-
- changePaths.add(convertPathToString(d.path));
- // If this is a property path change, simply add for evaluation and move on
- if (!this.isDynamicLeaf(unEvalTree, convertPathToString(d.path))) {
- // A parent level property has been added or deleted
- /**
- * We want to add all pre-existing dynamic and static bindings in dynamic paths of this entity to get evaluated and validated.
- * Example:
- * - Table1.tableData = {{Api1.data}}
- * - Api1 gets created.
- * - This function gets called with a diff {path:["Api1"]}
- * We want to add `Api.data` to changedPaths so that `Table1.tableData` can be discovered below.
- */
- const entityName = d.path[0];
- const entity = unEvalTree[entityName];
- if (!entity) {
- continue;
- }
- if (!isAction(entity) && !isWidget(entity)) {
- continue;
- }
- if (isAction(entity)) {
- // TODO create proper binding paths for actions
- changePaths.add(convertPathToString(d.path));
- continue;
- }
- const parentPropertyPath = convertPathToString(d.path);
- Object.keys(entity.bindingPaths).forEach((relativePath) => {
- const childPropertyPath = `${entityName}.${relativePath}`;
- if (isChildPropertyPath(parentPropertyPath, childPropertyPath)) {
- changePaths.add(childPropertyPath);
- }
- });
- }
- }
-
- // If a nested property path has changed and someone (say x) is dependent on the parent of the said property,
- // x must also be evaluated. For example, the following relationship exists in dependency map:
- // < "Input1.defaultText" : ["Table1.selectedRow.email"] >
- // If Table1.selectedRow has changed, then Input1.defaultText must also be evaluated because Table1.selectedRow.email
- // is a nested property of Table1.selectedRow
- const changePathsWithNestedDependants = addDependantsOfNestedPropertyPaths(
- Array.from(changePaths),
- this.inverseDependencyMap,
- );
-
- const trimmedChangedPaths = trimDependantChangePaths(
- changePathsWithNestedDependants,
- this.dependencyMap,
- );
-
- // Now that we have all the root nodes which have to be evaluated, recursively find all the other paths which
- // would get impacted because they are dependent on the said root nodes and add them in order
- const completeSortOrder = this.getCompleteSortOrder(
- trimmedChangedPaths,
- this.inverseDependencyMap,
- );
- // Remove any paths that do no exist in the data tree any more
- return _.difference(completeSortOrder, removedPaths);
- }
-
- getInverseDependencyTree(): DependencyMap {
- const inverseDag: DependencyMap = {};
- this.sortedDependencies.forEach((propertyPath) => {
- const incomingEdges: Array = this.dependencyMap[propertyPath];
- if (incomingEdges) {
- incomingEdges.forEach((edge) => {
- const node = inverseDag[edge];
- if (node) {
- node.push(propertyPath);
- } else {
- inverseDag[edge] = [propertyPath];
- }
- });
- }
- });
- return inverseDag;
- }
-
- // TODO: create the lookup dictionary once
- // Response from listEntityDependencies only needs to change if the entity itself changed.
- // Check if it is possible to make a flat structure with O(1) or at least O(m) lookup instead of O(n*m)
- getPropertyPathReferencesInExistingBindings(
- dataTree: DataTree,
- propertyPath: string,
- ) {
- const possibleRefs: DependencyMap = {};
- Object.keys(dataTree).forEach((entityName) => {
- const entity = dataTree[entityName];
- if (
- isValidEntity(entity) &&
- (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION ||
- entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET)
- ) {
- const entityPropertyBindings = this.listEntityDependencies(
- entity,
- entityName,
- );
- Object.keys(entityPropertyBindings).forEach((path) => {
- const propertyBindings = entityPropertyBindings[path];
- const references = _.flatten(
- propertyBindings.map((binding) =>
- extractReferencesFromBinding(binding, this.allKeys),
- ),
- );
- references.forEach((value) => {
- if (isChildPropertyPath(propertyPath, value)) {
- possibleRefs[path] = propertyBindings;
- }
- });
- });
- }
- });
- return possibleRefs;
- }
-
- evaluateActionBindings(
- bindings: string[],
- executionParams?: Record | string,
- ) {
- // We might get execution params as an object or as a string.
- // If the user has added a proper object (valid case) it will be an object
- // If they have not added any execution params or not an object
- // it would be a string (invalid case)
- let evaluatedExecutionParams: Record = {};
- if (executionParams && _.isObject(executionParams)) {
- evaluatedExecutionParams = this.getDynamicValue(
- `{{${JSON.stringify(executionParams)}}}`,
- this.evalTree,
- false,
- );
- }
-
- // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility)
- const bindingsForExecutionParams: string[] = bindings.map(
- (binding: string) =>
- binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY),
- );
-
- const dataTreeWithExecutionParams = Object.assign({}, this.evalTree, {
- [EXECUTION_PARAM_KEY]: evaluatedExecutionParams,
- });
-
- return bindingsForExecutionParams.map((binding) =>
- this.getDynamicValue(
- `{{${binding}}}`,
- dataTreeWithExecutionParams,
- false,
- ),
- );
- }
-
- clearErrors() {
- this.errors = [];
- }
-}
-
-const extractReferencesFromBinding = (
- path: string,
- all: Record,
-): Array => {
- const subDeps: Array = [];
- const identifiers = path.match(/[a-zA-Z_$][a-zA-Z_$0-9.\[\]]*/g) || [path];
- identifiers.forEach((identifier: string) => {
- // If the identifier exists directly, add it and return
- if (all.hasOwnProperty(identifier)) {
- subDeps.push(identifier);
- return;
- }
- const subpaths = _.toPath(identifier);
- let current = "";
- // We want to keep going till we reach top level, but not add top level
- // Eg: Input1.text should not depend on entire Table1 unless it explicitly asked for that.
- // This is mainly to avoid a lot of unnecessary evals, if we feel this is wrong
- // we can remove the length requirement and it will still work
- while (subpaths.length > 1) {
- current = convertPathToString(subpaths);
- // We've found the dep, add it and return
- if (all.hasOwnProperty(current)) {
- subDeps.push(current);
- return;
- }
- subpaths.pop();
- }
- });
- return _.uniq(subDeps);
-};
-
-// TODO cryptic comment below. Dont know if we still need this. Duplicate function
-// 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({
- // type: EvalErrorTypes.BAD_UNEVAL_TREE_ERROR,
- // message: "Data tree entity is not an object",
- // context: entity,
- // });
- return false;
- }
- return "ENTITY_TYPE" in entity;
-}
-
-function isWidget(entity: DataTreeEntity): entity is DataTreeWidget {
- return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET;
-}
-
-function isAction(entity: DataTreeEntity): entity is DataTreeAction {
- return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.ACTION;
-}
-
-const addFunctions = (dataTree: Readonly): DataTree => {
- const withFunction: DataTree = _.cloneDeep(dataTree);
- withFunction.actionPaths = [];
- Object.keys(withFunction).forEach((entityName) => {
- const entity = withFunction[entityName];
- if (isAction(entity)) {
- const runFunction = function(
- this: DataTreeAction,
- onSuccess: string,
- onError: string,
- params = "",
- ) {
- return {
- type: "RUN_ACTION",
- payload: {
- actionId: this.actionId,
- onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "",
- onError: onError ? `{{${onError.toString()}}}` : "",
- params,
- },
- };
- };
- _.set(withFunction, `${entityName}.run`, runFunction);
- withFunction.actionPaths &&
- withFunction.actionPaths.push(`${entityName}.run`);
- }
- });
- withFunction.navigateTo = function(
- pageNameOrUrl: string,
- params: Record,
- target?: string,
- ) {
- return {
- type: "NAVIGATE_TO",
- payload: { pageNameOrUrl, params, target },
- };
- };
- withFunction.actionPaths.push("navigateTo");
-
- withFunction.showAlert = function(message: string, style: string) {
- return {
- type: "SHOW_ALERT",
- payload: { message, style },
- };
- };
- withFunction.actionPaths.push("showAlert");
-
- withFunction.showModal = function(modalName: string) {
- return {
- type: "SHOW_MODAL_BY_NAME",
- payload: { modalName },
- };
- };
- withFunction.actionPaths.push("showModal");
-
- withFunction.closeModal = function(modalName: string) {
- return {
- type: "CLOSE_MODAL",
- payload: { modalName },
- };
- };
- withFunction.actionPaths.push("closeModal");
-
- withFunction.storeValue = function(key: string, value: string) {
- return {
- type: "STORE_VALUE",
- payload: { key, value },
- };
- };
- withFunction.actionPaths.push("storeValue");
-
- withFunction.download = function(data: string, name: string, type: string) {
- return {
- type: "DOWNLOAD",
- payload: { data, name, type },
- };
- };
- withFunction.actionPaths.push("download");
-
- withFunction.copyToClipboard = function(
- data: string,
- options?: { debug?: boolean; format?: string },
- ) {
- return {
- type: "COPY_TO_CLIPBOARD",
- payload: {
- data,
- options: { debug: options?.debug, format: options?.format },
- },
- };
- };
- withFunction.actionPaths.push("copyToClipboard");
-
- return withFunction;
-};
diff --git a/app/client/src/workers/evaluationUtils.ts b/app/client/src/workers/evaluationUtils.ts
index e1f3000244..367fdec08c 100644
--- a/app/client/src/workers/evaluationUtils.ts
+++ b/app/client/src/workers/evaluationUtils.ts
@@ -2,10 +2,10 @@ import {
DependencyMap,
isChildPropertyPath,
isDynamicValue,
-} from "../utils/DynamicBindingUtils";
-import { WidgetType } from "../constants/WidgetConstants";
-import { WidgetProps } from "../widgets/BaseWidget";
-import { WidgetTypeConfigMap } from "../utils/WidgetFactory";
+} from "utils/DynamicBindingUtils";
+import { WidgetType } from "constants/WidgetConstants";
+import { WidgetProps } from "widgets/BaseWidget";
+import { WidgetTypeConfigMap } from "utils/WidgetFactory";
import { VALIDATORS } from "./validations";
import { Diff } from "deep-diff";
import {
@@ -14,7 +14,7 @@ import {
DataTreeEntity,
DataTreeWidget,
ENTITY_TYPE,
-} from "../entities/DataTree/dataTreeFactory";
+} from "entities/DataTree/dataTreeFactory";
import _ from "lodash";
// Dropdown1.options[1].value -> Dropdown1.options[1]
@@ -368,3 +368,99 @@ export const trimDependantChangePaths = (
}
return trimmedPaths;
};
+
+export const addFunctions = (dataTree: Readonly): DataTree => {
+ const withFunction: DataTree = _.cloneDeep(dataTree);
+ withFunction.actionPaths = [];
+ Object.keys(withFunction).forEach((entityName) => {
+ const entity = withFunction[entityName];
+ if (isAction(entity)) {
+ const runFunction = function(
+ this: DataTreeAction,
+ onSuccess: string,
+ onError: string,
+ params = "",
+ ) {
+ return {
+ type: "RUN_ACTION",
+ payload: {
+ actionId: this.actionId,
+ onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "",
+ onError: onError ? `{{${onError.toString()}}}` : "",
+ params,
+ },
+ };
+ };
+ _.set(withFunction, `${entityName}.run`, runFunction);
+ withFunction.actionPaths &&
+ withFunction.actionPaths.push(`${entityName}.run`);
+ }
+ });
+ withFunction.navigateTo = function(
+ pageNameOrUrl: string,
+ params: Record,
+ target?: string,
+ ) {
+ return {
+ type: "NAVIGATE_TO",
+ payload: { pageNameOrUrl, params, target },
+ };
+ };
+ withFunction.actionPaths.push("navigateTo");
+
+ withFunction.showAlert = function(message: string, style: string) {
+ return {
+ type: "SHOW_ALERT",
+ payload: { message, style },
+ };
+ };
+ withFunction.actionPaths.push("showAlert");
+
+ withFunction.showModal = function(modalName: string) {
+ return {
+ type: "SHOW_MODAL_BY_NAME",
+ payload: { modalName },
+ };
+ };
+ withFunction.actionPaths.push("showModal");
+
+ withFunction.closeModal = function(modalName: string) {
+ return {
+ type: "CLOSE_MODAL",
+ payload: { modalName },
+ };
+ };
+ withFunction.actionPaths.push("closeModal");
+
+ withFunction.storeValue = function(key: string, value: string) {
+ return {
+ type: "STORE_VALUE",
+ payload: { key, value },
+ };
+ };
+ withFunction.actionPaths.push("storeValue");
+
+ withFunction.download = function(data: string, name: string, type: string) {
+ return {
+ type: "DOWNLOAD",
+ payload: { data, name, type },
+ };
+ };
+ withFunction.actionPaths.push("download");
+
+ withFunction.copyToClipboard = function(
+ data: string,
+ options?: { debug?: boolean; format?: string },
+ ) {
+ return {
+ type: "COPY_TO_CLIPBOARD",
+ payload: {
+ data,
+ options: { debug: options?.debug, format: options?.format },
+ },
+ };
+ };
+ withFunction.actionPaths.push("copyToClipboard");
+
+ return withFunction;
+};
diff --git a/app/client/yarn.lock b/app/client/yarn.lock
index 814b8e518d..805e3ddb9c 100644
--- a/app/client/yarn.lock
+++ b/app/client/yarn.lock
@@ -6030,9 +6030,10 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
-codemirror@^5.55.0:
- version "5.58.1"
- resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.58.1.tgz#ec6bf38ad2a17f74c61bd00cc6dc5a69bd167854"
+codemirror@^5.59.2:
+ version "5.59.2"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.59.2.tgz#ee674d3a4a8d241af38d52afc482625ba7393922"
+ integrity sha512-/D5PcsKyzthtSy2NNKCyJi3b+htRkoKv3idswR/tR6UAvMNKA7SrmyZy6fOONJxSRs1JlUWEDAbxqfdArbK8iA==
collapse-white-space@^1.0.2:
version "1.0.6"
diff --git a/app/server/appsmith-interfaces/pom.xml b/app/server/appsmith-interfaces/pom.xml
index 5b8464b63c..cba3efd203 100644
--- a/app/server/appsmith-interfaces/pom.xml
+++ b/app/server/appsmith-interfaces/pom.xml
@@ -92,6 +92,29 @@
querydsl-jpa4.2.2
+
+ org.apache.commons
+ commons-text
+ 1.8
+ compile
+
+
+ commons-validator
+ commons-validator
+ 1.7
+ compile
+
+
+ commons-io
+ commons-io
+ 2.6
+ compile
+
+
+ org.assertj
+ assertj-core
+ test
+
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java
new file mode 100644
index 0000000000..7a813bbc8e
--- /dev/null
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/DataType.java
@@ -0,0 +1,15 @@
+package com.appsmith.external.constants;
+
+public enum DataType {
+ INTEGER,
+ LONG,
+ FLOAT,
+ DOUBLE,
+ BOOLEAN,
+ DATE,
+ TIME,
+ ASCII,
+ BINARY,
+ BYTES,
+ STRING
+}
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java
new file mode 100644
index 0000000000..6565a8109c
--- /dev/null
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java
@@ -0,0 +1,21 @@
+package com.appsmith.external.dtos;
+
+import com.appsmith.external.models.PaginationField;
+import com.appsmith.external.models.Param;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+public class ExecuteActionDTO {
+
+ String actionId;
+
+ List params;
+
+ PaginationField paginationField;
+
+ Boolean viewMode = false;
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/BeanCopyUtils.java
similarity index 98%
rename from app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java
rename to app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/BeanCopyUtils.java
index 7ca08a737c..6ba6f56949 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/BeanCopyUtils.java
@@ -1,4 +1,4 @@
-package com.appsmith.server.helpers;
+package com.appsmith.external.helpers;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java
similarity index 92%
rename from app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java
rename to app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java
index a68bd66e68..136f7b4733 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java
@@ -1,4 +1,4 @@
-package com.appsmith.server.helpers;
+package com.appsmith.external.helpers;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.StringEscapeUtils;
@@ -18,7 +18,7 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import static com.appsmith.server.helpers.BeanCopyUtils.isDomainModel;
+import static com.appsmith.external.helpers.BeanCopyUtils.isDomainModel;
@Slf4j
public class MustacheHelper {
@@ -161,6 +161,22 @@ public class MustacheHelper {
return keys;
}
+ // For prepared statements we should extract the bindings in order in a list and include duplicate bindings as well.
+ public static List extractMustacheKeysInOrder(String template) {
+ List keys = new ArrayList<>();
+
+ for (String token : tokenize(template)) {
+ if (token.startsWith("{{") && token.endsWith("}}")) {
+ // Allowing empty tokens to be added, to be compatible with the previous `extractMustacheKeys` method.
+ // Calling `.trim()` before adding because Mustache compiler strips keys in the template before looking
+ // up a value. Addresses https://www.notion.so/appsmith/Bindings-with-a-space-at-the-start-fail-to-execute-properly-in-the-API-pane-2eb65d5c6064466b9ef059fa01ef3261
+ keys.add(token.substring(2, token.length() - 2).trim());
+ }
+ }
+
+ return keys;
+ }
+
public static Set extractMustacheKeysFromFields(Object object) {
final Set keys = new HashSet<>();
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java
new file mode 100644
index 0000000000..9ebd467909
--- /dev/null
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SqlStringUtils.java
@@ -0,0 +1,235 @@
+package com.appsmith.external.helpers;
+
+import com.appsmith.external.constants.DataType;
+import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
+import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
+import com.appsmith.external.models.ActionConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.validator.routines.DateValidator;
+
+import java.io.UnsupportedEncodingException;
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class SqlStringUtils {
+
+ /**
+ * SQL query : The regex pattern below looks for '?' or "?". This pattern is later replaced with ?
+ * to fit the requirements of prepared statements.
+ */
+ private static String regexQuotesTrimming = "([\"']\\?[\"'])";
+ // The final replacement string of ? for replacing '?' or "?"
+ private static String postQuoteTrimmingQuestionMark = "\\?";
+
+ private static Pattern quoteQuestionPattern = Pattern.compile(regexQuotesTrimming);
+
+ public static class DateValidatorUsingDateFormat extends DateValidator {
+ private String dateFormat;
+
+ public DateValidatorUsingDateFormat(String dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public boolean isValid(String dateStr) {
+ DateFormat sdf = new SimpleDateFormat(this.dateFormat);
+ sdf.setLenient(false);
+ try {
+ sdf.parse(dateStr);
+ } catch (ParseException e) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ public static DataType stringToKnownDataTypeConverter(String input) {
+
+ try {
+ Integer.parseInt(input);
+ return DataType.INTEGER;
+ } catch (NumberFormatException e) {
+ // Not an integer
+ }
+
+ try {
+ Long.parseLong(input);
+ return DataType.LONG;
+ } catch (NumberFormatException e1) {
+ // Not long
+ }
+
+ try {
+ Float.parseFloat(input);
+ return DataType.FLOAT;
+ } catch (NumberFormatException e2) {
+ // Not float
+ }
+
+ try {
+ Double.parseDouble(input);
+ return DataType.DOUBLE;
+ } catch (NumberFormatException e3) {
+ // Not double
+ }
+
+ // Creating a copy of the input in lower case form to do simple string equality to check for boolean/null types.
+ String copyInput = String.valueOf(input).toLowerCase().trim();
+ if (copyInput.equals("true") || copyInput.equals("false")) {
+ return DataType.BOOLEAN;
+ }
+
+ if (copyInput.equals("null")) {
+ return null;
+ }
+
+ DateValidator dateValidator = new DateValidatorUsingDateFormat("yyyy-mm-dd");
+ if (dateValidator.isValid(input)) {
+ return DataType.DATE;
+ }
+
+ DateValidator dateTimeValidator = new DateValidatorUsingDateFormat("yyyy-mm-dd hh:mm:ss");
+ if (dateTimeValidator.isValid(input)) {
+ return DataType.DATE;
+ }
+
+ DateValidator timeValidator = new DateValidatorUsingDateFormat("hh:mm:ss");
+ if (timeValidator.isValid(input)) {
+ return DataType.TIME;
+ }
+
+ /**
+ * TODO : Timestamp, ASCII, Binary and Bytes Array
+ */
+
+// // Check if unicode stream also gets handled as part of this since the destination SQL type is the same.
+// if(StandardCharsets.US_ASCII.newEncoder().canEncode(input)) {
+// return Ascii.class;
+// }
+// if (isBinary(input)) {
+// return Binary.class;
+// }
+
+// try
+// {
+// input.getBytes("UTF-8");
+// return Byte.class;
+// } catch (UnsupportedEncodingException e) {
+// // Not byte
+// }
+
+
+ // default return type if none of the above matches.
+ return DataType.STRING;
+ }
+
+ public static PreparedStatement setValueInPreparedStatement(int index, String binding, String value, PreparedStatement preparedStatement) throws UnsupportedEncodingException, AppsmithPluginException {
+ DataType valueType = SqlStringUtils.stringToKnownDataTypeConverter(value);
+
+ /**
+ * TODO : Parse the column name for which the value is null and if the column name exists in the
+ * database structure, find the column field type and use PreparedStatement.setNull() function.
+ */
+ // If the value being set is null, return without setting.
+ if (valueType == null) {
+ return preparedStatement;
+ }
+
+ try {
+ switch (valueType) {
+ case BINARY: {
+ preparedStatement.setBinaryStream(index, IOUtils.toInputStream(value));
+ break;
+ }
+ case BYTES: {
+ preparedStatement.setBytes(index, value.getBytes("UTF-8"));
+ break;
+ }
+ case INTEGER: {
+ preparedStatement.setInt(index, Integer.parseInt(value));
+ break;
+ }
+ case LONG: {
+ preparedStatement.setLong(index, Long.parseLong(value));
+ break;
+ }
+ case FLOAT: {
+ preparedStatement.setFloat(index, Float.parseFloat(value));
+ break;
+ }
+ case DOUBLE: {
+ preparedStatement.setDouble(index, Double.parseDouble(value));
+ break;
+ }
+ case BOOLEAN: {
+ preparedStatement.setBoolean(index, Boolean.parseBoolean(value));
+ break;
+ }
+ case DATE: {
+ preparedStatement.setDate(index, Date.valueOf(value));
+ break;
+ }
+ case TIME: {
+ preparedStatement.setTime(index, Time.valueOf(value));
+ break;
+ }
+ case STRING: {
+ preparedStatement.setString(index, value);
+ break;
+ }
+ default:
+ break;
+ }
+
+ } catch (SQLException | IllegalArgumentException e) {
+ String message = "Query preparation failed while inserting value: "
+ + value + " for binding: {{" + binding + "}}. Please check the query again.\nError: " + e.getMessage();
+ throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, message);
+ }
+
+ return preparedStatement;
+ }
+
+ private static boolean isBinary(String input) {
+ for (int i = 0; i < input.length(); i++) {
+ int tempB = input.charAt(i);
+ if (tempB == '0' || tempB == '1') {
+ continue;
+ }
+ return false;
+ }
+ // no failures, so
+ return true;
+ }
+
+ public static String replaceMustacheWithQuestionMark(String query, List mustacheBindings) {
+
+ ActionConfiguration actionConfiguration = new ActionConfiguration();
+ actionConfiguration.setBody(query);
+
+ Map replaceParamsMap = mustacheBindings
+ .stream()
+ .collect(Collectors.toMap(Function.identity(), v -> "?"));
+
+ ActionConfiguration updatedActionConfiguration = MustacheHelper.renderFieldValues(actionConfiguration, replaceParamsMap);
+
+ String body = updatedActionConfiguration.getBody();
+
+ // Trim the quotes around ? if present
+ body = quoteQuestionPattern.matcher(body).replaceAll(postQuoteTrimmingQuestionMark);
+
+ return body;
+ }
+}
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java
index cc2b828b88..730a293bd6 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionConfiguration.java
@@ -47,7 +47,6 @@ public class ActionConfiguration {
// DB action fields
// JS action fields
-
String jsFunction;
/*
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java
index 6e0f4bdbf8..ea5024c003 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java
@@ -1,20 +1,27 @@
package com.appsmith.external.plugins;
+import com.appsmith.external.dtos.ExecuteActionDTO;
+import com.appsmith.external.helpers.MustacheHelper;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
+import com.appsmith.external.models.Param;
import org.pf4j.ExtensionPoint;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;
+import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
public interface PluginExecutor extends ExtensionPoint {
/**
- * This function is used to execute the action.
+ * This function is implemented by the plugins by default to execute the action.
+ *
+ * If executeParametrized has a custom implementation by a plugin, this function would not be used.
*
* @param connection : This is the connection that is established to the data source. This connection is according
* to the parameters in Datasource Configuration
@@ -56,11 +63,11 @@ public interface PluginExecutor extends ExtensionPoint {
* This function checks if the datasource is valid. It should only check if all the mandatory fields are filled and
* if the values are of the right format. It does NOT check the validity of those fields.
* Please use {@link #testDatasource(DatasourceConfiguration)} to establish the correctness of those fields.
- *
+ *
* If the datasource configuration is valid, it should return an empty set of invalid strings.
* If not, it should return the list of invalid messages as a set.
*
- * @param datasourceConfiguration : The datasource to be validated
+ * @param datasourceConfiguration : The datasource to be validated
* @return Set : The set of invalid strings informing the user of all the invalid fields
*/
Set validateDatasource(DatasourceConfiguration datasourceConfiguration);
@@ -85,4 +92,72 @@ public interface PluginExecutor extends ExtensionPoint {
default Mono getStructure(C connection, DatasourceConfiguration datasourceConfiguration) {
return Mono.empty();
}
+
+ /**
+ * Appsmith Server calls this function for execution of the action.
+ * Default implementation which takes the variables that need to be substituted and then calls the plugin execute function
+ *
+ * Plugins requiring their custom implementation of variable substitution should override this function and then are
+ * responsible both for variable substitution and final execution.
+ *
+ * @param connection : This is the connection that is established to the data source. This connection is according
+ * to the parameters in Datasource Configuration
+ * @param executeActionDTO : This is the data structure sent by the client during execute. This contains the params
+ * which would be used for substitution
+ * @param datasourceConfiguration : These are the configurations which have been used to create a Datasource from a Plugin
+ * @param actionConfiguration : These are the configurations which have been used to create an Action from a Datasource.
+ * @return ActionExecutionResult : This object is returned to the user which contains the result values from the execution.
+ */
+ default Mono executeParameterized(C connection,
+ ExecuteActionDTO executeActionDTO,
+ DatasourceConfiguration datasourceConfiguration,
+ ActionConfiguration actionConfiguration) {
+ prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration);
+ return this.execute(connection, datasourceConfiguration, actionConfiguration);
+ }
+
+ /**
+ * This function is responsible for preparing the action and datasource configurations to be ready for execution.
+ *
+ * @param executeActionDTO
+ * @param actionConfiguration
+ * @param datasourceConfiguration
+ */
+ default void prepareConfigurationsForExecution(ExecuteActionDTO executeActionDTO,
+ ActionConfiguration actionConfiguration,
+ DatasourceConfiguration datasourceConfiguration) {
+
+ variableSubstitution(actionConfiguration, datasourceConfiguration, executeActionDTO);
+
+ return;
+ }
+
+ /**
+ * This function replaces the variables in the action and datasource configuration with the actual params
+ */
+ default void variableSubstitution(ActionConfiguration actionConfiguration,
+ DatasourceConfiguration datasourceConfiguration,
+ ExecuteActionDTO executeActionDTO) {
+ //Do variable substitution
+ //Do this only if params have been provided in the execute command
+ if (executeActionDTO.getParams() != null && !executeActionDTO.getParams().isEmpty()) {
+ Map replaceParamsMap = executeActionDTO
+ .getParams()
+ .stream()
+ .collect(Collectors.toMap(
+ // Trimming here for good measure. If the keys have space on either side,
+ // Mustache won't be able to find the key.
+ // We also add a backslash before every double-quote or backslash character
+ // because we apply the template replacing in a JSON-stringified version of
+ // these properties, where these two characters are escaped.
+ p -> p.getKey().trim(), // .replaceAll("[\"\n\\\\]", "\\\\$0"),
+ Param::getValue,
+ // In case of a conflict, we pick the older value
+ (oldValue, newValue) -> oldValue)
+ );
+
+ MustacheHelper.renderFieldValues(datasourceConfiguration, replaceParamsMap);
+ MustacheHelper.renderFieldValues(actionConfiguration, replaceParamsMap);
+ }
+ }
}
diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/BeanCopyUtilsTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/BeanCopyUtilsTest.java
similarity index 98%
rename from app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/BeanCopyUtilsTest.java
rename to app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/BeanCopyUtilsTest.java
index 9d7e518606..fdc8ea5a64 100644
--- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/BeanCopyUtilsTest.java
+++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/BeanCopyUtilsTest.java
@@ -1,4 +1,4 @@
-package com.appsmith.server.helpers;
+package com.appsmith.external.helpers;
import lombok.AllArgsConstructor;
import lombok.Getter;
diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MustacheHelperTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java
similarity index 98%
rename from app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MustacheHelperTest.java
rename to app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java
index d9b550a883..536306b6ad 100644
--- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MustacheHelperTest.java
+++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java
@@ -1,4 +1,4 @@
-package com.appsmith.server.helpers;
+package com.appsmith.external.helpers;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.Connection;
@@ -14,11 +14,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
-import static com.appsmith.server.helpers.MustacheHelper.extractMustacheKeys;
-import static com.appsmith.server.helpers.MustacheHelper.extractMustacheKeysFromFields;
-import static com.appsmith.server.helpers.MustacheHelper.render;
-import static com.appsmith.server.helpers.MustacheHelper.renderFieldValues;
-import static com.appsmith.server.helpers.MustacheHelper.tokenize;
+import static com.appsmith.external.helpers.MustacheHelper.extractMustacheKeys;
+import static com.appsmith.external.helpers.MustacheHelper.extractMustacheKeysFromFields;
+import static com.appsmith.external.helpers.MustacheHelper.render;
+import static com.appsmith.external.helpers.MustacheHelper.renderFieldValues;
+import static com.appsmith.external.helpers.MustacheHelper.tokenize;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings(
diff --git a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java
index f11370377d..e737561449 100644
--- a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java
+++ b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java
@@ -15,6 +15,9 @@ import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.TransferManagerBuilder;
import com.amazonaws.util.IOUtils;
+import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
+import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
+import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.DBAuth;
@@ -22,9 +25,6 @@ import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Property;
-import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
-import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
-import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import lombok.extern.slf4j.Slf4j;
@@ -32,20 +32,20 @@ import org.apache.commons.lang.StringUtils;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
import org.springframework.util.CollectionUtils;
-import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
-import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.net.URL;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
+import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -65,6 +65,7 @@ public class AmazonS3Plugin extends BasePlugin {
private static final int USING_FILEPICKER_FOR_UPLOAD_PROPERTY_INDEX = 6;
private static final int URL_EXPIRY_DURATION_FOR_UPLOAD_PROPERTY_INDEX = 7;
private static final int CLIENT_REGION_PROPERTY_INDEX = 0;
+ private static final int DEFAULT_URL_EXPIRY_IN_MINUTES = 5; // max 7 days is possible
private static final String YES = "YES";
private static final String BASE64_DELIMITER = ";base64,";
@@ -81,11 +82,11 @@ public class AmazonS3Plugin extends BasePlugin {
* - Exception thrown by this method is expected to be handled by the caller.
*/
ArrayList getFilenamesFromObjectListing(ObjectListing objectListing) throws AppsmithPluginException {
- if(objectListing == null) {
+ if (objectListing == null) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Appsmith server has encountered an unexpected error when fetching file " +
- "content from AWS S3 server. Please reach out to Appsmith customer support to resolve this"
+ "content from AWS S3 server. Please reach out to Appsmith customer support to resolve this"
);
}
@@ -104,15 +105,15 @@ public class AmazonS3Plugin extends BasePlugin {
ArrayList listAllFilesInBucket(AmazonS3 connection,
String bucketName,
String prefix) throws AppsmithPluginException {
- if(connection == null) {
+ if (connection == null) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Appsmith server has encountered an unexpected error when establishing " +
- "connection with AWS S3 server. Please reach out to Appsmith customer support to resolve this."
+ "connection with AWS S3 server. Please reach out to Appsmith customer support to resolve this."
);
}
- if(bucketName == null) {
+ if (bucketName == null) {
/*
* - bucketName is NOT expected to be null at this program point. A null check has been added in the
* execute function already.
@@ -120,11 +121,11 @@ public class AmazonS3Plugin extends BasePlugin {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Appsmith has encountered an unexpected error when getting bucket name. Please reach out to " +
- "Appsmith customer support to resolve this."
+ "Appsmith customer support to resolve this."
);
}
- if(prefix == null) {
+ if (prefix == null) {
/*
* - prefix is NOT expected to be null at this program point. A null check has been added in the
* execute function already.
@@ -132,15 +133,14 @@ public class AmazonS3Plugin extends BasePlugin {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Appsmith has encountered an unexpected error when getting path prefix. Please reach out to " +
- "Appsmith customer support to resolve this."
+ "Appsmith customer support to resolve this."
);
}
- ArrayList fileList = new ArrayList<>();
ObjectListing result = connection.listObjects(bucketName, prefix);
- fileList.addAll(getFilenamesFromObjectListing(result));
+ ArrayList fileList = new ArrayList<>(getFilenamesFromObjectListing(result));
- while(result.isTruncated()) {
+ while (result.isTruncated()) {
result = connection.listNextBatchOfObjects(result);
fileList.addAll(getFilenamesFromObjectListing(result));
}
@@ -151,18 +151,15 @@ public class AmazonS3Plugin extends BasePlugin {
ArrayList getSignedUrls(AmazonS3 connection,
String bucketName,
ArrayList listOfFiles,
- int durationInMilliseconds) {
- Date expiration = new java.util.Date();
- long expTimeMillis = expiration.getTime();
- expTimeMillis += durationInMilliseconds;
- expiration.setTime(expTimeMillis);
-
+ Date expiryDateTime) {
ArrayList urlList = new ArrayList<>();
- for(String filePath : listOfFiles) {
+
+ for (String filePath : listOfFiles) {
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName,
- filePath)
- .withMethod(HttpMethod.GET)
- .withExpiration(expiration);
+ filePath)
+ .withMethod(HttpMethod.GET)
+ .withExpiration(expiryDateTime);
+
URL url = connection.generatePresignedUrl(generatePresignedUrlRequest);
urlList.add(url.toString());
}
@@ -175,32 +172,24 @@ public class AmazonS3Plugin extends BasePlugin {
* - Returns signed url of the created file on success.
*/
String uploadFileFromBody(AmazonS3 connection,
- String bucketName,
- String path,
- String body,
- Boolean usingFilePicker,
- int durationInMillis)
+ String bucketName,
+ String path,
+ String body,
+ Boolean usingFilePicker,
+ Date expiryDateTime)
throws InterruptedException, AppsmithPluginException {
- byte[] payload = null;
- if(Boolean.TRUE.equals(usingFilePicker)) {
+ byte[] payload;
+ if (Boolean.TRUE.equals(usingFilePicker)) {
String encodedPayload = body;
/*
* - For files uploaded using Filepicker.xyz.base64, body format is ";base64,".
* - Strip off the redundant part in the beginning to get actual payload.
*/
- if(body.contains(BASE64_DELIMITER)) {
+ if (body.contains(BASE64_DELIMITER)) {
List bodyArrayList = Arrays.asList(body.split(BASE64_DELIMITER));
- encodedPayload = bodyArrayList.get(bodyArrayList.size()-1);
- }
- else {
- throw new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Missing Base64 encoding. When uploading file from a Filepicker widget its Base64 encoded" +
- " value must be used - e.g. Filepicker1.files[0].base64. Did you forget to use the Base64" +
- " encoded value ?"
- );
+ encodedPayload = bodyArrayList.get(bodyArrayList.size() - 1);
}
try {
@@ -208,12 +197,11 @@ public class AmazonS3Plugin extends BasePlugin {
} catch (IllegalArgumentException e) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
- "Appsmith has encountered an unexpected error when decoding base64 encoded content. " +
- "Please reach out to Appsmith customer support to resolve this."
+ "File content is not base64 encoded. File content needs to be base64 encoded when the " +
+ "'File Data Type: Base64/Text' field is selected 'Yes'."
);
}
- }
- else {
+ } else {
payload = body.getBytes();
}
@@ -223,12 +211,12 @@ public class AmazonS3Plugin extends BasePlugin {
ArrayList listOfFiles = new ArrayList<>();
listOfFiles.add(path);
- ArrayList listOfUrls = getSignedUrls(connection, bucketName, listOfFiles, durationInMillis);
- if(listOfUrls.size() != 1) {
+ ArrayList listOfUrls = getSignedUrls(connection, bucketName, listOfFiles, expiryDateTime);
+ if (listOfUrls.size() != 1) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Appsmith has encountered an unexpected error when fetching url from AmazonS3 after file " +
- "creation. Please reach out to Appsmith customer support to resolve this."
+ "creation. Please reach out to Appsmith customer support to resolve this."
);
}
String signedUrl = listOfUrls.get(0);
@@ -244,11 +232,10 @@ public class AmazonS3Plugin extends BasePlugin {
S3ObjectInputStream content = fullObject.getObjectContent();
byte[] bytes = IOUtils.toByteArray(content);
- String result = null;
- if(Boolean.TRUE.equals(encodeContent)) {
+ String result;
+ if (Boolean.TRUE.equals(encodeContent)) {
result = new String(Base64.getEncoder().encode(bytes));
- }
- else {
+ } else {
result = new String(bytes);
}
@@ -264,93 +251,84 @@ public class AmazonS3Plugin extends BasePlugin {
* Hence, unable to do stale connection check explicitly.
* - If connection object is null, then assume stale connection.
*/
- if(connection == null) {
+ if (connection == null) {
return Mono.error(new StaleConnectionException());
}
- if(datasourceConfiguration == null) {
+ if (datasourceConfiguration == null) {
return Mono.error(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"At least one of the mandatory fields in S3 datasource creation form is empty - " +
- "'Access Key'/'Secret Key'/'Region'. Please fill all the mandatory fields and try again."
+ "'Access Key'/'Secret Key'/'Region'. Please fill all the mandatory fields and try again."
)
);
}
- if(actionConfiguration == null) {
+ if (actionConfiguration == null) {
return Mono.error(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"At least one of the mandatory fields in S3 query creation form is empty - 'Action'/" +
- "'Bucket Name'/'File Path'/'Content'. Please fill all the mandatory fields and try " +
- "again."
+ "'Bucket Name'/'File Path'/'Content'. Please fill all the mandatory fields and try " +
+ "again."
)
);
}
final String path = actionConfiguration.getPath();
final List properties = actionConfiguration.getPluginSpecifiedTemplates();
- if(CollectionUtils.isEmpty(properties)) {
+ if (CollectionUtils.isEmpty(properties)) {
return Mono.error(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"Mandatory parameters 'Action' and 'Bucket Name' are missing. Did you forget to edit " +
- "the 'Action' and 'Bucket Name' fields in the query form ?"
+ "the 'Action' and 'Bucket Name' fields in the query form ?"
)
);
}
- if(properties.get(ACTION_PROPERTY_INDEX) == null) {
+ if (properties.get(ACTION_PROPERTY_INDEX) == null) {
return Mono.error(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"Mandatory parameter 'Action' is missing. Did you forget to select one of the actions" +
- " from the Action dropdown ?"
+ " from the Action dropdown ?"
)
);
}
AmazonS3Action s3Action = AmazonS3Action.valueOf(properties.get(ACTION_PROPERTY_INDEX).getValue());
- if (s3Action == null) {
- return Mono.error(
- new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_ERROR,
- "Mandatory parameter 'Action' is missing. Did you forget to select one of the actions" +
- " from the Action dropdown ?"
- )
- );
- }
if ((s3Action == AmazonS3Action.UPLOAD_FILE_FROM_BODY || s3Action == AmazonS3Action.READ_FILE ||
- s3Action == AmazonS3Action.DELETE_FILE) && StringUtils.isBlank(path)) {
+ s3Action == AmazonS3Action.DELETE_FILE) && StringUtils.isBlank(path)) {
return Mono.error(
new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Required parameter 'File Path' is missing. Did you forget to edit the 'File Path' field " +
- "in the query form ? This field cannot be left empty with the chosen action."
+ AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
+ "Required parameter 'File Path' is missing. Did you forget to edit the 'File Path' field " +
+ "in the query form ? This field cannot be left empty with the chosen action."
)
);
}
- if(properties.size() < (1+BUCKET_NAME_PROPERTY_INDEX)
- || properties.get(BUCKET_NAME_PROPERTY_INDEX) == null) {
+ if (properties.size() < (1 + BUCKET_NAME_PROPERTY_INDEX)
+ || properties.get(BUCKET_NAME_PROPERTY_INDEX) == null) {
return Mono.error(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"Mandatory parameter 'Bucket Name' is missing. Did you forget to edit the 'Bucket " +
- "Name' field in the query form ?"
+ "Name' field in the query form ?"
)
);
}
final String bucketName = properties.get(BUCKET_NAME_PROPERTY_INDEX).getValue();
- if(StringUtils.isEmpty(bucketName)) {
+ if (StringUtils.isEmpty(bucketName)) {
return Mono.error(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"Mandatory parameter 'Bucket Name' is missing. Did you forget to edit the 'Bucket " +
- "Name' field in the query form ?"
+ "Name' field in the query form ?"
)
);
}
@@ -359,12 +337,12 @@ public class AmazonS3Plugin extends BasePlugin {
* - Allow users to upload empty file. Hence, only check for null value.
*/
final String body = actionConfiguration.getBody();
- if(s3Action == AmazonS3Action.UPLOAD_FILE_FROM_BODY && body == null) {
+ if (s3Action == AmazonS3Action.UPLOAD_FILE_FROM_BODY && body == null) {
return Mono.error(
new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
"Mandatory parameter 'Content' is missing. Did you forget to edit the 'Content' " +
- "field in the query form ?"
+ "field in the query form ?"
)
);
}
@@ -374,169 +352,181 @@ public class AmazonS3Plugin extends BasePlugin {
switch (s3Action) {
case LIST:
String prefix = "";
- if(properties.size() > PREFIX_PROPERTY_INDEX
- && properties.get(PREFIX_PROPERTY_INDEX) != null
- && properties.get(PREFIX_PROPERTY_INDEX).getValue() != null) {
+ if (properties.size() > PREFIX_PROPERTY_INDEX
+ && properties.get(PREFIX_PROPERTY_INDEX) != null
+ && properties.get(PREFIX_PROPERTY_INDEX).getValue() != null) {
prefix = properties.get(PREFIX_PROPERTY_INDEX).getValue();
}
ArrayList listOfFiles = listAllFilesInBucket(connection, bucketName, prefix);
- if(properties.size() > GET_SIGNED_URL_PROPERTY_INDEX
- && properties.get(GET_SIGNED_URL_PROPERTY_INDEX) != null
- && properties.get(GET_SIGNED_URL_PROPERTY_INDEX).getValue().equals(YES)) {
+ if (properties.size() > GET_SIGNED_URL_PROPERTY_INDEX
+ && properties.get(GET_SIGNED_URL_PROPERTY_INDEX) != null
+ && properties.get(GET_SIGNED_URL_PROPERTY_INDEX).getValue().equals(YES)) {
- if(properties.size() < (1+URL_EXPIRY_DURATION_PROPERTY_INDEX)
- || properties.get(URL_EXPIRY_DURATION_PROPERTY_INDEX) == null
- || StringUtils.isEmpty(properties.get(URL_EXPIRY_DURATION_PROPERTY_INDEX).getValue())) {
- throw new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Required parameter 'URL Expiry Duration' is missing. Did you forget to" +
- " edit the 'URL Expiry Duration' field ?"
- );
+ int durationInMinutes;
+ if (properties.size() < (1 + URL_EXPIRY_DURATION_PROPERTY_INDEX)
+ || properties.get(URL_EXPIRY_DURATION_PROPERTY_INDEX) == null
+ || StringUtils.isEmpty(properties.get(URL_EXPIRY_DURATION_PROPERTY_INDEX).getValue())) {
+ durationInMinutes = DEFAULT_URL_EXPIRY_IN_MINUTES;
+ } else {
+ try {
+ durationInMinutes = Integer
+ .parseInt(
+ properties
+ .get(URL_EXPIRY_DURATION_PROPERTY_INDEX)
+ .getValue()
+ );
+ } catch (NumberFormatException e) {
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
+ "Parameter 'Expiry Duration of Signed URL' is NOT a number. Please ensure that the " +
+ "input to 'Expiry Duration of Signed URL' field is a valid number - i.e. " +
+ "any non-negative integer. Please note that the maximum expiry " +
+ "duration supported by Amazon S3 is 7 days i.e. 10080 minutes."
+ );
+ }
}
- int durationInMilliseconds = 0;
- try {
- durationInMilliseconds = Integer
- .parseInt(
- properties
- .get(URL_EXPIRY_DURATION_PROPERTY_INDEX)
- .getValue()
- );
- } catch (NumberFormatException e) {
- throw new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Parameter 'URL Expiry Duration' is NOT a number. Please ensure that the " +
- "input to 'URL Expiry Duration' field is a valid number - i.e. any non-negative integer."
- );
- }
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.MINUTE, durationInMinutes);
+ Date expiryDateTime = calendar.getTime();
+ DateFormat dateTimeFormat = new SimpleDateFormat("dd MMM yyyy HH:mm:ss:SSS z");
+ String expiryDateTimeString = dateTimeFormat.format(expiryDateTime);
ArrayList listOfSignedUrls = getSignedUrls(connection,
- bucketName,
- listOfFiles,
- durationInMilliseconds);
- if(listOfFiles.size() != listOfSignedUrls.size()) {
+ bucketName,
+ listOfFiles,
+ expiryDateTime);
+ if (listOfFiles.size() != listOfSignedUrls.size()) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Appsmith server has encountered an unexpected error when getting " +
- "list of files from AWS S3 server. Please reach out to Appsmith customer " +
- "support to resolve this."
+ "list of files from AWS S3 server. Please reach out to Appsmith customer " +
+ "support to resolve this."
);
}
- ArrayList> listOfFilesAndUrls = new ArrayList<>();
- for(int i=0; i fileUrlPair = new ArrayList<>();
- fileUrlPair.add(listOfFiles.get(i));
- fileUrlPair.add(listOfSignedUrls.get(i));
- listOfFilesAndUrls.add(fileUrlPair);
+ actionResult = new ArrayList<>();
+ for (int i = 0; i < listOfFiles.size(); i++) {
+ HashMap fileInfo = new HashMap<>();
+ fileInfo.put("fileName", listOfFiles.get(i));
+ fileInfo.put("signedUrl", listOfSignedUrls.get(i));
+ fileInfo.put("urlExpiryDate", expiryDateTimeString);
+ ((ArrayList