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 ; +}