diff --git a/app/client/src/actions/canvasSelectionActions.ts b/app/client/src/actions/canvasSelectionActions.ts
index 8f59ecc967b7..1c14985c0e20 100644
--- a/app/client/src/actions/canvasSelectionActions.ts
+++ b/app/client/src/actions/canvasSelectionActions.ts
@@ -1,5 +1,20 @@
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import { SelectedArenaDimensions } from "pages/common/CanvasSelectionArena";
+import { XYCord } from "utils/hooks/useCanvasDragging";
+
+export const setCanvasSelectionFromEditor = (
+ start: boolean,
+ startPoints?: XYCord,
+) => {
+ return {
+ type: start
+ ? ReduxActionTypes.START_CANVAS_SELECTION_FROM_EDITOR
+ : ReduxActionTypes.STOP_CANVAS_SELECTION_FROM_EDITOR,
+ payload: {
+ ...(start && startPoints ? { startPoints } : {}),
+ },
+ };
+};
export const setCanvasSelectionStateAction = (
start: boolean,
diff --git a/app/client/src/actions/widgetSelectionActions.ts b/app/client/src/actions/widgetSelectionActions.ts
index bca16b91ea11..c48239016545 100644
--- a/app/client/src/actions/widgetSelectionActions.ts
+++ b/app/client/src/actions/widgetSelectionActions.ts
@@ -1,5 +1,4 @@
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
-import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
export const selectWidgetAction = (
widgetId?: string,
@@ -50,14 +49,9 @@ export const deselectMultipleWidgetsAction = (
};
};
-export const selectAllWidgetsInCanvasInitAction = (
- canvasId = MAIN_CONTAINER_WIDGET_ID,
-): ReduxAction<{ canvasId: string }> => {
+export const selectAllWidgetsInCanvasInitAction = () => {
return {
type: ReduxActionTypes.SELECT_ALL_WIDGETS_IN_CANVAS_INIT,
- payload: {
- canvasId,
- },
};
};
diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx
index 3bd2e80824d5..4d9fc88a421b 100644
--- a/app/client/src/components/editorComponents/ResizableComponent.tsx
+++ b/app/client/src/components/editorComponents/ResizableComponent.tsx
@@ -1,5 +1,5 @@
import React, { useContext, useRef, memo, useMemo } from "react";
-import { XYCoord } from "react-dnd";
+import { XYCord } from "utils/hooks/useCanvasDragging";
import {
WidgetOperations,
@@ -132,7 +132,7 @@ export const ResizableComponent = memo(function ResizableComponent(
// Checks if the current resize position has any collisions
// If yes, set isColliding flag to true.
// If no, set isColliding flag to false.
- const isColliding = (newDimensions: UIElementSize, position: XYCoord) => {
+ const isColliding = (newDimensions: UIElementSize, position: XYCord) => {
// Moving the bounding element calculations inside
// to make this expensive operation only whne
const boundingElementClientRect = boundingElement
@@ -231,7 +231,7 @@ export const ResizableComponent = memo(function ResizableComponent(
// 1) There is no collision
// 2) There is a change in widget size
// Update widget, if both of the above are true.
- const updateSize = (newDimensions: UIElementSize, position: XYCoord) => {
+ const updateSize = (newDimensions: UIElementSize, position: XYCord) => {
// Get the difference in size of the widget, before and after resizing.
const delta: UIElementSize = {
height: newDimensions.height - dimensions.height,
diff --git a/app/client/src/components/editorComponents/ResizableUtils.tsx b/app/client/src/components/editorComponents/ResizableUtils.tsx
index 0a0fc4cb1f6f..08f908f6a3cf 100644
--- a/app/client/src/components/editorComponents/ResizableUtils.tsx
+++ b/app/client/src/components/editorComponents/ResizableUtils.tsx
@@ -1,4 +1,4 @@
-import { XYCoord } from "react-dnd";
+import { XYCord } from "utils/hooks/useCanvasDragging";
import { WidgetProps, WidgetRowCols } from "widgets/BaseWidget";
import { GridDefaults } from "constants/WidgetConstants";
@@ -8,7 +8,7 @@ export const RESIZABLE_CONTAINER_BORDER_THEME_INDEX = 1;
export const computeRowCols = (
delta: UIElementSize,
- position: XYCoord,
+ position: XYCord,
props: WidgetProps,
) => {
return {
@@ -51,7 +51,7 @@ export const hasRowColsChanged = (
export const computeFinalRowCols = (
delta: UIElementSize,
- position: XYCoord,
+ position: XYCord,
props: WidgetProps,
): WidgetRowCols | false => {
const newRowCols = computeBoundedRowCols(
diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx
index 3e0b41d03eb5..0c8125df9d5d 100644
--- a/app/client/src/constants/ReduxActionConstants.tsx
+++ b/app/client/src/constants/ReduxActionConstants.tsx
@@ -124,6 +124,8 @@ export const ReduxActionTypes = {
LOAD_WIDGET_PANE: "LOAD_WIDGET_PANE",
ZOOM_IN_CANVAS: "ZOOM_IN_CANVAS",
ZOOM_OUT_CANVAS: "ZOOM_OUT_CANVAS",
+ START_CANVAS_SELECTION_FROM_EDITOR: "START_CANVAS_SELECTION_FROM_EDITOR",
+ STOP_CANVAS_SELECTION_FROM_EDITOR: "STOP_CANVAS_SELECTION_FROM_EDITOR",
START_CANVAS_SELECTION: "START_CANVAS_SELECTION",
STOP_CANVAS_SELECTION: "STOP_CANVAS_SELECTION",
UNDO_CANVAS_ACTION: "UNDO_CANVAS_ACTION",
@@ -539,6 +541,7 @@ export const ReduxActionErrorTypes = {
SAVE_PAGE_ERROR: "SAVE_PAGE_ERROR",
FETCH_WIDGET_CARDS_ERROR: "FETCH_WIDGET_CARDS_ERROR",
WIDGET_OPERATION_ERROR: "WIDGET_OPERATION_ERROR",
+ WIDGET_SELECTION_ERROR: "WIDGET_SELECTION_ERROR",
FETCH_PROPERTY_PANE_CONFIGS_ERROR: "FETCH_PROPERTY_PANE_CONFIGS_ERROR",
FETCH_CONFIGS_ERROR: "FETCH_CONFIGS_ERROR",
PROPERTY_PANE_ERROR: "PROPERTY_PANE_ERROR",
diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx
index b03efcc19645..f109bc6d6428 100644
--- a/app/client/src/constants/WidgetConstants.tsx
+++ b/app/client/src/constants/WidgetConstants.tsx
@@ -120,6 +120,7 @@ export const DroppableWidgets: WidgetType[] = [
WidgetTypes.FORM_WIDGET,
WidgetTypes.LIST_WIDGET,
WidgetTypes.TABS_WIDGET,
+ WidgetTypes.MODAL_WIDGET,
];
// Note: Widget Padding + Container Padding === DEFAULT_GRID_ROW_HEIGHT to gracefully lose one row when a container is used,
diff --git a/app/client/src/pages/Editor/GlobalHotKeys.test.tsx b/app/client/src/pages/Editor/GlobalHotKeys.test.tsx
index b10a9d1c8981..efd6c6af996e 100644
--- a/app/client/src/pages/Editor/GlobalHotKeys.test.tsx
+++ b/app/client/src/pages/Editor/GlobalHotKeys.test.tsx
@@ -20,7 +20,8 @@ import {
} from "test/testCommon";
import { MockCanvas } from "test/testMockedWidgets";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
-describe("Select all hotkey", () => {
+import { generateReactKey } from "utils/generators";
+describe("Canvas Hot Keys", () => {
const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage");
const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl");
@@ -50,71 +51,159 @@ describe("Select all hotkey", () => {
}));
});
- it("Cmd + A - select all widgets on canvas", async () => {
- const children: any = buildChildren([
- { type: "TABS_WIDGET" },
- { type: "SWITCH_WIDGET" },
- ]);
- const dsl: any = widgetCanvasFactory.build({
- children,
+ describe("Select all hotkey", () => {
+ it("Cmd + A - select all widgets on canvas", async () => {
+ const children: any = buildChildren([
+ { type: "TABS_WIDGET", parentId: MAIN_CONTAINER_WIDGET_ID },
+ { type: "SWITCH_WIDGET", parentId: MAIN_CONTAINER_WIDGET_ID },
+ ]);
+ const dsl: any = widgetCanvasFactory.build({
+ children,
+ });
+ spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
+ mockGetIsFetchingPage.mockImplementation(() => false);
+
+ const component = render(
+
+
+
+
+
+
+ ,
+ { initialState: store.getState(), sagasToRun: sagasToRunForTests },
+ );
+ let propPane = component.queryByTestId("t--propertypane");
+ expect(propPane).toBeNull();
+ const canvasWidgets = component.queryAllByTestId("test-widget");
+ expect(canvasWidgets.length).toBe(2);
+ act(() => {
+ if (canvasWidgets[0].firstChild) {
+ fireEvent.mouseOver(canvasWidgets[0].firstChild);
+ fireEvent.click(canvasWidgets[0].firstChild);
+ }
+ });
+ const tabsWidgetName: any = component.container.querySelector(
+ `span.t--widget-name`,
+ );
+ fireEvent.click(tabsWidgetName);
+ propPane = component.queryByTestId("t--propertypane");
+ expect(propPane).not.toBeNull();
+
+ const artBoard: any = component.queryByTestId("t--canvas-artboard");
+ // deselect all other widgets
+ fireEvent.click(artBoard);
+
+ dispatchTestKeyboardEventWithCode(
+ component.container,
+ "keydown",
+ "A",
+ 65,
+ false,
+ true,
+ );
+ let selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(2);
+ dispatchTestKeyboardEventWithCode(
+ component.container,
+ "keydown",
+ "escape",
+ 27,
+ false,
+ false,
+ );
+ selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(0);
+ act(() => {
+ dispatchTestKeyboardEventWithCode(
+ component.container,
+ "keydown",
+ "A",
+ 65,
+ false,
+ true,
+ );
+ });
+
+ selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(2);
+ act(() => {
+ dispatchTestKeyboardEventWithCode(
+ component.container,
+ "keydown",
+ "C",
+ 67,
+ false,
+ true,
+ );
+ });
+ act(() => {
+ dispatchTestKeyboardEventWithCode(
+ component.container,
+ "keydown",
+ "V",
+ 86,
+ false,
+ true,
+ );
+ });
+ selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(2);
});
- spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
- mockGetIsFetchingPage.mockImplementation(() => false);
+ it("Cmd + A - select all widgets inside last selected container", async () => {
+ const containerId = generateReactKey();
+ const canvasId = generateReactKey();
+ const children: any = buildChildren([
+ { type: "CHECKBOX_WIDGET", parentId: canvasId },
+ { type: "SWITCH_WIDGET", parentId: canvasId },
+ { type: "BUTTON_WIDGET", parentId: canvasId },
+ ]);
+ const canvasWidget = buildChildren([
+ {
+ type: "CANVAS_WIDGET",
+ parentId: containerId,
+ children,
+ widgetId: canvasId,
+ },
+ ]);
+ const containerChildren: any = buildChildren([
+ {
+ type: "CONTAINER_WIDGET",
+ children: canvasWidget,
+ widgetId: containerId,
+ parentId: "0",
+ },
+ { type: "CHART_WIDGET", parentId: "0" },
+ ]);
+ const dsl: any = widgetCanvasFactory.build({
+ children: containerChildren,
+ });
+ spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
+ mockGetIsFetchingPage.mockImplementation(() => false);
- const component = render(
-
-
-
-
-
-
- ,
- { initialState: store.getState(), sagasToRun: sagasToRunForTests },
- );
- let propPane = component.queryByTestId("t--propertypane");
- expect(propPane).toBeNull();
- const canvasWidgets = component.queryAllByTestId("test-widget");
- expect(canvasWidgets.length).toBe(2);
- act(() => {
+ const component = render(
+
+
+
+
+
+
+ ,
+ { initialState: store.getState(), sagasToRun: sagasToRunForTests },
+ );
+ const propPane = component.queryByTestId("t--propertypane");
+ expect(propPane).toBeNull();
+ const canvasWidgets = component.queryAllByTestId("test-widget");
+ expect(canvasWidgets.length).toBe(5);
if (canvasWidgets[0].firstChild) {
fireEvent.mouseOver(canvasWidgets[0].firstChild);
fireEvent.click(canvasWidgets[0].firstChild);
}
- });
- const tabsWidgetName: any = component.container.querySelector(
- `span.t--widget-name`,
- );
- fireEvent.click(tabsWidgetName);
- propPane = component.queryByTestId("t--propertypane");
- expect(propPane).not.toBeNull();
-
- const artBoard: any = component.queryByTestId("t--canvas-artboard");
- // deselect all other widgets
- fireEvent.click(artBoard);
- dispatchTestKeyboardEventWithCode(
- component.container,
- "keydown",
- "A",
- 65,
- false,
- true,
- );
- let selectedWidgets = component.queryAllByTestId("t--selected");
- expect(selectedWidgets.length).toBe(2);
- dispatchTestKeyboardEventWithCode(
- component.container,
- "keydown",
- "escape",
- 27,
- false,
- false,
- );
- selectedWidgets = component.queryAllByTestId("t--selected");
- expect(selectedWidgets.length).toBe(0);
- act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
@@ -123,33 +212,138 @@ describe("Select all hotkey", () => {
false,
true,
);
+ const selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(children.length);
});
+ it("Cmd + A - select all widgets inside a form", async () => {
+ const children: any = buildChildren([
+ { type: "FORM_WIDGET", parentId: MAIN_CONTAINER_WIDGET_ID },
+ ]);
+ const dsl: any = widgetCanvasFactory.build({
+ children,
+ });
+ spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
+ mockGetIsFetchingPage.mockImplementation(() => false);
+
+ const component = render(
+
+
+
+
+
+
+ ,
+ { initialState: store.getState(), sagasToRun: sagasToRunForTests },
+ );
+ const propPane = component.queryByTestId("t--propertypane");
+ expect(propPane).toBeNull();
+ const canvasWidgets = component.queryAllByTestId("test-widget");
+ expect(canvasWidgets.length).toBe(4);
+ if (canvasWidgets[0].firstChild) {
+ fireEvent.mouseOver(canvasWidgets[0].firstChild);
+ fireEvent.click(canvasWidgets[0].firstChild);
+ }
- selectedWidgets = component.queryAllByTestId("t--selected");
- expect(selectedWidgets.length).toBe(2);
- act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
- "C",
- 67,
+ "A",
+ 65,
false,
true,
);
+ const selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(3);
});
- act(() => {
+ it("Cmd + A - select all widgets inside a list", async () => {
+ const listId = generateReactKey();
+ const containerId = generateReactKey();
+ const canvasId = generateReactKey();
+ const listCanvasId = generateReactKey();
+ const children: any = buildChildren([
+ { type: "CHECKBOX_WIDGET", parentId: canvasId },
+ { type: "SWITCH_WIDGET", parentId: canvasId },
+ { type: "BUTTON_WIDGET", parentId: canvasId },
+ ]);
+ const canvasWidget = buildChildren([
+ {
+ type: "CANVAS_WIDGET",
+ parentId: containerId,
+ children,
+ widgetId: canvasId,
+ bottomRow: 20,
+ },
+ ]);
+ const containerChildren: any = buildChildren([
+ {
+ type: "CONTAINER_WIDGET",
+ children: canvasWidget,
+ widgetId: containerId,
+ parentId: listCanvasId,
+ dropDisabled: true,
+ bottomRow: 4,
+ },
+ ]);
+ const listCanvasChildren: any = buildChildren([
+ {
+ type: "CANVAS_WIDGET",
+ children: containerChildren,
+ widgetId: listCanvasId,
+ dropDisabled: true,
+ parentId: listId,
+ bottomRow: 20,
+ },
+ ]);
+ const listChildren: any = buildChildren([
+ {
+ type: "LIST_WIDGET",
+ children: listCanvasChildren,
+ widgetId: listId,
+ parentId: "0",
+ },
+ ]);
+ const dsl: any = widgetCanvasFactory.build({
+ children: listChildren,
+ });
+ spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
+ mockGetIsFetchingPage.mockImplementation(() => false);
+
+ const component = render(
+
+
+
+
+
+
+ ,
+ { initialState: store.getState(), sagasToRun: sagasToRunForTests },
+ );
+ const propPane = component.queryByTestId("t--propertypane");
+ expect(propPane).toBeNull();
+ const canvasWidgets = component.queryAllByTestId("test-widget");
+
+ if (canvasWidgets[0].firstChild) {
+ fireEvent.mouseOver(canvasWidgets[0].firstChild);
+ fireEvent.click(canvasWidgets[0].firstChild);
+ }
+
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
- "V",
- 86,
+ "A",
+ 65,
false,
true,
);
+ const selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(3);
});
- selectedWidgets = component.queryAllByTestId("t--selected");
- expect(selectedWidgets.length).toBe(2);
});
+
afterAll(() => jest.resetModules());
});
@@ -162,6 +356,7 @@ describe("Cut/Copy/Paste hotkey", () => {
bottomRow: 30,
leftColumn: 5,
rightColumn: 30,
+ parentId: MAIN_CONTAINER_WIDGET_ID,
},
{
type: "SWITCH_WIDGET",
@@ -169,6 +364,7 @@ describe("Cut/Copy/Paste hotkey", () => {
bottomRow: 10,
leftColumn: 40,
rightColumn: 48,
+ parentId: MAIN_CONTAINER_WIDGET_ID,
},
]);
const dsl: any = widgetCanvasFactory.build({
diff --git a/app/client/src/pages/Editor/MainContainer.test.tsx b/app/client/src/pages/Editor/MainContainer.test.tsx
index 256547890034..21ad67a8d522 100644
--- a/app/client/src/pages/Editor/MainContainer.test.tsx
+++ b/app/client/src/pages/Editor/MainContainer.test.tsx
@@ -5,7 +5,6 @@ import {
} from "test/factories/WidgetFactoryUtils";
import { act, render, fireEvent } from "test/testUtils";
import GlobalHotKeys from "./GlobalHotKeys";
-import MainContainer from "./MainContainer";
import { MemoryRouter } from "react-router-dom";
import * as utilities from "selectors/editorSelectors";
import store from "store";
@@ -15,18 +14,14 @@ import {
MockApplication,
mockGetCanvasWidgetDsl,
syntheticTestMouseEvent,
- useMockDsl,
} from "test/testCommon";
import lodash from "lodash";
import { getAbsolutePixels } from "utils/helpers";
+import { UpdatedMainContainer } from "test/testMockedWidgets";
describe("Drag and Drop widgets into Main container", () => {
const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage");
const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl");
- function UpdatedMainContainer({ dsl }: any) {
- useMockDsl(dsl);
- return ;
- }
// These need to be at the top to avoid imports not being mocked. ideally should be in setup.ts but will override for all other tests
beforeAll(() => {
const mockGenerator = function*() {
diff --git a/app/client/src/pages/Editor/WidgetsEditor.tsx b/app/client/src/pages/Editor/WidgetsEditor.tsx
index cfb0cd6d34f9..7a896f6337f7 100644
--- a/app/client/src/pages/Editor/WidgetsEditor.tsx
+++ b/app/client/src/pages/Editor/WidgetsEditor.tsx
@@ -26,6 +26,7 @@ import { useDynamicAppLayout } from "utils/hooks/useDynamicAppLayout";
import Debugger from "components/editorComponents/Debugger";
import { closePropertyPane, closeTableFilterPane } from "actions/widgetActions";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
+import { setCanvasSelectionFromEditor } from "actions/canvasSelectionActions";
import CrudInfoModal from "./GeneratePage/components/CrudInfoModal";
const EditorWrapper = styled.div`
@@ -103,6 +104,7 @@ function WidgetsEditor() {
deselectAll && deselectAll();
dispatch(closePropertyPane());
dispatch(closeTableFilterPane());
+ dispatch(setCanvasSelectionFromEditor(false));
}, [focusWidget, deselectAll]);
const pageLoading = (
@@ -118,12 +120,26 @@ function WidgetsEditor() {
if (!isFetchingPage && widgets) {
node = ;
}
+ const onDragStart = (e: any) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const startPoints = {
+ x: e.clientX,
+ y: e.clientY,
+ };
+ dispatch(setCanvasSelectionFromEditor(true, startPoints));
+ };
log.debug("Canvas rendered");
PerformanceTracker.stopTracking();
return (
-
+
{node}
diff --git a/app/client/src/pages/common/CanvasSelectionArena.test.tsx b/app/client/src/pages/common/CanvasSelectionArena.test.tsx
index 9bc3a61934a5..accae7526ce4 100644
--- a/app/client/src/pages/common/CanvasSelectionArena.test.tsx
+++ b/app/client/src/pages/common/CanvasSelectionArena.test.tsx
@@ -1,12 +1,24 @@
-import { fireEvent, render } from "test/testUtils";
+import { act, fireEvent, render } from "test/testUtils";
import Canvas from "pages/Editor/Canvas";
import React from "react";
import {
buildChildren,
widgetCanvasFactory,
} from "test/factories/WidgetFactoryUtils";
-import { MockPageDSL, syntheticTestMouseEvent } from "test/testCommon";
+import {
+ MockApplication,
+ mockGetCanvasWidgetDsl,
+ MockPageDSL,
+ syntheticTestMouseEvent,
+} from "test/testCommon";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
+import { generateReactKey } from "utils/generators";
+import store from "store";
+import { sagasToRunForTests } from "test/sagas";
+import GlobalHotKeys from "pages/Editor/GlobalHotKeys";
+import { UpdatedMainContainer } from "test/testMockedWidgets";
+import { MemoryRouter } from "react-router-dom";
+import * as utilities from "selectors/editorSelectors";
describe("Canvas selection test cases", () => {
it("Should select using canvas draw", () => {
@@ -124,4 +136,245 @@ describe("Canvas selection test cases", () => {
const selectedWidgets = component.queryAllByTestId("t--selected");
expect(selectedWidgets.length).toBe(2);
});
+
+ it("Should allow draw to select using cmd + draw in Container component", () => {
+ const containerId = generateReactKey();
+ const canvasId = generateReactKey();
+ const children: any = buildChildren([
+ { type: "CHECKBOX_WIDGET", parentId: canvasId },
+ { type: "SWITCH_WIDGET", parentId: canvasId },
+ { type: "BUTTON_WIDGET", parentId: canvasId },
+ ]);
+ const canvasWidget = buildChildren([
+ {
+ type: "CANVAS_WIDGET",
+ parentId: containerId,
+ children,
+ widgetId: canvasId,
+ },
+ ]);
+ const containerChildren: any = buildChildren([
+ {
+ type: "CONTAINER_WIDGET",
+ children: canvasWidget,
+ widgetId: containerId,
+ parentId: "0",
+ },
+ { type: "CHART_WIDGET", parentId: "0" },
+ ]);
+ const dsl: any = widgetCanvasFactory.build({
+ children: containerChildren,
+ });
+
+ const component = render(
+
+
+ ,
+ );
+ let selectionCanvas: any = component.queryByTestId(`canvas-${canvasId}`);
+ expect(selectionCanvas.style.zIndex).toBe("");
+ fireEvent.mouseDown(selectionCanvas);
+ // should not allow draw when cmd/ctrl is not pressed
+ selectionCanvas = component.queryByTestId(`canvas-${canvasId}`);
+ expect(selectionCanvas.style.zIndex).toBe("");
+ fireEvent.mouseDown(selectionCanvas, {
+ metaKey: true,
+ });
+
+ selectionCanvas = component.queryByTestId(`canvas-${canvasId}`);
+
+ expect(selectionCanvas.style.zIndex).toBe("2");
+ fireEvent.mouseUp(selectionCanvas);
+ selectionCanvas = component.queryByTestId(`canvas-${canvasId}`);
+
+ expect(selectionCanvas.style.zIndex).toBe("");
+ });
+
+ it("Should select all elements inside a CONTAINER using draw on canvas from top to bottom", () => {
+ const containerId = generateReactKey();
+ const canvasId = generateReactKey();
+ const children: any = buildChildren([
+ {
+ type: "CHECKBOX_WIDGET",
+ parentColumnSpace: 10,
+ parentRowSpace: 10,
+ parentId: canvasId,
+ },
+ {
+ type: "BUTTON_WIDGET",
+ parentColumnSpace: 10,
+ parentRowSpace: 10,
+ parentId: canvasId,
+ },
+ ]);
+ const canvasWidget = buildChildren([
+ {
+ type: "CANVAS_WIDGET",
+ parentId: containerId,
+ bottomRow: 20,
+ rightColumn: 20,
+ children,
+ widgetId: canvasId,
+ },
+ ]);
+ const containerChildren: any = buildChildren([
+ {
+ type: "CONTAINER_WIDGET",
+ children: canvasWidget,
+ widgetId: containerId,
+ parentColumnSpace: 10,
+ parentRowSpace: 10,
+ bottomRow: 20,
+ rightColumn: 20,
+ parentId: "0",
+ },
+ { type: "CHART_WIDGET", parentId: "0" },
+ ]);
+ const dsl: any = widgetCanvasFactory.build({
+ children: containerChildren,
+ });
+ const component = render(
+
+
+ ,
+ );
+ const selectionCanvas: any = component.queryByTestId(`canvas-${canvasId}`);
+ fireEvent(
+ selectionCanvas,
+ syntheticTestMouseEvent(
+ new MouseEvent("mousedown", {
+ metaKey: true,
+ bubbles: true,
+ cancelable: true,
+ }),
+ {
+ offsetX: 10,
+ offsetY: 10,
+ },
+ ),
+ );
+ fireEvent(
+ selectionCanvas,
+ syntheticTestMouseEvent(
+ new MouseEvent("mousemove", {
+ metaKey: true,
+ bubbles: true,
+ cancelable: true,
+ }),
+ {
+ offsetX: 800,
+ offsetY: 800,
+ },
+ ),
+ );
+
+ fireEvent(
+ selectionCanvas,
+ syntheticTestMouseEvent(
+ new MouseEvent("mouseup", {
+ metaKey: true,
+ bubbles: true,
+ cancelable: true,
+ }),
+ {
+ offsetX: 800,
+ offsetY: 800,
+ },
+ ),
+ );
+ const selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(children.length);
+ });
+
+ it("Draw to select from outside of canvas(editor) ", () => {
+ const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage");
+ const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl");
+ const children: any = buildChildren([
+ {
+ type: "TABS_WIDGET",
+ topRow: 1,
+ bottomRow: 3,
+ leftColumn: 1,
+ rightColumn: 3,
+ },
+ {
+ type: "SWITCH_WIDGET",
+ topRow: 1,
+ bottomRow: 2,
+ leftColumn: 5,
+ rightColumn: 13,
+ },
+ ]);
+ const dsl: any = widgetCanvasFactory.build({
+ children,
+ });
+ spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
+ mockGetIsFetchingPage.mockImplementation(() => false);
+
+ const component = render(
+
+
+
+
+
+
+ ,
+ { initialState: store.getState(), sagasToRun: sagasToRunForTests },
+ );
+ const widgetEditor: any = component.queryByTestId("widgets-editor");
+ let selectionCanvas: any = component.queryByTestId(
+ `canvas-${MAIN_CONTAINER_WIDGET_ID}`,
+ );
+ expect(selectionCanvas.style.zIndex).toBe("");
+ act(() => {
+ fireEvent.dragStart(widgetEditor);
+ });
+
+ selectionCanvas = component.queryByTestId(
+ `canvas-${MAIN_CONTAINER_WIDGET_ID}`,
+ );
+
+ expect(selectionCanvas.style.zIndex).toBe("2");
+ fireEvent.mouseEnter(selectionCanvas);
+ fireEvent.mouseUp(selectionCanvas);
+ selectionCanvas = component.queryByTestId(
+ `canvas-${MAIN_CONTAINER_WIDGET_ID}`,
+ );
+
+ expect(selectionCanvas.style.zIndex).toBe("");
+ act(() => {
+ fireEvent.dragStart(widgetEditor);
+ });
+ fireEvent(
+ selectionCanvas,
+ syntheticTestMouseEvent(
+ new MouseEvent("mousemove", {
+ bubbles: true,
+ cancelable: true,
+ }),
+ {
+ offsetX: dsl.rightColumn * 4,
+ offsetY: dsl.bottomRow * 4,
+ },
+ ),
+ );
+
+ fireEvent(
+ selectionCanvas,
+ syntheticTestMouseEvent(
+ new MouseEvent("mouseup", {
+ bubbles: true,
+ cancelable: true,
+ }),
+ {
+ offsetX: dsl.rightColumn * 4,
+ offsetY: dsl.bottomRow * 4,
+ },
+ ),
+ );
+ const selectedWidgets = component.queryAllByTestId("t--selected");
+ expect(selectedWidgets.length).toBe(2);
+ });
});
diff --git a/app/client/src/pages/common/CanvasSelectionArena.tsx b/app/client/src/pages/common/CanvasSelectionArena.tsx
index 7fa600026377..ba5779ade1d6 100644
--- a/app/client/src/pages/common/CanvasSelectionArena.tsx
+++ b/app/client/src/pages/common/CanvasSelectionArena.tsx
@@ -3,7 +3,7 @@ import {
setCanvasSelectionStateAction,
} from "actions/canvasSelectionActions";
import { throttle } from "lodash";
-import React, { useEffect, useCallback } from "react";
+import React, { useEffect, useCallback, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "reducers";
import { APP_MODE } from "entities/App";
@@ -17,6 +17,8 @@ import styled from "styled-components";
import { getNearestParentCanvas } from "utils/generators";
import { useCanvasDragToScroll } from "utils/hooks/useCanvasDragToScroll";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
+import { XYCord } from "utils/hooks/useCanvasDragging";
+import { theme } from "constants/DefaultTheme";
const StyledSelectionCanvas = styled.canvas`
position: absolute;
@@ -66,6 +68,9 @@ export function CanvasSelectionArena({
const isDragging = useSelector(
(state: AppState) => state.ui.widgetDragResize.isDragging,
);
+ const isResizing = useSelector(
+ (state: AppState) => state.ui.widgetDragResize.isResizing,
+ );
const mainContainer = useSelector((state: AppState) =>
getWidget(state, widgetId),
);
@@ -98,7 +103,7 @@ export function CanvasSelectionArena({
trailing: true,
},
),
- [widgetId],
+ [widgetId, snapColumnSpace, snapRowSpace],
);
const isDraggingForSelection = useSelector((state: AppState) => {
return state.ui.canvasSelection.isDraggingForSelection;
@@ -106,6 +111,38 @@ export function CanvasSelectionArena({
const isCurrentWidgetDrawing = useSelector((state: AppState) => {
return state.ui.canvasSelection.widgetId === widgetId;
});
+ const outOfCanvasStartPositions = useSelector((state: AppState) => {
+ return state.ui.canvasSelection.outOfCanvasStartPositions;
+ });
+ const defaultDrawOnObj = {
+ canDraw: false,
+ startPoints: undefined,
+ };
+ const drawOnEnterObj = useRef<{
+ canDraw: boolean;
+ startPoints?: XYCord;
+ }>(defaultDrawOnObj);
+
+ // start main container selection from widget editor
+ useEffect(() => {
+ const canDrawOnEnter =
+ isDraggingForSelection &&
+ isCurrentWidgetDrawing &&
+ !!outOfCanvasStartPositions;
+
+ drawOnEnterObj.current = {
+ canDraw: canDrawOnEnter,
+ startPoints: canDrawOnEnter ? outOfCanvasStartPositions : undefined,
+ };
+ if (canvasRef.current && canDrawOnEnter) {
+ canvasRef.current.style.zIndex = "2";
+ }
+ }, [
+ isDraggingForSelection,
+ isCurrentWidgetDrawing,
+ outOfCanvasStartPositions,
+ ]);
+
useCanvasDragToScroll(
canvasRef,
isCurrentWidgetDrawing,
@@ -119,12 +156,12 @@ export function CanvasSelectionArena({
// as of today (Pixels rendered by canvas) ∝ (Application height) so as height increases will run into to dead renders.
// https://on690.codesandbox.io/ to check the number of pixels limit supported for a canvas
// const { devicePixelRatio: scale = 1 } = window;
+
const scale = 1;
const scrollParent: Element | null = getNearestParentCanvas(
canvasRef.current,
);
const scrollObj: any = {};
-
let canvasCtx: any = canvasRef.current.getContext("2d");
const initRectangle = (): SelectedArenaDimensions => ({
top: 0,
@@ -136,26 +173,6 @@ export function CanvasSelectionArena({
let isMultiSelect = false;
let isDragging = false;
- const init = () => {
- if (canvasRef.current) {
- const { height, width } = canvasRef.current.getBoundingClientRect();
- if (height && width) {
- canvasRef.current.width = width * scale;
- canvasRef.current.height =
- (snapRows * snapRowSpace + (widgetId === "0" ? 200 : 0)) * scale;
- }
- canvasCtx = canvasRef.current.getContext("2d");
- canvasCtx.scale(scale, scale);
- canvasRef.current.addEventListener("click", onClick, false);
- canvasRef.current.addEventListener("mousedown", onMouseDown, false);
- canvasRef.current.addEventListener("mouseup", onMouseUp, false);
- canvasRef.current.addEventListener("mousemove", onMouseMove, false);
- canvasRef.current.addEventListener("mouseleave", onMouseLeave, false);
- canvasRef.current.addEventListener("mouseenter", onMouseEnter, false);
- scrollParent?.addEventListener("scroll", onScroll, false);
- }
- };
-
const getSelectionDimensions = () => {
return {
top:
@@ -212,13 +229,24 @@ export function CanvasSelectionArena({
};
const onMouseLeave = () => {
- document.body.addEventListener("mouseup", onMouseUp, false);
- document.body.addEventListener("click", onClick, false);
+ if (widgetId === MAIN_CONTAINER_WIDGET_ID) {
+ document.body.addEventListener("mouseup", onMouseUp, false);
+ document.body.addEventListener("click", onClick, false);
+ }
};
- const onMouseEnter = () => {
- document.body.removeEventListener("mouseup", onMouseUp);
- document.body.removeEventListener("click", onClick);
+ const onMouseEnter = (e: any) => {
+ if (
+ canvasRef.current &&
+ !isDragging &&
+ drawOnEnterObj?.current.canDraw
+ ) {
+ firstRender(e, true);
+ drawOnEnterObj.current = defaultDrawOnObj;
+ } else if (widgetId === MAIN_CONTAINER_WIDGET_ID) {
+ document.body.removeEventListener("mouseup", onMouseUp);
+ document.body.removeEventListener("click", onClick);
+ }
};
const onClick = (e: any) => {
@@ -235,22 +263,73 @@ export function CanvasSelectionArena({
}
};
- const onMouseDown = (e: any) => {
- if (
- canvasRef.current &&
- (!isDraggableParent || e.ctrlKey || e.metaKey)
- ) {
+ const startPositionsForOutCanvasSelection = () => {
+ const startPoints = drawOnEnterObj.current.startPoints;
+ const startPositions = {
+ top: 0,
+ left: 0,
+ };
+ if (canvasRef.current && startPoints) {
+ const {
+ height,
+ left,
+ top,
+ width,
+ } = canvasRef.current.getBoundingClientRect();
+ const outOfMaxBounds = {
+ x: startPoints.x < left + width,
+ y: startPoints.y < top + height,
+ };
+ const outOfMinBounds = {
+ x: startPoints.x > left,
+ y: startPoints.y > top,
+ };
+ const xInRange = outOfMaxBounds.x && outOfMinBounds.x;
+ const yInRange = outOfMaxBounds.y && outOfMinBounds.y;
+ const bufferFromBoundary = 2;
+ startPositions.left = xInRange
+ ? startPoints.x - left
+ : outOfMinBounds.x
+ ? width - bufferFromBoundary
+ : bufferFromBoundary;
+ startPositions.top = yInRange
+ ? startPoints.y - top
+ : outOfMinBounds.y
+ ? height - bufferFromBoundary
+ : bufferFromBoundary;
+ }
+ return startPositions;
+ };
+
+ const firstRender = (e: any, fromOuterCanvas = false) => {
+ if (canvasRef.current && !isDragging) {
isMultiSelect = e.ctrlKey || e.metaKey || e.shiftKey;
- selectionRectangle.left = e.offsetX - canvasRef.current.offsetLeft;
- selectionRectangle.top = e.offsetY - canvasRef.current.offsetTop;
+ if (fromOuterCanvas) {
+ const { left, top } = startPositionsForOutCanvasSelection();
+ selectionRectangle.left = left;
+ selectionRectangle.top = top;
+ } else {
+ selectionRectangle.left = e.offsetX - canvasRef.current.offsetLeft;
+ selectionRectangle.top = e.offsetY - canvasRef.current.offsetTop;
+ }
selectionRectangle.width = 0;
selectionRectangle.height = 0;
+
isDragging = true;
- dispatch(setCanvasSelectionStateAction(true, widgetId));
// bring the canvas to the top layer
canvasRef.current.style.zIndex = "2";
}
};
+
+ const onMouseDown = (e: any) => {
+ if (
+ canvasRef.current &&
+ (!isDraggableParent || e.ctrlKey || e.metaKey)
+ ) {
+ dispatch(setCanvasSelectionStateAction(true, widgetId));
+ firstRender(e);
+ }
+ };
const onMouseUp = () => {
if (isDragging && canvasRef.current) {
isDragging = false;
@@ -306,10 +385,17 @@ export function CanvasSelectionArena({
});
}
};
- if (appMode === APP_MODE.EDIT) {
- init();
- }
- return () => {
+
+ const addEventListeners = () => {
+ canvasRef.current?.addEventListener("click", onClick, false);
+ canvasRef.current?.addEventListener("mousedown", onMouseDown, false);
+ canvasRef.current?.addEventListener("mouseup", onMouseUp, false);
+ canvasRef.current?.addEventListener("mousemove", onMouseMove, false);
+ canvasRef.current?.addEventListener("mouseleave", onMouseLeave, false);
+ canvasRef.current?.addEventListener("mouseenter", onMouseEnter, false);
+ scrollParent?.addEventListener("scroll", onScroll, false);
+ };
+ const removeEventListeners = () => {
canvasRef.current?.removeEventListener("mousedown", onMouseDown);
canvasRef.current?.removeEventListener("mouseup", onMouseUp);
canvasRef.current?.removeEventListener("mousemove", onMouseMove);
@@ -317,10 +403,42 @@ export function CanvasSelectionArena({
canvasRef.current?.removeEventListener("mouseenter", onMouseEnter);
canvasRef.current?.removeEventListener("click", onClick);
};
+ const init = () => {
+ if (canvasRef.current) {
+ const { height, width } = canvasRef.current.getBoundingClientRect();
+ if (height && width) {
+ canvasRef.current.width = width * scale;
+ canvasRef.current.height =
+ (snapRows * snapRowSpace +
+ (widgetId === MAIN_CONTAINER_WIDGET_ID
+ ? theme.canvasBottomPadding
+ : 0)) *
+ scale;
+ }
+ canvasCtx = canvasRef.current.getContext("2d");
+ canvasCtx.scale(scale, scale);
+ removeEventListeners();
+ addEventListeners();
+ }
+ };
+ if (appMode === APP_MODE.EDIT) {
+ init();
+ }
+ return () => {
+ removeEventListeners();
+ };
}
- }, [appLayout, currentPageId, mainContainer, isDragging, snapRows]);
+ }, [
+ appLayout,
+ currentPageId,
+ mainContainer,
+ isDragging,
+ snapRows,
+ snapColumnSpace,
+ snapRowSpace,
+ ]);
- return appMode === APP_MODE.EDIT && !isDragging ? (
+ return appMode === APP_MODE.EDIT && !(isDragging || isResizing) ? (
{
state.isDraggingForSelection = false;
+ state.widgetId = "";
+ state.outOfCanvasStartPositions = undefined;
+ },
+ [ReduxActionTypes.START_CANVAS_SELECTION_FROM_EDITOR]: (
+ state: CanvasSelectionState,
+ action: ReduxAction<{ startPoints?: XYCord }>,
+ ) => {
+ state.isDraggingForSelection = true;
+ state.widgetId = MAIN_CONTAINER_WIDGET_ID;
+ state.outOfCanvasStartPositions = action.payload.startPoints;
+ },
+ [ReduxActionTypes.STOP_CANVAS_SELECTION_FROM_EDITOR]: (
+ state: CanvasSelectionState,
+ ) => {
+ state.isDraggingForSelection = false;
+ state.widgetId = "";
+ state.outOfCanvasStartPositions = undefined;
},
});
export type CanvasSelectionState = {
isDraggingForSelection: boolean;
widgetId?: string;
+ outOfCanvasStartPositions?: XYCord;
};
export default canvasSelectionReducer;
diff --git a/app/client/src/sagas/SelectionCanvasSagas.ts b/app/client/src/sagas/SelectionCanvasSagas.ts
index 0c85ce236eeb..9ce550b138e0 100644
--- a/app/client/src/sagas/SelectionCanvasSagas.ts
+++ b/app/client/src/sagas/SelectionCanvasSagas.ts
@@ -1,6 +1,7 @@
import { selectMultipleWidgetsAction } from "actions/widgetSelectionActions";
import { OccupiedSpace } from "constants/editorConstants";
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
+import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { isEqual } from "lodash";
import { SelectedArenaDimensions } from "pages/common/CanvasSelectionArena";
import { all, cancel, put, select, take, takeLatest } from "redux-saga/effects";
@@ -102,10 +103,8 @@ function* startCanvasSelectionSaga(
actionPayload: ReduxAction<{ widgetId: string }>,
) {
const lastSelectedWidgets: string[] = yield select(getSelectedWidgets);
- const mainContainer: WidgetProps = yield select(
- getWidget,
- actionPayload.payload.widgetId,
- );
+ const widgetId = actionPayload.payload.widgetId || MAIN_CONTAINER_WIDGET_ID;
+ const mainContainer: WidgetProps = yield select(getWidget, widgetId);
const lastSelectedWidgetsWithoutParent = lastSelectedWidgets.filter(
(each) => each !== mainContainer.parentId,
);
@@ -133,5 +132,9 @@ export default function* selectionCanvasSagas() {
ReduxActionTypes.START_CANVAS_SELECTION,
startCanvasSelectionSaga,
),
+ takeLatest(
+ ReduxActionTypes.START_CANVAS_SELECTION_FROM_EDITOR,
+ startCanvasSelectionSaga,
+ ),
]);
}
diff --git a/app/client/src/sagas/WidgetSelectionSagas.ts b/app/client/src/sagas/WidgetSelectionSagas.ts
index 4f910b4729a1..89a56b570b28 100644
--- a/app/client/src/sagas/WidgetSelectionSagas.ts
+++ b/app/client/src/sagas/WidgetSelectionSagas.ts
@@ -1,7 +1,19 @@
-import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
-import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
-import { all, fork, put, select, takeLatest } from "redux-saga/effects";
-import { getWidgetImmediateChildren, getWidgets } from "./selectors";
+import {
+ ReduxAction,
+ ReduxActionErrorTypes,
+ ReduxActionTypes,
+} from "constants/ReduxActionConstants";
+import {
+ DroppableWidgets,
+ WidgetTypes,
+ MAIN_CONTAINER_WIDGET_ID,
+} from "constants/WidgetConstants";
+import { all, call, fork, put, select, takeLatest } from "redux-saga/effects";
+import {
+ getWidgetImmediateChildren,
+ getWidgetMetaProps,
+ getWidgets,
+} from "./selectors";
import log from "loglevel";
import {
deselectMultipleWidgetsAction,
@@ -14,7 +26,11 @@ import { Toaster } from "components/ads/Toast";
import { createMessage, SELECT_ALL_WIDGETS_MSG } from "constants/messages";
import { Variant } from "components/ads/common";
import { getSelectedWidget, getSelectedWidgets } from "selectors/ui";
-import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
+import {
+ CanvasWidgetsReduxState,
+ FlattenedWidgetProps,
+} from "reducers/entityReducers/canvasWidgetsReducer";
+import { getWidgetChildren } from "./WidgetOperationUtils";
import { AppState } from "reducers";
// The following is computed to be used in the entity explorer
@@ -24,7 +40,7 @@ function* selectedWidgetAncestrySaga(
action: ReduxAction<{ widgetId: string; isMultiSelect: boolean }>,
) {
try {
- const canvasWidgets = yield select(getWidgets);
+ const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgetIdsExpandList = [];
const { isMultiSelect, widgetId: selectedWidget } = action.payload;
@@ -35,7 +51,7 @@ function* selectedWidgetAncestrySaga(
// If there is a parentId for the selectedWidget
if (widgetId) {
// Keep including the parent until we reach the main container
- while (widgetId !== MAIN_CONTAINER_WIDGET_ID) {
+ while (widgetId && widgetId !== MAIN_CONTAINER_WIDGET_ID) {
widgetIdsExpandList.push(widgetId);
if (canvasWidgets[widgetId] && canvasWidgets[widgetId].parentId)
widgetId = canvasWidgets[widgetId].parentId;
@@ -61,20 +77,122 @@ function* selectedWidgetAncestrySaga(
}
}
-function* selectAllWidgetsInCanvasSaga(
- action: ReduxAction<{ canvasId: string }>,
-) {
- const { canvasId } = action.payload;
- const allWidgetsOnCanvas: string[] = yield select(
- getWidgetImmediateChildren,
- canvasId,
- );
- if (allWidgetsOnCanvas && allWidgetsOnCanvas.length) {
- yield put(selectMultipleWidgetsAction(allWidgetsOnCanvas));
- Toaster.show({
- text: createMessage(SELECT_ALL_WIDGETS_MSG),
- variant: Variant.info,
- duration: 3000,
+function* getDroppingCanvasOfWidget(widgetLastSelected: FlattenedWidgetProps) {
+ if (DroppableWidgets.includes(widgetLastSelected.type)) {
+ const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
+ const childWidgets: string[] = yield select(
+ getWidgetImmediateChildren,
+ widgetLastSelected.widgetId,
+ );
+ const firstCanvas = childWidgets.find((each) => {
+ const widget = canvasWidgets[each];
+ return widget.type === WidgetTypes.CANVAS_WIDGET;
+ });
+ if (widgetLastSelected.type === WidgetTypes.TABS_WIDGET) {
+ const tabMetaProps: Record = yield select(
+ getWidgetMetaProps,
+ widgetLastSelected.widgetId,
+ );
+ return tabMetaProps.selectedTabWidgetId;
+ }
+ if (firstCanvas) {
+ return firstCanvas;
+ }
+ }
+ return widgetLastSelected.parentId;
+}
+
+function* getLastSelectedCanvas() {
+ const lastSelectedWidget: string = yield select(getSelectedWidget);
+ const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
+ const widgetLastSelected =
+ lastSelectedWidget && canvasWidgets[lastSelectedWidget];
+ if (widgetLastSelected) {
+ const canvasToSelect: string = yield call(
+ getDroppingCanvasOfWidget,
+ widgetLastSelected,
+ );
+ return canvasToSelect ? canvasToSelect : MAIN_CONTAINER_WIDGET_ID;
+ }
+ return MAIN_CONTAINER_WIDGET_ID;
+}
+
+// used for List widget cases
+const isChildOfDropDisabledCanvas = (
+ canvasWidgets: CanvasWidgetsReduxState,
+ widgetId: string,
+) => {
+ const widget = canvasWidgets[widgetId];
+ const parentId = widget.parentId || MAIN_CONTAINER_WIDGET_ID;
+ const parent = canvasWidgets[parentId];
+ return !!parent?.dropDisabled;
+};
+
+function* getAllSelectableChildren() {
+ const lastSelectedWidget: string = yield select(getSelectedWidget);
+ const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
+ const widgetLastSelected = canvasWidgets[lastSelectedWidget];
+ const canvasId: string = yield call(getLastSelectedCanvas);
+ let allChildren: string[] = [];
+ const selectGrandChildren: boolean = lastSelectedWidget
+ ? widgetLastSelected.type === WidgetTypes.LIST_WIDGET
+ : false;
+ if (selectGrandChildren) {
+ allChildren = yield call(
+ getWidgetChildren,
+ canvasWidgets,
+ lastSelectedWidget,
+ );
+ } else {
+ allChildren = yield select(getWidgetImmediateChildren, canvasId);
+ }
+ if (allChildren && allChildren.length) {
+ const selectableChildren = allChildren.filter((each) => {
+ const isCanvasWidget =
+ each &&
+ canvasWidgets[each] &&
+ canvasWidgets[each].type === WidgetTypes.CANVAS_WIDGET;
+ const isImmovableWidget = isChildOfDropDisabledCanvas(
+ canvasWidgets,
+ each,
+ );
+ return !(isCanvasWidget || isImmovableWidget);
+ });
+ return selectableChildren;
+ }
+ return [];
+}
+
+function* selectAllWidgetsInCanvasSaga() {
+ try {
+ const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
+ const allSelectableChildren: string[] = yield call(
+ getAllSelectableChildren,
+ );
+ if (allSelectableChildren && allSelectableChildren.length) {
+ yield put(selectMultipleWidgetsAction(allSelectableChildren));
+ const isAnyModalSelected = allSelectableChildren.some((each) => {
+ return (
+ each &&
+ canvasWidgets[each] &&
+ canvasWidgets[each].type === WidgetTypes.MODAL_WIDGET
+ );
+ });
+ if (isAnyModalSelected) {
+ Toaster.show({
+ text: createMessage(SELECT_ALL_WIDGETS_MSG),
+ variant: Variant.info,
+ duration: 3000,
+ });
+ }
+ }
+ } catch (error) {
+ yield put({
+ type: ReduxActionErrorTypes.WIDGET_SELECTION_ERROR,
+ payload: {
+ action: ReduxActionTypes.SELECT_ALL_WIDGETS_IN_CANVAS_INIT,
+ error,
+ },
});
}
}
@@ -82,79 +200,119 @@ function* selectAllWidgetsInCanvasSaga(
function* deselectNonSiblingsOfWidgetSaga(
action: ReduxAction<{ widgetId: string; isMultiSelect: boolean }>,
) {
- const { isMultiSelect, widgetId } = action.payload;
- if (isMultiSelect) {
- const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
- const parentId: any = allWidgets[widgetId].parentId;
- const childWidgets: string[] = yield select(
- getWidgetImmediateChildren,
- parentId,
- );
- const currentSelectedWidgets: string[] = yield select(getSelectedWidgets);
+ try {
+ const { isMultiSelect, widgetId } = action.payload;
+ if (isMultiSelect) {
+ const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
+ const parentId: any = allWidgets[widgetId].parentId;
+ const childWidgets: string[] = yield select(
+ getWidgetImmediateChildren,
+ parentId,
+ );
+ const currentSelectedWidgets: string[] = yield select(getSelectedWidgets);
- const nonSiblings = currentSelectedWidgets.filter(
- (each) => !childWidgets.includes(each),
- );
- if (nonSiblings && nonSiblings.length) {
- yield put(
- deselectMultipleWidgetsAction(
- nonSiblings.filter((each) => each !== widgetId),
- ),
+ const nonSiblings = currentSelectedWidgets.filter(
+ (each) => !childWidgets.includes(each),
);
+ if (nonSiblings && nonSiblings.length) {
+ yield put(
+ deselectMultipleWidgetsAction(
+ nonSiblings.filter((each) => each !== widgetId),
+ ),
+ );
+ }
}
+ } catch (error) {
+ yield put({
+ type: ReduxActionErrorTypes.WIDGET_SELECTION_ERROR,
+ payload: {
+ action: ReduxActionTypes.SELECT_WIDGET_INIT,
+ error,
+ },
+ });
}
}
function* selectWidgetSaga(
action: ReduxAction<{ widgetId: string; isMultiSelect: boolean }>,
) {
- const { isMultiSelect, widgetId } = action.payload;
- yield put(selectWidgetAction(widgetId, isMultiSelect));
+ try {
+ const { isMultiSelect, widgetId } = action.payload;
+ yield put(selectWidgetAction(widgetId, isMultiSelect));
+ } catch (error) {
+ yield put({
+ type: ReduxActionErrorTypes.WIDGET_SELECTION_ERROR,
+ payload: {
+ action: ReduxActionTypes.SELECT_WIDGET_INIT,
+ error,
+ },
+ });
+ }
}
function* shiftSelectWidgetsSaga(
action: ReduxAction<{ widgetId: string; siblingWidgets: string[] }>,
) {
- const { siblingWidgets, widgetId } = action.payload;
- const selectedWidgets: string[] = yield select(getSelectedWidgets);
- const lastSelectedWidget: string = yield select(getSelectedWidget);
- const lastSelectedWidgetIndex = siblingWidgets.indexOf(lastSelectedWidget);
- const isWidgetSelected = selectedWidgets.includes(widgetId);
- if (!isWidgetSelected && lastSelectedWidgetIndex > -1) {
- const selectedWidgetIndex = siblingWidgets.indexOf(widgetId);
- const start =
- lastSelectedWidgetIndex < selectedWidgetIndex
- ? lastSelectedWidgetIndex
- : selectedWidgetIndex;
- const end =
- lastSelectedWidgetIndex < selectedWidgetIndex
- ? selectedWidgetIndex
- : lastSelectedWidgetIndex;
- const unSelectedSiblings = siblingWidgets.slice(start + 1, end);
- if (unSelectedSiblings && unSelectedSiblings.length) {
- yield put(silentAddSelectionsAction(unSelectedSiblings));
+ try {
+ const { siblingWidgets, widgetId } = action.payload;
+ const selectedWidgets: string[] = yield select(getSelectedWidgets);
+ const lastSelectedWidget: string = yield select(getSelectedWidget);
+ const lastSelectedWidgetIndex = siblingWidgets.indexOf(lastSelectedWidget);
+ const isWidgetSelected = selectedWidgets.includes(widgetId);
+ if (!isWidgetSelected && lastSelectedWidgetIndex > -1) {
+ const selectedWidgetIndex = siblingWidgets.indexOf(widgetId);
+ const start =
+ lastSelectedWidgetIndex < selectedWidgetIndex
+ ? lastSelectedWidgetIndex
+ : selectedWidgetIndex;
+ const end =
+ lastSelectedWidgetIndex < selectedWidgetIndex
+ ? selectedWidgetIndex
+ : lastSelectedWidgetIndex;
+ const unSelectedSiblings = siblingWidgets.slice(start + 1, end);
+ if (unSelectedSiblings && unSelectedSiblings.length) {
+ yield put(silentAddSelectionsAction(unSelectedSiblings));
+ }
}
+ yield put(selectWidgetInitAction(widgetId, true));
+ } catch (error) {
+ yield put({
+ type: ReduxActionErrorTypes.WIDGET_SELECTION_ERROR,
+ payload: {
+ action: ReduxActionTypes.SHIFT_SELECT_WIDGET_INIT,
+ error,
+ },
+ });
}
- yield put(selectWidgetInitAction(widgetId, true));
}
function* selectMultipleWidgetsSaga(
action: ReduxAction<{ widgetIds: string[] }>,
) {
- const { widgetIds } = action.payload;
- if (!widgetIds || !widgetIds.length) {
- return;
- }
- const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
- const parentToMatch = allWidgets[widgetIds[0]].parentId;
- const doesNotMatchParent = widgetIds.some((each) => {
- return allWidgets[each].parentId !== parentToMatch;
- });
- if (doesNotMatchParent) {
- return;
- } else {
- yield put(selectWidgetAction());
- yield put(selectMultipleWidgetsAction(widgetIds));
+ try {
+ const { widgetIds } = action.payload;
+ if (!widgetIds || !widgetIds.length) {
+ return;
+ }
+ const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
+ const parentToMatch = allWidgets[widgetIds[0]].parentId;
+ const doesNotMatchParent = widgetIds.some((each) => {
+ return allWidgets[each].parentId !== parentToMatch;
+ });
+ if (doesNotMatchParent) {
+ return;
+ } else {
+ yield put(selectWidgetAction());
+ yield put(selectMultipleWidgetsAction(widgetIds));
+ }
+ } catch (error) {
+ yield put({
+ type: ReduxActionErrorTypes.WIDGET_SELECTION_ERROR,
+ payload: {
+ action: ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT,
+ error,
+ },
+ });
}
}
diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx
index 231a9a313f32..7fdf60e6ff7c 100644
--- a/app/client/src/utils/WidgetPropsUtils.tsx
+++ b/app/client/src/utils/WidgetPropsUtils.tsx
@@ -1,6 +1,6 @@
import { FetchPageResponse } from "api/PageApi";
import { CANVAS_DEFAULT_HEIGHT_PX } from "constants/AppConstants";
-import { XYCoord } from "react-dnd";
+import { XYCord } from "utils/hooks/useCanvasDragging";
import { ContainerWidgetProps } from "widgets/ContainerWidget";
import { WidgetConfigProps } from "reducers/entityReducers/widgetConfigReducer";
import {
@@ -1090,8 +1090,8 @@ export const extractCurrentDSL = (
export const getDropZoneOffsets = (
colWidth: number,
rowHeight: number,
- dragOffset: XYCoord,
- parentOffset: XYCoord,
+ dragOffset: XYCord,
+ parentOffset: XYCord,
) => {
// Calculate actual drop position by snapping based on x, y and grid cell size
return snapToGrid(
@@ -1145,10 +1145,10 @@ export const isWidgetOverflowingParentBounds = (
};
export const noCollision = (
- clientOffset: XYCoord,
+ clientOffset: XYCord,
colWidth: number,
rowHeight: number,
- dropTargetOffset: XYCoord,
+ dropTargetOffset: XYCord,
widgetWidth: number,
widgetHeight: number,
widgetId: string,
@@ -1164,7 +1164,7 @@ export const noCollision = (
const [left, top] = getDropZoneOffsets(
colWidth,
rowHeight,
- clientOffset as XYCoord,
+ clientOffset as XYCord,
dropTargetOffset,
);
if (left < 0 || top < 0) {
@@ -1203,8 +1203,8 @@ export const currentDropRow = (
export const widgetOperationParams = (
widget: WidgetProps & Partial,
- widgetOffset: XYCoord,
- parentOffset: XYCoord,
+ widgetOffset: XYCord,
+ parentOffset: XYCord,
parentColumnSpace: number,
parentRowSpace: number,
parentWidgetId: string, // parentWidget
diff --git a/app/client/src/utils/hooks/useBlocksToBeDraggedOnCanvas.ts b/app/client/src/utils/hooks/useBlocksToBeDraggedOnCanvas.ts
index aee212870a0a..bf50987428a6 100644
--- a/app/client/src/utils/hooks/useBlocksToBeDraggedOnCanvas.ts
+++ b/app/client/src/utils/hooks/useBlocksToBeDraggedOnCanvas.ts
@@ -17,7 +17,7 @@ import {
widgetOperationParams,
} from "utils/WidgetPropsUtils";
import { DropTargetContext } from "components/editorComponents/DropTargetComponent";
-import { XYCoord } from "react-dnd";
+import { XYCord } from "utils/hooks/useCanvasDragging";
import { isEmpty } from "lodash";
import { CanvasDraggingArenaProps } from "pages/common/CanvasDraggingArena";
import { useDispatch } from "react-redux";
@@ -110,8 +110,8 @@ export const useBlocksToBeDraggedOnCanvas = ({
const getSnappedXY = (
parentColumnWidth: number,
parentRowHeight: number,
- currentOffset: XYCoord,
- parentOffset: XYCoord,
+ currentOffset: XYCord,
+ parentOffset: XYCord,
) => {
// TODO(abhinav): There is a simpler math to use.
const [leftColumn, topRow] = snapToGrid(
@@ -278,7 +278,7 @@ export const useBlocksToBeDraggedOnCanvas = ({
{
x: bottomMostBlock.left,
y: bottomMostBlock.top + bottomMostBlock.height,
- } as XYCoord,
+ } as XYCord,
{ x: 0, y: 0 },
);
if (top > rows - GridDefaults.CANVAS_EXTENSION_OFFSET) {
diff --git a/app/client/src/utils/hooks/useCanvasDragging.ts b/app/client/src/utils/hooks/useCanvasDragging.ts
index c429aee7dd88..02a3e834aa9a 100644
--- a/app/client/src/utils/hooks/useCanvasDragging.ts
+++ b/app/client/src/utils/hooks/useCanvasDragging.ts
@@ -14,6 +14,11 @@ import {
} from "./useBlocksToBeDraggedOnCanvas";
import { useCanvasDragToScroll } from "./useCanvasDragToScroll";
+export interface XYCord {
+ x: number;
+ y: number;
+}
+
export const useCanvasDragging = (
canvasRef: React.RefObject,
canvasDrawRef: React.RefObject,
diff --git a/app/client/test/factories/Widgets/CanvasFactory.ts b/app/client/test/factories/Widgets/CanvasFactory.ts
index e23c26bca639..5cb499428f41 100644
--- a/app/client/test/factories/Widgets/CanvasFactory.ts
+++ b/app/client/test/factories/Widgets/CanvasFactory.ts
@@ -13,6 +13,7 @@ export const CanvasFactory = Factory.Sync.makeFactory({
snapRows: 33,
parentRowSpace: 1,
type: "CANVAS_WIDGET",
+ isVisible: true,
canExtend: true,
minHeight: 1292,
parentColumnSpace: 1,
diff --git a/app/client/test/factories/Widgets/ListFactory.ts b/app/client/test/factories/Widgets/ListFactory.ts
index 4da12f41e4f6..960174ea3bfb 100644
--- a/app/client/test/factories/Widgets/ListFactory.ts
+++ b/app/client/test/factories/Widgets/ListFactory.ts
@@ -6,13 +6,14 @@ export const ListFactory = Factory.Sync.makeFactory({
image: "",
defaultImage: "",
type: "LIST_WIDGET",
+ template: {},
parentId: "Container1",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
- bottomRow: 3,
+ bottomRow: 10,
isLoading: false,
listData: [],
disablePropertyPane: false,
diff --git a/app/client/test/testMockedWidgets.tsx b/app/client/test/testMockedWidgets.tsx
index eea110370e72..b5dee0b7720a 100644
--- a/app/client/test/testMockedWidgets.tsx
+++ b/app/client/test/testMockedWidgets.tsx
@@ -1,9 +1,14 @@
import Canvas from "pages/Editor/Canvas";
+import MainContainer from "pages/Editor/MainContainer";
import React from "react";
import { useSelector } from "react-redux";
-import { mockGetCanvasWidgetDsl } from "./testCommon";
+import { mockGetCanvasWidgetDsl, useMockDsl } from "./testCommon";
export const MockCanvas = () => {
const dsl = useSelector(mockGetCanvasWidgetDsl);
return ;
};
+export function UpdatedMainContainer({ dsl }: any) {
+ useMockDsl(dsl);
+ return ;
+}