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:
rahulramesha 2022-05-04 13:28:57 +05:30 committed by GitHub
parent 7dbe9ccf26
commit e128b2daf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1661 additions and 42 deletions

View 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"
}
]
}
}

View File

@ -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);
});
});

View File

@ -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,
},
};
};

View File

@ -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()}`

View 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";

View File

@ -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>
</>,

View File

@ -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()),

View 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;

View File

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

View File

@ -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",
);

View File

@ -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);

View File

@ -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}

View File

@ -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));
}

View File

@ -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,
},
]);
});
});

View File

@ -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
*

View File

@ -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";

View File

@ -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) {

View File

@ -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);

View File

@ -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}
>

View File

@ -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)}
>