2021-08-12 05:45:38 +00:00
|
|
|
import React, { useMemo, useRef } from "react";
|
2021-07-08 06:30:19 +00:00
|
|
|
import styled from "styled-components";
|
2022-06-29 11:55:26 +00:00
|
|
|
import { get, minBy } from "lodash";
|
2021-07-08 06:30:19 +00:00
|
|
|
import { useSelector, useDispatch } from "react-redux";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
copyWidget,
|
|
|
|
|
cutWidget,
|
2021-08-25 05:00:31 +00:00
|
|
|
groupWidgets,
|
2021-07-08 06:30:19 +00:00
|
|
|
deleteSelectedWidget,
|
|
|
|
|
} from "actions/widgetActions";
|
2022-03-14 03:24:53 +00:00
|
|
|
import { modText } from "utils/helpers";
|
2021-07-08 06:30:19 +00:00
|
|
|
import { Layers } from "constants/Layers";
|
|
|
|
|
import { FormIcons } from "icons/FormIcons";
|
2023-01-23 03:50:47 +00:00
|
|
|
import { TooltipComponent as Tooltip } from "design-system-old";
|
2021-07-08 06:30:19 +00:00
|
|
|
import { ControlIcons } from "icons/ControlIcons";
|
|
|
|
|
import { getSelectedWidgets } from "selectors/ui";
|
|
|
|
|
|
|
|
|
|
import { stopEventPropagation } from "utils/AppsmithUtils";
|
|
|
|
|
import { getCanvasWidgets } from "selectors/entitiesSelector";
|
2022-06-24 14:23:02 +00:00
|
|
|
import { IPopoverSharedProps } from "@blueprintjs/core";
|
2021-07-08 06:30:19 +00:00
|
|
|
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
|
2021-09-09 15:10:22 +00:00
|
|
|
import WidgetFactory from "utils/WidgetFactory";
|
2022-08-24 12:16:32 +00:00
|
|
|
import { AppState } from "@appsmith/reducers";
|
2021-08-12 05:45:38 +00:00
|
|
|
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
|
2022-06-29 11:55:26 +00:00
|
|
|
import { getBoundariesFromSelectedWidgets } from "sagas/WidgetOperationUtils";
|
|
|
|
|
import { CONTAINER_GRID_PADDING } from "constants/WidgetConstants";
|
2021-07-08 06:30:19 +00:00
|
|
|
|
2021-09-09 15:10:22 +00:00
|
|
|
const WidgetTypes = WidgetFactory.widgetTypes;
|
2021-07-08 06:30:19 +00:00
|
|
|
const StyledSelectionBox = styled.div`
|
|
|
|
|
position: absolute;
|
2021-08-12 05:45:38 +00:00
|
|
|
cursor: grab;
|
2021-07-08 06:30:19 +00:00
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const StyledActionsContainer = styled.div`
|
|
|
|
|
position: relative;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const StyledActions = styled.div`
|
|
|
|
|
left: calc(100% - 38px);
|
|
|
|
|
padding: 5px 0;
|
|
|
|
|
width: max-content;
|
|
|
|
|
z-index: ${Layers.contextMenu};
|
|
|
|
|
position: absolute;
|
|
|
|
|
background-color: ${(props) => props.theme.colors.appBackground};
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const StyledAction = styled.button`
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
height: 28px;
|
|
|
|
|
width: 28px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin: 0 5px;
|
|
|
|
|
outline: none;
|
|
|
|
|
border: none;
|
|
|
|
|
background: transparent;
|
|
|
|
|
z-index: ${Layers.contextMenu};
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
|
&:active,
|
|
|
|
|
&.active {
|
|
|
|
|
background: ${(props) =>
|
|
|
|
|
props.disabled
|
|
|
|
|
? "initial"
|
|
|
|
|
: props.theme.colors.widgetGroupingContextMenu.actionActiveBg};
|
|
|
|
|
}
|
|
|
|
|
&:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const StyledSelectBoxHandleTop = styled.div`
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 1px;
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: ${Layers.contextMenu};
|
|
|
|
|
border-top: 1px dashed
|
|
|
|
|
${(props) => props.theme.colors.widgetGroupingContextMenu.border};
|
|
|
|
|
top: 0px;
|
|
|
|
|
left: -1px;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const StyledSelectBoxHandleLeft = styled.div`
|
|
|
|
|
width: 0px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: ${Layers.contextMenu};
|
|
|
|
|
border-left: 1px dashed
|
|
|
|
|
${(props) => props.theme.colors.widgetGroupingContextMenu.border};
|
|
|
|
|
top: 0px;
|
|
|
|
|
left: -1px;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const StyledSelectBoxHandleRight = styled.div`
|
|
|
|
|
width: 0px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: ${Layers.contextMenu};
|
|
|
|
|
border-left: 1px dashed
|
|
|
|
|
${(props) => props.theme.colors.widgetGroupingContextMenu.border};
|
|
|
|
|
top: 0px;
|
|
|
|
|
left: calc(100% - 1px);
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const StyledSelectBoxHandleBottom = styled.div`
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 1px;
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: ${Layers.contextMenu};
|
|
|
|
|
border-bottom: 1px dashed
|
|
|
|
|
${(props) => props.theme.colors.widgetGroupingContextMenu.border};
|
|
|
|
|
top: 100%;
|
|
|
|
|
left: -1px;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
export const PopoverModifiers: IPopoverSharedProps["modifiers"] = {
|
|
|
|
|
offset: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
},
|
|
|
|
|
arrow: {
|
|
|
|
|
enabled: false,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CopyIcon = ControlIcons.COPY2_CONTROL;
|
|
|
|
|
const DeleteIcon = FormIcons.DELETE_ICON;
|
|
|
|
|
const CutIcon = ControlIcons.CUT_CONTROL;
|
2021-08-25 05:00:31 +00:00
|
|
|
const GroupIcon = ControlIcons.GROUP_CONTROL;
|
2021-07-08 06:30:19 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* helper text that comes in popover on hover of actions in context menu
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
const copyHelpText = (
|
|
|
|
|
<>
|
2022-03-14 03:24:53 +00:00
|
|
|
Click or <b>{modText()} C</b> & {modText()} V
|
2021-07-08 06:30:19 +00:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
const cutHelpText = (
|
|
|
|
|
<>
|
2022-03-14 03:24:53 +00:00
|
|
|
Click or <b>{modText()} X</b> & {modText()} V
|
2021-07-08 06:30:19 +00:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
const deleteHelpText = (
|
|
|
|
|
<>
|
|
|
|
|
Click or <b> Del </b>
|
|
|
|
|
</>
|
|
|
|
|
);
|
2021-08-25 05:00:31 +00:00
|
|
|
const groupHelpText = (
|
|
|
|
|
<>
|
2022-03-14 03:24:53 +00:00
|
|
|
Click or <b>{modText()} G to group</b>
|
2021-08-25 05:00:31 +00:00
|
|
|
</>
|
|
|
|
|
);
|
2021-07-08 06:30:19 +00:00
|
|
|
|
|
|
|
|
function WidgetsMultiSelectBox(props: {
|
|
|
|
|
widgetId: string;
|
|
|
|
|
widgetType: string;
|
2022-06-29 11:55:26 +00:00
|
|
|
noContainerOffset: boolean;
|
2021-08-12 05:45:38 +00:00
|
|
|
snapColumnSpace: number;
|
|
|
|
|
snapRowSpace: number;
|
2021-07-08 06:30:19 +00:00
|
|
|
}): any {
|
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
|
const canvasWidgets = useSelector(getCanvasWidgets);
|
|
|
|
|
const selectedWidgetIDs = useSelector(getSelectedWidgets);
|
|
|
|
|
const selectedWidgets = selectedWidgetIDs.map(
|
|
|
|
|
(widgetID) => canvasWidgets[widgetID],
|
|
|
|
|
);
|
|
|
|
|
const { focusWidget } = useWidgetSelection();
|
2021-08-12 05:45:38 +00:00
|
|
|
const isDragging = useSelector(
|
|
|
|
|
(state: AppState) => state.ui.widgetDragResize.isDragging,
|
|
|
|
|
);
|
2021-07-08 06:30:19 +00:00
|
|
|
/**
|
|
|
|
|
* the multi-selection bounding box should only render when:
|
|
|
|
|
*
|
|
|
|
|
* 1. the widgetID is equal to the parent id of selected widget
|
|
|
|
|
* 2. has common parent
|
|
|
|
|
* 3. multiple widgets are selected
|
|
|
|
|
*/
|
|
|
|
|
const shouldRender = useMemo(() => {
|
2022-08-03 07:02:49 +00:00
|
|
|
if (isDragging) {
|
2021-08-12 05:45:38 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2021-07-08 06:30:19 +00:00
|
|
|
const parentIDs = selectedWidgets
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.map((widget) => widget.parentId);
|
|
|
|
|
const hasCommonParent = parentIDs.every((v) => v === parentIDs[0]);
|
|
|
|
|
const isMultipleWidgetsSelected = selectedWidgetIDs.length > 1;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
props.widgetType === WidgetTypes.CANVAS_WIDGET &&
|
|
|
|
|
isMultipleWidgetsSelected &&
|
|
|
|
|
hasCommonParent &&
|
|
|
|
|
get(selectedWidgets, "0.parentId") === props.widgetId
|
|
|
|
|
);
|
2022-08-03 07:02:49 +00:00
|
|
|
}, [selectedWidgets, isDragging]);
|
2021-08-12 05:45:38 +00:00
|
|
|
const draggableRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const { setDraggingState } = useWidgetDragResize();
|
|
|
|
|
|
|
|
|
|
const onDragStart = (e: any) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (draggableRef.current) {
|
|
|
|
|
const bounds = draggableRef.current.getBoundingClientRect();
|
|
|
|
|
const parentId = get(selectedWidgets, "0.parentId");
|
|
|
|
|
const startPoints = {
|
|
|
|
|
top: (e.clientY - bounds.top) / props.snapRowSpace,
|
|
|
|
|
left: (e.clientX - bounds.left) / props.snapColumnSpace,
|
|
|
|
|
};
|
|
|
|
|
const top = minBy(selectedWidgets, (rect) => rect.topRow)?.topRow;
|
|
|
|
|
const left = minBy(selectedWidgets, (rect) => rect.leftColumn)
|
|
|
|
|
?.leftColumn;
|
|
|
|
|
setDraggingState({
|
|
|
|
|
isDragging: true,
|
|
|
|
|
dragGroupActualParent: parentId || "",
|
|
|
|
|
draggingGroupCenter: {
|
|
|
|
|
top,
|
|
|
|
|
left,
|
|
|
|
|
},
|
|
|
|
|
startPoints,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-07-08 06:30:19 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* calculate bounding box
|
|
|
|
|
*/
|
|
|
|
|
const { height, left, top, width } = useMemo(() => {
|
|
|
|
|
if (shouldRender) {
|
2022-06-29 11:55:26 +00:00
|
|
|
const {
|
|
|
|
|
leftMostColumn,
|
|
|
|
|
topMostRow,
|
|
|
|
|
totalHeight,
|
|
|
|
|
totalWidth,
|
|
|
|
|
} = getBoundariesFromSelectedWidgets(selectedWidgets);
|
2021-07-08 06:30:19 +00:00
|
|
|
|
|
|
|
|
return {
|
2022-06-29 11:55:26 +00:00
|
|
|
top:
|
|
|
|
|
topMostRow * props.snapRowSpace +
|
|
|
|
|
(props.noContainerOffset ? 0 : CONTAINER_GRID_PADDING),
|
|
|
|
|
left:
|
|
|
|
|
leftMostColumn * props.snapColumnSpace +
|
|
|
|
|
(props.noContainerOffset ? 0 : CONTAINER_GRID_PADDING),
|
|
|
|
|
height: totalHeight * props.snapRowSpace,
|
|
|
|
|
width: totalWidth * props.snapColumnSpace,
|
2021-07-08 06:30:19 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
2022-06-29 11:55:26 +00:00
|
|
|
}, [
|
|
|
|
|
selectedWidgets,
|
|
|
|
|
props.snapColumnSpace,
|
|
|
|
|
props.snapRowSpace,
|
|
|
|
|
props.noContainerOffset,
|
|
|
|
|
]);
|
2021-07-08 06:30:19 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* copies the selected widgets
|
|
|
|
|
*
|
|
|
|
|
* @param e
|
|
|
|
|
*/
|
|
|
|
|
const onCopySelectedWidgets = (
|
|
|
|
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
|
|
|
|
) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
dispatch(copyWidget(true));
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* cut the selected widgets
|
|
|
|
|
*
|
|
|
|
|
* @param e
|
|
|
|
|
*/
|
|
|
|
|
const onCutSelectedWidgets = (
|
|
|
|
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
|
|
|
|
) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
dispatch(cutWidget());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* cut the selected widgets
|
|
|
|
|
*
|
|
|
|
|
* @param e
|
|
|
|
|
*/
|
|
|
|
|
const onDeleteSelectedWidgets = (
|
|
|
|
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
|
|
|
|
) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
dispatch(deleteSelectedWidget(true));
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-25 05:00:31 +00:00
|
|
|
/**
|
|
|
|
|
* group widgets into container
|
|
|
|
|
*
|
|
|
|
|
* @param e
|
|
|
|
|
*/
|
|
|
|
|
const onGroupWidgets = (
|
|
|
|
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
|
|
|
|
) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
dispatch(groupWidgets());
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-08 06:30:19 +00:00
|
|
|
if (!shouldRender) return false;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StyledSelectionBox
|
|
|
|
|
className="t--multi-selection-box"
|
2021-08-12 05:45:38 +00:00
|
|
|
data-testid="t--selection-box"
|
|
|
|
|
draggable
|
2021-07-08 06:30:19 +00:00
|
|
|
key={`selection-box-${props.widgetId}`}
|
2021-08-12 05:45:38 +00:00
|
|
|
onDragStart={onDragStart}
|
2021-07-08 06:30:19 +00:00
|
|
|
onMouseMove={() => focusWidget()}
|
|
|
|
|
onMouseOver={() => focusWidget()}
|
2021-08-12 05:45:38 +00:00
|
|
|
ref={draggableRef}
|
2021-07-08 06:30:19 +00:00
|
|
|
style={{
|
2022-06-29 11:55:26 +00:00
|
|
|
left,
|
|
|
|
|
top,
|
|
|
|
|
height,
|
|
|
|
|
width,
|
2021-07-08 06:30:19 +00:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<StyledSelectBoxHandleTop />
|
|
|
|
|
<StyledSelectBoxHandleLeft />
|
|
|
|
|
<StyledSelectBoxHandleRight />
|
|
|
|
|
<StyledSelectBoxHandleBottom />
|
|
|
|
|
<StyledActionsContainer>
|
|
|
|
|
<StyledActions>
|
|
|
|
|
{/* copy widgets */}
|
|
|
|
|
<Tooltip
|
|
|
|
|
boundary="viewport"
|
|
|
|
|
content={copyHelpText}
|
|
|
|
|
maxWidth="400px"
|
|
|
|
|
modifiers={PopoverModifiers}
|
2022-06-24 14:23:02 +00:00
|
|
|
position="right"
|
2021-07-08 06:30:19 +00:00
|
|
|
>
|
|
|
|
|
<StyledAction
|
|
|
|
|
onClick={stopEventPropagation}
|
|
|
|
|
onClickCapture={onCopySelectedWidgets}
|
|
|
|
|
>
|
|
|
|
|
<CopyIcon color="black" height={16} width={16} />
|
|
|
|
|
</StyledAction>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
{/* cut widgets */}
|
|
|
|
|
<Tooltip
|
|
|
|
|
boundary="viewport"
|
|
|
|
|
content={cutHelpText}
|
|
|
|
|
maxWidth="400px"
|
|
|
|
|
modifiers={PopoverModifiers}
|
2022-06-24 14:23:02 +00:00
|
|
|
position="right"
|
2021-07-08 06:30:19 +00:00
|
|
|
>
|
|
|
|
|
<StyledAction
|
|
|
|
|
onClick={stopEventPropagation}
|
|
|
|
|
onClickCapture={onCutSelectedWidgets}
|
|
|
|
|
>
|
|
|
|
|
<CutIcon color="black" height={16} width={16} />
|
|
|
|
|
</StyledAction>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
{/* delete widgets */}
|
|
|
|
|
<Tooltip
|
|
|
|
|
boundary="viewport"
|
|
|
|
|
content={deleteHelpText}
|
|
|
|
|
maxWidth="400px"
|
|
|
|
|
modifiers={PopoverModifiers}
|
2022-06-24 14:23:02 +00:00
|
|
|
position="right"
|
2021-07-08 06:30:19 +00:00
|
|
|
>
|
|
|
|
|
<StyledAction
|
|
|
|
|
onClick={stopEventPropagation}
|
|
|
|
|
onClickCapture={onDeleteSelectedWidgets}
|
|
|
|
|
>
|
|
|
|
|
<DeleteIcon color="black" height={16} width={16} />
|
|
|
|
|
</StyledAction>
|
|
|
|
|
</Tooltip>
|
2021-08-25 05:00:31 +00:00
|
|
|
{/* group widgets */}
|
|
|
|
|
<Tooltip
|
|
|
|
|
boundary="viewport"
|
|
|
|
|
content={groupHelpText}
|
|
|
|
|
maxWidth="400px"
|
|
|
|
|
modifiers={PopoverModifiers}
|
2022-06-24 14:23:02 +00:00
|
|
|
position="right"
|
2021-08-25 05:00:31 +00:00
|
|
|
>
|
|
|
|
|
<StyledAction
|
|
|
|
|
onClick={stopEventPropagation}
|
|
|
|
|
onClickCapture={onGroupWidgets}
|
|
|
|
|
>
|
|
|
|
|
<GroupIcon color="black" height={16} width={16} />
|
|
|
|
|
</StyledAction>
|
|
|
|
|
</Tooltip>
|
2021-07-08 06:30:19 +00:00
|
|
|
</StyledActions>
|
|
|
|
|
</StyledActionsContainer>
|
|
|
|
|
</StyledSelectionBox>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default WidgetsMultiSelectBox;
|