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
This commit is contained in:
parent
7dbe9ccf26
commit
e128b2daf3
149
app/client/cypress/fixtures/WidgetCopyPaste.json
Normal file
149
app/client/cypress/fixtures/WidgetCopyPaste.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()}`
|
||||
|
|
|
|||
13
app/client/src/constants/componentClassNameConstants.ts
Normal file
13
app/client/src/constants/componentClassNameConstants.ts
Normal file
|
|
@ -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";
|
||||
|
|
@ -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"]}
|
||||
>
|
||||
<MockApplication>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<UpdatedMainContainer dsl={dsl} />
|
||||
</GlobalHotKeys>
|
||||
</MockApplication>
|
||||
|
|
@ -204,7 +208,11 @@ describe("Canvas Hot Keys", () => {
|
|||
initialEntries={["/app/applicationSlug/pageSlug-page_id/edit"]}
|
||||
>
|
||||
<MockApplication>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<UpdatedMainContainer dsl={dsl} />
|
||||
</GlobalHotKeys>
|
||||
</MockApplication>
|
||||
|
|
@ -246,7 +254,11 @@ describe("Canvas Hot Keys", () => {
|
|||
initialEntries={["/app/applicationSlug/pageSlug-page_id/edit"]}
|
||||
>
|
||||
<MockApplication>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<UpdatedMainContainer dsl={dsl} />
|
||||
</GlobalHotKeys>
|
||||
</MockApplication>
|
||||
|
|
@ -331,7 +343,11 @@ describe("Canvas Hot Keys", () => {
|
|||
initialEntries={["/app/applicationSlug/pageSlug-page_id/edit"]}
|
||||
>
|
||||
<MockApplication>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<UpdatedMainContainer dsl={dsl} />
|
||||
</GlobalHotKeys>
|
||||
</MockApplication>
|
||||
|
|
@ -388,7 +404,11 @@ describe("Cut/Copy/Paste hotkey", () => {
|
|||
});
|
||||
const component = render(
|
||||
<MockPageDSL dsl={dsl}>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<MockCanvas />
|
||||
</GlobalHotKeys>
|
||||
</MockPageDSL>,
|
||||
|
|
@ -469,7 +489,11 @@ describe("Cut/Copy/Paste hotkey", () => {
|
|||
});
|
||||
const component = render(
|
||||
<MockPageDSL dsl={dsl}>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<MockCanvas />
|
||||
</GlobalHotKeys>
|
||||
</MockPageDSL>,
|
||||
|
|
@ -540,7 +564,11 @@ describe("Undo/Redo hotkey", () => {
|
|||
const dispatchSpy = jest.spyOn(store, "dispatch");
|
||||
const component = render(
|
||||
<MockPageDSL>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<MockCanvas />
|
||||
</GlobalHotKeys>
|
||||
</MockPageDSL>,
|
||||
|
|
@ -566,7 +594,11 @@ describe("Undo/Redo hotkey", () => {
|
|||
const dispatchSpy = jest.spyOn(store, "dispatch");
|
||||
const component = render(
|
||||
<MockPageDSL>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<MockCanvas />
|
||||
</GlobalHotKeys>
|
||||
</MockPageDSL>,
|
||||
|
|
@ -592,7 +624,11 @@ describe("Undo/Redo hotkey", () => {
|
|||
const dispatchSpy = jest.spyOn(store, "dispatch");
|
||||
const component = render(
|
||||
<MockPageDSL>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<MockCanvas />
|
||||
</GlobalHotKeys>
|
||||
</MockPageDSL>,
|
||||
|
|
@ -628,7 +664,11 @@ describe("cmd + s hotkey", () => {
|
|||
pauseOnHover={false}
|
||||
transition={Slide}
|
||||
/>
|
||||
<GlobalHotKeys>
|
||||
<GlobalHotKeys
|
||||
getMousePosition={() => {
|
||||
return { x: 0, y: 0 };
|
||||
}}
|
||||
>
|
||||
<div />
|
||||
</GlobalHotKeys>
|
||||
</>,
|
||||
|
|
@ -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<Props> {
|
|||
group="Canvas"
|
||||
label="Paste Widget"
|
||||
onKeyDown={() => {
|
||||
this.props.pasteCopiedWidget();
|
||||
this.props.pasteCopiedWidget(
|
||||
this.props.getMousePosition() || { x: 0, y: 0 },
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
|
|
@ -420,7 +423,8 @@ const mapStateToProps = (state: AppState) => ({
|
|||
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()),
|
||||
16
app/client/src/pages/Editor/GlobalHotKeys/index.tsx
Normal file
16
app/client/src/pages/Editor/GlobalHotKeys/index.tsx
Normal file
|
|
@ -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 (
|
||||
<GlobalHotKeys {...props} getMousePosition={getMousePosition}>
|
||||
{props.children}
|
||||
</GlobalHotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
export default HotKeysHOC;
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>(widgetClasses);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<StickyCanvasArena
|
||||
canExtend={canExtend}
|
||||
canvasId={`canvas-selection-${widgetId}`}
|
||||
canvasId={getSlidingCanvasName(widgetId)}
|
||||
canvasPadding={canvasPadding}
|
||||
getRelativeScrollingParent={getNearestParentCanvas}
|
||||
id={`div-selection-${widgetId}`}
|
||||
id={getStickyCanvasName(widgetId)}
|
||||
ref={canvasRef}
|
||||
showCanvas={shouldShow}
|
||||
snapColSpace={snapColumnSpace}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,13 @@ import {
|
|||
} from "constants/WidgetConstants";
|
||||
import { getCopiedWidgets, saveCopiedWidgets } from "utils/storage";
|
||||
import { generateReactKey } from "utils/generators";
|
||||
import { flashElementsById } from "utils/helpers";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import log from "loglevel";
|
||||
import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/utils";
|
||||
import { getCurrentPageId } from "selectors/editorSelectors";
|
||||
import {
|
||||
getCurrentPageId,
|
||||
getWidgetSpacesSelectorForContainer,
|
||||
} from "selectors/editorSelectors";
|
||||
import { selectMultipleWidgetsInitAction } from "actions/widgetSelectionActions";
|
||||
|
||||
import { getDataTree } from "selectors/dataTreeSelectors";
|
||||
|
|
@ -92,6 +94,22 @@ import {
|
|||
getParentWidgetIdForGrouping,
|
||||
isCopiedModalWidget,
|
||||
purgeOrphanedDynamicPaths,
|
||||
getReflowedPositions,
|
||||
NewPastePositionVariables,
|
||||
getContainerIdForCanvas,
|
||||
getSnappedGrid,
|
||||
getNewPositionsForCopiedWidgets,
|
||||
getVerticallyAdjustedPositions,
|
||||
getOccupiedSpacesFromProps,
|
||||
changeIdsOfPastePositions,
|
||||
getCanvasIdForContainer,
|
||||
getMousePositions,
|
||||
getPastePositionMapFromMousePointer,
|
||||
getBoundariesFromSelectedWidgets,
|
||||
WIDGET_PASTE_PADDING,
|
||||
getWidgetsFromIds,
|
||||
getDefaultCanvas,
|
||||
isDropTarget,
|
||||
} from "./WidgetOperationUtils";
|
||||
import { getSelectedWidgets } from "selectors/ui";
|
||||
import { widgetSelectionSagas } from "./WidgetSelectionSagas";
|
||||
|
|
@ -102,7 +120,16 @@ import widgetDeletionSagas from "./WidgetDeletionSagas";
|
|||
import { getReflow } from "selectors/widgetReflowSelectors";
|
||||
import { widgetReflow } from "reducers/uiReducers/reflowReducer";
|
||||
import { stopReflowAction } from "actions/reflowActions";
|
||||
import { collisionCheckPostReflow } from "utils/reflowHookUtils";
|
||||
import {
|
||||
collisionCheckPostReflow,
|
||||
getBottomRowAfterReflow,
|
||||
} from "utils/reflowHookUtils";
|
||||
import { PrevReflowState, ReflowDirection, SpaceMap } from "reflow/reflowTypes";
|
||||
import { WidgetSpace } from "constants/CanvasEditorConstants";
|
||||
import { reflow } from "reflow";
|
||||
import { getBottomMostRow } from "reflow/reflowUtils";
|
||||
import { flashElementsById } from "utils/helpers";
|
||||
import { getSlidingCanvasName } from "constants/componentClassNameConstants";
|
||||
|
||||
export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
|
||||
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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(
|
||||
`.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<HTMLDivElement>(
|
||||
`.appsmith_widget_${this.props.widgetId} .bp3-button-group`,
|
||||
`.${getBaseWidgetClassName(this.props.widgetId)} .bp3-button-group`,
|
||||
);
|
||||
|
||||
if (element !== null && element.childNodes) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Resizable
|
||||
allowResize
|
||||
|
|
@ -215,6 +217,7 @@ export default function ModalComponent(props: ModalComponentProps) {
|
|||
>
|
||||
<Content
|
||||
className={`${getCanvasClassName()} ${props.className}`}
|
||||
id={props.widgetId}
|
||||
ref={modalContentRef}
|
||||
scroll={props.scrollContents}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ export class ModalWidget extends BaseWidget<ModalWidgetProps, WidgetState> {
|
|||
portalContainer={portalContainer}
|
||||
resizeModal={this.onModalResize}
|
||||
scrollContents={!!this.props.shouldScrollContents}
|
||||
widgetId={this.props.widgetId}
|
||||
widgetName={this.props.widgetName}
|
||||
width={this.getModalWidth(this.props.width)}
|
||||
>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user