From e128b2daf351586fb37d5a81cd79b4abcc2aa9a3 Mon Sep 17 00:00:00 2001
From: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
Date: Wed, 4 May 2022 13:28:57 +0530
Subject: [PATCH] feat: new Widget Copy paste experience (#12906)
* copy paste commit
* class name generator changes
* modal widget fixes change
* addressing review comments
* bug fix for after deleting a widget by undoing action
* additional fix for modal widget
* additional tests for fixes
---
.../cypress/fixtures/WidgetCopyPaste.json | 149 ++++++
.../WidgetCopyPaste/WidgetCopyPaste_spec.js | 108 ++++
app/client/src/actions/widgetActions.tsx | 6 +-
.../appsmith/PositionedContainer.tsx | 4 +-
.../constants/componentClassNameConstants.ts | 13 +
.../GlobalHotKeys.test.tsx | 64 ++-
.../{ => GlobalHotKeys}/GlobalHotKeys.tsx | 10 +-
.../src/pages/Editor/GlobalHotKeys/index.tsx | 16 +
.../Editor/GlobalHotKeys/useMouseLocation.tsx | 26 +
.../GuidedTour/useComputeCurrentStep.ts | 5 +-
.../pages/Editor/WidgetsMultiSelectBox.tsx | 3 +-
.../CanvasArenas/CanvasSelectionArena.tsx | 8 +-
app/client/src/sagas/WidgetOperationSagas.tsx | 425 ++++++++++++++-
.../src/sagas/WidgetOperationUtils.test.ts | 356 +++++++++++++
app/client/src/sagas/WidgetOperationUtils.ts | 491 +++++++++++++++++-
app/client/src/utils/generators.tsx | 3 +-
.../BaseInputWidget/component/index.tsx | 5 +-
.../widgets/InputWidget/component/index.tsx | 7 +-
.../widgets/ModalWidget/component/index.tsx | 3 +
.../src/widgets/ModalWidget/widget/index.tsx | 1 +
20 files changed, 1661 insertions(+), 42 deletions(-)
create mode 100644 app/client/cypress/fixtures/WidgetCopyPaste.json
create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/WidgetCopyPaste/WidgetCopyPaste_spec.js
create mode 100644 app/client/src/constants/componentClassNameConstants.ts
rename app/client/src/pages/Editor/{ => GlobalHotKeys}/GlobalHotKeys.test.tsx (92%)
rename app/client/src/pages/Editor/{ => GlobalHotKeys}/GlobalHotKeys.tsx (97%)
create mode 100644 app/client/src/pages/Editor/GlobalHotKeys/index.tsx
create mode 100644 app/client/src/pages/Editor/GlobalHotKeys/useMouseLocation.tsx
diff --git a/app/client/cypress/fixtures/WidgetCopyPaste.json b/app/client/cypress/fixtures/WidgetCopyPaste.json
new file mode 100644
index 0000000000..50569b707b
--- /dev/null
+++ b/app/client/cypress/fixtures/WidgetCopyPaste.json
@@ -0,0 +1,149 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 1936,
+ "snapColumns": 64,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 1160,
+ "containerStyle": "none",
+ "snapRows": 116,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 54,
+ "minHeight": 1170,
+ "parentColumnSpace": 1,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "boxShadow": "NONE",
+ "widgetName": "Container1",
+ "borderColor": "transparent",
+ "isCanvas": true,
+ "displayName": "Container",
+ "iconSVG": "/static/media/icon.1977dca3.svg",
+ "topRow": 18,
+ "bottomRow": 58,
+ "parentRowSpace": 10,
+ "type": "CONTAINER_WIDGET",
+ "hideCard": false,
+ "animateLoading": true,
+ "parentColumnSpace": 30.0625,
+ "leftColumn": 6,
+ "children": [
+ {
+ "widgetName": "Canvas1",
+ "rightColumn": 721.5,
+ "detachFromLayout": true,
+ "displayName": "Canvas",
+ "widgetId": "79a7avach5",
+ "containerStyle": "none",
+ "topRow": 0,
+ "bottomRow": 400,
+ "parentRowSpace": 1,
+ "isVisible": true,
+ "type": "CANVAS_WIDGET",
+ "canExtend": false,
+ "version": 1,
+ "hideCard": true,
+ "parentId": "drqlbbf2jm",
+ "minHeight": 400,
+ "renderMode": "CANVAS",
+ "isLoading": false,
+ "parentColumnSpace": 1,
+ "leftColumn": 0,
+ "children": [],
+ "key": "wv7g2n64td"
+ }
+ ],
+ "borderWidth": "0",
+ "key": "t9ac12itzf",
+ "backgroundColor": "#FFFFFF",
+ "rightColumn": 30,
+ "widgetId": "drqlbbf2jm",
+ "containerStyle": "card",
+ "isVisible": true,
+ "version": 1,
+ "parentId": "0",
+ "renderMode": "CANVAS",
+ "isLoading": false,
+ "borderRadius": "0"
+ },
+ {
+ "widgetName": "Chart1",
+ "allowScroll": false,
+ "displayName": "Chart",
+ "iconSVG": "/static/media/icon.6adbe31e.svg",
+ "topRow": 20,
+ "bottomRow": 52,
+ "parentRowSpace": 10,
+ "type": "CHART_WIDGET",
+ "hideCard": false,
+ "chartData": {
+ "0jqgz3wqpx": {
+ "seriesName": "Sales",
+ "data": [
+ {
+ "x": "Product1",
+ "y": 20000
+ },
+ {
+ "x": "Product2",
+ "y": 22000
+ },
+ {
+ "x": "Product3",
+ "y": 32000
+ }
+ ]
+ }
+ },
+ "animateLoading": true,
+ "parentColumnSpace": 30.0625,
+ "leftColumn": 35,
+ "customFusionChartConfig": {
+ "type": "column2d",
+ "dataSource": {
+ "chart": {
+ "caption": "Sales Report",
+ "xAxisName": "Product Line",
+ "yAxisName": "Revenue($)",
+ "theme": "fusion"
+ },
+ "data": [
+ {
+ "label": "Product1",
+ "value": 20000
+ },
+ {
+ "label": "Product2",
+ "value": 22000
+ },
+ {
+ "label": "Product3",
+ "value": 32000
+ }
+ ]
+ }
+ },
+ "key": "5dh7y0hcpk",
+ "rightColumn": 59,
+ "widgetId": "mxhtzoaizs",
+ "isVisible": true,
+ "version": 1,
+ "parentId": "0",
+ "labelOrientation": "auto",
+ "renderMode": "CANVAS",
+ "isLoading": false,
+ "yAxisName": "Revenue($)",
+ "chartName": "Sales Report",
+ "xAxisName": "Product Line",
+ "chartType": "COLUMN_CHART"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/WidgetCopyPaste/WidgetCopyPaste_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/WidgetCopyPaste/WidgetCopyPaste_spec.js
new file mode 100644
index 0000000000..5234504354
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/WidgetCopyPaste/WidgetCopyPaste_spec.js
@@ -0,0 +1,108 @@
+const widgetsPage = require("../../../../locators/Widgets.json");
+const commonLocators = require("../../../../locators/commonlocators.json");
+const explorer = require("../../../../locators/explorerlocators.json");
+const dsl = require("../../../../fixtures/WidgetCopyPaste.json");
+
+describe("Widget Copy paste", function() {
+ const modifierKey = Cypress.platform === "darwin" ? "meta" : "ctrl";
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ it("when non Layout widget is selected, it should place below the widget selected", function() {
+ // Selection
+ cy.get(`#${dsl.dsl.children[1].widgetId}`).click({
+ ctrlKey: true,
+ });
+ cy.get(`div[data-testid='t--selected']`).should("have.length", 1);
+
+ //copy
+ cy.get("body").type(`{${modifierKey}}{c}`);
+ cy.get(commonLocators.toastmsg).contains("Copied");
+
+ //paste
+ cy.get("body").type(`{${modifierKey}}{v}`);
+ cy.get(widgetsPage.chartWidget).should("have.length", 2);
+
+ // verify the position
+ cy.get(widgetsPage.chartWidget)
+ .eq(0)
+ .then((element) => {
+ const elementTop = parseFloat(element.css("top"));
+ const elementHeight = parseFloat(element.css("height"));
+ const pastedWidgetTop =
+ (elementTop + elementHeight + 10).toString() + "px";
+ cy.get(widgetsPage.chartWidget)
+ .eq(1)
+ .invoke("attr", "style")
+ .should("contain", `left: ${element.css("left")}`)
+ .should("contain", `top: ${pastedWidgetTop}`);
+ });
+ });
+
+ it("when Layout widget is selected, it should place it inside the layout widget", function() {
+ cy.get(`#div-selection-0`).click({
+ force: true,
+ });
+
+ // Selection
+ cy.get(`#${dsl.dsl.children[0].widgetId}`).click({
+ ctrlKey: true,
+ });
+ cy.get(`div[data-testid='t--selected']`).should("have.length", 1);
+
+ //paste
+ cy.get("body").type(`{${modifierKey}}{v}`);
+
+ cy.get(`#${dsl.dsl.children[0].widgetId}`)
+ .find(widgetsPage.chartWidget)
+ .should("have.length", 1);
+ });
+
+ it("when widget inside the layout widget is selected, then it should paste inside the layout widget below the selected widget", function() {
+ cy.get(`#div-selection-0`).click({
+ force: true,
+ });
+
+ // Selection
+ cy.get(`#${dsl.dsl.children[0].widgetId}`)
+ .find(widgetsPage.chartWidget)
+ .click({
+ ctrlKey: true,
+ });
+ cy.get(`div[data-testid='t--selected']`).should("have.length", 1);
+
+ //paste
+ cy.get("body").type(`{${modifierKey}}{v}`);
+ cy.get(`#${dsl.dsl.children[0].widgetId}`)
+ .find(widgetsPage.chartWidget)
+ .should("have.length", 2);
+ });
+
+ it("when modal is open, it should paste inside the modal", () => {
+ //add modal widget
+ cy.get(explorer.addWidget).click();
+ cy.dragAndDropToCanvas("modalwidget", { x: 300, y: 700 });
+ cy.get(".t--modal-widget").should("exist");
+
+ //paste
+ cy.get("body").type(`{${modifierKey}}{v}`);
+ cy.get(".t--modal-widget")
+ .find(widgetsPage.chartWidget)
+ .should("have.length", 1);
+ });
+
+ it("when widget Inside a modal is selected, it should paste inside the modal", () => {
+ //verify modal and selected widget
+ cy.get(".t--modal-widget").should("exist");
+ cy.get(".t--modal-widget")
+ .find(`div[data-testid='t--selected']`)
+ .should("have.length", 1);
+
+ //paste
+ cy.get("body").type(`{${modifierKey}}{v}`);
+ cy.get(".t--modal-widget")
+ .find(widgetsPage.chartWidget)
+ .should("have.length", 2);
+ });
+});
diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx
index 1855f1189b..db4609c3d5 100644
--- a/app/client/src/actions/widgetActions.tsx
+++ b/app/client/src/actions/widgetActions.tsx
@@ -103,11 +103,15 @@ export const copyWidget = (isShortcut: boolean) => {
};
};
-export const pasteWidget = (groupWidgets = false) => {
+export const pasteWidget = (
+ groupWidgets = false,
+ mouseLocation: { x: number; y: number },
+) => {
return {
type: ReduxActionTypes.PASTE_COPIED_WIDGET_INIT,
payload: {
groupWidgets: groupWidgets,
+ mouseLocation,
},
};
};
diff --git a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx
index acffc2b0af..fd67df21aa 100644
--- a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx
+++ b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx
@@ -11,6 +11,7 @@ import WidgetFactory from "utils/WidgetFactory";
import { isEqual, memoize } from "lodash";
import { getReflowSelector } from "selectors/widgetReflowSelectors";
import { AppState } from "reducers";
+import { POSITIONED_WIDGET } from "constants/componentClassNameConstants";
const PositionedWidget = styled.div<{ zIndexOnHover: number }>`
&:hover {
@@ -44,8 +45,7 @@ export function PositionedContainer(props: PositionedContainerProps) {
const containerClassName = useMemo(() => {
return (
generateClassName(props.widgetId) +
- " positioned-widget " +
- `t--widget-${props.widgetType
+ ` ${POSITIONED_WIDGET} t--widget-${props.widgetType
.split("_")
.join("")
.toLowerCase()}`
diff --git a/app/client/src/constants/componentClassNameConstants.ts b/app/client/src/constants/componentClassNameConstants.ts
new file mode 100644
index 0000000000..fb79f40505
--- /dev/null
+++ b/app/client/src/constants/componentClassNameConstants.ts
@@ -0,0 +1,13 @@
+export function getStickyCanvasName(widgetId: string) {
+ return `div-selection-${widgetId}`;
+}
+
+export function getSlidingCanvasName(widgetId: string) {
+ return `canvas-selection-${widgetId}`;
+}
+
+export function getBaseWidgetClassName(id?: string) {
+ return `appsmith_widget_${id}`;
+}
+
+export const POSITIONED_WIDGET = "positioned-widget";
diff --git a/app/client/src/pages/Editor/GlobalHotKeys.test.tsx b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.test.tsx
similarity index 92%
rename from app/client/src/pages/Editor/GlobalHotKeys.test.tsx
rename to app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.test.tsx
index 345fb07217..3c399a951f 100644
--- a/app/client/src/pages/Editor/GlobalHotKeys.test.tsx
+++ b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.test.tsx
@@ -7,7 +7,7 @@ import {
} from "test/factories/WidgetFactoryUtils";
import { act, render, fireEvent, waitFor } from "test/testUtils";
import GlobalHotKeys from "./GlobalHotKeys";
-import MainContainer from "./MainContainer";
+import MainContainer from "../MainContainer";
import { MemoryRouter } from "react-router-dom";
import * as utilities from "selectors/editorSelectors";
import store from "store";
@@ -56,7 +56,7 @@ describe("Canvas Hot Keys", () => {
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.
+ // only the default 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,
@@ -84,7 +84,11 @@ describe("Canvas Hot Keys", () => {
initialEntries={["/app/applicationSlug/pageSlug-page_id/edit"]}
>
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
@@ -204,7 +208,11 @@ describe("Canvas Hot Keys", () => {
initialEntries={["/app/applicationSlug/pageSlug-page_id/edit"]}
>
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
@@ -246,7 +254,11 @@ describe("Canvas Hot Keys", () => {
initialEntries={["/app/applicationSlug/pageSlug-page_id/edit"]}
>
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
@@ -331,7 +343,11 @@ describe("Canvas Hot Keys", () => {
initialEntries={["/app/applicationSlug/pageSlug-page_id/edit"]}
>
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
@@ -388,7 +404,11 @@ describe("Cut/Copy/Paste hotkey", () => {
});
const component = render(
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
,
@@ -469,7 +489,11 @@ describe("Cut/Copy/Paste hotkey", () => {
});
const component = render(
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
,
@@ -540,7 +564,11 @@ describe("Undo/Redo hotkey", () => {
const dispatchSpy = jest.spyOn(store, "dispatch");
const component = render(
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
,
@@ -566,7 +594,11 @@ describe("Undo/Redo hotkey", () => {
const dispatchSpy = jest.spyOn(store, "dispatch");
const component = render(
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
,
@@ -592,7 +624,11 @@ describe("Undo/Redo hotkey", () => {
const dispatchSpy = jest.spyOn(store, "dispatch");
const component = render(
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
,
@@ -628,7 +664,11 @@ describe("cmd + s hotkey", () => {
pauseOnHover={false}
transition={Slide}
/>
-
+ {
+ return { x: 0, y: 0 };
+ }}
+ >
>,
diff --git a/app/client/src/pages/Editor/GlobalHotKeys.tsx b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.tsx
similarity index 97%
rename from app/client/src/pages/Editor/GlobalHotKeys.tsx
rename to app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.tsx
index c873597ee0..1445839b61 100644
--- a/app/client/src/pages/Editor/GlobalHotKeys.tsx
+++ b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.tsx
@@ -55,7 +55,7 @@ import { commentModeSelector } from "selectors/commentsSelectors";
type Props = {
copySelectedWidget: () => void;
- pasteCopiedWidget: () => void;
+ pasteCopiedWidget: (mouseLocation: { x: number; y: number }) => void;
deleteSelectedWidget: () => void;
cutSelectedWidget: () => void;
groupSelectedWidget: () => void;
@@ -80,6 +80,7 @@ type Props = {
isExplorerPinned: boolean;
setExplorerPinnedAction: (shouldPinned: boolean) => void;
showCommitModal: () => void;
+ getMousePosition: () => { x: number; y: number };
};
@HotkeysTarget
@@ -215,7 +216,9 @@ class GlobalHotKeys extends React.Component {
group="Canvas"
label="Paste Widget"
onKeyDown={() => {
- this.props.pasteCopiedWidget();
+ this.props.pasteCopiedWidget(
+ this.props.getMousePosition() || { x: 0, y: 0 },
+ );
}}
/>
({
const mapDispatchToProps = (dispatch: any) => {
return {
copySelectedWidget: () => dispatch(copyWidget(true)),
- pasteCopiedWidget: () => dispatch(pasteWidget()),
+ pasteCopiedWidget: (mouseLocation: { x: number; y: number }) =>
+ dispatch(pasteWidget(false, mouseLocation)),
deleteSelectedWidget: () => dispatch(deleteSelectedWidget(true)),
cutSelectedWidget: () => dispatch(cutWidget()),
groupSelectedWidget: () => dispatch(groupWidgets()),
diff --git a/app/client/src/pages/Editor/GlobalHotKeys/index.tsx b/app/client/src/pages/Editor/GlobalHotKeys/index.tsx
new file mode 100644
index 0000000000..4685d4240b
--- /dev/null
+++ b/app/client/src/pages/Editor/GlobalHotKeys/index.tsx
@@ -0,0 +1,16 @@
+import GlobalHotKeys from "./GlobalHotKeys";
+import React from "react";
+import { useMouseLocation } from "./useMouseLocation";
+
+//HOC to track user's mouse location, separated out so that it doesn't render the component on every mouse move
+function HotKeysHOC(props: any) {
+ const getMousePosition = useMouseLocation();
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+export default HotKeysHOC;
diff --git a/app/client/src/pages/Editor/GlobalHotKeys/useMouseLocation.tsx b/app/client/src/pages/Editor/GlobalHotKeys/useMouseLocation.tsx
new file mode 100644
index 0000000000..a9a4015230
--- /dev/null
+++ b/app/client/src/pages/Editor/GlobalHotKeys/useMouseLocation.tsx
@@ -0,0 +1,26 @@
+import { useEffect, useRef } from "react";
+
+export const useMouseLocation = () => {
+ const mousePosition = useRef<{ x: number; y: number }>({
+ x: 0,
+ y: 0,
+ });
+
+ const setMousePosition = (e: any) => {
+ if (e) {
+ mousePosition.current = { x: e.clientX, y: e.clientY };
+ }
+ };
+
+ useEffect(() => {
+ window.addEventListener("mousemove", setMousePosition);
+
+ () => {
+ window.removeEventListener("mousemove", setMousePosition);
+ };
+ }, []);
+
+ return function() {
+ return mousePosition.current;
+ };
+};
diff --git a/app/client/src/pages/Editor/GuidedTour/useComputeCurrentStep.ts b/app/client/src/pages/Editor/GuidedTour/useComputeCurrentStep.ts
index df58874a93..8cf03e0b15 100644
--- a/app/client/src/pages/Editor/GuidedTour/useComputeCurrentStep.ts
+++ b/app/client/src/pages/Editor/GuidedTour/useComputeCurrentStep.ts
@@ -28,6 +28,7 @@ import {
countryInputSelector,
imageWidgetSelector,
} from "selectors/onboardingSelectors";
+import { getBaseWidgetClassName } from "constants/componentClassNameConstants";
import { GUIDED_TOUR_STEPS, Steps } from "./constants";
import { hideIndicator, highlightSection, showIndicator } from "./utils";
@@ -230,11 +231,11 @@ function useComputeCurrentStep(showInfoMessage: boolean) {
// Highlight the selected row and the NameInput widget
highlightSection(
"selected-row",
- `appsmith_widget_${isTableWidgetBound}`,
+ getBaseWidgetClassName(isTableWidgetBound),
"class",
);
highlightSection(
- `appsmith_widget_${nameInputWidgetId}`,
+ getBaseWidgetClassName(nameInputWidgetId),
undefined,
"class",
);
diff --git a/app/client/src/pages/Editor/WidgetsMultiSelectBox.tsx b/app/client/src/pages/Editor/WidgetsMultiSelectBox.tsx
index 454f109a70..c3e02fcde1 100644
--- a/app/client/src/pages/Editor/WidgetsMultiSelectBox.tsx
+++ b/app/client/src/pages/Editor/WidgetsMultiSelectBox.tsx
@@ -25,6 +25,7 @@ import WidgetFactory from "utils/WidgetFactory";
import { AppState } from "reducers";
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
import { commentModeSelector } from "selectors/commentsSelectors";
+import { POSITIONED_WIDGET } from "constants/componentClassNameConstants";
const WidgetTypes = WidgetFactory.widgetTypes;
const StyledSelectionBox = styled.div`
@@ -239,7 +240,7 @@ function WidgetsMultiSelectBox(props: {
const { height, left, top, width } = useMemo(() => {
if (shouldRender) {
const widgetClasses = selectedWidgetIDs
- .map((id) => `.${generateClassName(id)}.positioned-widget`)
+ .map((id) => `.${generateClassName(id)}.${POSITIONED_WIDGET}`)
.join(",");
const elements = document.querySelectorAll(widgetClasses);
diff --git a/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx b/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx
index ae129d1f6e..37d74407f0 100644
--- a/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx
+++ b/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.tsx
@@ -23,6 +23,10 @@ import { getIsDraggingForSelection } from "selectors/canvasSelectors";
import { commentModeSelector } from "selectors/commentsSelectors";
import { StickyCanvasArena } from "./StickyCanvasArena";
import { getAbsolutePixels } from "utils/helpers";
+import {
+ getSlidingCanvasName,
+ getStickyCanvasName,
+} from "constants/componentClassNameConstants";
export interface SelectedArenaDimensions {
top: number;
@@ -482,10 +486,10 @@ export function CanvasSelectionArena({
return shouldShow ? (
) {
try {
@@ -739,7 +766,10 @@ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
* @param parentId
* @param canvasWidgets
* @param parentBottomRow
- * @param persistColumnPosition
+ * @param newPastingPositionMap
+ * @param shouldPersistColumnPosition
+ * @param isThereACollision
+ * @param shouldGroup
* @returns
*/
export function calculateNewWidgetPosition(
@@ -747,6 +777,7 @@ export function calculateNewWidgetPosition(
parentId: string,
canvasWidgets: { [widgetId: string]: FlattenedWidgetProps },
parentBottomRow?: number,
+ newPastingPositionMap?: SpaceMap,
shouldPersistColumnPosition = false,
isThereACollision = false,
shouldGroup = false,
@@ -756,6 +787,20 @@ export function calculateNewWidgetPosition(
leftColumn: number;
rightColumn: number;
} {
+ if (
+ !shouldGroup &&
+ newPastingPositionMap &&
+ newPastingPositionMap[widget.widgetId]
+ ) {
+ const newPastingPosition = newPastingPositionMap[widget.widgetId];
+ return {
+ topRow: newPastingPosition.top,
+ bottomRow: newPastingPosition.bottom,
+ leftColumn: newPastingPosition.left,
+ rightColumn: newPastingPosition.right,
+ };
+ }
+
const nextAvailableRow = parentBottomRow
? parentBottomRow
: nextAvailableRowInContainer(parentId, canvasWidgets);
@@ -779,10 +824,335 @@ export function calculateNewWidgetPosition(
};
}
+/**
+ * Method to provide the new positions where the widgets can be pasted.
+ * It will return an empty object if it doesn't have any selected widgets, or if the mouse is outside the canvas.
+ *
+ * @param copiedWidgetGroups Contains information on the copied widgets
+ * @param mouseLocation location of the mouse in absolute pixels
+ * @param copiedTotalWidth total width of the copied widgets
+ * @param copiedTopMostRow top row of the top most copied widget
+ * @param copiedLeftMostColumn left column of the left most copied widget
+ * @returns
+ */
+const getNewPositions = function*(
+ copiedWidgetGroups: CopiedWidgetGroup[],
+ mouseLocation: { x: number; y: number },
+ copiedTotalWidth: number,
+ copiedTopMostRow: number,
+ copiedLeftMostColumn: number,
+) {
+ const selectedWidgetIDs: string[] = yield select(getSelectedWidgets);
+ const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
+ const selectedWidgets = getWidgetsFromIds(selectedWidgetIDs, canvasWidgets);
+
+ //if the copied widget is a modal widget, then it has to paste on the main container
+ if (
+ copiedWidgetGroups.length === 1 &&
+ copiedWidgetGroups[0].list[0] &&
+ copiedWidgetGroups[0].list[0].type === "MODAL_WIDGET"
+ )
+ return {};
+
+ //if multiple widgets are selected or if a single non-layout widget is selected,
+ // then call the method to calculate and return positions based on selected widgets.
+ if (
+ !(
+ selectedWidgets.length === 1 &&
+ isDropTarget(selectedWidgets[0].type, true)
+ ) &&
+ selectedWidgets.length > 0
+ ) {
+ const newPastingPositionDetails: NewPastePositionVariables = yield call(
+ getNewPositionsBasedOnSelectedWidgets,
+ copiedWidgetGroups,
+ selectedWidgets,
+ canvasWidgets,
+ copiedTotalWidth,
+ copiedTopMostRow,
+ copiedLeftMostColumn,
+ );
+ return newPastingPositionDetails;
+ }
+
+ //if a layout widget is selected or mouse is on the main canvas
+ // then call the method to calculate and return positions mouse positions.
+ const newPastingPositionDetails: NewPastePositionVariables = yield call(
+ getNewPositionsBasedOnMousePositions,
+ copiedWidgetGroups,
+ mouseLocation,
+ selectedWidgets,
+ canvasWidgets,
+ copiedTotalWidth,
+ copiedTopMostRow,
+ copiedLeftMostColumn,
+ );
+ return newPastingPositionDetails;
+};
+
+/**
+ * Calculates the new positions of the pasting widgets, based on the selected widgets
+ * The new positions will be just below the selected widgets
+ *
+ * @param copiedWidgetGroups Contains information on the copied widgets
+ * @param selectedWidgets array of selected widgets
+ * @param canvasWidgets canvas widgets from the DSL
+ * @param copiedTotalWidth total width of the copied widgets
+ * @param copiedTopMostRow top row of the top most copied widget
+ * @param copiedLeftMostColumn left column of the left most copied widget
+ * @returns
+ */
+function* getNewPositionsBasedOnSelectedWidgets(
+ copiedWidgetGroups: CopiedWidgetGroup[],
+ selectedWidgets: WidgetProps[],
+ canvasWidgets: CanvasWidgetsReduxState,
+ copiedTotalWidth: number,
+ copiedTopMostRow: number,
+ copiedLeftMostColumn: number,
+) {
+ //get Parent canvasId
+ const parentId = selectedWidgets[0].parentId || "";
+
+ // get the Id of the container widget based on the canvasId
+ const containerId = getContainerIdForCanvas(parentId);
+
+ const containerWidget = canvasWidgets[containerId];
+ const canvasDOM = document.querySelector(
+ `#${getSlidingCanvasName(parentId)}`,
+ );
+
+ if (!canvasDOM || !containerWidget) return {};
+
+ const rect = canvasDOM.getBoundingClientRect();
+
+ // get Grid values such as snapRowSpace and snapColumnSpace
+ const { snapGrid } = getSnappedGrid(containerWidget, rect.width);
+
+ const selectedWidgetsArray = selectedWidgets.length ? selectedWidgets : [];
+ //from selected widgets get some information required for position calculation
+ const {
+ leftMostColumn: selectedLeftMostColumn,
+ maxThickness,
+ topMostRow: selectedTopMostRow,
+ totalWidth,
+ } = getBoundariesFromSelectedWidgets(selectedWidgetsArray);
+
+ // calculation of left most column of where widgets are to be pasted
+ let pasteLeftMostColumn =
+ selectedLeftMostColumn - (copiedTotalWidth - totalWidth) / 2;
+
+ pasteLeftMostColumn = Math.round(pasteLeftMostColumn);
+
+ // conditions to adjust to the edges of the boundary, so that it doesn't go out of canvas
+ if (pasteLeftMostColumn < 0) pasteLeftMostColumn = 0;
+ if (
+ pasteLeftMostColumn + copiedTotalWidth >
+ GridDefaults.DEFAULT_GRID_COLUMNS
+ )
+ pasteLeftMostColumn = GridDefaults.DEFAULT_GRID_COLUMNS - copiedTotalWidth;
+
+ // based on the above calculation get the new Positions that are aligned to the top left of selected widgets
+ // i.e., the top of the selected widgets will be equal to the top of copied widgets and both are horizontally centered
+ const newPositionsForCopiedWidgets = getNewPositionsForCopiedWidgets(
+ copiedWidgetGroups,
+ copiedTopMostRow,
+ selectedTopMostRow,
+ copiedLeftMostColumn,
+ pasteLeftMostColumn,
+ );
+
+ // with the new positions, calculate the map of new position, which are moved down to the point where
+ // it doesn't overlap with any of the selected widgets.
+ const newPastingPositionMap = getVerticallyAdjustedPositions(
+ newPositionsForCopiedWidgets,
+ getOccupiedSpacesFromProps(selectedWidgetsArray),
+ maxThickness,
+ );
+
+ if (!newPastingPositionMap) return {};
+
+ const gridProps = {
+ parentColumnSpace: snapGrid.snapColumnSpace,
+ parentRowSpace: snapGrid.snapRowSpace,
+ maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
+ };
+
+ const reflowSpacesSelector = getWidgetSpacesSelectorForContainer(parentId);
+ const widgetSpaces: WidgetSpace[] = yield select(reflowSpacesSelector) || [];
+
+ // Ids of each pasting are changed just for reflow
+ const newPastePositions = changeIdsOfPastePositions(newPastingPositionMap);
+
+ const { movementMap: reflowedMovementMap } = reflow(
+ newPastePositions,
+ newPastePositions,
+ widgetSpaces,
+ ReflowDirection.BOTTOM,
+ gridProps,
+ true,
+ false,
+ { prevSpacesMap: {} } as PrevReflowState,
+ );
+
+ // calculate the new bottom most row of the canvas
+ const bottomMostRow = getBottomRowAfterReflow(
+ reflowedMovementMap,
+ getBottomMostRow(newPastePositions),
+ widgetSpaces,
+ gridProps,
+ );
+
+ return {
+ bottomMostRow:
+ (bottomMostRow + GridDefaults.CANVAS_EXTENSION_OFFSET) *
+ gridProps.parentRowSpace,
+ gridProps,
+ newPastingPositionMap,
+ reflowedMovementMap,
+ canvasId: parentId,
+ };
+}
+
+/**
+ * Calculates the new positions of the pasting widgets, based on the mouse position
+ * If the mouse position is on the canvas it the top left of the new positions aligns itself to the mouse position
+ * returns a empty object if the mouse is out of canvas
+ *
+ * @param copiedWidgetGroups Contains information on the copied widgets
+ * @param mouseLocation location of the mouse in absolute pixels
+ * @param selectedWidgets array of selected widgets
+ * @param canvasWidgets canvas widgets from the DSL
+ * @param copiedTotalWidth total width of the copied widgets
+ * @param copiedTopMostRow top row of the top most copied widget
+ * @param copiedLeftMostColumn left column of the left most copied widget
+ * @returns
+ */
+function* getNewPositionsBasedOnMousePositions(
+ copiedWidgetGroups: CopiedWidgetGroup[],
+ mouseLocation: { x: number; y: number },
+ selectedWidgets: WidgetProps[],
+ canvasWidgets: CanvasWidgetsReduxState,
+ copiedTotalWidth: number,
+ copiedTopMostRow: number,
+ copiedLeftMostColumn: number,
+) {
+ let { canvasDOM, canvasId, containerWidget } = getDefaultCanvas(
+ canvasWidgets,
+ );
+
+ //if the selected widget is a layout widget then change the pasting canvas.
+ if (selectedWidgets.length === 1 && isDropTarget(selectedWidgets[0].type)) {
+ containerWidget = selectedWidgets[0];
+ ({ canvasDOM, canvasId } = getCanvasIdForContainer(containerWidget));
+ }
+
+ if (!canvasDOM || !containerWidget || !canvasId) return {};
+
+ const canvasRect = canvasDOM.getBoundingClientRect();
+
+ // get Grid values such as snapRowSpace and snapColumnSpace
+ const { padding, snapGrid } = getSnappedGrid(
+ containerWidget,
+ canvasRect.width,
+ );
+
+ // get mouse positions in terms of grid rows and columns of the pasting canvas
+ const mousePositions = getMousePositions(
+ canvasRect,
+ canvasId,
+ snapGrid,
+ padding,
+ mouseLocation,
+ );
+
+ if (!snapGrid || !mousePositions) return {};
+
+ const reflowSpacesSelector = getWidgetSpacesSelectorForContainer(canvasId);
+ const widgetSpaces: WidgetSpace[] = yield select(reflowSpacesSelector) || [];
+
+ let mouseTopRow = mousePositions.top;
+ let mouseLeftColumn = mousePositions.left;
+
+ // if the mouse position is on another widget on the canvas, then new positions are below it.
+ for (const widgetSpace of widgetSpaces) {
+ if (
+ widgetSpace.top < mousePositions.top &&
+ widgetSpace.left < mousePositions.left &&
+ widgetSpace.bottom > mousePositions.top &&
+ widgetSpace.right > mousePositions.left
+ ) {
+ mouseTopRow = widgetSpace.bottom + WIDGET_PASTE_PADDING;
+ mouseLeftColumn =
+ widgetSpace.left -
+ (copiedTotalWidth - (widgetSpace.right - widgetSpace.left)) / 2;
+ break;
+ }
+ }
+
+ mouseLeftColumn = Math.round(mouseLeftColumn);
+
+ // adjust the top left based on the edges of the canvas
+ if (mouseLeftColumn < 0) mouseLeftColumn = 0;
+ if (mouseLeftColumn + copiedTotalWidth > GridDefaults.DEFAULT_GRID_COLUMNS)
+ mouseLeftColumn = GridDefaults.DEFAULT_GRID_COLUMNS - copiedTotalWidth;
+
+ // get the new Pasting positions of the widgets based on the adjusted mouse top-left
+ const newPastingPositionMap = getPastePositionMapFromMousePointer(
+ copiedWidgetGroups,
+ copiedTopMostRow,
+ mouseTopRow,
+ copiedLeftMostColumn,
+ mouseLeftColumn,
+ );
+
+ const gridProps = {
+ parentColumnSpace: snapGrid.snapColumnSpace,
+ parentRowSpace: snapGrid.snapRowSpace,
+ maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
+ };
+
+ // Ids of each pasting are changed just for reflow
+ const newPastePositions = changeIdsOfPastePositions(newPastingPositionMap);
+
+ const { movementMap: reflowedMovementMap } = reflow(
+ newPastePositions,
+ newPastePositions,
+ widgetSpaces,
+ ReflowDirection.BOTTOM,
+ gridProps,
+ true,
+ false,
+ { prevSpacesMap: {} } as PrevReflowState,
+ );
+
+ // calculate the new bottom most row of the canvas.
+ const bottomMostRow = getBottomRowAfterReflow(
+ reflowedMovementMap,
+ getBottomMostRow(newPastePositions),
+ widgetSpaces,
+ gridProps,
+ );
+
+ return {
+ bottomMostRow:
+ (bottomMostRow + GridDefaults.CANVAS_EXTENSION_OFFSET) *
+ gridProps.parentRowSpace,
+ gridProps,
+ newPastingPositionMap,
+ reflowedMovementMap,
+ canvasId,
+ };
+}
+
/**
* this saga create a new widget from the copied one to store
*/
-function* pasteWidgetSaga(action: ReduxAction<{ groupWidgets: boolean }>) {
+function* pasteWidgetSaga(
+ action: ReduxAction<{
+ groupWidgets: boolean;
+ mouseLocation: { x: number; y: number };
+ }>,
+) {
let copiedWidgetGroups: CopiedWidgetGroup[] = yield getCopiedWidgets();
const shouldGroup: boolean = action.payload.groupWidgets;
@@ -836,14 +1206,36 @@ function* pasteWidgetSaga(action: ReduxAction<{ groupWidgets: boolean }>) {
)
return;
- const { topMostWidget } = getBoundaryWidgetsFromCopiedGroups(
- copiedWidgetGroups,
- );
+ const {
+ leftMostWidget,
+ topMostWidget,
+ totalWidth: copiedTotalWidth,
+ } = getBoundaryWidgetsFromCopiedGroups(copiedWidgetGroups);
+
const nextAvailableRow: number = nextAvailableRowInContainer(
pastingIntoWidgetId,
widgets,
);
+ // new pasting positions, the variables are undefined if the positions cannot be calculated,
+ // then it pastes the regular way at the bottom of the canvas
+ const {
+ bottomMostRow,
+ canvasId,
+ gridProps,
+ newPastingPositionMap,
+ reflowedMovementMap,
+ }: NewPastePositionVariables = yield call(
+ getNewPositions,
+ copiedWidgetGroups,
+ action.payload.mouseLocation,
+ copiedTotalWidth,
+ topMostWidget.topRow,
+ leftMostWidget.leftColumn,
+ );
+
+ if (canvasId) pastingIntoWidgetId = canvasId;
+
yield all(
copiedWidgetGroups.map((copiedWidgets) =>
call(function*() {
@@ -883,6 +1275,7 @@ function* pasteWidgetSaga(action: ReduxAction<{ groupWidgets: boolean }>) {
pastingIntoWidgetId,
widgets,
nextAvailableRow,
+ newPastingPositionMap,
true,
isThereACollision,
shouldGroup,
@@ -1002,7 +1395,7 @@ function* pasteWidgetSaga(action: ReduxAction<{ groupWidgets: boolean }>) {
...widgets,
[pastingIntoWidgetId]: {
...widgets[pastingIntoWidgetId],
- bottomRow: parentBottomRow,
+ bottomRow: Math.max(parentBottomRow, bottomMostRow || 0),
children: parentChildren,
},
};
@@ -1064,9 +1457,19 @@ function* pasteWidgetSaga(action: ReduxAction<{ groupWidgets: boolean }>) {
),
);
- yield put(updateAndSaveLayout(widgets));
+ //calculate the new positions of the reflowed widgets
+ const reflowedWidgets = getReflowedPositions(
+ widgets,
+ gridProps,
+ reflowedMovementMap,
+ );
- flashElementsById(newlyCreatedWidgetIds, 100);
+ yield put(updateAndSaveLayout(reflowedWidgets));
+
+ //if pasting at the bottom of the canvas, then flash it.
+ if (shouldGroup || !newPastingPositionMap) {
+ flashElementsById(newlyCreatedWidgetIds, 100);
+ }
yield put(selectMultipleWidgetsInitAction(newlyCreatedWidgetIds));
}
diff --git a/app/client/src/sagas/WidgetOperationUtils.test.ts b/app/client/src/sagas/WidgetOperationUtils.test.ts
index a084d3dd8d..126fa671eb 100644
--- a/app/client/src/sagas/WidgetOperationUtils.test.ts
+++ b/app/client/src/sagas/WidgetOperationUtils.test.ts
@@ -1,5 +1,7 @@
+import { OccupiedSpace } from "constants/CanvasEditorConstants";
import { get } from "lodash";
import { WidgetProps } from "widgets/BaseWidget";
+import { FlattenedWidgetProps } from "widgets/constants";
import {
handleIfParentIsListWidgetWhilePasting,
handleSpecificCasesWhilePasting,
@@ -7,6 +9,15 @@ import {
checkIfPastingIntoListWidget,
updateListWidgetPropertiesOnChildDelete,
purgeOrphanedDynamicPaths,
+ getBoundariesFromSelectedWidgets,
+ getSnappedGrid,
+ changeIdsOfPastePositions,
+ getVerticallyAdjustedPositions,
+ getNewPositionsForCopiedWidgets,
+ CopiedWidgetGroup,
+ getPastePositionMapFromMousePointer,
+ getReflowedPositions,
+ getWidgetsFromIds,
} from "./WidgetOperationUtils";
describe("WidgetOperationSaga", () => {
@@ -622,4 +633,349 @@ describe("WidgetOperationSaga", () => {
const result = purgeOrphanedDynamicPaths((input as any) as WidgetProps);
expect(result).toStrictEqual(expected);
});
+ it("should return boundaries of selected Widgets", () => {
+ const selectedWidgets = ([
+ {
+ id: "1234",
+ topRow: 10,
+ leftColumn: 20,
+ rightColumn: 45,
+ bottomRow: 40,
+ },
+ {
+ id: "1233",
+ topRow: 45,
+ leftColumn: 30,
+ rightColumn: 60,
+ bottomRow: 70,
+ },
+ ] as any) as WidgetProps[];
+ expect(getBoundariesFromSelectedWidgets(selectedWidgets)).toEqual({
+ totalWidth: 40,
+ maxThickness: 30,
+ topMostRow: 10,
+ leftMostColumn: 20,
+ });
+ });
+ describe("test getSnappedGrid", () => {
+ it("should return snapGrids for a ContainerWidget", () => {
+ const canvasWidget = ({
+ widgetId: "1234",
+ type: "CONTAINER_WIDGET",
+ noPad: true,
+ } as any) as WidgetProps;
+ expect(getSnappedGrid(canvasWidget, 250)).toEqual({
+ padding: 4,
+ snapGrid: {
+ snapColumnSpace: 3.78125,
+ snapRowSpace: 10,
+ },
+ });
+ });
+ it("should return snapGrids for non ContainerWidget", () => {
+ const canvasWidget = ({
+ widgetId: "1234",
+ type: "LIST_WIDGET",
+ noPad: false,
+ } as any) as WidgetProps;
+ expect(getSnappedGrid(canvasWidget, 250)).toEqual({
+ padding: 10,
+ snapGrid: {
+ snapColumnSpace: 3.59375,
+ snapRowSpace: 10,
+ },
+ });
+ });
+ });
+ it("should test changeIdsOfPastePositions", () => {
+ const newPastingPositionMap = {
+ "1234": {
+ id: "1234",
+ left: 10,
+ right: 20,
+ top: 10,
+ bottom: 20,
+ },
+ "1235": {
+ id: "1235",
+ left: 11,
+ right: 22,
+ top: 11,
+ bottom: 22,
+ },
+ };
+ expect(changeIdsOfPastePositions(newPastingPositionMap)).toEqual([
+ {
+ id: "1",
+ left: 10,
+ right: 20,
+ top: 10,
+ bottom: 20,
+ },
+ {
+ id: "2",
+ left: 11,
+ right: 22,
+ top: 11,
+ bottom: 22,
+ },
+ ]);
+ });
+
+ it("should offset widgets vertically so that it doesn't overlap with selected widgets", () => {
+ const selectedWidgets = [
+ {
+ id: "1234",
+ top: 10,
+ left: 20,
+ right: 45,
+ bottom: 40,
+ },
+ {
+ id: "1233",
+ top: 45,
+ left: 30,
+ right: 60,
+ bottom: 70,
+ },
+ {
+ id: "1235",
+ topRow: 80,
+ left: 10,
+ right: 50,
+ bottom: 100,
+ },
+ ] as OccupiedSpace[];
+ const copiedWidgets = ([
+ {
+ id: "1234",
+ top: 10,
+ left: 20,
+ right: 45,
+ bottom: 40,
+ },
+ {
+ id: "1233",
+ top: 45,
+ left: 30,
+ right: 60,
+ bottom: 70,
+ },
+ ] as any) as OccupiedSpace[];
+ expect(
+ getVerticallyAdjustedPositions(copiedWidgets, selectedWidgets, 30),
+ ).toEqual({
+ "1234": {
+ id: "1234",
+ top: 71,
+ left: 20,
+ right: 45,
+ bottom: 101,
+ },
+ "1233": {
+ id: "1233",
+ top: 106,
+ left: 30,
+ right: 60,
+ bottom: 131,
+ },
+ });
+ });
+ it("should test getNewPositionsForCopiedWidgets", () => {
+ const copiedGroups = ([
+ {
+ widgetId: "1234",
+ list: [
+ {
+ topRow: 10,
+ leftColumn: 20,
+ rightColumn: 45,
+ bottomRow: 40,
+ },
+ ],
+ },
+ {
+ widgetId: "1235",
+ list: [
+ {
+ topRow: 45,
+ leftColumn: 25,
+ rightColumn: 40,
+ bottomRow: 80,
+ },
+ ],
+ },
+ ] as any) as CopiedWidgetGroup[];
+ expect(
+ getNewPositionsForCopiedWidgets(copiedGroups, 10, 40, 20, 10),
+ ).toEqual([
+ {
+ id: "1234",
+ top: 40,
+ left: 10,
+ right: 35,
+ bottom: 70,
+ },
+ {
+ id: "1235",
+ top: 75,
+ left: 15,
+ right: 30,
+ bottom: 110,
+ },
+ ]);
+ });
+ it("should test getPastePositionMapFromMousePointer", () => {
+ const copiedGroups = ([
+ {
+ widgetId: "1234",
+ list: [
+ {
+ topRow: 10,
+ leftColumn: 20,
+ rightColumn: 45,
+ bottomRow: 40,
+ },
+ ],
+ },
+ {
+ widgetId: "1235",
+ list: [
+ {
+ topRow: 45,
+ leftColumn: 25,
+ rightColumn: 40,
+ bottomRow: 80,
+ },
+ ],
+ },
+ ] as any) as CopiedWidgetGroup[];
+ expect(
+ getPastePositionMapFromMousePointer(copiedGroups, 10, 40, 20, 10),
+ ).toEqual({
+ "1234": {
+ id: "1234",
+ top: 40,
+ left: 10,
+ right: 35,
+ bottom: 70,
+ },
+ "1235": {
+ id: "1235",
+ top: 75,
+ left: 15,
+ right: 30,
+ bottom: 110,
+ },
+ });
+ });
+ it("should test getReflowedPositions", () => {
+ const widgets = {
+ "1234": {
+ widgetId: "1234",
+ topRow: 40,
+ leftColumn: 10,
+ rightColumn: 35,
+ bottomRow: 70,
+ } as FlattenedWidgetProps,
+ "1233": {
+ widgetId: "1233",
+ topRow: 45,
+ leftColumn: 30,
+ rightColumn: 60,
+ bottomRow: 70,
+ } as FlattenedWidgetProps,
+ "1235": {
+ widgetId: "1235",
+ topRow: 75,
+ leftColumn: 15,
+ rightColumn: 30,
+ bottomRow: 110,
+ } as FlattenedWidgetProps,
+ };
+
+ const gridProps = {
+ parentRowSpace: 10,
+ parentColumnSpace: 10,
+ maxGridColumns: 64,
+ };
+
+ const reflowingWidgets = {
+ "1234": {
+ X: 30,
+ width: 200,
+ },
+ "1235": {
+ X: 40,
+ width: 250,
+ Y: 50,
+ height: 250,
+ },
+ };
+
+ expect(getReflowedPositions(widgets, gridProps, reflowingWidgets)).toEqual({
+ "1234": {
+ widgetId: "1234",
+ topRow: 40,
+ leftColumn: 13,
+ rightColumn: 33,
+ bottomRow: 70,
+ },
+ "1233": {
+ widgetId: "1233",
+ topRow: 45,
+ leftColumn: 30,
+ rightColumn: 60,
+ bottomRow: 70,
+ },
+ "1235": {
+ widgetId: "1235",
+ topRow: 80,
+ leftColumn: 19,
+ rightColumn: 44,
+ bottomRow: 105,
+ },
+ });
+ });
+ it("should test getWidgetsFromIds", () => {
+ const widgets = {
+ "1234": {
+ widgetId: "1234",
+ topRow: 40,
+ leftColumn: 10,
+ rightColumn: 35,
+ bottomRow: 70,
+ } as FlattenedWidgetProps,
+ "1233": {
+ widgetId: "1233",
+ topRow: 45,
+ leftColumn: 30,
+ rightColumn: 60,
+ bottomRow: 70,
+ } as FlattenedWidgetProps,
+ "1235": {
+ widgetId: "1235",
+ topRow: 75,
+ leftColumn: 15,
+ rightColumn: 30,
+ bottomRow: 110,
+ } as FlattenedWidgetProps,
+ };
+ expect(getWidgetsFromIds(["1235", "1234", "1237"], widgets)).toEqual([
+ {
+ widgetId: "1235",
+ topRow: 75,
+ leftColumn: 15,
+ rightColumn: 30,
+ bottomRow: 110,
+ },
+ {
+ widgetId: "1234",
+ topRow: 40,
+ leftColumn: 10,
+ rightColumn: 35,
+ bottomRow: 70,
+ },
+ ]);
+ });
});
diff --git a/app/client/src/sagas/WidgetOperationUtils.ts b/app/client/src/sagas/WidgetOperationUtils.ts
index a170a36118..d2b2b3c95d 100644
--- a/app/client/src/sagas/WidgetOperationUtils.ts
+++ b/app/client/src/sagas/WidgetOperationUtils.ts
@@ -6,10 +6,12 @@ import {
} from "./selectors";
import _, { isString, remove } from "lodash";
import {
+ CONTAINER_GRID_PADDING,
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
RenderModes,
WidgetType,
+ WIDGET_PADDING,
} from "constants/WidgetConstants";
import { all, call } from "redux-saga/effects";
import { DataTree } from "entities/DataTree/dataTreeFactory";
@@ -31,6 +33,15 @@ import {
import { getNextEntityName } from "utils/AppsmithUtils";
import WidgetFactory from "utils/WidgetFactory";
import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers";
+import { OccupiedSpace } from "constants/CanvasEditorConstants";
+import { areIntersecting } from "utils/WidgetPropsUtils";
+import { GridProps, ReflowedSpaceMap, SpaceMap } from "reflow/reflowTypes";
+import {
+ getBaseWidgetClassName,
+ getSlidingCanvasName,
+ getStickyCanvasName,
+ POSITIONED_WIDGET,
+} from "constants/componentClassNameConstants";
export interface CopiedWidgetGroup {
widgetId: string;
@@ -38,6 +49,16 @@ export interface CopiedWidgetGroup {
list: WidgetProps[];
}
+export type NewPastePositionVariables = {
+ bottomMostRow?: number;
+ gridProps?: GridProps;
+ newPastingPositionMap?: SpaceMap;
+ reflowedMovementMap?: ReflowedSpaceMap;
+ canvasId?: string;
+};
+
+export const WIDGET_PASTE_PADDING = 1;
+
/**
* checks if triggerpaths contains property path passed
*
@@ -310,6 +331,8 @@ export const getParentWidgetIdForPasting = function*(
if (childWidget && childWidget.type === "CANVAS_WIDGET") {
newWidgetParentId = childWidget.widgetId;
}
+ } else if (selectedWidget && selectedWidget.type === "CANVAS_WIDGET") {
+ newWidgetParentId = selectedWidget.widgetId;
}
return newWidgetParentId;
};
@@ -365,7 +388,7 @@ export const checkIfPastingIntoListWidget = function(
};
/**
- * get top, left, right, bottom most widgets from copied groups when pasting
+ * get top, left, right, bottom most widgets and and totalWidth from copied groups when pasting
*
* @param copiedWidgetGroups
* @returns
@@ -385,15 +408,43 @@ export const getBoundaryWidgetsFromCopiedGroups = function(
const bottomMostWidget = copiedWidgetGroups.sort(
(a, b) => b.list[0].bottomRow - a.list[0].bottomRow,
)[0].list[0];
-
return {
topMostWidget,
leftMostWidget,
rightMostWidget,
bottomMostWidget,
+ totalWidth: rightMostWidget.rightColumn - leftMostWidget.leftColumn,
};
};
+/**
+ * get totalWidth, maxThickness, topMostRow and leftMostColumn from selected Widgets
+ *
+ * @param selectedWidgets
+ * @returns
+ */
+export function getBoundariesFromSelectedWidgets(
+ selectedWidgets: WidgetProps[],
+) {
+ const topMostWidget = selectedWidgets.sort((a, b) => a.topRow - b.topRow)[0];
+ const leftMostWidget = selectedWidgets.sort(
+ (a, b) => a.leftColumn - b.leftColumn,
+ )[0];
+ const rightMostWidget = selectedWidgets.sort(
+ (a, b) => b.rightColumn - a.rightColumn,
+ )[0];
+ const thickestWidget = selectedWidgets.sort(
+ (a, b) => b.bottomRow - b.topRow - a.bottomRow + a.topRow,
+ )[0];
+
+ return {
+ totalWidth: rightMostWidget.rightColumn - leftMostWidget.leftColumn,
+ maxThickness: thickestWidget.bottomRow - thickestWidget.topRow,
+ topMostRow: topMostWidget.topRow,
+ leftMostColumn: leftMostWidget.leftColumn,
+ };
+}
+
/**
* -------------------------------------------------------------------------------
* OPERATION = PASTING
@@ -432,6 +483,442 @@ export const getSelectedWidgetWhenPasting = function*() {
return selectedWidget;
};
+/**
+ * calculates mouse positions in terms of grid values
+ *
+ * @param canvasRect canvas DOM rect
+ * @param canvasId Id of the canvas widget
+ * @param snapGrid grid parameters
+ * @param padding padding inside of widget
+ * @param mouseLocation mouse Location in terms of absolute pixel values
+ * @returns
+ */
+export function getMousePositions(
+ canvasRect: DOMRect,
+ canvasId: string,
+ snapGrid: { snapRowSpace: number; snapColumnSpace: number },
+ padding: number,
+ mouseLocation?: { x: number; y: number },
+) {
+ //check if the mouse location is inside of the container widget
+ if (
+ !mouseLocation ||
+ !(
+ canvasRect.top < mouseLocation.y &&
+ canvasRect.left < mouseLocation.x &&
+ canvasRect.bottom > mouseLocation.y &&
+ canvasRect.right > mouseLocation.x
+ )
+ )
+ return;
+
+ //get DOM of the overall canvas including it's total scroll height
+ const stickyCanvasDOM = document.querySelector(
+ `#${getStickyCanvasName(canvasId)}`,
+ );
+ if (!stickyCanvasDOM) return;
+
+ const rect = stickyCanvasDOM.getBoundingClientRect();
+
+ // get mouse position relative to the canvas.
+ const relativeMouseLocation = {
+ y: mouseLocation.y - rect.top - padding,
+ x: mouseLocation.x - rect.left - padding,
+ };
+
+ return {
+ top: Math.floor(relativeMouseLocation.y / snapGrid.snapRowSpace),
+ left: Math.floor(relativeMouseLocation.x / snapGrid.snapColumnSpace),
+ };
+}
+
+/**
+ * This method calculates the snap Grid dimensions.
+ *
+ * @param LayoutWidget
+ * @param canvasWidth
+ * @returns
+ */
+export function getSnappedGrid(LayoutWidget: WidgetProps, canvasWidth: number) {
+ let padding = (CONTAINER_GRID_PADDING + WIDGET_PADDING) * 2;
+ if (
+ LayoutWidget.widgetId === MAIN_CONTAINER_WIDGET_ID ||
+ LayoutWidget.type === "CONTAINER_WIDGET"
+ ) {
+ //For MainContainer and any Container Widget padding doesn't exist coz there is already container padding.
+ padding = CONTAINER_GRID_PADDING * 2;
+ }
+ if (LayoutWidget.noPad) {
+ // Widgets like ListWidget choose to have no container padding so will only have widget padding
+ padding = WIDGET_PADDING * 2;
+ }
+ const width = canvasWidth - padding;
+ return {
+ snapGrid: {
+ snapRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
+ snapColumnSpace: canvasWidth
+ ? width / GridDefaults.DEFAULT_GRID_COLUMNS
+ : 0,
+ },
+ padding: padding / 2,
+ };
+}
+
+/**
+ * method to return default canvas,
+ * It is MAIN_CONTAINER_WIDGET_ID by default or
+ * if a modal is open, then default canvas is a Modal's canvas
+ *
+ * @param canvasWidgets
+ * @returns
+ */
+export function getDefaultCanvas(canvasWidgets: CanvasWidgetsReduxState) {
+ const containerDOM = document.querySelector(".t--modal-widget");
+ //if a modal is open, then get it's canvas Id
+ if (containerDOM && containerDOM.id && canvasWidgets[containerDOM.id]) {
+ const containerWidget = canvasWidgets[containerDOM.id];
+ const { canvasDOM, canvasId } = getCanvasIdForContainer(containerWidget);
+ return {
+ canvasId,
+ canvasDOM,
+ containerWidget,
+ };
+ } else {
+ //default canvas is set as MAIN_CONTAINER_WIDGET_ID
+ return {
+ canvasId: MAIN_CONTAINER_WIDGET_ID,
+ containerWidget: canvasWidgets[MAIN_CONTAINER_WIDGET_ID],
+ canvasDOM: document.querySelector(
+ `#${getSlidingCanvasName(MAIN_CONTAINER_WIDGET_ID)}`,
+ ),
+ };
+ }
+}
+
+/**
+ * This method returns the Id of the parent widget of the canvas widget
+ *
+ * @param canvasId
+ * @returns
+ */
+export function getContainerIdForCanvas(canvasId: string) {
+ if (canvasId === MAIN_CONTAINER_WIDGET_ID) return canvasId;
+
+ const selector = `#${getSlidingCanvasName(canvasId)}`;
+ const canvasDOM = document.querySelector(selector);
+ if (!canvasDOM) return "";
+ //check for positionedWidget parent
+ let containerDOM = canvasDOM.closest(`.${POSITIONED_WIDGET}`);
+ //if positioned widget parent is not found, most likely is a modal widget
+ if (!containerDOM) containerDOM = canvasDOM.closest(".t--modal-widget");
+
+ return containerDOM ? containerDOM.id : "";
+}
+
+/**
+ * This method returns Id of the child canvas inside of the Layout Widget
+ *
+ * @param layoutWidget
+ * @returns
+ */
+export function getCanvasIdForContainer(layoutWidget: WidgetProps) {
+ const selector =
+ layoutWidget.type === "MODAL_WIDGET"
+ ? `.${getBaseWidgetClassName(layoutWidget.widgetId)}`
+ : `.${POSITIONED_WIDGET}.${getBaseWidgetClassName(
+ layoutWidget.widgetId,
+ )}`;
+ const containerDOM = document.querySelector(selector);
+ if (!containerDOM) return {};
+ const canvasDOM = containerDOM.getElementsByTagName("canvas");
+
+ return {
+ canvasId: canvasDOM ? canvasDOM[0]?.id.split("-")[2] : undefined,
+ canvasDOM: canvasDOM[0],
+ };
+}
+
+/**
+ * This method returns array of occupiedSpaces with changes Ids
+ *
+ * @param newPastingPositionMap
+ * @returns
+ */
+export function changeIdsOfPastePositions(newPastingPositionMap: SpaceMap) {
+ const newPastePositions = [];
+ const newPastingPositionArray = Object.values(newPastingPositionMap);
+ let count = 1;
+ for (const position of newPastingPositionArray) {
+ newPastePositions.push({
+ ...position,
+ id: count.toString(),
+ });
+ count++;
+ }
+
+ return newPastePositions;
+}
+
+/**
+ * Iterates over the selected widgets to find the next available space below the selected widgets
+ * where in the new pasting positions dont overlap with the selected widgets
+ *
+ * @param copiedSpaces
+ * @param selectedSpaces
+ * @param thickness
+ * @returns
+ */
+export function getVerticallyAdjustedPositions(
+ copiedSpaces: OccupiedSpace[],
+ selectedSpaces: OccupiedSpace[],
+ thickness: number,
+) {
+ let verticalOffset = thickness;
+
+ const newPastingPositionMap: SpaceMap = {};
+
+ //iterate over the widgets to calculate verticalOffset
+ //TODO: find a better way to do this.
+ for (let i = 0; i < copiedSpaces.length; i++) {
+ const copiedSpace = {
+ ...copiedSpaces[i],
+ top: copiedSpaces[i].top + verticalOffset,
+ bottom: copiedSpaces[i].bottom + verticalOffset,
+ };
+
+ for (let j = 0; j < selectedSpaces.length; j++) {
+ const selectedSpace = selectedSpaces[j];
+ if (areIntersecting(copiedSpace, selectedSpace)) {
+ const increment = selectedSpace.bottom - copiedSpace.top;
+ if (increment > 0) {
+ verticalOffset += increment;
+ i = 0;
+ j = 0;
+ break;
+ } else return;
+ }
+ }
+ }
+
+ verticalOffset += WIDGET_PASTE_PADDING;
+
+ // offset the pasting positions down
+ for (const copiedSpace of copiedSpaces) {
+ newPastingPositionMap[copiedSpace.id] = {
+ ...copiedSpace,
+ top: copiedSpace.top + verticalOffset,
+ bottom: copiedSpace.bottom + verticalOffset,
+ };
+ }
+
+ return newPastingPositionMap;
+}
+
+/**
+ * Simple method to convert widget props to occupied spaces
+ *
+ * @param widgets
+ * @returns
+ */
+export function getOccupiedSpacesFromProps(
+ widgets: WidgetProps[],
+): OccupiedSpace[] {
+ const occupiedSpaces = [];
+ for (const widget of widgets) {
+ const currentSpace = {
+ id: widget.widgetId,
+ top: widget.topRow,
+ left: widget.leftColumn,
+ bottom: widget.bottomRow,
+ right: widget.rightColumn,
+ } as OccupiedSpace;
+ occupiedSpaces.push(currentSpace);
+ }
+
+ return occupiedSpaces;
+}
+
+/**
+ * Method that adjusts the positions of copied spaces using,
+ * the top-left of copied widgets and top left of where it should be placed
+ *
+ * @param copiedWidgetGroups
+ * @param copiedTopMostRow
+ * @param selectedTopMostRow
+ * @param copiedLeftMostColumn
+ * @param pasteLeftMostColumn
+ * @returns
+ */
+export function getNewPositionsForCopiedWidgets(
+ copiedWidgetGroups: CopiedWidgetGroup[],
+ copiedTopMostRow: number,
+ selectedTopMostRow: number,
+ copiedLeftMostColumn: number,
+ pasteLeftMostColumn: number,
+): OccupiedSpace[] {
+ const copiedSpacePositions = [];
+
+ // the logic is that, when subtracted by top-left of copied widget, the new position's top-left will be zero
+ // by adding the selectedTopMostRow or pasteLeftMostColumn, copied widget's top row is aligned there
+
+ const leftOffSet = copiedLeftMostColumn - pasteLeftMostColumn;
+ const topOffSet = copiedTopMostRow - selectedTopMostRow;
+
+ for (const copiedWidgetGroup of copiedWidgetGroups) {
+ const copiedWidget = copiedWidgetGroup.list[0];
+
+ const currentSpace = {
+ id: copiedWidgetGroup.widgetId,
+ top: copiedWidget.topRow - topOffSet,
+ left: copiedWidget.leftColumn - leftOffSet,
+ bottom: copiedWidget.bottomRow - topOffSet,
+ right: copiedWidget.rightColumn - leftOffSet,
+ } as OccupiedSpace;
+
+ copiedSpacePositions.push(currentSpace);
+ }
+
+ return copiedSpacePositions;
+}
+
+/**
+ * Method that adjusts the positions of copied spaces using,
+ * the top-left of copied widgets and top left of where it should be placed
+ *
+ * @param copiedWidgetGroups
+ * @param copiedTopMostRow
+ * @param mouseTopRow
+ * @param copiedLeftMostColumn
+ * @param mouseLeftColumn
+ * @returns
+ */
+export function getPastePositionMapFromMousePointer(
+ copiedWidgetGroups: CopiedWidgetGroup[],
+ copiedTopMostRow: number,
+ mouseTopRow: number,
+ copiedLeftMostColumn: number,
+ mouseLeftColumn: number,
+): SpaceMap {
+ const newPastingPositionMap: SpaceMap = {};
+
+ // the logic is that, when subtracted by top-left of copied widget, the new position's top-left will be zero
+ // by adding the selectedTopMostRow or pasteLeftMostColumn, copied widget's top row is aligned there
+
+ const leftOffSet = copiedLeftMostColumn - mouseLeftColumn;
+ const topOffSet = copiedTopMostRow - mouseTopRow;
+
+ for (const copiedWidgetGroup of copiedWidgetGroups) {
+ const copiedWidget = copiedWidgetGroup.list[0];
+
+ newPastingPositionMap[copiedWidgetGroup.widgetId] = {
+ id: copiedWidgetGroup.widgetId,
+ top: copiedWidget.topRow - topOffSet,
+ left: copiedWidget.leftColumn - leftOffSet,
+ bottom: copiedWidget.bottomRow - topOffSet,
+ right: copiedWidget.rightColumn - leftOffSet,
+ type: copiedWidget.type,
+ } as OccupiedSpace;
+ }
+
+ return newPastingPositionMap;
+}
+
+/**
+ * Take the canvas widgets and move them with the reflowed values
+ *
+ *
+ * @param widgets
+ * @param gridProps
+ * @param reflowingWidgets
+ * @returns
+ */
+export function getReflowedPositions(
+ widgets: {
+ [widgetId: string]: FlattenedWidgetProps;
+ },
+ gridProps?: GridProps,
+ reflowingWidgets?: ReflowedSpaceMap,
+) {
+ const currentWidgets: {
+ [widgetId: string]: FlattenedWidgetProps;
+ } = { ...widgets };
+
+ const reflowWidgetKeys = Object.keys(reflowingWidgets || {});
+
+ // if there are no reflowed widgets return the original widgets
+ if (!reflowingWidgets || !gridProps || reflowWidgetKeys.length <= 0)
+ return widgets;
+
+ for (const reflowedWidgetId of reflowWidgetKeys) {
+ const reflowWidget = reflowingWidgets[reflowedWidgetId];
+ const canvasWidget = { ...currentWidgets[reflowedWidgetId] };
+
+ let { bottomRow, leftColumn, rightColumn, topRow } = canvasWidget;
+
+ // adjust the positions with respect to the reflowed positions
+ if (reflowWidget.X !== undefined && reflowWidget.width !== undefined) {
+ leftColumn = Math.round(
+ canvasWidget.leftColumn + reflowWidget.X / gridProps.parentColumnSpace,
+ );
+ rightColumn = Math.round(
+ leftColumn + reflowWidget.width / gridProps.parentColumnSpace,
+ );
+ }
+
+ if (reflowWidget.Y !== undefined && reflowWidget.height !== undefined) {
+ topRow = Math.round(
+ canvasWidget.topRow + reflowWidget.Y / gridProps.parentRowSpace,
+ );
+ bottomRow = Math.round(
+ topRow + reflowWidget.height / gridProps.parentRowSpace,
+ );
+ }
+
+ currentWidgets[reflowedWidgetId] = {
+ ...canvasWidget,
+ topRow,
+ leftColumn,
+ bottomRow,
+ rightColumn,
+ };
+ }
+
+ return currentWidgets;
+}
+
+/**
+ * method to return array of widget properties from widgetsIds, without any undefined values
+ *
+ * @param widgetsIds
+ * @param canvasWidgets
+ * @returns array of widgets properties
+ */
+export function getWidgetsFromIds(
+ widgetsIds: string[],
+ canvasWidgets: CanvasWidgetsReduxState,
+) {
+ const widgets = [];
+ for (const currentId of widgetsIds) {
+ if (canvasWidgets[currentId]) widgets.push(canvasWidgets[currentId]);
+ }
+
+ return widgets;
+}
+
+/**
+ * Check if it is drop target Including the CANVAS_WIDGET
+ *
+ * @param type
+ * @returns
+ */
+export function isDropTarget(type: WidgetType, includeCanvasWidget = false) {
+ const isLayoutWidget = !!WidgetFactory.widgetConfigMap.get(type)?.isCanvas;
+
+ if (includeCanvasWidget) return isLayoutWidget || type === "CANVAS_WIDGET";
+
+ return isLayoutWidget;
+}
+
/**
* group copied widgets into a container
*
diff --git a/app/client/src/utils/generators.tsx b/app/client/src/utils/generators.tsx
index ea18eede87..660693c106 100644
--- a/app/client/src/utils/generators.tsx
+++ b/app/client/src/utils/generators.tsx
@@ -1,5 +1,6 @@
import { WidgetType } from "constants/WidgetConstants";
import generate from "nanoid/generate";
+import { getBaseWidgetClassName } from "../constants/componentClassNameConstants";
const ALPHANUMERIC = "1234567890abcdefghijklmnopqrstuvwxyz";
// const ALPHABET = "abcdefghijklmnopqrstuvwxyz";
@@ -16,7 +17,7 @@ export const generateReactKey = ({
// 2. Property pane reference for positioning
// 3. Table widget filter pan reference for positioning
export const generateClassName = (seed?: string) => {
- return `appsmith_widget_${seed}`;
+ return getBaseWidgetClassName(seed);
};
export const getCanvasClassName = () => "canvas";
diff --git a/app/client/src/widgets/BaseInputWidget/component/index.tsx b/app/client/src/widgets/BaseInputWidget/component/index.tsx
index bb452af4c1..ff80dca595 100644
--- a/app/client/src/widgets/BaseInputWidget/component/index.tsx
+++ b/app/client/src/widgets/BaseInputWidget/component/index.tsx
@@ -27,6 +27,7 @@ import { InputTypes } from "../constants";
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
import Icon from "components/ads/Icon";
import { InputType } from "widgets/InputWidget/constants";
+import { getBaseWidgetClassName } from "constants/componentClassNameConstants";
import { LabelPosition } from "components/constants";
import LabelWithTooltip, {
labelLayoutStyles,
@@ -331,7 +332,7 @@ class BaseInputComponent extends React.Component<
componentDidMount() {
if (isNumberInputType(this.props.inputHTMLType) && this.props.onStep) {
const element = document.querySelector(
- `.appsmith_widget_${this.props.widgetId} .bp3-button-group`,
+ `.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button-group`,
);
if (element !== null && element.childNodes) {
@@ -350,7 +351,7 @@ class BaseInputComponent extends React.Component<
componentWillUnmount() {
if (isNumberInputType(this.props.inputHTMLType) && this.props.onStep) {
const element = document.querySelector(
- `.appsmith_widget_${this.props.widgetId} .bp3-button-group`,
+ `.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button-group`,
);
if (element !== null && element.childNodes) {
diff --git a/app/client/src/widgets/InputWidget/component/index.tsx b/app/client/src/widgets/InputWidget/component/index.tsx
index 541ee5ab91..a068f73116 100644
--- a/app/client/src/widgets/InputWidget/component/index.tsx
+++ b/app/client/src/widgets/InputWidget/component/index.tsx
@@ -36,6 +36,7 @@ import ISDCodeDropdown, {
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
import Icon from "components/ads/Icon";
import { limitDecimalValue, getSeparators } from "./utilities";
+import { getBaseWidgetClassName } from "constants/componentClassNameConstants";
import { LabelPosition } from "components/constants";
import LabelWithTooltip, {
labelLayoutStyles,
@@ -298,7 +299,7 @@ class InputComponent extends React.Component<
componentDidMount() {
if (this.props.inputType === InputTypes.CURRENCY) {
const element: any = document.querySelectorAll(
- `.appsmith_widget_${this.props.widgetId} .bp3-button`,
+ `.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button`,
);
if (element !== null) {
element[0].addEventListener("click", this.onIncrementButtonClick);
@@ -313,7 +314,7 @@ class InputComponent extends React.Component<
this.props.inputType !== prevProps.inputType
) {
const element: any = document.querySelectorAll(
- `.appsmith_widget_${this.props.widgetId} .bp3-button`,
+ `.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button`,
);
if (element !== null) {
element[0].addEventListener("click", this.onIncrementButtonClick);
@@ -325,7 +326,7 @@ class InputComponent extends React.Component<
componentWillUnmount() {
if (this.props.inputType === InputTypes.CURRENCY) {
const element: any = document.querySelectorAll(
- `.appsmith_widget_${this.props.widgetId} .bp3-button`,
+ `.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button`,
);
if (element !== null) {
element[0].removeEventListener("click", this.onIncrementButtonClick);
diff --git a/app/client/src/widgets/ModalWidget/component/index.tsx b/app/client/src/widgets/ModalWidget/component/index.tsx
index 12de4f6088..afeca158cf 100644
--- a/app/client/src/widgets/ModalWidget/component/index.tsx
+++ b/app/client/src/widgets/ModalWidget/component/index.tsx
@@ -122,6 +122,7 @@ export type ModalComponentProps = {
resizeModal?: (dimensions: UIElementSize) => void;
maxWidth?: number;
minSize?: number;
+ widgetId: string;
widgetName: string;
};
@@ -198,6 +199,7 @@ export default function ModalComponent(props: ModalComponentProps) {
};
const getResizableContent = () => {
+ //id for Content is required for Copy Paste inside the modal
return (
diff --git a/app/client/src/widgets/ModalWidget/widget/index.tsx b/app/client/src/widgets/ModalWidget/widget/index.tsx
index b0a27bdd8d..94355fafcb 100644
--- a/app/client/src/widgets/ModalWidget/widget/index.tsx
+++ b/app/client/src/widgets/ModalWidget/widget/index.tsx
@@ -198,6 +198,7 @@ export class ModalWidget extends BaseWidget {
portalContainer={portalContainer}
resizeModal={this.onModalResize}
scrollContents={!!this.props.shouldScrollContents}
+ widgetId={this.props.widgetId}
widgetName={this.props.widgetName}
width={this.getModalWidth(this.props.width)}
>