diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js index 6e3009fe9a..1f969e2c13 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Resize_spec.js @@ -7,10 +7,17 @@ describe("Canvas Resize", function() { }); it("Deleting bottom widget should resize canvas", function() { const InitHeight = "2960px"; - const FinalHeight = "1292px"; cy.get(commonlocators.dropTarget).should("have.css", "height", InitHeight); cy.openPropertyPane("textwidget"); + cy.intercept("PUT", "/api/v1/layouts/*/pages/*").as("deleteUpdate"); cy.get(commonlocators.deleteWidget).click(); - cy.get(commonlocators.dropTarget).should("have.css", "height", FinalHeight); + cy.wait("@deleteUpdate").then((response) => { + const dsl = response.response.body.data.dsl; + cy.get(commonlocators.dropTarget).should( + "have.css", + "height", + `${dsl.bottomRow}px`, + ); + }); }); }); diff --git a/app/client/src/actions/widgetSelectionActions.ts b/app/client/src/actions/widgetSelectionActions.ts index 730ab56ec1..c323a53128 100644 --- a/app/client/src/actions/widgetSelectionActions.ts +++ b/app/client/src/actions/widgetSelectionActions.ts @@ -1,4 +1,5 @@ import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; export const selectWidgetAction = ( widgetId?: string, @@ -16,7 +17,7 @@ export const selectWidgetInitAction = ( payload: { widgetId, isMultiSelect }, }); -export const selectAllWidgetsAction = ( +export const selectMultipleWidgetsAction = ( widgetIds?: string[], ): ReduxAction<{ widgetIds?: string[] }> => { return { @@ -25,7 +26,7 @@ export const selectAllWidgetsAction = ( }; }; -export const selectMultipleWidgetsAction = ( +export const silentAddSelectionsAction = ( widgetIds?: string[], ): ReduxAction<{ widgetIds?: string[] }> => { return { @@ -43,9 +44,21 @@ export const deselectMultipleWidgetsAction = ( }; }; -export const selectAllWidgetsInitAction = () => { +export const selectAllWidgetsInCanvasInitAction = ( + canvasId = MAIN_CONTAINER_WIDGET_ID, +): ReduxAction<{ canvasId: string }> => { + return { + type: ReduxActionTypes.SELECT_ALL_WIDGETS_IN_CANVAS_INIT, + payload: { + canvasId, + }, + }; +}; + +export const selectMultipleWidgetsInitAction = (widgetIds: string[]) => { return { type: ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT, + payload: { widgetIds }, }; }; diff --git a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx index f560429703..14d9ee1608 100644 --- a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx @@ -5,6 +5,7 @@ import { ComponentProps } from "./BaseComponent"; import { invisible } from "constants/DefaultTheme"; import { Color } from "constants/Colors"; import { generateClassName, getCanvasClassName } from "utils/generators"; +import { useCanvasMinHeightUpdateHook } from "utils/hooks/useCanvasMinHeightUpdateHook"; const scrollContents = css` overflow-y: auto; @@ -48,7 +49,7 @@ const StyledContainerComponent = styled.div< function ContainerComponent(props: ContainerComponentProps) { const containerStyle = props.containerStyle || "card"; const containerRef: RefObject = useRef(null); - + useCanvasMinHeightUpdateHook(props.widgetId, props.minHeight); useEffect(() => { if (!props.shouldScrollContents) { const supportsNativeSmoothScroll = @@ -89,6 +90,7 @@ export interface ContainerComponentProps extends ComponentProps { resizeDisabled?: boolean; selected?: boolean; focused?: boolean; + minHeight?: number; } export default ContainerComponent; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index a5bdf3632f..01909c1dee 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -306,6 +306,7 @@ export const ReduxActionTypes: { [key: string]: string } = { SELECT_WIDGET: "SELECT_WIDGET", SELECT_MULTIPLE_WIDGETS: "SELECT_MULTIPLE_WIDGETS", SELECT_MULTIPLE_WIDGETS_INIT: "SELECT_MULTIPLE_WIDGETS_INIT", + SELECT_ALL_WIDGETS_IN_CANVAS_INIT: "SELECT_ALL_WIDGETS_IN_CANVAS_INIT", DESELECT_WIDGETS: "DESELECT_WIDGETS", SELECT_WIDGETS: "SELECT_WIDGETS", FOCUS_WIDGET: "FOCUS_WIDGET", diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx index 76220df0fd..c00c31c147 100644 --- a/app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx +++ b/app/client/src/pages/Editor/Explorer/Pages/PageContextMenu.tsx @@ -103,8 +103,6 @@ export function PageContextMenu(props: { }); } - console.log({ props }); - if (!props.isDefaultPage) { optionTree.push({ value: "delete", diff --git a/app/client/src/pages/Editor/GlobalHotKeys.test.tsx b/app/client/src/pages/Editor/GlobalHotKeys.test.tsx index f3c7c75997..507c40715f 100644 --- a/app/client/src/pages/Editor/GlobalHotKeys.test.tsx +++ b/app/client/src/pages/Editor/GlobalHotKeys.test.tsx @@ -1,29 +1,9 @@ -// 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 -const mockGenerator = function*() { - yield all([]); -}; - -// top avoid the first middleware run which wud initiate all sagas. -jest.mock("sagas", () => ({ - rootSaga: mockGenerator, -})); - -// only the deafault exports are mocked to avoid overriding utilities exported out of them. defaults are marked to avoid worker initiation and page api calls in tests. -jest.mock("sagas/EvaluationsSaga", () => ({ - ...jest.requireActual("sagas/EvaluationsSaga"), - default: mockGenerator, -})); -jest.mock("sagas/PageSagas", () => ({ - ...jest.requireActual("sagas/PageSagas"), - default: mockGenerator, -})); - import React from "react"; import { buildChildren, widgetCanvasFactory, } from "test/factories/WidgetFactoryUtils"; -import { render, fireEvent } from "test/testUtils"; +import { act, render, fireEvent } from "test/testUtils"; import GlobalHotKeys from "./GlobalHotKeys"; import MainContainer from "./MainContainer"; import { MemoryRouter } from "react-router-dom"; @@ -34,72 +14,316 @@ import { all } from "@redux-saga/core/effects"; import { dispatchTestKeyboardEventWithCode, MockApplication, + mockGetCanvasWidgetDsl, + MockPageDSL, useMockDsl, } from "test/testCommon"; -const mockGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl"); -const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage"); -function UpdatedMainContaner({ dsl }: any) { - useMockDsl(dsl); - return ; -} - -it("Cmd + A - select all widgets on canvas", () => { - const children: any = buildChildren([ - { type: "TABS_WIDGET" }, - { type: "SWITCH_WIDGET" }, - ]); - const dsl: any = widgetCanvasFactory.build({ - children, - }); - mockGetCanvasWidgetDsl.mockImplementation(() => dsl); - 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); - if (canvasWidgets[1].firstChild) { - fireEvent.mouseOver(canvasWidgets[1].firstChild); - fireEvent.click(canvasWidgets[1].firstChild); +import { MockCanvas } from "test/testMockedWidgets"; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; +describe("Select all hotkey", () => { + const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage"); + const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl"); + function UpdatedMainContainer({ dsl }: any) { + useMockDsl(dsl); + return ; } - propPane = component.queryByTestId("t--propertypane"); - expect(propPane).not.toBeNull(); + // 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*() { + yield all([]); + }; - const artBoard: any = component.queryByTestId("t--canvas-artboard"); - // deselect all other widgets - fireEvent.click(artBoard); + // top avoid the first middleware run which wud initiate all sagas. + jest.mock("sagas", () => ({ + rootSaga: mockGenerator, + })); - dispatchTestKeyboardEventWithCode( - component.container, - "keydown", - "A", - 65, - false, - true, - ); - let selectedWidgets = component.queryAllByTestId( - "t--widget-propertypane-toggle", - ); - expect(selectedWidgets.length).toBe(2); - dispatchTestKeyboardEventWithCode( - component.container, - "keydown", - "escape", - 27, - false, - false, - ); - selectedWidgets = component.queryAllByTestId("t--widget-propertypane-toggle"); - expect(selectedWidgets.length).toBe(0); + // only the deafault exports are mocked to avoid overriding utilities exported out of them. defaults are marked to avoid worker initiation and page api calls in tests. + jest.mock("sagas/EvaluationsSaga", () => ({ + ...jest.requireActual("sagas/EvaluationsSaga"), + default: mockGenerator, + })); + jest.mock("sagas/PageSagas", () => ({ + ...jest.requireActual("sagas/PageSagas"), + default: mockGenerator, + })); + }); + + it("Cmd + A - select all widgets on canvas", () => { + const children: any = buildChildren([ + { type: "TABS_WIDGET" }, + { type: "SWITCH_WIDGET" }, + ]); + 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); + if (canvasWidgets[1].firstChild) { + fireEvent.mouseOver(canvasWidgets[1].firstChild); + fireEvent.click(canvasWidgets[1].firstChild); + } + 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--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(2); + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "escape", + 27, + false, + false, + ); + selectedWidgets = component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(0); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "A", + 65, + false, + true, + ); + }); + + selectedWidgets = component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + 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--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(2); + }); + afterAll(() => jest.resetModules()); +}); + +describe("Cut/Copy/Paste hotkey", () => { + it("Should copy and paste all selected widgets with hotkey cmd + c and cmd + v ", async () => { + const children: any = buildChildren([ + { + type: "TABS_WIDGET", + topRow: 5, + bottomRow: 30, + leftColumn: 5, + rightColumn: 30, + }, + { + type: "SWITCH_WIDGET", + topRow: 5, + bottomRow: 10, + leftColumn: 40, + rightColumn: 48, + }, + ]); + const dsl: any = widgetCanvasFactory.build({ + children, + }); + const component = render( + + + + + , + ); + const artBoard: any = await component.queryByTestId("t--canvas-artboard"); + // deselect all other widgets + fireEvent.click(artBoard); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "A", + 65, + false, + true, + ); + }); + + let selectedWidgets = await component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(2); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "C", + 67, + false, + true, + ); + }); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "V", + 86, + false, + true, + ); + }); + await component.findByText(children[0].widgetName + "Copy"); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "A", + 65, + false, + true, + ); + }); + + selectedWidgets = await component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(4); + }); + it("Should cut and paste all selected widgets with hotkey cmd + x and cmd + v ", async () => { + const children: any = buildChildren([ + { + type: "TABS_WIDGET", + topRow: 5, + bottomRow: 30, + leftColumn: 5, + rightColumn: 30, + parentId: MAIN_CONTAINER_WIDGET_ID, + }, + { + type: "SWITCH_WIDGET", + topRow: 5, + bottomRow: 10, + leftColumn: 40, + rightColumn: 48, + parentId: MAIN_CONTAINER_WIDGET_ID, + }, + ]); + const dsl: any = widgetCanvasFactory.build({ + children, + }); + const component = render( + + + + + , + ); + const artBoard: any = await component.queryByTestId("t--canvas-artboard"); + // deselect all other widgets + fireEvent.click(artBoard); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "A", + 65, + false, + true, + ); + }); + + let selectedWidgets = await component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(2); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "X", + 88, + false, + true, + ); + }); + await component.findByTestId("canvas-0"); + selectedWidgets = await component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(0); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "V", + 86, + false, + true, + ); + }); + await component.findByText(children[0].widgetName); + act(() => { + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "A", + 65, + false, + true, + ); + }); + + selectedWidgets = await component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(2); + }); }); -afterAll(() => jest.resetModules()); diff --git a/app/client/src/pages/Editor/GlobalHotKeys.tsx b/app/client/src/pages/Editor/GlobalHotKeys.tsx index 00dace7be3..e29910838a 100644 --- a/app/client/src/pages/Editor/GlobalHotKeys.tsx +++ b/app/client/src/pages/Editor/GlobalHotKeys.tsx @@ -11,8 +11,8 @@ import { pasteWidget, } from "actions/widgetActions"; import { - selectAllWidgetsInitAction, - selectAllWidgetsAction, + selectAllWidgetsInCanvasInitAction, + selectMultipleWidgetsAction, } from "actions/widgetSelectionActions"; import { toggleShowGlobalSearchModal } from "actions/globalSearchActions"; import { isMac } from "utils/helpers"; @@ -64,12 +64,6 @@ class GlobalHotKeys extends React.Component { return false; } - public areMultipleWidgetsSelected() { - const multipleWidgetsSelected = - this.props.selectedWidgets && this.props.selectedWidgets.length >= 2; - return !!multipleWidgetsSelected; - } - public onOnmnibarHotKeyDown(e: KeyboardEvent) { e.preventDefault(); this.props.toggleShowGlobalSearchModal(); @@ -129,10 +123,7 @@ class GlobalHotKeys extends React.Component { group="Canvas" label="Copy Widget" onKeyDown={(e: any) => { - if ( - this.stopPropagationIfWidgetSelected(e) && - !this.areMultipleWidgetsSelected() - ) { + if (this.stopPropagationIfWidgetSelected(e)) { this.props.copySelectedWidget(); } }} @@ -174,10 +165,7 @@ class GlobalHotKeys extends React.Component { group="Canvas" label="Cut Widget" onKeyDown={(e: any) => { - if ( - this.stopPropagationIfWidgetSelected(e) && - !this.areMultipleWidgetsSelected() - ) { + if (this.stopPropagationIfWidgetSelected(e)) { this.props.cutSelectedWidget(); } }} @@ -250,8 +238,8 @@ const mapDispatchToProps = (dispatch: any) => { resetCommentMode: () => dispatch(setCommentModeAction(false)), openDebugger: () => dispatch(showDebugger()), closeProppane: () => dispatch(closePropertyPane()), - selectAllWidgetsInit: () => dispatch(selectAllWidgetsInitAction()), - deselectAllWidgets: () => dispatch(selectAllWidgetsAction([])), + selectAllWidgetsInit: () => dispatch(selectAllWidgetsInCanvasInitAction()), + deselectAllWidgets: () => dispatch(selectMultipleWidgetsAction([])), executeAction: () => dispatch(runActionViaShortcut()), }; }; diff --git a/app/client/src/pages/common/CanvasSelectionArena.tsx b/app/client/src/pages/common/CanvasSelectionArena.tsx index 1d718a10a8..cc79994f75 100644 --- a/app/client/src/pages/common/CanvasSelectionArena.tsx +++ b/app/client/src/pages/common/CanvasSelectionArena.tsx @@ -158,10 +158,12 @@ export const CanvasSelectionArena = memo( const onMouseLeave = () => { 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 onClick = (e: any) => { @@ -236,6 +238,7 @@ export const CanvasSelectionArena = memo( currentPageId, mainContainer.rightColumn, mainContainer.bottomRow, + mainContainer.minHeight, ]); return appMode === APP_MODE.EDIT ? ( diff --git a/app/client/src/sagas/SelectionCanvasSagas.ts b/app/client/src/sagas/SelectionCanvasSagas.ts index 4eadab3e74..d464068bfe 100644 --- a/app/client/src/sagas/SelectionCanvasSagas.ts +++ b/app/client/src/sagas/SelectionCanvasSagas.ts @@ -1,4 +1,4 @@ -import { selectAllWidgetsAction } from "actions/widgetSelectionActions"; +import { selectMultipleWidgetsAction } from "actions/widgetSelectionActions"; import { OccupiedSpace } from "constants/editorConstants"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; import { @@ -99,7 +99,7 @@ function* selectAllWidgetsInAreaSaga( const currentSelectedWidgets: string[] = yield select(getSelectedWidgets); if (!isEqual(filteredWidgetsToSelect, currentSelectedWidgets)) { - yield put(selectAllWidgetsAction(filteredWidgetsToSelect)); + yield put(selectMultipleWidgetsAction(filteredWidgetsToSelect)); } } } diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index c205d51227..bdf93b0997 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -16,12 +16,7 @@ import { CanvasWidgetsReduxState, FlattenedWidgetProps, } from "reducers/entityReducers/canvasWidgetsReducer"; -import { - getSelectedWidget, - getWidget, - getWidgetMetaProps, - getWidgets, -} from "./selectors"; +import { getSelectedWidget, getWidget, getWidgets } from "./selectors"; import { generateWidgetProps, updateWidgetPosition, @@ -93,7 +88,10 @@ import { closePropertyPane, forceOpenPropertyPane, } from "actions/widgetActions"; -import { selectWidgetInitAction } from "actions/widgetSelectionActions"; +import { + selectMultipleWidgetsInitAction, + selectWidgetInitAction, +} from "actions/widgetSelectionActions"; import { getDataTree } from "selectors/dataTreeSelectors"; import { @@ -124,7 +122,9 @@ import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; import { + checkIfPastingIntoListWidget, doesTriggerPathsContainPropertyPath, + getParentWidgetIdForPasting, getWidgetChildren, handleSpecificCasesWhilePasting, } from "./WidgetOperationUtils"; @@ -443,7 +443,10 @@ const resizeCanvasToLowestWidget = ( return; } - let lowestBottomRow = 0; + let lowestBottomRow = Math.ceil( + (finalWidgets[parentId].minHeight || 0) / + GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + ); const childIds = finalWidgets[parentId].children || []; // find lowest row childIds.forEach((cId) => { @@ -492,6 +495,9 @@ export function* deleteAllSelectedWidgetsSaga( parentUpdatedWidgets, falttendedWidgets.map((widgets: any) => widgets.widgetId), ); + // assuming only widgets with same parent can be selected + const parentId = widgets[selectedWidgets[0]].parentId; + resizeCanvasToLowestWidget(finalWidgets, parentId); yield put(updateAndSaveLayout(finalWidgets)); yield put(selectWidgetInitAction("")); @@ -815,8 +821,15 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { }, stateWidgets, ); - + const parentId = deletedWidgets[0].parentId; + if (parentId) { + resizeCanvasToLowestWidget(finalWidgets, parentId); + } yield put(updateAndSaveLayout(finalWidgets)); + deletedWidgetIds.forEach((widgetId) => { + setTimeout(() => flashElementById(widgetId), 100); + }); + yield put(selectMultipleWidgetsInitAction(deletedWidgetIds)); if (deletedWidgetIds.length === 1) { yield put(forceOpenPropertyPane(action.payload.widgetId)); } @@ -1319,14 +1332,26 @@ function* updateCanvasSize( } } -function* createWidgetCopy() { - const selectedWidget = yield select(getSelectedWidget); - if (!selectedWidget) return; - const widgets = yield select(getWidgets); - const widgetsToStore = getAllWidgetsInTree(selectedWidget.widgetId, widgets); - return yield saveCopiedWidgets( - JSON.stringify({ widgetId: selectedWidget.widgetId, list: widgetsToStore }), +function* createWidgetCopy(widget: FlattenedWidgetProps) { + const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select( + getWidgets, ); + const widgetsToStore = getAllWidgetsInTree(widget.widgetId, allWidgets); + return { + widgetId: widget.widgetId, + list: widgetsToStore, + parentId: widget.parentId, + }; +} + +function* createSelectedWidgetsCopy(selectedWidgets: FlattenedWidgetProps[]) { + if (!selectedWidgets || !selectedWidgets.length) return; + const widgetListsToStore: { + widgetId: string; + parentId: string; + list: FlattenedWidgetProps[]; + }[] = yield all(selectedWidgets.map((each) => call(createWidgetCopy, each))); + return yield saveCopiedWidgets(JSON.stringify(widgetListsToStore)); } /** @@ -1337,8 +1362,11 @@ function* createWidgetCopy() { * @returns */ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) { - const selectedWidget = yield select(getSelectedWidget); - if (!selectedWidget) { + const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select( + getWidgets, + ); + const selectedWidgets: string[] = yield select(getSelectedWidgets); + if (!selectedWidgets) { Toaster.show({ text: createMessage(ERROR_WIDGET_COPY_NO_WIDGET_SELECTED), variant: Variant.info, @@ -1346,7 +1374,11 @@ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) { return; } - if (selectedWidget.disallowCopy === true) { + const allAllowedToCopy = selectedWidgets.some((each) => { + return !allWidgets[each].disallowCopy; + }); + + if (!allAllowedToCopy) { Toaster.show({ text: createMessage(ERROR_WIDGET_COPY_NOT_ALLOWED), variant: Variant.info, @@ -1354,20 +1386,28 @@ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) { return; } + const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]); - const saveResult = yield createWidgetCopy(); + const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps); - const eventName = action.payload.isShortcut - ? "WIDGET_COPY_VIA_SHORTCUT" - : "WIDGET_COPY"; - AnalyticsUtil.logEvent(eventName, { - widgetName: selectedWidget.widgetName, - widgetType: selectedWidget.type, + selectedWidgetProps.forEach((each) => { + const eventName = action.payload.isShortcut + ? "WIDGET_COPY_VIA_SHORTCUT" + : "WIDGET_COPY"; + AnalyticsUtil.logEvent(eventName, { + widgetName: each.widgetName, + widgetType: each.type, + }); }); if (saveResult) { Toaster.show({ - text: createMessage(WIDGET_COPY, selectedWidget.widgetName), + text: createMessage( + WIDGET_COPY, + selectedWidgetProps.length > 1 + ? `${selectedWidgetProps.length} Widgets` + : selectedWidgetProps[0].widgetName, + ), variant: Variant.success, }); } @@ -1377,16 +1417,26 @@ export function calculateNewWidgetPosition( widget: WidgetProps, parentId: string, canvasWidgets: { [widgetId: string]: FlattenedWidgetProps }, + parentBottomRow?: number, + persistColumnPosition = false, ) { // Note: This is a very simple algorithm. // We take the bottom most widget in the canvas, then calculate the top,left,right,bottom // co-ordinates for the new widget, such that it can be placed at the bottom of the canvas. - const nextAvailableRow = nextAvailableRowInContainer(parentId, canvasWidgets); + const nextAvailableRow = parentBottomRow + ? parentBottomRow + : nextAvailableRowInContainer(parentId, canvasWidgets); return { - leftColumn: 0, - rightColumn: widget.rightColumn - widget.leftColumn, - topRow: nextAvailableRow, - bottomRow: nextAvailableRow + (widget.bottomRow - widget.topRow), + leftColumn: persistColumnPosition ? widget.leftColumn : 0, + rightColumn: persistColumnPosition + ? widget.rightColumn + : widget.rightColumn - widget.leftColumn, + topRow: parentBottomRow + ? nextAvailableRow + widget.topRow + : nextAvailableRow, + bottomRow: parentBottomRow + ? nextAvailableRow + widget.bottomRow + : nextAvailableRow + (widget.bottomRow - widget.topRow), }; } @@ -1398,7 +1448,12 @@ function* getEntityNames() { function getNextWidgetName( widgets: CanvasWidgetsReduxState, type: WidgetType, - evalTree: Record, + evalTree: { + bottomRow: any; + leftColumn: any; + rightColumn: any; + topRow: any; + }, options?: Record, ) { // Compute the new widget's name @@ -1423,286 +1478,274 @@ function getNextWidgetName( * this saga create a new widget from the copied one to store */ function* pasteWidgetSaga() { - const copiedWidgets: { + const copiedWidgetGroups: { widgetId: string; + parentId: string; list: WidgetProps[]; - } = yield getCopiedWidgets(); - // Don't try to paste if there is no copied widget - if (!copiedWidgets) return; - const copiedWidgetId = copiedWidgets.widgetId; - const copiedWidget = copiedWidgets.list.find( - (widget) => widget.widgetId === copiedWidgetId, + }[] = yield getCopiedWidgets(); + if (!Array.isArray(copiedWidgetGroups)) { + return; + // to avoid invoking old copied widgets + } + const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets); + let selectedWidget: FlattenedWidgetProps | undefined = yield select( + getSelectedWidget, ); - if (copiedWidget) { - // Log the paste event - AnalyticsUtil.logEvent("WIDGET_PASTE", { - widgetName: copiedWidget.widgetName, - widgetType: copiedWidget.type, - }); - const stateWidgets = yield select(getWidgets); - let widgets = { ...stateWidgets }; + const pastingIntoWidgetId: string = yield getParentWidgetIdForPasting( + { ...stateWidgets }, + selectedWidget, + ); - let selectedWidget = yield select(getSelectedWidget); - - // when list widget is selected, if the user is pasting, we want it to be pasted in the template - // which is first children of list widget - if (selectedWidget?.type === WidgetTypes.LIST_WIDGET) { - const childrenIds: string[] = yield call( - getWidgetChildren, - selectedWidget.children[0], - ); - const firstChildId = childrenIds[0]; - - selectedWidget = yield select(getWidget, firstChildId); - } - - let newWidgetParentId = MAIN_CONTAINER_WIDGET_ID; - let parentWidget = widgets[MAIN_CONTAINER_WIDGET_ID]; - - // If the selected widget is not the main container - if ( - selectedWidget && - selectedWidget.widgetId !== MAIN_CONTAINER_WIDGET_ID - ) { - // Select the parent of the selected widget if parent is not - // the main container - if ( - selectedWidget.parentId !== MAIN_CONTAINER_WIDGET_ID && - widgets[selectedWidget.parentId] && - widgets[selectedWidget.parentId].children && - widgets[selectedWidget.parentId].children.length > 0 - ) { - parentWidget = widgets[selectedWidget.parentId]; - newWidgetParentId = selectedWidget.parentId; - } - // Select the selected widget if the widget is container like - if (selectedWidget.children) { - parentWidget = widgets[selectedWidget.widgetId]; - } - } - - // If the parent widget in which to paste the copied widget - // is not the main container and is not a canvas widget - if ( - parentWidget.widgetId !== MAIN_CONTAINER_WIDGET_ID && - parentWidget.type !== WidgetTypes.CANVAS_WIDGET - ) { - let childWidget; - // If the widget in which to paste the new widget is NOT - // a tabs widget - if (parentWidget.type !== WidgetTypes.TABS_WIDGET) { - // The child will be a CANVAS_WIDGET, as we've established - // this parent widget to be a container like widget - // Which always has its first child as a canvas widget - childWidget = widgets[parentWidget.children[0]]; - } else { - // If the widget in which to paste the new widget is a tabs widget - // Find the currently selected tab canvas widget - const { selectedTabWidgetId } = yield select( - getWidgetMetaProps, - parentWidget.widgetId, + selectedWidget = yield checkIfPastingIntoListWidget(selectedWidget); + let widgets = { ...stateWidgets }; + const newlyCreatedWidgetIds: string[] = []; + const sortedWidgetList = copiedWidgetGroups.sort( + (a, b) => a.list[0].topRow - b.list[0].topRow, + ); + const copiedGroupTopRow = sortedWidgetList[0].list[0].topRow; + const nextAvailableRow: number = nextAvailableRowInContainer( + pastingIntoWidgetId, + widgets, + ); + yield all( + copiedWidgetGroups.map((copiedWidgets) => + call(function*() { + // Don't try to paste if there is no copied widget + if (!copiedWidgets) return; + const copiedWidgetId = copiedWidgets.widgetId; + const unUpdatedCopyOfWidget = copiedWidgets.list.find( + (widget) => widget.widgetId === copiedWidgetId, ); - if (selectedTabWidgetId) childWidget = widgets[selectedTabWidgetId]; - } - // If the finally selected parent in which to paste the widget - // is a CANVAS_WIDGET, use its widgetId as the new widget's parent Id - if (childWidget && childWidget.type === WidgetTypes.CANVAS_WIDGET) { - newWidgetParentId = childWidget.widgetId; - } - } - // Compute the new widget's positional properties - const { - bottomRow, - leftColumn, - rightColumn, - topRow, - } = yield calculateNewWidgetPosition( - copiedWidget, - newWidgetParentId, - widgets, - ); + if (unUpdatedCopyOfWidget) { + const copiedWidget = { + ...unUpdatedCopyOfWidget, + topRow: unUpdatedCopyOfWidget.topRow - copiedGroupTopRow, + bottomRow: unUpdatedCopyOfWidget.bottomRow - copiedGroupTopRow, + }; - const evalTree = yield select(getDataTree); + // Log the paste event + AnalyticsUtil.logEvent("WIDGET_PASTE", { + widgetName: copiedWidget.widgetName, + widgetType: copiedWidget.type, + }); - // Get a flat list of all the widgets to be updated - const widgetList = copiedWidgets.list; - const widgetIdMap: Record = {}; - const widgetNameMap: Record = {}; - const newWidgetList: FlattenedWidgetProps[] = []; - let newWidgetId: string = copiedWidget.widgetId; - // Generate new widgetIds for the flat list of all the widgets to be updated - widgetList.forEach((widget) => { - // Create a copy of the widget properties - const newWidget = cloneDeep(widget); - newWidget.widgetId = generateReactKey(); - // Add the new widget id so that it maps the previous widget id - widgetIdMap[widget.widgetId] = newWidget.widgetId; - - // Add the new widget to the list - newWidgetList.push(newWidget); - }); - - // For each of the new widgets generated - for (let i = 0; i < newWidgetList.length; i++) { - const widget = newWidgetList[i]; - const oldWidgetName = widget.widgetName; - - // Update the children widgetIds if it has children - if (widget.children && widget.children.length > 0) { - widget.children.forEach((childWidgetId: string, index: number) => { - if (widget.children) { - widget.children[index] = widgetIdMap[childWidgetId]; - } - }); - } - - // Update the tabs for the tabs widget. - if (widget.tabsObj && widget.type === WidgetTypes.TABS_WIDGET) { - try { - const tabs = Object.values(widget.tabsObj); - if (Array.isArray(tabs)) { - widget.tabsObj = tabs.reduce((obj: any, tab) => { - tab.widgetId = widgetIdMap[tab.widgetId]; - obj[tab.id] = tab; - return obj; - }, {}); - } - } catch (error) { - log.debug("Error updating tabs", error); - } - } - - // Update the table widget column properties - if (widget.type === WidgetTypes.TABLE_WIDGET) { - try { - const oldWidgetName = widget.widgetName; - const newWidgetName = getNextWidgetName( + // Compute the new widget's positional properties + const { + bottomRow, + leftColumn, + rightColumn, + topRow, + } = yield calculateNewWidgetPosition( + copiedWidget, + pastingIntoWidgetId, widgets, - widget.type, - evalTree, + nextAvailableRow, + true, ); - // If the primaryColumns of the table exist - if (widget.primaryColumns) { - // For each column - for (const [columnId, column] of Object.entries( - widget.primaryColumns, - )) { - // For each property in the column - for (const [key, value] of Object.entries( - column as ColumnProperties, - )) { - // Replace reference of previous widget with the new widgetName - // This handles binding scenarios like `{{Table2.tableData.map((currentRow) => (currentRow.id))}}` - widget.primaryColumns[columnId][key] = isString(value) - ? value.replace(`${oldWidgetName}.`, `${newWidgetName}.`) - : value; + // goToNextAvailableRow = true, + // persistColumnPosition = false, + + const evalTree = yield select(getDataTree); + + // Get a flat list of all the widgets to be updated + const widgetList = copiedWidgets.list; + const widgetIdMap: Record = {}; + const widgetNameMap: Record = {}; + const newWidgetList: FlattenedWidgetProps[] = []; + let newWidgetId: string = copiedWidget.widgetId; + // Generate new widgetIds for the flat list of all the widgets to be updated + widgetList.forEach((widget) => { + // Create a copy of the widget properties + const newWidget = cloneDeep(widget); + newWidget.widgetId = generateReactKey(); + // Add the new widget id so that it maps the previous widget id + widgetIdMap[widget.widgetId] = newWidget.widgetId; + + // Add the new widget to the list + newWidgetList.push(newWidget); + }); + + // For each of the new widgets generated + for (let i = 0; i < newWidgetList.length; i++) { + const widget = newWidgetList[i]; + const oldWidgetName = widget.widgetName; + + // Update the children widgetIds if it has children + if (widget.children && widget.children.length > 0) { + widget.children.forEach( + (childWidgetId: string, index: number) => { + if (widget.children) { + widget.children[index] = widgetIdMap[childWidgetId]; + } + }, + ); + } + + // Update the tabs for the tabs widget. + if (widget.tabsObj && widget.type === WidgetTypes.TABS_WIDGET) { + try { + const tabs = Object.values(widget.tabsObj); + if (Array.isArray(tabs)) { + widget.tabsObj = tabs.reduce((obj: any, tab) => { + tab.widgetId = widgetIdMap[tab.widgetId]; + obj[tab.id] = tab; + return obj; + }, {}); + } + } catch (error) { + log.debug("Error updating tabs", error); } } - } - // Use the new widget name we used to replace the column properties above. - widget.widgetName = newWidgetName; - } catch (error) { - log.debug("Error updating table widget properties", error); - } - } - // If it is the copied widget, update position properties - if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) { - newWidgetId = widget.widgetId; - widget.leftColumn = leftColumn; - widget.topRow = topRow; - widget.bottomRow = bottomRow; - widget.rightColumn = rightColumn; - widget.parentId = newWidgetParentId; - // Also, update the parent widget in the canvas widgets - // to include this new copied widget's id in the parent's children - let parentChildren = [widget.widgetId]; - if ( - widgets[newWidgetParentId].children && - Array.isArray(widgets[newWidgetParentId].children) - ) { - // Add the new child to existing children - parentChildren = parentChildren.concat( - widgets[newWidgetParentId].children, - ); - } - widgets = { - ...widgets, - [newWidgetParentId]: { - ...widgets[newWidgetParentId], - children: parentChildren, - }, - }; - // If the copied widget's boundaries exceed the parent's - // Make the parent scrollable - if ( - widgets[newWidgetParentId].bottomRow * - widgets[widget.parentId].parentRowSpace <= - widget.bottomRow * widget.parentRowSpace - ) { - if (widget.parentId !== MAIN_CONTAINER_WIDGET_ID) { - const parent = widgets[widgets[newWidgetParentId].parentId]; - widgets[widgets[newWidgetParentId].parentId] = { - ...parent, - shouldScrollContents: true, - }; + // Update the table widget column properties + if (widget.type === WidgetTypes.TABLE_WIDGET) { + try { + const oldWidgetName = widget.widgetName; + const newWidgetName = getNextWidgetName( + widgets, + widget.type, + evalTree, + ); + // If the primaryColumns of the table exist + if (widget.primaryColumns) { + // For each column + for (const [columnId, column] of Object.entries( + widget.primaryColumns, + )) { + // For each property in the column + for (const [key, value] of Object.entries( + column as ColumnProperties, + )) { + // Replace reference of previous widget with the new widgetName + // This handles binding scenarios like `{{Table2.tableData.map((currentRow) => (currentRow.id))}}` + widget.primaryColumns[columnId][key] = isString(value) + ? value.replace( + `${oldWidgetName}.`, + `${newWidgetName}.`, + ) + : value; + } + } + } + // Use the new widget name we used to replace the column properties above. + widget.widgetName = newWidgetName; + } catch (error) { + log.debug("Error updating table widget properties", error); + } + } + + // If it is the copied widget, update position properties + if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) { + newWidgetId = widget.widgetId; + widget.leftColumn = leftColumn; + widget.topRow = topRow; + widget.bottomRow = bottomRow; + widget.rightColumn = rightColumn; + widget.parentId = pastingIntoWidgetId; + // Also, update the parent widget in the canvas widgets + // to include this new copied widget's id in the parent's children + let parentChildren = [widget.widgetId]; + const widgetChildren = widgets[pastingIntoWidgetId].children; + if (widgetChildren && Array.isArray(widgetChildren)) { + // Add the new child to existing children + parentChildren = parentChildren.concat(widgetChildren); + } + const updateBottomRow = + widget.bottomRow * widget.parentRowSpace > + widgets[pastingIntoWidgetId].bottomRow; + widgets = { + ...widgets, + [pastingIntoWidgetId]: { + ...widgets[pastingIntoWidgetId], + ...(updateBottomRow + ? { + bottomRow: widget.bottomRow * widget.parentRowSpace, + } + : {}), + children: parentChildren, + }, + }; + // If the copied widget's boundaries exceed the parent's + // Make the parent scrollable + if ( + widgets[pastingIntoWidgetId].bottomRow * + widgets[widget.parentId].parentRowSpace <= + widget.bottomRow * widget.parentRowSpace + ) { + const parentOfPastingWidget = + widgets[pastingIntoWidgetId].parentId; + if ( + parentOfPastingWidget && + widget.parentId !== MAIN_CONTAINER_WIDGET_ID + ) { + const parent = widgets[parentOfPastingWidget]; + widgets[parentOfPastingWidget] = { + ...parent, + shouldScrollContents: true, + }; + } + } + } else { + // For all other widgets in the list + // (These widgets will be descendants of the copied widget) + // This means, that their parents will also be newly copied widgets + // Update widget's parent widget ids with the new parent widget ids + const newParentId = newWidgetList.find((newWidget) => + widget.parentId + ? newWidget.widgetId === widgetIdMap[widget.parentId] + : false, + )?.widgetId; + if (newParentId) widget.parentId = newParentId; + } + // Generate a new unique widget name + widget.widgetName = getNextWidgetName( + widgets, + widget.type, + evalTree, + { + prefix: oldWidgetName, + startWithoutIndex: true, + }, + ); + widgetNameMap[oldWidgetName] = widget.widgetName; + // Add the new widget to the canvas widgets + widgets[widget.widgetId] = widget; + } + newlyCreatedWidgetIds.push(widgetIdMap[copiedWidgetId]); + // 1. updating template in the copied widget and deleting old template associations + // 2. updating dynamicBindingPathList in the copied grid widget + for (let i = 0; i < newWidgetList.length; i++) { + const widget = newWidgetList[i]; + + widgets = handleSpecificCasesWhilePasting( + widget, + widgets, + widgetNameMap, + newWidgetList, + ); } } - } else { - // For all other widgets in the list - // (These widgets will be descendants of the copied widget) - // This means, that their parents will also be newly copied widgets - // Update widget's parent widget ids with the new parent widget ids - const newParentId = newWidgetList.find((newWidget) => - widget.parentId - ? newWidget.widgetId === widgetIdMap[widget.parentId] - : false, - )?.widgetId; - if (newParentId) widget.parentId = newParentId; - } - // Generate a new unique widget name - widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree, { - prefix: oldWidgetName, - startWithoutIndex: true, - }); - widgetNameMap[oldWidgetName] = widget.widgetName; - // Add the new widget to the canvas widgets - widgets[widget.widgetId] = widget; - } - - // 1. updating template in the copied widget and deleting old template associations - // 2. updating dynamicBindingPathList in the copied grid widget - for (let i = 0; i < newWidgetList.length; i++) { - const widget = newWidgetList[i]; - - widgets = handleSpecificCasesWhilePasting( - widget, - widgets, - widgetNameMap, - newWidgetList, - ); - } - - // save the new DSL - yield put(updateAndSaveLayout(widgets)); - - // hydrating enhancements map after save layout so that enhancement map - // for newly copied widget is hydrated - - // Flash the newly pasted widget once the DSL is re-rendered + }), + ), + ); + // save the new DSL + yield put(updateAndSaveLayout(widgets)); + newlyCreatedWidgetIds.forEach((newWidgetId) => { setTimeout(() => flashElementById(newWidgetId), 100); - yield put({ - type: ReduxActionTypes.SELECT_WIDGET_INIT, - payload: { widgetId: newWidgetId }, - }); - } + }); + // hydrating enhancements map after save layout so that enhancement map + // for newly copied widget is hydrated + yield put(selectMultipleWidgetsInitAction(newlyCreatedWidgetIds)); } function* cutWidgetSaga() { - const selectedWidget = yield select(getSelectedWidget); - if (!selectedWidget) { + const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select( + getWidgets, + ); + const selectedWidgets: string[] = yield select(getSelectedWidgets); + if (!selectedWidgets) { Toaster.show({ text: createMessage(ERROR_WIDGET_CUT_NO_WIDGET_SELECTED), variant: Variant.info, @@ -1710,17 +1753,26 @@ function* cutWidgetSaga() { return; } - const saveResult = yield createWidgetCopy(); + const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]); - const eventName = "WIDGET_CUT_VIA_SHORTCUT"; // cut only supported through a shortcut - AnalyticsUtil.logEvent(eventName, { - widgetName: selectedWidget.widgetName, - widgetType: selectedWidget.type, + const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps); + + selectedWidgetProps.forEach((each) => { + const eventName = "WIDGET_CUT_VIA_SHORTCUT"; // cut only supported through a shortcut + AnalyticsUtil.logEvent(eventName, { + widgetName: each.widgetName, + widgetType: each.type, + }); }); if (saveResult) { Toaster.show({ - text: createMessage(WIDGET_CUT, selectedWidget.widgetName), + text: createMessage( + WIDGET_CUT, + selectedWidgetProps.length > 1 + ? `${selectedWidgetProps.length} Widgets` + : selectedWidgetProps[0].widgetName, + ), variant: Variant.success, }); } diff --git a/app/client/src/sagas/WidgetOperationUtils.ts b/app/client/src/sagas/WidgetOperationUtils.ts index be4e1da75a..4628ea805a 100644 --- a/app/client/src/sagas/WidgetOperationUtils.ts +++ b/app/client/src/sagas/WidgetOperationUtils.ts @@ -3,10 +3,13 @@ import { WidgetTypes, } from "constants/WidgetConstants"; import { cloneDeep, get, isString, filter, set } from "lodash"; -import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; +import { + CanvasWidgetsReduxState, + FlattenedWidgetProps, +} from "reducers/entityReducers/canvasWidgetsReducer"; import { call, select } from "redux-saga/effects"; import { getDynamicBindings } from "utils/DynamicBindingUtils"; -import { getWidget } from "./selectors"; +import { getWidget, getWidgetMetaProps } from "./selectors"; /** * checks if triggerpaths contains property path passed @@ -224,3 +227,85 @@ export function* getWidgetChildren(widgetId: string): any { } return childrenIds; } + +export const getParentWidgetIdForPasting = function*( + widgets: CanvasWidgetsReduxState, + selectedWidget: FlattenedWidgetProps | undefined, +) { + let newWidgetParentId = MAIN_CONTAINER_WIDGET_ID; + let parentWidget = widgets[MAIN_CONTAINER_WIDGET_ID]; + + // If the selected widget is not the main container + if (selectedWidget && selectedWidget.widgetId !== MAIN_CONTAINER_WIDGET_ID) { + // Select the parent of the selected widget if parent is not + // the main container + if ( + selectedWidget && + selectedWidget.parentId && + selectedWidget.parentId !== MAIN_CONTAINER_WIDGET_ID && + widgets[selectedWidget.parentId] + ) { + const children = widgets[selectedWidget.parentId].children || []; + if (children.length > 0) { + parentWidget = widgets[selectedWidget.parentId]; + newWidgetParentId = selectedWidget.parentId; + } + } + // Select the selected widget if the widget is container like + if (selectedWidget.children) { + parentWidget = widgets[selectedWidget.widgetId]; + } + } + + // If the parent widget in which to paste the copied widget + // is not the main container and is not a canvas widget + if ( + parentWidget.widgetId !== MAIN_CONTAINER_WIDGET_ID && + parentWidget.type !== WidgetTypes.CANVAS_WIDGET + ) { + let childWidget; + // If the widget in which to paste the new widget is NOT + // a tabs widget + if (parentWidget.type !== WidgetTypes.TABS_WIDGET) { + // The child will be a CANVAS_WIDGET, as we've established + // this parent widget to be a container like widget + // Which always has its first child as a canvas widget + childWidget = parentWidget.children && widgets[parentWidget.children[0]]; + } else { + // If the widget in which to paste the new widget is a tabs widget + // Find the currently selected tab canvas widget + const { selectedTabWidgetId } = yield select( + getWidgetMetaProps, + parentWidget.widgetId, + ); + if (selectedTabWidgetId) childWidget = widgets[selectedTabWidgetId]; + } + // If the finally selected parent in which to paste the widget + // is a CANVAS_WIDGET, use its widgetId as the new widget's parent Id + if (childWidget && childWidget.type === WidgetTypes.CANVAS_WIDGET) { + newWidgetParentId = childWidget.widgetId; + } + } + return newWidgetParentId; +}; + +export const checkIfPastingIntoListWidget = function*( + selectedWidget: FlattenedWidgetProps | undefined, +) { + // when list widget is selected, if the user is pasting, we want it to be pasted in the template + // which is first children of list widget + if ( + selectedWidget && + selectedWidget.children && + selectedWidget?.type === WidgetTypes.LIST_WIDGET + ) { + const childrenIds: string[] = yield call( + getWidgetChildren, + selectedWidget.children[0], + ); + const firstChildId = childrenIds[0]; + + selectedWidget = yield select(getWidget, firstChildId); + } + return selectedWidget; +}; diff --git a/app/client/src/sagas/WidgetSelectionSagas.ts b/app/client/src/sagas/WidgetSelectionSagas.ts index 861f9da361..67fe87e16e 100644 --- a/app/client/src/sagas/WidgetSelectionSagas.ts +++ b/app/client/src/sagas/WidgetSelectionSagas.ts @@ -5,15 +5,16 @@ import { getWidgetImmediateChildren, getWidgets } from "./selectors"; import log from "loglevel"; import { deselectMultipleWidgetsAction, - selectAllWidgetsAction, selectMultipleWidgetsAction, selectWidgetAction, selectWidgetInitAction, + silentAddSelectionsAction, } from "actions/widgetSelectionActions"; 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"; // The following is computed to be used in the entity explorer // Every time a widget is selected, we need to expand widget entities @@ -59,13 +60,16 @@ function* selectedWidgetAncestrySaga( } } -function* selectAllWidgetsSaga() { - const allWidgetsOnMainContainer: string[] = yield select( +function* selectAllWidgetsInCanvasSaga( + action: ReduxAction<{ canvasId: string }>, +) { + const { canvasId } = action.payload; + const allWidgetsOnCanvas: string[] = yield select( getWidgetImmediateChildren, - MAIN_CONTAINER_WIDGET_ID, + canvasId, ); - if (allWidgetsOnMainContainer && allWidgetsOnMainContainer.length) { - yield put(selectAllWidgetsAction(allWidgetsOnMainContainer)); + if (allWidgetsOnCanvas && allWidgetsOnCanvas.length) { + yield put(selectMultipleWidgetsAction(allWidgetsOnCanvas)); Toaster.show({ text: createMessage(SELECT_ALL_WIDGETS_MSG), variant: Variant.info, @@ -79,8 +83,8 @@ function* deselectNonSiblingsOfWidgetSaga( ) { const { isMultiSelect, widgetId } = action.payload; if (isMultiSelect) { - const allWidgets = yield select(getWidgets); - const parentId = allWidgets[widgetId].parentId; + const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets); + const parentId: any = allWidgets[widgetId].parentId; const childWidgets: string[] = yield select( getWidgetImmediateChildren, parentId, @@ -127,12 +131,32 @@ function* shiftSelectWidgetsSaga( : lastSelectedWidgetIndex; const unSelectedSiblings = siblingWidgets.slice(start + 1, end); if (unSelectedSiblings && unSelectedSiblings.length) { - yield put(selectMultipleWidgetsAction(unSelectedSiblings)); + yield put(silentAddSelectionsAction(unSelectedSiblings)); } } 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)); + } +} + export function* widgetSelectionSagas() { yield all([ takeLatest( @@ -145,9 +169,13 @@ export function* widgetSelectionSagas() { ReduxActionTypes.SELECT_WIDGET_INIT, deselectNonSiblingsOfWidgetSaga, ), + takeLatest( + ReduxActionTypes.SELECT_ALL_WIDGETS_IN_CANVAS_INIT, + selectAllWidgetsInCanvasSaga, + ), takeLatest( ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT, - selectAllWidgetsSaga, + selectMultipleWidgetsSaga, ), ]); } diff --git a/app/client/src/utils/hooks/useCanvasMinHeightUpdateHook.ts b/app/client/src/utils/hooks/useCanvasMinHeightUpdateHook.ts new file mode 100644 index 0000000000..cdedcbbbed --- /dev/null +++ b/app/client/src/utils/hooks/useCanvasMinHeightUpdateHook.ts @@ -0,0 +1,30 @@ +import { updateWidget } from "actions/pageActions"; +import { WidgetTypes } from "constants/WidgetConstants"; +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { AppState } from "reducers"; +import { getWidget } from "sagas/selectors"; +import { useSelector } from "store"; +import { WidgetOperations } from "widgets/BaseWidget"; + +export const useCanvasMinHeightUpdateHook = ( + widgetId: string, + minHeight = 0, +) => { + const widget = useSelector((state: AppState) => getWidget(state, widgetId)); + const dispatch = useDispatch(); + useEffect(() => { + if ( + widget && + widget.type === WidgetTypes.CANVAS_WIDGET && + widget.minHeight !== minHeight + ) { + dispatch( + updateWidget(WidgetOperations.UPDATE_PROPERTY, widgetId, { + propertyPath: "minHeight", + propertyValue: minHeight, + }), + ); + } + }, [minHeight]); +}; diff --git a/app/client/src/utils/hooks/useWidgetSelection.ts b/app/client/src/utils/hooks/useWidgetSelection.ts index 2be038fe03..2ac6c1d361 100644 --- a/app/client/src/utils/hooks/useWidgetSelection.ts +++ b/app/client/src/utils/hooks/useWidgetSelection.ts @@ -1,7 +1,7 @@ import { useDispatch } from "react-redux"; import { focusWidget } from "actions/widgetActions"; import { - selectAllWidgetsAction, + selectMultipleWidgetsAction, selectWidgetInitAction, shiftSelectWidgetsEntityExplorerInitAction, } from "actions/widgetSelectionActions"; @@ -29,7 +29,7 @@ export const useWidgetSelection = () => { (widgetId?: string) => dispatch(focusWidget(widgetId)), [dispatch], ), - deselectAll: useCallback(() => dispatch(selectAllWidgetsAction([])), [ + deselectAll: useCallback(() => dispatch(selectMultipleWidgetsAction([])), [ dispatch, ]), }; diff --git a/app/client/test/testCommon.tsx b/app/client/test/testCommon.ts similarity index 83% rename from app/client/test/testCommon.tsx rename to app/client/test/testCommon.ts index 203690b059..d1e3c644f1 100644 --- a/app/client/test/testCommon.tsx +++ b/app/client/test/testCommon.ts @@ -10,6 +10,12 @@ import { useDispatch } from "react-redux"; import { extractCurrentDSL } from "utils/WidgetPropsUtils"; import { setAppMode } from "actions/pageActions"; import { APP_MODE } from "reducers/entityReducers/appReducer"; +import { createSelector } from "reselect"; +import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; +import { getCanvasWidgets } from "selectors/entitiesSelector"; +import { ContainerWidgetProps } from "widgets/ContainerWidget"; +import { WidgetProps } from "widgets/BaseWidget"; +import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; export const useMockDsl = (dsl: any) => { const dispatch = useDispatch(); @@ -69,6 +75,17 @@ export function MockPageDSL({ dsl, children }: any) { return children; } +export const mockGetCanvasWidgetDsl = createSelector( + getCanvasWidgets, + ( + canvasWidgets: CanvasWidgetsReduxState, + ): ContainerWidgetProps => { + return CanvasWidgetsNormalizer.denormalize("0", { + canvasWidgets, + }); + }, +); + export const syntheticTestMouseEvent = ( event: MouseEvent, optionsToAdd = {}, diff --git a/app/client/test/testMockedWidgets.tsx b/app/client/test/testMockedWidgets.tsx new file mode 100644 index 0000000000..eea110370e --- /dev/null +++ b/app/client/test/testMockedWidgets.tsx @@ -0,0 +1,9 @@ +import Canvas from "pages/Editor/Canvas"; +import React from "react"; +import { useSelector } from "react-redux"; +import { mockGetCanvasWidgetDsl } from "./testCommon"; + +export const MockCanvas = () => { + const dsl = useSelector(mockGetCanvasWidgetDsl); + return ; +};