PromucFlow_constructor/app/client/src/components/editorComponents/ResizableComponent.tsx
Hetu Nandu aa9b19c995
refactor: Widget Selection (#19643)
## Description

This change is a refactor of widget selection logic. It consolidates all
the business logic to make it easy to maintain. It also improves the
performance a bit.

It touched a lot of features as we heavily rely on this 

```
Select one
Select multiple with drag
Select multiple with shift
Select multiple with cmd/ctrl
Selections should be on the same level of hierarchy
Unselect all by clicking on the canvas
Unselect all by pressing esc
Select all with cmd + a
Paste in main container
Paste in another container
Undo
Redo
Modal Selection
Modal child selection
Context switching
cmd click
snipping mode
new widget suggestion
onboarding
```

> Refactor widget selection logic

Fixes #19570

## Type of change

- Refactor


## How Has This Been Tested?

All existing tests should pass

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

### Issues raised during DP testing

https://github.com/appsmithorg/appsmith/pull/19643#issuecomment-1383570810

https://github.com/appsmithorg/appsmith/pull/19643#issuecomment-1383607820

https://github.com/appsmithorg/appsmith/pull/19643#issuecomment-1385095478
[Bug bash
issues](https://www.notion.so/appsmith/610aa302f3e146a7b090b7dc6bc63ef9?v=0d277a9b07bf4aac9d717bcaf138c33a)

## 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
2023-01-28 07:47:06 +05:30

325 lines
10 KiB
TypeScript

import React, { memo, useContext, useMemo } from "react";
import {
WidgetOperations,
WidgetProps,
WidgetRowCols,
} from "widgets/BaseWidget";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import {
computeFinalRowCols,
computeRowCols,
UIElementSize,
} from "./ResizableUtils";
import {
useShowPropertyPane,
useShowTableFilterPane,
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
import { useSelector } from "react-redux";
import { AppState } from "@appsmith/reducers";
import Resizable from "resizable/resizenreflow";
import { get, omit } from "lodash";
import { getSnapColumns } from "utils/WidgetPropsUtils";
import {
BottomHandleStyles,
BottomLeftHandleStyles,
BottomRightHandleStyles,
LeftHandleStyles,
RightHandleStyles,
TopHandleStyles,
TopLeftHandleStyles,
TopRightHandleStyles,
VisibilityContainer,
} from "./ResizeStyledComponents";
import AnalyticsUtil from "utils/AnalyticsUtil";
import {
previewModeSelector,
snipingModeSelector,
} from "selectors/editorSelectors";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
import { focusWidget } from "actions/widgetActions";
import { GridDefaults } from "constants/WidgetConstants";
import { DropTargetContext } from "./DropTargetComponent";
import { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas";
import { isAutoHeightEnabledForWidget } from "widgets/WidgetUtils";
import {
getParentToOpenSelector,
isCurrentWidgetFocused,
isCurrentWidgetLastSelected,
isMultiSelectedWidget,
isWidgetSelected,
} from "selectors/widgetSelectors";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
export type ResizableComponentProps = WidgetProps & {
paddingOffset: number;
};
export const ResizableComponent = memo(function ResizableComponent(
props: ResizableComponentProps,
) {
// Fetch information from the context
const { updateWidget } = useContext(EditorContext);
const isSnipingMode = useSelector(snipingModeSelector);
const isPreviewMode = useSelector(previewModeSelector);
const showPropertyPane = useShowPropertyPane();
const showTableFilterPane = useShowTableFilterPane();
const { selectWidget } = useWidgetSelection();
const { setIsResizing } = useWidgetDragResize();
// Check if current widget is in the list of selected widgets
const isSelected = useSelector(isWidgetSelected(props.widgetId));
// Check if current widget is the last selected widget
const isLastSelected = useSelector(
isCurrentWidgetLastSelected(props.widgetId),
);
const isFocused = useSelector(isCurrentWidgetFocused(props.widgetId));
// Check if current widget is one of multiple selected widgets
const isMultiSelected = useSelector(isMultiSelectedWidget(props.widgetId));
const isDragging = useSelector(
(state: AppState) => state.ui.widgetDragResize.isDragging,
);
const isResizing = useSelector(
(state: AppState) => state.ui.widgetDragResize.isResizing,
);
const parentWidgetToSelect = useSelector(
getParentToOpenSelector(props.widgetId),
);
const isParentWidgetSelected = useSelector(
isCurrentWidgetLastSelected(parentWidgetToSelect?.widgetId || ""),
);
const isWidgetFocused = isFocused || isLastSelected || isSelected;
// Calculate the dimensions of the widget,
// The ResizableContainer's size prop is controlled
const dimensions: UIElementSize = {
width:
(props.rightColumn - props.leftColumn) * props.parentColumnSpace -
2 * props.paddingOffset,
height:
(props.bottomRow - props.topRow) * props.parentRowSpace -
2 * props.paddingOffset,
};
// onResize handler
const getResizedPositions = (
newDimensions: UIElementSize,
position: XYCord,
) => {
const delta: UIElementSize = {
height: newDimensions.height - dimensions.height,
width: newDimensions.width - dimensions.width,
};
const newRowCols: WidgetRowCols = computeRowCols(delta, position, props);
let canResizeVertically = true;
let canResizeHorizontally = true;
// this is required for list widget so that template have no collision
if (props.ignoreCollision)
return {
canResizeHorizontally,
canResizeVertically,
};
if (
newRowCols &&
(newRowCols.rightColumn > getSnapColumns() ||
newRowCols.leftColumn < 0 ||
newRowCols.rightColumn - newRowCols.leftColumn < 2)
) {
canResizeHorizontally = false;
}
if (
newRowCols &&
(newRowCols.topRow < 0 || newRowCols.bottomRow - newRowCols.topRow < 4)
) {
canResizeVertically = false;
}
const resizedPositions = {
id: props.widgetId,
left: newRowCols.leftColumn,
top: newRowCols.topRow,
bottom: newRowCols.bottomRow,
right: newRowCols.rightColumn,
};
if (isAutoHeightEnabledForWidget(props)) {
canResizeVertically = false;
resizedPositions.top = props.topRow;
resizedPositions.bottom = props.bottomRow;
}
// Check if new row cols are occupied by sibling widgets
return {
canResizeHorizontally,
canResizeVertically,
resizedPositions,
};
};
// onResizeStop handler
// when done resizing, check if;
// 1) There is no collision
// 2) There is a change in widget size
// Update widget, if both of the above are true.
const updateSize = (newDimensions: UIElementSize, position: XYCord) => {
// Get the difference in size of the widget, before and after resizing.
const delta: UIElementSize = {
height: newDimensions.height - dimensions.height,
width: newDimensions.width - dimensions.width,
};
// Get the updated Widget rows and columns props
// False, if there is collision
// False, if none of the rows and cols have changed.
const newRowCols: WidgetRowCols | false = computeFinalRowCols(
delta,
position,
props,
);
if (newRowCols) {
updateWidget &&
updateWidget(WidgetOperations.RESIZE, props.widgetId, {
...newRowCols,
parentId: props.parentId,
snapColumnSpace: props.parentColumnSpace,
snapRowSpace: props.parentRowSpace,
});
}
// Tell the Canvas that we've stopped resizing
// Put it later in the stack so that other updates like click, are not propagated to the parent container
setTimeout(() => {
setIsResizing && setIsResizing(false);
}, 0);
// Tell the Canvas to put the focus back to this widget
// By setting the focus, we enable the control buttons on the widget
selectWidget &&
!isLastSelected &&
parentWidgetToSelect?.widgetId !== props.widgetId &&
selectWidget(SelectionRequestType.One, [props.widgetId]);
if (parentWidgetToSelect) {
selectWidget &&
!isParentWidgetSelected &&
selectWidget(SelectionRequestType.One, [parentWidgetToSelect.widgetId]);
focusWidget(parentWidgetToSelect.widgetId);
} else {
selectWidget &&
!isLastSelected &&
selectWidget(SelectionRequestType.One, [props.widgetId]);
}
// Property pane closes after a resize/drag
showPropertyPane && showPropertyPane();
AnalyticsUtil.logEvent("WIDGET_RESIZE_END", {
widgetName: props.widgetName,
widgetType: props.type,
startHeight: dimensions.height,
startWidth: dimensions.width,
endHeight: newDimensions.height,
endWidth: newDimensions.width,
});
};
const handleResizeStart = () => {
setIsResizing && !isResizing && setIsResizing(true);
selectWidget &&
!isLastSelected &&
selectWidget(SelectionRequestType.One, [props.widgetId]);
// Make sure that this tableFilterPane should close
showTableFilterPane && showTableFilterPane();
AnalyticsUtil.logEvent("WIDGET_RESIZE_START", {
widgetName: props.widgetName,
widgetType: props.type,
});
};
const handles = useMemo(() => {
const allHandles = {
left: LeftHandleStyles,
top: TopHandleStyles,
bottom: BottomHandleStyles,
right: RightHandleStyles,
bottomRight: BottomRightHandleStyles,
topLeft: TopLeftHandleStyles,
topRight: TopRightHandleStyles,
bottomLeft: BottomLeftHandleStyles,
};
return omit(allHandles, get(props, "disabledResizeHandles", []));
}, [props]);
const isEnabled =
!isDragging &&
isWidgetFocused &&
!props.resizeDisabled &&
!isSnipingMode &&
!isPreviewMode;
const { updateDropTargetRows } = useContext(DropTargetContext);
const gridProps = {
parentColumnSpace: props.parentColumnSpace,
parentRowSpace: props.parentRowSpace,
paddingOffset: props.paddingOffset,
maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
};
const originalPositions = {
id: props.widgetId,
left: props.leftColumn,
top: props.topRow,
bottom: props.bottomRow,
right: props.rightColumn,
};
const updateBottomRow = (bottom: number) => {
if (props.parentId) {
updateDropTargetRows && updateDropTargetRows([props.parentId], bottom);
}
};
const snapGrid = useMemo(
() => ({
x: props.parentColumnSpace,
y: props.parentRowSpace,
}),
[props.parentColumnSpace, props.parentRowSpace],
);
const isVerticalResizeEnabled = useMemo(() => {
return !isAutoHeightEnabledForWidget(props) && isEnabled;
}, [props, isAutoHeightEnabledForWidget, isEnabled]);
return (
<Resizable
allowResize={!isMultiSelected}
componentHeight={dimensions.height}
componentWidth={dimensions.width}
enableHorizontalResize={isEnabled}
enableVerticalResize={isVerticalResizeEnabled}
getResizedPositions={getResizedPositions}
gridProps={gridProps}
handles={handles}
onStart={handleResizeStart}
onStop={updateSize}
originalPositions={originalPositions}
parentId={props.parentId}
snapGrid={snapGrid}
updateBottomRow={updateBottomRow}
widgetId={props.widgetId}
// Used only for performance tracking, can be removed after optimization.
zWidgetId={props.widgetId}
zWidgetType={props.type}
>
<VisibilityContainer
padding={props.paddingOffset}
visible={!!props.isVisible}
>
{props.children}
</VisibilityContainer>
</Resizable>
);
});
export default ResizableComponent;