fix: Anvil Widgets not accessible when widget has no content. (#30780)
> Pull Request Template > > Use this template to quickly create a well written pull request. Delete all quotes before creating the pull request. > ## Description In this PR, we are fixing a few issues and also destructuring AnvilFlexComponent for edit and view. Issues Fixed - Widgets without any content like a text widget without text are not hoverable - Modal once opened does not close when other widgets on the canvas are selected via the enitty explorer. Anvil Flex Component was common component inspired from PositionedContainer of Fixed layout. It had all features of Edit and View together in one place. This mean viewer was unnecessarily interpreting more code. Now AnvilFlexComponent has been broken into AnvilFlexComponent and AnvilEditorFlexComponent. AnvilEditorFlexComponent is a wrapper around AnvilFlexComponent with abilities needed for Edit Mode. Another issue addressed in the PR is removal of DraggableComponennt, which was just making dragging possible and providing a few styles like fading the widget when it is being dragged. With this PR all the above mentioned functions will be taken care of by AnvilEditorFlexComponent. #### PR fixes following issue(s) Fixes #30734 > 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 > > > ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] JUnit - [ ] 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: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced feature flag management with additional flags for better control over application features. - Introduced a new editor component for Anvil layout system, improving layout and behavior management in edit mode. - Added a custom hook for managing hover states on Anvil widgets, enhancing user interaction. - **Refactor** - Updated AnvilFlexComponent to use `forwardRef` for better ref management and optimized widget configuration and rendering logic. - Modified selector logic to simplify the retrieval of layout system type, enhancing code maintainability. - Adjusted test methodologies to improve reliability and accuracy. - **Bug Fixes** - Corrected assertions in Cypress end-to-end tests to accurately locate and interact with widgets in the Anvil canvas, ensuring test reliability. - **Chores** - Updated common locators and assertion methods in Cypress support files for consistency and clarity in test scripts. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
4be051f143
commit
c37d0c283f
|
|
@ -70,8 +70,13 @@ describe(
|
|||
);
|
||||
agHelper.AssertElementExist(appSettings.locators._sideNavbar);
|
||||
agHelper.GetNClick(locators._canvas);
|
||||
agHelper.AssertElementExist(locators._widgetInCanvas(WIDGET.WDSINPUT));
|
||||
agHelper.AssertElementExist(locators._widgetInCanvas(WIDGET.WDSINPUT), 1);
|
||||
agHelper.AssertElementExist(
|
||||
locators._anvilWidgetInCanvas(WIDGET.WDSINPUT),
|
||||
);
|
||||
agHelper.AssertElementExist(
|
||||
locators._anvilWidgetInCanvas(WIDGET.WDSINPUT),
|
||||
1,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ describe(
|
|||
},
|
||||
);
|
||||
agHelper.AssertElementLength(
|
||||
locators._widgetInCanvas(WIDGET.WDSBUTTON),
|
||||
locators._anvilWidgetInCanvas(WIDGET.WDSBUTTON),
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class CommonLocators {
|
|||
_publishButton = ".t--application-publish-btn";
|
||||
_widgetInCanvas = (widgetType: string) => `.t--draggable-${widgetType}`;
|
||||
_widgetInDeployed = (widgetType: string) => `.t--widget-${widgetType}`;
|
||||
_anvilWidgetInCanvas = this._widgetInDeployed;
|
||||
_widgetInputSelector = (widgetType: string) =>
|
||||
this._widgetInDeployed(widgetType) + " input";
|
||||
_textWidgetInDeployed = this._widgetInDeployed("textwidget") + " span";
|
||||
|
|
|
|||
|
|
@ -114,7 +114,9 @@ export class AnvilLayout {
|
|||
) {
|
||||
this.DragNDropAnvilWidget(widgetType, x, y, options);
|
||||
this.agHelper.AssertAutoSave(); //settling time for widget on canvas!
|
||||
this.agHelper.AssertElementExist(this.locator._widgetInCanvas(widgetType));
|
||||
this.agHelper.AssertElementExist(
|
||||
this.locator._anvilWidgetInCanvas(widgetType),
|
||||
);
|
||||
this.agHelper.Sleep(200); //waiting a bit for widget properties to open
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1684,7 +1684,7 @@ export class DataSources {
|
|||
force,
|
||||
);
|
||||
this.agHelper.AssertElementVisibility(
|
||||
this.locator._widgetInCanvas(WIDGET.WDSTABLE),
|
||||
this.locator._anvilWidgetInCanvas(WIDGET.WDSTABLE),
|
||||
);
|
||||
break;
|
||||
case Widgets.Chart:
|
||||
|
|
|
|||
|
|
@ -1,34 +1,26 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { CSSProperties, MouseEventHandler } from "react";
|
||||
import React, { forwardRef, useEffect, useMemo, useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
import { Flex } from "@design-system/widgets";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { snipingModeSelector } from "selectors/editorSelectors";
|
||||
import { usePositionedContainerZIndex } from "utils/hooks/usePositionedContainerZIndex";
|
||||
import {
|
||||
isCurrentWidgetFocused,
|
||||
isWidgetSelected,
|
||||
} from "selectors/widgetSelectors";
|
||||
import { widgetTypeClassname } from "widgets/WidgetUtils";
|
||||
import {
|
||||
FlexVerticalAlignment,
|
||||
ResponsiveBehavior,
|
||||
} from "layoutSystems/common/utils/constants";
|
||||
import type { FlexProps } from "@design-system/widgets/src/components/Flex/src/types";
|
||||
import { checkIsDropTarget } from "WidgetProvider/factory/helpers";
|
||||
import type { AnvilFlexComponentProps } from "../utils/types";
|
||||
import WidgetFactory from "WidgetProvider/factory";
|
||||
import type { WidgetProps } from "widgets/BaseWidget";
|
||||
import type { WidgetConfigProps } from "WidgetProvider/constants";
|
||||
import { usePositionObserver } from "layoutSystems/common/utils/LayoutElementPositionsObserver/usePositionObserver";
|
||||
import { useWidgetBorderStyles } from "./hooks/useWidgetBorderStyles";
|
||||
import { getAnvilWidgetDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import { SELECT_ANVIL_WIDGET_CUSTOM_EVENT } from "../utils/constants";
|
||||
|
||||
const anvilWidgetStyleProps: CSSProperties = {
|
||||
position: "relative",
|
||||
// overflow is set to make sure widgets internal components/divs don't overflow this boundary causing scrolls
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds following functionalities to the widget:
|
||||
* 1. Click handler to select the widget and open property pane.
|
||||
* Adds the following functionalities to the widget:
|
||||
* 1. Click handler to select the widget and open the property pane.
|
||||
* 2. Widget size based on responsiveBehavior:
|
||||
* 2a. Hug widgets will stick to the size provided to them. (flex: 0 0 auto;)
|
||||
* 2b. Fill widgets will automatically take up all available width in the parent container. (flex: 1 1 0%;)
|
||||
|
|
@ -38,136 +30,71 @@ import { SELECT_ANVIL_WIDGET_CUSTOM_EVENT } from "../utils/constants";
|
|||
* @param props | AnvilFlexComponentProps
|
||||
* @returns Widget
|
||||
*/
|
||||
export const AnvilFlexComponent = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
flexGrow,
|
||||
widgetId,
|
||||
widgetSize,
|
||||
widgetType,
|
||||
}: AnvilFlexComponentProps,
|
||||
ref: any,
|
||||
) => {
|
||||
// State to manage whether the widget is a fill widget
|
||||
const [isFillWidget, setIsFillWidget] = useState<boolean>(false);
|
||||
|
||||
export function AnvilFlexComponent(props: AnvilFlexComponentProps) {
|
||||
const isDropTarget = checkIsDropTarget(props.widgetType);
|
||||
const isFocused = useSelector(isCurrentWidgetFocused(props.widgetId));
|
||||
const isSelected = useSelector(isWidgetSelected(props.widgetId));
|
||||
const isSnipingMode = useSelector(snipingModeSelector);
|
||||
const isDragging = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.isDragging,
|
||||
);
|
||||
// State to manage vertical alignment of the widget
|
||||
const [verticalAlignment, setVerticalAlignment] =
|
||||
useState<FlexVerticalAlignment>(FlexVerticalAlignment.Top);
|
||||
|
||||
/** POSITIONS OBSERVER LOGIC */
|
||||
// Create a ref so that this DOM node can be
|
||||
// observed by the observer for changes in size
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
usePositionObserver(
|
||||
"widget",
|
||||
{ widgetId: props.widgetId, layoutId: props.layoutId },
|
||||
ref,
|
||||
);
|
||||
/** EO POSITIONS OBSERVER LOGIC */
|
||||
// Effect to update state based on widget type
|
||||
useEffect(() => {
|
||||
const widgetConfig:
|
||||
| (Partial<WidgetProps> & WidgetConfigProps & { type: string })
|
||||
| undefined = WidgetFactory.getConfig(widgetType);
|
||||
if (!widgetConfig) return;
|
||||
setIsFillWidget(
|
||||
widgetConfig?.responsiveBehavior === ResponsiveBehavior.Fill,
|
||||
);
|
||||
setVerticalAlignment(
|
||||
widgetConfig?.flexVerticalAlignment || FlexVerticalAlignment.Top,
|
||||
);
|
||||
}, [widgetType]);
|
||||
|
||||
const [isFillWidget, setIsFillWidget] = useState<boolean>(false);
|
||||
const [verticalAlignment, setVerticalAlignment] =
|
||||
useState<FlexVerticalAlignment>(FlexVerticalAlignment.Top);
|
||||
|
||||
const onClickFn = useCallback(
|
||||
function () {
|
||||
if (ref.current && isFocused) {
|
||||
ref.current.dispatchEvent(
|
||||
new CustomEvent(SELECT_ANVIL_WIDGET_CUSTOM_EVENT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: { widgetId: props.widgetId },
|
||||
}),
|
||||
);
|
||||
// Memoize flex props to be passed to the WDS Flex component.
|
||||
// If the widget is being resized => update width and height to auto.
|
||||
const flexProps: FlexProps = useMemo(() => {
|
||||
const data: FlexProps = {
|
||||
alignSelf: verticalAlignment || FlexVerticalAlignment.Top,
|
||||
flexGrow: flexGrow ? flexGrow : isFillWidget ? 1 : 0,
|
||||
flexShrink: isFillWidget ? 1 : 0,
|
||||
flexBasis: isFillWidget ? "0%" : "auto",
|
||||
padding: "spacing-1",
|
||||
alignItems: "center",
|
||||
};
|
||||
if (widgetSize) {
|
||||
const { maxHeight, maxWidth, minHeight, minWidth } = widgetSize;
|
||||
data.maxHeight = maxHeight;
|
||||
data.maxWidth = maxWidth;
|
||||
data.minHeight = minHeight ?? { base: "sizing-12" };
|
||||
data.minWidth = minWidth;
|
||||
}
|
||||
},
|
||||
[props.widgetId, isFocused],
|
||||
);
|
||||
return data;
|
||||
}, [isFillWidget, widgetSize, verticalAlignment, flexGrow]);
|
||||
|
||||
const stopEventPropagation: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
!isSnipingMode && e.stopPropagation();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const widgetConfig:
|
||||
| (Partial<WidgetProps> & WidgetConfigProps & { type: string })
|
||||
| undefined = WidgetFactory.getConfig(props.widgetType);
|
||||
if (!widgetConfig) return;
|
||||
setIsFillWidget(
|
||||
widgetConfig?.responsiveBehavior === ResponsiveBehavior.Fill,
|
||||
// Render the Anvil Flex Component using the Flex component from WDS
|
||||
return (
|
||||
<Flex
|
||||
{...flexProps}
|
||||
className={className}
|
||||
id={getAnvilWidgetDOMId(widgetId)}
|
||||
ref={ref}
|
||||
style={anvilWidgetStyleProps}
|
||||
>
|
||||
<div className="h-full w-full">{children}</div>
|
||||
</Flex>
|
||||
);
|
||||
setVerticalAlignment(
|
||||
widgetConfig?.flexVerticalAlignment || FlexVerticalAlignment.Top,
|
||||
);
|
||||
}, [props.widgetType]);
|
||||
|
||||
const { onHoverZIndex } = usePositionedContainerZIndex(
|
||||
isDropTarget,
|
||||
props.widgetId,
|
||||
isFocused,
|
||||
isSelected,
|
||||
);
|
||||
|
||||
const className = useMemo(
|
||||
() =>
|
||||
`anvil-layout-parent-${props.parentId} anvil-layout-child-${
|
||||
props.widgetId
|
||||
} ${widgetTypeClassname(
|
||||
props.widgetType,
|
||||
)} t--widget-${props.widgetName.toLowerCase()} drop-target-${
|
||||
props.layoutId
|
||||
} row-index-${props.rowIndex} anvil-widget-wrapper`,
|
||||
[
|
||||
props.parentId,
|
||||
props.widgetId,
|
||||
props.widgetType,
|
||||
props.widgetName,
|
||||
props.layoutId,
|
||||
props.rowIndex,
|
||||
],
|
||||
);
|
||||
|
||||
// Memoize flex props to be passed to the WDS Flex component.
|
||||
// If the widget is being resized => update width and height to auto.
|
||||
const flexProps: FlexProps = useMemo(() => {
|
||||
const data: FlexProps = {
|
||||
alignSelf: verticalAlignment || FlexVerticalAlignment.Top,
|
||||
flexGrow: props.flexGrow ? props.flexGrow : isFillWidget ? 1 : 0,
|
||||
flexShrink: isFillWidget ? 1 : 0,
|
||||
flexBasis: isFillWidget ? "0%" : "auto",
|
||||
padding: "spacing-1",
|
||||
alignItems: "center",
|
||||
};
|
||||
if (props.widgetSize) {
|
||||
const { maxHeight, maxWidth, minHeight, minWidth } = props.widgetSize;
|
||||
data.maxHeight = maxHeight;
|
||||
data.maxWidth = maxWidth;
|
||||
data.minHeight = minHeight ?? { base: "sizing-12" };
|
||||
data.minWidth = minWidth;
|
||||
}
|
||||
return data;
|
||||
}, [isFillWidget, props.widgetSize, verticalAlignment, props.flexGrow]);
|
||||
|
||||
const borderStyles = useWidgetBorderStyles(props.widgetId);
|
||||
|
||||
const styleProps: CSSProperties = useMemo(() => {
|
||||
return {
|
||||
position: "relative",
|
||||
// overflow is set to make sure widgets internal components/divs don't overflow this boundary causing scrolls
|
||||
overflow: "hidden",
|
||||
opacity: (isDragging && isSelected) || !props.isVisible ? 0.5 : 1,
|
||||
"&:hover": {
|
||||
zIndex: onHoverZIndex,
|
||||
},
|
||||
...borderStyles,
|
||||
};
|
||||
}, [borderStyles, isDragging, isSelected, onHoverZIndex]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
{...flexProps}
|
||||
className={className}
|
||||
id={getAnvilWidgetDOMId(props.widgetId)}
|
||||
onClick={stopEventPropagation}
|
||||
onClickCapture={onClickFn}
|
||||
ref={ref}
|
||||
style={styleProps}
|
||||
>
|
||||
<div className="h-full w-full">{props.children}</div>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import React, { useMemo } from "react";
|
||||
import type { AnvilFlexComponentProps } from "../utils/types";
|
||||
import { AnvilFlexComponent } from "../common/AnvilFlexComponent";
|
||||
import { widgetTypeClassname } from "widgets/WidgetUtils";
|
||||
import { usePositionObserver } from "layoutSystems/common/utils/LayoutElementPositionsObserver/usePositionObserver";
|
||||
import { useAnvilWidgetStyles } from "./hooks/useAnvilWidgetStyles";
|
||||
import { useAnvilWidgetClick } from "./hooks/useAnvilWidgetClick";
|
||||
import { useAnvilWidgetDrag } from "./hooks/useAnvilWidgetDrag";
|
||||
import { useAnvilWidgetHover } from "./hooks/useAnvilWidgetHover";
|
||||
|
||||
export const AnvilEditorFlexComponent = (props: AnvilFlexComponentProps) => {
|
||||
// Create a ref for the AnvilFlexComponent
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Generate a className for the AnvilFlexComponent
|
||||
const className = useMemo(
|
||||
() =>
|
||||
`anvil-layout-parent-${props.parentId} anvil-layout-child-${
|
||||
props.widgetId
|
||||
} ${widgetTypeClassname(
|
||||
props.widgetType,
|
||||
)} t--widget-${props.widgetName.toLowerCase()} drop-target-${
|
||||
props.layoutId
|
||||
} row-index-${props.rowIndex} anvil-widget-wrapper`,
|
||||
[
|
||||
props.parentId,
|
||||
props.widgetId,
|
||||
props.widgetType,
|
||||
props.widgetName,
|
||||
props.layoutId,
|
||||
props.rowIndex,
|
||||
],
|
||||
);
|
||||
|
||||
// observe the layout element's position
|
||||
usePositionObserver(
|
||||
"widget",
|
||||
{ widgetId: props.widgetId, layoutId: props.layoutId },
|
||||
ref,
|
||||
);
|
||||
|
||||
// Use custom hooks to manage styles, click, drag, and hover behavior exclusive for Edit mode
|
||||
useAnvilWidgetStyles(props.widgetId, props.widgetName, props.isVisible, ref);
|
||||
useAnvilWidgetClick(props.widgetId, ref);
|
||||
useAnvilWidgetDrag(props.widgetId, props.layoutId, ref);
|
||||
useAnvilWidgetHover(props.widgetId, ref);
|
||||
|
||||
// Render the AnvilFlexComponent
|
||||
return <AnvilFlexComponent {...props} className={className} ref={ref} />;
|
||||
};
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
import { useMemo } from "react";
|
||||
import type { BaseWidgetProps } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
|
||||
import { AnvilFlexComponent } from "../common/AnvilFlexComponent";
|
||||
import { AnvilWidgetComponent } from "../common/widgetComponent/AnvilWidgetComponent";
|
||||
import DraggableComponent from "layoutSystems/common/draggable/DraggableComponent";
|
||||
import { generateDragStateForAnvilLayout } from "../utils/widgetUtils";
|
||||
import type { SizeConfig } from "WidgetProvider/constants";
|
||||
import { getWidgetSizeConfiguration } from "../utils/widgetUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
|
||||
import { AnvilEditorFlexComponent } from "./AnvilEditorFlexComponent";
|
||||
|
||||
/**
|
||||
* AnvilEditorWidgetOnion
|
||||
|
|
@ -26,22 +24,13 @@ import { combinedPreviewModeSelector } from "selectors/editorSelectors";
|
|||
* @returns Enhanced Widget
|
||||
*/
|
||||
export const AnvilEditorWidgetOnion = (props: BaseWidgetProps) => {
|
||||
const { layoutId } = props;
|
||||
// if layoutId is not present on widget props then we need a selector to fetch layout id of a widget.
|
||||
// const layoutId = useSelector(getLayoutIdByWidgetId(props.widgetId));
|
||||
const generateDragState = useCallback(() => {
|
||||
return generateDragStateForAnvilLayout({
|
||||
layoutId,
|
||||
});
|
||||
}, [layoutId]);
|
||||
const isPreviewMode = useSelector(combinedPreviewModeSelector);
|
||||
const widgetSize: SizeConfig = useMemo(
|
||||
() => getWidgetSizeConfiguration(props.type, props, isPreviewMode),
|
||||
[isPreviewMode, props.type],
|
||||
);
|
||||
|
||||
return (
|
||||
<AnvilFlexComponent
|
||||
<AnvilEditorFlexComponent
|
||||
flexGrow={props.flexGrow}
|
||||
isResizeDisabled={props.resizeDisabled}
|
||||
isVisible={!!props.isVisible}
|
||||
|
|
@ -53,17 +42,7 @@ export const AnvilEditorWidgetOnion = (props: BaseWidgetProps) => {
|
|||
widgetSize={widgetSize}
|
||||
widgetType={props.type}
|
||||
>
|
||||
<DraggableComponent
|
||||
dragDisabled={!!props.dragDisabled}
|
||||
generateDragState={generateDragState}
|
||||
isFlexChild
|
||||
parentId={props.parentId}
|
||||
resizeDisabled={props.resizeDisabled}
|
||||
type={props.type}
|
||||
widgetId={props.widgetId}
|
||||
>
|
||||
<AnvilWidgetComponent {...props}>{props.children}</AnvilWidgetComponent>
|
||||
</DraggableComponent>
|
||||
</AnvilFlexComponent>
|
||||
<AnvilWidgetComponent {...props}>{props.children}</AnvilWidgetComponent>
|
||||
</AnvilEditorFlexComponent>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { SELECT_ANVIL_WIDGET_CUSTOM_EVENT } from "layoutSystems/anvil/utils/constants";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { snipingModeSelector } from "selectors/editorSelectors";
|
||||
import { isCurrentWidgetFocused } from "selectors/widgetSelectors";
|
||||
|
||||
export const useAnvilWidgetClick = (
|
||||
widgetId: string,
|
||||
ref: React.RefObject<HTMLDivElement>, // Ref object to reference the AnvilFlexComponent
|
||||
) => {
|
||||
// Retrieve state from the Redux store
|
||||
const isFocused = useSelector(isCurrentWidgetFocused(widgetId));
|
||||
const isSnipingMode = useSelector(snipingModeSelector);
|
||||
|
||||
// Function to stop event propagation if not in sniping mode
|
||||
// Note: Sniping mode is irrelevant to the Anvil however it becomes relevant if we decide to make Anvil the default editor
|
||||
const stopEventPropagation = (e: MouseEvent) => {
|
||||
!isSnipingMode && e.stopPropagation();
|
||||
};
|
||||
|
||||
// Callback function for handling click events on AnvilFlexComponent in Edit mode
|
||||
const onClickFn = useCallback(
|
||||
function () {
|
||||
// Dispatch a custom event when the Anvil widget is clicked and focused
|
||||
if (ref.current && isFocused) {
|
||||
ref.current.dispatchEvent(
|
||||
new CustomEvent(SELECT_ANVIL_WIDGET_CUSTOM_EVENT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: { widgetId: widgetId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[widgetId, isFocused],
|
||||
);
|
||||
|
||||
// Effect hook to add and remove click event listeners
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
// Add click event listener to select the Anvil widget
|
||||
ref.current.addEventListener("click", onClickFn, { capture: true });
|
||||
|
||||
// Add click event listener to stop event propagation in certain modes
|
||||
ref.current.addEventListener("click", stopEventPropagation, {
|
||||
capture: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up event listeners when the component unmounts
|
||||
return () => {
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener("click", onClickFn, { capture: true });
|
||||
ref.current.removeEventListener("click", stopEventPropagation, {
|
||||
capture: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [onClickFn, stopEventPropagation]);
|
||||
};
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { generateDragStateForAnvilLayout } from "layoutSystems/anvil/utils/widgetUtils";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
|
||||
import { getShouldAllowDrag } from "selectors/widgetDragSelectors";
|
||||
import {
|
||||
isCurrentWidgetFocused,
|
||||
isWidgetSelected,
|
||||
} from "selectors/widgetSelectors";
|
||||
import { useWidgetDragResize } from "utils/hooks/dragResizeHooks";
|
||||
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
|
||||
|
||||
export const useAnvilWidgetDrag = (
|
||||
widgetId: string,
|
||||
layoutId: string,
|
||||
ref: React.RefObject<HTMLDivElement>, // Ref object to reference the AnvilFlexComponent
|
||||
) => {
|
||||
// Retrieve state from the Redux store
|
||||
const isSelected = useSelector(isWidgetSelected(widgetId));
|
||||
const isFocused = useSelector(isCurrentWidgetFocused(widgetId));
|
||||
const shouldAllowDrag = useSelector(getShouldAllowDrag);
|
||||
const { selectWidget } = useWidgetSelection();
|
||||
const generateDragState = useCallback(() => {
|
||||
return generateDragStateForAnvilLayout({
|
||||
layoutId,
|
||||
});
|
||||
}, [layoutId]);
|
||||
const { setDraggingState } = useWidgetDragResize();
|
||||
|
||||
// Callback function for handling drag start events
|
||||
const onDragStart = useCallback(
|
||||
(e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (shouldAllowDrag && ref.current && !(e.metaKey || e.ctrlKey)) {
|
||||
if (!isFocused) return;
|
||||
|
||||
if (!isSelected) {
|
||||
// Select the widget if not already selected
|
||||
selectWidget(SelectionRequestType.One, [widgetId]);
|
||||
}
|
||||
|
||||
// Generate and set the dragging state for the Anvil layout
|
||||
const draggingState = generateDragState();
|
||||
setDraggingState(draggingState);
|
||||
}
|
||||
},
|
||||
[
|
||||
shouldAllowDrag,
|
||||
isFocused,
|
||||
isSelected,
|
||||
selectWidget,
|
||||
widgetId,
|
||||
generateDragState,
|
||||
setDraggingState,
|
||||
],
|
||||
);
|
||||
|
||||
// Effect hook to add and remove drag start event listeners
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
// Configure the draggable attribute and cursor style based on drag permission
|
||||
ref.current.draggable = shouldAllowDrag;
|
||||
ref.current.style.cursor = shouldAllowDrag ? "grab" : "default";
|
||||
|
||||
// Add drag start event listener
|
||||
ref.current.addEventListener("dragstart", onDragStart);
|
||||
}
|
||||
|
||||
// Clean up event listeners when the component unmounts
|
||||
return () => {
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener("dragstart", onDragStart);
|
||||
}
|
||||
};
|
||||
}, [onDragStart, shouldAllowDrag]);
|
||||
};
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { getAnvilSpaceDistributionStatus } from "layoutSystems/anvil/integrations/selectors";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
|
||||
import { isCurrentWidgetFocused } from "selectors/widgetSelectors";
|
||||
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
|
||||
|
||||
export const useAnvilWidgetHover = (
|
||||
widgetId: string,
|
||||
ref: React.RefObject<HTMLDivElement>, // Ref object to reference the AnvilFlexComponent
|
||||
) => {
|
||||
// Retrieve state from the Redux store
|
||||
const isFocused = useSelector(isCurrentWidgetFocused(widgetId));
|
||||
const isPreviewMode = useSelector(combinedPreviewModeSelector);
|
||||
const isDistributingSpace = useSelector(getAnvilSpaceDistributionStatus);
|
||||
|
||||
// Access the focusWidget function from the useWidgetSelection hook
|
||||
const { focusWidget } = useWidgetSelection();
|
||||
|
||||
// Callback function for handling mouseover events
|
||||
const handleMouseOver = useCallback(
|
||||
(e: any) => {
|
||||
// Check conditions before focusing the widget on mouseover
|
||||
focusWidget &&
|
||||
!isFocused &&
|
||||
!isDistributingSpace &&
|
||||
!isPreviewMode &&
|
||||
focusWidget(widgetId);
|
||||
|
||||
// Prevent the event from propagating further
|
||||
e.stopPropagation();
|
||||
},
|
||||
[focusWidget, isFocused, isDistributingSpace, isPreviewMode, widgetId],
|
||||
);
|
||||
|
||||
// Callback function for handling mouseleave events
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
// On leaving a widget, reset the focused widget
|
||||
focusWidget && focusWidget();
|
||||
}, [focusWidget]);
|
||||
|
||||
// Effect hook to add and remove mouseover and mouseleave event listeners
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
// Add mouseover and mouseleave event listeners
|
||||
ref.current.addEventListener("mouseover", handleMouseOver);
|
||||
ref.current.addEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
|
||||
// Clean up event listeners when the component unmounts
|
||||
return () => {
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener("mouseover", handleMouseOver);
|
||||
ref.current.removeEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
};
|
||||
}, [handleMouseOver, handleMouseLeave]);
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { isWidgetSelected } from "selectors/widgetSelectors";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useWidgetBorderStyles } from "layoutSystems/anvil/common/hooks/useWidgetBorderStyles";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
|
||||
export const useAnvilWidgetStyles = (
|
||||
widgetId: string,
|
||||
widgetName: string,
|
||||
isVisible = true,
|
||||
ref: React.RefObject<HTMLDivElement>, // Ref object to reference the AnvilFlexComponent
|
||||
) => {
|
||||
// Get widget border styles using useWidgetBorderStyles
|
||||
const widgetBorderStyles = useWidgetBorderStyles(widgetId);
|
||||
|
||||
// Effect hook to apply widget border styles to the widget
|
||||
useEffect(() => {
|
||||
Object.entries(widgetBorderStyles).forEach(([property, value]) => {
|
||||
if (ref.current) {
|
||||
// Set each border style property on the widget's DOM element
|
||||
ref.current.style[property as any] = value;
|
||||
}
|
||||
});
|
||||
}, [widgetBorderStyles]);
|
||||
|
||||
// Effect hook to set a data attribute for testing purposes
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.setAttribute("data-widgetname-cy", widgetName);
|
||||
}
|
||||
}, [widgetName]);
|
||||
|
||||
// Selectors to determine whether the widget is selected or dragging
|
||||
const isSelected = useSelector(isWidgetSelected(widgetId));
|
||||
const isDragging = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.isDragging,
|
||||
);
|
||||
|
||||
// Calculate whether the widget should fade based on dragging, selection, and visibility
|
||||
const shouldFadeWidget = (isDragging && isSelected) || !isVisible;
|
||||
|
||||
// Calculate opacity factor based on whether the widget should fade
|
||||
const opacityFactor = useMemo(() => {
|
||||
return shouldFadeWidget ? 0.5 : 1;
|
||||
}, [shouldFadeWidget]);
|
||||
|
||||
// Effect hook to set the opacity of the widget's DOM element
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.style.opacity = opacityFactor.toString();
|
||||
}
|
||||
}, [opacityFactor]);
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@ import type { WidgetType } from "WidgetProvider/factory";
|
|||
|
||||
export interface AnvilFlexComponentProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
isResizeDisabled?: boolean;
|
||||
layoutId: string;
|
||||
focused?: boolean;
|
||||
|
|
|
|||
|
|
@ -251,9 +251,10 @@ describe("Layout System HOC's Tests", () => {
|
|||
.spyOn(layoutSystemSelectors, "getLayoutSystemType")
|
||||
.mockImplementation(() => LayoutSystemTypes.ANVIL);
|
||||
const component = render(<HOC {...widgetProps} />);
|
||||
const flexPositionedLayer = component.container.getElementsByClassName(
|
||||
"anvil-layout-child-" + widgetProps.widgetId,
|
||||
)[0];
|
||||
const flexPositionedLayer =
|
||||
component.container.ownerDocument.getElementById(
|
||||
"anvil_widget_" + widgetProps.widgetId,
|
||||
);
|
||||
expect(flexPositionedLayer).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ export function* closeModalSaga(
|
|||
// If modalName is not provided, find all open modals
|
||||
// Get all meta prop records
|
||||
const metaProps: Record<string, any> = yield select(getWidgetsMeta);
|
||||
const modalWidgetType: string = yield select(getModalWidgetType);
|
||||
|
||||
// Get widgetIds of all widgets of type MODAL_WIDGET
|
||||
// Note: Not updating this code path for WDS_MODAL_WIDGET, as the functionality
|
||||
|
|
@ -213,7 +214,7 @@ export function* closeModalSaga(
|
|||
// In this, the flow of switching back and forth between multiple modals is to be tested.
|
||||
const modalWidgetIds: string[] = yield select(
|
||||
getWidgetIdsByType,
|
||||
WidgetTypes.MODAL_WIDGET,
|
||||
modalWidgetType,
|
||||
);
|
||||
|
||||
// Loop through all modal widgetIds
|
||||
|
|
|
|||
|
|
@ -249,7 +249,15 @@ function* handleWidgetSelectionSaga(
|
|||
}
|
||||
|
||||
function* openOrCloseModalSaga(action: ReduxAction<{ widgetIds: string[] }>) {
|
||||
if (action.payload.widgetIds.length !== 1) return;
|
||||
const widgetsToSelect = action.payload.widgetIds;
|
||||
if (widgetsToSelect.length !== 1) return;
|
||||
if (
|
||||
widgetsToSelect.length === 1 &&
|
||||
widgetsToSelect[0] === MAIN_CONTAINER_WIDGET_ID
|
||||
) {
|
||||
// for cases where a widget inside modal is deleted and main canvas gets selected post that.
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's assume that the payload widgetId is a modal widget and we need to open the modal as it is selected
|
||||
let modalWidgetToOpen: string = action.payload.widgetIds[0];
|
||||
|
|
@ -307,6 +315,12 @@ function* openOrCloseModalSaga(action: ReduxAction<{ widgetIds: string[] }>) {
|
|||
if (widgetIsModal || widgetIsChildOfModal) {
|
||||
yield put(showModal(modalWidgetToOpen));
|
||||
}
|
||||
if (!widgetIsModal && !widgetIsChildOfModal) {
|
||||
yield put({
|
||||
type: ReduxActionTypes.CLOSE_MODAL,
|
||||
payload: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function* focusOnWidgetSaga(action: ReduxAction<{ widgetIds: string[] }>) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user