PromucFlow_constructor/app/client/src/pages/Editor/WidgetsMultiSelectBox.tsx
albinAppsmith 629999f124
feat: [epic] appsmith design system version 2 deduplication (#22030)
## Description

### Fixes
- [x] https://github.com/appsmithorg/appsmith/issues/19383
- [x] https://github.com/appsmithorg/appsmith/issues/19384
- [x] https://github.com/appsmithorg/appsmith/issues/19385
- [x] https://github.com/appsmithorg/appsmith/issues/19386
- [x] https://github.com/appsmithorg/appsmith/issues/19387
- [x] https://github.com/appsmithorg/appsmith/issues/19388
- [x] https://github.com/appsmithorg/appsmith/issues/19389
- [x] https://github.com/appsmithorg/appsmith/issues/19390
- [x] https://github.com/appsmithorg/appsmith/issues/19391
- [x] https://github.com/appsmithorg/appsmith/issues/19392
- [x] https://github.com/appsmithorg/appsmith/issues/19393
- [x] https://github.com/appsmithorg/appsmith/issues/19394
- [x] https://github.com/appsmithorg/appsmith/issues/19395
- [x] https://github.com/appsmithorg/appsmith/issues/19396
- [x] https://github.com/appsmithorg/appsmith/issues/19397
- [x] https://github.com/appsmithorg/appsmith/issues/19398
- [x] https://github.com/appsmithorg/appsmith/issues/19399
- [x] https://github.com/appsmithorg/appsmith/issues/19400
- [x] https://github.com/appsmithorg/appsmith/issues/19401
- [x] https://github.com/appsmithorg/appsmith/issues/19402
- [x] https://github.com/appsmithorg/appsmith/issues/19403
- [x] https://github.com/appsmithorg/appsmith/issues/19404
- [x] https://github.com/appsmithorg/appsmith/issues/19405
- [x] https://github.com/appsmithorg/appsmith/issues/19406
- [x] https://github.com/appsmithorg/appsmith/issues/19407
- [x] https://github.com/appsmithorg/appsmith/issues/19408
- [x] https://github.com/appsmithorg/appsmith/issues/19409

Fixes # (issue)
> if no issue exists, please create an issue and ask the maintainers
about this first


Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video


## Type of change

> Please delete options that are not relevant.

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update


## How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Provide
instructions, so we can reproduce.
> Please also list any relevant details for your test configuration.
> Delete anything that is not important

- Manual
- Jest
- Cypress

### Test Plan
> Add Testsmith test cases links that relate to this PR

### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)


## Checklist:
### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test

---------

Co-authored-by: Ankita Kinger <ankita@appsmith.com>
Co-authored-by: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com>
Co-authored-by: Tanvi Bhakta <tanvi@appsmith.com>
Co-authored-by: Arsalan <arsalanyaldram0211@outlook.com>
Co-authored-by: Aman Agarwal <aman@appsmith.com>
Co-authored-by: Rohit Agarwal <rohit_agarwal@live.in>
Co-authored-by: Nilesh Sarupriya <nilesh@appsmith.com>
Co-authored-by: Nilesh Sarupriya <20905988+nsarupr@users.noreply.github.com>
Co-authored-by: Tanvi Bhakta <tanvibhakta@gmail.com>
Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
Co-authored-by: Parthvi Goswami <parthvigoswami@Parthvis-MacBook-Pro.local>
Co-authored-by: Vijetha-Kaja <vijetha@appsmith.com>
Co-authored-by: Parthvi <80334441+Parthvi12@users.noreply.github.com>
Co-authored-by: Apple <nandan@thinkify.io>
Co-authored-by: Saroj <43822041+sarojsarab@users.noreply.github.com>
Co-authored-by: Sangeeth Sivan <74818788+berzerkeer@users.noreply.github.com>
Co-authored-by: Ashok Kumar M <35134347+marks0351@users.noreply.github.com>
Co-authored-by: Aishwarya-U-R <91450662+Aishwarya-U-R@users.noreply.github.com>
Co-authored-by: rahulramesha <rahul@appsmith.com>
Co-authored-by: Aswath K <aswath.sana@gmail.com>
Co-authored-by: Preet Sidhu <preetsidhu.bits@gmail.com>
Co-authored-by: Vijetha-Kaja <119562824+Vijetha-Kaja@users.noreply.github.com>
Co-authored-by: Shrikant Sharat Kandula <shrikant@appsmith.com>
2023-05-20 00:07:06 +05:30

402 lines
10 KiB
TypeScript

import React, { useMemo, useRef } from "react";
import styled from "styled-components";
import { get, minBy } from "lodash";
import { useSelector, useDispatch } from "react-redux";
import {
copyWidget,
cutWidget,
groupWidgets,
deleteSelectedWidget,
} from "actions/widgetActions";
import { modText } from "utils/helpers";
import { Layers } from "constants/Layers";
import { TooltipComponent as Tooltip } from "design-system-old";
import { getSelectedWidgets } from "selectors/ui";
import { stopEventPropagation } from "utils/AppsmithUtils";
import { getCanvasWidgets } from "selectors/entitiesSelector";
import type { IPopoverSharedProps } from "@blueprintjs/core";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
import WidgetFactory from "utils/WidgetFactory";
import type { AppState } from "@appsmith/reducers";
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
import { getBoundariesFromSelectedWidgets } from "sagas/WidgetOperationUtils";
import { CONTAINER_GRID_PADDING } from "constants/WidgetConstants";
import { Icon } from "design-system";
const WidgetTypes = WidgetFactory.widgetTypes;
const StyledSelectionBox = styled.div`
position: absolute;
cursor: grab;
`;
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: 0px;
`;
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: 0px;
`;
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: 0px;
`;
export const PopoverModifiers: IPopoverSharedProps["modifiers"] = {
offset: {
enabled: true,
},
arrow: {
enabled: false,
},
};
/**
* helper text that comes in popover on hover of actions in context menu
* @returns
*/
const copyHelpText = (
<>
Click or <b>{modText()} C</b> & {modText()} V
</>
);
const cutHelpText = (
<>
Click or <b>{modText()} X</b> & {modText()} V
</>
);
const deleteHelpText = (
<>
Click or <b> Del </b>
</>
);
const groupHelpText = (
<>
Click or <b>{modText()} G to group</b>
</>
);
function WidgetsMultiSelectBox(props: {
widgetId: string;
widgetType: string;
noContainerOffset: boolean;
snapColumnSpace: number;
snapRowSpace: number;
}): any {
const dispatch = useDispatch();
const canvasWidgets = useSelector(getCanvasWidgets);
const selectedWidgetIDs = useSelector(getSelectedWidgets);
const selectedWidgets = selectedWidgetIDs.map(
(widgetID) => canvasWidgets[widgetID],
);
const { focusWidget } = useWidgetSelection();
const isDragging = useSelector(
(state: AppState) => state.ui.widgetDragResize.isDragging,
);
/**
* 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(() => {
if (isDragging) {
return false;
}
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
);
}, [selectedWidgets, isDragging]);
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,
});
}
};
/**
* calculate bounding box
*/
const { height, left, top, width } = useMemo(() => {
if (shouldRender) {
const { leftMostColumn, topMostRow, totalHeight, totalWidth } =
getBoundariesFromSelectedWidgets(selectedWidgets);
return {
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,
};
}
return {};
}, [
selectedWidgets,
props.snapColumnSpace,
props.snapRowSpace,
props.noContainerOffset,
]);
/**
* 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));
};
/**
* group widgets into container
*
* @param e
*/
const onGroupWidgets = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => {
e.preventDefault();
e.stopPropagation();
dispatch(groupWidgets());
};
if (!shouldRender) return false;
return (
<StyledSelectionBox
className="t--multi-selection-box"
data-testid="t--selection-box"
draggable
key={`selection-box-${props.widgetId}`}
onDragStart={onDragStart}
onMouseMove={() => focusWidget()}
onMouseOver={() => focusWidget()}
ref={draggableRef}
style={{
left,
top,
height,
width,
}}
>
<StyledSelectBoxHandleTop />
<StyledSelectBoxHandleLeft />
<StyledSelectBoxHandleRight />
<StyledSelectBoxHandleBottom />
<StyledActionsContainer>
<StyledActions>
{/* copy widgets */}
<Tooltip
boundary="viewport"
content={copyHelpText}
maxWidth="400px"
modifiers={PopoverModifiers}
position="right"
>
<StyledAction
onClick={stopEventPropagation}
onClickCapture={onCopySelectedWidgets}
>
<Icon name="duplicate" size="md" />
</StyledAction>
</Tooltip>
{/* cut widgets */}
<Tooltip
boundary="viewport"
content={cutHelpText}
maxWidth="400px"
modifiers={PopoverModifiers}
position="right"
>
<StyledAction
onClick={stopEventPropagation}
onClickCapture={onCutSelectedWidgets}
>
<Icon name="cut-control" size="md" />
</StyledAction>
</Tooltip>
{/* delete widgets */}
<Tooltip
boundary="viewport"
content={deleteHelpText}
maxWidth="400px"
modifiers={PopoverModifiers}
position="right"
>
<StyledAction
onClick={stopEventPropagation}
onClickCapture={onDeleteSelectedWidgets}
>
<Icon name="delete-bin-line" size="md" />
</StyledAction>
</Tooltip>
{/* group widgets */}
<Tooltip
boundary="viewport"
content={groupHelpText}
maxWidth="400px"
modifiers={PopoverModifiers}
position="right"
>
<StyledAction
onClick={stopEventPropagation}
onClickCapture={onGroupWidgets}
>
<Icon name="group-control" size="sm" />
</StyledAction>
</Tooltip>
</StyledActions>
</StyledActionsContainer>
</StyledSelectionBox>
);
}
export default WidgetsMultiSelectBox;