PromucFlow_constructor/app/client/src/layoutSystems/common/draggable/DraggableComponent.tsx
Hetu Nandu e99cc39e47
chore: Block Selections when Canvas is in Side by Side mode (#31587)
## Description

This pull request aims to enhance the user experience within the
application by modifying the canvas behavior when it is displayed in
"Side by Side" mode alongside Queries or JavaScript sections. The key
change is the disabling of direct selections on the canvas, allowing
interactions with canvas elements only through cmd + click or by
clicking on the widget's name. This adjustment is intended to facilitate
a view-only mode for the canvas during Queries or JS editing, thereby
improving layout and user interaction.

Additionally, the PR introduces enhancements to the application's
testing framework, focusing on improving test reliability in scenarios
involving UI interaction and state changes. Notable updates include:

- Improved error tooltip handling in CurrencyInput_spec.js.
- Ensured page state saving before verifying element presence in
Listv2_BasicChildWidgetInteraction_spec.js.
- Replaced cy.wait("@updateLayout") with cy.assertPageSave() and
introduced a delay in Listv2_spec.js to accommodate functionality
changes.
- Implemented visibility checks in
TableV2_Button_Icon_validation_spec.js to prevent timing-related test
failures.
These technical updates collectively aim to bolster the application's
testing framework, enhancing the reliability and accuracy of automated
tests, especially in UI interaction and state change scenarios.


#### PR fixes following issue(s)
Fixes #30864

## Automation

/ok-to-test tags="@tag.Widget"
<!-- This is an auto-generated comment: Cypress test results  -->
> [!IMPORTANT]  
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/8259916944>
> Commit: `15e1cf937a9d15adaea68e16a55006d993a07cbf`
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8259916944&attempt=1"
target="_blank">Click here!</a>
> All cypress tests have passed 🎉🎉🎉

<!-- end of auto-generated comment: Cypress test results  -->





















<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
	- Added new constants for widget selection and focus management.
- Introduced a new event type for tracking widget selections in code
mode.
- **Tests**
- Enhanced test assertions and interactions for better reliability and
error handling in various widgets.
- **Refactor**
- Improved widget selection logic and URL handling for a more intuitive
user experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-13 11:53:49 +05:30

176 lines
5.9 KiB
TypeScript

import type { AppState } from "@appsmith/reducers";
import { getColorWithOpacity } from "constants/DefaultTheme";
import { WIDGET_PADDING } from "constants/WidgetConstants";
import type { CSSProperties, DragEventHandler, ReactNode } from "react";
import React, { useMemo, useRef } from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import {
isCurrentWidgetFocused,
isWidgetSelected,
} from "selectors/widgetSelectors";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
import type { SetDraggingStateActionPayload } from "utils/hooks/dragResizeHooks";
import {
useShowTableFilterPane,
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
import { getShouldAllowDrag } from "selectors/widgetDragSelectors";
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
import { getAnvilSpaceDistributionStatus } from "layoutSystems/anvil/integrations/selectors";
const DraggableWrapper = styled.div<{ draggable: boolean }>`
display: block;
flex-direction: column;
width: 100%;
height: 100%;
user-select: none;
cursor: ${(props) => (props.draggable ? "grab" : "unset")};
`;
export interface DraggableComponentProps {
widgetId: string;
parentId?: string;
isFlexChild?: boolean;
resizeDisabled?: boolean;
type: string;
children: ReactNode;
generateDragState: (
e: React.DragEvent,
draggableRef: HTMLElement,
) => SetDraggingStateActionPayload;
dragDisabled: boolean;
}
// Widget Boundaries which is shown to indicate the boundaries of the widget
const WidgetBoundaries = styled.div`
z-index: 0;
width: calc(100% + ${WIDGET_PADDING - 2}px);
height: calc(100% + ${WIDGET_PADDING - 2}px);
position: absolute;
border: 1px dashed
${(props) => getColorWithOpacity(props.theme.colors.textAnchor, 0.5)};
pointer-events: none;
top: 0;
left: 0;
`;
function DraggableComponent(props: DraggableComponentProps) {
// Dispatch hook handy to set a widget as focused/selected
const { focusWidget, selectWidget } = useWidgetSelection();
const shouldAllowDrag = useSelector(getShouldAllowDrag);
// Dispatch hook handy to set any `DraggableComponent` as dragging/ not dragging
// The value is boolean
const { setDraggingState } = useWidgetDragResize();
const showTableFilterPane = useShowTableFilterPane();
const isSelected = useSelector(isWidgetSelected(props.widgetId));
// This state tels us which widget is focused
// The value is the widgetId of the focused widget.
const isFocused = useSelector(isCurrentWidgetFocused(props.widgetId));
// This state tells us whether a `ResizableComponent` is resizing
const isResizing = useSelector(
(state: AppState) => state.ui.widgetDragResize.isResizing,
);
// This state tells us whether space redistribution is in process
const isDistributingSpace = useSelector(getAnvilSpaceDistributionStatus);
// This state tells us whether a `DraggableComponent` is dragging
const isDragging = useSelector(
(state: AppState) => state.ui.widgetDragResize.isDragging,
);
const isDraggingSibling = useSelector(
(state) =>
state.ui.widgetDragResize?.dragDetails?.draggedOn === props.parentId,
);
const isPreviewMode = useSelector(combinedPreviewModeSelector);
// True when any widget is dragging or resizing, including this one
const isResizingOrDragging = !!isResizing || !!isDragging;
const isCurrentWidgetDragging = isDragging && isSelected;
const isCurrentWidgetResizing = isResizing && isSelected;
const showBoundary =
!props.isFlexChild && (isCurrentWidgetDragging || isDraggingSibling);
// When mouse is over this draggable
const handleMouseOver = (e: React.MouseEvent) => {
focusWidget &&
!isResizingOrDragging &&
!isFocused &&
!isDistributingSpace &&
!props.resizeDisabled &&
!isPreviewMode &&
focusWidget(props.widgetId, e.metaKey);
e.stopPropagation();
};
const handleMouseLeave = () => {
// on leaving a widget, we reset the focused widget
focusWidget && focusWidget();
};
// Display this draggable based on the current drag state
const dragWrapperStyle: CSSProperties = {
display: !props.isFlexChild && isCurrentWidgetDragging ? "none" : "block",
};
const dragBoundariesStyle: React.CSSProperties = useMemo(() => {
return {
opacity: !isResizingOrDragging || isCurrentWidgetResizing ? 0 : 1,
};
}, [isResizingOrDragging, isCurrentWidgetResizing]);
const classNameForTesting = `t--draggable-${props.type
.split("_")
.join("")
.toLowerCase()}`;
const allowDrag = !props.dragDisabled && shouldAllowDrag;
const className = `${classNameForTesting}`;
const draggableRef = useRef<HTMLDivElement>(null);
const onDragStart: DragEventHandler = (e) => {
e.preventDefault();
e.stopPropagation();
// allowDrag check is added as react jest test simulation is not respecting default behaviour
// of draggable=false and triggering onDragStart. allowDrag condition check is purely for the test cases.
if (allowDrag && draggableRef.current && !(e.metaKey || e.ctrlKey)) {
if (!isFocused) return;
if (!isSelected) {
selectWidget(SelectionRequestType.One, [props.widgetId]);
}
showTableFilterPane();
const draggingState = props.generateDragState(e, draggableRef.current);
setDraggingState(draggingState);
}
};
return (
<DraggableWrapper
className={className}
data-testid={isSelected ? "t--selected" : ""}
draggable={allowDrag}
onDragStart={onDragStart}
onMouseLeave={handleMouseLeave}
onMouseOver={handleMouseOver}
ref={draggableRef}
style={dragWrapperStyle}
>
{props.children}
{showBoundary && (
<WidgetBoundaries
className={`widget-boundary-${props.widgetId}`}
style={dragBoundariesStyle}
/>
)}
</DraggableWrapper>
);
}
export default DraggableComponent;