+ {uploadFileForm}
-
- {fileInfo.name}
- {fileInfo.size}KB
-
+ {uploadStatus}
@@ -341,6 +431,45 @@ function FilePickerComponent(props: FilePickerProps) {
text="remove"
/>
+ >
+ );
+
+ const uploadComponent = (
+
+ {uploadFileForm}
+
+ {uploadStatus}
+
+
+
+ Successfully Uploaded!
+
+
+ removeFile()}>
+
+
+
+
+
+
+ );
+
+ return (
+
+ {fileType === FileType.IMAGE ? imageUploadComponent : uploadComponent}
);
}
diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx
index e02af7fe68..ecca134ab8 100644
--- a/app/client/src/components/ads/Icon.tsx
+++ b/app/client/src/components/ads/Icon.tsx
@@ -64,6 +64,8 @@ import { ReactComponent as Pin3 } from "assets/icons/comments/pin_3.svg";
import { ReactComponent as Unpin } from "assets/icons/comments/unpin.svg";
import { ReactComponent as Reaction } from "assets/icons/comments/reaction.svg";
import { ReactComponent as Reaction2 } from "assets/icons/comments/reaction-2.svg";
+import { ReactComponent as Upload } from "assets/icons/ads/upload.svg";
+import { ReactComponent as Download } from "assets/icons/ads/download.svg";
import styled from "styled-components";
import { CommonComponentProps, Classes } from "./common";
import { noop } from "lodash";
@@ -117,6 +119,8 @@ export const sizeHandler = (size?: IconSize) => {
};
export const IconCollection = [
+ "upload",
+ "download",
"book",
"bug",
"cancel",
@@ -477,6 +481,14 @@ const Icon = forwardRef(
returnIcon =
;
break;
+ case "upload":
+ returnIcon =
;
+ break;
+
+ case "download":
+ returnIcon =
;
+ break;
+
default:
returnIcon = null;
break;
diff --git a/app/client/src/components/ads/MentionsInput.tsx b/app/client/src/components/ads/MentionsInput.tsx
index e62b5fe676..87d4ed8a39 100644
--- a/app/client/src/components/ads/MentionsInput.tsx
+++ b/app/client/src/components/ads/MentionsInput.tsx
@@ -11,7 +11,10 @@ import "draft-js/dist/Draft.css";
import { getTypographyByKey } from "constants/DefaultTheme";
import { EntryComponentProps } from "@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry";
import UserApi from "api/UserApi";
-import Text, { TextType } from "./Text";
+
+import Icon from "components/ads/Icon";
+
+import { INVITE_A_NEW_USER, createMessage } from "constants/messages";
const StyledMention = styled.span`
color: ${(props) => props.theme.colors.comments.mention};
@@ -62,6 +65,21 @@ const Username = styled.div`
color: ${(props) => props.theme.colors.mentionSuggestion.usernameText};
`;
+const PlusCircle = styled.div`
+ width: 25px;
+ height: 25px;
+ display: flex;
+ border-radius: 50%;
+ align-items: center;
+ justify-content: center;
+ background-color: ${(props) =>
+ props.theme.colors.mentionsInput.mentionsInviteBtnPlusIcon};
+ & svg path {
+ stroke: #fff;
+ }
+ margin-right: ${(props) => props.theme.spaces[4]}px;
+`;
+
function SuggestionComponent(props: EntryComponentProps) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { theme, ...parentProps } = props;
@@ -70,9 +88,13 @@ function SuggestionComponent(props: EntryComponentProps) {
if (props.mention?.isInviteTrigger) {
return (
-
- Invite {props.mention.name}
-
+
+
+
+
+ {createMessage(INVITE_A_NEW_USER)}
+ {props.mention.name}
+
);
}
diff --git a/app/client/src/components/ads/Menu.tsx b/app/client/src/components/ads/Menu.tsx
index dc3437cba9..b235273c38 100644
--- a/app/client/src/components/ads/Menu.tsx
+++ b/app/client/src/components/ads/Menu.tsx
@@ -12,6 +12,8 @@ type MenuProps = CommonComponentProps & {
onOpening?: (node: HTMLElement) => void;
onClosing?: (node: HTMLElement) => void;
modifiers?: PopperModifiers;
+ isOpen?: boolean;
+ onClose?: () => void;
};
const MenuWrapper = styled.div`
@@ -30,8 +32,10 @@ function Menu(props: MenuProps) {
className={props.className}
data-cy={props.cypressSelector}
disabled={props.disabled}
+ isOpen={props.isOpen}
minimal
modifiers={props.modifiers}
+ onClose={props.onClose}
onClosing={props.onClosing}
onOpening={props.onOpening}
portalClassName={props.className}
diff --git a/app/client/src/components/ads/Toast.tsx b/app/client/src/components/ads/Toast.tsx
index 34154c8fb8..d0a78223e3 100644
--- a/app/client/src/components/ads/Toast.tsx
+++ b/app/client/src/components/ads/Toast.tsx
@@ -97,6 +97,7 @@ const ToastBody = styled.div<{
color: ${props.theme.colors.toast.undo};
line-height: 18px;
font-weight: 600;
+ white-space: nowrap
}
`
: null}
diff --git a/app/client/src/components/ads/Tooltip.tsx b/app/client/src/components/ads/Tooltip.tsx
index 5adec64426..277665dfa2 100644
--- a/app/client/src/components/ads/Tooltip.tsx
+++ b/app/client/src/components/ads/Tooltip.tsx
@@ -2,6 +2,7 @@ import React from "react";
import { CommonComponentProps } from "./common";
import { Position, Tooltip, PopperBoundary } from "@blueprintjs/core";
import { GLOBAL_STYLE_TOOLTIP_CLASSNAME } from "globalStyles/tooltip";
+import { Modifiers } from "popper.js";
type Variant = "dark" | "light";
@@ -17,6 +18,7 @@ type TooltipProps = CommonComponentProps & {
autoFocus?: boolean;
hoverOpenDelay?: number;
minimal?: boolean;
+ modifiers?: Modifiers;
isOpen?: boolean;
};
@@ -33,6 +35,7 @@ function TooltipComponent(props: TooltipProps) {
minimal={props.minimal}
modifiers={{
preventOverflow: { enabled: false },
+ ...props.modifiers,
}}
openOnTargetFocus={props.openOnTargetFocus}
popoverClassName={GLOBAL_STYLE_TOOLTIP_CLASSNAME}
diff --git a/app/client/src/components/ads/tour/TourTooltipWrapper.tsx b/app/client/src/components/ads/tour/TourTooltipWrapper.tsx
index 3446348638..abd14b7a81 100644
--- a/app/client/src/components/ads/tour/TourTooltipWrapper.tsx
+++ b/app/client/src/components/ads/tour/TourTooltipWrapper.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useRef } from "react";
import TooltipComponent from "components/ads/Tooltip";
import { useSelector } from "react-redux";
import Text, { TextType } from "../Text";
@@ -8,14 +8,44 @@ import { TourType } from "entities/Tour";
import TourStepsByType from "constants/TourSteps";
import { AppState } from "reducers";
import { noop } from "lodash";
+import styled, { CSSProperties } from "styled-components";
+import { Modifiers } from "popper.js";
+import lottie from "lottie-web";
+import pulsatingDot from "assets/lottie/pulse-dot.json";
+import { Indices } from "constants/Layers";
type Props = {
children: React.ReactNode;
+ hasOverlay?: boolean;
tourType: TourType;
tourIndex: number;
+ modifiers?: Modifiers;
onClick?: () => void;
+ pulseStyles?: CSSProperties;
+ showPulse?: boolean;
};
+const Overlay = styled.div`
+ background-color: ${(props) => props.theme.colors.overlayColor};
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
+ position: fixed;
+ z-index: ${Indices.Layer1};
+`;
+
+const PulseDot = styled.div`
+ position: absolute;
+ height: 50px;
+ width: 50px;
+`;
+
+const Container = styled.div`
+ position: relative;
+ z-index: ${Indices.Layer1};
+`;
+
function TourTooltipWrapper(props: Props) {
const { children, tourIndex, tourType } = props;
const isCurrentStepActive = useSelector(
@@ -27,30 +57,53 @@ function TourTooltipWrapper(props: Props) {
const tourStepsConfig = TourStepsByType[tourType as TourType];
const tourStepConfig = tourStepsConfig[tourIndex];
const isOpen = isCurrentStepActive && isCurrentTourActive;
+ const dotRef = useRef
(null);
+
+ useEffect(() => {
+ const anim = lottie.loadAnimation({
+ animationData: pulsatingDot,
+ autoplay: true,
+ container: dotRef?.current as HTMLDivElement,
+ renderer: "svg",
+ loop: true,
+ });
+
+ return () => {
+ anim?.destroy();
+ };
+ }, [isOpen, dotRef?.current]);
return (
-
-
- {tourStepConfig?.data.message}
-
- }
- isOpen={!!isOpen}
- position={Position.BOTTOM}
- >
- {children}
-
-
+ <>
+ {/* A crude overlay which won't work with containers having overflow hidden */}
+ {isOpen && props.hasOverlay && }
+
+ {isOpen && props.showPulse && (
+
+ )}
+
+ {tourStepConfig?.data.message}
+
+ }
+ isOpen={!!isOpen}
+ modifiers={props.modifiers}
+ position={Position.BOTTOM}
+ >
+ {children}
+
+
+ >
);
}
diff --git a/app/client/src/components/designSystems/appsmith/Dropdown.tsx b/app/client/src/components/designSystems/appsmith/Dropdown.tsx
index 92e1a76645..2053b0310f 100644
--- a/app/client/src/components/designSystems/appsmith/Dropdown.tsx
+++ b/app/client/src/components/designSystems/appsmith/Dropdown.tsx
@@ -50,12 +50,15 @@ const selectStyles = {
padding: "5px",
}),
indicatorSeparator: () => ({}),
+ menu: (provided: any) => ({ ...provided, zIndex: 2 }),
+ menuPortal: (base: any) => ({ ...base, zIndex: 2 }),
};
export function BaseDropdown(props: DropdownProps) {
const { customSelectStyles, input } = props;
return (
;
}
return (
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts
index a8dbb7d807..88c74bac35 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/Constants.ts
@@ -94,6 +94,7 @@ export interface CellLayoutProperties {
buttonStyle?: string;
buttonLabelColor?: string;
buttonLabel?: string;
+ displayText?: string;
}
export interface TableColumnMetaProps {
@@ -144,6 +145,7 @@ export interface ColumnProperties {
inputFormat?: string;
dropdownOptions?: string;
onOptionChange?: string;
+ displayText?: string;
}
export const ConditionFunctions: {
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx
index cfc1b0bb85..b9a8b56731 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/Table.tsx
@@ -60,6 +60,11 @@ interface TableProps {
applyFilter: (filters: ReactTableFilter[]) => void;
compactMode?: CompactMode;
updateCompactMode: (compactMode: CompactMode) => void;
+ isVisibleCompactMode?: boolean;
+ isVisibleDownload?: boolean;
+ isVisibleFilters?: boolean;
+ isVisiblePagination?: boolean;
+ isVisibleSearch?: boolean;
}
const defaultColumn = {
@@ -161,65 +166,83 @@ export function Table(props: TableProps) {
const tableWrapperRef = useRef(null);
const tableBodyRef = useRef(null);
const tableHeaderWrapperRef = React.createRef();
+ const isHeaderVisible =
+ props.isVisibleSearch ||
+ props.isVisibleFilters ||
+ props.isVisibleDownload ||
+ props.isVisibleCompactMode ||
+ props.isVisiblePagination;
+
return (
-
-
-
-
-
-
-
+ width={props.width}
+ >
+
+
+
+
+ )}
void;
tableSizes: TableSizes;
+ isVisibleCompactMode?: boolean;
+ isVisibleDownload?: boolean;
+ isVisibleFilters?: boolean;
+ isVisiblePagination?: boolean;
+ isVisibleSearch?: boolean;
}
function TableHeader(props: TableHeaderProps) {
return (
<>
-
-
-
-
-
-
- {props.serverSidePaginationEnabled && (
+ )}
+ {(props.isVisibleFilters ||
+ props.isVisibleDownload ||
+ props.isVisibleCompactMode) && (
+
+ {props.isVisibleFilters && (
+
+ )}
+
+ {props.isVisibleDownload && (
+
+ )}
+
+ {props.isVisibleCompactMode && (
+
+ )}
+
+ )}
+
+ {props.isVisiblePagination && props.serverSidePaginationEnabled && (
)}
- {!props.serverSidePaginationEnabled && (
+ {props.isVisiblePagination && !props.serverSidePaginationEnabled && (
{props.tableData?.length} Records
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx
index 25cc63fcd1..369dc0df8e 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx
@@ -14,6 +14,7 @@ export const TableWrapper = styled.div<{
tableSizes: TableSizes;
backgroundColor?: Color;
triggerRowSelection: boolean;
+ isHeaderVisible?: boolean;
}>`
width: 100%;
height: 100%;
@@ -56,7 +57,8 @@ export const TableWrapper = styled.div<{
overflow: hidden;
}
.tbody {
- height: ${(props) => props.height - 80}px;
+ height: ${(props) =>
+ props.isHeaderVisible ? props.height - 80 : props.height - 40}px;
width: 100%;
overflow-y: auto;
${hideScrollbar};
@@ -111,8 +113,10 @@ export const TableWrapper = styled.div<{
}
.th {
padding: 0 10px 0 0;
- height: ${(props) => props.tableSizes.COLUMN_HEADER_HEIGHT}px;
- line-height: ${(props) => props.tableSizes.COLUMN_HEADER_HEIGHT}px;
+ height: ${(props) =>
+ props.isHeaderVisible ? props.tableSizes.COLUMN_HEADER_HEIGHT : 40}px;
+ line-height: ${(props) =>
+ props.isHeaderVisible ? props.tableSizes.COLUMN_HEADER_HEIGHT : 40}px;
background: ${Colors.ATHENS_GRAY_DARKER};
}
.td {
@@ -411,6 +415,9 @@ export const CellWrapper = styled.div<{
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ text-align: ${(props) =>
+ props?.cellProperties?.horizontalAlignment &&
+ TEXT_ALIGN[props?.cellProperties?.horizontalAlignment]};
}
.hidden-icon {
display: none;
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx
index e639867e84..b913aaf66f 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableUtilities.tsx
@@ -112,7 +112,9 @@ export const renderCell = (
tableWidth={tableWidth}
title={value.toString()}
>
- {value.toString()}
+ {value && columnType === ColumnTypes.URL && cellProperties.displayText
+ ? cellProperties.displayText
+ : value.toString()}
);
}
diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx
index 2d7ebbda4c..5850f52bf0 100644
--- a/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx
@@ -66,6 +66,11 @@ interface ReactTableComponentProps {
columns: ReactTableColumnProps[];
compactMode?: CompactMode;
updateCompactMode: (compactMode: CompactMode) => void;
+ isVisibleSearch?: boolean;
+ isVisibleFilters?: boolean;
+ isVisibleDownload?: boolean;
+ isVisibleCompactMode?: boolean;
+ isVisiblePagination?: boolean;
}
function ReactTableComponent(props: ReactTableComponentProps) {
@@ -81,6 +86,11 @@ function ReactTableComponent(props: ReactTableComponentProps) {
handleResizeColumn,
height,
isLoading,
+ isVisibleCompactMode,
+ isVisibleDownload,
+ isVisibleFilters,
+ isVisiblePagination,
+ isVisibleSearch,
nextPageClick,
onRowClick,
pageNo,
@@ -230,6 +240,11 @@ function ReactTableComponent(props: ReactTableComponentProps) {
handleResizeColumn={handleResizeColumn}
height={height}
isLoading={isLoading}
+ isVisibleCompactMode={isVisibleCompactMode}
+ isVisibleDownload={isVisibleDownload}
+ isVisibleFilters={isVisibleFilters}
+ isVisiblePagination={isVisiblePagination}
+ isVisibleSearch={isVisibleSearch}
nextPageClick={nextPageClick}
pageNo={pageNo - 1}
pageSize={pageSize || 1}
@@ -262,6 +277,11 @@ export default React.memo(ReactTableComponent, (prev, next) => {
prev.handleResizeColumn === next.handleResizeColumn &&
prev.height === next.height &&
prev.isLoading === next.isLoading &&
+ prev.isVisibleCompactMode === next.isVisibleCompactMode &&
+ prev.isVisibleDownload === next.isVisibleDownload &&
+ prev.isVisibleFilters === next.isVisibleFilters &&
+ prev.isVisiblePagination === next.isVisiblePagination &&
+ prev.isVisibleSearch === next.isVisibleSearch &&
prev.nextPageClick === next.nextPageClick &&
prev.onRowClick === next.onRowClick &&
prev.pageNo === next.pageNo &&
diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx
index 44b5338312..daa1459448 100644
--- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx
@@ -180,8 +180,25 @@ function RecaptchaComponent(
});
props.onClick && props.onClick(event);
}
+
+ // Check if a string is a valid JSON string
+ const checkValidJson = (inputString: string): boolean => {
+ try {
+ JSON.parse(inputString);
+ return true;
+ } catch (err) {
+ return false;
+ }
+ };
+
+ let validGoogleRecaptchaKey = props.googleRecaptchaKey;
+
+ if (validGoogleRecaptchaKey && checkValidJson(validGoogleRecaptchaKey)) {
+ validGoogleRecaptchaKey = undefined;
+ }
+
const status = useScript(
- `https://www.google.com/recaptcha/api.js?render=${props.googleRecaptchaKey}`,
+ `https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`,
);
return (
props.height - WIDGET_PADDING * 2 - 2}px;
+ align-content: flex-start;
}
.${Classes.TAG} {
diff --git a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx
index acf3aa652d..2039ada9d1 100644
--- a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx
@@ -56,11 +56,13 @@ const Content = styled.div<{
export type ModalComponentProps = {
isOpen: boolean;
onClose: (e: any) => void;
+ onModalClose?: () => void;
children: ReactNode;
width?: number;
className?: string;
canOutsideClickClose: boolean;
canEscapeKeyClose: boolean;
+ overlayClassName?: string;
scrollContents: boolean;
height?: number;
top?: number;
@@ -76,6 +78,14 @@ export function ModalComponent(props: ModalComponentProps) {
const modalContentRef: RefObject
= useRef(
null,
);
+ useEffect(() => {
+ return () => {
+ // handle modal close events when this component unmounts
+ // will be called in all cases :-
+ // escape key press, click out side, close click from other btn widget
+ if (props.onModalClose) props.onModalClose();
+ };
+ }, []);
useEffect(() => {
if (!props.scrollContents) {
modalContentRef.current?.scrollTo({ top: 0, behavior: "smooth" });
@@ -94,6 +104,7 @@ export function ModalComponent(props: ModalComponentProps) {
{
>
diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx
index 145807ae65..bb56ee5406 100644
--- a/app/client/src/components/editorComponents/ApiResponseView.tsx
+++ b/app/client/src/components/editorComponents/ApiResponseView.tsx
@@ -13,7 +13,13 @@ import { getActionResponses } from "selectors/entitiesSelector";
import { Colors } from "constants/Colors";
import _ from "lodash";
import { useLocalStorage } from "utils/hooks/localstorage";
-import { CHECK_REQUEST_BODY, createMessage } from "constants/messages";
+import {
+ CHECK_REQUEST_BODY,
+ createMessage,
+ DEBUGGER_ERRORS,
+ DEBUGGER_LOGS,
+ INSPECT_ENTITY,
+} from "constants/messages";
import { TabComponent } from "components/ads/Tabs";
import Text, { TextType } from "components/ads/Text";
import Icon from "components/ads/Icon";
@@ -25,6 +31,7 @@ import ErrorLogs from "./Debugger/Errors";
import Resizer, { ResizerCSS } from "./Debugger/Resizer";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { DebugButton } from "./Debugger/DebugCTA";
+import EntityDeps from "./Debugger/EntityDependecies";
const ResponseContainer = styled.div`
${ResizerCSS}
@@ -228,14 +235,19 @@ function ApiResponseView(props: Props) {
},
{
key: "ERROR",
- title: "Errors",
+ title: createMessage(DEBUGGER_ERRORS),
panelComponent: ,
},
{
key: "LOGS",
- title: "Logs",
+ title: createMessage(DEBUGGER_LOGS),
panelComponent: ,
},
+ {
+ key: "ENTITY_DEPENDENCIES",
+ title: createMessage(INSPECT_ENTITY),
+ panelComponent: ,
+ },
];
const onTabSelect = (index: number) => {
diff --git a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts
index c61b59dafb..3960306bf9 100644
--- a/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/EditorConfig.ts
@@ -46,7 +46,11 @@ export type HintHelper = (
additionalData?: Record>,
) => Hinter;
export type Hinter = {
- showHint: (editor: CodeMirror.Editor, expected: string) => void;
+ showHint: (
+ editor: CodeMirror.Editor,
+ expected: string,
+ entityName: string,
+ ) => void;
update?: (data: DataTree) => void;
trigger?: (editor: CodeMirror.Editor) => void;
};
diff --git a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts
index a6f747c593..27448ca0c4 100644
--- a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.ts
@@ -16,7 +16,8 @@ export const bindingHint: HintHelper = (editor, data, additionalData) => {
[KeyboardShortcuts.CodeEditor.OpenAutocomplete]: (
cm: CodeMirror.Editor,
expected: string,
- ) => ternServer.complete(cm, expected),
+ entity: string,
+ ) => ternServer.complete(cm, expected, entity),
[KeyboardShortcuts.CodeEditor.ShowTypeAndInfo]: (cm: CodeMirror.Editor) => {
ternServer.showType(cm);
},
@@ -29,7 +30,11 @@ export const bindingHint: HintHelper = (editor, data, additionalData) => {
const dataTreeDef = dataTreeTypeDefCreator(data);
ternServer.updateDef("dataTree", dataTreeDef);
},
- showHint: (editor: CodeMirror.Editor, expected: string) => {
+ showHint: (
+ editor: CodeMirror.Editor,
+ expected: string,
+ entityName: string,
+ ) => {
let cursorBetweenBinding = false;
const cursor = editor.getCursor();
const value = editor.getValue();
@@ -66,7 +71,7 @@ export const bindingHint: HintHelper = (editor, data, additionalData) => {
const shouldShow = cursorBetweenBinding;
if (shouldShow) {
AnalyticsUtil.logEvent("AUTO_COMPELTE_SHOW", {});
- ternServer.complete(editor, expected);
+ ternServer.complete(editor, expected, entityName);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx
index 0f28126d93..a0c9aeca22 100644
--- a/app/client/src/components/editorComponents/CodeEditor/index.tsx
+++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx
@@ -50,12 +50,19 @@ import "codemirror/addon/fold/foldgutter";
import "codemirror/addon/fold/foldgutter.css";
import * as Sentry from "@sentry/react";
import { removeNewLineChars, getInputValue } from "./codeEditorUtils";
+import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
const LightningMenu = lazy(() =>
retryPromise(() => import("components/editorComponents/LightningMenu")),
);
-const AUTOCOMPLETE_CLOSE_KEY_CODES = ["Enter", "Tab", "Escape", "Comma"];
+const AUTOCOMPLETE_CLOSE_KEY_CODES = [
+ "Enter",
+ "Tab",
+ "Escape",
+ "Comma",
+ "Backspace",
+];
interface ReduxStateProps {
dynamicData: DataTree;
@@ -255,6 +262,7 @@ class CodeEditor extends Component {
};
handleEditorFocus = () => {
+ if (this.state.isFocused) return;
this.setState({ isFocused: true });
this.editor.refresh();
if (this.props.size === EditorSize.COMPACT) {
@@ -265,9 +273,9 @@ class CodeEditor extends Component {
}
};
- handleEditorBlur = () => {
+ handleEditorBlur = (cm: CodeMirror.Editor) => {
this.handleChange();
- this.setState({ isFocused: false });
+ if (!cm.state.completionActive) this.setState({ isFocused: false });
if (this.props.size === EditorSize.COMPACT) {
this.editor.setOption("lineWrapping", false);
}
@@ -295,7 +303,10 @@ class CodeEditor extends Component {
handleAutocompleteVisibility = (cm: CodeMirror.Editor) => {
const expected = this.props.expected ? this.props.expected : "";
- this.hinters.forEach((hinter) => hinter.showHint(cm, expected));
+ const { entityName } = getEntityNameAndPropertyPath(
+ this.props.dataTreePath || "",
+ );
+ this.hinters.forEach((hinter) => hinter.showHint(cm, expected, entityName));
};
handleAutocompleteHide = (cm: any, event: KeyboardEvent) => {
@@ -340,16 +351,26 @@ class CodeEditor extends Component {
if (!dataTreePath) {
return { isValid: true, validationMessage: "", jsErrorMessage: "" };
}
- const isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps");
- const validationMessagePath = dataTreePath.replace(
- "evaluatedValues",
- "validationMessages",
+ const { entityName, propertyPath } = getEntityNameAndPropertyPath(
+ dataTreePath,
);
- const jsErrorMessagePath = dataTreePath.replace(
- "evaluatedValues",
- "jsErrorMessages",
- );
-
+ let isValidPath, validationMessagePath, jsErrorMessagePath;
+ if (dataTreePath && dataTreePath.match(/evaluatedValues/g)) {
+ isValidPath = dataTreePath.replace("evaluatedValues", "invalidProps");
+ validationMessagePath = dataTreePath.replace(
+ "evaluatedValues",
+ "validationMessages",
+ );
+ jsErrorMessagePath = dataTreePath.replace(
+ "evaluatedValues",
+ "jsErrorMessages",
+ );
+ } else {
+ isValidPath = entityName + "invalidProps" + propertyPath;
+ validationMessagePath =
+ entityName + ".validationMessages." + propertyPath;
+ jsErrorMessagePath = entityName + ".jsErrorMessages." + propertyPath;
+ }
const isValid = !_.get(dataTree, isValidPath, false);
const validationMessage = _.get(
dataTree,
diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
index cdd2e42f20..e2d0202f57 100644
--- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
@@ -59,7 +59,7 @@ export const HintStyles = createGlobalStyle<{
}
.datasource-hint {
- padding: 10px;
+ padding: 10px 20px 10px 10px !important;
display: block;
width: 500px;
height: 32px;
diff --git a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx
index 3ff07385a2..ece100822f 100644
--- a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx
+++ b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx
@@ -3,8 +3,9 @@ import styled from "styled-components";
import { isUndefined } from "lodash";
import { Severity } from "entities/AppsmithConsole";
import FilterHeader from "./FilterHeader";
-import { BlankState, useFilteredLogs, usePagination } from "./helpers";
+import { BlankState } from "./helpers";
import LogItem, { getLogItemProps } from "./LogItem";
+import { usePagination, useFilteredLogs } from "./hooks";
const LIST_HEADER_HEIGHT = "38px";
diff --git a/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx b/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx
index a7641165a9..03a260a9c5 100644
--- a/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx
+++ b/app/client/src/components/editorComponents/Debugger/DebuggerTabs.tsx
@@ -8,6 +8,13 @@ import { showDebugger } from "actions/debuggerActions";
import Errors from "./Errors";
import Resizer, { ResizerCSS } from "./Resizer";
import AnalyticsUtil from "utils/AnalyticsUtil";
+import EntityDeps from "./EntityDependecies";
+import {
+ createMessage,
+ DEBUGGER_ERRORS,
+ DEBUGGER_LOGS,
+ INSPECT_ENTITY,
+} from "constants/messages";
const TABS_HEADER_HEIGHT = 36;
@@ -41,14 +48,19 @@ type DebuggerTabsProps = {
const DEBUGGER_TABS = [
{
key: "ERROR",
- title: "Errors",
+ title: createMessage(DEBUGGER_ERRORS),
panelComponent: ,
},
{
key: "LOGS",
- title: "Logs",
+ title: createMessage(DEBUGGER_LOGS),
panelComponent: ,
},
+ {
+ key: "INSPECT_ELEMENTS",
+ title: createMessage(INSPECT_ENTITY),
+ panelComponent: ,
+ },
];
function DebuggerTabs(props: DebuggerTabsProps) {
diff --git a/app/client/src/components/editorComponents/Debugger/EntityDependecies.tsx b/app/client/src/components/editorComponents/Debugger/EntityDependecies.tsx
new file mode 100644
index 0000000000..76672346ed
--- /dev/null
+++ b/app/client/src/components/editorComponents/Debugger/EntityDependecies.tsx
@@ -0,0 +1,176 @@
+/* eslint-disable prefer-const */
+import { Collapse } from "@blueprintjs/core";
+import React, { memo, ReactNode, useMemo, useState } from "react";
+import { useSelector } from "react-redux";
+import { AppState } from "reducers";
+import styled from "styled-components";
+import Icon, { IconSize } from "components/ads/Icon";
+import { Classes } from "components/ads/common";
+import InspectElement from "assets/images/InspectElement.svg";
+import { SourceEntity } from "entities/AppsmithConsole";
+import { createMessage, INSPECT_ENTITY_BLANK_STATE } from "constants/messages";
+import { getDependenciesFromInverseDependencies } from "./helpers";
+import { useEntityLink, useSelectedEntity } from "./hooks";
+
+const CollapsibleWrapper = styled.div<{ step: number; isOpen: boolean }>`
+ margin-left: ${(props) => props.step * 10}px;
+ padding-top: ${(props) => props.theme.spaces[3]}px;
+
+ .label-wrapper {
+ display: flex;
+ flex-direction: row;
+ font-weight: ${(props) => props.theme.fontWeights[2]};
+
+ span {
+ margin-left: ${(props) => props.theme.spaces[3] - 1}px;
+ }
+ }
+
+ .${Classes.ICON} {
+ ${(props) => !props.isOpen && `transform: rotate(-90deg);`}
+ }
+`;
+
+const DependenciesWrapper = styled.div`
+ padding: ${(props) => props.theme.spaces[7]}px
+ ${(props) => props.theme.spaces[13] + 1}px;
+ color: ${(props) => props.theme.colors.debugger.inspectElement.color};
+
+ .no-dependencies {
+ margin-left: ${(props) => props.theme.spaces[4]}px;
+ }
+`;
+
+const StyledSpan = styled.div<{ step: number }>`
+ padding-top: ${(props) => props.theme.spaces[3]}px;
+ padding-left: ${(props) => props.theme.spaces[6] + 1}px;
+ margin-left: ${(props) => props.theme.spaces[4]}px;
+ border-left: solid 1px rgba(147, 144, 144, 0.7);
+ text-decoration-line: underline;
+ cursor: pointer;
+`;
+
+const BlankStateContainer = styled.div`
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ flex-direction: column;
+ color: ${(props) => props.theme.colors.debugger.blankState.color};
+
+ span {
+ margin-top: ${(props) => props.theme.spaces[9] + 1}px;
+ }
+`;
+
+function EntityDeps() {
+ const deps = useSelector((state: AppState) => state.evaluations.dependencies);
+ const selectedEntity = useSelectedEntity();
+
+ const entityDependencies: {
+ directDependencies: string[];
+ inverseDependencies: string[];
+ } | null = useMemo(
+ () =>
+ getDependenciesFromInverseDependencies(
+ deps.inverseDependencyMap,
+ selectedEntity ? selectedEntity.name : null,
+ ),
+ [selectedEntity, deps.inverseDependencyMap],
+ );
+
+ if (!selectedEntity || !entityDependencies) return ;
+
+ return (
+
+
+
+
+ );
+}
+
+function BlankState() {
+ return (
+
+
+ {createMessage(INSPECT_ENTITY_BLANK_STATE)}
+
+ );
+}
+
+function DependencyHierarchy(props: {
+ dependencies: string[];
+ entityName: string;
+ selectedEntity: SourceEntity;
+ type: string;
+}) {
+ const { navigateToEntity } = useEntityLink();
+ const label = props.dependencies.length
+ ? props.entityName
+ : `No ${props.type} exist for ${props.selectedEntity.name}`;
+
+ return (
+
+ {props.dependencies.length ? (
+
+ {props.dependencies.map((item) => {
+ return (
+ {
+ e.stopPropagation();
+ navigateToEntity(item);
+ }}
+ step={2}
+ >
+ {item}
+
+ );
+ })}
+
+ ) : (
+ {label}
+ )}
+
+ );
+}
+const MemoizedDependencyHierarchy = memo(DependencyHierarchy);
+
+function Collapsible(props: {
+ label: string;
+ step: number;
+ children: ReactNode;
+}) {
+ const [isOpen, setIsOpen] = useState(true);
+
+ return (
+ {
+ e.stopPropagation();
+ setIsOpen(!isOpen);
+ }}
+ step={props.step}
+ >
+
+
+ {props.label}
+
+ {props.children}
+
+ );
+}
+
+export default EntityDeps;
diff --git a/app/client/src/components/editorComponents/Debugger/helpers.test.ts b/app/client/src/components/editorComponents/Debugger/helpers.test.ts
new file mode 100644
index 0000000000..4d4bd71ea0
--- /dev/null
+++ b/app/client/src/components/editorComponents/Debugger/helpers.test.ts
@@ -0,0 +1,26 @@
+import { getDependenciesFromInverseDependencies } from "./helpers";
+
+describe("getDependencies", () => {
+ it("Check if getDependencies returns in a correct format", () => {
+ const input = {
+ "Button1.text": ["Input1.defaultText", "Button1"],
+ "Input1.defaultText": ["Input1.text", "Input1"],
+ "Input1.inputType": ["Input1.isValid", "Input1"],
+ "Input1.text": ["Input1.isValid", "Input1.value", "Input1"],
+ "Input1.isRequired": ["Input1.isValid", "Input1"],
+ "Input1.isValid": ["Button1.isVisible", "Input1"],
+ "Button1.isVisible": ["Button1"],
+ Button1: ["Chart1.chartName"],
+ "Chart1.chartName": ["Chart1"],
+ "Input1.value": ["Input1"],
+ };
+ const output = {
+ directDependencies: ["Input1"],
+ inverseDependencies: ["Input1", "Chart1"],
+ };
+
+ expect(
+ getDependenciesFromInverseDependencies(input, "Button1"),
+ ).toStrictEqual(output);
+ });
+});
diff --git a/app/client/src/components/editorComponents/Debugger/helpers.tsx b/app/client/src/components/editorComponents/Debugger/helpers.tsx
index 29e019b9cf..4f1c1a5584 100644
--- a/app/client/src/components/editorComponents/Debugger/helpers.tsx
+++ b/app/client/src/components/editorComponents/Debugger/helpers.tsx
@@ -1,7 +1,5 @@
-import { Message, Severity } from "entities/AppsmithConsole";
-import React, { useCallback, useEffect, useState } from "react";
-import { useSelector } from "react-redux";
-import { AppState } from "reducers";
+import { Severity } from "entities/AppsmithConsole";
+import React from "react";
import styled from "styled-components";
import { getTypographyByKey } from "constants/DefaultTheme";
import {
@@ -10,6 +8,12 @@ import {
OPEN_THE_DEBUGGER,
PRESS,
} from "constants/messages";
+import { DependencyMap } from "utils/DynamicBindingUtils";
+import {
+ API_EDITOR_URL,
+ QUERIES_EDITOR_URL,
+ BUILDER_PAGE_URL,
+} from "constants/routes";
const BlankStateWrapper = styled.div`
overflow: auto;
@@ -54,46 +58,73 @@ export const SeverityIconColor: Record = {
[Severity.WARNING]: "rgb(224, 179, 14)",
};
-export const useFilteredLogs = (query: string, filter?: any) => {
- let logs = useSelector((state: AppState) => state.ui.debugger.logs);
+export function getDependenciesFromInverseDependencies(
+ deps: DependencyMap,
+ entityName: string | null,
+) {
+ if (!entityName) return null;
- if (filter) {
- logs = logs.filter((log: Message) => log.severity === filter);
- }
+ const directDependencies = new Set();
+ const inverseDependencies = new Set();
- if (query) {
- logs = logs.filter((log: Message) => {
- if (log.source?.name)
- return (
- log.source?.name.toUpperCase().indexOf(query.toUpperCase()) !== -1
- );
+ Object.entries(deps).forEach(([dependant, dependencies]) => {
+ (dependencies as any).map((dependency: any) => {
+ if (!dependant.includes(entityName) && dependency.includes(entityName)) {
+ const entity = dependant
+ .split(".")
+ .slice(0, 1)
+ .join("");
+
+ directDependencies.add(entity);
+ } else if (
+ dependant.includes(entityName) &&
+ !dependency.includes(entityName)
+ ) {
+ const entity = dependency
+ .split(".")
+ .slice(0, 1)
+ .join("");
+
+ inverseDependencies.add(entity);
+ }
});
- }
+ });
- return logs;
+ return {
+ inverseDependencies: Array.from(inverseDependencies),
+ directDependencies: Array.from(directDependencies),
+ };
+}
+
+export const onApiEditor = (
+ applicationId: string | undefined,
+ currentPageId: string | undefined,
+) => {
+ return (
+ window.location.pathname.indexOf(
+ API_EDITOR_URL(applicationId, currentPageId),
+ ) > -1
+ );
};
-export const usePagination = (data: Message[], itemsPerPage = 50) => {
- const [currentPage, setCurrentPage] = useState(1);
- const [paginatedData, setPaginatedData] = useState([]);
- const maxPage = Math.ceil(data.length / itemsPerPage);
-
- useEffect(() => {
- const data = currentData();
- setPaginatedData(data);
- }, [currentPage, data.length]);
-
- const currentData = useCallback(() => {
- const end = currentPage * itemsPerPage;
- return data.slice(0, end);
- }, [data]);
-
- const next = useCallback(() => {
- setCurrentPage((currentPage) => {
- const newCurrentPage = Math.min(currentPage + 1, maxPage);
- return newCurrentPage <= 0 ? 1 : newCurrentPage;
- });
- }, []);
-
- return { next, paginatedData };
+export const onQueryEditor = (
+ applicationId: string | undefined,
+ currentPageId: string | undefined,
+) => {
+ return (
+ window.location.pathname.indexOf(
+ QUERIES_EDITOR_URL(applicationId, currentPageId),
+ ) > -1
+ );
+};
+
+export const onCanvas = (
+ applicationId: string | undefined,
+ currentPageId: string | undefined,
+) => {
+ return (
+ window.location.pathname.indexOf(
+ BUILDER_PAGE_URL(applicationId, currentPageId),
+ ) > -1
+ );
};
diff --git a/app/client/src/components/editorComponents/Debugger/hooks.ts b/app/client/src/components/editorComponents/Debugger/hooks.ts
new file mode 100644
index 0000000000..aca5c9f578
--- /dev/null
+++ b/app/client/src/components/editorComponents/Debugger/hooks.ts
@@ -0,0 +1,144 @@
+import { useCallback, useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { useParams } from "react-router";
+import { ENTITY_TYPE, Message } from "entities/AppsmithConsole";
+import { AppState } from "reducers";
+import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
+import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/WidgetEntity";
+import { getWidget } from "sagas/selectors";
+import { getDataTree } from "selectors/dataTreeSelectors";
+import {
+ getCurrentApplicationId,
+ getCurrentPageId,
+} from "selectors/editorSelectors";
+import { getAction } from "selectors/entitiesSelector";
+import {
+ getCurrentWidgetId,
+ getIsPropertyPaneVisible,
+} from "selectors/propertyPaneSelectors";
+import { isWidget, isAction } from "workers/evaluationUtils";
+import { onApiEditor, onQueryEditor, onCanvas } from "./helpers";
+import history from "utils/history";
+
+export const useFilteredLogs = (query: string, filter?: any) => {
+ let logs = useSelector((state: AppState) => state.ui.debugger.logs);
+
+ if (filter) {
+ logs = logs.filter((log: Message) => log.severity === filter);
+ }
+
+ if (query) {
+ logs = logs.filter((log: Message) => {
+ if (log.source?.name)
+ return (
+ log.source?.name.toUpperCase().indexOf(query.toUpperCase()) !== -1
+ );
+ });
+ }
+
+ return logs;
+};
+
+export const usePagination = (data: Message[], itemsPerPage = 50) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [paginatedData, setPaginatedData] = useState([]);
+ const maxPage = Math.ceil(data.length / itemsPerPage);
+
+ useEffect(() => {
+ const data = currentData();
+ setPaginatedData(data);
+ }, [currentPage, data.length]);
+
+ const currentData = useCallback(() => {
+ const end = currentPage * itemsPerPage;
+ return data.slice(0, end);
+ }, [data]);
+
+ const next = useCallback(() => {
+ setCurrentPage((currentPage) => {
+ const newCurrentPage = Math.min(currentPage + 1, maxPage);
+ return newCurrentPage <= 0 ? 1 : newCurrentPage;
+ });
+ }, []);
+
+ return { next, paginatedData };
+};
+
+export const useSelectedEntity = () => {
+ const applicationId = useSelector(getCurrentApplicationId);
+ const currentPageId = useSelector(getCurrentPageId);
+
+ const params: any = useParams();
+ const action = useSelector((state: AppState) => {
+ if (
+ onApiEditor(applicationId, currentPageId) ||
+ onQueryEditor(applicationId, currentPageId)
+ ) {
+ const id = params.apiId || params.queryId;
+
+ return getAction(state, id);
+ }
+
+ return null;
+ });
+
+ const isPropertyPaneVisible = useSelector(getIsPropertyPaneVisible);
+ const selectedWidget = useSelector(getCurrentWidgetId);
+ const widget = useSelector((state: AppState) => {
+ if (onCanvas(applicationId, currentPageId) && isPropertyPaneVisible) {
+ return selectedWidget ? getWidget(state, selectedWidget) : null;
+ }
+
+ return null;
+ });
+
+ if (
+ onApiEditor(applicationId, currentPageId) ||
+ onQueryEditor(applicationId, currentPageId)
+ ) {
+ return {
+ name: action?.name ?? "",
+ type: ENTITY_TYPE.ACTION,
+ id: action?.id ?? "",
+ };
+ } else if (onCanvas(applicationId, currentPageId)) {
+ return {
+ name: widget?.widgetName ?? "",
+ type: ENTITY_TYPE.WIDGET,
+ id: widget?.widgetId ?? "",
+ };
+ }
+
+ return null;
+};
+
+export const useEntityLink = () => {
+ const dataTree = useSelector(getDataTree);
+ const applicationId = useSelector(getCurrentApplicationId);
+ const pageId = useSelector(getCurrentPageId);
+
+ const { navigateToWidget } = useNavigateToWidget();
+
+ const navigateToEntity = useCallback(
+ (name) => {
+ const entity = dataTree[name];
+ if (isWidget(entity)) {
+ navigateToWidget(entity.widgetId, entity.type, pageId || "");
+ } else if (isAction(entity)) {
+ const actionConfig = getActionConfig(entity.pluginType);
+ const url =
+ applicationId &&
+ actionConfig?.getURL(applicationId, pageId || "", entity.actionId);
+
+ if (url) {
+ history.push(url);
+ }
+ }
+ },
+ [dataTree],
+ );
+
+ return {
+ navigateToEntity,
+ };
+};
diff --git a/app/client/src/components/editorComponents/DraggableComponent.tsx b/app/client/src/components/editorComponents/DraggableComponent.tsx
index 60f334bf90..62111a81fe 100644
--- a/app/client/src/components/editorComponents/DraggableComponent.tsx
+++ b/app/client/src/components/editorComponents/DraggableComponent.tsx
@@ -12,6 +12,7 @@ import {
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
import AnalyticsUtil from "utils/AnalyticsUtil";
+import { commentModeSelector } from "selectors/commentsSelectors";
const DraggableWrapper = styled.div`
display: block;
@@ -50,8 +51,11 @@ export const canDrag = (
isResizing: boolean,
isDraggingDisabled: boolean,
props: any,
+ isCommentMode: boolean,
) => {
- return !isResizing && !isDraggingDisabled && !props.dragDisabled;
+ return (
+ !isResizing && !isDraggingDisabled && !props?.dragDisabled && !isCommentMode
+ );
};
function DraggableComponent(props: DraggableComponentProps) {
@@ -61,6 +65,8 @@ function DraggableComponent(props: DraggableComponentProps) {
// Dispatch hook handy to set a widget as focused/selected
const { focusWidget, selectWidget } = useWidgetSelection();
+ const isCommentMode = useSelector(commentModeSelector);
+
// Dispatch hook handy to set any `DraggableComponent` as dragging/ not dragging
// The value is boolean
const { setIsDragging } = useWidgetDragResize();
@@ -138,7 +144,7 @@ function DraggableComponent(props: DraggableComponentProps) {
},
canDrag: () => {
// Dont' allow drag if we're resizing or the drag of `DraggableComponent` is disabled
- return canDrag(isResizing, isDraggingDisabled, props);
+ return canDrag(isResizing, isDraggingDisabled, props, isCommentMode);
},
});
diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx
index b8c2983a02..baa81e42c0 100644
--- a/app/client/src/components/editorComponents/EditableText.tsx
+++ b/app/client/src/components/editorComponents/EditableText.tsx
@@ -92,10 +92,25 @@ const TextContainer = styled.div<{ isValid: boolean; minimal: boolean }>`
`;
export function EditableText(props: EditableTextProps) {
- const [isEditing, setIsEditing] = useState(!!props.isEditingDefault);
- const [value, setStateValue] = useState(props.defaultValue);
+ const {
+ beforeUnmount,
+ className,
+ defaultValue,
+ editInteractionKind,
+ forceDefault,
+ hideEditIcon,
+ isEditingDefault,
+ isInvalid,
+ minimal,
+ onBlur,
+ onTextChanged,
+ placeholder,
+ updating,
+ valueTransform,
+ } = props;
+ const [isEditing, setIsEditing] = useState(!!isEditingDefault);
+ const [value, setStateValue] = useState(defaultValue);
const inputValRef = useRef("");
- const { beforeUnmount } = props;
const setValue = useCallback((value) => {
inputValRef.current = value;
@@ -103,16 +118,16 @@ export function EditableText(props: EditableTextProps) {
}, []);
useEffect(() => {
- setValue(props.defaultValue);
- }, [props.defaultValue]);
+ setValue(defaultValue);
+ }, [defaultValue]);
useEffect(() => {
- setIsEditing(!!props.isEditingDefault);
- }, [props.defaultValue, props.isEditingDefault]);
+ setIsEditing(!!isEditingDefault);
+ }, [defaultValue, isEditingDefault]);
useEffect(() => {
- if (props.forceDefault === true) setValue(props.defaultValue);
- }, [props.forceDefault, props.defaultValue]);
+ if (forceDefault === true) setValue(defaultValue);
+ }, [forceDefault, defaultValue]);
// at times onTextChange is not fired
// for example when the modal is closed on clicking the overlay
@@ -128,58 +143,63 @@ export function EditableText(props: EditableTextProps) {
e.preventDefault();
e.stopPropagation();
};
- const onChange = (_value: string) => {
- props.onBlur && props.onBlur();
- const isInvalid = props.isInvalid ? props.isInvalid(_value) : false;
- if (!isInvalid) {
- props.onTextChanged(_value);
- setIsEditing(false);
- } else {
- Toaster.show({
- text: "Invalid name",
- variant: Variant.danger,
- });
- }
- };
+ const onChange = useCallback(
+ (_value: string) => {
+ onBlur && onBlur();
+ const _isInvalid = isInvalid ? isInvalid(_value) : false;
+ if (!_isInvalid) {
+ onTextChanged(_value);
+ setIsEditing(false);
+ } else {
+ Toaster.show({
+ text: "Invalid name",
+ variant: Variant.danger,
+ });
+ }
+ },
+ [isInvalid],
+ );
- const onInputchange = (_value: string) => {
- let finalVal: string = _value;
- if (props.valueTransform) {
- finalVal = props.valueTransform(_value);
- }
- setValue(finalVal);
- };
+ const onInputchange = useCallback(
+ (_value: string) => {
+ let finalVal: string = _value;
+ if (valueTransform) {
+ finalVal = valueTransform(_value);
+ }
+ setValue(finalVal);
+ },
+ [valueTransform],
+ );
- const errorMessage = props.isInvalid && props.isInvalid(value);
+ const errorMessage = isInvalid && isInvalid(value);
const error = errorMessage ? errorMessage : undefined;
return (
-
+
- {!props.minimal &&
- !props.hideEditIcon &&
- !props.updating &&
- !isEditing && }
+ {!minimal && !hideEditIcon && !updating && !isEditing && (
+
+ )}
diff --git a/app/client/src/components/editorComponents/GlobalSearch/Description.tsx b/app/client/src/components/editorComponents/GlobalSearch/Description.tsx
index e9af253255..cae754bb9e 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/Description.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/Description.tsx
@@ -1,8 +1,8 @@
-import React, { useCallback, useEffect } from "react";
+import React, { useEffect } from "react";
import styled from "styled-components";
import ActionLink from "./ActionLink";
import Highlight from "./Highlight";
-import { getItemTitle, SEARCH_ITEM_TYPES } from "./utils";
+import { algoliaHighlightTag, getItemTitle, SEARCH_ITEM_TYPES } from "./utils";
import { getTypographyByKey } from "constants/DefaultTheme";
import { SearchItem } from "./utils";
import parseDocumentationContent from "./parseDocumentationContent";
@@ -74,27 +74,57 @@ const Container = styled.div`
}
`;
-const DocumentationDescription = ({ item }: { item: SearchItem }) => {
- try {
- const {
- _highlightResult: {
- document: { value: rawDocument },
- title: { value: rawTitle },
- },
- } = item;
- const content = parseDocumentationContent({
- rawDocument: rawDocument,
- rawTitle: rawTitle,
- path: item.path,
- });
+function DocumentationDescription({
+ item,
+ query,
+}: {
+ item: SearchItem;
+ query: string;
+}) {
+ const {
+ _highlightResult: {
+ document: { value: rawDocument },
+ title: { value: rawTitle },
+ },
+ } = item;
+ const content = parseDocumentationContent({
+ rawDocument: rawDocument,
+ rawTitle: rawTitle,
+ path: item.path,
+ query,
+ });
+ const containerRef = React.useRef(null);
+ useEffect(() => {
+ scrollToMatchedValue();
+ }, [content]);
- return content ? (
-
- ) : null;
- } catch (e) {
- return null;
- }
-};
+ const scrollToMatchedValue = () => {
+ const root = containerRef.current;
+ if (!root) return;
+ const list = root.getElementsByTagName(algoliaHighlightTag);
+ if (list.length) {
+ const bestMatch = Array.from(list).reduce((accumulator, currentValue) => {
+ if (
+ currentValue.textContent &&
+ accumulator.textContent &&
+ currentValue.textContent.length > accumulator.textContent.length
+ )
+ return currentValue;
+ return accumulator;
+ }, list[0]);
+
+ bestMatch.scrollIntoView();
+ } else {
+ setTimeout(() => {
+ root.firstElementChild?.scrollIntoView();
+ }, 0);
+ }
+ };
+
+ return content ? (
+
+ ) : null;
+}
const StyledHitEnterMessageContainer = styled.div`
background: ${(props) =>
@@ -142,28 +172,12 @@ const descriptionByType = {
function Description(props: Props) {
const { activeItem, activeItemType } = props;
- const containerRef = React.useRef(null);
-
- const onScroll = useCallback((e: React.UIEvent) => {
- if (
- props.scrollPositionRef?.current ||
- props.scrollPositionRef?.current === 0
- ) {
- props.scrollPositionRef.current = (e.target as HTMLDivElement).scrollTop;
- }
- }, []);
-
- useEffect(() => {
- if (containerRef.current) {
- containerRef.current.scrollTop = props.scrollPositionRef?.current;
- }
- }, [containerRef.current, activeItem]);
if (!activeItemType || !activeItem) return null;
const Component = descriptionByType[activeItemType];
return (
-
+
);
diff --git a/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx
new file mode 100644
index 0000000000..ad5dfb33b4
--- /dev/null
+++ b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+import styled from "styled-components";
+
+const Wrapper = styled.div`
+ span {
+ color: white;
+ font-variant: all-small-caps;
+ font-size: ${(props) => props.theme.fontSizes[2]}px;
+ margin-right: ${(props) => props.theme.spaces[1]}px;
+ }
+ div {
+ margin-right: ${(props) => props.theme.spaces[7]}px;
+ font-size: ${(props) => props.theme.fontSizes[2]}px;
+ text-align: center;
+ }
+ color: ${(props) => props.theme.colors.globalSearch.searchItemText};
+ padding: ${(props) => props.theme.spaces[1]}px
+ ${(props) => props.theme.spaces[6]}px;
+ display: flex;
+ flex-direction: row;
+`;
+
+const FOOTER_INFO = [
+ {
+ action: "\u2191\u2193",
+ description: "Select",
+ },
+ {
+ action: "ENTER",
+ description: "Open",
+ },
+];
+
+function Footer() {
+ return (
+
+ {FOOTER_INFO.map((info) => {
+ return (
+
+ {info.action}
+ {info.description}
+
+ );
+ })}
+
+ );
+}
+
+export default Footer;
diff --git a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx
index f1af2aa4fc..84766c4720 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx
@@ -4,6 +4,7 @@ import NoSearchDataImage from "assets/images/no_search_data.png";
import { NO_SEARCH_DATA_TEXT } from "constants/messages";
import { getTypographyByKey } from "constants/DefaultTheme";
import { ReactComponent as DiscordIcon } from "assets/icons/help/discord.svg";
+import AnalyticsUtil from "utils/AnalyticsUtil";
const Container = styled.div`
display: flex;
@@ -49,6 +50,7 @@ function ResultsNotFound() {
className="discord-link"
onClick={() => {
window.open("https://discord.gg/rBTTVJp", "_blank");
+ AnalyticsUtil.logEvent("DISCORD_LINK_CLICK");
}}
>
diff --git a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx
index 5da83a7c9e..42a7cb03e3 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx
@@ -25,6 +25,7 @@ import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
import { AppState } from "reducers";
import { keyBy, noop } from "lodash";
import { getPageList } from "selectors/editorSelectors";
+import { PluginType } from "entities/Action";
const DocumentIcon = HelpIcons.DOCUMENT;
@@ -158,10 +159,13 @@ function ActionItem(props: {
return state.entities.plugins.list;
});
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
- const icon = getActionConfig(pluginType)?.getIcon(
- item.config,
- pluginGroups[item.config.datasource.pluginId],
- );
+ const icon =
+ pluginType === PluginType.API
+ ? getActionConfig(pluginType)?.icon
+ : getActionConfig(pluginType)?.getIcon(
+ item.config,
+ pluginGroups[item.config.datasource.pluginId],
+ );
let title = getItemTitle(item);
const pageName = usePageName(config.pageId);
@@ -293,9 +297,7 @@ function SearchItemComponent(props: ItemProps) {
itemType !== SEARCH_ITEM_TYPES.placeholder
) {
setActiveItemIndex(index);
- if (itemType !== SEARCH_ITEM_TYPES.document) {
- searchContext?.handleItemLinkClick(item, "SEARCH_ITEM");
- }
+ searchContext?.handleItemLinkClick(item, "SEARCH_ITEM");
}
}}
ref={itemRef}
diff --git a/app/client/src/components/editorComponents/GlobalSearch/index.tsx b/app/client/src/components/editorComponents/GlobalSearch/index.tsx
index 603cf995e7..07577ddb08 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/index.tsx
+++ b/app/client/src/components/editorComponents/GlobalSearch/index.tsx
@@ -46,6 +46,7 @@ import { keyBy, noop } from "lodash";
import EntitiesIcon from "assets/icons/ads/entities.svg";
import DocsIcon from "assets/icons/ads/docs.svg";
import RecentIcon from "assets/icons/ads/recent.svg";
+import Footer from "./Footer";
const StyledContainer = styled.div`
width: 750px;
@@ -63,12 +64,8 @@ const StyledContainer = styled.div`
${algoliaHighlightTag},
& .ais-Highlight-highlighted,
& .search-highlighted {
- background: unset;
- color: ${(props) => props.theme.colors.globalSearch.searchItemHighlight};
+ background-color: #6287b0;
font-style: normal;
- text-decoration: underline;
- text-decoration-color: ${(props) =>
- props.theme.colors.globalSearch.highlightedTextUnderline};
}
`;
@@ -208,7 +205,7 @@ function GlobalSearch() {
);
}, [pages, query]);
- const recentsSectionTitle = getSectionTitle("Recents", RecentIcon);
+ const recentsSectionTitle = getSectionTitle("Recent Entities", RecentIcon);
const docsSectionTitle = getSectionTitle("Documentation Links", DocsIcon);
const entitiesSectionTitle = getSectionTitle("Entities", EntitiesIcon);
@@ -401,6 +398,7 @@ function GlobalSearch() {
)}
+ {!query && }
diff --git a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts
index bc7fa299e8..2f1e3be00f 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts
+++ b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.test.ts
@@ -37,6 +37,7 @@ describe("parseDocumentationContent", () => {
rawTitle: sampleTitleResponse,
rawDocument: sampleDocumentResponse,
path: "master/security",
+ query: "Security",
};
const result = parseDocumentationContent(sampleItem);
expect(result).toStrictEqual(expectedResult);
diff --git a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts
index 756fc62cf2..1a3177b4f4 100644
--- a/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts
+++ b/app/client/src/components/editorComponents/GlobalSearch/parseDocumentationContent.ts
@@ -66,9 +66,29 @@ const updateDocumentDescriptionTitle = (documentObj: any, item: any) => {
// append documentation button after title:
const ctaElement = getDocumentationCTA(path) as Node;
firstChild.appendChild(ctaElement);
+
+ removeBadHighlights(firstChild, item.query);
}
};
+// Remove highlights if they don't match well
+const removeBadHighlights = (node: HTMLElement | Document, query: string) => {
+ Array.from(
+ node.querySelectorAll(algoliaHighlightTag) as NodeListOf,
+ ).forEach((match) => {
+ // If the length of the highlighted node is less than the 1/2 length of
+ // the query we remove the highlighted tag.
+ // E.g query: "store any" won't highlight "any" nodes
+ if (
+ match.textContent &&
+ match.textContent.length < Math.floor(query.length / 2)
+ ) {
+ const innerHtml = match.innerHTML;
+ match.replaceWith(innerHtml);
+ }
+ });
+};
+
const replaceHintTagsWithCode = (text: string) => {
let result = text.replace(/{% hint .*?%}/, "```");
result = result.replace(/{% endhint .*?%}/, "```");
@@ -78,10 +98,14 @@ const replaceHintTagsWithCode = (text: string) => {
const parseDocumentationContent = (item: any): string | undefined => {
try {
- const { rawDocument } = item;
+ const { query, rawDocument } = item;
let value = rawDocument;
if (!value) return;
+ const aisTag = new RegExp(
+ `<${algoliaHighlightTag}>|</${algoliaHighlightTag}>`,
+ "g",
+ );
value = stripMarkdown(value);
value = replaceHintTagsWithCode(value);
@@ -91,10 +115,7 @@ const parseDocumentationContent = (item: any): string | undefined => {
const documentObj = domparser.parseFromString(parsedDocument, "text/html");
// remove algolia highlight within code sections
- const aisTag = new RegExp(
- `<${algoliaHighlightTag}>|</${algoliaHighlightTag}>`,
- "g",
- );
+
Array.from(documentObj.querySelectorAll("code")).forEach((match) => {
match.innerHTML = match.innerHTML.replace(aisTag, "");
});
@@ -117,6 +138,73 @@ const parseDocumentationContent = (item: any): string | undefined => {
} catch (e) {}
});
+ // Combine adjacent highlighted nodes into a single one
+ let adjacentMatches: string[] = [];
+ const letterRegex = /[a-zA-Z]/g;
+ // Get highlighted tags
+ Array.from(documentObj.querySelectorAll(algoliaHighlightTag)).forEach(
+ (match) => {
+ // If adjacent element is an `algoliaHighlightTag` and the next
+ // text content does not include a letter
+ if (
+ match.nextSibling?.textContent &&
+ !letterRegex.test(match.nextSibling?.textContent) &&
+ match.nextElementSibling?.nodeName.toLowerCase() ===
+ algoliaHighlightTag &&
+ !adjacentMatches.length &&
+ match.textContent
+ ) {
+ // Store the matched word and the text content
+ adjacentMatches = adjacentMatches.concat([
+ match.textContent,
+ match.nextSibling?.textContent,
+ ]);
+ // Remove the text node as we have stored this above
+ match.nextSibling.remove();
+ // Remove the node as we have it's text content
+ match.remove();
+ }
+ // If this is part of a group of highligted words
+ else if (adjacentMatches.length && match.textContent) {
+ // store it's text content
+ adjacentMatches.push(match.textContent);
+
+ // If there are more adjacent highlight nodes ahead
+ if (
+ match.nextElementSibling?.nodeName.toLowerCase() ===
+ algoliaHighlightTag &&
+ match.nextSibling?.textContent &&
+ !letterRegex.test(match.nextSibling?.textContent)
+ ) {
+ // store the text content
+ adjacentMatches.push(match.nextSibling?.textContent);
+ match.nextSibling.remove();
+ // delete the node
+ match.remove();
+ }
+ // We are at the last node of the group
+ else {
+ // Create a algoliaHighlightTag element and add the
+ // grouped text
+ const highlightTag = document.createElement(algoliaHighlightTag);
+ highlightTag.innerText = adjacentMatches.join("");
+
+ // Simply replace(or remove) the last node with the
+ // newly created algoliaHighlightTag
+ match.replaceWith(highlightTag);
+ // Reset
+ adjacentMatches = [];
+ }
+ } else {
+ // Reset adjacentMatches. We are no longer part of a group of adjacent nodes.
+ // Start afresh
+ adjacentMatches = [];
+ }
+ },
+ );
+
+ // Remove highlight for nodes that don't match well
+ removeBadHighlights(documentObj, query);
// update description title
updateDocumentDescriptionTitle(documentObj, item);
diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx
index 1b401fdbe2..41763f349a 100644
--- a/app/client/src/components/editorComponents/ResizableComponent.tsx
+++ b/app/client/src/components/editorComponents/ResizableComponent.tsx
@@ -38,6 +38,7 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import { scrollElementIntoParentCanvasView } from "utils/helpers";
import { getNearestParentCanvas } from "utils/generators";
import { getOccupiedSpaces } from "selectors/editorSelectors";
+import { commentModeSelector } from "selectors/commentsSelectors";
export type ResizableComponentProps = WidgetProps & {
paddingOffset: number;
@@ -55,6 +56,8 @@ export const ResizableComponent = memo(function ResizableComponent(
DropTargetContext,
);
+ const isCommentMode = useSelector(commentModeSelector);
+
const showPropertyPane = useShowPropertyPane();
const { selectWidget } = useWidgetSelection();
const { setIsResizing } = useWidgetDragResize();
@@ -258,11 +261,14 @@ export const ResizableComponent = memo(function ResizableComponent(
});
};
+ const isEnabled =
+ !isDragging && isWidgetFocused && !props.resizeDisabled && !isCommentMode;
+
return (
`
- position: absolute;
- right: -155px;
- top: 7px;
display: flex;
align-items: center;
cursor: pointer;
+ margin-left: 10px;
+ height: 100%;
+ min-height: 37px;
.${Classes.TEXT} {
color: ${(props) => props.theme.colors.text.heading};
}
diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
index 24c333c964..e9f186d150 100644
--- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
+++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
@@ -63,7 +63,6 @@ type Props = EditorProps &
const DatasourceContainer = styled.div`
display: flex;
position: relative;
- width: calc(100% - 155px);
`;
const hintContainerStyles: React.CSSProperties = {
@@ -82,14 +81,21 @@ const datasourceNameStyles: React.CSSProperties = {
fontSize: "14px",
fontWeight: 500,
color: "#090707",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
};
const datasourceInfoStyles: React.CSSProperties = {
color: "#4B4848",
fontWeight: 400,
fontSize: "12px",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
};
const italicInfoStyles = {
...datasourceInfoStyles,
+ flexShrink: 0,
fontStyle: "italic",
};
@@ -299,7 +305,7 @@ class EmbeddedDatasourcePathComponent extends React.Component {
return (
- {datasource && !("id" in datasource) ? (
+ {displayValue && datasource && !("id" in datasource) ? (
) : datasource && "id" in datasource ? (
+
{label} {isRequired && "*"}
diff --git a/app/client/src/components/formControls/FieldArrayControl.tsx b/app/client/src/components/formControls/FieldArrayControl.tsx
new file mode 100644
index 0000000000..6dba80f6b7
--- /dev/null
+++ b/app/client/src/components/formControls/FieldArrayControl.tsx
@@ -0,0 +1,106 @@
+import React, { useEffect } from "react";
+import FormControl from "pages/Editor/FormControl";
+import Text, { TextType } from "components/ads/Text";
+import Icon, { IconSize } from "components/ads/Icon";
+import { Classes } from "components/ads/common";
+import styled from "styled-components";
+import { FieldArray } from "redux-form";
+import FormLabel from "components/editorComponents/FormLabel";
+import { ControlProps } from "./BaseControl";
+
+const CenteredIcon = styled(Icon)`
+ margin-top: 25px;
+ &.hide {
+ opacity: 0;
+ pointer-events: none;
+ }
+`;
+
+const PrimaryBox = styled.div`
+ display: flex;
+ flex-direction: column;
+ border: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
+ padding: 10px;
+ border-radius: 5px;
+`;
+
+const SecondaryBox = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px;
+`;
+
+const AddMoreAction = styled.div`
+ width: fit-content;
+ cursor: pointer;
+ display: flex;
+ margin-top: 16px;
+ .${Classes.TEXT} {
+ margin-left: 8px;
+ color: #03b365;
+ }
+`;
+
+function NestedComponents(props: any) {
+ useEffect(() => {
+ if (props.fields.length < 1) {
+ props.fields.push({});
+ }
+ }, [props.fields.length]);
+ return (
+
+ {props.fields &&
+ props.fields.length > 0 &&
+ props.fields.map((field: string, index: number) => {
+ return (
+
+ {props.schema.map((sch: any, idx: number) => {
+ sch = {
+ ...sch,
+ configProperty: `${field}.${sch.key}`,
+ };
+ return (
+
+ );
+ })}
+ {
+ e.stopPropagation();
+ props.fields.remove(index);
+ }}
+ size={IconSize.XXL}
+ />
+
+ );
+ })}
+ props.fields.push({})}>
+ {/*Hardcoded label to be removed */}
+ + Add Condition (And)
+
+
+ );
+}
+
+export default function FieldArrayControl(props: FieldArrayControlProps) {
+ const { configProperty, formName, label, schema } = props;
+ return (
+ <>
+
{label}
+
+ >
+ );
+}
+
+export type FieldArrayControlProps = ControlProps;
diff --git a/app/client/src/components/propertyControls/CustomFusionChartControl.tsx b/app/client/src/components/propertyControls/CustomFusionChartControl.tsx
index 6b741a1330..bcf39b84f7 100644
--- a/app/client/src/components/propertyControls/CustomFusionChartControl.tsx
+++ b/app/client/src/components/propertyControls/CustomFusionChartControl.tsx
@@ -3,7 +3,7 @@ import InputTextControl, { InputText } from "./InputTextControl";
class CustomFusionChartControl extends InputTextControl {
render() {
- const expected = "{\n type: string,\n dataSource: Object\n}";
+ const expected = '{\n "type": string,\n "dataSource": Object\n}';
const { dataTreePath, label, placeholderText, propertyValue } = this.props;
return (
(
ShowUploadedFile(data)}
+ />
+);
+
+export const withJsonInputType = () => (
+ ShowUploadedFile(data)}
/>
diff --git a/app/client/src/configs/index.ts b/app/client/src/configs/index.ts
index fa580394d0..a50565f606 100644
--- a/app/client/src/configs/index.ts
+++ b/app/client/src/configs/index.ts
@@ -42,6 +42,8 @@ export type INJECTED_CONFIGS = {
mailEnabled: boolean;
disableTelemetry: boolean;
cloudServicesBaseUrl: string;
+ googleRecaptchaSiteKey: string;
+ onboardingFormEnabled: boolean;
};
declare global {
interface Window {
@@ -115,6 +117,9 @@ const getConfigsFromEnvVars = (): INJECTED_CONFIGS => {
: false,
disableTelemetry: true,
cloudServicesBaseUrl: process.env.REACT_APP_CLOUD_SERVICES_BASE_URL || "",
+ googleRecaptchaSiteKey:
+ process.env.REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY || "",
+ onboardingFormEnabled: !!process.env.REACT_APP_SHOW_ONBOARDING_FORM,
};
};
@@ -165,6 +170,11 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => {
);
const google = getConfig(ENV_CONFIG.google, APPSMITH_FEATURE_CONFIGS.google);
+ const googleRecaptchaSiteKey = getConfig(
+ ENV_CONFIG.googleRecaptchaSiteKey,
+ APPSMITH_FEATURE_CONFIGS.googleRecaptchaSiteKey,
+ );
+
// As the following shows, the config variables can be set using a combination
// of env variables and injected configs
const smartLook = getConfig(
@@ -243,6 +253,10 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => {
enabled: google.enabled,
apiKey: google.value,
},
+ googleRecaptchaSiteKey: {
+ enabled: googleRecaptchaSiteKey.enabled,
+ apiKey: googleRecaptchaSiteKey.value,
+ },
enableRapidAPI:
ENV_CONFIG.enableRapidAPI || APPSMITH_FEATURE_CONFIGS.enableRapidAPI,
enableGithubOAuth:
@@ -269,5 +283,6 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => {
cloudServicesBaseUrl:
ENV_CONFIG.cloudServicesBaseUrl ||
APPSMITH_FEATURE_CONFIGS.cloudServicesBaseUrl,
+ onboardingFormEnabled: ENV_CONFIG.onboardingFormEnabled,
};
};
diff --git a/app/client/src/configs/types.ts b/app/client/src/configs/types.ts
index 7a73abd169..41fde2417f 100644
--- a/app/client/src/configs/types.ts
+++ b/app/client/src/configs/types.ts
@@ -72,4 +72,10 @@ export type AppsmithUIConfigs = {
commentsTestModeEnabled: boolean;
cloudServicesBaseUrl: string;
+
+ googleRecaptchaSiteKey: {
+ enabled: boolean;
+ apiKey: string;
+ };
+ onboardingFormEnabled: boolean;
};
diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx
index 7e63eb2edc..6f8dc9c1dc 100644
--- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx
+++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx
@@ -56,6 +56,7 @@ export enum EventType {
ON_HOVER = "ON_HOVER",
ON_TOGGLE = "ON_TOGGLE",
ON_LOAD = "ON_LOAD",
+ ON_MODAL_CLOSE = "ON_MODAL_CLOSE",
ON_TEXT_CHANGE = "ON_TEXT_CHANGE",
ON_SUBMIT = "ON_SUBMIT",
ON_CHECK_CHANGE = "ON_CHECK_CHANGE",
diff --git a/app/client/src/constants/AppsmithActionConstants/formConfig/ApiSettingsConfig.ts b/app/client/src/constants/AppsmithActionConstants/formConfig/ApiSettingsConfig.ts
index 9924dc4954..e8bd302a02 100644
--- a/app/client/src/constants/AppsmithActionConstants/formConfig/ApiSettingsConfig.ts
+++ b/app/client/src/constants/AppsmithActionConstants/formConfig/ApiSettingsConfig.ts
@@ -23,12 +23,12 @@ export default [
"Encode query params for all APIs. Also encode form body when Content-Type header is set to x-www-form-encoded",
},
{
- label: "[Beta] Smart JSON Substitution",
+ label: "Smart JSON Substitution",
configProperty: "actionConfiguration.pluginSpecifiedTemplates[0].value",
controlType: "CHECKBOX",
info:
"Turning on this property fixes the JSON substitution of bindings in API body by adding/removing quotes intelligently and reduces developer errors",
- initialValue: false,
+ initialValue: true,
},
// {
// label: "Cache response",
diff --git a/app/client/src/constants/CustomChartConstants.ts b/app/client/src/constants/CustomChartConstants.ts
new file mode 100644
index 0000000000..fa11c207e9
--- /dev/null
+++ b/app/client/src/constants/CustomChartConstants.ts
@@ -0,0 +1,64 @@
+export const CUSTOM_CHART_TYPES = [
+ "column2d",
+ "column3d",
+ "line",
+ "area",
+ "bar2d",
+ "bar3d",
+ "pie2d",
+ "pie3d",
+ "doughnut2d",
+ "doughnut3d",
+ "pareto2d",
+ "pareto3d",
+ "scrollcombidy2d",
+ "scrollcombi2d",
+ "scrollstackedcolumn2d",
+ "scrollmsstackedcolumn2d",
+ "scrollmsstackedcolumn2dlinedy",
+ "scrollstackedbar2d",
+ "scrollarea2d",
+ "scrollline2d",
+ "scrollcolumn2d",
+ "scrollbar2d",
+ "bubble",
+ "scatter",
+ "msstackedcolumn2d",
+ "stackedarea2d",
+ "stackedbar3d",
+ "stackedbar2d",
+ "stackedcolumn3d",
+ "stackedcolumn2d",
+ "msstackedcolumn2dlinedy",
+ "stackedcolumn3dlinedy",
+ "mscolumn3dlinedy",
+ "mscombidy2d",
+ "mscombidy3d",
+ "stackedcolumn3dline",
+ "stackedcolumn2dline",
+ "mscolumnline3d",
+ "mscombi3d",
+ "mscombi2d",
+ "marimekko",
+ "msarea",
+ "msbar3d",
+ "msbar2d",
+ "msline",
+ "mscolumn3d",
+ "mscolumn2d",
+ "spline",
+ "splinearea",
+ "msspline",
+ "mssplinedy",
+ "mssplinearea",
+ "stackedcolumn2dlinedy",
+ "stackedarea2dlinedy",
+];
+
+export const CUSTOM_CHART_DEFAULT_PARSED = {
+ type: "",
+ dataSource: {
+ chart: {},
+ data: [],
+ },
+};
diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx
index aeecdd5164..139b66eb84 100644
--- a/app/client/src/constants/DefaultTheme.tsx
+++ b/app/client/src/constants/DefaultTheme.tsx
@@ -530,6 +530,7 @@ type buttonVariant = {
};
type ColorType = {
+ overlayColor: string;
button: {
disabledText: ShadeColor;
};
@@ -990,6 +991,9 @@ type ColorType = {
label: string;
entity: string;
entityLink: string;
+ inspectElement: {
+ color: string;
+ };
floatingButton: {
background: string;
color: string;
@@ -1022,6 +1026,16 @@ type ColorType = {
mentionsInput: Record;
showcaseCarousel: Record;
displayImageUpload: Record;
+ notifications: Record;
+};
+
+const notifications = {
+ time: "#858282",
+ listHeaderTitle: "#090707",
+ markAllAsReadButtonBackground: "#f0f0f0",
+ markAllAsReadButtonText: "#716E6E",
+ unreadIndicator: "#F86A2B",
+ bellIndicator: "#E22C2C",
};
const displayImageUpload = {
@@ -1161,9 +1175,12 @@ const mentionsInput = {
focusedItemBackground: "#cee4e5",
itemBorderBottom: "#cee4e5",
mentionBackground: "#cee4e5",
+ mentionsInviteBtnPlusIcon: "#6A86CE",
};
export const dark: ColorType = {
+ overlayColor: "#090707cc",
+ notifications,
displayImageUpload,
showcaseCarousel,
mentionSuggestion,
@@ -1594,6 +1611,9 @@ export const dark: ColorType = {
errorCount: "#F22B2B",
noErrorCount: "#03B365",
},
+ inspectElement: {
+ color: "#D4D4D4",
+ },
blankState: {
color: "#D4D4D4",
shortcut: "#D4D4D4",
@@ -1613,6 +1633,8 @@ export const dark: ColorType = {
};
export const light: ColorType = {
+ overlayColor: "#090707cc",
+ notifications,
displayImageUpload,
showcaseCarousel,
mentionSuggestion,
@@ -2044,8 +2066,11 @@ export const light: ColorType = {
errorCount: "#F22B2B",
noErrorCount: "#03B365",
},
+ inspectElement: {
+ color: "#090707",
+ },
blankState: {
- color: "#716e6e",
+ color: "#090707",
shortcut: "black",
},
info: {
diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx
index b8b4711c9a..06cf80a084 100644
--- a/app/client/src/constants/ReduxActionConstants.tsx
+++ b/app/client/src/constants/ReduxActionConstants.tsx
@@ -9,6 +9,14 @@ export const ReduxSagaChannels: { [key: string]: string } = {
};
export const ReduxActionTypes: { [key: string]: string } = {
+ MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST:
+ "MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST",
+ MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS:
+ "MARK_ALL_NOTIFICATIONS_AS_READ_SUCCESS",
+ SET_IS_NOTIFICATIONS_LIST_VISIBLE: "SET_IS_NOTIFICATIONS_LIST_VISIBLE",
+ NEW_NOTIFICATION_EVENT: "NEW_NOTIFICATION_EVENT",
+ FETCH_NOTIFICATIONS_REQUEST: "FETCH_NOTIFICATIONS_REQUEST",
+ FETCH_NOTIFICATIONS_SUCCESS: "FETCH_NOTIFICATIONS_SUCCESS",
SET_SHOW_APP_INVITE_USERS_MODAL: "SET_SHOW_APP_INVITE_USERS_MODAL",
UPDATE_COMMENT_EVENT: "UPDATE_COMMENT_EVENT",
ADD_COMMENT_REACTION: "ADD_COMMENT_REACTION",
@@ -213,7 +221,6 @@ export const ReduxActionTypes: { [key: string]: string } = {
PUBLISH_APPLICATION_SUCCESS: "PUBLISH_APPLICATION_SUCCESS",
CHANGE_APPVIEW_ACCESS_INIT: "CHANGE_APPVIEW_ACCESS_INIT",
CHANGE_APPVIEW_ACCESS_SUCCESS: "CHANGE_APPVIEW_ACCESS_SUCCESS",
- CHANGE_APPVIEW_ACCESS_ERROR: "CHANGE_APPVIEW_ACCESS_ERROR",
CREATE_PAGE_INIT: "CREATE_PAGE_INIT",
CREATE_PAGE_SUCCESS: "CREATE_PAGE_SUCCESS",
FETCH_PAGE_LIST_INIT: "FETCH_PAGE_LIST_INIT",
@@ -418,6 +425,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
CURRENT_APPLICATION_LAYOUT_UPDATE: "CURRENT_APPLICATION_LAYOUT_UPDATE",
FORK_APPLICATION_INIT: "FORK_APPLICATION_INIT",
FORK_APPLICATION_SUCCESS: "FORK_APPLICATION_SUCCESS",
+ IMPORT_APPLICATION_INIT: "IMPORT_APPLICATION_INIT",
+ IMPORT_APPLICATION_SUCCESS: "IMPORT_APPLICATION_SUCCESS",
SET_WIDGET_LOADING: "SET_WIDGET_LOADING",
SET_GLOBAL_SEARCH_QUERY: "SET_GLOBAL_SEARCH_QUERY",
TOGGLE_SHOW_GLOBAL_SEARCH_MODAL: "TOGGLE_SHOW_GLOBAL_SEARCH_MODAL",
@@ -525,6 +534,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
SAVE_ACTION_NAME_ERROR: "SAVE_ACTION_NAME_ERROR",
FETCH_USER_APPLICATIONS_ORGS_ERROR: "FETCH_USER_APPLICATIONS_ORGS_ERROR",
FORK_APPLICATION_ERROR: "FORK_APPLICATION_ERROR",
+ IMPORT_APPLICATION_ERROR: "IMPORT_APPLICATION_ERROR",
FETCH_ALL_USERS_ERROR: "FETCH_ALL_USERS_ERROR",
FETCH_ALL_ROLES_ERROR: "FETCH_ALL_ROLES_ERROR",
UPDATE_USER_DETAILS_ERROR: "UPDATE_USER_DETAILS_ERROR",
@@ -538,6 +548,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
WIDGET_ADD_CHILDREN_ERROR: "WIDGET_ADD_CHILDREN_ERROR",
FAILED_CORRECTING_BINDING_PATHS: "FAILED_CORRECTING_BINDING_PATHS",
DELETE_ORG_USER_ERROR: "DELETE_ORG_USER_ERROR",
+ CHANGE_APPVIEW_ACCESS_ERROR: "CHANGE_APPVIEW_ACCESS_ERROR",
};
export const ReduxFormActionTypes: { [key: string]: string } = {
diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx
index a1b2807a35..ba6c4c711f 100644
--- a/app/client/src/constants/WidgetConstants.tsx
+++ b/app/client/src/constants/WidgetConstants.tsx
@@ -96,7 +96,7 @@ export const layoutConfigurations: LayoutConfigurations = {
FLUID: { minWidth: -1, maxWidth: -1 },
};
-export const LATEST_PAGE_VERSION = 23;
+export const LATEST_PAGE_VERSION = 24;
export const GridDefaults = {
DEFAULT_CELL_SIZE: 1,
diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts
index 9084eb5eaf..717f38b2e9 100644
--- a/app/client/src/constants/messages.ts
+++ b/app/client/src/constants/messages.ts
@@ -202,7 +202,7 @@ export const GOOGLE_RECAPTCHA_DOMAIN_ERROR = () =>
export const SERVER_API_TIMEOUT_ERROR = () =>
`Appsmith server is taking too long to respond. Please try again after some time`;
export const DEFAULT_ERROR_MESSAGE = () => `There was an unexpected error`;
-
+export const REMOVE_FILE_TOOL_TIP = () => "Remove Upload";
export const ERROR_FILE_TOO_LARGE = (fileSize: string) =>
`File size should be less than ${fileSize}!`;
export const ERROR_DATEPICKER_MIN_DATE = () =>
@@ -317,6 +317,8 @@ export const FULL_NAME = () => "Full Name";
export const DISPLAY_NAME = () => "Display Name";
export const EMAIL_ADDRESS = () => "Email Address";
export const FIRST_AND_LAST_NAME = () => "First and last name";
+export const MARK_ALL_AS_READ = () => "Mark all as read";
+export const INVITE_A_NEW_USER = () => "Invite a new user";
// Showcase Carousel
export const NEXT = () => "NEXT";
@@ -327,5 +329,16 @@ export const CLICK_ON = () => "🙌 Click on ";
export const PRESS = () => "🎉 Press ";
export const OPEN_THE_DEBUGGER = () => " to open the debugger";
export const NO_LOGS = () => "No logs to show";
+export const DEBUGGER_ERRORS = () => "Errors";
+export const DEBUGGER_LOGS = () => "Logs";
+export const INSPECT_ENTITY = () => "Inspect Entity";
+export const INSPECT_ENTITY_BLANK_STATE = () => "Select an entity to inspect";
export const TROUBLESHOOT_ISSUE = () => "Troubleshoot issue";
+
+// Import/Export Application features
+export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import Application";
+
+export const DELETE_CONFIRMATION_MODAL_TITLE = () => `Are you sure?`;
+export const DELETE_CONFIRMATION_MODAL_SUBTITLE = (name?: string | null) =>
+ `You want to remove ${name} from this organization`;
diff --git a/app/client/src/entities/AppsmithConsole/logtype.ts b/app/client/src/entities/AppsmithConsole/logtype.ts
index dfbe511f23..8fd1658e13 100644
--- a/app/client/src/entities/AppsmithConsole/logtype.ts
+++ b/app/client/src/entities/AppsmithConsole/logtype.ts
@@ -5,6 +5,7 @@ enum LOG_TYPE {
ACTION_EXECUTION_SUCCESS,
ENTITY_DELETED,
EVAL_ERROR,
+ ACTION_UPDATE,
}
export default LOG_TYPE;
diff --git a/app/client/src/entities/Comments/CommentsInterfaces.ts b/app/client/src/entities/Comments/CommentsInterfaces.ts
index be5a8f07df..18f30eff35 100644
--- a/app/client/src/entities/Comments/CommentsInterfaces.ts
+++ b/app/client/src/entities/Comments/CommentsInterfaces.ts
@@ -19,6 +19,7 @@ export type CreateCommentRequest = {
export type CreateCommentThreadRequest = {
applicationId: string;
+ pageId: string;
refId: string; // could be an id to refer any parent based on parent type
tabId?: string;
position: { top: number; left: number }; // used as a percentage value
diff --git a/app/client/src/entities/DataTree/dataTreeAction.ts b/app/client/src/entities/DataTree/dataTreeAction.ts
index da13794861..d108491405 100644
--- a/app/client/src/entities/DataTree/dataTreeAction.ts
+++ b/app/client/src/entities/DataTree/dataTreeAction.ts
@@ -45,5 +45,6 @@ export const generateDataTreeAction = (
isLoading: action.isLoading,
bindingPaths: getBindingPathsOfAction(action.config, editorConfig),
dependencyMap,
+ logBlackList: {},
};
};
diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts
index ea142ce8ae..a1831c2632 100644
--- a/app/client/src/entities/DataTree/dataTreeFactory.ts
+++ b/app/client/src/entities/DataTree/dataTreeFactory.ts
@@ -56,6 +56,8 @@ export interface DataTreeAction
bindingPaths: Record;
ENTITY_TYPE: ENTITY_TYPE.ACTION;
dependencyMap: DependencyMap;
+ jsErrorMessages?: Record;
+ logBlackList: Record;
}
export interface DataTreeWidget extends WidgetProps {
@@ -63,6 +65,7 @@ export interface DataTreeWidget extends WidgetProps {
triggerPaths: Record;
validationPaths: Record;
ENTITY_TYPE: ENTITY_TYPE.WIDGET;
+ logBlackList: Record;
}
export interface DataTreeAppsmith extends Omit {
diff --git a/app/client/src/entities/DataTree/dataTreeWidget.test.ts b/app/client/src/entities/DataTree/dataTreeWidget.test.ts
index 5ee2df1219..38dcb236f5 100644
--- a/app/client/src/entities/DataTree/dataTreeWidget.test.ts
+++ b/app/client/src/entities/DataTree/dataTreeWidget.test.ts
@@ -221,6 +221,10 @@ describe("generateDataTreeWidget", () => {
key: "value",
},
],
+ logBlackList: {
+ isValid: true,
+ value: true,
+ },
value: "{{Input1.text}}",
isDirty: true,
isFocused: false,
diff --git a/app/client/src/entities/DataTree/dataTreeWidget.ts b/app/client/src/entities/DataTree/dataTreeWidget.ts
index fa77cfa3fb..86ca7318cb 100644
--- a/app/client/src/entities/DataTree/dataTreeWidget.ts
+++ b/app/client/src/entities/DataTree/dataTreeWidget.ts
@@ -45,6 +45,10 @@ export const generateDataTreeWidget = (
unInitializedDefaultProps[propertyName] = undefined;
}
});
+ const blockedDerivedProps: Record = {};
+ Object.keys(derivedProps).forEach((propertyName) => {
+ blockedDerivedProps[propertyName] = true;
+ });
const {
bindingPaths,
triggerPaths,
@@ -62,6 +66,10 @@ export const generateDataTreeWidget = (
...derivedProps,
...unInitializedDefaultProps,
dynamicBindingPathList,
+ logBlackList: {
+ ...widget.logBlackList,
+ ...blockedDerivedProps,
+ },
bindingPaths,
triggerPaths,
validationPaths,
diff --git a/app/client/src/entities/Notification.ts b/app/client/src/entities/Notification.ts
new file mode 100644
index 0000000000..8e021b66c4
--- /dev/null
+++ b/app/client/src/entities/Notification.ts
@@ -0,0 +1,12 @@
+export type AppsmithNotification = {
+ id: string;
+ _id?: string;
+ type: string;
+ new: boolean;
+ [key: string]: any;
+};
+
+export enum NotificationTypes {
+ CommentNotification = "CommentNotification",
+ CommentThreadNotification = "CommentThreadNotification",
+}
diff --git a/app/client/src/globalStyles/popover.ts b/app/client/src/globalStyles/popover.ts
index 64946b663d..983df404f0 100644
--- a/app/client/src/globalStyles/popover.ts
+++ b/app/client/src/globalStyles/popover.ts
@@ -18,4 +18,7 @@ export const PopoverStyles = createGlobalStyle`
min-width: 233px !important ;
}
}
+ .comments-onboarding-carousel .${Classes.OVERLAY_CONTENT} {
+ filter: drop-shadow(0px 6px 20px rgba(0, 0, 0, 0.15));
+ }
`;
diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx
index 09b9329784..adac756b97 100755
--- a/app/client/src/index.tsx
+++ b/app/client/src/index.tsx
@@ -26,11 +26,7 @@ import AppErrorBoundary from "./AppErrorBoundry";
import GlobalStyles from "globalStyles";
appInitializer();
-import useRemoveSignUpCompleteParam from "utils/hooks/useRemoveSignUpCompleteParam";
-
function App() {
- useRemoveSignUpCompleteParam();
-
return (
diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx
index a189560f6b..7099a3dd8d 100644
--- a/app/client/src/mockResponses/WidgetConfigResponse.tsx
+++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx
@@ -148,7 +148,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
},
TABLE_WIDGET: {
rows: 7 * GRID_DENSITY_MIGRATION_V1,
- columns: 8 * GRID_DENSITY_MIGRATION_V1,
+ columns: 9 * GRID_DENSITY_MIGRATION_V1,
label: "Data",
widgetName: "Table",
searchKey: "",
@@ -263,6 +263,11 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
step: 62,
status: 75,
},
+ isVisibleSearch: true,
+ isVisibleFilters: true,
+ isVisibleDownload: true,
+ isVisibleCompactMode: true,
+ isVisiblePagination: true,
version: 1,
},
DROP_DOWN_WIDGET: {
@@ -541,6 +546,58 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
},
xAxisName: "Last Week",
yAxisName: "Total Order Revenue $",
+ customFusionChartConfig: {
+ type: "column2d",
+ dataSource: {
+ chart: {
+ caption: "Last week's revenue",
+ xAxisName: "Last Week",
+ yAxisName: "Total Order Revenue $",
+ theme: "fusion",
+ },
+ data: [
+ {
+ label: "Mon",
+ value: 10000,
+ },
+ {
+ label: "Tue",
+ value: 12000,
+ },
+ {
+ label: "Wed",
+ value: 32000,
+ },
+ {
+ label: "Thu",
+ value: 28000,
+ },
+ {
+ label: "Fri",
+ value: 14000,
+ },
+ {
+ label: "Sat",
+ value: 19000,
+ },
+ {
+ label: "Sun",
+ value: 36000,
+ },
+ ],
+ trendlines: [
+ {
+ line: [
+ {
+ startvalue: "38000",
+ valueOnRight: "1",
+ displayvalue: "Weekly Target",
+ },
+ ],
+ },
+ ],
+ },
+ },
},
FORM_BUTTON_WIDGET: {
rows: 1 * GRID_DENSITY_MIGRATION_V1,
@@ -886,6 +943,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
widgets: { [widgetId: string]: FlattenedWidgetProps },
) => {
let template = {};
+ const logBlackListMap: any = {};
const container = get(
widgets,
`${get(widget, "children.0.children.0")}`,
@@ -901,6 +959,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
canvas.children &&
get(canvas, "children", []).forEach((child: string) => {
const childWidget = cloneDeep(get(widgets, `${child}`));
+ const logBlackList: { [key: string]: boolean } = {};
const keys = Object.keys(childWidget);
for (let i = 0; i < keys.length; i++) {
@@ -927,6 +986,12 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
}
}
+ Object.keys(childWidget).map((key) => {
+ logBlackList[key] = true;
+ });
+
+ logBlackListMap[childWidget.widgetId] = logBlackList;
+
template = {
...template,
[childWidget.widgetName]: childWidget,
@@ -945,6 +1010,18 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
propertyValue: template,
},
];
+
+ // add logBlackList to updateProperyMap for all children
+ updatePropertyMap = updatePropertyMap.concat(
+ Object.keys(logBlackListMap).map((logBlackListMapKey) => {
+ return {
+ widgetId: logBlackListMapKey,
+ propertyName: "logBlackList",
+ propertyValue: logBlackListMap[logBlackListMapKey],
+ };
+ }),
+ );
+
return updatePropertyMap;
},
},
@@ -961,6 +1038,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
if (!parentId) return { widgets };
const widget = { ...widgets[widgetId] };
const parent = { ...widgets[parentId] };
+ const logBlackList: { [key: string]: boolean } = {};
const disallowedWidgets = [WidgetTypes.FILE_PICKER_WIDGET];
@@ -998,7 +1076,15 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
parent.template = template;
+ // add logBlackList for the children being added
+ Object.keys(widget).map((key) => {
+ logBlackList[key] = true;
+ });
+
+ widget.logBlackList = logBlackList;
+
widgets[parentId] = parent;
+ widgets[widgetId] = widget;
return { widgets };
},
diff --git a/app/client/src/notifications/Bell.tsx b/app/client/src/notifications/Bell.tsx
new file mode 100644
index 0000000000..ba06a9711e
--- /dev/null
+++ b/app/client/src/notifications/Bell.tsx
@@ -0,0 +1,107 @@
+import React, { useEffect } from "react";
+import { useSelector } from "react-redux";
+
+import NotificationsList from "./NotificationsList";
+import { ReactComponent as BellIcon } from "assets/icons/ads/bell.svg";
+import { Popover2 } from "@blueprintjs/popover2";
+
+import "@blueprintjs/popover2/lib/css/blueprint-popover2.css";
+import { useDispatch } from "react-redux";
+
+import {
+ fetchNotificationsRequest,
+ setIsNotificationsListVisible,
+} from "actions/notificationActions";
+import styled from "styled-components";
+
+import {
+ unreadCountSelector,
+ isNotificationsListVisibleSelector,
+} from "selectors/notificationSelectors";
+
+const Container = styled.div`
+ position: relative;
+ padding: ${(props) => props.theme.spaces[1]}px;
+ margin-right: ${(props) => props.theme.spaces[9]}px;
+ top: 3px;
+ cursor: pointer;
+`;
+
+const BellIndicatorContainer = styled.div`
+ position: absolute;
+ top: -7px;
+ right: -5px;
+`;
+
+const Count = styled.div`
+ position: absolute;
+ top: 43%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+`;
+
+const BellIndicatorIcon = styled.div`
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: ${(props) =>
+ props.theme.colors.notifications.bellIndicator};
+`;
+
+const StyledBellIcon = styled(BellIcon)`
+ width: 22px;
+ height: 22px;
+`;
+
+function Bell() {
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(fetchNotificationsRequest());
+ }, []);
+
+ const unreadCount = useSelector(unreadCountSelector);
+ const showIndicator = unreadCount > 0;
+
+ const isOpen = useSelector(isNotificationsListVisibleSelector);
+
+ return (
+ }
+ isOpen={isOpen}
+ minimal
+ modifiers={{
+ offset: {
+ enabled: true,
+ options: {
+ offset: [-15, 10],
+ },
+ },
+ preventOverflow: {
+ enabled: true,
+ },
+ }}
+ onInteraction={(nextOpenState) =>
+ dispatch(setIsNotificationsListVisible(nextOpenState))
+ }
+ placement={"bottom-end"}
+ >
+
+
+ {showIndicator && (
+
+ {/** Not using overflow ellipsis here for UI specs */}
+
+
+ {unreadCount > 100
+ ? `${unreadCount}`.slice(0, 2) + ".."
+ : unreadCount}
+
+
+ )}
+
+
+ );
+}
+
+export default Bell;
diff --git a/app/client/src/notifications/NotificationListItem.tsx b/app/client/src/notifications/NotificationListItem.tsx
new file mode 100644
index 0000000000..0c9018f388
--- /dev/null
+++ b/app/client/src/notifications/NotificationListItem.tsx
@@ -0,0 +1,141 @@
+import React from "react";
+import ProfileImage, { Profile } from "pages/common/ProfileImage";
+
+import styled from "styled-components";
+
+import UserApi from "api/UserApi";
+
+import { AppsmithNotification, NotificationTypes } from "entities/Notification";
+
+import { getTypographyByKey } from "constants/DefaultTheme";
+// import moment from "moment";
+
+import { getCommentThreadURL } from "comments/utils";
+
+import history from "utils/history";
+import { useDispatch } from "react-redux";
+import { markThreadAsReadRequest } from "actions/commentActions";
+
+const Container = styled.div`
+ display: flex;
+ width: 100%;
+ padding: ${(props) => props.theme.spaces[6]}px;
+
+ ${Profile} {
+ margin-right: ${(props) => props.theme.spaces[4]}px;
+ }
+
+ cursor: pointer;
+`;
+
+const NotificationBodyContainer = styled.div`
+ ${(props) => getTypographyByKey(props, "p1")};
+ & b {
+ font-weight: 500;
+ }
+`;
+
+const FlexContainer = styled.div`
+ display: flex;
+`;
+
+const Time = styled.div`
+ color: ${(props) => props.theme.colors.notifications.time};
+ ${(props) => getTypographyByKey(props, "p3")};
+`;
+
+const ProfileImageContainer = styled.div`
+ position: relative;
+`;
+
+const UnreadIndicator = styled.div`
+ position: absolute;
+ right: 9px;
+ top: 0;
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ background-color: ${(props) =>
+ props.theme.colors.notifications.unreadIndicator};
+`;
+
+// eslint-disable-next-line
+function CommentNotification(props: { notification?: AppsmithNotification }) {
+ // TODO handle click
+ return (
+
+
+
+
+ Comment notification body
+
+ {/* {moment().fromNow()} */}
+ An hour ago
+
+
+ );
+}
+
+function CommentThreadNotification(props: {
+ notification: AppsmithNotification;
+}) {
+ const dispatch = useDispatch();
+ const { commentThread } = props.notification;
+ // TODO add isResolved, applicationId, pageId
+ const commentThreadUrl = getCommentThreadURL({
+ commentThreadId: commentThread?.id,
+ });
+
+ const handleClick = () => {
+ history.push(
+ `${commentThreadUrl.pathname}${commentThreadUrl.search}${commentThreadUrl.hash}`,
+ );
+
+ dispatch(markThreadAsReadRequest(commentThread?.id));
+ };
+
+ // TODO use notification isRead state
+ const isRead = true;
+
+ return (
+
+
+
+ {!isRead && }
+
+
+
+ Comment Thread notification body
+
+ {/* {moment().fromNow()} */}
+ An hour ago
+
+
+ );
+}
+
+const notificationByType = {
+ [NotificationTypes.CommentNotification]: CommentNotification,
+ [NotificationTypes.CommentThreadNotification]: CommentThreadNotification,
+};
+
+function NotificationListItem(props: { notification: AppsmithNotification }) {
+ const Component =
+ notificationByType[NotificationTypes.CommentThreadNotification];
+
+ return (
+
+
+
+ );
+}
+
+export default NotificationListItem;
diff --git a/app/client/src/notifications/NotificationsList.tsx b/app/client/src/notifications/NotificationsList.tsx
new file mode 100644
index 0000000000..cb1b64d694
--- /dev/null
+++ b/app/client/src/notifications/NotificationsList.tsx
@@ -0,0 +1,104 @@
+import React from "react";
+import { useDispatch, useSelector } from "react-redux";
+import styled from "styled-components";
+
+import { notificationsSelector } from "selectors/notificationSelectors";
+import NotificationListItem from "./NotificationListItem";
+import { AppsmithNotification } from "entities/Notification";
+
+import { createMessage, COMMENTS, MARK_ALL_AS_READ } from "constants/messages";
+
+import Button, { Category } from "components/ads/Button";
+import { getTypographyByKey } from "constants/DefaultTheme";
+
+import { Virtuoso } from "react-virtuoso";
+
+import { markAllNotificationsAsReadRequest } from "actions/notificationActions";
+
+const Container = styled.div`
+ width: 326px;
+ max-height: 376px;
+`;
+
+const StyledHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ padding: ${(props) => props.theme.spaces[4] + 1}px;
+
+ & .title {
+ ${(props) => getTypographyByKey(props, "p1")};
+ color: ${(props) => props.theme.colors.notifications.listHeaderTitle};
+ }
+
+ & .mark-all-as-read {
+ border-color: ${(props) =>
+ props.theme.colors.notifications.markAllAsReadButtonBackground};
+ background-color: ${(props) =>
+ props.theme.colors.notifications.markAllAsReadButtonBackground};
+ color: ${(props) =>
+ props.theme.colors.notifications.markAllAsReadButtonText};
+ }
+`;
+
+function NotificationsListHeader() {
+ const dispatch = useDispatch();
+
+ return (
+
+ {createMessage(COMMENTS)}
+ {
+ dispatch(markAllNotificationsAsReadRequest());
+ }}
+ text={createMessage(MARK_ALL_AS_READ)}
+ type="button"
+ />
+
+ );
+}
+
+const NOTIFICATION_HEIGHT = 63;
+
+function NotificationsList() {
+ const notifications = useSelector(notificationsSelector);
+ const height = Math.min(4, notifications.length) * NOTIFICATION_HEIGHT;
+
+ return (
+
+
+
+ Loading...
+
+ );
+ },
+ }}
+ data={notifications}
+ endReached={() => {
+ return Promise.resolve([]);
+ }}
+ itemContent={(index: number, notification: AppsmithNotification) => (
+
+ )}
+ overscan={1}
+ style={{ height }}
+ />
+
+ );
+}
+
+export default NotificationsList;
diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx
index a76fc6fcc0..519769b606 100644
--- a/app/client/src/pages/AppViewer/index.tsx
+++ b/app/client/src/pages/AppViewer/index.tsx
@@ -28,6 +28,8 @@ import { editorInitializer } from "utils/EditorUtils";
import * as Sentry from "@sentry/react";
import log from "loglevel";
import { getViewModePageList } from "selectors/editorSelectors";
+import AppComments from "comments/AppComments/AppComments";
+import AddCommentTourComponent from "comments/tour/AddCommentTourComponent";
const SentryRoute = Sentry.withSentryRouting(Route);
@@ -42,6 +44,16 @@ const AppViewerBody = styled.section<{ hasPages: boolean }>`
);
`;
+const ContainerWithComments = styled.div`
+ display: flex;
+ width: 100%;
+`;
+
+const AppViewerBodyContainer = styled.div<{ width?: string }>`
+ overflow: auto;
+ margin: 0 auto;
+`;
+
export type AppViewerProps = {
initializeAppViewer: (applicationId: string, pageId?: string) => void;
isInitialized: boolean;
@@ -93,22 +105,28 @@ class AppViewer extends Component<
resetChildrenMetaProperty: this.props.resetChildrenMetaProperty,
}}
>
- 1}>
- {isInitialized && this.state.registered && (
-
-
-
-
- )}
-
+
+
+
+ 1}>
+ {isInitialized && this.state.registered && (
+
+
+
+
+ )}
+
+
+
+
);
}
diff --git a/app/client/src/pages/Applications/ApplicationCard.tsx b/app/client/src/pages/Applications/ApplicationCard.tsx
index 6f6fae0bc4..c0bc403bed 100644
--- a/app/client/src/pages/Applications/ApplicationCard.tsx
+++ b/app/client/src/pages/Applications/ApplicationCard.tsx
@@ -46,6 +46,8 @@ import { Classes as CsClasses } from "components/ads/common";
import TooltipComponent from "components/ads/Tooltip";
import { isEllipsisActive } from "utils/helpers";
import ForkApplicationModal from "./ForkApplicationModal";
+import { Toaster } from "components/ads/Toast";
+import { Variant } from "components/ads/common";
type NameWrapperProps = {
hasReadPermission: boolean;
@@ -220,6 +222,7 @@ type ApplicationCardProps = {
share?: (applicationId: string) => void;
delete?: (applicationId: string) => void;
update?: (id: string, data: UpdateApplicationPayload) => void;
+ enableImportExport?: boolean;
};
const EditButton = styled(Button)`
@@ -297,6 +300,14 @@ export function ApplicationCard(props: ApplicationCardProps) {
cypressSelector: "t--fork-app",
});
}
+ if (!!props.enableImportExport && hasExportPermission) {
+ moreActionItems.push({
+ onSelect: exportApplicationAsJSONFile,
+ text: "Export",
+ icon: "download",
+ cypressSelector: "t--export-app",
+ });
+ }
setMoreActionItems(moreActionItems);
addDeleteOption();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -312,6 +323,10 @@ export function ApplicationCard(props: ApplicationCardProps) {
props.application?.userPermissions ?? [],
PERMISSION_TYPE.READ_APPLICATION,
);
+ const hasExportPermission = isPermitted(
+ props.application?.userPermissions ?? [],
+ PERMISSION_TYPE.EXPORT_APPLICATION,
+ );
const updateColor = (color: string) => {
setSelectedColor(color);
props.update &&
@@ -331,6 +346,25 @@ export function ApplicationCard(props: ApplicationCardProps) {
const shareApp = () => {
props.share && props.share(props.application.id);
};
+ const exportApplicationAsJSONFile = () => {
+ // export api response comes with content-disposition header.
+ // there is no straightforward way to handle it with axios/fetch
+ const id = `t--export-app-link`;
+ const existingLink = document.getElementById(id);
+ existingLink && existingLink.remove();
+ const link = document.createElement("a");
+ link.href = `/api/v1/applications/export/${props.application.id}`;
+ link.target = "_blank";
+ link.id = id;
+ document.body.appendChild(link);
+ link.click();
+ setIsMenuOpen(false);
+ Toaster.show({
+ text: `Successfully exported ${props.application.name}`,
+ variant: Variant.success,
+ });
+ link.remove();
+ };
const forkApplicationInitiate = () => {
// open fork application modal
// on click on an organisation, create app and take to app
diff --git a/app/client/src/pages/Applications/ForkModalStyles.ts b/app/client/src/pages/Applications/ForkModalStyles.ts
index a79e04f042..2e731b7b57 100644
--- a/app/client/src/pages/Applications/ForkModalStyles.ts
+++ b/app/client/src/pages/Applications/ForkModalStyles.ts
@@ -26,9 +26,10 @@ const StyledRadioComponent = styled(RadioComponent)`
}
`;
-const ForkButton = styled(Button)`
+const ForkButton = styled(Button)<{ disabled?: boolean }>`
height: 38px;
width: 203px;
+ pointer-events: ${(props) => (!!props.disabled ? "none" : "auto")};
`;
const OrganizationList = styled.div`
diff --git a/app/client/src/pages/Applications/ImportApplicationModal.tsx b/app/client/src/pages/Applications/ImportApplicationModal.tsx
new file mode 100644
index 0000000000..4f37426224
--- /dev/null
+++ b/app/client/src/pages/Applications/ImportApplicationModal.tsx
@@ -0,0 +1,117 @@
+import React, { useCallback, useState } from "react";
+import styled from "styled-components";
+import Button, { Size } from "components/ads/Button";
+import { StyledDialog } from "./ForkModalStyles";
+import { useSelector } from "store";
+import { AppState } from "reducers";
+import FilePicker, { SetProgress, FileType } from "components/ads/FilePicker";
+import { useDispatch } from "react-redux";
+import { importApplication } from "actions/applicationActions";
+import { Toaster } from "components/ads/Toast";
+import { Variant } from "components/ads/common";
+import { IMPORT_APPLICATION_MODAL_TITLE } from "constants/messages";
+
+const ImportButton = styled(Button)<{ disabled?: boolean }>`
+ height: 30px;
+ width: 81px;
+ pointer-events: ${(props) => (!!props.disabled ? "none" : "auto")};
+`;
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+`;
+
+const FilePickerWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+type ImportApplicationModalProps = {
+ // import?: (file: any) => void;
+ organizationId?: string;
+ isModalOpen?: boolean;
+ onClose?: () => void;
+};
+
+function ImportApplicationModal(props: ImportApplicationModalProps) {
+ const { isModalOpen, onClose, organizationId } = props;
+ const [appFileToBeUploaded, setAppFileToBeUploaded] = useState<{
+ file: File;
+ setProgress: SetProgress;
+ } | null>(null);
+ const dispatch = useDispatch();
+
+ const importingApplication = useSelector(
+ (state: AppState) => state.ui.applications.importingApplication,
+ );
+
+ const FileUploader = useCallback(
+ async (file: File, setProgress: SetProgress) => {
+ if (!!file) {
+ setAppFileToBeUploaded({
+ file,
+ setProgress,
+ });
+ } else {
+ setAppFileToBeUploaded(null);
+ }
+ },
+ [],
+ );
+
+ const onImportApplication = useCallback(() => {
+ if (!appFileToBeUploaded) {
+ Toaster.show({
+ text: "Please choose a valid application file!",
+ variant: Variant.danger,
+ });
+ return;
+ }
+ const { file } = appFileToBeUploaded || {};
+
+ dispatch(
+ importApplication({
+ orgId: organizationId as string,
+ applicationFile: file,
+ }),
+ );
+ }, [appFileToBeUploaded, organizationId]);
+
+ const onRemoveFile = useCallback(() => setAppFileToBeUploaded(null), []);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ImportApplicationModal;
diff --git a/app/client/src/pages/Applications/OnboardingForm.tsx b/app/client/src/pages/Applications/OnboardingForm.tsx
new file mode 100644
index 0000000000..7e1ca92b99
--- /dev/null
+++ b/app/client/src/pages/Applications/OnboardingForm.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import { useSelector } from "react-redux";
+import { useScript, ScriptStatus } from "utils/hooks/useScript";
+import { getCurrentUser } from "selectors/usersSelectors";
+import styled from "styled-components";
+
+export const TypeformContainer = styled.div`
+ & iframe {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ border: 0;
+ }
+`;
+
+function OnboardingForm() {
+ const status = useScript(`https://embed.typeform.com/embed.js`);
+ const currentUser = useSelector(getCurrentUser);
+
+ if (status !== ScriptStatus.READY || !currentUser) return null;
+
+ return (
+
+
+
+ );
+}
+
+export default OnboardingForm;
diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx
index 3b3282c734..36a5425ec3 100644
--- a/app/client/src/pages/Applications/index.tsx
+++ b/app/client/src/pages/Applications/index.tsx
@@ -78,6 +78,9 @@ import WelcomeHelper from "components/editorComponents/Onboarding/WelcomeHelper"
import { useIntiateOnboarding } from "components/editorComponents/Onboarding/utils";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { createOrganizationSubmitHandler } from "../organization/helpers";
+import ImportApplicationModal from "./ImportApplicationModal";
+import OnboardingForm from "./OnboardingForm";
+import { getAppsmithConfigs } from "configs";
const OrgDropDown = styled.div`
display: flex;
@@ -505,6 +508,7 @@ const NoSearchResultImg = styled.img`
`;
function ApplicationsSection(props: any) {
+ const enableImportExport = true;
const dispatch = useDispatch();
const theme = useContext(ThemeContext);
const isSavingOrgInfo = useSelector(getIsSavingOrgInfo);
@@ -522,6 +526,8 @@ function ApplicationsSection(props: any) {
});
}
};
+ const [warnLeavingOrganization, setWarnLeavingOrganization] = useState(false);
+ const [orgToOpenMenu, setOrgToOpenMenu] = useState(null);
const updateApplicationDispatch = (
id: string,
data: UpdateApplicationPayload,
@@ -534,9 +540,15 @@ function ApplicationsSection(props: any) {
};
const [selectedOrgId, setSelectedOrgId] = useState();
+ const [
+ selectedOrgIdForImportApplication,
+ setSelectedOrgIdForImportApplication,
+ ] = useState();
const Form: any = OrgInviteUsersForm;
const leaveOrg = (orgId: string) => {
+ setWarnLeavingOrganization(false);
+ setOrgToOpenMenu(null);
dispatch(leaveOrganization(orgId));
};
@@ -557,7 +569,11 @@ function ApplicationsSection(props: any) {
const { disabled, orgName, orgSlug } = props;
return (
-
+ setOrgToOpenMenu(orgSlug)}
+ >
{
+ setOrgToOpenMenu(null);
+ }}
+ onClosing={() => {
+ setWarnLeavingOrganization(false);
+ }}
position={Position.BOTTOM_RIGHT}
target={OrgMenuTarget({
orgName: organization.name,
@@ -668,6 +691,18 @@ function ApplicationsSection(props: any) {
}
text="Organization Settings"
/>
+ {enableImportExport && (
+
+ setSelectedOrgIdForImportApplication(
+ organization.id,
+ )
+ }
+ text="Import Application"
+ />
+ )}
setSelectedOrgId(organization.id)}
@@ -686,12 +721,29 @@ function ApplicationsSection(props: any) {
)}
leaveOrg(organization.id)}
- text="Leave Organization"
+ onSelect={() =>
+ !warnLeavingOrganization
+ ? setWarnLeavingOrganization(true)
+ : leaveOrg(organization.id)
+ }
+ text={
+ !warnLeavingOrganization
+ ? "Leave Organization"
+ : "Are you sure?"
+ }
+ type={!warnLeavingOrganization ? undefined : "warning"}
/>
)}
-
+ {selectedOrgIdForImportApplication && (
+ setSelectedOrgIdForImportApplication("")}
+ organizationId={selectedOrgIdForImportApplication}
+ />
+ )}
{hasManageOrgPermissions && (
@@ -832,15 +885,26 @@ type ApplicationProps = {
currentUser?: User;
searchKeyword: string | undefined;
};
+
+const getIsFromSignup = () => {
+ if (window.location.href) {
+ const url = new URL(window.location.href);
+ const searchParams = url.searchParams;
+ return !!searchParams.get("isFromSignup");
+ }
+ return false;
+};
+const { onboardingFormEnabled } = getAppsmithConfigs();
class Applications extends Component<
ApplicationProps,
- { selectedOrgId: string }
+ { selectedOrgId: string; showOnboardingForm: boolean }
> {
constructor(props: ApplicationProps) {
super(props);
this.state = {
selectedOrgId: "",
+ showOnboardingForm: false,
};
}
@@ -848,20 +912,29 @@ class Applications extends Component<
PerformanceTracker.stopTracking(PerformanceTransactionName.LOGIN_CLICK);
PerformanceTracker.stopTracking(PerformanceTransactionName.SIGN_UP);
this.props.getAllApplication();
+ this.setState({
+ showOnboardingForm: getIsFromSignup() && onboardingFormEnabled,
+ });
}
public render() {
return (
-
-
-
-
+ {this.state.showOnboardingForm ? (
+
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
);
}
diff --git a/app/client/src/pages/Applications/permissionHelpers.tsx b/app/client/src/pages/Applications/permissionHelpers.tsx
index 0aac048df2..821f92503f 100644
--- a/app/client/src/pages/Applications/permissionHelpers.tsx
+++ b/app/client/src/pages/Applications/permissionHelpers.tsx
@@ -2,6 +2,7 @@ export enum PERMISSION_TYPE {
MANAGE_ORGANIZATION = "manage:organizations",
CREATE_APPLICATION = "manage:orgApplications",
MANAGE_APPLICATION = "manage:applications",
+ EXPORT_APPLICATION = "export:applications",
READ_APPLICATION = "read:applications",
READ_ORGANIZATION = "read:organizations",
INVITE_USER_TO_ORGANIZATION = "inviteUsers:organization",
diff --git a/app/client/src/pages/Editor/APIEditor/DatasourceList.tsx b/app/client/src/pages/Editor/APIEditor/DatasourceList.tsx
index 61a245108e..faf71e45e3 100644
--- a/app/client/src/pages/Editor/APIEditor/DatasourceList.tsx
+++ b/app/client/src/pages/Editor/APIEditor/DatasourceList.tsx
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import styled from "styled-components";
-import { IconSize } from "components/ads/Icon";
+import Icon, { IconSize } from "components/ads/Icon";
import { StyledSeparator } from "pages/Applications/ProductUpdatesModal/ReleaseComponent";
import { DATA_SOURCES_EDITOR_ID_URL } from "constants/routes";
import history from "utils/history";
@@ -54,22 +54,28 @@ const DatasourceCard = styled.div`
border: 1px solid ${(props) => props.theme.colors.apiPane.dividerBg};
cursor: pointer;
transition: 0.3s all ease;
+ .cs-icon {
+ opacity: 0;
+ transition: 0.3s all ease;
+ }
&:hover {
box-shadow: 0 0 5px #c7c7c7;
+ .cs-icon {
+ opacity: 1;
+ }
}
`;
const DatasourceURL = styled.span`
margin: 8px 0;
- padding: 5px;
font-size: 12px;
- border: 1px solid #69b5ff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- background: #e7f3ff;
+ color: #457ae6;
width: fit-content;
max-width: 100%;
+ font-weight: 500;
`;
const PadTop = styled.div`
@@ -77,6 +83,28 @@ const PadTop = styled.div`
border: none;
`;
+const DataSourceNameContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ .cs-text {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ .cs-icon {
+ flex-shrink: 0;
+ svg {
+ path {
+ fill: #4b4848;
+ }
+ }
+ &: hover {
+ background-color: ${(props) => props.theme.colors.apiPane.iconHoverBg};
+ }
+ }
+`;
+
export const getDatasourceInfo = (datasource: any): string => {
const info = [];
const headers = get(datasource, "datasourceConfiguration.headers", []);
@@ -110,33 +138,51 @@ export default function DataSourceList(props: any) {
- {(props.datasources || []).map((d: any, idx: number) => (
-
- history.push(
- DATA_SOURCES_EDITOR_ID_URL(
- props.applicationId,
- props.currentPageId,
- d.id,
- ),
- )
- }
- >
-
- {d.name}
-
-
- {d.datasourceConfiguration.url}
-
-
-
-
- {getDatasourceInfo(d)}
-
-
-
- ))}
+ {(props.datasources || []).map((d: any, idx: number) => {
+ const dataSourceInfo: string = getDatasourceInfo(d);
+ return (
+ props.onClick(d)}
+ >
+
+
+ {d.name}
+
+ {
+ e.stopPropagation();
+ history.push(
+ DATA_SOURCES_EDITOR_ID_URL(
+ props.applicationId,
+ props.currentPageId,
+ d.id,
+ ),
+ );
+ }}
+ size={IconSize.LARGE}
+ />
+
+
+ {d.datasourceConfiguration.url}
+
+ {dataSourceInfo && (
+ <>
+
+
+
+ {dataSourceInfo}
+
+
+ >
+ )}
+
+ );
+ })}
) : (
diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx
index 7565e40ce4..044eece242 100644
--- a/app/client/src/pages/Editor/APIEditor/Form.tsx
+++ b/app/client/src/pages/Editor/APIEditor/Form.tsx
@@ -1,6 +1,11 @@
import React, { useState } from "react";
import { connect, useSelector, useDispatch } from "react-redux";
-import { formValueSelector, InjectedFormProps, reduxForm } from "redux-form";
+import {
+ formValueSelector,
+ InjectedFormProps,
+ reduxForm,
+ change,
+} from "redux-form";
import {
HTTP_METHOD_OPTIONS,
HTTP_METHODS,
@@ -41,6 +46,7 @@ import { Icon as ButtonIcon } from "@blueprintjs/core";
import { IconSize } from "components/ads/Icon";
import get from "lodash/get";
import DataSourceList from "./DatasourceList";
+import { Datasource } from "entities/Datasource";
const Form = styled.form`
display: flex;
flex-direction: column;
@@ -197,6 +203,7 @@ interface APIFormProps {
datasources?: any;
currentPageId?: string;
applicationId?: string;
+ updateDatasource: (datasource: Datasource) => void;
}
type Props = APIFormProps & InjectedFormProps;
@@ -319,6 +326,11 @@ const DatasourceListTrigger = styled.div`
}
`;
+const BoundaryContainer = styled.div`
+ border: 1px solid transparent;
+ border-right: none;
+`;
+
function renderImportedHeadersButton(
headersCount: number,
onClick: any,
@@ -351,6 +363,11 @@ const CloseIconContainer = styled.div`
position: absolute;
top: 12px;
right: 10px;
+ svg {
+ path {
+ fill: #a9a7a7;
+ }
+ }
`;
function renderHelpSection(
@@ -415,7 +432,9 @@ function ImportedHeaders(props: { headers: any }) {
function ApiEditorForm(props: Props) {
const [selectedIndex, setSelectedIndex] = useState(0);
- const [showDatasources, toggleDatasources] = useState(false);
+ const [showDatasources, toggleDatasources] = useState(
+ !!props.datasources.length,
+ );
const [
apiBindHelpSectionVisible,
setApiBindHelpSectionVisible,
@@ -433,6 +452,7 @@ function ApiEditorForm(props: Props) {
paramsCount,
pluginId,
settingsConfig,
+ updateDatasource,
} = props;
const dispatch = useDispatch();
const allowPostBody =
@@ -484,15 +504,17 @@ function ApiEditorForm(props: Props) {
-
+
+
+
void;
+};
+
+const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
+ updateDatasource: (datasource) => {
+ dispatch(change(API_EDITOR_FORM_NAME, "datasource", datasource));
+ },
+});
+
export default connect((state: AppState, props: { pluginId: string }) => {
const httpMethodFromForm = selector(state, "actionConfiguration.httpMethod");
const actionConfigurationHeaders =
@@ -690,7 +723,7 @@ export default connect((state: AppState, props: { pluginId: string }) => {
currentPageId: state.entities.pageList.currentPageId,
applicationId: state.entities.pageList.applicationId,
};
-})(
+}, mapDispatchToProps)(
reduxForm({
form: API_EDITOR_FORM_NAME,
})(ApiEditorForm),
diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx
index 868a3e2229..7072847f95 100644
--- a/app/client/src/pages/Editor/EditorHeader.tsx
+++ b/app/client/src/pages/Editor/EditorHeader.tsx
@@ -91,7 +91,7 @@ const HeaderSection = styled.div`
top: -1px;
display: flex;
flex: 1;
- overflow: hidden;
+ overflow: visible;
align-items: center;
:nth-child(1) {
justify-content: flex-start;
diff --git a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
index 5d0fe93033..6c24de0fe6 100644
--- a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
+++ b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx
@@ -86,7 +86,7 @@ function EntityExplorer(props: IPanelProps) {
return (
-
+
`
+ display: ${(props) => (props.isHidden ? "none" : "grid")};
grid-template-columns: 30px 1fr 30px;
margin-bottom: 5px;
height: 48px;
@@ -64,11 +64,16 @@ const Underline = styled.div`
/*eslint-disable react/display-name */
export const ExplorerSearch = forwardRef(
(
- props: { clear: () => void; placeholder?: string; autoFocus?: boolean },
+ props: {
+ clear: () => void;
+ placeholder?: string;
+ autoFocus?: boolean;
+ isHidden?: boolean;
+ },
ref: Ref,
) => {
return (
-
+
{
return !!multipleWidgetsSelected;
}
+ public onOnmnibarHotKeyDown(e: KeyboardEvent) {
+ e.preventDefault();
+ this.props.toggleShowGlobalSearchModal();
+ AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "HOTKEY_COMBO" });
+ }
+
public renderHotkeys() {
return (
@@ -79,16 +82,14 @@ class GlobalHotKeys extends React.Component {
global
label="Search entities"
onKeyDown={(e: any) => {
- const entitySearchInput = document.getElementById(
- ENTITY_EXPLORER_SEARCH_ID,
- );
const widgetSearchInput = document.getElementById(
WIDGETS_SEARCH_ID,
);
- if (entitySearchInput) entitySearchInput.focus();
- if (widgetSearchInput) widgetSearchInput.focus();
- e.preventDefault();
- e.stopPropagation();
+ if (widgetSearchInput) {
+ widgetSearchInput.focus();
+ e.preventDefault();
+ e.stopPropagation();
+ }
}}
/>
{
combo="mod + k"
global
label="Show omnibar"
- onKeyDown={(e: KeyboardEvent) => {
- console.log("toggleShowGlobalSearchModal");
- e.preventDefault();
- this.props.toggleShowGlobalSearchModal();
- AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "HOTKEY_COMBO" });
- }}
+ onKeyDown={(e) => this.onOnmnibarHotKeyDown(e)}
+ />
+ this.onOnmnibarHotKeyDown(e)}
/>
{
onPositionChange = noop,
themeMode = props.themeMode || ThemeMode.LIGHT,
} = props;
- const popperTheme = useSelector((state: AppState) =>
- getThemeDetails(state, themeMode),
+ // Meomoizing to avoid rerender of draggable icon.
+ // What is the cost of memoizing?
+ const popperTheme = useMemo(
+ () => getThemeDetails({} as AppState, themeMode),
+ [themeMode],
);
+
useEffect(() => {
const parentElement = props.targetNode && props.targetNode.parentElement;
if (
@@ -133,7 +136,7 @@ export default (props: PopperProps) => {
}, [
props.targetNode,
props.isOpen,
- props.modifiers,
+ JSON.stringify(props.modifiers),
props.placement,
disablePopperEvents,
]);
diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
index 0d718d56c3..004f287434 100644
--- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
+++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
@@ -47,7 +47,14 @@ import CloseEditor from "components/editorComponents/CloseEditor";
import { setGlobalSearchQuery } from "actions/globalSearchActions";
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
import { omnibarDocumentationHelper } from "constants/OmnibarDocumentationConstants";
+import EntityDeps from "components/editorComponents/Debugger/EntityDependecies";
import { isHidden } from "components/formControls/utils";
+import {
+ createMessage,
+ DEBUGGER_ERRORS,
+ DEBUGGER_LOGS,
+ INSPECT_ENTITY,
+} from "constants/messages";
const QueryFormContainer = styled.form`
display: flex;
@@ -461,7 +468,7 @@ export function EditorJSONtoForm(props: Props) {
const renderEachConfig = (formName: string) => (section: any): any => {
return section.children.map((formControlOrSection: ControlProps) => {
if (isHidden(props.formData, section.hidden)) return null;
- if ("children" in formControlOrSection) {
+ if (formControlOrSection.hasOwnProperty("children")) {
return renderEachConfig(formName)(formControlOrSection);
} else {
try {
@@ -537,14 +544,19 @@ export function EditorJSONtoForm(props: Props) {
},
{
key: "ERROR",
- title: "Errors",
+ title: createMessage(DEBUGGER_ERRORS),
panelComponent: ,
},
{
key: "LOGS",
- title: "Logs",
+ title: createMessage(DEBUGGER_LOGS),
panelComponent: ,
},
+ {
+ key: "ENTITY_DEPENDENCIES",
+ title: createMessage(INSPECT_ENTITY),
+ panelComponent: ,
+ },
];
const onTabSelect = (index: number) => {
diff --git a/app/client/src/pages/Editor/ToggleModeButton.tsx b/app/client/src/pages/Editor/ToggleModeButton.tsx
index 74b24be777..55199fa4e5 100644
--- a/app/client/src/pages/Editor/ToggleModeButton.tsx
+++ b/app/client/src/pages/Editor/ToggleModeButton.tsx
@@ -6,6 +6,7 @@ import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
import { ReactComponent as Pen } from "assets/icons/comments/pen.svg";
import { ReactComponent as CommentModeUnread } from "assets/icons/comments/comment-mode-unread-indicator.svg";
import { ReactComponent as CommentMode } from "assets/icons/comments/chat.svg";
+import { Indices } from "constants/Layers";
import {
setCommentMode as setCommentModeAction,
@@ -54,6 +55,7 @@ const ModeButton = styled.div<{ active: boolean }>`
const Container = styled.div`
display: flex;
flex: 1;
+ z-index: ${Indices.Layer1};
`;
/**
@@ -133,47 +135,73 @@ function ToggleCommentModeButton() {
return (
- setCommentModeInUrl(false)}
- >
-
- Edit Mode
- V
- >
- }
- hoverOpenDelay={1000}
- position={Position.BOTTOM}
- >
-
-
-
{
- proceedToNextTourStep();
+ hasOverlay
+ modifiers={{
+ offset: { enabled: true, offset: "3, 20" },
+ arrow: {
+ enabled: true,
+ fn: (data) => ({
+ ...data,
+ offsets: {
+ ...data.offsets,
+ arrow: {
+ top: -8,
+ left: 80,
+ },
+ },
+ }),
+ },
}}
+ pulseStyles={{
+ top: 20,
+ left: 28,
+ height: 30,
+ width: 30,
+ }}
+ showPulse
tourIndex={0}
tourType={TourType.COMMENTS_TOUR}
>
- setCommentModeInUrl(true)}
- >
-
- Comment Mode
- C
- >
- }
- hoverOpenDelay={1000}
- position={Position.BOTTOM}
+
+
setCommentModeInUrl(false)}
>
-
-
-
+
+ Edit Mode
+ V
+ >
+ }
+ hoverOpenDelay={1000}
+ position={Position.BOTTOM}
+ >
+
+
+
+
{
+ setCommentModeInUrl(true);
+ proceedToNextTourStep();
+ }}
+ >
+
+ Comment Mode
+ C
+ >
+ }
+ hoverOpenDelay={1000}
+ position={Position.BOTTOM}
+ >
+
+
+
+
);
diff --git a/app/client/src/pages/UserAuth/SignUp.tsx b/app/client/src/pages/UserAuth/SignUp.tsx
index 97604e2671..74c74ebcbc 100644
--- a/app/client/src/pages/UserAuth/SignUp.tsx
+++ b/app/client/src/pages/UserAuth/SignUp.tsx
@@ -35,7 +35,6 @@ import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers";
import { SignupFormValues } from "./helpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
-import { getAppsmithConfigs } from "configs";
import { SIGNUP_SUBMIT_PATH } from "constants/ApiConstants";
import { connect } from "react-redux";
import { AppState } from "reducers";
@@ -45,6 +44,8 @@ import PerformanceTracker, {
import { useIntiateOnboarding } from "components/editorComponents/Onboarding/utils";
import { SIGNUP_FORM_EMAIL_FIELD_NAME } from "constants/forms";
+import { getAppsmithConfigs } from "configs";
+import { useScript, ScriptStatus, AddScriptTo } from "utils/hooks/useScript";
const { enableGithubOAuth, enableGoogleOAuth } = getAppsmithConfigs();
const SocialLoginList: string[] = [];
@@ -54,6 +55,13 @@ if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB);
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
+declare global {
+ interface Window {
+ grecaptcha: any;
+ }
+}
+const { googleRecaptchaSiteKey } = getAppsmithConfigs();
+
const validate = (values: SignupFormValues) => {
const errors: SignupFormValues = {};
if (!values.password || isEmptyString(values.password)) {
@@ -82,6 +90,11 @@ export function SignUp(props: SignUpFormProps) {
const location = useLocation();
const initiateOnboarding = useIntiateOnboarding();
+ const recaptchaStatus = useScript(
+ `https://www.google.com/recaptcha/api.js?render=${googleRecaptchaSiteKey.apiKey}`,
+ AddScriptTo.HEAD,
+ );
+
let showError = false;
let errorMessage = "";
const queryParams = new URLSearchParams(location.search);
@@ -118,7 +131,37 @@ export function SignUp(props: SignUpFormProps) {
{SocialLoginList.length > 0 && (
)}
-
+ {
+ e.preventDefault();
+ const formElement: HTMLFormElement = document.getElementById(
+ "signup-form",
+ ) as HTMLFormElement;
+ if (
+ googleRecaptchaSiteKey.enabled &&
+ recaptchaStatus === ScriptStatus.READY
+ ) {
+ window.grecaptcha
+ .execute(googleRecaptchaSiteKey.apiKey, {
+ action: "submit",
+ })
+ .then(function(token: any) {
+ formElement &&
+ formElement.setAttribute(
+ "action",
+ `${signupURL}?recaptchaToken=${token}`,
+ );
+ formElement && formElement.submit();
+ });
+ } else {
+ formElement && formElement.submit();
+ }
+ return false;
+ }}
+ >
@@ -58,19 +65,22 @@ export function PageHeader(props: PageHeaderProps) {
{user && (
-
- {user.username === ANONYMOUS_USERNAME ? (
- history.push(loginUrl)}
- size="small"
- text="Sign In"
- />
- ) : (
-
- )}
-
+ <>
+ {areCommentsEnabledForUserAndApp && }
+
+ {user.username === ANONYMOUS_USERNAME ? (
+ history.push(loginUrl)}
+ size="small"
+ text="Sign In"
+ />
+ ) : (
+
+ )}
+
+ >
)}
);
diff --git a/app/client/src/pages/organization/DeleteConfirmationModal.tsx b/app/client/src/pages/organization/DeleteConfirmationModal.tsx
new file mode 100644
index 0000000000..a8f3176b9b
--- /dev/null
+++ b/app/client/src/pages/organization/DeleteConfirmationModal.tsx
@@ -0,0 +1,88 @@
+import React from "react";
+import styled from "styled-components";
+import Button, { Size, Category } from "components/ads/Button";
+import Text, { TextType } from "components/ads/Text";
+import { Variant } from "components/ads/common";
+import {
+ DELETE_CONFIRMATION_MODAL_TITLE,
+ DELETE_CONFIRMATION_MODAL_SUBTITLE,
+} from "constants/messages";
+import Dialog from "components/ads/DialogComponent";
+import { Classes } from "@blueprintjs/core";
+
+const StyledDialog = styled(Dialog)`
+ && .${Classes.DIALOG_BODY} {
+ padding-top: 0px;
+ }
+`;
+
+const CenteredContainer = styled.div`
+ text-align: center;
+`;
+
+const ImportButton = styled(Button)<{ disabled?: boolean }>`
+ height: 30px;
+ width: 81px;
+ pointer-events: ${(props) => (!!props.disabled ? "none" : "auto")};
+`;
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ margin-top: 20px;
+
+ & > a {
+ margin: 0 4px;
+ }
+`;
+
+type DeleteConfirmationProps = {
+ username?: string | null;
+ name?: string | null;
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ isDeletingUser: boolean;
+};
+
+function DeleteConfirmationModal(props: DeleteConfirmationProps) {
+ const { isDeletingUser, isOpen, name, onClose, onConfirm, username } = props;
+
+ return (
+
+
+
+ {DELETE_CONFIRMATION_MODAL_SUBTITLE(name || username)}
+
+
+
+
+
+
+
+ );
+}
+
+export default DeleteConfirmationModal;
diff --git a/app/client/src/pages/organization/General.tsx b/app/client/src/pages/organization/General.tsx
index f9bd6e2010..b426b6230d 100644
--- a/app/client/src/pages/organization/General.tsx
+++ b/app/client/src/pages/organization/General.tsx
@@ -20,6 +20,7 @@ import { getOrgLoadingStates } from "selectors/organizationSelectors";
import FilePicker, {
SetProgress,
UploadCallback,
+ FileType,
} from "components/ads/FilePicker";
import { getIsFetchingApplications } from "selectors/applicationSelectors";
@@ -171,6 +172,7 @@ export function GeneralSettings() {
{isFetchingOrg && }
{!isFetchingOrg && (
setShowMemberDeletionConfirmation(true);
+ const onCloseConfirmationModal = () =>
+ setShowMemberDeletionConfirmation(false);
+
+ const [userToBeDeleted, setUserToBeDeleted] = useState<{
+ name: string;
+ username: string;
+ orgId: string;
+ } | null>(null);
+
+ const onConfirmMemberDeletion = (
+ name: string,
+ username: string,
+ orgId: string,
+ ) => {
+ setUserToBeDeleted({ name, username, orgId });
+ onOpenConfirmationModal();
+ };
+
+ const onDeleteMember = () => {
+ if (!userToBeDeleted) return null;
+ dispatch(deleteOrgUser(userToBeDeleted.orgId, userToBeDeleted.username));
+ };
+
const {
deletingUserInfo,
isFetchingAllRoles,
@@ -66,6 +96,21 @@ export default function MemberSettings(props: PageProps) {
(el) => el.id === orgId,
)[0];
+ useEffect(() => {
+ if (!!userToBeDeleted && showMemberDeletionConfirmation) {
+ const userBeingDeleted = allUsers.find(
+ (user) => user.username === userToBeDeleted.username,
+ );
+ if (!userBeingDeleted) {
+ setUserToBeDeleted(null);
+ onCloseConfirmationModal();
+ setIsDeletingUser(false);
+ } else {
+ setIsDeletingUser(userBeingDeleted.isDeleting);
+ }
+ }
+ }, [allUsers]);
+
const userTableData = allUsers.map((user) => ({
...user,
isCurrentUser: user.username === currentUser?.username,
@@ -139,8 +184,10 @@ export default function MemberSettings(props: PageProps) {
}
name="delete"
onClick={() => {
- dispatch(
- deleteOrgUser(orgId, cellProps.cell.row.values.username),
+ onConfirmMemberDeletion(
+ cellProps.cell.row.values.username,
+ cellProps.cell.row.values.username,
+ orgId,
);
}}
size={IconSize.LARGE}
@@ -174,7 +221,17 @@ export default function MemberSettings(props: PageProps) {
{isFetchingAllUsers && isFetchingAllRoles ? (
) : (
-
+ <>
+
+
+ >
)}
>
);
diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx
index 3b3cfdc90e..1ab0c49ee5 100644
--- a/app/client/src/reducers/index.tsx
+++ b/app/client/src/reducers/index.tsx
@@ -45,6 +45,7 @@ import { CommentsReduxState } from "./uiReducers/commentsReducer/interfaces";
import { WebsocketReduxState } from "./uiReducers/websocketReducer";
import { DebuggerReduxState } from "./uiReducers/debuggerReducer";
import { TourReducerState } from "./uiReducers/tourReducer";
+import { NotificationReducerState } from "./uiReducers/notificationsReducer";
const appReducer = combineReducers({
entities: entityReducer,
@@ -88,6 +89,7 @@ export interface AppState {
websocket: WebsocketReduxState;
debugger: DebuggerReduxState;
tour: TourReducerState;
+ notifications: NotificationReducerState;
};
entities: {
canvasWidgets: CanvasWidgetsReduxState;
diff --git a/app/client/src/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/reducers/uiReducers/applicationsReducer.tsx
index abc14e6c9a..61df88cc5e 100644
--- a/app/client/src/reducers/uiReducers/applicationsReducer.tsx
+++ b/app/client/src/reducers/uiReducers/applicationsReducer.tsx
@@ -26,6 +26,8 @@ const initialState: ApplicationsReduxState = {
duplicatingApplication: false,
userOrgs: [],
isSavingOrgInfo: false,
+ importingApplication: false,
+ importedApplication: null,
showAppInviteUsersDialog: false,
};
@@ -214,6 +216,28 @@ const applicationsReducer = createReducer(initialState, {
forkingApplication: false,
};
},
+ [ReduxActionTypes.IMPORT_APPLICATION_INIT]: (
+ state: ApplicationsReduxState,
+ ) => ({ ...state, importingApplication: true }),
+ [ReduxActionTypes.IMPORT_APPLICATION_SUCCESS]: (
+ state: ApplicationsReduxState,
+ action: ReduxAction<{ importedApplication: any }>,
+ ) => {
+ const { importedApplication } = action.payload;
+ return {
+ ...state,
+ importingApplication: false,
+ importedApplication,
+ };
+ },
+ [ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR]: (
+ state: ApplicationsReduxState,
+ ) => {
+ return {
+ ...state,
+ importingApplication: false,
+ };
+ },
[ReduxActionTypes.SAVING_ORG_INFO]: (state: ApplicationsReduxState) => {
return {
...state,
@@ -349,6 +373,8 @@ export interface ApplicationsReduxState {
currentApplication?: ApplicationPayload;
userOrgs: Organization[];
isSavingOrgInfo: boolean;
+ importingApplication: boolean;
+ importedApplication: any;
showAppInviteUsersDialog: boolean;
}
diff --git a/app/client/src/reducers/uiReducers/commentsReducer/commentsReducer.ts b/app/client/src/reducers/uiReducers/commentsReducer/commentsReducer.ts
index ad0e5c07d5..c3160874f7 100644
--- a/app/client/src/reducers/uiReducers/commentsReducer/commentsReducer.ts
+++ b/app/client/src/reducers/uiReducers/commentsReducer/commentsReducer.ts
@@ -41,14 +41,6 @@ const initialState: CommentsReduxState = {
* They are handled separately
*/
const commentsReducer = createReducer(initialState, {
- // todo: remove (for dev)
- [ReduxActionTypes.SET_COMMENT_THREADS_SUCCESS]: (
- state: CommentsReduxState,
- action: ReduxAction,
- ) => ({
- ...state,
- ...action.payload,
- }),
// Only one unpublished comment threads exists at a time
[ReduxActionTypes.CREATE_UNPUBLISHED_COMMENT_THREAD_SUCCESS]: (
state: CommentsReduxState,
@@ -235,6 +227,12 @@ const commentsReducer = createReducer(initialState, {
delete state.commentThreadsMap[commentThreadId];
+ state.commentThreadsMap = { ...state.commentThreadsMap };
+
+ state.applicationCommentThreadsByRef[appId as string] = {
+ ...state.applicationCommentThreadsByRef[appId as string],
+ };
+
return { ...state };
},
[ReduxActionTypes.SHOW_COMMENTS_INTRO_CAROUSEL]: (
diff --git a/app/client/src/reducers/uiReducers/commentsReducer/handleAddCommentToThreadSuccess.ts b/app/client/src/reducers/uiReducers/commentsReducer/handleAddCommentToThreadSuccess.ts
index 97606eb1e7..472d73a05e 100644
--- a/app/client/src/reducers/uiReducers/commentsReducer/handleAddCommentToThreadSuccess.ts
+++ b/app/client/src/reducers/uiReducers/commentsReducer/handleAddCommentToThreadSuccess.ts
@@ -1,5 +1,5 @@
import { ReduxAction } from "constants/ReduxActionConstants";
-import { get } from "lodash";
+import { get, uniqBy } from "lodash";
import { CommentsReduxState } from "./interfaces";
const handleAddCommentToThreadSuccess = (
@@ -11,7 +11,7 @@ const handleAddCommentToThreadSuccess = (
const existingComments = get(commentThreadInStore, "comments", []);
state.commentThreadsMap[commentThreadId] = {
...commentThreadInStore,
- comments: Array.from(new Set([...existingComments, comment])),
+ comments: uniqBy([...existingComments, comment], "id"),
};
return { ...state };
diff --git a/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentEvent.ts b/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentEvent.ts
index 74f7455258..3c75803f22 100644
--- a/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentEvent.ts
+++ b/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentEvent.ts
@@ -9,6 +9,9 @@ const handleUpdateCommentThreadEvent = (
const { _id, threadId } = action.payload;
const threadInState = state.commentThreadsMap[threadId as string];
+
+ if (!threadInState) return state;
+
const commentIdx = threadInState.comments.findIndex(
(comment) => comment.id === _id,
);
diff --git a/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadEvent.ts b/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadEvent.ts
index a7044318eb..78dbffe0db 100644
--- a/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadEvent.ts
+++ b/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadEvent.ts
@@ -3,6 +3,7 @@ import { ReduxAction } from "constants/ReduxActionConstants";
import { get, uniqBy } from "lodash";
import { CommentsReduxState } from "./interfaces";
+// TODO verify cases where commentThread can be undefined for update event
const handleUpdateCommentThreadEvent = (
state: CommentsReduxState,
action: ReduxAction>,
@@ -11,13 +12,34 @@ const handleUpdateCommentThreadEvent = (
const commentThreadInStore = state.commentThreadsMap[id];
const existingComments = get(commentThreadInStore, "comments", []);
const newComments = get(action.payload, "comments", []);
+
+ const pinnedStateChanged =
+ commentThreadInStore?.pinnedState?.active !==
+ action.payload?.pinnedState?.active;
+
+ const isNowResolved =
+ !commentThreadInStore?.resolvedState?.active &&
+ action.payload?.resolvedState?.active;
+
+ const shouldRefreshList = isNowResolved || pinnedStateChanged;
+
state.commentThreadsMap[id] = {
- ...commentThreadInStore,
+ ...(commentThreadInStore || {}),
...action.payload,
- isViewed: commentThreadInStore.isViewed || action.payload.isViewed, // TODO refactor this
+ isViewed: commentThreadInStore?.isViewed || action.payload.isViewed, // TODO refactor this
comments: uniqBy([...existingComments, ...newComments], "id"),
};
+ if (shouldRefreshList) {
+ state.applicationCommentThreadsByRef[
+ action.payload.applicationId as string
+ ] = {
+ ...state.applicationCommentThreadsByRef[
+ action.payload.applicationId as string
+ ],
+ };
+ }
+
const showUnreadIndicator = !state.isCommentMode;
return { ...state, showUnreadIndicator };
diff --git a/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadSuccess.ts b/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadSuccess.ts
index b8321ee00b..68edf97ee4 100644
--- a/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadSuccess.ts
+++ b/app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentThreadSuccess.ts
@@ -13,12 +13,32 @@ const handleUpdateCommentThreadSuccess = (
if (!commentThreadInStore) return state;
+ const pinnedStateChanged =
+ commentThreadInStore?.pinnedState?.active !==
+ action.payload?.pinnedState?.active;
+
+ const isNowResolved =
+ !commentThreadInStore?.resolvedState?.active &&
+ action.payload?.resolvedState?.active;
+
+ const shouldRefreshList = isNowResolved || pinnedStateChanged;
+
state.commentThreadsMap[id] = {
...commentThreadInStore,
...action.payload,
comments: existingComments,
};
+ if (shouldRefreshList) {
+ state.applicationCommentThreadsByRef[
+ action.payload.applicationId as string
+ ] = {
+ ...state.applicationCommentThreadsByRef[
+ action.payload.applicationId as string
+ ],
+ };
+ }
+
return {
...state,
creatingNewThreadComment: false,
diff --git a/app/client/src/reducers/uiReducers/index.tsx b/app/client/src/reducers/uiReducers/index.tsx
index 9dc39dbb53..7f05e2b580 100644
--- a/app/client/src/reducers/uiReducers/index.tsx
+++ b/app/client/src/reducers/uiReducers/index.tsx
@@ -30,6 +30,7 @@ import commentsReducer from "./commentsReducer/commentsReducer";
import websocketReducer from "./websocketReducer";
import debuggerReducer from "./debuggerReducer";
import tourReducer from "./tourReducer";
+import notificationsReducer from "./notificationsReducer";
const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer,
@@ -63,6 +64,7 @@ const uiReducer = combineReducers({
websocket: websocketReducer,
debugger: debuggerReducer,
tour: tourReducer,
+ notifications: notificationsReducer,
});
export default uiReducer;
diff --git a/app/client/src/reducers/uiReducers/notificationsReducer.ts b/app/client/src/reducers/uiReducers/notificationsReducer.ts
new file mode 100644
index 0000000000..9b3a5d71ce
--- /dev/null
+++ b/app/client/src/reducers/uiReducers/notificationsReducer.ts
@@ -0,0 +1,54 @@
+import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
+import { AppsmithNotification } from "entities/Notification";
+import { uniqBy } from "lodash";
+import { createReducer } from "utils/AppsmithUtils";
+
+const initialState: NotificationReducerState = {
+ unreadNotificationsCount: 0,
+ showNotificationsMenu: false,
+ notifications: [],
+};
+
+const tourReducer = createReducer(initialState, {
+ [ReduxActionTypes.FETCH_NOTIFICATIONS_SUCCESS]: (
+ state: NotificationReducerState,
+ action: ReduxAction<{ notifications: Array }>,
+ ) => ({
+ ...state,
+ notifications: action.payload.notifications,
+ }),
+ [ReduxActionTypes.NEW_NOTIFICATION_EVENT]: (
+ state: NotificationReducerState,
+ action: ReduxAction,
+ ) => {
+ if (!state.showNotificationsMenu) {
+ state.unreadNotificationsCount += 1;
+ }
+
+ return {
+ ...state,
+ notifications: uniqBy(
+ [{ ...action.payload, id: action.payload._id }, ...state.notifications],
+ "id",
+ ),
+ };
+ },
+ [ReduxActionTypes.SET_IS_NOTIFICATIONS_LIST_VISIBLE]: (
+ state: NotificationReducerState,
+ action: ReduxAction,
+ ) => ({
+ ...state,
+ showNotificationsMenu: action.payload,
+ unreadNotificationsCount: action.payload
+ ? 0
+ : state.unreadNotificationsCount,
+ }),
+});
+
+export type NotificationReducerState = {
+ unreadNotificationsCount: number;
+ notifications: Array;
+ showNotificationsMenu: boolean;
+};
+
+export default tourReducer;
diff --git a/app/client/src/reducers/uiReducers/orgReducer.ts b/app/client/src/reducers/uiReducers/orgReducer.ts
index 984506b53d..9207c16725 100644
--- a/app/client/src/reducers/uiReducers/orgReducer.ts
+++ b/app/client/src/reducers/uiReducers/orgReducer.ts
@@ -78,6 +78,14 @@ const orgReducer = createImmerReducer(initialState, {
}
});
},
+ [ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR]: (
+ draftState: OrgReduxState,
+ ) => {
+ draftState.orgUsers.forEach((user: OrgUser) => {
+ //TODO: This will change the status to false even if one role change api fails.
+ user.isChangingRole = false;
+ });
+ },
[ReduxActionTypes.DELETE_ORG_USER_INIT]: (
draftState: OrgReduxState,
action: ReduxAction<{ username: string }>,
@@ -96,14 +104,6 @@ const orgReducer = createImmerReducer(initialState, {
(user: OrgUser) => user.username !== action.payload.username,
);
},
- [ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR]: (
- draftState: OrgReduxState,
- ) => {
- draftState.orgUsers.forEach((user: OrgUser) => {
- //TODO: This will change the status to false even if one role change api fails.
- user.isChangingRole = false;
- });
- },
[ReduxActionErrorTypes.DELETE_ORG_USER_ERROR]: (
draftState: OrgReduxState,
) => {
diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts
index f01caaf990..e630d3476c 100644
--- a/app/client/src/sagas/ActionSagas.ts
+++ b/app/client/src/sagas/ActionSagas.ts
@@ -624,15 +624,21 @@ function* setActionPropertySaga(action: ReduxAction) {
if (propertyName === "name") return;
const actionObj = yield select(getAction, actionId);
+ const fieldToBeUpdated = propertyName.replace(
+ "actionConfiguration",
+ "config",
+ );
AppsmithConsole.info({
+ logType: LOG_TYPE.ACTION_UPDATE,
text: "Configuration updated",
source: {
type: ENTITY_TYPE.ACTION,
name: actionObj.name,
id: actionId,
+ propertyPath: fieldToBeUpdated,
},
state: {
- [propertyName]: value,
+ [fieldToBeUpdated]: value,
},
});
diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts
index 29b8e690be..e968ce79c3 100644
--- a/app/client/src/sagas/ApiPaneSagas.ts
+++ b/app/client/src/sagas/ApiPaneSagas.ts
@@ -68,7 +68,11 @@ import { Toaster } from "components/ads/Toast";
import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages";
import { checkCurrentStep } from "./OnboardingSagas";
import { OnboardingStep } from "constants/OnboardingConstants";
-import { getIndextoUpdate } from "utils/ApiPaneUtils";
+import {
+ getIndextoUpdate,
+ parseUrlForQueryParams,
+ queryParamsRegEx,
+} from "utils/ApiPaneUtils";
function* syncApiParamsSaga(
actionPayload: ReduxActionWithMeta,
@@ -76,58 +80,25 @@ function* syncApiParamsSaga(
) {
const field = actionPayload.meta.field;
//Payload here contains the path and query params of a typical url like https://{domain}/{path}?{query_params}
- let value = actionPayload.payload;
+ const value = actionPayload.payload;
// Regular expression to find the query params group
- const padQueryParams = { key: "", value: "" };
- const queryParamsRegEx = /(\/[\s\S]*?)(\?(?![^{]*})[\s\S]*)?$/;
PerformanceTracker.startTracking(PerformanceTransactionName.SYNC_PARAMS_SAGA);
if (field === "actionConfiguration.path") {
- value = (value.match(queryParamsRegEx) || [])[2] || "";
- if (value.indexOf("?") > -1) {
- const paramsString = value.substr(value.indexOf("?") + 1);
- const params = paramsString.split("&").map((p) => {
- const firstEqualPos = p.indexOf("=");
- const keyValue =
- firstEqualPos > -1
- ? [p.substring(0, firstEqualPos), p.substring(firstEqualPos + 1)]
- : [];
- return { key: keyValue[0] || "", value: keyValue[1] || "" };
- });
- if (params.length < 2) {
- while (params.length < 2) {
- params.push(padQueryParams);
- }
- }
- yield put(
- autofill(
- API_EDITOR_FORM_NAME,
- "actionConfiguration.queryParameters",
- params,
- ),
- );
- yield put(
- setActionProperty({
- actionId: actionId,
- propertyName: "actionConfiguration.queryParameters",
- value: params,
- }),
- );
- } else {
- yield put(
- autofill(
- API_EDITOR_FORM_NAME,
- "actionConfiguration.queryParameters",
- Array(2).fill(padQueryParams),
- ),
- );
- yield put(
- setActionProperty({
- actionId: actionId,
- propertyName: "actionConfiguration.queryParameters",
- value: Array(2).fill(padQueryParams),
- }),
- );
- }
+ const params = parseUrlForQueryParams(value);
+ yield put(
+ autofill(
+ API_EDITOR_FORM_NAME,
+ "actionConfiguration.queryParameters",
+ params,
+ ),
+ );
+ yield put(
+ setActionProperty({
+ actionId: actionId,
+ propertyName: "actionConfiguration.queryParameters",
+ value: params,
+ }),
+ );
} else if (field.includes("actionConfiguration.queryParameters")) {
const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
const path = values.actionConfiguration.path || "";
diff --git a/app/client/src/sagas/ApplicationSagas.tsx b/app/client/src/sagas/ApplicationSagas.tsx
index 5346152181..e5a08dd70c 100644
--- a/app/client/src/sagas/ApplicationSagas.tsx
+++ b/app/client/src/sagas/ApplicationSagas.tsx
@@ -20,6 +20,7 @@ import ApplicationApi, {
PublishApplicationResponse,
SetDefaultPageRequest,
UpdateApplicationRequest,
+ ImportApplicationRequest,
} from "api/ApplicationApi";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
@@ -55,8 +56,11 @@ import {
getCurrentPageId,
} from "selectors/editorSelectors";
import { showCompletionDialog } from "./OnboardingSagas";
+
import { deleteRecentAppEntities } from "utils/storage";
import { reconnectWebsocket as reconnectWebsocketAction } from "actions/websocketActions";
+import { getCurrentOrg } from "selectors/organizationSelectors";
+import { Org } from "constants/orgConstants";
const getDefaultPageId = (
pages?: ApplicationPagePayload[],
@@ -502,6 +506,53 @@ export function* forkApplicationSaga(
}
}
+export function* importApplicationSaga(
+ action: ReduxAction,
+) {
+ try {
+ const response: ApiResponse = yield call(
+ ApplicationApi.importApplicationToOrg,
+ action.payload,
+ );
+ const isValidResponse = yield validateResponse(response);
+ if (isValidResponse) {
+ const allOrgs = yield select(getCurrentOrg);
+ const currentOrg = allOrgs.filter(
+ (el: Org) => el.id === action.payload.orgId,
+ );
+ if (currentOrg.length > 0) {
+ const {
+ id: appId,
+ pages,
+ }: {
+ id: string;
+ pages: { default?: boolean; id: string; isDefault?: boolean }[];
+ } = response.data;
+ yield put({
+ type: ReduxActionTypes.IMPORT_APPLICATION_SUCCESS,
+ payload: {
+ importedApplication: appId,
+ },
+ });
+ const defaultPage = pages.filter((eachPage) => !!eachPage.isDefault);
+ const pageURL = BUILDER_PAGE_URL(appId, defaultPage[0].id);
+ history.push(pageURL);
+ Toaster.show({
+ text: "Application imported successfully",
+ variant: Variant.success,
+ });
+ }
+ }
+ } catch (error) {
+ yield put({
+ type: ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR,
+ payload: {
+ error,
+ },
+ });
+ }
+}
+
export default function* applicationSagas() {
yield all([
takeLatest(
@@ -530,5 +581,6 @@ export default function* applicationSagas() {
ReduxActionTypes.DUPLICATE_APPLICATION_INIT,
duplicateApplicationSaga,
),
+ takeLatest(ReduxActionTypes.IMPORT_APPLICATION_INIT, importApplicationSaga),
]);
}
diff --git a/app/client/src/sagas/CommentSagas/handleCommentEvents.ts b/app/client/src/sagas/CommentSagas/handleCommentEvents.ts
deleted file mode 100644
index f2265e7555..0000000000
--- a/app/client/src/sagas/CommentSagas/handleCommentEvents.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { put } from "redux-saga/effects";
-import {
- setCommentThreadsSuccess,
- newCommentEvent,
- newCommentThreadEvent,
- updateCommentThreadEvent,
- updateCommentEvent,
-} from "actions/commentActions";
-import { COMMENT_EVENTS } from "constants/CommentConstants";
-import { reduceCommentsByRef } from "comments/utils";
-
-export default function* handleCommentEvents(event: any) {
- switch (event.type) {
- case COMMENT_EVENTS.SET_COMMENTS: {
- const comments = event.payload;
- const payload = reduceCommentsByRef(comments);
- yield put(setCommentThreadsSuccess(payload));
- return;
- }
- case COMMENT_EVENTS.INSERT_COMMENT_THREAD: {
- yield put(newCommentThreadEvent(event.payload[0]));
- return;
- }
- case COMMENT_EVENTS.INSERT_COMMENT: {
- yield put(newCommentEvent(event.payload[0]));
- return;
- }
- case COMMENT_EVENTS.UPDATE_COMMENT_THREAD: {
- yield put(updateCommentThreadEvent(event.payload[0].thread));
- return;
- }
- case COMMENT_EVENTS.UPDATE_COMMENT: {
- yield put(updateCommentEvent(event.payload[0].comment));
- return;
- }
- }
-}
diff --git a/app/client/src/sagas/CommentSagas/index.ts b/app/client/src/sagas/CommentSagas/index.ts
index d26796a8d6..1b0585d4b4 100644
--- a/app/client/src/sagas/CommentSagas/index.ts
+++ b/app/client/src/sagas/CommentSagas/index.ts
@@ -1,22 +1,6 @@
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
+import { put, takeLatest, all, call, fork, select } from "redux-saga/effects";
import {
- put,
- takeLatest,
- take,
- all,
- call,
- actionChannel,
- fork,
- select,
-} from "redux-saga/effects";
-// import { updateLayout, getTestComments } from "comments/init";
-import {
- COMMENT_EVENTS_CHANNEL,
- // COMMENT_EVENTS,
-} from "constants/CommentConstants";
-import handleCommentEvents from "./handleCommentEvents";
-import {
- // commentEvent,
createUnpublishedCommentThreadSuccess,
removeUnpublishedCommentThreads,
createCommentThreadSuccess,
@@ -40,11 +24,12 @@ import { waitForFetchUserSuccess } from "sagas/userSagas";
import CommentsApi from "api/CommentsAPI";
-// import { getAppsmithConfigs } from "configs";
-
import { validateResponse } from "../ErrorSagas";
-import { getCurrentApplicationId } from "selectors/editorSelectors";
+import {
+ getCurrentApplicationId,
+ getCurrentPageId,
+} from "selectors/editorSelectors";
import {
AddCommentToCommentThreadRequestPayload,
CreateCommentThreadPayload,
@@ -53,38 +38,9 @@ import {
import { RawDraftContentState } from "draft-js";
import { getCurrentUser } from "selectors/usersSelectors";
import { get } from "lodash";
-import { getCurrentApplication } from "selectors/applicationSelectors";
import { commentModeSelector } from "selectors/commentsSelectors";
-// const { commentsTestModeEnabled } = getAppsmithConfigs();
-// export function* initCommentThreads() {
-// if (!commentsTestModeEnabled) return;
-// try {
-// yield race([
-// take(ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS),
-// take(ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS),
-// ]);
-// yield put(updateLayout());
-// yield put(
-// commentEvent({
-// type: COMMENT_EVENTS.SET_COMMENTS,
-// payload: getTestComments(),
-// }),
-// );
-// } catch (err) {
-// console.log(err, "err");
-// }
-// }
-
-function* watchCommentEvents() {
- const requestChan = yield actionChannel(COMMENT_EVENTS_CHANNEL);
- while (true) {
- const { payload } = yield take(requestChan);
- yield fork(handleCommentEvents, payload);
- }
-}
-
function* createUnpublishedCommentThread(
action: ReduxAction>,
) {
@@ -100,9 +56,11 @@ function* createCommentThread(action: ReduxAction) {
action.payload,
);
const applicationId = yield select(getCurrentApplicationId);
+ const pageId = yield select(getCurrentPageId);
const response = yield call(CommentsApi.createNewThread, {
...newCommentThreadPayload,
applicationId,
+ pageId,
});
const isValidResponse = yield validateResponse(response);
@@ -266,30 +224,17 @@ function* deleteCommentThread(action: ReduxAction) {
}
function* setIfCommentsAreEnabled() {
- while (true) {
- // Reset if comments are enabled when appview access is updated
- yield take([
- ReduxActionTypes.FETCH_APPLICATION_SUCCESS,
- ReduxActionTypes.CHANGE_APPVIEW_ACCESS_SUCCESS,
- ]);
+ yield call(waitForFetchUserSuccess);
- yield call(waitForInit);
- yield call(waitForFetchUserSuccess);
+ const user = yield select(getCurrentUser);
+ const email = get(user, "email", "");
+ const isAppsmithEmail = email.toLowerCase().indexOf("@appsmith.com") !== -1;
- const user = yield select(getCurrentUser);
- const email = get(user, "email", "");
- const isAppsmithEmail = email.toLowerCase().indexOf("@appsmith.com") !== -1;
+ const isCommentModeEnabled = isAppsmithEmail;
+ yield put(setAreCommentsEnabled(isAppsmithEmail));
- const currentApplication = yield select(getCurrentApplication);
-
- const isModeEnaabledForAppAndUser =
- isAppsmithEmail && !currentApplication?.isPublic;
- yield put(setAreCommentsEnabled(isModeEnaabledForAppAndUser));
-
- const isCommentMode = yield select(commentModeSelector);
- if (isCommentMode && !isModeEnaabledForAppAndUser)
- yield put(setCommentMode(false));
- }
+ const isCommentMode = yield select(commentModeSelector);
+ if (isCommentMode && !isCommentModeEnabled) yield put(setCommentMode(false));
}
function* addCommentReaction(
@@ -318,7 +263,6 @@ function* deleteCommentReaction(
export default function* commentSagas() {
yield all([
- // takeLatest(ReduxActionTypes.INIT_COMMENT_THREADS, initCommentThreads),
takeLatest(
ReduxActionTypes.FETCH_APPLICATION_COMMENTS_REQUEST,
fetchApplicationComments,
@@ -339,7 +283,6 @@ export default function* commentSagas() {
ReduxActionTypes.SET_COMMENT_THREAD_RESOLUTION_REQUEST,
setCommentResolution,
),
- call(watchCommentEvents),
takeLatest(ReduxActionTypes.PIN_COMMENT_THREAD_REQUEST, pinCommentThread),
takeLatest(ReduxActionTypes.DELETE_COMMENT_REQUEST, deleteComment),
takeLatest(ReduxActionTypes.MARK_THREAD_AS_READ_REQUEST, markThreadAsRead),
diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts
index 261d50fc2b..7db8ea5cf6 100644
--- a/app/client/src/sagas/DatasourcesSagas.ts
+++ b/app/client/src/sagas/DatasourcesSagas.ts
@@ -76,6 +76,7 @@ import { APPSMITH_TOKEN_STORAGE_KEY } from "pages/Editor/SaaSEditor/constants";
import { checkAndGetPluginFormConfigsSaga } from "sagas/PluginSagas";
import { PluginType } from "entities/Action";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
+import { isDynamicValue } from "utils/DynamicBindingUtils";
function* fetchDatasourcesSaga() {
try {
@@ -573,7 +574,21 @@ function* storeAsDatasourceSaga() {
const pageId = yield select(getCurrentPageId);
let datasource = _.get(values, "datasource");
datasource = _.omit(datasource, ["name"]);
-
+ const originalHeaders = _.get(values, "actionConfiguration.headers", []);
+ const [datasourceHeaders, actionHeaders] = _.partition(
+ originalHeaders,
+ ({ key, value }: { key: string; value: string }) => {
+ return !(isDynamicValue(key) || isDynamicValue(value));
+ },
+ );
+ yield put(
+ setActionProperty({
+ actionId: values.id,
+ propertyName: "actionConfiguration.headers",
+ value: actionHeaders,
+ }),
+ );
+ _.set(datasource, "datasourceConfiguration.headers", datasourceHeaders);
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId));
yield put(createDatasourceFromForm(datasource));
diff --git a/app/client/src/sagas/DebuggerSagas.ts b/app/client/src/sagas/DebuggerSagas.ts
index db2122159d..a8ddde6731 100644
--- a/app/client/src/sagas/DebuggerSagas.ts
+++ b/app/client/src/sagas/DebuggerSagas.ts
@@ -1,73 +1,12 @@
import { debuggerLog, errorLog, updateErrorLog } from "actions/debuggerActions";
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
-import { WidgetTypes } from "constants/WidgetConstants";
import { LogActionPayload, Message } from "entities/AppsmithConsole";
-import {
- all,
- put,
- takeEvery,
- select,
- take,
- fork,
- call,
-} from "redux-saga/effects";
-import { getDataTree } from "selectors/dataTreeSelectors";
-import { isEmpty, set, get } from "lodash";
+import { all, call, fork, put, select, takeEvery } from "redux-saga/effects";
+import { set } from "lodash";
import { getDebuggerErrors } from "selectors/debuggerSelectors";
import { getAction } from "selectors/entitiesSelector";
import { Action, PluginType } from "entities/Action";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
-import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory";
-import { isWidget } from "workers/evaluationUtils";
-import { getWidget } from "./selectors";
-import { WidgetProps } from "widgets/BaseWidget";
-
-function* onWidgetUpdateSaga(payload: LogActionPayload) {
- if (!payload.source) return;
- // Wait for data tree update
- yield take(ReduxActionTypes.SET_EVALUATED_TREE);
- const dataTree: DataTree = yield select(getDataTree);
- const widget = dataTree[payload.source.name];
-
- if (
- !isWidget(widget) ||
- !widget.validationMessages ||
- !widget.jsErrorMessages
- )
- return;
-
- // Ignore canvas widget updates
- if (widget.type === WidgetTypes.CANVAS_WIDGET) {
- return;
- }
- const source = payload.source;
-
- // If widget properties no longer have validation errors update the same
- if (payload.state) {
- const propertyPath = Object.keys(payload.state)[0];
-
- const validationMessages = widget.validationMessages;
- const validationMessage = validationMessages[propertyPath];
- const jsErrorMessages = widget.jsErrorMessages;
- const jsErrorMessage = jsErrorMessages[propertyPath];
- const errors = yield select(getDebuggerErrors);
- const errorId = `${source.id}-${propertyPath}`;
- const widgetErrorLog = errors[errorId];
- if (!widgetErrorLog) return;
-
- const noError = isEmpty(validationMessage);
- const noJsError = isEmpty(jsErrorMessage);
-
- if (noError && noJsError) {
- delete errors[errorId];
-
- yield put({
- type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
- payload: errors,
- });
- }
- }
-}
function* formatActionRequestSaga(payload: LogActionPayload, request?: any) {
if (!payload.source || !payload.state || !request || !request.headers) {
@@ -128,7 +67,9 @@ function* debuggerLogSaga(action: ReduxAction) {
switch (payload.logType) {
case LOG_TYPE.WIDGET_UPDATE:
- yield call(onWidgetUpdateSaga, payload);
+ yield put(debuggerLog(payload));
+ return;
+ case LOG_TYPE.ACTION_UPDATE:
yield put(debuggerLog(payload));
return;
case LOG_TYPE.EVAL_ERROR:
@@ -178,42 +119,6 @@ function* debuggerLogSaga(action: ReduxAction) {
}
}
-// Pass through error list once after on page load actions executions are complete
-function* onExecutePageActionsCompleteSaga() {
- yield take(ReduxActionTypes.SET_EVALUATED_TREE);
-
- const dataTree: DataTree = yield select(getDataTree);
- const errors = yield select(getDebuggerErrors);
- const updatedErrors = { ...errors };
- const errorIds = Object.keys(errors);
-
- for (const id of errorIds) {
- const splits = id.split("-");
- const entityId = splits[0];
- const propertyName = splits[1];
- const widget: WidgetProps | null = yield select(getWidget, entityId);
-
- if (widget) {
- const dataTreeWidget = dataTree[widget.widgetName] as DataTreeWidget;
-
- if (!get(dataTreeWidget.invalidProps, propertyName, null)) {
- delete updatedErrors[id];
- }
- }
- }
-
- yield put({
- type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
- payload: updatedErrors,
- });
-}
-
export default function* debuggerSagasListeners() {
- yield all([
- takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga),
- takeEvery(
- ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE,
- onExecutePageActionsCompleteSaga,
- ),
- ]);
+ yield all([takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga)]);
}
diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts
index 4991934f22..947ad83fc3 100644
--- a/app/client/src/sagas/EvaluationsSaga.ts
+++ b/app/client/src/sagas/EvaluationsSaga.ts
@@ -28,29 +28,127 @@ import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, {
PerformanceTransactionName,
} from "../utils/PerformanceTracker";
-import { Variant } from "components/ads/common";
-import { Toaster } from "components/ads/Toast";
import * as Sentry from "@sentry/react";
import { Action } from "redux";
import _ from "lodash";
+import { ENTITY_TYPE, Message, Severity } from "entities/AppsmithConsole";
+import LOG_TYPE from "entities/AppsmithConsole/logtype";
+import { DataTree } from "entities/DataTree/dataTreeFactory";
+import { AppState } from "reducers";
+import {
+ getEntityNameAndPropertyPath,
+ isAction,
+ isWidget,
+} from "workers/evaluationUtils";
+import moment from "moment/moment";
+import { Toaster } from "components/ads/Toast";
+import { Variant } from "components/ads/common";
+import AppsmithConsole from "utils/AppsmithConsole";
+import AnalyticsUtil from "utils/AnalyticsUtil";
import {
createMessage,
ERROR_EVAL_ERROR_GENERIC,
ERROR_EVAL_TRIGGER,
} from "constants/messages";
-import AppsmithConsole from "utils/AppsmithConsole";
-import LOG_TYPE from "entities/AppsmithConsole/logtype";
-import AnalyticsUtil from "utils/AnalyticsUtil";
let widgetTypeConfigMap: WidgetTypeConfigMap;
const worker = new GracefulWorkerService(Worker);
-const evalErrorHandler = (errors: EvalError[]) => {
- if (!errors) return;
+const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors;
+
+function getLatestEvalPropertyErrors(
+ currentDebuggerErrors: Record,
+ dataTree: DataTree,
+ evaluationOrder: Array,
+) {
+ const updatedDebuggerErrors: Record = {
+ ...currentDebuggerErrors,
+ };
+
+ for (const evaluatedPath of evaluationOrder) {
+ const { entityName, propertyPath } = getEntityNameAndPropertyPath(
+ evaluatedPath,
+ );
+ const entity = dataTree[entityName];
+ if (isWidget(entity) || isAction(entity)) {
+ if (propertyPath in entity.logBlackList) {
+ continue;
+ }
+ const jsError = _.get(entity, `jsErrorMessages.${propertyPath}`, "");
+ const validationError = _.get(
+ entity,
+ `validationMessages.${propertyPath}`,
+ "",
+ );
+ const evaluatedValue = _.get(
+ entity,
+ `evaluatedValues.${propertyPath}`,
+ "",
+ );
+ const error = jsError || validationError;
+ const idField = isWidget(entity) ? entity.widgetId : entity.actionId;
+ const nameField = isWidget(entity) ? entity.widgetName : entity.name;
+ const entityType = isWidget(entity)
+ ? ENTITY_TYPE.WIDGET
+ : ENTITY_TYPE.ACTION;
+ const debuggerKey = idField + "-" + propertyPath;
+ // if dataTree has error but debugger does not -> add
+ // if debugger has error and data tree has error -> update error
+ // if debugger has error but data tree does not -> remove
+ // if debugger or data tree does not have an error -> no change
+
+ if (_.isString(error) && error !== "") {
+ // Add or update
+ updatedDebuggerErrors[debuggerKey] = {
+ logType: LOG_TYPE.EVAL_ERROR,
+ text: `The value at ${propertyPath} is invalid`,
+ message: error,
+ severity: Severity.ERROR,
+ timestamp: moment().format("hh:mm:ss"),
+ source: {
+ id: idField,
+ name: nameField,
+ type: entityType,
+ propertyPath: propertyPath,
+ },
+ state: {
+ value: evaluatedValue,
+ },
+ };
+ } else if (debuggerKey in updatedDebuggerErrors) {
+ // Remove
+ delete updatedDebuggerErrors[debuggerKey];
+ }
+ }
+ }
+ return updatedDebuggerErrors;
+}
+
+function* evalErrorHandler(
+ errors: EvalError[],
+ dataTree?: DataTree,
+ evaluationOrder?: Array,
+): any {
+ if (dataTree && evaluationOrder) {
+ const currentDebuggerErrors: Record = yield select(
+ getDebuggerErrors,
+ );
+ const evalPropertyErrors = getLatestEvalPropertyErrors(
+ currentDebuggerErrors,
+ dataTree,
+ evaluationOrder,
+ );
+
+ yield put({
+ type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
+ payload: evalPropertyErrors,
+ });
+ }
+
errors.forEach((error) => {
switch (error.type) {
- case EvalErrorTypes.DEPENDENCY_ERROR: {
+ case EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR: {
if (error.context) {
// Add more info about node for the toast
const { entityType, node } = error.context;
@@ -104,33 +202,13 @@ const evalErrorHandler = (errors: EvalError[]) => {
});
break;
}
- case EvalErrorTypes.EVAL_ERROR: {
- log.debug(error);
- AppsmithConsole.error({
- logType: LOG_TYPE.EVAL_ERROR,
- text: `The value at ${error.context?.source.propertyPath} is invalid`,
- message: error.message,
- source: error.context?.source,
- });
- break;
- }
- case EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR: {
- AppsmithConsole.error({
- logType: LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR,
- text: `The value at ${error.context?.source.propertyPath} is invalid`,
- message: error.message,
- source: error.context?.source,
- state: error.context?.state,
- });
- break;
- }
default: {
Sentry.captureException(error);
log.debug(error);
}
}
});
-};
+}
function* postEvalActionDispatcher(
actions: Array | ReduxActionWithoutPayload>,
@@ -156,10 +234,16 @@ function* evaluateTreeSaga(
widgetTypeConfigMap,
},
);
- const { dataTree, dependencies, errors, logs } = workerResponse;
+ const {
+ dataTree,
+ dependencies,
+ errors,
+ evaluationOrder,
+ logs,
+ } = workerResponse;
log.debug({ dataTree: dataTree });
logs.forEach((evalLog: any) => log.debug(evalLog));
- evalErrorHandler(errors);
+ yield call(evalErrorHandler, errors, dataTree, evaluationOrder);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION,
);
@@ -197,7 +281,7 @@ export function* evaluateActionBindings(
const { errors, values } = workerResponse;
- evalErrorHandler(errors);
+ yield call(evalErrorHandler, errors);
return values;
}
@@ -214,7 +298,7 @@ export function* evaluateDynamicTrigger(
);
const { errors, triggers } = workerResponse;
- evalErrorHandler(errors);
+ yield call(evalErrorHandler, errors);
return triggers;
}
@@ -302,7 +386,6 @@ const EVALUATE_REDUX_ACTIONS = [
];
const shouldProcessAction = (action: ReduxAction) => {
- // debugger;
if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload)
diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts
index fdcaf0683e..0c69a82821 100644
--- a/app/client/src/sagas/InitSagas.ts
+++ b/app/client/src/sagas/InitSagas.ts
@@ -41,7 +41,6 @@ import {
restoreRecentEntitiesRequest,
} from "actions/globalSearchActions";
import { resetEditorSuccess } from "actions/initActions";
-import { initCommentThreads } from "actions/commentActions";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
@@ -154,9 +153,6 @@ function* initializeEditorSaga(
appName: appName,
});
- // todo remove (for dev)
- yield put(initCommentThreads());
-
yield put({
type: ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS,
});
@@ -249,9 +245,6 @@ export function* initializeAppViewerSaga(
yield put(setAppMode(APP_MODE.PUBLISHED));
- // todo remove (for dev)
- yield put(initCommentThreads());
-
yield put({
type: ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS,
});
diff --git a/app/client/src/sagas/NotificationsSagas.ts b/app/client/src/sagas/NotificationsSagas.ts
new file mode 100644
index 0000000000..b68ffba1a7
--- /dev/null
+++ b/app/client/src/sagas/NotificationsSagas.ts
@@ -0,0 +1,47 @@
+import { call, takeLatest, put, all } from "redux-saga/effects";
+import NotificationApi from "api/NotificationsAPI";
+import { ReduxActionTypes } from "constants/ReduxActionConstants";
+import { validateResponse } from "./ErrorSagas";
+
+// import { markAllNotificationsAsReadSuccess } from "actions/notificationActions";
+
+export function* fetchNotifications() {
+ try {
+ const response = yield call(NotificationApi.fetchNotifications);
+ const isValidResponse = yield validateResponse(response);
+ if (isValidResponse) {
+ yield put({
+ type: ReduxActionTypes.FETCH_NOTIFICATIONS_SUCCESS,
+ payload: { notifications: response.data },
+ });
+ }
+ } catch (error) {
+ console.log(error, "error");
+ }
+}
+
+// TODO implement mark all notifications as read
+function* markAllNotificationsAsRead() {
+ // try {
+ // const response = yield call(NotificationApi.markAllNotificationsAsRead);
+ // const isValidResponse = yield validateResponse(response);
+ // if (isValidResponse) {
+ // yield put(markAllNotificationsAsReadSuccess());
+ // }
+ // } catch (error) {
+ // console.log(error, "error");
+ // }
+}
+
+export default function* notificationsSagas() {
+ yield all([
+ takeLatest(
+ ReduxActionTypes.FETCH_NOTIFICATIONS_REQUEST,
+ fetchNotifications,
+ ),
+ takeLatest(
+ ReduxActionTypes.MARK_ALL_NOTIFICATIONS_AS_READ_REQUEST,
+ markAllNotificationsAsRead,
+ ),
+ ]);
+}
diff --git a/app/client/src/sagas/OrgSagas.ts b/app/client/src/sagas/OrgSagas.ts
index 201263b504..a7158c54ca 100644
--- a/app/client/src/sagas/OrgSagas.ts
+++ b/app/client/src/sagas/OrgSagas.ts
@@ -28,8 +28,10 @@ import { ApiResponse } from "api/ApiResponses";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import { getCurrentOrg } from "selectors/organizationSelectors";
+import { getCurrentUser } from "selectors/usersSelectors";
import { Org } from "constants/orgConstants";
import history from "utils/history";
+import { APPLICATIONS_URL } from "constants/routes";
import { getAllApplications } from "actions/applicationActions";
import log from "loglevel";
@@ -133,12 +135,17 @@ export function* deleteOrgUserSaga(action: ReduxAction) {
const response: ApiResponse = yield call(OrgApi.deleteOrgUser, request);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
- yield put({
- type: ReduxActionTypes.DELETE_ORG_USER_SUCCESS,
- payload: {
- username: action.payload.username,
- },
- });
+ const currentUser = yield select(getCurrentUser);
+ if (currentUser?.username == action.payload.username) {
+ history.replace(APPLICATIONS_URL);
+ } else {
+ yield put({
+ type: ReduxActionTypes.DELETE_ORG_USER_SUCCESS,
+ payload: {
+ username: action.payload.username,
+ },
+ });
+ }
Toaster.show({
text: `${response.data.username} has been removed successfully`,
variant: Variant.success,
diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx
index 4c7d33a305..27d0e09c50 100644
--- a/app/client/src/sagas/PageSagas.tsx
+++ b/app/client/src/sagas/PageSagas.tsx
@@ -371,7 +371,7 @@ function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) {
});
if (error instanceof IncorrectBindingError) {
- const { isRetry } = action.payload;
+ const { isRetry } = action?.payload;
const incorrectBindingError = JSON.parse(error.message);
const { message } = incorrectBindingError;
if (isRetry) {
@@ -428,10 +428,7 @@ function getLayoutSavePayload(
export function* saveLayoutSaga(action: ReduxAction<{ isRetry?: boolean }>) {
try {
- yield put({
- type: ReduxActionTypes.SAVE_PAGE_INIT,
- payload: action.payload,
- });
+ yield put(saveLayout(action.payload.isRetry));
} catch (error) {
yield put({
type: ReduxActionErrorTypes.SAVE_PAGE_ERROR,
diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts
index 018583d8e1..c54f1954f9 100644
--- a/app/client/src/sagas/QueryPaneSagas.ts
+++ b/app/client/src/sagas/QueryPaneSagas.ts
@@ -37,6 +37,7 @@ import { Toaster } from "components/ads/Toast";
import { Datasource } from "entities/Datasource";
import _ from "lodash";
import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages";
+import get from "lodash/get";
function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload;
@@ -114,13 +115,27 @@ function* formValueChangeSaga(
return;
}
- yield put(
- setActionProperty({
- actionId: values.id,
- propertyName: field,
- value: actionPayload.payload,
- }),
- );
+ if (
+ actionPayload.type === ReduxFormActionTypes.ARRAY_REMOVE ||
+ actionPayload.type === ReduxFormActionTypes.ARRAY_PUSH
+ ) {
+ const value = get(values, field);
+ yield put(
+ setActionProperty({
+ actionId: values.id,
+ propertyName: field,
+ value,
+ }),
+ );
+ } else {
+ yield put(
+ setActionProperty({
+ actionId: values.id,
+ propertyName: field,
+ value: actionPayload.payload,
+ }),
+ );
+ }
}
function* handleQueryCreatedSaga(actionPayload: ReduxAction) {
diff --git a/app/client/src/sagas/WebsocketSagas.ts b/app/client/src/sagas/WebsocketSagas/WebsocketSagas.ts
similarity index 96%
rename from app/client/src/sagas/WebsocketSagas.ts
rename to app/client/src/sagas/WebsocketSagas/WebsocketSagas.ts
index 9f1327b68c..a321ef20d4 100644
--- a/app/client/src/sagas/WebsocketSagas.ts
+++ b/app/client/src/sagas/WebsocketSagas/WebsocketSagas.ts
@@ -19,7 +19,6 @@ import {
websocketConnectedEvent,
} from "constants/WebsocketConstants";
-import { commentEvent } from "actions/commentActions";
import {
setIsWebsocketConnected,
retrySocketConnection,
@@ -27,6 +26,8 @@ import {
import { areCommentsEnabledForUserAndApp } from "selectors/commentsSelectors";
+import handleSocketEvent from "./handleSocketEvent";
+
function connect() {
const socket = io();
@@ -68,8 +69,9 @@ function* read(socket: any) {
case WEBSOCKET_EVENTS.CONNECTED:
yield put(setIsWebsocketConnected(true));
break;
- default:
- yield put(commentEvent(action));
+ default: {
+ yield call(handleSocketEvent, action);
+ }
}
}
}
diff --git a/app/client/src/sagas/WebsocketSagas/constants.ts b/app/client/src/sagas/WebsocketSagas/constants.ts
new file mode 100644
index 0000000000..30808d8bdb
--- /dev/null
+++ b/app/client/src/sagas/WebsocketSagas/constants.ts
@@ -0,0 +1,11 @@
+export const SOCKET_EVENTS = {
+ // comment events
+ // SET_COMMENTS: "SET_COMMENTS",
+ INSERT_COMMENT_THREAD: "insert:commentThread",
+ INSERT_COMMENT: "insert:comment",
+ UPDATE_COMMENT_THREAD: "update:commentThread",
+ UPDATE_COMMENT: "update:comment",
+
+ // notification events
+ INSERT_NOTIFICATION: "insert:notification",
+};
diff --git a/app/client/src/sagas/WebsocketSagas/handleSocketEvent.ts b/app/client/src/sagas/WebsocketSagas/handleSocketEvent.ts
new file mode 100644
index 0000000000..86f3fecee8
--- /dev/null
+++ b/app/client/src/sagas/WebsocketSagas/handleSocketEvent.ts
@@ -0,0 +1,38 @@
+import { put } from "redux-saga/effects";
+import { SOCKET_EVENTS } from "./constants";
+
+import {
+ newCommentEvent,
+ newCommentThreadEvent,
+ updateCommentThreadEvent,
+ updateCommentEvent,
+} from "actions/commentActions";
+
+import { newNotificationEvent } from "actions/notificationActions";
+
+export default function* handleSocketEvent(event: any) {
+ switch (event.type) {
+ // comments
+ case SOCKET_EVENTS.INSERT_COMMENT_THREAD: {
+ yield put(newCommentThreadEvent(event.payload[0]));
+ return;
+ }
+ case SOCKET_EVENTS.INSERT_COMMENT: {
+ yield put(newCommentEvent(event.payload[0]));
+ return;
+ }
+ case SOCKET_EVENTS.UPDATE_COMMENT_THREAD: {
+ yield put(updateCommentThreadEvent(event.payload[0].thread));
+ return;
+ }
+ case SOCKET_EVENTS.UPDATE_COMMENT: {
+ yield put(updateCommentEvent(event.payload[0].comment));
+ return;
+ }
+ // notifications
+ case SOCKET_EVENTS.INSERT_NOTIFICATION: {
+ yield put(newNotificationEvent(event.payload[0].notification));
+ return;
+ }
+ }
+}
diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx
index a7e16db98b..7669036f94 100644
--- a/app/client/src/sagas/WidgetOperationSagas.tsx
+++ b/app/client/src/sagas/WidgetOperationSagas.tsx
@@ -1043,6 +1043,7 @@ function* setWidgetDynamicPropertySaga(
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
let widget = cloneDeep({ ...stateWidget });
const propertyValue = _.get(widget, propertyPath);
+
let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(widget);
if (isDynamic) {
const keyExists =
diff --git a/app/client/src/sagas/WidgetOperationUtils.test.ts b/app/client/src/sagas/WidgetOperationUtils.test.ts
index 56bb2fa02e..f91f454753 100644
--- a/app/client/src/sagas/WidgetOperationUtils.test.ts
+++ b/app/client/src/sagas/WidgetOperationUtils.test.ts
@@ -204,4 +204,184 @@ describe("WidgetOperationSaga", () => {
"template.Text2.text",
);
});
+
+ it("should returns correct close model reference name after executing handleSpecificCasesWhilePasting", async () => {
+ const result = handleSpecificCasesWhilePasting(
+ {
+ widgetName: "Modal1Copy",
+ rightColumn: 24,
+ detachFromLayout: true,
+ widgetId: "k441huwm77",
+ topRow: 34,
+ bottomRow: 58,
+ parentRowSpace: 10,
+ canOutsideClickClose: true,
+ type: "MODAL_WIDGET",
+ canEscapeKeyClose: true,
+ version: 1,
+ parentId: "0",
+ shouldScrollContents: true,
+ isLoading: false,
+ parentColumnSpace: 17.21875,
+ size: "MODAL_SMALL",
+ leftColumn: 0,
+ children: ["ihxw5r23hd"],
+ renderMode: "CANVAS",
+ },
+ {
+ k441huwm77: {
+ widgetName: "Modal1Copy",
+ rightColumn: 24,
+ detachFromLayout: true,
+ widgetId: "k441huwm77",
+ topRow: 34,
+ bottomRow: 58,
+ parentRowSpace: 10,
+ canOutsideClickClose: true,
+ type: "MODAL_WIDGET",
+ canEscapeKeyClose: true,
+ version: 1,
+ parentId: "0",
+ shouldScrollContents: true,
+ isLoading: false,
+ parentColumnSpace: 17.21875,
+ size: "MODAL_SMALL",
+ leftColumn: 0,
+ children: ["ihxw5r23hd"],
+ renderMode: "CANVAS",
+ },
+ suhkuyfpk3: {
+ widgetName: "Icon1Copy",
+ rightColumn: 64,
+ onClick: "{{closeModal('Modal1')}}",
+ color: "#040627",
+ iconName: "cross",
+ widgetId: "suhkuyfpk3",
+ topRow: 1,
+ bottomRow: 5,
+ isVisible: true,
+ type: "ICON_WIDGET",
+ version: 1,
+ parentId: "ihxw5r23hd",
+ isLoading: false,
+ leftColumn: 56,
+ iconSize: 24,
+ renderMode: "CANVAS",
+ parentColumnSpace: 2,
+ parentRowSpace: 3,
+ },
+ twnxjwy3r1: {
+ widgetName: "Button1Copy",
+ rightColumn: 48,
+ onClick: "{{closeModal('Modal1')}}",
+ isDefaultClickDisabled: true,
+ widgetId: "twnxjwy3r1",
+ buttonStyle: "SECONDARY_BUTTON",
+ topRow: 16,
+ bottomRow: 20,
+ isVisible: true,
+ type: "BUTTON_WIDGET",
+ version: 1,
+ parentId: "ihxw5r23hd",
+ isLoading: false,
+ dynamicTriggerPathList: [
+ {
+ key: "onClick",
+ },
+ ],
+ leftColumn: 36,
+ dynamicBindingPathList: [],
+ text: "Cancel",
+ isDisabled: false,
+ renderMode: "CANVAS",
+ parentColumnSpace: 2,
+ parentRowSpace: 3,
+ },
+ },
+ {
+ Modal1: "Modal1Copy",
+ Canvas1: "Canvas1Copy",
+ Icon1: "Icon1Copy",
+ Text1: "Text1Copy",
+ Button1: "Button1Copy",
+ Button2: "Button2Copy",
+ },
+ [
+ {
+ widgetName: "Modal1Copy",
+ rightColumn: 24,
+ detachFromLayout: true,
+ widgetId: "k441huwm77",
+ topRow: 34,
+ bottomRow: 58,
+ parentRowSpace: 10,
+ canOutsideClickClose: true,
+ type: "MODAL_WIDGET",
+ canEscapeKeyClose: true,
+ version: 1,
+ parentId: "0",
+ shouldScrollContents: true,
+ isLoading: false,
+ parentColumnSpace: 17.21875,
+ size: "MODAL_SMALL",
+ leftColumn: 0,
+ children: ["ihxw5r23hd"],
+ renderMode: "CANVAS",
+ },
+ {
+ widgetName: "Icon1Copy",
+ rightColumn: 64,
+ onClick: "{{closeModal('Modal1')}}",
+ color: "#040627",
+ iconName: "cross",
+ widgetId: "suhkuyfpk3",
+ topRow: 1,
+ bottomRow: 5,
+ isVisible: true,
+ type: "ICON_WIDGET",
+ version: 1,
+ parentId: "ihxw5r23hd",
+ isLoading: false,
+ leftColumn: 56,
+ iconSize: 24,
+ renderMode: "CANVAS",
+ parentColumnSpace: 2,
+ parentRowSpace: 3,
+ },
+ {
+ widgetName: "Button1Copy",
+ rightColumn: 48,
+ onClick: "{{closeModal('Modal1')}}",
+ isDefaultClickDisabled: true,
+ widgetId: "twnxjwy3r1",
+ buttonStyle: "SECONDARY_BUTTON",
+ topRow: 16,
+ bottomRow: 20,
+ isVisible: true,
+ type: "BUTTON_WIDGET",
+ version: 1,
+ parentId: "ihxw5r23hd",
+ isLoading: false,
+ dynamicTriggerPathList: [
+ {
+ key: "onClick",
+ },
+ ],
+ leftColumn: 36,
+ dynamicBindingPathList: [],
+ text: "Cancel",
+ isDisabled: false,
+ renderMode: "CANVAS",
+ parentColumnSpace: 2,
+ parentRowSpace: 3,
+ },
+ ],
+ );
+ expect(result["suhkuyfpk3"].onClick).toStrictEqual(
+ "{{closeModal('Modal1Copy')}}",
+ );
+ expect(result["twnxjwy3r1"].onClick).toStrictEqual(
+ "{{closeModal('Modal1Copy')}}",
+ );
+ });
});
diff --git a/app/client/src/sagas/WidgetOperationUtils.ts b/app/client/src/sagas/WidgetOperationUtils.ts
index 8a12124c9d..a4e477b5e3 100644
--- a/app/client/src/sagas/WidgetOperationUtils.ts
+++ b/app/client/src/sagas/WidgetOperationUtils.ts
@@ -2,7 +2,7 @@ import {
MAIN_CONTAINER_WIDGET_ID,
WidgetTypes,
} from "constants/WidgetConstants";
-import { cloneDeep, get, isString } from "lodash";
+import { cloneDeep, get, isString, filter, set } from "lodash";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { getDynamicBindings } from "utils/DynamicBindingUtils";
@@ -169,6 +169,28 @@ export const handleSpecificCasesWhilePasting = (
});
widgets[widget.widgetId] = widget;
+ } else if (widget.type === WidgetTypes.MODAL_WIDGET) {
+ // if Modal is being coppied handle all onClose action rename
+ const oldWidgetName = Object.keys(widgetNameMap).find(
+ (key) => widgetNameMap[key] === widget.widgetName,
+ );
+ // get all the button, icon widgets
+ const copiedBtnIcnWidgets = filter(
+ newWidgetList,
+ (copyWidget) =>
+ copyWidget.type === "BUTTON_WIDGET" ||
+ copyWidget.type === "ICON_WIDGET",
+ );
+ // replace oldName with new one if any of this widget have onClick action for old modal
+ copiedBtnIcnWidgets.map((copyWidget) => {
+ if (copyWidget.onClick) {
+ const newOnClick = widgets[copyWidget.widgetId].onClick.replace(
+ oldWidgetName,
+ widget.widgetName,
+ );
+ set(widgets[copyWidget.widgetId], "onClick", newOnClick);
+ }
+ });
}
widgets = handleIfParentIsListWidgetWhilePasting(widget, widgets);
diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx
index fb162bb594..ef47bc620a 100644
--- a/app/client/src/sagas/index.tsx
+++ b/app/client/src/sagas/index.tsx
@@ -27,9 +27,10 @@ import actionExecutionChangeListeners from "./WidgetLoadingSaga";
import globalSearchSagas from "./GlobalSearchSagas";
import recentEntitiesSagas from "./RecentEntitiesSagas";
import commentSagas from "./CommentSagas";
-import websocketSagas from "./WebsocketSagas";
+import websocketSagas from "./WebsocketSagas/WebsocketSagas";
import debuggerSagas from "./DebuggerSagas";
import tourSagas from "./TourSagas";
+import notificationsSagas from "./NotificationsSagas";
import log from "loglevel";
import * as sentry from "@sentry/react";
@@ -67,6 +68,7 @@ const sagas = [
utilSagas,
saaSPaneSagas,
tourSagas,
+ notificationsSagas,
];
export function* rootSaga(sagasToRun = sagas) {
diff --git a/app/client/src/selectors/commentsSelectors.ts b/app/client/src/selectors/commentsSelectors.ts
index 87d219ad4f..231d3c6367 100644
--- a/app/client/src/selectors/commentsSelectors.ts
+++ b/app/client/src/selectors/commentsSelectors.ts
@@ -2,7 +2,6 @@ import { AppState } from "reducers";
import { get } from "lodash";
import { CommentThread, Comment } from "entities/Comments/CommentsInterfaces";
import { options as filterOptions } from "comments/AppComments/AppCommentsFilterPopover";
-import moment from "moment";
export const refCommentThreadsSelector = (
refId: string,
@@ -62,8 +61,11 @@ const getSortIndexTime = (
a: string | number = new Date().toISOString(),
b: string | number = new Date().toISOString(),
) => {
- if (moment(a).isSame(moment(b))) return 0;
- if (moment(a).isAfter(moment(b))) return -1;
+ const tsA = new Date(a).valueOf();
+ const tsB = new Date(b).valueOf();
+
+ if (tsA === tsB) return 0;
+ else if (tsA > tsB) return -1;
else return 1;
};
@@ -81,10 +83,14 @@ export const getSortedAndFilteredAppCommentThreadIds = (
shouldShowResolved: boolean,
appCommentsFilter: typeof filterOptions[number]["value"],
currentUserUsername?: string,
+ currentPageId?: string,
): Array => {
if (!applicationThreadIds) return [];
- return applicationThreadIds
+ const result = applicationThreadIds
.sort((a, b) => {
+ // TODO verify cases where commentThread can be undefined
+ if (!commentThreadsMap[a] || !commentThreadsMap[b]) return -1;
+
const {
pinnedState: isAPinned,
updationTime: updationTimeA,
@@ -94,23 +100,22 @@ export const getSortedAndFilteredAppCommentThreadIds = (
updationTime: updationTimeB,
} = commentThreadsMap[b];
- let sortIdx = getSortIndexBool(!!isAPinned?.active, !!isBPinned?.active);
- if (sortIdx !== 0) return sortIdx;
-
- sortIdx = getSortIndexTime(
- isAPinned?.updationTime?.epochSecond,
- isBPinned?.updationTime?.epochSecond,
+ const sortIdx = getSortIndexBool(
+ !!isAPinned?.active,
+ !!isBPinned?.active,
);
-
if (sortIdx !== 0) return sortIdx;
- return getSortIndexTime(updationTimeA, updationTimeB);
+ const result = getSortIndexTime(updationTimeA, updationTimeB);
+
+ return result;
})
.filter((threadId: string) => {
const thread = commentThreadsMap[threadId];
// Happens during delete thread
if (!thread) return false;
+ if (thread?.pageId !== currentPageId) return false;
const isResolved = thread.resolvedState?.active;
const isPinned = thread.pinnedState?.active;
@@ -131,6 +136,8 @@ export const getSortedAndFilteredAppCommentThreadIds = (
}
}
});
+
+ return result;
};
export const shouldShowResolved = (state: AppState) =>
diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx
index 3203ae1519..97ce93c0e2 100644
--- a/app/client/src/selectors/editorSelectors.tsx
+++ b/app/client/src/selectors/editorSelectors.tsx
@@ -264,6 +264,7 @@ const createLoadingWidget = (
bindingPaths: {},
triggerPaths: {},
validationPaths: {},
+ logBlackList: {},
isLoading: true,
};
};
diff --git a/app/client/src/selectors/notificationSelectors.ts b/app/client/src/selectors/notificationSelectors.ts
new file mode 100644
index 0000000000..0552751d3b
--- /dev/null
+++ b/app/client/src/selectors/notificationSelectors.ts
@@ -0,0 +1,10 @@
+import { AppState } from "reducers";
+
+export const notificationsSelector = (state: AppState) =>
+ state.ui.notifications.notifications;
+
+export const unreadCountSelector = (state: AppState) =>
+ state.ui.notifications.unreadNotificationsCount;
+
+export const isNotificationsListVisibleSelector = (state: AppState) =>
+ state.ui.notifications.showNotificationsMenu;
diff --git a/app/client/src/selectors/ui.ts b/app/client/src/selectors/ui.tsx
similarity index 100%
rename from app/client/src/selectors/ui.ts
rename to app/client/src/selectors/ui.tsx
diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx
index 92d6de3e6e..7a5a7dbb21 100644
--- a/app/client/src/utils/AnalyticsUtil.tsx
+++ b/app/client/src/utils/AnalyticsUtil.tsx
@@ -122,7 +122,8 @@ export type EventName =
| "DEBUGGER_ENTITY_NAVIGATION"
| "GSHEET_AUTH_INIT"
| "GSHEET_AUTH_COMPLETE"
- | "CYCLICAL_DEPENDENCY_ERROR";
+ | "CYCLICAL_DEPENDENCY_ERROR"
+ | "DISCORD_LINK_CLICK";
function getApplicationId(location: Location) {
const pathSplit = location.pathname.split("/");
diff --git a/app/client/src/utils/ApiPaneUtils.test.ts b/app/client/src/utils/ApiPaneUtils.test.ts
index f1640e7bc9..a924bf7e19 100644
--- a/app/client/src/utils/ApiPaneUtils.test.ts
+++ b/app/client/src/utils/ApiPaneUtils.test.ts
@@ -1,7 +1,7 @@
-import { getIndextoUpdate } from "utils/ApiPaneUtils";
+import { getIndextoUpdate, parseUrlForQueryParams } from "utils/ApiPaneUtils";
describe("api pane header insertion or removal", () => {
- describe("index for header needs to be retuened", () => {
+ describe("index for header needs to be returned", () => {
test("it gives correct index", () => {
const headers = [
{ key: "content-type", value: "application/json" },
@@ -29,3 +29,30 @@ describe("api pane header insertion or removal", () => {
});
});
});
+
+describe("Api pane query parameters parsing", () => {
+ test("It gives correct query parameters", () => {
+ const url1 = "user?q=2&b='Auth=xyz'";
+ const params1 = [
+ { key: "q", value: "2" },
+ { key: "b", value: "'Auth=xyz'" },
+ ];
+ expect(parseUrlForQueryParams(url1)).toEqual(params1);
+ const url2 = "/user?q=2&b='Auth=xyz'";
+ expect(parseUrlForQueryParams(url2)).toEqual(params1);
+ const url3 = "user?q=2&b={{Api1.data.isLatest ? 'v1' : 'v2'}}";
+ const params2 = [
+ { key: "q", value: "2" },
+ { key: "b", value: "{{Api1.data.isLatest ? 'v1' : 'v2'}}" },
+ ];
+ expect(parseUrlForQueryParams(url3)).toEqual(params2);
+ const url4 = "";
+ const params3 = [
+ { key: "", value: "" },
+ { key: "", value: "" },
+ ];
+ expect(parseUrlForQueryParams(url4)).toEqual(params3);
+ const url5 = "/";
+ expect(parseUrlForQueryParams(url5)).toEqual(params3);
+ });
+});
diff --git a/app/client/src/utils/ApiPaneUtils.tsx b/app/client/src/utils/ApiPaneUtils.tsx
index b60d8f292b..13d2e158df 100644
--- a/app/client/src/utils/ApiPaneUtils.tsx
+++ b/app/client/src/utils/ApiPaneUtils.tsx
@@ -12,3 +12,26 @@ export const getIndextoUpdate = (
contentTypeHeaderIndex > -1 ? contentTypeHeaderIndex : newHeaderIndex;
return indexToUpdate;
};
+
+export const queryParamsRegEx = /([\s\S]*?)(\?(?![^{]*})[\s\S]*)?$/;
+
+export function parseUrlForQueryParams(url: string) {
+ const padQueryParams = { key: "", value: "" };
+ let params = Array(2).fill(padQueryParams);
+ const matchGroup = url.match(queryParamsRegEx) || [];
+ const parsedUrlWithQueryParams = matchGroup[2] || "";
+ if (parsedUrlWithQueryParams.indexOf("?") > -1) {
+ const paramsString = parsedUrlWithQueryParams.substr(
+ parsedUrlWithQueryParams.indexOf("?") + 1,
+ );
+ params = paramsString.split("&").map((p) => {
+ const firstEqualPos = p.indexOf("=");
+ const keyValue =
+ firstEqualPos > -1
+ ? [p.substring(0, firstEqualPos), p.substring(firstEqualPos + 1)]
+ : [];
+ return { key: keyValue[0] || "", value: keyValue[1] || "" };
+ });
+ }
+ return params;
+}
diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts
index d8cf9ce8e8..30ebd67f4d 100644
--- a/app/client/src/utils/DynamicBindingUtils.ts
+++ b/app/client/src/utils/DynamicBindingUtils.ts
@@ -84,12 +84,10 @@ export const getDynamicBindings = (
};
export enum EvalErrorTypes {
- DEPENDENCY_ERROR = "DEPENDENCY_ERROR",
+ CYCLICAL_DEPENDENCY_ERROR = "CYCLICAL_DEPENDENCY_ERROR",
EVAL_PROPERTY_ERROR = "EVAL_PROPERTY_ERROR",
WIDGET_PROPERTY_VALIDATION_ERROR = "WIDGET_PROPERTY_VALIDATION_ERROR",
EVAL_TREE_ERROR = "EVAL_TREE_ERROR",
- UNESCAPE_STRING_ERROR = "UNESCAPE_STRING_ERROR",
- EVAL_ERROR = "EVAL_ERROR",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
BAD_UNEVAL_TREE_ERROR = "BAD_UNEVAL_TREE_ERROR",
EVAL_TRIGGER_ERROR = "EVAL_TRIGGER_ERROR",
diff --git a/app/client/src/utils/FormControlRegistry.tsx b/app/client/src/utils/FormControlRegistry.tsx
index d66a647f09..c25ae1440d 100644
--- a/app/client/src/utils/FormControlRegistry.tsx
+++ b/app/client/src/utils/FormControlRegistry.tsx
@@ -31,6 +31,9 @@ import DynamicInputTextControl, {
DynamicInputControlProps,
} from "components/formControls/DynamicInputTextControl";
import InputNumberControl from "components/formControls/InputNumberControl";
+import FieldArrayControl, {
+ FieldArrayControlProps,
+} from "components/formControls/FieldArrayControl";
class FormControlRegistry {
static registerFormControlBuilders() {
@@ -93,6 +96,11 @@ class FormControlRegistry {
return ;
},
});
+ FormControlFactory.registerControlBuilder("ARRAY_FIELD", {
+ buildPropertyControl(controlProps: FieldArrayControlProps): JSX.Element {
+ return ;
+ },
+ });
}
}
diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx
index 1f580a6abc..ccb35889cc 100644
--- a/app/client/src/utils/WidgetPropsUtils.tsx
+++ b/app/client/src/utils/WidgetPropsUtils.tsx
@@ -22,12 +22,13 @@ import defaultTemplate from "templates/default";
import { generateReactKey } from "./generators";
import { ChartDataPoint } from "widgets/ChartWidget";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
-import { has, isString, omit, set } from "lodash";
+import { get, has, isString, omit, set } from "lodash";
import log from "loglevel";
import {
migrateTablePrimaryColumnsBindings,
tableWidgetPropertyPaneMigrations,
migrateTableWidgetParentRowSpaceProperty,
+ migrateTableWidgetHeaderVisibilityProperties,
} from "utils/migrations/TableWidget";
import { migrateIncorrectDynamicBindingPathLists } from "utils/migrations/IncorrectDynamicBindingPathLists";
import * as Sentry from "@sentry/react";
@@ -761,9 +762,19 @@ const transformDSL = (currentDSL: ContainerWidgetProps) => {
if (currentDSL.version === 22) {
currentDSL = migrateTableWidgetParentRowSpaceProperty(currentDSL);
+ currentDSL.version = 23;
+ }
+
+ if (currentDSL.version === 23) {
+ currentDSL = migrateTableWidgetHeaderVisibilityProperties(currentDSL);
currentDSL.version = LATEST_PAGE_VERSION;
}
+ if (currentDSL.version === 22) {
+ currentDSL = addLogBlackListToAllListWidgetChildren(currentDSL);
+ currentDSL.version = 23;
+ }
+
return currentDSL;
};
@@ -1164,3 +1175,41 @@ export const generateWidgetProps = (
} else throw Error("Failed to create widget: Parent was not provided ");
}
};
+
+/**
+ * adds logBlackList key for all list widget children
+ *
+ * @param currentDSL
+ * @returns
+ */
+const addLogBlackListToAllListWidgetChildren = (
+ currentDSL: ContainerWidgetProps,
+) => {
+ currentDSL.children = currentDSL.children?.map((children: WidgetProps) => {
+ if (children.type === WidgetTypes.LIST_WIDGET) {
+ const widgets = get(
+ children,
+ "children.0.children.0.children.0.children",
+ );
+
+ widgets.map((widget: any, index: number) => {
+ const logBlackList: { [key: string]: boolean } = {};
+
+ Object.keys(widget).map((key) => {
+ logBlackList[key] = true;
+ });
+ if (!widget.logBlackList) {
+ set(
+ children,
+ `children.0.children.0.children.0.children.${index}.logBlackList`,
+ logBlackList,
+ );
+ }
+ });
+ }
+
+ return children;
+ });
+
+ return currentDSL;
+};
diff --git a/app/client/src/utils/autocomplete/TernServer.ts b/app/client/src/utils/autocomplete/TernServer.ts
index d74b1601be..1cbddcd3b8 100644
--- a/app/client/src/utils/autocomplete/TernServer.ts
+++ b/app/client/src/utils/autocomplete/TernServer.ts
@@ -61,6 +61,7 @@ class TernServer {
cachedArgHints: ArgHints | null = null;
active: any;
expected?: string;
+ entityName?: string;
constructor(
dataTree: DataTree,
@@ -79,8 +80,9 @@ class TernServer {
});
}
- complete(cm: CodeMirror.Editor, expected: string) {
+ complete(cm: CodeMirror.Editor, expected: string, entityName: string) {
this.expected = expected;
+ this.entityName = entityName;
cm.showHint({
hint: this.getHint.bind(this),
completeSingle: false,
@@ -152,15 +154,18 @@ class TernServer {
const completion = data.completions[i];
let className = this.typeToIcon(completion.type);
const dataType = this.getDataType(completion.type);
+ const entityName = this.entityName;
if (data.guess) className += " " + cls + "guess";
- completions.push({
- text: completion.name + after,
- displayText: completion.displayName || completion.name,
- className: className,
- data: completion,
- origin: completion.origin,
- type: dataType,
- });
+ if (!entityName || !completion.name.includes(entityName)) {
+ completions.push({
+ text: completion.name + after,
+ displayText: completion.displayName || completion.name,
+ className: className,
+ data: completion,
+ origin: completion.origin,
+ type: dataType,
+ });
+ }
}
completions = this.sortCompletions(completions);
const indexToBeSelected = completions.length > 1 ? 1 : 0;
@@ -292,7 +297,13 @@ class TernServer {
getExpectedDataType() {
const type = this.expected;
- if (type === "Array" || type === "Array") return "ARRAY";
+ if (
+ type === "Array" ||
+ type === "Array" ||
+ type === "Array<{ label: string, value: string }>" ||
+ type === "Array"
+ )
+ return "ARRAY";
if (type === "boolean") return "BOOLEAN";
if (type === "string") return "STRING";
if (type === "number") return "NUMBER";
diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts
index d12652fc8f..9de77de512 100644
--- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts
+++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts
@@ -90,7 +90,6 @@ describe("dataTreeTypeDefCreator", () => {
"entity1.nested": {
someExtraNested: "string",
},
- "entity1.nested.someExtraNested": "string",
};
const value = flattenObjKeys(options, "entity1");
diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts
index fc7f634dca..67119bdc5f 100644
--- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts
+++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts
@@ -49,10 +49,6 @@ export const dataTreeTypeDefCreator = (dataTree: DataTree) => {
if (entity.ENTITY_TYPE === ENTITY_TYPE.APPSMITH) {
const options: any = generateTypeDef(_.omit(entity, "ENTITY_TYPE"));
def.appsmith = options;
- const flattenedObjects = flattenObjKeys(options, "appsmith");
- for (const [key, value] of Object.entries(flattenedObjects)) {
- def[key] = value;
- }
}
}
});
@@ -101,9 +97,6 @@ export const flattenObjKeys = (
const r: any = results;
for (const [key, value] of Object.entries(options)) {
if (!skipProperties.includes(key)) {
- if (_.isObject(value)) {
- flattenObjKeys(value, parentKey + "." + key, r);
- }
r[parentKey + "." + key] = value;
}
}
diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx
index 5534d658df..2d7a9b6346 100644
--- a/app/client/src/utils/helpers.tsx
+++ b/app/client/src/utils/helpers.tsx
@@ -110,11 +110,13 @@ export const flashElementById = (id: string) => {
};
export const resolveAsSpaceChar = (value: string, limit?: number) => {
- const separatorRegex = /[^\w\s]/;
+ // ensures that all special characters are disallowed
+ // while allowing all utf-8 characters
+ const removeSpecialCharsRegex = /`|\~|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\+|\=|\[|\{|\]|\}|\||\\|\'|\<|\,|\.|\>|\?|\/|\""|\;|\:|\s/;
const duplicateSpaceRegex = /\s+/;
return value
- .split(separatorRegex)
- .join("")
+ .split(removeSpecialCharsRegex)
+ .join(" ")
.split(duplicateSpaceRegex)
.join(" ")
.slice(0, limit || 30);
diff --git a/app/client/src/utils/hooks/useRemoveSignUpCompleteParam.ts b/app/client/src/utils/hooks/useRemoveSignUpCompleteParam.ts
deleted file mode 100644
index 478bc5ffaf..0000000000
--- a/app/client/src/utils/hooks/useRemoveSignUpCompleteParam.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useEffect } from "react";
-import history from "utils/history";
-
-const useRemoveSignUpCompleteParam = () => {
- useEffect(() => {
- if (window.location.href) {
- const url = new URL(window.location.href);
- const searchParams = url.searchParams;
- if (searchParams.get("isFromSignup")) {
- searchParams.delete("isFromSignup");
- history.replace({
- pathname: url.pathname,
- search: url.search,
- hash: url.hash,
- });
- }
- }
- }, []);
-};
-
-export default useRemoveSignUpCompleteParam;
diff --git a/app/client/src/utils/migrations/TableWidget.test.ts b/app/client/src/utils/migrations/TableWidget.test.ts
index a135af9384..e4e4bde2f3 100644
--- a/app/client/src/utils/migrations/TableWidget.test.ts
+++ b/app/client/src/utils/migrations/TableWidget.test.ts
@@ -3,6 +3,7 @@ import { ContainerWidgetProps } from "widgets/ContainerWidget";
import {
tableWidgetPropertyPaneMigrations,
migrateTableWidgetParentRowSpaceProperty,
+ migrateTableWidgetHeaderVisibilityProperties,
} from "./TableWidget";
const input1: ContainerWidgetProps = {
@@ -919,4 +920,286 @@ describe("Table Widget Property Pane Upgrade", () => {
const newDsl = migrateTableWidgetParentRowSpaceProperty(inputDsl);
expect(JSON.stringify(newDsl) === JSON.stringify(outputDsl));
});
+
+ it("TableWidget : should update header options visibilities", () => {
+ const inputDsl: ContainerWidgetProps = {
+ widgetName: "MainContainer",
+ backgroundColor: "none",
+ rightColumn: 1224,
+ snapColumns: 16,
+ detachFromLayout: true,
+ widgetId: "0",
+ topRow: 0,
+ bottomRow: 1840,
+ containerStyle: "none",
+ snapRows: 33,
+ parentRowSpace: 1,
+ type: "CANVAS_WIDGET",
+ canExtend: true,
+ version: 7,
+ minHeight: 1292,
+ parentColumnSpace: 1,
+ dynamicBindingPathList: [],
+ leftColumn: 0,
+ isLoading: false,
+ parentId: "",
+ renderMode: "CANVAS",
+ children: [
+ {
+ isVisible: true,
+ label: "Data",
+ widgetName: "Table1",
+ searchKey: "",
+ tableData:
+ '[\n {\n "id": 2381224,\n "email": "michael.lawson@reqres.in",\n "userName": "Michael Lawson",\n "productName": "Chicken Sandwich",\n "orderAmount": 4.99\n },\n {\n "id": 2736212,\n "email": "lindsay.ferguson@reqres.in",\n "userName": "Lindsay Ferguson",\n "productName": "Tuna Salad",\n "orderAmount": 9.99\n },\n {\n "id": 6788734,\n "email": "tobias.funke@reqres.in",\n "userName": "Tobias Funke",\n "productName": "Beef steak",\n "orderAmount": 19.99\n }\n]',
+ type: "TABLE_WIDGET",
+ isLoading: false,
+ parentColumnSpace: 74,
+ parentRowSpace: 40,
+ leftColumn: 0,
+ rightColumn: 8,
+ topRow: 19,
+ bottomRow: 26,
+ parentId: "0",
+ widgetId: "fs785w9gcy",
+ dynamicBindingPathList: [],
+ primaryColumns: {
+ id: {
+ index: 0,
+ width: 150,
+ id: "id",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "id",
+ computedValue: "",
+ },
+ email: {
+ index: 1,
+ width: 150,
+ id: "email",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "email",
+ computedValue: "",
+ },
+ userName: {
+ index: 2,
+ width: 150,
+ id: "userName",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "userName",
+ computedValue: "",
+ },
+ productName: {
+ index: 3,
+ width: 150,
+ id: "productName",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "productName",
+ computedValue: "",
+ },
+ orderAmount: {
+ index: 4,
+ width: 150,
+ id: "orderAmount",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "orderAmount",
+ computedValue: "",
+ },
+ },
+ textSize: "PARAGRAPH",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ renderMode: "CANVAS",
+ version: 1,
+ },
+ ],
+ };
+ const outputDsl: ContainerWidgetProps = {
+ widgetName: "MainContainer",
+ backgroundColor: "none",
+ rightColumn: 1224,
+ snapColumns: 16,
+ detachFromLayout: true,
+ widgetId: "0",
+ topRow: 0,
+ bottomRow: 1840,
+ containerStyle: "none",
+ snapRows: 33,
+ parentRowSpace: 1,
+ type: "CANVAS_WIDGET",
+ canExtend: true,
+ version: 7,
+ minHeight: 1292,
+ parentColumnSpace: 1,
+ dynamicBindingPathList: [],
+ leftColumn: 0,
+ isLoading: false,
+ parentId: "",
+ renderMode: "CANVAS",
+ children: [
+ {
+ isVisible: true,
+ label: "Data",
+ widgetName: "Table1",
+ searchKey: "",
+ tableData:
+ '[\n {\n "id": 2381224,\n "email": "michael.lawson@reqres.in",\n "userName": "Michael Lawson",\n "productName": "Chicken Sandwich",\n "orderAmount": 4.99\n },\n {\n "id": 2736212,\n "email": "lindsay.ferguson@reqres.in",\n "userName": "Lindsay Ferguson",\n "productName": "Tuna Salad",\n "orderAmount": 9.99\n },\n {\n "id": 6788734,\n "email": "tobias.funke@reqres.in",\n "userName": "Tobias Funke",\n "productName": "Beef steak",\n "orderAmount": 19.99\n }\n]',
+ type: "TABLE_WIDGET",
+ isLoading: false,
+ parentColumnSpace: 74,
+ parentRowSpace: 10,
+ leftColumn: 0,
+ rightColumn: 8,
+ topRow: 19,
+ bottomRow: 26,
+ parentId: "0",
+ widgetId: "fs785w9gcy",
+ dynamicBindingPathList: [],
+ primaryColumns: {
+ id: {
+ index: 0,
+ width: 150,
+ id: "id",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "id",
+ computedValue: "",
+ },
+ email: {
+ index: 1,
+ width: 150,
+ id: "email",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "email",
+ computedValue: "",
+ },
+ userName: {
+ index: 2,
+ width: 150,
+ id: "userName",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "userName",
+ computedValue: "",
+ },
+ productName: {
+ index: 3,
+ width: 150,
+ id: "productName",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "productName",
+ computedValue: "",
+ },
+ orderAmount: {
+ index: 4,
+ width: 150,
+ id: "orderAmount",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "orderAmount",
+ computedValue: "",
+ },
+ },
+ textSize: "PARAGRAPH",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ renderMode: "CANVAS",
+ isVisibleSearch: true,
+ isVisibleFilters: true,
+ isVisibleDownload: true,
+ isVisibleCompactMode: true,
+ isVisiblePagination: true,
+ version: 1,
+ },
+ ],
+ };
+ const newDsl = migrateTableWidgetHeaderVisibilityProperties(inputDsl);
+ expect(JSON.stringify(newDsl) === JSON.stringify(outputDsl));
+ });
});
diff --git a/app/client/src/utils/migrations/TableWidget.ts b/app/client/src/utils/migrations/TableWidget.ts
index edd976764b..52b84364b7 100644
--- a/app/client/src/utils/migrations/TableWidget.ts
+++ b/app/client/src/utils/migrations/TableWidget.ts
@@ -231,3 +231,23 @@ export const migrateTableWidgetParentRowSpaceProperty = (
});
return currentDSL;
};
+
+export const migrateTableWidgetHeaderVisibilityProperties = (
+ currentDSL: ContainerWidgetProps,
+) => {
+ currentDSL.children = currentDSL.children?.map((child: WidgetProps) => {
+ if (child.type === WidgetTypes.TABLE_WIDGET) {
+ if (!("isVisibleSearch" in child)) {
+ child.isVisibleSearch = true;
+ child.isVisibleFilters = true;
+ child.isVisibleDownload = true;
+ child.isVisibleCompactMode = true;
+ child.isVisiblePagination = true;
+ }
+ } else if (child.children && child.children.length > 0) {
+ child = migrateTableWidgetHeaderVisibilityProperties(child);
+ }
+ return child;
+ });
+ return currentDSL;
+};
diff --git a/app/client/src/widgets/ChartWidget/index.tsx b/app/client/src/widgets/ChartWidget/index.tsx
index 84432435a9..b06ec6153c 100644
--- a/app/client/src/widgets/ChartWidget/index.tsx
+++ b/app/client/src/widgets/ChartWidget/index.tsx
@@ -92,7 +92,7 @@ export interface ChartData {
export interface ChartWidgetProps extends WidgetProps, WithMeta {
chartType: ChartType;
chartData: AllChartData;
- customFusionChartConfig: { config: CustomFusionChartConfig };
+ customFusionChartConfig: CustomFusionChartConfig;
xAxisName: string;
yAxisName: string;
chartName: string;
diff --git a/app/client/src/widgets/ChartWidget/propertyConfig.ts b/app/client/src/widgets/ChartWidget/propertyConfig.ts
index 05eff18a0d..0394059f2b 100644
--- a/app/client/src/widgets/ChartWidget/propertyConfig.ts
+++ b/app/client/src/widgets/ChartWidget/propertyConfig.ts
@@ -68,8 +68,8 @@ export default [
children: [
{
helpText:
- "Manually configure a FusionChart, see https://www.fusioncharts.com",
- placeholderText: `Enter {type: "bar2d","dataSource": {}}`,
+ "Manually configure a FusionChart, see https://docs.appsmith.com/widget-reference/chart#custom-chart",
+ placeholderText: `Enter {"type": "bar2d","dataSource": {}}`,
propertyName: "customFusionChartConfig",
label: "Custom Fusion Chart Configuration",
controlType: "CUSTOM_FUSION_CHARTS_DATA",
diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx
index 89c9452c04..90d6176276 100644
--- a/app/client/src/widgets/DropdownWidget.tsx
+++ b/app/client/src/widgets/DropdownWidget.tsx
@@ -152,7 +152,7 @@ class DropdownWidget extends BaseWidget {
}
getPageView() {
- const options = this.props.options || [];
+ const options = _.isArray(this.props.options) ? this.props.options : [];
const selectedIndex = _.findIndex(this.props.options, {
value: this.props.selectedOptionValue,
});
diff --git a/app/client/src/widgets/FormWidget.tsx b/app/client/src/widgets/FormWidget.tsx
index 40af23180a..585320c45f 100644
--- a/app/client/src/widgets/FormWidget.tsx
+++ b/app/client/src/widgets/FormWidget.tsx
@@ -19,7 +19,7 @@ class FormWidget extends ContainerWidget {
label: "Background Color",
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
- controlType: "INPUT_TEXT",
+ controlType: "COLOR_PICKER",
isBindProperty: true,
isTriggerProperty: false,
validation: VALIDATION_TYPES.TEXT,
diff --git a/app/client/src/widgets/ModalWidget.tsx b/app/client/src/widgets/ModalWidget.tsx
index a740c9eaec..6fae51bd08 100644
--- a/app/client/src/widgets/ModalWidget.tsx
+++ b/app/client/src/widgets/ModalWidget.tsx
@@ -3,6 +3,7 @@ import React, { ReactNode } from "react";
import { connect } from "react-redux";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
+import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import WidgetFactory from "utils/WidgetFactory";
import ModalComponent from "components/designSystems/blueprint/ModalComponent";
import {
@@ -70,6 +71,20 @@ export class ModalWidget extends BaseWidget {
},
],
},
+ {
+ sectionName: "Actions",
+ children: [
+ {
+ helpText: "Triggers an action when the modal is closed",
+ propertyName: "onClose",
+ label: "onClose",
+ controlType: "ACTION_SELECTOR",
+ isJSConvertible: true,
+ isBindProperty: true,
+ isTriggerProperty: true,
+ },
+ ],
+ },
];
}
static defaultProps = {
@@ -99,6 +114,18 @@ export class ModalWidget extends BaseWidget {
return WidgetFactory.createWidget(childWidgetData, this.props.renderMode);
};
+ onModalClose = () => {
+ if (this.props.onClose) {
+ super.executeAction({
+ triggerPropertyName: "onClose",
+ dynamicString: this.props.onClose,
+ event: {
+ type: EventType.ON_MODAL_CLOSE,
+ },
+ });
+ }
+ };
+
closeModal = (e: any) => {
this.props.showPropertyPane(undefined);
// TODO(abhinav): Create a static property with is a map of widget properties
@@ -124,6 +151,7 @@ export class ModalWidget extends BaseWidget {
height={MODAL_SIZE[this.props.size].height}
isOpen={!!this.props.isVisible}
onClose={this.closeModal}
+ onModalClose={this.onModalClose}
scrollContents={!!this.props.shouldScrollContents}
width={this.getModalWidth()}
>
@@ -159,6 +187,7 @@ export interface ModalWidgetProps extends WidgetProps, WithMeta {
canEscapeKeyClose?: boolean;
shouldScrollContents?: boolean;
size: string;
+ onClose: string;
mainContainer: WidgetProps;
}
diff --git a/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts b/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts
index 1d8af685c3..b3980b24e9 100644
--- a/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts
+++ b/app/client/src/widgets/TableWidget/TablePropertyPaneConfig.ts
@@ -203,6 +203,24 @@ export default [
isBindProperty: false,
isTriggerProperty: false,
},
+ {
+ propertyName: "displayText",
+ label: "Display Text",
+ controlType: "COMPUTE_VALUE",
+ customJSControl: "COMPUTE_VALUE",
+ updateHook: updateDerivedColumnsHook,
+ hidden: (props: TableWidgetProps, propertyPath: string) => {
+ const baseProperty = getBasePropertyPath(propertyPath);
+ const columnType = get(
+ props,
+ `${baseProperty}.columnType`,
+ "",
+ );
+ return columnType !== "url";
+ },
+ isBindProperty: false,
+ isTriggerProperty: false,
+ },
{
propertyName: "computedValue",
label: "Computed Value",
@@ -683,6 +701,51 @@ export default [
},
],
},
+ {
+ sectionName: "Header options",
+ children: [
+ {
+ helpText: "Toggle visibility of the search box",
+ propertyName: "isVisibleSearch",
+ label: "Search",
+ controlType: "SWITCH",
+ isBindProperty: false,
+ isTriggerProperty: false,
+ },
+ {
+ helpText: "Toggle visibility of the filters",
+ propertyName: "isVisibleFilters",
+ label: "Filters",
+ controlType: "SWITCH",
+ isBindProperty: false,
+ isTriggerProperty: false,
+ },
+ {
+ helpText: "Toggle visibility of the data download",
+ propertyName: "isVisibleDownload",
+ label: "Download",
+ controlType: "SWITCH",
+ isBindProperty: false,
+ isTriggerProperty: false,
+ },
+ {
+ helpText: "Toggle visibility of the compact mode",
+ propertyName: "isVisibleCompactMode",
+ label: "Compact Mode",
+ controlType: "SWITCH",
+ isBindProperty: false,
+ isTriggerProperty: false,
+ },
+ {
+ helpText: "Toggle visibility of the pagination",
+ propertyName: "isVisiblePagination",
+ label: "Pagination",
+ controlType: "SWITCH",
+ isBindProperty: false,
+ isTriggerProperty: false,
+ },
+ ],
+ },
{
sectionName: "Actions",
children: [
diff --git a/app/client/src/widgets/TableWidget/index.tsx b/app/client/src/widgets/TableWidget/index.tsx
index baa72efb0e..f48ca5175b 100644
--- a/app/client/src/widgets/TableWidget/index.tsx
+++ b/app/client/src/widgets/TableWidget/index.tsx
@@ -120,6 +120,11 @@ class TableWidget extends BaseWidget {
textSize: this.getPropertyValue(columnProperties.textSize, rowIndex),
textColor: this.getPropertyValue(columnProperties.textColor, rowIndex),
fontStyle: this.getPropertyValue(columnProperties.fontStyle, rowIndex), //Fix this
+ displayText: this.getPropertyValue(
+ columnProperties.displayText,
+ rowIndex,
+ true,
+ ),
};
return cellProperties;
};
@@ -613,10 +618,24 @@ class TableWidget extends BaseWidget {
};
getPageView() {
- const { pageSize, filteredTableData = [] } = this.props;
+ const {
+ pageSize,
+ filteredTableData = [],
+ isVisibleCompactMode,
+ isVisibleDownload,
+ isVisibleFilters,
+ isVisiblePagination,
+ isVisibleSearch,
+ } = this.props;
const tableColumns = this.getTableColumns() || [];
const transformedData = this.transformData(filteredTableData, tableColumns);
const { componentHeight, componentWidth } = this.getComponentDimensions();
+ const isVisibleHeaderOptions =
+ isVisibleCompactMode ||
+ isVisibleDownload ||
+ isVisibleFilters ||
+ isVisiblePagination ||
+ isVisibleSearch;
return (
}>
@@ -632,12 +651,19 @@ class TableWidget extends BaseWidget {
handleResizeColumn={this.handleResizeColumn}
height={componentHeight}
isLoading={this.props.isLoading}
+ isVisibleCompactMode={isVisibleCompactMode}
+ isVisibleDownload={isVisibleDownload}
+ isVisibleFilters={isVisibleFilters}
+ isVisiblePagination={isVisiblePagination}
+ isVisibleSearch={isVisibleSearch}
multiRowSelection={this.props.multiRowSelection}
nextPageClick={this.handleNextPageClick}
onCommandClick={this.onCommandClick}
onRowClick={this.handleRowClick}
pageNo={this.props.pageNo}
- pageSize={Math.max(1, pageSize)}
+ pageSize={
+ isVisibleHeaderOptions ? Math.max(1, pageSize) : pageSize + 1
+ }
prevPageClick={this.handlePrevPageClick}
searchKey={this.props.searchText}
searchTableData={this.handleSearchTable}
diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts
index 5ac99562ad..b778feb192 100644
--- a/app/client/src/workers/DataTreeEvaluator.ts
+++ b/app/client/src/workers/DataTreeEvaluator.ts
@@ -112,6 +112,10 @@ export default class DataTreeEvaluator {
},
};
this.logs.push({ timeTakenForFirstTree });
+ return {
+ dataTree: this.evalTree,
+ evaluationOrder: this.sortedDependencies,
+ };
}
isDynamicLeaf(unEvalTree: DataTree, propertyPath: string) {
@@ -206,7 +210,10 @@ export default class DataTreeEvaluator {
evaluate: (evalStop - evalStart).toFixed(2),
};
this.logs.push({ timeTakenForSubTreeEval });
- return this.evalTree;
+ return {
+ dataTree: this.evalTree,
+ evaluationOrder: evaluationOrder,
+ };
}
getCompleteSortOrder(
@@ -502,7 +509,7 @@ export default class DataTreeEvaluator {
entityType = entity.pluginType;
}
this.errors.push({
- type: EvalErrorTypes.DEPENDENCY_ERROR,
+ type: EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR,
message: "Cyclic dependency found while evaluating.",
context: {
node,
@@ -612,21 +619,13 @@ export default class DataTreeEvaluator {
fullPropertyPath,
);
_.set(data, `${entityName}.jsErrorMessages.${propertyPath}`, e.message);
- const entity = data[entityName];
- if (isWidget(entity)) {
- this.errors.push({
- type: EvalErrorTypes.EVAL_ERROR,
- message: e.message,
- context: {
- source: {
- id: entity.widgetId,
- name: entity.widgetName,
- type: ENTITY_TYPE.WIDGET,
- propertyPath: propertyPath,
- },
- },
- });
- }
+ } else {
+ // TODO clean up
+ // This is to handle situations with evaluation of triggers for execution
+ this.errors.push({
+ type: EvalErrorTypes.EVAL_PROPERTY_ERROR,
+ message: e.message,
+ });
}
return { result: undefined, triggers: [] };
}
@@ -668,23 +667,7 @@ export default class DataTreeEvaluator {
: transformed;
const safeEvaluatedValue = removeFunctions(evaluatedValue);
_.set(widget, `evaluatedValues.${propertyPath}`, safeEvaluatedValue);
- const jsError = _.get(widget, `jsErrorMessages.${propertyPath}`);
- if (!isValid && !jsError) {
- this.errors.push({
- type: EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR,
- message: message || "",
- context: {
- source: {
- id: widget.widgetId,
- name: widget.widgetName,
- type: ENTITY_TYPE.WIDGET,
- propertyPath: propertyPath,
- },
- state: {
- value: safeEvaluatedValue,
- },
- },
- });
+ if (!isValid) {
_.set(widget, `invalidProps.${propertyPath}`, true);
_.set(widget, `validationMessages.${propertyPath}`, message);
} else {
diff --git a/app/client/src/workers/evaluate.test.ts b/app/client/src/workers/evaluate.test.ts
index d658e35fc4..d708ce2952 100644
--- a/app/client/src/workers/evaluate.test.ts
+++ b/app/client/src/workers/evaluate.test.ts
@@ -34,11 +34,6 @@ describe("evaluate", () => {
const response = evaluate(js, {});
expect(response.result).toBe("Hello!");
});
- it("unescapes string and removes linebreaks before evaluation", () => {
- const js = "'Hello,\\nworld!'";
- const response = evaluate(js, {});
- expect(response.result).toBe("Hello,world!");
- });
it("throws error for undefined js", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
diff --git a/app/client/src/workers/evaluate.ts b/app/client/src/workers/evaluate.ts
index 6e182f1d88..37d9b91465 100644
--- a/app/client/src/workers/evaluate.ts
+++ b/app/client/src/workers/evaluate.ts
@@ -17,7 +17,7 @@ export default function evaluate(
data: DataTree,
callbackData?: Array,
): EvalResult {
- const unescapedJS = unescapeJS(js).replace(/(\r\n|\n|\r)/gm, "");
+ const unescapedJS = unescapeJS(js);
const scriptToEvaluate = `
const result = ${unescapedJS};
return { result, triggers: self.triggers }
diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts
index 56a3d83843..f257d124ba 100644
--- a/app/client/src/workers/evaluation.test.ts
+++ b/app/client/src/workers/evaluation.test.ts
@@ -384,9 +384,9 @@ describe("DataTreeEvaluator", () => {
text: "Hey there",
},
};
- const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
- expect(updatedEvalTree).toHaveProperty("Text2.text", "Hey there");
- expect(updatedEvalTree).toHaveProperty("Text3.text", "Hey there");
+ const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
+ expect(dataTree).toHaveProperty("Text2.text", "Hey there");
+ expect(dataTree).toHaveProperty("Text3.text", "Hey there");
});
it("Evaluates a dependency change in update run", () => {
@@ -397,10 +397,10 @@ describe("DataTreeEvaluator", () => {
text: "Label 3",
},
};
- const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
+ const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap;
- expect(updatedEvalTree).toHaveProperty("Text2.text", "Label");
- expect(updatedEvalTree).toHaveProperty("Text3.text", "Label 3");
+ expect(dataTree).toHaveProperty("Text2.text", "Label");
+ expect(dataTree).toHaveProperty("Text3.text", "Label 3");
expect(updatedDependencyMap).toStrictEqual({
Text1: ["Text1.text"],
Text2: ["Text2.text"],
@@ -445,8 +445,8 @@ describe("DataTreeEvaluator", () => {
},
};
- const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
- expect(updatedEvalTree).toHaveProperty("Input1.text", "Default value");
+ const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
+ expect(dataTree).toHaveProperty("Input1.text", "Default value");
});
it("Evaluates for value changes in nested diff paths", () => {
@@ -481,11 +481,8 @@ describe("DataTreeEvaluator", () => {
},
},
};
- const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
- expect(updatedEvalTree).toHaveProperty(
- "Dropdown2.options.0.label",
- "newValue",
- );
+ const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
+ expect(dataTree).toHaveProperty("Dropdown2.options.0.label", "newValue");
});
it("Adds an entity with a complicated binding", () => {
@@ -504,9 +501,9 @@ describe("DataTreeEvaluator", () => {
],
},
};
- const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
+ const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap;
- expect(updatedEvalTree).toHaveProperty("Table1.tableData", [
+ expect(dataTree).toHaveProperty("Table1.tableData", [
{
test: "Hey",
raw: "Label",
@@ -568,9 +565,9 @@ describe("DataTreeEvaluator", () => {
],
},
};
- const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree);
+ const { dataTree } = evaluator.updateDataTree(updatedUnEvalTree);
const updatedDependencyMap = evaluator.dependencyMap;
- expect(updatedEvalTree).toHaveProperty("Table1.tableData", [
+ expect(dataTree).toHaveProperty("Table1.tableData", [
{
test: "Hey",
raw: "Label",
@@ -580,7 +577,7 @@ describe("DataTreeEvaluator", () => {
raw: "Label",
},
]);
- expect(updatedEvalTree).toHaveProperty("Text4.text", "Hey");
+ expect(dataTree).toHaveProperty("Text4.text", "Hey");
expect(updatedDependencyMap).toStrictEqual({
Api1: ["Api1.data"],
Text1: ["Text1.text"],
@@ -657,14 +654,14 @@ describe("DataTreeEvaluator", () => {
},
},
};
- const evaluatedDataTree2 = evaluator.updateDataTree(updatedTree2);
+ const { dataTree } = evaluator.updateDataTree(updatedTree2);
expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([
"Text1.text",
"Api2.config.pluginSpecifiedTemplates[0].value",
]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- expect(evaluatedDataTree2.Api2.config.body).toBe("{ 'name': Test }");
+ expect(dataTree.Api2.config.body).toBe("{ 'name': Test }");
const updatedTree3 = {
...updatedTree2,
Api2: {
@@ -683,13 +680,14 @@ describe("DataTreeEvaluator", () => {
},
},
};
- const evaluatedDataTree3 = evaluator.updateDataTree(updatedTree3);
+ const evaluatedDataTreeObject = evaluator.updateDataTree(updatedTree3);
+ const dataTree3 = evaluatedDataTreeObject.dataTree;
expect(evaluator.dependencyMap["Api2.config.body"]).toStrictEqual([
"Text1.text",
"Api2.config.pluginSpecifiedTemplates[0].value",
]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- expect(evaluatedDataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }");
+ expect(dataTree3.Api2.config.body).toBe("{ 'name': \"Test\" }");
});
});
diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts
index f08f4f2c30..42d5477c78 100644
--- a/app/client/src/workers/evaluation.worker.ts
+++ b/app/client/src/workers/evaluation.worker.ts
@@ -47,18 +47,24 @@ ctx.addEventListener(
let errors: EvalError[] = [];
let logs: any[] = [];
let dependencies: DependencyMap = {};
+ let dataTreeObject: any = {};
+ let evaluationOrder: Array = [];
try {
if (!dataTreeEvaluator) {
dataTreeEvaluator = new DataTreeEvaluator(widgetTypeConfigMap);
- dataTreeEvaluator.createFirstTree(unevalTree);
- dataTree = dataTreeEvaluator.evalTree;
+ dataTreeObject = dataTreeEvaluator.createFirstTree(unevalTree);
+ dataTree = dataTreeObject.dataTree;
+ evaluationOrder = dataTreeObject.evaluationOrder;
+ // dataTreeEvaluator.sortedDepedencies
} else {
- dataTree = dataTreeEvaluator.updateDataTree(unevalTree);
+ dataTreeObject = dataTreeEvaluator.updateDataTree(unevalTree);
+ dataTree = dataTreeObject.dataTree;
+ evaluationOrder = dataTreeObject.evaluationOrder;
}
// We need to clean it to remove any possible functions inside the tree.
// If functions exist, it will crash the web worker
- dataTree = JSON.parse(JSON.stringify(dataTree));
+ dataTree = dataTree && JSON.parse(JSON.stringify(dataTree));
dependencies = dataTreeEvaluator.inverseDependencyMap;
errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors();
@@ -79,10 +85,12 @@ ctx.addEventListener(
dataTree = getSafeToRenderDataTree(unevalTree, widgetTypeConfigMap);
dataTreeEvaluator = undefined;
}
+ // step 6: eval order
return {
dataTree,
dependencies,
errors,
+ evaluationOrder,
logs,
};
}
@@ -108,7 +116,8 @@ ctx.addEventListener(
if (!dataTreeEvaluator) {
return { triggers: [], errors: [] };
}
- const evalTree = dataTreeEvaluator.updateDataTree(dataTree);
+ dataTreeEvaluator.updateDataTree(dataTree);
+ const evalTree = dataTreeEvaluator.evalTree;
const triggers = dataTreeEvaluator.getDynamicValue(
dynamicTrigger,
evalTree,
@@ -120,7 +129,7 @@ ctx.addEventListener(
// 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) {
+ if (error.type === EvalErrorTypes.EVAL_PROPERTY_ERROR) {
return {
...error,
type: EvalErrorTypes.EVAL_TRIGGER_ERROR,
diff --git a/app/client/src/workers/validations.test.ts b/app/client/src/workers/validations.test.ts
index 4956ddaed4..b1053b5bd2 100644
--- a/app/client/src/workers/validations.test.ts
+++ b/app/client/src/workers/validations.test.ts
@@ -126,7 +126,7 @@ describe("Chart Custom Config validator", () => {
const cases = [
{
input: {
- type: "area2d",
+ type: "area",
dataSource: {
chart: {
caption: "Countries With Most Oil Reserves [2017-18]",
@@ -175,7 +175,7 @@ describe("Chart Custom Config validator", () => {
output: {
isValid: true,
parsed: {
- type: "area2d",
+ type: "area",
dataSource: {
chart: {
caption: "Countries With Most Oil Reserves [2017-18]",
@@ -222,7 +222,7 @@ describe("Chart Custom Config validator", () => {
},
transformed: {
- type: "area2d",
+ type: "area",
dataSource: {
chart: {
caption: "Countries With Most Oil Reserves [2017-18]",
@@ -310,83 +310,14 @@ describe("Chart Custom Config validator", () => {
},
},
output: {
- isValid: true,
+ isValid: false,
+ message:
+ 'This value does not evaluate to type "{type: string, dataSource: { chart: object, data: Array<{label: string, value: number}>}}"',
parsed: {
- type: "area2d",
+ type: "",
dataSource: {
- data: [
- {
- label: "Venezuela",
- value: "290",
- },
- {
- label: "Saudi",
- value: "260",
- },
- {
- label: "Canada",
- value: "180",
- },
- {
- label: "Iran",
- value: "140",
- },
- {
- label: "Russia",
- value: "115",
- },
- {
- label: "UAE",
- value: "100",
- },
- {
- label: "US",
- value: "30",
- },
- {
- label: "China",
- value: "30",
- },
- ],
- },
- },
- transformed: {
- type: "area2d",
- dataSource: {
- data: [
- {
- label: "Venezuela",
- value: "290",
- },
- {
- label: "Saudi",
- value: "260",
- },
- {
- label: "Canada",
- value: "180",
- },
- {
- label: "Iran",
- value: "140",
- },
- {
- label: "Russia",
- value: "115",
- },
- {
- label: "UAE",
- value: "100",
- },
- {
- label: "US",
- value: "30",
- },
- {
- label: "China",
- value: "30",
- },
- ],
+ chart: {},
+ data: [],
},
},
},
diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts
index c374c9d737..53b8378468 100644
--- a/app/client/src/workers/validations.ts
+++ b/app/client/src/workers/validations.ts
@@ -7,6 +7,7 @@ import {
import { DataTree } from "../entities/DataTree/dataTreeFactory";
import _, {
every,
+ indexOf,
isBoolean,
isNil,
isNumber,
@@ -18,6 +19,10 @@ import _, {
toString,
} from "lodash";
import { WidgetProps } from "../widgets/BaseWidget";
+import {
+ CUSTOM_CHART_TYPES,
+ CUSTOM_CHART_DEFAULT_PARSED,
+} from "../constants/CustomChartConstants";
import moment from "moment";
export function validateDateString(
@@ -155,7 +160,7 @@ export const VALIDATORS: Record = {
if (!isValid) {
return {
isValid: isValid,
- parsed: parsed,
+ parsed: !!parsed,
message: `${WIDGET_TYPE_VALIDATION_ERROR} "boolean"`,
};
}
@@ -416,6 +421,15 @@ export const VALIDATORS: Record = {
message: `${WIDGET_TYPE_VALIDATION_ERROR} "{type: string, dataSource: { chart: object, data: Array<{label: string, value: number}>}}"`,
};
}
+ // check custom chart exist or not
+ const typeExist = indexOf(CUSTOM_CHART_TYPES, parsed.type) !== -1;
+ if (!typeExist) {
+ return {
+ isValid: false,
+ parsed: { ...CUSTOM_CHART_DEFAULT_PARSED },
+ message: `${WIDGET_TYPE_VALIDATION_ERROR} "{type: string, dataSource: { chart: object, data: Array<{label: string, value: number}>}}"`,
+ };
+ }
return { isValid, parsed, transformed: parsed };
},
[VALIDATION_TYPES.MARKERS]: (
diff --git a/app/client/start-https.sh b/app/client/start-https.sh
index fdf492df30..259c14286b 100755
--- a/app/client/start-https.sh
+++ b/app/client/start-https.sh
@@ -87,14 +87,6 @@ case "${uname_out}" in
"
;;
Darwin*) machine=Mac
- # workaround for apple silicon until host.docker.interal works as expected
- if [[ "$(uname -m)" = "arm64" ]]; then
- # if no server was passed
- if [[ -z $1 ]]; then
- server_proxy_pass="http://"$(ipconfig getifaddr en0)":8080"
- fi
- client_proxy_pass="http://"$(ipconfig getifaddr en0)":3000"
- fi
echo "
Starting nginx for MacOS...
"
diff --git a/app/client/test/sagas.ts b/app/client/test/sagas.ts
index 3a15551947..e563dd6e3e 100644
--- a/app/client/test/sagas.ts
+++ b/app/client/test/sagas.ts
@@ -17,7 +17,7 @@ import actionExecutionChangeListeners from "../src/sagas/WidgetLoadingSaga";
import globalSearchSagas from "../src/sagas/GlobalSearchSagas";
import recentEntitiesSagas from "../src/sagas/RecentEntitiesSagas";
import commentSagas from "../src/sagas/CommentSagas";
-import websocketSagas from "../src/sagas/WebsocketSagas";
+import websocketSagas from "../src/sagas/WebsocketSagas/WebsocketSagas";
import debuggerSagas from "../src/sagas/DebuggerSagas";
import { fetchWidgetCardsSaga } from "../src/sagas/WidgetSidebarSagas";
import { watchActionSagas } from "../src/sagas/ActionSagas";
diff --git a/app/client/yarn.lock b/app/client/yarn.lock
index e4c970715d..6054154bc7 100644
--- a/app/client/yarn.lock
+++ b/app/client/yarn.lock
@@ -3589,7 +3589,6 @@
"@types/emoji-mart@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.4.tgz#418868330c13b510a7b4b86b1ee9904291daeec1"
- integrity sha512-Uqegqi54lXzz1qlDnLYEbFlUXY46auTVwbeOO8mj+9maGu2WBx/lGnmLKg7WS4CC5lB8Q6ch8K5VPBR9bDjWgg==
dependencies:
"@types/react" "*"
@@ -3945,7 +3944,6 @@
"@types/resize-observer-browser@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
- integrity sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==
"@types/resolve@0.0.8":
version "0.0.8"
@@ -4312,7 +4310,6 @@
"@uppy/image-editor@^0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@uppy/image-editor/-/image-editor-0.2.4.tgz#af49fb1f7ab94ed63dc6d8aa04e64b6477baea7b"
- integrity sha512-LTkT536CuJwurIhpn7Gj1p/F+U+ULYYXMxz5XMVijSlDLkfoi7IF6E3kbt4N4raUkyBtGgtmd2Ka7oUvoEFtxA==
dependencies:
"@uppy/utils" "^3.5.0"
cropperjs "1.5.7"
@@ -4401,7 +4398,6 @@
"@uppy/utils@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-3.5.0.tgz#0822f68e8a4c7e833a7ca9ddf387adbd3ea0535c"
- integrity sha512-Hhe8e/ArclSascuRjpwWtiEqAcykh9Qb/tZrA6cw+L4QjoYhxaxnOZOQoPG8LOz+zZS/DgQyF7IjWp+oiHDzag==
dependencies:
abortcontroller-polyfill "^1.4.0"
lodash.throttle "^4.1.1"
@@ -4413,6 +4409,18 @@
"@uppy/utils" "^3.4.0"
preact "8.2.9"
+"@virtuoso.dev/react-urx@^0.2.5":
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/@virtuoso.dev/react-urx/-/react-urx-0.2.6.tgz#e1d8bc717723b2fc23d80ea4e07703dbc276448b"
+ integrity sha512-+PLQ2iWmSH/rW7WGPEf+Kkql+xygHFL43Jij5aREde/O9mE0OFFGqeetA2a6lry3LDVWzupPntvvWhdaYw0TyA==
+ dependencies:
+ "@virtuoso.dev/urx" "^0.2.6"
+
+"@virtuoso.dev/urx@^0.2.5", "@virtuoso.dev/urx@^0.2.6":
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/@virtuoso.dev/urx/-/urx-0.2.6.tgz#0028c49e52037e673993900d32abea83262fbd53"
+ integrity sha512-EKJ0WvJgWaXIz6zKbh9Q63Bcq//p8OHXHbdz4Fy+ruhjJCyI8ADE8E5gwSqBoUchaiYlgwKrT+sX4L2h/H+hMg==
+
"@vue/compiler-core@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.0.tgz#25e4f079cf6c39f83bad23700f814c619105a0f2"
@@ -4614,6 +4622,11 @@
version "4.2.2"
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+"@yarnpkg/lockfile@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
+ integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
+
abab@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@@ -4895,6 +4908,7 @@ arr-flatten@^1.1.0:
arr-union@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
array-find-index@^1.0.1:
version "1.0.2"
@@ -6289,6 +6303,7 @@ cliui@^7.0.2:
clone-deep@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6"
+ integrity sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=
dependencies:
for-own "^0.1.3"
is-plain-object "^2.0.1"
@@ -6581,6 +6596,11 @@ core-js@^3.0.1, core-js@^3.0.4, core-js@^3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
+core-js@^3.5.0:
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.13.1.tgz#30303fabd53638892062d8b4e802cac7599e9fb7"
+ integrity sha512-JqveUc4igkqwStL2RTRn/EPFGBOfEZHxJl/8ej1mXJR75V3go2mFF4bmUYkEIT1rveHKnkUlcJX/c+f1TyIovQ==
+
core-js@^3.6.4:
version "3.10.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.10.1.tgz#e683963978b6806dcc6c0a4a8bd4ab0bdaf3f21a"
@@ -6667,7 +6687,6 @@ create-react-context@0.3.0, create-react-context@^0.3.0:
cropperjs@1.5.7:
version "1.5.7"
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.5.7.tgz#b65019725bae1c6285e881fb661b2141fa57025b"
- integrity sha512-sGj+G/ofKh+f6A4BtXLJwtcKJgMUsXYVUubfTo9grERiDGXncttefmue/fyQFvn8wfdyoD1KhDRYLfjkJFl0yw==
cross-fetch@^3.0.4:
version "3.1.4"
@@ -6675,7 +6694,7 @@ cross-fetch@^3.0.4:
dependencies:
node-fetch "2.6.1"
-cross-spawn@6.0.5, cross-spawn@^6.0.0:
+cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
dependencies:
@@ -7577,7 +7596,6 @@ emittery@^0.7.1:
emoji-mart@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-3.0.1.tgz#9ce86706e02aea0506345f98464814a662ca54c6"
- integrity sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==
dependencies:
"@babel/runtime" "^7.0.0"
prop-types "^15.6.0"
@@ -7958,7 +7976,6 @@ eslint-plugin-react@^7.21.5:
eslint-plugin-sort-destructure-keys@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-sort-destructure-keys/-/eslint-plugin-sort-destructure-keys-1.3.5.tgz#c6f45c3e58d4435564025a6ca5f4a838010800fd"
- integrity sha512-JmVpidhDsLwZsmRDV7Tf/vZgOAOEQGkLtwToSvX5mD8fuWYS/xkgMRBsalW1fGlc8CgJJwnzropt4oMQ7YCHLg==
dependencies:
natural-compare-lite "^1.4.0"
@@ -8602,6 +8619,13 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"
+find-yarn-workspace-root@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
+ integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
+ dependencies:
+ micromatch "^4.0.2"
+
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@@ -8624,9 +8648,10 @@ flatten@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
-flow-bin@^0.91.0:
- version "0.91.0"
- resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.91.0.tgz#f5c89729f74b2ccbd47df6fbfadbdcc89cc1e478"
+flow-bin@^0.148.0:
+ version "0.148.0"
+ resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.148.0.tgz#1d264606dbb4d6e6070cc98a775e21dcd64e6890"
+ integrity sha512-7Cx6BUm8UAlbqtYJNYXdMrh900MQhNV+SjtBxZuWN7UmlVG4tIRNzNLEOjNnj2DN2vcL1wfI5IlSUXnws/QCEw==
flow-parser@0.*:
version "0.135.0"
@@ -8661,14 +8686,17 @@ follow-redirects@^1.10.0:
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
+ integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
for-own@^0.1.3:
version "0.1.5"
resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=
dependencies:
for-in "^1.0.1"
@@ -8744,7 +8772,7 @@ fs-extra@^0.30.0:
path-is-absolute "^1.0.0"
rimraf "^2.2.8"
-fs-extra@^7.0.0:
+fs-extra@^7.0.0, fs-extra@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
dependencies:
@@ -9768,20 +9796,15 @@ interpret@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
-interweave-autolink@^4.0.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/interweave-autolink/-/interweave-autolink-4.2.1.tgz#b875e67970512484e47666adf952b9025743bd9d"
- dependencies:
- "@types/react" "*"
- prop-types "^15.7.2"
+interweave-autolink@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/interweave-autolink/-/interweave-autolink-4.4.2.tgz#5ef9272361c78cb39180207a9926e034dfaca4e3"
-interweave@^12.1.1:
- version "12.5.0"
- resolved "https://registry.yarnpkg.com/interweave/-/interweave-12.5.0.tgz#c1c6cbda55e3d2864c94eeed8e86f4159111f493"
+interweave@^12.7.2:
+ version "12.7.2"
+ resolved "https://registry.yarnpkg.com/interweave/-/interweave-12.7.2.tgz#c42c74a512202e2cd05165d94e6590c095a7132b"
dependencies:
- "@types/react" "*"
escape-html "^1.0.3"
- prop-types "^15.7.2"
invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4:
version "2.2.4"
@@ -9859,6 +9882,7 @@ is-binary-path@~2.1.0:
is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-buffer@^2.0.0, is-buffer@~2.0.3:
version "2.0.4"
@@ -10223,6 +10247,7 @@ isobject@^2.0.0:
isobject@^3.0.0, isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isobject@^4.0.0:
version "4.0.0"
@@ -11041,12 +11066,14 @@ killable@^1.0.1:
kind-of@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5"
+ integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=
dependencies:
is-buffer "^1.0.2"
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
dependencies:
is-buffer "^1.1.5"
@@ -11064,6 +11091,13 @@ kind-of@^6.0.0, kind-of@^6.0.2:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+klaw-sync@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
+ integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+
klaw@^1.0.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
@@ -11098,10 +11132,12 @@ lazy-ass@^1.6.0:
lazy-cache@^0.2.3:
version "0.2.7"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
+ integrity sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=
lazy-cache@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+ integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4=
lazy-universal-dotenv@^3.0.1:
version "3.0.1"
@@ -11700,8 +11736,9 @@ merge-class-names@^1.1.1:
resolved "https://registry.yarnpkg.com/merge-class-names/-/merge-class-names-1.3.0.tgz#c4cdc1a981a81dd9afc27aa4287e912a337c5dee"
merge-deep@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2"
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.3.tgz#1a2b2ae926da8b2ae93a0ac15d90cd1922766003"
+ integrity sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==
dependencies:
arr-union "^3.1.0"
clone-deep "^0.2.4"
@@ -11914,6 +11951,7 @@ mixin-deep@^1.2.0:
mixin-object@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
+ integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=
dependencies:
for-in "^0.1.3"
is-extendable "^0.1.1"
@@ -12134,7 +12172,6 @@ native-url@^0.2.6:
natural-compare-lite@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
- integrity sha1-F7CVgZiJef3a/gIB6TG6kzyWy7Q=
natural-compare@^1.4.0:
version "1.4.0"
@@ -12537,6 +12574,14 @@ open@^7.0.0, open@^7.0.2, open@^7.1.0:
is-docker "^2.0.0"
is-wsl "^2.1.1"
+open@^7.4.2:
+ version "7.4.2"
+ resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
+ integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
+ dependencies:
+ is-docker "^2.0.0"
+ is-wsl "^2.1.1"
+
opencollective-postinstall@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
@@ -12801,6 +12846,25 @@ pascalcase@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+patch-package@^6.4.7:
+ version "6.4.7"
+ resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148"
+ integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==
+ dependencies:
+ "@yarnpkg/lockfile" "^1.1.0"
+ chalk "^2.4.2"
+ cross-spawn "^6.0.5"
+ find-yarn-workspace-root "^2.0.0"
+ fs-extra "^7.0.1"
+ is-ci "^2.0.0"
+ klaw-sync "^6.0.0"
+ minimist "^1.2.0"
+ open "^7.4.2"
+ rimraf "^2.6.3"
+ semver "^5.6.0"
+ slash "^2.0.0"
+ tmp "^0.0.33"
+
path-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
@@ -13618,6 +13682,11 @@ postcss@^8.1.0:
nanoid "^3.1.15"
source-map "^0.6.1"
+postinstall-postinstall@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3"
+ integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==
+
preact@8.2.9:
version "8.2.9"
resolved "https://registry.yarnpkg.com/preact/-/preact-8.2.9.tgz#813ba9dd45e5d97c5ea0d6c86d375b3be711cc40"
@@ -13767,7 +13836,7 @@ promise@^7.0.1, promise@^7.1.1:
dependencies:
asap "~2.0.3"
-promise@^8.1.0:
+promise@^8.0.3, promise@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e"
dependencies:
@@ -14076,6 +14145,18 @@ re-reselect@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd"
+react-app-polyfill@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0"
+ integrity sha512-OfBnObtnGgLGfweORmdZbyEz+3dgVePQBb3zipiaDsMHV1NpWm0rDFYIVXFV/AK+x4VIIfWHhrdMIeoTLyRr2g==
+ dependencies:
+ core-js "^3.5.0"
+ object-assign "^4.1.1"
+ promise "^8.0.3"
+ raf "^3.4.1"
+ regenerator-runtime "^0.13.3"
+ whatwg-fetch "^3.0.0"
+
react-app-polyfill@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz#a0bea50f078b8a082970a9d853dc34b6dcc6a3cf"
@@ -14701,6 +14782,16 @@ react-virtualized-auto-sizer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
+react-virtuoso@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-1.9.0.tgz#5223d82b4b88021e73cd7cb671824e65537ce3ed"
+ integrity sha512-7ugCTy+zuKIplhhRLvOVHjyluLeB/BNBF2XwwJyKifsQNY/H565BV4yJYzejbmusdFGI7Dt8iJi1Zb9Nfj/pew==
+ dependencies:
+ "@virtuoso.dev/react-urx" "^0.2.5"
+ "@virtuoso.dev/urx" "^0.2.5"
+ react-app-polyfill "^1.0.6"
+ resize-observer-polyfill "^1.5.1"
+
react-window@^1.8.2:
version "1.8.5"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
@@ -15663,6 +15754,7 @@ sha.js@^2.4.0, sha.js@^2.4.8:
shallow-clone@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060"
+ integrity sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=
dependencies:
is-extendable "^0.1.1"
kind-of "^2.0.1"
@@ -17594,6 +17686,11 @@ whatwg-fetch@>=0.10.0, whatwg-fetch@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3"
+whatwg-fetch@^3.0.0:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
+ integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
+
whatwg-mimetype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java
old mode 100644
new mode 100755
index e03dfcf423..a2dadc3552
--- a/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java
+++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java
@@ -15,6 +15,7 @@ import org.junit.ClassRule;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.utility.DockerImageName;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -37,9 +38,8 @@ public class ElasticSearchPluginTest {
ElasticSearchPlugin.ElasticSearchPluginExecutor pluginExecutor = new ElasticSearchPlugin.ElasticSearchPluginExecutor();
@ClassRule
- public static final ElasticsearchContainer container =
- new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:6.4.1")
- .withEnv("discovery.type", "single-node");
+ public static final ElasticsearchContainer container = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.12.1")
+ .withEnv("discovery.type", "single-node");
private static final DatasourceConfiguration dsConfig = new DatasourceConfiguration();
private static String host;
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java
index 29301e269c..dd5e34540b 100644
--- a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java
+++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java
@@ -57,6 +57,7 @@ import java.util.stream.StreamSupport;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_PATH;
import static com.appsmith.external.helpers.PluginUtils.getActionConfigurationPropertyPath;
+import static com.external.utils.WhereConditionUtils.applyWhereConditional;
/**
* Datasource properties:
@@ -71,9 +72,7 @@ public class FirestorePlugin extends BasePlugin {
private static final int ORDER_PROPERTY_INDEX = 1;
private static final int LIMIT_PROPERTY_INDEX = 2;
- private static final int QUERY_PROPERTY_INDEX = 3;
- private static final int OPERATOR_PROPERTY_INDEX = 4;
- private static final int QUERY_VALUE_PROPERTY_INDEX = 5;
+ private static final int WHERE_CONDITIONAL_PROPERTY_INDEX = 3;
private static final int START_AFTER_PROPERTY_INDEX = 6;
private static final int END_BEFORE_PROPERTY_INDEX = 7;
private static final int FIELDVALUE_TIMESTAMP_PROPERTY_INDEX = 8;
@@ -543,14 +542,6 @@ public class FirestorePlugin extends BasePlugin {
List requestParams) {
final String limitString = getPropertyAt(properties, LIMIT_PROPERTY_INDEX, "10");
final int limit = StringUtils.isEmpty(limitString) ? 10 : Integer.parseInt(limitString);
-
- final String queryFieldPath = getPropertyAt(properties, QUERY_PROPERTY_INDEX, null);
-
- final String operatorString = getPropertyAt(properties, OPERATOR_PROPERTY_INDEX, null);
- final Op operator = StringUtils.isEmpty(operatorString) ? null : Op.valueOf(operatorString);
-
- final String queryValue = getPropertyAt(properties, QUERY_VALUE_PROPERTY_INDEX, null);
-
final String orderByString = getPropertyAt(properties, ORDER_PROPERTY_INDEX, "");
requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(ORDER_PROPERTY_INDEX),
orderByString, null, null, null));
@@ -589,12 +580,6 @@ public class FirestorePlugin extends BasePlugin {
requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(LIMIT_PROPERTY_INDEX),
limitString == null ? "" : limitString, null, null, null));
- requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(QUERY_PROPERTY_INDEX),
- queryFieldPath == null ? "" : queryFieldPath, null, null, null));
- requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(OPERATOR_PROPERTY_INDEX),
- operatorString == null ? "" : operatorString, null, null, null));
- requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(QUERY_VALUE_PROPERTY_INDEX),
- queryValue == null ? "" : queryValue, null, null, null));
final Map startAfter = startAfterTemp;
final Map endBefore = endBeforeTemp;
@@ -633,53 +618,42 @@ public class FirestorePlugin extends BasePlugin {
})
// Apply where condition, if provided.
.flatMap(query1 -> {
- if (StringUtils.isEmpty(queryFieldPath) || operator == null || queryValue == null) {
+ if (!isWhereMethodUsed(properties)) {
return Mono.just(query1);
}
- switch (operator) {
- case LT:
- return Mono.just(query1.whereLessThan(queryFieldPath, queryValue));
- case LTE:
- return Mono.just(query1.whereLessThanOrEqualTo(queryFieldPath, queryValue));
- case EQ:
- return Mono.just(query1.whereEqualTo(queryFieldPath, queryValue));
- // TODO: NOT_EQ operator support is awaited in the next version of Firestore driver.
- // case NOT_EQ:
- // return Mono.just(query1.whereNotEqualTo(queryFieldPath, queryValue));
- case GT:
- return Mono.just(query1.whereGreaterThan(queryFieldPath, queryValue));
- case GTE:
- return Mono.just(query1.whereGreaterThanOrEqualTo(queryFieldPath, queryValue));
- case ARRAY_CONTAINS:
- return Mono.just(query1.whereArrayContains(queryFieldPath, queryValue));
- case ARRAY_CONTAINS_ANY:
- try {
- return Mono.just(query1.whereArrayContainsAny(queryFieldPath, parseList(queryValue)));
- } catch (IOException e) {
- return Mono.error(new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Unable to parse condition value as a JSON list."
- ));
- }
- case IN:
- try {
- return Mono.just(query1.whereIn(queryFieldPath, parseList(queryValue)));
- } catch (IOException e) {
- return Mono.error(new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Unable to parse condition value as a JSON list."
- ));
- }
- // TODO: NOT_IN operator support is awaited in the next version of Firestore driver.
- // case NOT_IN:
- // return Mono.just(query1.whereNotIn(queryFieldPath, queryValue));
- default:
- return Mono.error(new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_ERROR,
- "Unsupported operator for `where` condition " + operator.toString() + "."
- ));
+ List conditionList = (List) properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX).getValue();
+ requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(WHERE_CONDITIONAL_PROPERTY_INDEX),
+ conditionList, null, null, null));
+
+ for(Object condition : conditionList) {
+ String path = ((Map)condition).get("path");
+ String operatorString = ((Map)condition).get("operator");
+ String value = ((Map)condition).get("value");
+
+ /**
+ * - If all values in all where condition tuples are null, then isWhereMethodUsed(...)
+ * function will indicate that where conditions are not used effectively and program
+ * execution would return without reaching here.
+ */
+ if (StringUtils.isEmpty(path) || StringUtils.isEmpty(operatorString) || StringUtils.isEmpty(value)) {
+ return Mono.error(
+ new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
+ "One of the where condition fields has been found empty. Please fill " +
+ "all the where condition fields and try again."
+ )
+ );
+ }
+
+ try {
+ query1 = applyWhereConditional(query1, path, operatorString, value);
+ } catch (AppsmithPluginException e) {
+ return Mono.error(e);
+ }
}
+
+ return Mono.just(query1);
})
// Apply limit, always provided, since without it we can inadvertently end up processing too much data.
.map(query1 -> query1.limit(limit))
@@ -710,6 +684,25 @@ public class FirestorePlugin extends BasePlugin {
});
}
+ private boolean isWhereMethodUsed(List properties) {
+ // Check if the where property list does not exist or is null
+ if(properties.size() <= WHERE_CONDITIONAL_PROPERTY_INDEX || properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX) == null
+ || CollectionUtils.isEmpty((List) properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX).getValue())) {
+ return false;
+ }
+
+ // Check if all values in the where property list are null.
+ boolean allValuesNull = ((List) properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX).getValue()).stream()
+ .allMatch(valueMap -> valueMap == null ||
+ ((Map) valueMap).entrySet().stream().allMatch(e -> ((Map.Entry) e).getValue() == null));
+
+ if (allValuesNull) {
+ return false;
+ }
+
+ return true;
+ }
+
private Mono methodAddToCollection(CollectionReference collection, Map mapBody) {
return Mono.justOrEmpty(collection.add(mapBody))
.flatMap(future -> {
@@ -878,10 +871,6 @@ public class FirestorePlugin extends BasePlugin {
return invalids;
}
- private List parseList(String arrayJson) throws IOException {
- return (List) objectMapper.readValue(arrayJson, ArrayList.class);
- }
-
@Override
public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) {
return datasourceCreate(datasourceConfiguration)
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/utils/WhereConditionUtils.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/utils/WhereConditionUtils.java
new file mode 100644
index 0000000000..579e4aa430
--- /dev/null
+++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/utils/WhereConditionUtils.java
@@ -0,0 +1,90 @@
+package com.external.utils;
+
+import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
+import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
+import com.external.plugins.Op;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.cloud.firestore.FieldPath;
+import com.google.cloud.firestore.Query;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class WhereConditionUtils {
+
+ protected static final ObjectMapper objectMapper = new ObjectMapper();
+
+ public static Query applyWhereConditional(Query query, String path, String operatorString, String value) throws AppsmithPluginException {
+
+ if (query == null) {
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_ERROR,
+ "Appsmith server has found null query object when applying where conditional on Firestore " +
+ "query. Please contact Appsmith's customer support to resolve this."
+ );
+ }
+
+ Op operator;
+ try {
+ operator = StringUtils.isEmpty(operatorString) ? null : Op.valueOf(operatorString);
+ } catch (IllegalArgumentException e) {
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_ERROR,
+ "Appsmith server has encountered an invalid operator for Firestore query's where conditional." +
+ " Please contact Appsmith's customer support to resolve this."
+ );
+ }
+
+ FieldPath fieldPath = FieldPath.of(path.split("\\."));
+ switch (operator) {
+ case LT:
+ return query.whereLessThan(fieldPath, value);
+ case LTE:
+ return query.whereLessThanOrEqualTo(fieldPath, value);
+ case EQ:
+ return query.whereEqualTo(fieldPath, value);
+ // TODO: NOT_EQ operator support is awaited in the next version of Firestore driver.
+ // case NOT_EQ:
+ // return Mono.just(query.whereNotEqualTo(path, value));
+ case GT:
+ return query.whereGreaterThan(fieldPath, value);
+ case GTE:
+ return query.whereGreaterThanOrEqualTo(fieldPath, value);
+ case ARRAY_CONTAINS:
+ return query.whereArrayContains(fieldPath, value);
+ case ARRAY_CONTAINS_ANY:
+ try {
+ return query.whereArrayContainsAny(fieldPath, parseList(value));
+ } catch (IOException e) {
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
+ "Unable to parse condition value as a JSON list."
+ );
+ }
+ case IN:
+ try {
+ return query.whereIn(fieldPath, parseList(value));
+ } catch (IOException e) {
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
+ "Unable to parse condition value as a JSON list."
+ );
+ }
+ // TODO: NOT_IN operator support is awaited in the next version of Firestore driver.
+ // case NOT_IN:
+ // return Mono.just(query.whereNotIn(fieldPath, value));
+ default:
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_ERROR,
+ "Appsmith server has encountered an invalid operator for Firestore query's where conditional." +
+ " Please contact Appsmith's customer support to resolve this."
+ );
+ }
+ }
+
+ private static List parseList(String arrayJson) throws IOException {
+ return (List) objectMapper.readValue(arrayJson, ArrayList.class);
+ }
+}
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json
index 464ed3b03a..308158cf77 100644
--- a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json
+++ b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json
@@ -167,43 +167,32 @@
"initialValue": "10"
},
{
- "sectionName": "Query",
- "id": 2,
- "children": [
+ "label": "Where Conditions Key",
+ "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].key",
+ "controlType": "INPUT_TEXT",
+ "hidden": true,
+ "initialValue": "whereConditionTuples"
+ },
+ {
+ "label": "Where Conditions",
+ "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].value",
+ "controlType": "ARRAY_FIELD",
+ "hidden": {
+ "path": "actionConfiguration.pluginSpecifiedTemplates[0].value",
+ "comparison": "NOT_EQUALS",
+ "value": "GET_COLLECTION"
+ },
+ "schema": [
{
- "label": "Field Path Key",
- "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].key",
- "controlType": "INPUT_TEXT",
- "hidden": true,
- "initialValue": "fieldPath"
- },
- {
- "label": "Where Condition: Field Path (leave empty to not apply any conditions)",
- "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].value",
+ "label": "Path",
+ "key": "path",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
- "hidden": {
- "path": "actionConfiguration.pluginSpecifiedTemplates[0].value",
- "comparison": "NOT_EQUALS",
- "value": "GET_COLLECTION"
- },
- "initialValue": ""
+ "placeholderText": "key1/nestedKey2"
},
{
- "label": "Operator Key",
- "configProperty": "actionConfiguration.pluginSpecifiedTemplates[4].key",
- "controlType": "INPUT_TEXT",
- "hidden": true,
- "initialValue": "operator"
- },
- {
- "label": "Where Condition: Operator",
- "configProperty": "actionConfiguration.pluginSpecifiedTemplates[4].value",
+ "label": "Operator",
+ "key": "operator",
"controlType": "DROP_DOWN",
- "hidden": {
- "path": "actionConfiguration.pluginSpecifiedTemplates[0].value",
- "comparison": "NOT_EQUALS",
- "value": "GET_COLLECTION"
- },
"initialValue": "EQ",
"options": [
{
@@ -241,22 +230,10 @@
]
},
{
- "label": "Value Key",
- "configProperty": "actionConfiguration.pluginSpecifiedTemplates[5].key",
- "controlType": "INPUT_TEXT",
- "hidden": true,
- "initialValue": "fieldValue"
- },
- {
- "label": "Where Condition: Value",
- "configProperty": "actionConfiguration.pluginSpecifiedTemplates[5].value",
+ "label": "Value",
+ "key": "value",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
- "hidden": {
- "path": "actionConfiguration.pluginSpecifiedTemplates[0].value",
- "comparison": "NOT_EQUALS",
- "value": "GET_COLLECTION"
- },
- "initialValue": ""
+ "placeholderText": "value"
}
]
},
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java b/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java
index d0325aa8eb..2a0eba77fa 100644
--- a/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java
+++ b/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java
@@ -77,7 +77,8 @@ public class FirestorePluginTest {
.build()
.getService();
- firestoreConnection.document("initial/one").set(Map.of("value", 1, "name", "one", "isPlural", false)).get();
+ firestoreConnection.document("initial/one").set(Map.of("value", 1, "name", "one", "isPlural", false,
+ "category", "test")).get();
final Map twoData = new HashMap<>(Map.of(
"value", 2,
"name", "two",
@@ -85,7 +86,8 @@ public class FirestorePluginTest {
"geo", new GeoPoint(-90, 90),
"dt", FieldValue.serverTimestamp(),
"ref", firestoreConnection.document("initial/one"),
- "bytes", Blob.fromBytes("abc def".getBytes(StandardCharsets.UTF_8))
+ "bytes", Blob.fromBytes("abc def".getBytes(StandardCharsets.UTF_8)),
+ "category", "test"
));
twoData.put("null-ref", null);
firestoreConnection.document("initial/two").set(twoData).get();
@@ -146,6 +148,7 @@ public class FirestorePluginTest {
assertFalse((Boolean) first.remove("isPlural"));
assertEquals(1L, first.remove("value"));
assertEquals(Map.of("id", "one", "path", "initial/one"), first.remove("_ref"));
+ assertEquals("test", first.remove("category"));
assertEquals(Collections.emptyMap(), first);
/*
@@ -184,6 +187,7 @@ public class FirestorePluginTest {
assertEquals("abc def", ((Blob) doc.remove("bytes")).toByteString().toStringUtf8());
assertNull(doc.remove("null-ref"));
assertEquals(Map.of("id", "two", "path", "initial/two"), doc.remove("_ref"));
+ assertEquals("test", doc.remove("category"));
assertEquals(Collections.emptyMap(), doc);
})
.verifyComplete();
@@ -240,6 +244,7 @@ public class FirestorePluginTest {
assertFalse((Boolean) first.remove("isPlural"));
assertEquals(1L, first.remove("value"));
assertEquals(Map.of("id", "one", "path", "initial/one"), first.remove("_ref"));
+ assertEquals("test", first.remove("category"));
assertEquals(Collections.emptyMap(), first);
final Map second = results.stream().filter(d -> "two".equals(d.get("name"))).findFirst().orElse(null);
@@ -253,6 +258,7 @@ public class FirestorePluginTest {
assertEquals("abc def", ((Blob) second.remove("bytes")).toByteString().toStringUtf8());
assertNull(second.remove("null-ref"));
assertEquals(Map.of("id", "two", "path", "initial/two"), second.remove("_ref"));
+ assertEquals("test", second.remove("category"));
assertEquals(Collections.emptyMap(), second);
final Map third = results.stream().filter(d -> "third".equals(d.get("name"))).findFirst().orElse(null);
@@ -637,12 +643,6 @@ public class FirestorePluginTest {
null, null)); // End before
expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(2), "15", null,
null, null)); // Limit
- expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(3), "", null,
- null, null)); // Field Path
- expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(4), "", null,
- null, null)); // Operator
- expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(5), "", null,
- null, null)); // Value
assertEquals(result.getRequest().getRequestParams().toString(), expectedRequestParams.toString());
})
@@ -696,6 +696,65 @@ public class FirestorePluginTest {
}
@Test
+ public void testWhereConditional() {
+ ActionConfiguration actionConfiguration = new ActionConfiguration();
+ actionConfiguration.setPath("initial");
+ List pluginSpecifiedTemplates = new ArrayList<>();
+ pluginSpecifiedTemplates.add(new Property("method", "GET_COLLECTION"));
+ pluginSpecifiedTemplates.add(new Property("order", null));
+ pluginSpecifiedTemplates.add(new Property("limit", null));
+ Property whereProperty = new Property("where", null);
+ whereProperty.setValue(new ArrayList<>());
+ /*
+ * - get all documents where category == test.
+ * - this returns 2 documents.
+ */
+ ((List)whereProperty.getValue()).add(new HashMap() {{
+ put("path", "category");
+ put("operator", "EQ");
+ put("value", "test");
+ }});
+
+ /*
+ * - get all documents where name == two.
+ * - Of the two documents returned by above condition, this will narrow it down to one.
+ */
+ ((List)whereProperty.getValue()).add(new HashMap() {{
+ put("path", "name");
+ put("operator", "EQ");
+ put("value", "two");
+ }});
+
+ pluginSpecifiedTemplates.add(whereProperty);
+ actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
+
+ Mono resultMono = pluginExecutor
+ .executeParameterized(firestoreConnection, null, dsConfig, actionConfiguration);
+
+ StepVerifier.create(resultMono)
+ .assertNext(result -> {
+ assertTrue(result.getIsExecutionSuccess());
+
+ List> results = (List) result.getBody();
+ assertEquals(1, results.size());
+
+ final Map second = results.stream().findFirst().orElse(null);
+ assertNotNull(second);
+ assertEquals("two", second.remove("name"));
+ assertTrue((Boolean) second.remove("isPlural"));
+ assertEquals(2L, second.remove("value"));
+ assertEquals(Map.of("path", "initial/one", "id", "one"), second.remove("ref"));
+ assertEquals(new GeoPoint(-90, 90), second.remove("geo"));
+ assertNotNull(second.remove("dt"));
+ assertEquals("abc def", ((Blob) second.remove("bytes")).toByteString().toStringUtf8());
+ assertNull(second.remove("null-ref"));
+ assertEquals(Map.of("id", "two", "path", "initial/two"), second.remove("_ref"));
+ assertEquals("test", second.remove("category"));
+ assertEquals(Collections.emptyMap(), second);
+ })
+ .verifyComplete();
+ }
+
public void testUpdateDocumentWithFieldValueTimestamp() {
List properties = new ArrayList<>();
properties.add(new Property("method", "UPDATE_DOCUMENT")); // index 0
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java
index 1bbeff0b17..0b2f161f27 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java
@@ -16,6 +16,7 @@ import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.Endpoint;
+import com.appsmith.external.models.Param;
import com.appsmith.external.models.ParsedDataType;
import com.appsmith.external.models.Property;
import com.appsmith.external.models.RequestParamDTO;
@@ -61,6 +62,7 @@ import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
@@ -76,8 +78,24 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
+import static com.external.plugins.MongoPluginUtils.validConfigurationPresent;
+import static com.external.plugins.constants.ConfigurationIndex.AGGREGATE_PIPELINE;
import static com.external.plugins.constants.ConfigurationIndex.COMMAND;
+import static com.external.plugins.constants.ConfigurationIndex.COUNT_QUERY;
+import static com.external.plugins.constants.ConfigurationIndex.DELETE_QUERY;
+import static com.external.plugins.constants.ConfigurationIndex.DISTINCT_QUERY;
+import static com.external.plugins.constants.ConfigurationIndex.FIND_PROJECTION;
+import static com.external.plugins.constants.ConfigurationIndex.FIND_QUERY;
+import static com.external.plugins.constants.ConfigurationIndex.FIND_SORT;
import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE;
+import static com.external.plugins.constants.ConfigurationIndex.INSERT_DOCUMENT;
+import static com.external.plugins.constants.ConfigurationIndex.SMART_BSON_SUBSTITUTION;
+import static com.external.plugins.constants.ConfigurationIndex.UPDATE_MANY_QUERY;
+import static com.external.plugins.constants.ConfigurationIndex.UPDATE_MANY_UPDATE;
+import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_QUERY;
+import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_SORT;
+import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_UPDATE;
+import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
public class MongoPlugin extends BasePlugin {
@@ -102,8 +120,6 @@ public class MongoPlugin extends BasePlugin {
private static final int TEST_DATASOURCE_TIMEOUT_SECONDS = 15;
- private static final int SMART_BSON_SUBSTITUTION_INDEX = 0;
-
/*
* - The regex matches the following two pattern types:
* - mongodb+srv://user:pass@some-url/some-db....
@@ -140,6 +156,21 @@ public class MongoPlugin extends BasePlugin {
private static final Integer MONGO_COMMAND_EXCEPTION_UNAUTHORIZED_ERROR_CODE = 13;
+ private static final Set bsonFields = new HashSet<>(Arrays.asList(AGGREGATE_PIPELINE,
+ COUNT_QUERY,
+ DELETE_QUERY,
+ DISTINCT_QUERY,
+ FIND_QUERY,
+ FIND_SORT,
+ FIND_PROJECTION,
+ INSERT_DOCUMENT,
+ UPDATE_MANY_QUERY,
+ UPDATE_MANY_UPDATE,
+ UPDATE_ONE_QUERY,
+ UPDATE_ONE_SORT,
+ UPDATE_ONE_UPDATE
+ ));
+
public MongoPlugin(PluginWrapper wrapper) {
super(wrapper);
}
@@ -181,8 +212,8 @@ public class MongoPlugin extends BasePlugin {
smartBsonSubstitution = false;
// Since properties is not empty, we are guaranteed to find the first property.
- } else if (properties.get(SMART_BSON_SUBSTITUTION_INDEX) != null) {
- Object ssubValue = properties.get(SMART_BSON_SUBSTITUTION_INDEX).getValue();
+ } else if (properties.get(SMART_BSON_SUBSTITUTION) != null) {
+ Object ssubValue = properties.get(SMART_BSON_SUBSTITUTION).getValue();
if (ssubValue instanceof Boolean) {
smartBsonSubstitution = (Boolean) ssubValue;
} else if (ssubValue instanceof String) {
@@ -196,32 +227,31 @@ public class MongoPlugin extends BasePlugin {
// Smartly substitute in actionConfiguration.body and replace all the bindings with values.
if (TRUE.equals(smartBsonSubstitution)) {
- // Do smart replacements in BSON body
- if (actionConfiguration.getBody() != null) {
- // First extract all the bindings in order
- List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(actionConfiguration.getBody());
- // Replace all the bindings with a ? as expected in a prepared statement.
- String updatedBody = MustacheHelper.replaceMustacheWithQuestionMark(actionConfiguration.getBody(), mustacheKeysInOrder);
-
- try {
- updatedBody = (String) smartSubstitutionOfBindings(updatedBody,
- mustacheKeysInOrder,
- executeActionDTO.getParams(),
- parameters);
- } catch (AppsmithPluginException e) {
- ActionExecutionResult errorResult = new ActionExecutionResult();
- errorResult.setStatusCode(AppsmithPluginError.PLUGIN_ERROR.getAppErrorCode().toString());
- errorResult.setIsExecutionSuccess(false);
- errorResult.setBody(e.getMessage());
- return Mono.just(errorResult);
+ if (isFormInput(actionConfiguration.getPluginSpecifiedTemplates())) {
+ List updatedTemplates = smartSubstituteFormCommand(actionConfiguration.getPluginSpecifiedTemplates(),
+ executeActionDTO.getParams(), parameters);
+ actionConfiguration.setPluginSpecifiedTemplates(updatedTemplates);
+ } else {
+ // For raw queries do smart replacements in BSON body
+ if (actionConfiguration.getBody() != null) {
+ try {
+ String updatedRawQuery = smartSubstituteBSON(actionConfiguration.getBody(),
+ executeActionDTO.getParams(), parameters);
+ actionConfiguration.setBody(updatedRawQuery);
+ } catch (AppsmithPluginException e) {
+ ActionExecutionResult errorResult = new ActionExecutionResult();
+ errorResult.setStatusCode(AppsmithPluginError.PLUGIN_ERROR.getAppErrorCode().toString());
+ errorResult.setIsExecutionSuccess(false);
+ errorResult.setBody(e.getMessage());
+ return Mono.just(errorResult);
+ }
}
-
- actionConfiguration.setBody(updatedBody);
}
}
prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration);
+
// In case the input type is form instead of raw, parse the same into BSON command
String parsedRawCommand = convertMongoFormInputToRawCommand(actionConfiguration);
if (parsedRawCommand != null) {
@@ -258,7 +288,7 @@ public class MongoPlugin extends BasePlugin {
Mono mongoOutputMono = Mono.from(database.runCommand(command));
ActionExecutionResult result = new ActionExecutionResult();
- List requestParams = List.of(new RequestParamDTO(ACTION_CONFIGURATION_BODY, query, null
+ List requestParams = List.of(new RequestParamDTO(ACTION_CONFIGURATION_BODY, query, null
, null, null));
return mongoOutputMono
@@ -382,15 +412,22 @@ public class MongoPlugin extends BasePlugin {
.subscribeOn(scheduler);
}
+ private Boolean isFormInput(List templates) {
+ if ((templates.size() >= (1 + INPUT_TYPE)) &&
+ (templates.get(INPUT_TYPE) != null) &&
+ ("FORM".equals(templates.get(INPUT_TYPE).getValue())) &&
+ (templates.size() >= (1 + COMMAND)) &&
+ (templates.get(COMMAND) != null) &&
+ (templates.get(COMMAND).getValue() != null)) {
+ return TRUE;
+ }
+ return FALSE;
+ }
+
private String convertMongoFormInputToRawCommand(ActionConfiguration actionConfiguration) {
List templates = actionConfiguration.getPluginSpecifiedTemplates();
if (templates != null) {
- if ((templates.size() >= (1 + INPUT_TYPE)) &&
- (templates.get(INPUT_TYPE) != null) &&
- ("FORM".equals(templates.get(INPUT_TYPE).getValue())) &&
- (templates.size() >= (1 + COMMAND)) &&
- (templates.get(COMMAND) != null) &&
- (templates.get(COMMAND).getValue() != null)) {
+ if (isFormInput(templates)) {
// The user has configured FORM for command input. Parse the commands appropriately
MongoCommand command = null;
@@ -435,6 +472,38 @@ public class MongoPlugin extends BasePlugin {
return actionConfiguration.getBody();
}
+ private String smartSubstituteBSON(String rawQuery,
+ List params,
+ List> parameters) throws AppsmithPluginException {
+
+ // First extract all the bindings in order
+ List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(rawQuery);
+ // Replace all the bindings with a ? as expected in a prepared statement.
+ String updatedQuery = MustacheHelper.replaceMustacheWithQuestionMark(rawQuery, mustacheKeysInOrder);
+
+ updatedQuery = (String) smartSubstitutionOfBindings(updatedQuery,
+ mustacheKeysInOrder,
+ params,
+ parameters);
+
+ return updatedQuery;
+ }
+
+ private List smartSubstituteFormCommand(List templates,
+ List params,
+ List> parameters) throws AppsmithPluginException {
+
+ for (int i = 0; i < templates.size(); i++) {
+ if (validConfigurationPresent(templates, i) && bsonFields.contains(i)) {
+ Property configuration = templates.get(i);
+ // Do Smart Substitution for each BSON field
+ configuration.setValue(smartSubstituteBSON((String) configuration.getValue(), params, parameters));
+ }
+ }
+
+ return templates;
+ }
+
private String getDatabaseName(DatasourceConfiguration datasourceConfiguration) {
// Explicitly set default database.
String databaseName = datasourceConfiguration.getConnection().getDefaultDatabaseName();
@@ -688,6 +757,7 @@ public class MongoPlugin extends BasePlugin {
public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) {
Set invalids = new HashSet<>();
List properties = datasourceConfiguration.getProperties();
+ DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (isUsingURI(datasourceConfiguration)) {
if (!hasNonEmptyURI(datasourceConfiguration)) {
invalids.add("'Mongo Connection String URI' field is empty. Please edit the 'Mongo Connection " +
@@ -702,11 +772,10 @@ public class MongoPlugin extends BasePlugin {
if (extractedInfo == null) {
invalids.add("Mongo Connection String URI does not seem to be in the correct format. " +
"Please check the URI once.");
- } else {
+ } else if (!isAuthenticated(authentication, mongoUri)) {
String mongoUriWithHiddenPassword = buildURIfromExtractedInfo(extractedInfo, "****");
properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).setValue(mongoUriWithHiddenPassword);
- DBAuth authentication = datasourceConfiguration.getAuthentication() == null ?
- new DBAuth() : (DBAuth) datasourceConfiguration.getAuthentication();
+ authentication = (authentication == null) ? new DBAuth() : authentication;
authentication.setUsername((String) extractedInfo.get(KEY_USERNAME));
authentication.setPassword((String) extractedInfo.get(KEY_PASSWORD));
authentication.setDatabaseName((String) extractedInfo.get(KEY_URI_DBNAME));
@@ -747,7 +816,6 @@ public class MongoPlugin extends BasePlugin {
}
}
- DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication != null) {
DBAuth.Type authType = authentication.getAuthType();
@@ -1014,4 +1082,13 @@ public class MongoPlugin extends BasePlugin {
return object;
}
+ private static boolean isAuthenticated(DBAuth authentication, String mongoUri) {
+ if (authentication != null && authentication.getUsername() != null
+ && authentication.getPassword() != null && mongoUri.contains("****")) {
+
+ return true;
+ }
+ return false;
+ }
+
}
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java
index d29c362afc..4a9ac56690 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java
@@ -18,7 +18,7 @@ import java.util.Map;
import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates;
import static com.external.plugins.MongoPluginUtils.parseSafely;
import static com.external.plugins.MongoPluginUtils.validConfigurationPresent;
-import static com.external.plugins.constants.ConfigurationIndex.BSON;
+import static com.external.plugins.constants.ConfigurationIndex.SMART_BSON_SUBSTITUTION;
import static com.external.plugins.constants.ConfigurationIndex.COLLECTION;
import static com.external.plugins.constants.ConfigurationIndex.COMMAND;
import static com.external.plugins.constants.ConfigurationIndex.DELETE_LIMIT;
@@ -88,7 +88,7 @@ public class Delete extends MongoCommand {
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.TRUE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "DELETE");
configMap.put(COLLECTION, collectionName);
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java
index 5c82aec0ad..53f3bcc318 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java
@@ -17,7 +17,7 @@ import java.util.Map;
import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates;
import static com.external.plugins.MongoPluginUtils.parseSafely;
import static com.external.plugins.MongoPluginUtils.validConfigurationPresent;
-import static com.external.plugins.constants.ConfigurationIndex.BSON;
+import static com.external.plugins.constants.ConfigurationIndex.SMART_BSON_SUBSTITUTION;
import static com.external.plugins.constants.ConfigurationIndex.COLLECTION;
import static com.external.plugins.constants.ConfigurationIndex.COMMAND;
import static com.external.plugins.constants.ConfigurationIndex.FIND_LIMIT;
@@ -89,7 +89,7 @@ public class Find extends MongoCommand {
}
if (!StringUtils.isNullOrEmpty(this.projection)) {
- document.put("projection", this.projection);
+ document.put("projection", parseSafely("Projection", this.projection));
}
// Default to returning 10 documents if not mentioned
@@ -125,7 +125,7 @@ public class Find extends MongoCommand {
private DatasourceStructure.Template generateFindTemplate(String collectionName, String filterFieldName, String filterFieldValue) {
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.TRUE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "FIND");
configMap.put(COLLECTION, collectionName);
@@ -162,7 +162,7 @@ public class Find extends MongoCommand {
private DatasourceStructure.Template generateFindByIdTemplate(String collectionName) {
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.TRUE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "FIND");
configMap.put(FIND_QUERY, "{\"_id\": ObjectId(\"id_to_query_with\")}");
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java
index 4b46d7d5ec..dc64192645 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java
@@ -24,7 +24,7 @@ import java.util.stream.Collectors;
import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates;
import static com.external.plugins.MongoPluginUtils.parseSafely;
import static com.external.plugins.MongoPluginUtils.validConfigurationPresent;
-import static com.external.plugins.constants.ConfigurationIndex.BSON;
+import static com.external.plugins.constants.ConfigurationIndex.SMART_BSON_SUBSTITUTION;
import static com.external.plugins.constants.ConfigurationIndex.COLLECTION;
import static com.external.plugins.constants.ConfigurationIndex.COMMAND;
import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE;
@@ -100,7 +100,7 @@ public class Insert extends MongoCommand {
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.TRUE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "INSERT");
configMap.put(INSERT_DOCUMENT, "[{" + sampleInsertDocuments + "}]");
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java
index b9aacf41c3..4f767d6538 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java
@@ -18,7 +18,7 @@ import java.util.Map;
import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates;
import static com.external.plugins.MongoPluginUtils.parseSafely;
import static com.external.plugins.MongoPluginUtils.validConfigurationPresent;
-import static com.external.plugins.constants.ConfigurationIndex.BSON;
+import static com.external.plugins.constants.ConfigurationIndex.SMART_BSON_SUBSTITUTION;
import static com.external.plugins.constants.ConfigurationIndex.COLLECTION;
import static com.external.plugins.constants.ConfigurationIndex.COMMAND;
import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE;
@@ -95,7 +95,7 @@ public class UpdateMany extends MongoCommand {
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.TRUE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "UPDATE_MANY");
configMap.put(COLLECTION, collectionName);
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java
index 65050c2a30..0542e09f44 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java
@@ -1,7 +1,7 @@
package com.external.plugins.constants;
public class ConfigurationIndex {
- public static final int BSON = 0;
+ public static final int SMART_BSON_SUBSTITUTION = 0;
public static final int INPUT_TYPE = 1;
public static final int COMMAND = 2;
public static final int COLLECTION = 19;
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json
index 0857096308..0357219a82 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json
@@ -27,11 +27,11 @@
"initialValue": "FIND",
"options": [
{
- "label": "Insert a document",
+ "label": "Insert Document(s)",
"value": "INSERT"
},
{
- "label": "Find one or more documents",
+ "label": "Find Document(s)",
"value": "FIND"
},
{
@@ -43,7 +43,7 @@
"value": "UPDATE_MANY"
},
{
- "label": "Delete one or more documents",
+ "label": "Delete Document(s)",
"value": "DELETE"
},
{
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java
index 9e19e4cfd5..d19ec31fdb 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java
@@ -49,7 +49,7 @@ import static com.appsmith.external.constants.DisplayDataType.JSON;
import static com.appsmith.external.constants.DisplayDataType.RAW;
import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates;
import static com.external.plugins.constants.ConfigurationIndex.AGGREGATE_PIPELINE;
-import static com.external.plugins.constants.ConfigurationIndex.BSON;
+import static com.external.plugins.constants.ConfigurationIndex.SMART_BSON_SUBSTITUTION;
import static com.external.plugins.constants.ConfigurationIndex.COLLECTION;
import static com.external.plugins.constants.ConfigurationIndex.COMMAND;
import static com.external.plugins.constants.ConfigurationIndex.COUNT_QUERY;
@@ -57,6 +57,8 @@ import static com.external.plugins.constants.ConfigurationIndex.DELETE_LIMIT;
import static com.external.plugins.constants.ConfigurationIndex.DELETE_QUERY;
import static com.external.plugins.constants.ConfigurationIndex.DISTINCT_KEY;
import static com.external.plugins.constants.ConfigurationIndex.DISTINCT_QUERY;
+import static com.external.plugins.constants.ConfigurationIndex.FIND_LIMIT;
+import static com.external.plugins.constants.ConfigurationIndex.FIND_PROJECTION;
import static com.external.plugins.constants.ConfigurationIndex.FIND_QUERY;
import static com.external.plugins.constants.ConfigurationIndex.FIND_SORT;
import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE;
@@ -331,7 +333,7 @@ public class MongoPluginTest {
// Clean up this newly inserted value
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "DELETE");
configMap.put(COLLECTION, "users");
@@ -937,7 +939,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "FIND");
configMap.put(FIND_QUERY, "{ age: { \"$gte\": 30 } }");
@@ -970,7 +972,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "INSERT");
configMap.put(COLLECTION, "users");
@@ -997,7 +999,7 @@ public class MongoPluginTest {
// Clean up this newly inserted value
configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "DELETE");
configMap.put(COLLECTION, "users");
@@ -1014,7 +1016,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "INSERT");
configMap.put(COLLECTION, "users");
@@ -1041,7 +1043,7 @@ public class MongoPluginTest {
// Clean up this newly inserted value
configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "DELETE");
configMap.put(COLLECTION, "users");
@@ -1058,7 +1060,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "UPDATE_ONE");
configMap.put(COLLECTION, "users");
@@ -1094,7 +1096,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "UPDATE_MANY");
configMap.put(COLLECTION, "users");
@@ -1133,7 +1135,7 @@ public class MongoPluginTest {
Map configMap = new HashMap<>();
// Insert multiple documents which would match the delete criterion
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "INSERT");
configMap.put(COLLECTION, "users");
@@ -1143,7 +1145,7 @@ public class MongoPluginTest {
dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block();
// Now that the documents have been inserted, lets delete one of them
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "DELETE");
configMap.put(COLLECTION, "users");
@@ -1180,7 +1182,7 @@ public class MongoPluginTest {
Map configMap = new HashMap<>();
// Insert multiple documents which would match the delete criterion
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "INSERT");
configMap.put(COLLECTION, "users");
@@ -1190,7 +1192,7 @@ public class MongoPluginTest {
dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block();
// Now that the documents have been inserted, lets delete both of them
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "DELETE");
configMap.put(COLLECTION, "users");
@@ -1221,7 +1223,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "COUNT");
configMap.put(COLLECTION, "users");
@@ -1251,7 +1253,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "DISTINCT");
configMap.put(COLLECTION, "users");
@@ -1282,7 +1284,7 @@ public class MongoPluginTest {
ActionConfiguration actionConfiguration = new ActionConfiguration();
Map configMap = new HashMap<>();
- configMap.put(BSON, Boolean.FALSE);
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
configMap.put(INPUT_TYPE, "FORM");
configMap.put(COMMAND, "AGGREGATE");
configMap.put(COLLECTION, "users");
@@ -1304,4 +1306,99 @@ public class MongoPluginTest {
.verifyComplete();
}
+ @Test
+ public void testFindCommandProjection() {
+ ActionConfiguration actionConfiguration = new ActionConfiguration();
+
+ Map configMap = new HashMap<>();
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.FALSE);
+ configMap.put(INPUT_TYPE, "FORM");
+ configMap.put(COMMAND, "FIND");
+ configMap.put(FIND_QUERY, "{ age: { \"$gte\": 30 } }");
+ configMap.put(FIND_SORT, "{ id: 1 }");
+ configMap.put(FIND_PROJECTION, "{ name: 1 }");
+ configMap.put(COLLECTION, "users");
+
+ actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap));
+
+ DatasourceConfiguration dsConfig = createDatasourceConfiguration();
+ Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);
+ Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration));
+ StepVerifier.create(executeMono)
+ .assertNext(obj -> {
+ System.out.println(obj);
+ ActionExecutionResult result = (ActionExecutionResult) obj;
+ assertNotNull(result);
+ assertTrue(result.getIsExecutionSuccess());
+ assertNotNull(result.getBody());
+ assertEquals(2, ((ArrayNode) result.getBody()).size());
+ JsonNode value = ((ArrayNode) result.getBody()).get(0).get("name");
+ assertNotNull(value);
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testBsonSmartSubstitutionMongoForm() {
+ DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
+
+ ActionConfiguration actionConfiguration = new ActionConfiguration();
+
+ Map configMap = new HashMap<>();
+ configMap.put(SMART_BSON_SUBSTITUTION, Boolean.TRUE);
+ configMap.put(INPUT_TYPE, "FORM");
+ configMap.put(COMMAND, "FIND");
+ configMap.put(FIND_QUERY, "\"{{Input1.text}}\"");
+ configMap.put(FIND_SORT, "{ id: {{Input2.text}} }");
+ configMap.put(FIND_LIMIT, "{{Input3.text}}");
+ configMap.put(COLLECTION, "{{Input4.text}}");
+
+ actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap));
+
+ ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
+ List params = new ArrayList<>();
+ Param param1 = new Param();
+ param1.setKey("Input1.text");
+ param1.setValue("{ age: { \"$gte\": 30 } }");
+ params.add(param1);
+ Param param3 = new Param();
+ param3.setKey("Input2.text");
+ param3.setValue("1");
+ params.add(param3);
+ Param param4 = new Param();
+ param4.setKey("Input3.text");
+ param4.setValue("10");
+ params.add(param4);
+ Param param5 = new Param();
+ param5.setKey("Input4.text");
+ param5.setValue("users");
+ params.add(param5);
+ executeActionDTO.setParams(params);
+
+ Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+ Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn,
+ executeActionDTO,
+ datasourceConfiguration,
+ actionConfiguration));
+
+ StepVerifier.create(executeMono)
+ .assertNext(obj -> {
+ ActionExecutionResult result = obj;
+ assertNotNull(result);
+ assertTrue(result.getIsExecutionSuccess());
+ assertNotNull(result.getBody());
+ assertEquals(2, ((ArrayNode) result.getBody()).size());
+
+ assertEquals(
+ List.of(new ParsedDataType(JSON), new ParsedDataType(RAW)).toString(),
+ result.getDataTypes().toString()
+ );
+
+ String expectedQuery = "{\"find\": \"users\", \"filter\": {\"age\": {\"$gte\": 30}}, \"sort\": {\"id\": 1}, \"limit\": 10, \"batchSize\": 10}";
+ assertEquals(expectedQuery,
+ ((RequestParamDTO)(((List)result.getRequest().getRequestParams())).get(0)).getValue());
+ })
+ .verifyComplete();
+ }
+
}
diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java
index 127d830550..6bb760376f 100644
--- a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java
+++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java
@@ -122,12 +122,8 @@ public class MssqlPlugin extends BasePlugin {
final List properties = actionConfiguration.getPluginSpecifiedTemplates();
if (properties == null || properties.get(PREPARED_STATEMENT_INDEX) == null) {
- /**
- * TODO :
- * In case the prepared statement configuration is missing, default to true once PreparedStatement
- * is no longer in beta.
- */
- isPreparedStatement = false;
+ // In case the prepared statement configuration is missing, default to true
+ isPreparedStatement = true;
} else if (properties.get(PREPARED_STATEMENT_INDEX) != null){
Object psValue = properties.get(PREPARED_STATEMENT_INDEX).getValue();
if (psValue instanceof Boolean) {
@@ -135,10 +131,10 @@ public class MssqlPlugin extends BasePlugin {
} else if (psValue instanceof String) {
isPreparedStatement = Boolean.parseBoolean((String) psValue);
} else {
- isPreparedStatement = false;
+ isPreparedStatement = true;
}
} else {
- isPreparedStatement = false;
+ isPreparedStatement = true;
}
// In case of non prepared statement, simply do binding replacement and execute
diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/setting.json
index 4c433b6bc8..a944c54241 100644
--- a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/setting.json
+++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/setting.json
@@ -17,7 +17,7 @@
"info": "Ask confirmation from the user each time before refreshing data"
},
{
- "label": "[Beta] Use Prepared Statement",
+ "label": "Use Prepared Statement",
"info": "Turning on Prepared Statement makes the query parametrized. This in turn makes it resilient against SQL injections",
"configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value",
"controlType": "SWITCH",
diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java
old mode 100644
new mode 100755
index 1efb1d2b97..c320487724
--- a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java
+++ b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java
@@ -22,6 +22,7 @@ import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.testcontainers.containers.MSSQLServerContainer;
+import org.testcontainers.utility.DockerImageName;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -31,6 +32,7 @@ import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -57,7 +59,8 @@ public class MssqlPluginTest {
@SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional.
@ClassRule
public static final MSSQLServerContainer container =
- new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2017-latest")
+ new MSSQLServerContainer<>(
+ DockerImageName.parse("mcr.microsoft.com/azure-sql-edge:1.0.3").asCompatibleSubstituteFor("mcr.microsoft.com/mssql/server:2017-latest"))
.acceptLicense()
.withExposedPorts(1433)
.withPassword("Mssql123");
@@ -229,7 +232,7 @@ public class MssqlPluginTest {
*/
List expectedRequestParams = new ArrayList<>();
expectedRequestParams.add(new RequestParamDTO(ACTION_CONFIGURATION_BODY,
- actionConfiguration.getBody(), null, null, null));
+ actionConfiguration.getBody(), null, null, new HashMap<>()));
assertEquals(result.getRequest().getRequestParams().toString(), expectedRequestParams.toString());
})
.verifyComplete();
diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java
index 6d72c48386..af260e87aa 100644
--- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java
+++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java
@@ -164,12 +164,8 @@ public class MySqlPlugin extends BasePlugin {
final List properties = actionConfiguration.getPluginSpecifiedTemplates();
if (properties == null || properties.get(PREPARED_STATEMENT_INDEX) == null) {
- /**
- * TODO :
- * In case the prepared statement configuration is missing, default to true once PreparedStatement
- * is no longer in beta.
- */
- isPreparedStatement = false;
+ // In case the prepared statement configuration is missing, default to true
+ isPreparedStatement = true;
} else if (properties.get(PREPARED_STATEMENT_INDEX) != null){
Object psValue = properties.get(PREPARED_STATEMENT_INDEX).getValue();
if (psValue instanceof Boolean) {
@@ -177,10 +173,10 @@ public class MySqlPlugin extends BasePlugin {
} else if (psValue instanceof String) {
isPreparedStatement = Boolean.parseBoolean((String) psValue);
} else {
- isPreparedStatement = false;
+ isPreparedStatement = true;
}
} else {
- isPreparedStatement = false;
+ isPreparedStatement = true;
}
requestData.put("preparedStatement", TRUE.equals(isPreparedStatement) ? true : false);
diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/setting.json
index 4c433b6bc8..cf812a3642 100644
--- a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/setting.json
+++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/setting.json
@@ -17,11 +17,11 @@
"info": "Ask confirmation from the user each time before refreshing data"
},
{
- "label": "[Beta] Use Prepared Statement",
+ "label": "Use Prepared Statement",
"info": "Turning on Prepared Statement makes the query parametrized. This in turn makes it resilient against SQL injections",
"configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value",
"controlType": "SWITCH",
- "initialValue": false
+ "initialValue": true
},
{
"label": "Query timeout (in milliseconds)",
diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java
old mode 100644
new mode 100755
index 9a9469f3ab..0567f1bfb9
--- a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java
+++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java
@@ -27,12 +27,14 @@ import org.junit.ClassRule;
import org.junit.Test;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
+import org.testcontainers.utility.DockerImageName;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -54,14 +56,16 @@ public class MySqlPluginTest {
@SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional.
@ClassRule
- public static MySQLContainer mySQLContainer = new MySQLContainer("mysql:5.7")
+ public static MySQLContainer mySQLContainer = new MySQLContainer(
+ DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql"))
.withUsername("mysql")
.withPassword("password")
.withDatabaseName("test_db");
@SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional.
@ClassRule
- public static MySQLContainer mySQLContainerWithInvalidTimezone = (MySQLContainer) new MySQLContainer()
+ public static MySQLContainer mySQLContainerWithInvalidTimezone = (MySQLContainer) new MySQLContainer(
+ DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql"))
.withUsername("mysql")
.withPassword("password")
.withDatabaseName("test_db")
@@ -296,7 +300,7 @@ public class MySqlPluginTest {
*/
List expectedRequestParams = new ArrayList<>();
expectedRequestParams.add(new RequestParamDTO(ACTION_CONFIGURATION_BODY,
- actionConfiguration.getBody(), null, null, null));
+ actionConfiguration.getBody(), null, null, new HashMap<>()));
assertEquals(result.getRequest().getRequestParams().toString(), expectedRequestParams.toString());
})
.verifyComplete();
@@ -764,7 +768,7 @@ public class MySqlPluginTest {
assertTrue(result.getIsExecutionSuccess());
Object body = result.getBody();
assertNotNull(body);
- assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-SHA\"}]",
+ assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]",
body.toString());
})
.verifyComplete();
@@ -788,7 +792,7 @@ public class MySqlPluginTest {
assertTrue(result.getIsExecutionSuccess());
Object body = result.getBody();
assertNotNull(body);
- assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-SHA\"}]",
+ assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]",
body.toString());
})
.verifyComplete();
@@ -812,7 +816,7 @@ public class MySqlPluginTest {
assertTrue(result.getIsExecutionSuccess());
Object body = result.getBody();
assertNotNull(body);
- assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-SHA\"}]",
+ assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]",
body.toString());
})
.verifyComplete();
diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java
index 4c58b7cacb..cb0ad956aa 100644
--- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java
+++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java
@@ -181,12 +181,8 @@ public class PostgresPlugin extends BasePlugin {
final List properties = actionConfiguration.getPluginSpecifiedTemplates();
if (properties == null || properties.get(PREPARED_STATEMENT_INDEX) == null) {
- /**
- * TODO :
- * In case the prepared statement configuration is missing, default to true once PreparedStatement
- * is no longer in beta.
- */
- isPreparedStatement = false;
+ //In case the prepared statement configuration is missing, default to true.
+ isPreparedStatement = true;
} else if (properties.get(PREPARED_STATEMENT_INDEX) != null){
Object psValue = properties.get(PREPARED_STATEMENT_INDEX).getValue();
if (psValue instanceof Boolean) {
@@ -194,10 +190,10 @@ public class PostgresPlugin extends BasePlugin {
} else if (psValue instanceof String) {
isPreparedStatement = Boolean.parseBoolean((String) psValue);
} else {
- isPreparedStatement = false;
+ isPreparedStatement = true;
}
} else {
- isPreparedStatement = false;
+ isPreparedStatement = true;
}
// In case of non prepared statement, simply do binding replacement and execute
@@ -206,7 +202,8 @@ public class PostgresPlugin extends BasePlugin {
return executeCommon(connection, datasourceConfiguration, actionConfiguration, FALSE, null, null);
}
- //Prepared Statement
+ // Prepared Statement
+
// First extract all the bindings in order
List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query);
// Replace all the bindings with a ? as expected in a prepared statement.
diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json
index 4c433b6bc8..cf812a3642 100644
--- a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json
+++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json
@@ -17,11 +17,11 @@
"info": "Ask confirmation from the user each time before refreshing data"
},
{
- "label": "[Beta] Use Prepared Statement",
+ "label": "Use Prepared Statement",
"info": "Turning on Prepared Statement makes the query parametrized. This in turn makes it resilient against SQL injections",
"configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value",
"controlType": "SWITCH",
- "initialValue": false
+ "initialValue": true
},
{
"label": "Query timeout (in milliseconds)",
diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
index f5dca31ecb..39fa6e97af 100644
--- a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
+++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
@@ -22,10 +22,13 @@ import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
-import redis.clients.jedis.exceptions.JedisConnectionException;
+import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.SafeEncoder;
+import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
@@ -37,6 +40,7 @@ import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATI
public class RedisPlugin extends BasePlugin {
private static final Long DEFAULT_PORT = 6379L;
+ private static final int CONNECTION_TIMEOUT = 60;
public RedisPlugin(PluginWrapper wrapper) {
super(wrapper);
@@ -44,12 +48,12 @@ public class RedisPlugin extends BasePlugin {
@Slf4j
@Extension
- public static class RedisPluginExecutor implements PluginExecutor {
+ public static class RedisPluginExecutor implements PluginExecutor {
private final Scheduler scheduler = Schedulers.elastic();
@Override
- public Mono execute(Jedis jedis,
+ public Mono execute(JedisPool jedisPool,
DatasourceConfiguration datasourceConfiguration,
ActionConfiguration actionConfiguration) {
@@ -57,6 +61,7 @@ public class RedisPlugin extends BasePlugin {
List requestParams = List.of(new RequestParamDTO(ACTION_CONFIGURATION_BODY, query, null
, null, null));
+ Jedis jedis = jedisPool.getResource();
return Mono.fromCallable(() -> {
if (StringUtils.isNullOrEmpty(query)) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
@@ -92,6 +97,7 @@ public class RedisPlugin extends BasePlugin {
.flatMap(obj -> obj)
.map(obj -> (ActionExecutionResult) obj)
.onErrorResume(error -> {
+ error.printStackTrace();
ActionExecutionResult result = new ActionExecutionResult();
result.setIsExecutionSuccess(false);
result.setErrorInfo(error);
@@ -107,15 +113,14 @@ public class RedisPlugin extends BasePlugin {
return result;
})
.doFinally(signalType -> {
- /*
- * - For some reason, Jedis throws a socket error when kept idle for like 10 min when
- * appsmith is setup via docker image.
- * - APMU, jedis.close() should disconnect the connection, causing jedis to refresh connection
- * during next execution.
- * - This is a placeholder solution till better fix is available (would connection pool fix
- * it ?)
+ /**
+ * - Return resource back to the pool.
+ * - https://stackoverflow.com/questions/54902337/is-it-necessary-to-use-jedis-close
+ * - https://www.baeldung.com/jedis-java-redis-client-library:
*/
- jedis.close();
+ if (jedis != null) {
+ jedis.close();
+ }
})
.subscribeOn(scheduler);
}
@@ -136,10 +141,31 @@ public class RedisPlugin extends BasePlugin {
}
}
- @Override
- public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
- return (Mono) Mono.fromCallable(() -> {
+ /**
+ * - Config taken from https://www.baeldung.com/jedis-java-redis-client-library
+ * - To understand what these config mean:
+ * https://www.infoworld.com/article/2071834/pool-resources-using-apache-s-commons-pool-framework.html
+ */
+ private JedisPoolConfig buildPoolConfig() {
+ final JedisPoolConfig poolConfig = new JedisPoolConfig();
+ poolConfig.setMaxTotal(5);
+ poolConfig.setMaxIdle(5);
+ poolConfig.setMinIdle(0);
+ poolConfig.setTestOnBorrow(true);
+ poolConfig.setTestOnReturn(true);
+ poolConfig.setTestWhileIdle(true);
+ poolConfig.setMinEvictableIdleTimeMillis(Duration.ofSeconds(60).toMillis());
+ poolConfig.setTimeBetweenEvictionRunsMillis(Duration.ofSeconds(30).toMillis());
+ poolConfig.setNumTestsPerEvictionRun(3);
+ poolConfig.setBlockWhenExhausted(true);
+ return poolConfig;
+ }
+
+ @Override
+ public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
+
+ return (Mono) Mono.fromCallable(() -> {
if (datasourceConfiguration.getEndpoints().isEmpty()) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "No endpoint(s) " +
"configured"));
@@ -147,29 +173,40 @@ public class RedisPlugin extends BasePlugin {
Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0);
Integer port = (int) (long) ObjectUtils.defaultIfNull(endpoint.getPort(), DEFAULT_PORT);
- Jedis jedis = new Jedis(endpoint.getHost(), port);
-
+ final JedisPoolConfig poolConfig = buildPoolConfig();
DBAuth auth = (DBAuth) datasourceConfiguration.getAuthentication();
- if (auth != null && DBAuth.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) {
- jedis.auth(auth.getUsername(), auth.getPassword());
+ int timeout = (int)Duration.ofSeconds(CONNECTION_TIMEOUT).toMillis();
+ JedisPool jedisPool;
+ if (auth != null && StringUtils.isNotNullOrEmpty(auth.getPassword())) {
+ if (StringUtils.isNullOrEmpty(auth.getUsername())) {
+ // If username is empty, then authenticate with password only.
+ jedisPool = new JedisPool(poolConfig, endpoint.getHost(), port, timeout, auth.getPassword());
+ }
+ else {
+ jedisPool = new JedisPool(poolConfig, endpoint.getHost(), port, timeout,
+ auth.getUsername(), auth.getPassword());
+ }
+ }
+ else {
+ jedisPool = new JedisPool(poolConfig, endpoint.getHost(), port);
}
- return Mono.just(jedis);
+ return Mono.just(jedisPool);
})
.flatMap(obj -> obj)
.subscribeOn(scheduler);
}
@Override
- public void datasourceDestroy(Jedis jedis) {
+ public void datasourceDestroy(JedisPool jedisPool) {
// Schedule on elastic thread pool and subscribe immediately.
Mono.fromSupplier(() -> {
try {
- if (jedis != null) {
- jedis.close();
+ if (jedisPool != null) {
+ jedisPool.destroy();
}
- } catch (JedisConnectionException exc) {
- System.out.println("Error closing Redis connection");
+ } catch (JedisException e) {
+ System.out.println("Error destroying Jedis pool.");
}
return Mono.empty();
@@ -226,9 +263,10 @@ public class RedisPlugin extends BasePlugin {
return Mono.fromCallable(() ->
datasourceCreate(datasourceConfiguration)
- .map(jedis -> {
+ .map(jedisPool -> {
+ Jedis jedis = jedisPool.getResource();
verifyPing(jedis).block();
- datasourceDestroy(jedis);
+ datasourceDestroy(jedisPool);
return new DatasourceTestResult();
})
.onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage()))))
diff --git a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java
index 58274a14d2..8b271a20c3 100644
--- a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java
+++ b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java
@@ -19,7 +19,7 @@ import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
-import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
import java.util.ArrayList;
import java.util.Collections;
@@ -59,13 +59,13 @@ public class RedisPluginTest {
@Test
public void itShouldCreateDatasource() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
- Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+ Mono jedisPoolMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
- StepVerifier.create(jedisMono)
+ StepVerifier.create(jedisPoolMono)
.assertNext(Assert::assertNotNull)
.verifyComplete();
- pluginExecutor.datasourceDestroy(jedisMono.block());
+ pluginExecutor.datasourceDestroy(jedisPoolMono.block());
}
@Test
@@ -152,12 +152,12 @@ public class RedisPluginTest {
@Test
public void itShouldThrowErrorIfEmptyBody() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
- Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+ Mono jedisPoolMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
ActionConfiguration actionConfiguration = new ActionConfiguration();
- Mono actionExecutionResultMono = jedisMono
- .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
+ Mono actionExecutionResultMono = jedisPoolMono
+ .flatMap(jedisPool -> pluginExecutor.execute(jedisPool, datasourceConfiguration, actionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.assertNext(result -> {
@@ -171,13 +171,13 @@ public class RedisPluginTest {
@Test
public void itShouldThrowErrorIfInvalidRedisCommand() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
- Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+ Mono jedisPoolMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("LOL");
- Mono actionExecutionResultMono = jedisMono
- .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
+ Mono actionExecutionResultMono = jedisPoolMono
+ .flatMap(jedisPool -> pluginExecutor.execute(jedisPool, datasourceConfiguration, actionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.assertNext(result -> {
@@ -191,13 +191,13 @@ public class RedisPluginTest {
@Test
public void itShouldExecuteCommandWithoutArgs() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
- Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+ Mono jedisPoolMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("PING");
- Mono actionExecutionResultMono = jedisMono
- .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration));
+ Mono actionExecutionResultMono = jedisPoolMono
+ .flatMap(jedisPool -> pluginExecutor.execute(jedisPool, datasourceConfiguration, actionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.assertNext(actionExecutionResult -> {
@@ -211,13 +211,14 @@ public class RedisPluginTest {
@Test
public void itShouldExecuteCommandWithArgs() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
- Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
+ Mono jedisPoolMono = pluginExecutor.datasourceCreate(datasourceConfiguration);
// Getting a non-existent key
ActionConfiguration getActionConfiguration = new ActionConfiguration();
getActionConfiguration.setBody("GET key");
- Mono actionExecutionResultMono = jedisMono
- .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, getActionConfiguration));
+ Mono actionExecutionResultMono = jedisPoolMono
+ .flatMap(jedisPool -> pluginExecutor.execute(jedisPool, datasourceConfiguration,
+ getActionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.assertNext(actionExecutionResult -> {
Assert.assertNotNull(actionExecutionResult);
@@ -225,8 +226,8 @@ public class RedisPluginTest {
final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0);
Assert.assertEquals("null", node.get("result").asText());
- /*
- * - Adding only in this test as the query editor form for Redis plugin is exactly same for each
+
+ /* - Adding only in this test as the query editor form for Redis plugin is exactly same for each
* query type. Hence, checking with only one query should suffice.
* - RequestParamDTO object only have attributes configProperty and value at this point.
* - The other two RequestParamDTO attributes - label and type are null at this point.
@@ -241,8 +242,9 @@ public class RedisPluginTest {
// Setting a key
ActionConfiguration setActionConfiguration = new ActionConfiguration();
setActionConfiguration.setBody("SET key value");
- actionExecutionResultMono = jedisMono
- .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, setActionConfiguration));
+ actionExecutionResultMono = jedisPoolMono
+ .flatMap(jedisPool -> pluginExecutor.execute(jedisPool, datasourceConfiguration,
+ setActionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.assertNext(actionExecutionResult -> {
Assert.assertNotNull(actionExecutionResult);
@@ -252,8 +254,9 @@ public class RedisPluginTest {
}).verifyComplete();
// Getting the key
- actionExecutionResultMono = jedisMono
- .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, getActionConfiguration));
+ actionExecutionResultMono = jedisPoolMono
+ .flatMap(jedisPool -> pluginExecutor.execute(jedisPool, datasourceConfiguration,
+ getActionConfiguration));
StepVerifier.create(actionExecutionResultMono)
.assertNext(actionExecutionResult -> {
Assert.assertNotNull(actionExecutionResult);
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java
index 75d967d126..e73b4ad822 100644
--- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java
+++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java
@@ -41,7 +41,6 @@ import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
-import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
@@ -127,12 +126,8 @@ public class RestApiPlugin extends BasePlugin {
List> parameters = new ArrayList<>();
if (CollectionUtils.isEmpty(properties)) {
- /**
- * TODO :
- * In case the smart json substitution configuration is missing, default to true once smart json
- * substitution is no longer in beta.
- */
- smartJsonSubstitution = false;
+ // In case the smart json substitution configuration is missing, default to true
+ smartJsonSubstitution = true;
// Since properties is not empty, we are guaranteed to find the first property.
} else if (properties.get(SMART_JSON_SUBSTITUTION_INDEX) != null) {
@@ -142,10 +137,10 @@ public class RestApiPlugin extends BasePlugin {
} else if (ssubValue instanceof String) {
smartJsonSubstitution = Boolean.parseBoolean((String) ssubValue);
} else {
- smartJsonSubstitution = false;
+ smartJsonSubstitution = true;
}
} else {
- smartJsonSubstitution = false;
+ smartJsonSubstitution = true;
}
// Smartly substitute in actionConfiguration.body and replace all the bindings with values.
diff --git a/app/server/appsmith-server/pom.xml b/app/server/appsmith-server/pom.xml
index 46f27e8d23..aab5928284 100644
--- a/app/server/appsmith-server/pom.xml
+++ b/app/server/appsmith-server/pom.xml
@@ -207,7 +207,7 @@
org.apache.httpcomponents
httpclient
- 4.5.10
+ 4.5.13
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java
index 2425036fa6..aa0b7ef0f7 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java
@@ -44,6 +44,7 @@ public enum AclPermission {
ORGANIZATION_MANAGE_APPLICATIONS("manage:orgApplications", Organization.class),
ORGANIZATION_READ_APPLICATIONS("read:orgApplications", Organization.class),
ORGANIZATION_PUBLISH_APPLICATIONS("publish:orgApplications", Organization.class),
+ ORGANIZATION_EXPORT_APPLICATIONS("export:orgApplications", Organization.class),
// Invitation related permissions
ORGANIZATION_INVITE_USERS("inviteUsers:organization", Organization.class),
@@ -51,6 +52,7 @@ public enum AclPermission {
MANAGE_APPLICATIONS("manage:applications", Application.class),
READ_APPLICATIONS("read:applications", Application.class),
PUBLISH_APPLICATIONS("publish:applications", Application.class),
+ EXPORT_APPLICATIONS("export:applications", Application.class),
// Making an application public permission at Organization level
MAKE_PUBLIC_APPLICATIONS("makePublic:applications", Application.class),
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java
index d1a80ec2fc..7138666b93 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java
@@ -8,6 +8,7 @@ import java.util.Set;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_ORGANIZATIONS;
+import static com.appsmith.server.acl.AclPermission.ORGANIZATION_EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_PUBLISH_APPLICATIONS;
@@ -19,10 +20,12 @@ import static com.appsmith.server.acl.AclPermission.READ_ORGANIZATIONS;
public enum AppsmithRole {
APPLICATION_ADMIN("Application Administrator", "", Set.of(MANAGE_APPLICATIONS)),
APPLICATION_VIEWER("Application Viewer", "", Set.of(READ_APPLICATIONS)),
- ORGANIZATION_ADMIN("Administrator", "Can modify all organization settings including editing applications and inviting other users to the organization",
- Set.of(MANAGE_ORGANIZATIONS, ORGANIZATION_INVITE_USERS)),
- ORGANIZATION_DEVELOPER("Developer", "Can edit and view applications along with inviting other users to the organization", Set.of(READ_ORGANIZATIONS,
- ORGANIZATION_MANAGE_APPLICATIONS, ORGANIZATION_READ_APPLICATIONS, ORGANIZATION_PUBLISH_APPLICATIONS, ORGANIZATION_INVITE_USERS)),
+ ORGANIZATION_ADMIN("Administrator", "Can modify all organization settings including editing applications, " +
+ "inviting other users to the organization and exporting applications from the organization",
+ Set.of(MANAGE_ORGANIZATIONS, ORGANIZATION_INVITE_USERS, ORGANIZATION_EXPORT_APPLICATIONS)),
+ ORGANIZATION_DEVELOPER("Developer", "Can edit and view applications along with inviting other users to the organization",
+ Set.of(READ_ORGANIZATIONS, ORGANIZATION_MANAGE_APPLICATIONS, ORGANIZATION_READ_APPLICATIONS,
+ ORGANIZATION_PUBLISH_APPLICATIONS, ORGANIZATION_INVITE_USERS)),
ORGANIZATION_VIEWER(
"App Viewer",
"Can view applications and invite other users to view applications",
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java
index 3b6112a892..cd5652f61e 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java
@@ -23,6 +23,7 @@ import static com.appsmith.server.acl.AclPermission.COMMENT_ON_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.COMMENT_ON_THREAD;
import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS;
import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES;
+import static com.appsmith.server.acl.AclPermission.EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
@@ -30,6 +31,7 @@ import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.MANAGE_ORGANIZATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
import static com.appsmith.server.acl.AclPermission.MANAGE_USERS;
+import static com.appsmith.server.acl.AclPermission.ORGANIZATION_EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_PUBLISH_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_READ_APPLICATIONS;
@@ -115,6 +117,7 @@ public class PolicyGenerator {
hierarchyGraph.addEdge(ORGANIZATION_READ_APPLICATIONS, READ_APPLICATIONS);
hierarchyGraph.addEdge(ORGANIZATION_PUBLISH_APPLICATIONS, PUBLISH_APPLICATIONS);
hierarchyGraph.addEdge(MANAGE_ORGANIZATIONS, MAKE_PUBLIC_APPLICATIONS);
+ hierarchyGraph.addEdge(ORGANIZATION_EXPORT_APPLICATIONS, EXPORT_APPLICATIONS);
// If the user is being given MANAGE_APPLICATION permission, they must also be given READ_APPLICATION perm
lateralGraph.addEdge(MANAGE_APPLICATIONS, READ_APPLICATIONS);
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java
index c8e035b655..a1d7fca78d 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java
@@ -135,11 +135,11 @@ public class ApplicationController extends BaseController {
-
+ String applicationName = fetchedResource.getExportedApplication().getName();
HttpHeaders responseHeaders = new HttpHeaders();
ContentDisposition contentDisposition = ContentDisposition
.builder("attachment")
- .filename("application-file.json", StandardCharsets.UTF_8)
+ .filename(applicationName + ".json", StandardCharsets.UTF_8)
.build();
responseHeaders.setContentDisposition(contentDisposition);
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java
index 7d37d09b08..9c57753dae 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java
@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -41,16 +42,16 @@ public class CommentController extends BaseController new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
}
@PostMapping("/threads")
@ResponseStatus(HttpStatus.CREATED)
public Mono> createThread(@Valid @RequestBody CommentThread resource,
- ServerWebExchange exchange) {
+ @RequestHeader(name = "Origin") String originHeader) {
log.debug("Going to create resource {}", resource.getClass().getName());
- return service.createThread(resource)
+ return service.createThread(resource, originHeader)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
}
@@ -63,10 +64,10 @@ public class CommentController extends BaseController> updateThread(
@Valid @RequestBody CommentThread resource,
- @PathVariable String threadId
+ @PathVariable String threadId, ServerWebExchange exchange
) {
log.debug("Going to update resource {}", resource.getClass().getName());
- return service.updateThread(threadId, resource)
+ return service.updateThread(threadId, resource, exchange.getRequest().getHeaders().getOrigin())
.map(updated -> new ResponseDTO<>(HttpStatus.ACCEPTED.value(), updated, null));
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThread.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThread.java
index 26deb328aa..288b6347d2 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThread.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThread.java
@@ -25,6 +25,8 @@ public class CommentThread extends BaseDomain {
String refId;
+ String pageId;
+
CommentThreadState pinnedState;
CommentThreadState resolvedState;
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/events/AbstractCommentEvent.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/AbstractCommentEvent.java
new file mode 100644
index 0000000000..003947218f
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/AbstractCommentEvent.java
@@ -0,0 +1,13 @@
+package com.appsmith.server.events;
+
+import com.appsmith.server.domains.Application;
+import com.appsmith.server.domains.Organization;
+import lombok.Data;
+
+@Data
+public abstract class AbstractCommentEvent {
+ private final String authorUserName;
+ private final Organization organization;
+ private final Application application;
+ private final String originHeader;
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/events/CommentAddedEvent.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/CommentAddedEvent.java
new file mode 100644
index 0000000000..441702d790
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/CommentAddedEvent.java
@@ -0,0 +1,16 @@
+package com.appsmith.server.events;
+
+import com.appsmith.server.domains.Application;
+import com.appsmith.server.domains.Comment;
+import com.appsmith.server.domains.Organization;
+import lombok.Getter;
+
+@Getter
+public class CommentAddedEvent extends AbstractCommentEvent {
+ private final Comment comment;
+
+ public CommentAddedEvent(String authorUserName, Organization organization, Application application, String originHeader, Comment comment) {
+ super(authorUserName, organization, application, originHeader);
+ this.comment = comment;
+ }
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/events/CommentThreadClosedEvent.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/CommentThreadClosedEvent.java
new file mode 100644
index 0000000000..6c2fc7bea1
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/CommentThreadClosedEvent.java
@@ -0,0 +1,16 @@
+package com.appsmith.server.events;
+
+import com.appsmith.server.domains.Application;
+import com.appsmith.server.domains.CommentThread;
+import com.appsmith.server.domains.Organization;
+import lombok.Getter;
+
+@Getter
+public class CommentThreadClosedEvent extends AbstractCommentEvent {
+ private final CommentThread commentThread;
+
+ public CommentThreadClosedEvent(String authorUserName, Organization organization, Application application, String originHeader, CommentThread commentThread) {
+ super(authorUserName, organization, application, originHeader);
+ this.commentThread = commentThread;
+ }
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/events/UserChangedEvent.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/UserChangedEvent.java
index ea6c2a0bec..af2d27d937 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/events/UserChangedEvent.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/events/UserChangedEvent.java
@@ -2,10 +2,8 @@ package com.appsmith.server.events;
import com.appsmith.server.domains.User;
import lombok.Data;
-import lombok.RequiredArgsConstructor;
@Data
-@RequiredArgsConstructor
public class UserChangedEvent {
private final User user;
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/CommentUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/CommentUtils.java
new file mode 100644
index 0000000000..50230fb998
--- /dev/null
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/CommentUtils.java
@@ -0,0 +1,43 @@
+package com.appsmith.server.helpers;
+
+import com.appsmith.server.domains.Comment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CommentUtils {
+ /**
+ * Checks whether provided user has been mentioned in the comment. Returns true if yes and false otherwise.
+ * @param comment Comment objects
+ * @param userEmail email address of the user
+ * @return true or false based on the condition
+ */
+ public static boolean isUserMentioned(Comment comment, String userEmail) {
+ if(comment.getBody() != null && comment.getBody().getEntityMap() != null) {
+ for(String key : comment.getBody().getEntityMap().keySet()) {
+ Comment.Entity commentEntity = comment.getBody().getEntityMap().get(key);
+ if(commentEntity != null && commentEntity.getType() != null
+ && commentEntity.getType().equals("mention")) {
+ // this comment has a mention, check the provided user is mentioned or not
+ if(commentEntity.getData() != null) {
+ Comment.EntityData.Mention mention = commentEntity.getData().getMention();
+ if(mention.getUser().getUsername().equals(userEmail)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public static List getCommentBody(Comment comment) {
+ List commentLines = new ArrayList<>();
+ if(comment.getBody() != null && comment.getBody().getBlocks() != null) {
+ for (Comment.Block block : comment.getBody().getBlocks()) {
+ commentLines.add(block.getText());
+ }
+ }
+ return commentLines;
+ }
+}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java
index 030aa70595..3ca1ab4cc6 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java
@@ -50,6 +50,7 @@ import com.appsmith.server.services.OrganizationService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.cloudyrock.mongock.ChangeLog;
import com.github.cloudyrock.mongock.ChangeSet;
+import com.github.cloudyrock.mongock.decorator.impl.MongockTemplate;
import com.google.gson.Gson;
import com.mongodb.MongoClient;
import com.mongodb.MongoException;
@@ -98,7 +99,9 @@ import java.util.stream.Collectors;
import static com.appsmith.external.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject;
import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS;
+import static com.appsmith.server.acl.AclPermission.EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS;
+import static com.appsmith.server.acl.AclPermission.ORGANIZATION_EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
import static com.appsmith.server.helpers.CollectionUtils.isNullOrEmpty;
@@ -2257,4 +2260,203 @@ public class DatabaseChangelog {
// Delete all the existing mongo datasource structures by setting the key to null.
mongoOperations.updateMulti(query, update, Datasource.class);
}
+
+ @ChangeSet(order = "069", id = "set-mongo-actions-type-to-raw", author = "")
+ public void setMongoActionInputToRaw(MongockTemplate mongockTemplate) {
+
+ // All the existing mongo actions at this point will only have ever been in the raw format
+ // For these actions to be readily available to users, we need to set their input type to raw manually
+ // This is required because since the mongo form, the default input type on the UI has been set to FORM
+ Plugin mongoPlugin = mongockTemplate.findOne(query(where("packageName").is("mongo-plugin")), Plugin.class);
+
+ // Fetch all the actions built on top of a mongo database, not having any value set for input type
+ assert mongoPlugin != null;
+ List rawMongoActions = mongockTemplate.find(
+ query(new Criteria().andOperator(
+ where(fieldName(QNewAction.newAction.pluginId)).is(mongoPlugin.getId()))),
+ NewAction.class
+ )
+ .stream()
+ .filter(mongoAction -> {
+ if (mongoAction.getUnpublishedAction() == null || mongoAction.getUnpublishedAction().getActionConfiguration() == null) {
+ return false;
+ }
+ final List pluginSpecifiedTemplates = mongoAction.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates();
+ return pluginSpecifiedTemplates != null && pluginSpecifiedTemplates.size() == 1;
+ })
+ .collect(Collectors.toList());
+
+ for (NewAction action : rawMongoActions) {
+ List pluginSpecifiedTemplates = action.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates();
+ pluginSpecifiedTemplates.add(new Property(null, "RAW"));
+
+ mongockTemplate.save(action);
+ }
+ }
+
+ /**
+ * - Older firestore action form had support for only on where condition, which mapped path, operator and value to
+ * three different indexes on the pluginSpecifiedTemplates list.
+ * - In the newer form, the three properties are treated as a tuple, and a list of tuples is mapped to only one
+ * index in pluginSpecifiedTemplates list.
+ * - [... path, operator, value, ...] --> [... [ {"path":path, "operator":operator, "value":value} ] ...]
+ */
+ @ChangeSet(order = "070", id = "update-firestore-where-conditions-data", author = "")
+ public void updateFirestoreWhereConditionsData(MongoTemplate mongoTemplate) {
+ Plugin firestorePlugin = mongoTemplate
+ .findOne(query(where("packageName").is("firestore-plugin")), Plugin.class);
+
+ Query query = query(new Criteria().andOperator(
+ where("pluginId").is(firestorePlugin.getId()),
+ new Criteria().orOperator(
+ where("unpublishedAction.actionConfiguration.pluginSpecifiedTemplates.3").exists(true),
+ where("unpublishedAction.actionConfiguration.pluginSpecifiedTemplates.4").exists(true),
+ where("unpublishedAction.actionConfiguration.pluginSpecifiedTemplates.5").exists(true),
+ where("publishedAction.actionConfiguration.pluginSpecifiedTemplates.3").exists(true),
+ where("publishedAction.actionConfiguration.pluginSpecifiedTemplates.4").exists(true),
+ where("publishedAction.actionConfiguration.pluginSpecifiedTemplates.5").exists(true)
+ )));
+
+ List firestoreActionQueries = mongoTemplate.find(query, NewAction.class);
+
+ firestoreActionQueries.stream()
+ .forEach(action -> {
+ // For unpublished action
+ if (action.getUnpublishedAction() != null
+ && action.getUnpublishedAction().getActionConfiguration() != null
+ && action.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates() != null
+ && action.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates().size() > 3) {
+
+ String path = null;
+ String op = null;
+ String value = null;
+ List properties = action.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates();
+ if (properties.size() > 3 && properties.get(3) != null) {
+ path = (String) properties.get(3).getValue();
+ }
+ if (properties.size() > 4 && properties.get(4) != null) {
+ op = (String) properties.get(4).getValue();
+ properties.set(4, null); // Index 4 does not map to any value in the new query format
+ }
+ if (properties.size() > 5 && properties.get(5) != null) {
+ value = (String) properties.get(5).getValue();
+ properties.set(5, null); // Index 5 does not map to any value in the new query format
+ }
+
+ Map newFormat = new HashMap();
+ newFormat.put("path", path);
+ newFormat.put("operator", op);
+ newFormat.put("value", value);
+ properties.set(3, new Property("whereConditionTuples", List.of(newFormat)));
+ }
+
+ // For published action
+ if (action.getPublishedAction() != null
+ && action.getPublishedAction().getActionConfiguration() != null
+ && action.getPublishedAction().getActionConfiguration().getPluginSpecifiedTemplates() != null
+ && action.getPublishedAction().getActionConfiguration().getPluginSpecifiedTemplates().size() > 3) {
+
+ String path = null;
+ String op = null;
+ String value = null;
+ List properties = action.getPublishedAction().getActionConfiguration().getPluginSpecifiedTemplates();
+ if (properties.size() > 3 && properties.get(3) != null) {
+ path = (String) properties.get(3).getValue();
+ }
+ if (properties.size() > 4 && properties.get(4) != null) {
+ op = (String) properties.get(4).getValue();
+ properties.set(4, null); // Index 4 does not map to any value in the new query format
+ }
+ if (properties.size() > 5 && properties.get(5) != null) {
+ value = (String) properties.get(5).getValue();
+ properties.set(5, null); // Index 5 does not map to any value in the new query format
+ }
+
+ HashMap newFormat = new HashMap();
+ newFormat.put("path", path);
+ newFormat.put("operator", op);
+ newFormat.put("value", value);
+ properties.set(3, new Property("whereConditionTuples", List.of(newFormat)));
+ }
+ });
+
+ /**
+ * - Save changes only after all the processing is done so that in case any data manipulation fails, no data
+ * write occurs.
+ * - Write data back to db only if all data manipulations done above have succeeded.
+ */
+ firestoreActionQueries.stream()
+ .forEach(action -> mongoTemplate.save(action));
+ }
+
+ @ChangeSet(order = "071", id = "add-application-export-permissions", author = "")
+ public void addApplicationExportPermissions(MongoTemplate mongoTemplate) {
+ final List organizations = mongoTemplate.find(
+ query(where("userRoles").exists(true)),
+ Organization.class
+ );
+
+ for (final Organization organization : organizations) {
+ Set adminUsernames = organization.getUserRoles()
+ .stream()
+ .filter(role -> (role.getRole().equals(AppsmithRole.ORGANIZATION_ADMIN)))
+ .map(role -> role.getUsername())
+ .collect(Collectors.toSet());
+
+ if (adminUsernames.isEmpty()) {
+ continue;
+ }
+ // All the administrators of the organization should be allowed to export applications permission
+ Set exportApplicationPermissionUsernames = new HashSet<>();
+ exportApplicationPermissionUsernames.addAll(adminUsernames);
+
+ Set policies = organization.getPolicies();
+ if (policies == null) {
+ policies = new HashSet<>();
+ }
+
+ Optional exportAppOrgLevelOptional = policies.stream()
+ .filter(policy -> policy.getPermission().equals(ORGANIZATION_EXPORT_APPLICATIONS.getValue())).findFirst();
+
+ if (exportAppOrgLevelOptional.isPresent()) {
+ Policy exportApplicationPolicy = exportAppOrgLevelOptional.get();
+ exportApplicationPolicy.getUsers().addAll(exportApplicationPermissionUsernames);
+ } else {
+ // this policy doesnt exist. create and add this to the policy set
+ Policy inviteUserPolicy = Policy.builder().permission(ORGANIZATION_EXPORT_APPLICATIONS.getValue())
+ .users(exportApplicationPermissionUsernames).build();
+ organization.getPolicies().add(inviteUserPolicy);
+ }
+
+ mongoTemplate.save(organization);
+
+ // Update the applications with export applications policy for all administrators of the organization
+ List orgApplications = mongoTemplate.find(
+ query(where(fieldName(QApplication.application.organizationId)).is(organization.getId())),
+ Application.class
+ );
+
+ for (final Application application : orgApplications) {
+ Set applicationPolicies = application.getPolicies();
+ if (applicationPolicies == null) {
+ applicationPolicies = new HashSet<>();
+ }
+
+ Optional exportAppOptional = applicationPolicies.stream()
+ .filter(policy -> policy.getPermission().equals(EXPORT_APPLICATIONS.getValue())).findFirst();
+
+ if (exportAppOptional.isPresent()) {
+ Policy exportAppPolicy = exportAppOptional.get();
+ exportAppPolicy.getUsers().addAll(adminUsernames);
+ } else {
+ // this policy doesn't exist, create and add this to the policy set
+ Policy newExportAppPolicy = Policy.builder().permission(EXPORT_APPLICATIONS.getValue())
+ .users(adminUsernames).build();
+ application.getPolicies().add(newExportAppPolicy);
+ }
+
+ mongoTemplate.save(application);
+ }
+ }
+ }
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java
index 00e320d92e..c90ad40cf8 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java
@@ -43,7 +43,7 @@ public class EmailSender {
REPLY_TO = makeReplyTo();
}
- public Mono sendMail(String to, String subject, String text, Map params) {
+ public Mono sendMail(String to, String subject, String text, Map params) {
/**
* Creating a publisher which sends email in a blocking fashion, subscribing on the bounded elastic
@@ -122,7 +122,7 @@ public class EmailSender {
* @return Template string with Mustache replacements applied.
* @throws IOException bubbled from Mustache renderer.
*/
- private String replaceEmailTemplate(String template, Map params) throws IOException {
+ private String replaceEmailTemplate(String template, Map params) throws IOException {
MustacheFactory mf = new DefaultMustacheFactory();
StringWriter stringWriter = new StringWriter();
Mustache mustache = mf.compile(template);
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java
index 3d80001be8..59d9e1f33a 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java
@@ -16,4 +16,5 @@ public interface CustomOrganizationRepository extends AppsmithRepository nextSlugNumber(String slugPrefix);
+ Mono updateUserRoleNames(String userId, String userName);
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java
index 9b45fa8d70..b96c0ff0b8 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java
@@ -1,6 +1,7 @@
package com.appsmith.server.repositories;
import com.appsmith.server.acl.AclPermission;
+import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.QOrganization;
import lombok.extern.slf4j.Slf4j;
@@ -9,6 +10,7 @@ import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -66,4 +68,14 @@ public class CustomOrganizationRepositoryImpl extends BaseAppsmithRepositoryImpl
});
}
+ @Override
+ public Mono updateUserRoleNames(String userId, String userName) {
+ return mongoOperations
+ .updateMulti(
+ Query.query(Criteria.where("userRoles.userId").is(userId)),
+ Update.update("userRoles.$.name", userName),
+ Organization.class
+ )
+ .then();
+ }
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/OrganizationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/OrganizationRepository.java
index 6d2224f775..76e51eaf35 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/OrganizationRepository.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/OrganizationRepository.java
@@ -13,4 +13,6 @@ public interface OrganizationRepository extends BaseRepository findByName(String name);
+ Mono updateUserRoleNames(String userId, String userName);
+
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java
index 4960633140..f20289c7ea 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java
@@ -8,11 +8,11 @@ import java.util.List;
public interface CommentService extends CrudService {
- Mono create(String threadId, Comment organization);
+ Mono create(String threadId, Comment organization, String originHeader);
- Mono createThread(CommentThread commentThread);
+ Mono createThread(CommentThread commentThread, String originHeader);
- Mono updateThread(String threadId, CommentThread commentThread);
+ Mono updateThread(String threadId, CommentThread commentThread, String originHeader);
Mono> getThreadsByApplicationId(String applicationId);
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java
index 95de56d12c..7643c784bb 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java
@@ -11,11 +11,14 @@ import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.CommentThreadNotification;
import com.appsmith.server.domains.Notification;
import com.appsmith.server.domains.User;
+import com.appsmith.server.events.CommentAddedEvent;
+import com.appsmith.server.events.CommentThreadClosedEvent;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.repositories.CommentRepository;
import com.appsmith.server.repositories.CommentThreadRepository;
+import com.appsmith.server.solutions.EmailEventHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -52,6 +55,7 @@ public class CommentServiceImpl extends BaseService create(String threadId, Comment comment) {
- return create(threadId, comment, true);
+ public Mono create(String threadId, Comment comment, String originHeader) {
+ return create(threadId, comment, originHeader, true);
}
- public Mono create(String threadId, Comment comment, boolean shouldCreateNotification) {
+ private Mono create(String threadId, Comment comment, String originHeader, boolean shouldCreateNotification) {
if (StringUtils.isWhitespace(comment.getAuthorName())) {
// Error: User can't explicitly set the author name. It will be the currently logged in user.
return Mono.empty();
@@ -125,36 +130,40 @@ public class CommentServiceImpl extends BaseService {
final User user = tuple.getT1();
- final Comment savedComment = tuple.getT2();
+ CommentThread commentThread = tuple.getT2();
+ final Comment savedComment = tuple.getT3();
+ Mono publishEmailMono = emailEventHandler.publish(
+ comment.getAuthorUsername(), commentThread.getApplicationId(), comment, originHeader
+ );
if (shouldCreateNotification) {
final Set usernames = policyUtils.findUsernamesWithPermission(
savedComment.getPolicies(), AclPermission.READ_COMMENT);
-
- List> monos = new ArrayList<>();
+ List> notificationMonos = new ArrayList<>();
for (String username : usernames) {
if (!username.equals(user.getUsername())) {
final CommentNotification notification = new CommentNotification();
notification.setComment(savedComment);
notification.setForUsername(username);
- monos.add(notificationService.create(notification));
+ Mono notificationMono = notificationService.create(notification);
+ notificationMonos.add(notificationMono);
}
}
-
- return Flux.concat(monos).then(Mono.just(savedComment));
+ return Flux.concat(notificationMonos).then(publishEmailMono).thenReturn(savedComment);
} else {
- return Mono.just(savedComment);
+ return publishEmailMono.thenReturn(savedComment);
}
});
}
@Override
- public Mono createThread(CommentThread commentThread) {
+ public Mono createThread(CommentThread commentThread, String originHeader) {
// 1. Check if this user has permission on the application given by `commentThread.applicationId`.
// 2. Save the comment thread and get it's id. This is the `threadId`.
// 3. Pull the comment out of the list of comments, set it's `threadId` and save it separately.
@@ -211,7 +220,7 @@ public class CommentServiceImpl extends BaseService updateThread(String threadId, CommentThread commentThread) {
+ public Mono updateThread(String threadId, CommentThread commentThread, String originHeader) {
return Mono.zip(
sessionUserService.getCurrentUser(),
// Resolving, pinning and marking as read don't need manage permission on the thread.
@@ -301,7 +309,14 @@ public class CommentServiceImpl extends BaseService {
updatedThread.setIsViewed(true);
- return Mono.just(updatedThread);
+ // send email if comment thread is resolved
+ CommentThread.CommentThreadState resolvedState = commentThread.getResolvedState();
+ if(resolvedState != null && resolvedState.getActive()) {
+ return emailEventHandler.publish(user.getUsername(), updatedThread.getApplicationId(),
+ updatedThread, originHeader).thenReturn(updatedThread);
+ } else {
+ return Mono.just(updatedThread);
+ }
});
});
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java
index cce58ac380..8c4d9cb739 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java
@@ -1,6 +1,12 @@
package com.appsmith.server.services;
+import com.appsmith.server.domains.Application;
+import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.Notification;
+import com.appsmith.server.domains.Organization;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
public interface NotificationService extends CrudService