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 -->
This commit is contained in:
Hetu Nandu 2024-03-13 11:53:49 +05:30 committed by GitHub
parent eb828bba1d
commit e99cc39e47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 276 additions and 80 deletions

View File

@ -5,7 +5,6 @@ import {
locators,
propPane,
} from "../../../../support/Objects/ObjectsCore";
import Canvas from "../../../../support/Pages/Canvas";
import EditorNavigation, {
EntityType,
PageLeftPane,
@ -94,12 +93,9 @@ describe(
);
deployMode.NavigateBacktoEditor();
// Verify multiple widgets selected groups into single container
Canvas.selectMultipleWidgets(["Input1", "Select1", "Text3"]);
agHelper.GetElement("body").type(`{${agHelper._modifierKey}}{g}`);
agHelper.Sleep(1000);
PageLeftPane.assertPresence("Container3");
entityExplorer.DeleteWidgetFromEntityExplorer("Container3");
entityExplorer.DeleteWidgetFromEntityExplorer("Input1");
entityExplorer.DeleteWidgetFromEntityExplorer("Select1");
entityExplorer.DeleteWidgetFromEntityExplorer("Text3");
});
it("4. Validate visible toggle", () => {

View File

@ -286,7 +286,7 @@ describe(
//Should check that widget input is not showing any errors on input
cy.get(widgetInput).type("123456789");
cy.focused().then(() => {
expect(Cypress.$(themelocators.popover)).not.to.exist;
cy.get(".error-tooltip .bp3-popover-content").should("not.exist");
});
});
},

View File

@ -154,6 +154,7 @@ describe(
it("5. Verify Theme change", () => {
agHelper.PressEscape();
appSettings.OpenPaneAndChangeTheme("Pacific");
agHelper.WaitUntilToastDisappear("Theme Pacific applied");
[0, 1, 2].forEach((index) => {
agHelper.AssertAttribute(
locators._listText,

View File

@ -11,6 +11,7 @@ const containerWidgetSelector = `[type="CONTAINER_WIDGET"]`;
function dragAndDropToWidget(widgetType, destinationWidget, { x, y }) {
const selector = `.t--widget-card-draggable-${widgetType}`;
cy.wait(800);
PageLeftPane.switchToAddNew();
cy.get(selector)
.first()
.scrollIntoView()
@ -34,9 +35,9 @@ function deleteAllWidgetsInContainer() {
force: true,
});
cy.get("body").type(`{${modifierKey}}{a}`);
cy.get("body").type("{del}");
cy.wait(200);
cy.get("body").type("{del}");
cy.get(commonlocators.layoutControls).should("be.visible");
}
function checkSelectedRadioValue(selector, value) {
@ -63,12 +64,13 @@ describe(
x: 250,
y: 50,
});
cy.assertPageSave();
// Verify drop
cy.get(publishLocators.inputWidget).should("exist");
// Type value
cy.get(publishLocators.inputWidget).find("input").type("abcd");
cy.get(publishLocators.inputWidget).find("input").first().type("abcd");
// Verify if the value got typed
cy.get(publishLocators.inputWidget)
@ -78,7 +80,6 @@ describe(
deleteAllWidgetsInContainer();
// Drop Select widget
PageLeftPane.switchToAddNew();
dragAndDropToWidget("selectwidget", "containerwidget", {
x: 250,
y: 50,
@ -115,7 +116,6 @@ describe(
deleteAllWidgetsInContainer();
// Drop Checkbox widget
PageLeftPane.switchToAddNew();
dragAndDropToWidget("checkboxgroupwidget", "containerwidget", {
x: 250,
y: 50,
@ -158,7 +158,6 @@ describe(
deleteAllWidgetsInContainer();
// Drop Switch widget
PageLeftPane.switchToAddNew();
dragAndDropToWidget("switchwidget", "containerwidget", {
x: 250,
y: 50,
@ -201,9 +200,8 @@ describe(
_.deployMode.NavigateBacktoEditor();
deleteAllWidgetsInContainer();
cy.wait(800);
// Drop Radio widget
PageLeftPane.switchToAddNew();
dragAndDropToWidget("radiogroupwidget", "containerwidget", {
x: 250,
y: 50,

View File

@ -68,7 +68,8 @@ describe(
entityExplorer.DragDropWidgetNVerify(widget);
//cy.dragAndDropToWidget(widget, "listwidgetv2", { x: 350, y: 50 });
agHelper.GetNClick(propPane._deleteWidget);
cy.wait("@updateLayout");
cy.assertPageSave();
cy.wait(800);
});
},
);
@ -88,7 +89,8 @@ describe(
cy.assertPageSave();
cy.get(`.t--draggable-${widget}`).should("exist");
cy.get(widgetsPage.removeWidget).click({ force: true });
cy.wait("@updateLayout");
cy.assertPageSave();
cy.wait(800);
});
},
);

View File

@ -23,9 +23,9 @@ describe(
//cy.createModal("Modal", this.dataSet.ModalName);
cy.createModal("Modal", "onRowSelected");
cy.isSelectRow(1);
cy.get(".bp3-overlay-backdrop").click({ force: true });
cy.get(".bp3-overlay-backdrop").last().click({ force: true });
cy.isSelectRow(2);
cy.get(".bp3-overlay-backdrop").click({ force: true });
cy.get(".bp3-overlay-backdrop").last().click({ force: true });
});
it("2. Table widget V2 with button colour change validation", function () {

View File

@ -81,7 +81,6 @@
"evaluatedCurrentValue": "div:last-of-type .t--CodeEditor-evaluatedValue > div:last-of-type pre",
"entityExplorersearch": "#entity-explorer-search",
"saveStatusContainer": ".t--save-status-container",
"saveStatusIsSaving": "t--save-status-is-saving",
"statusSaving": ".t--save-status-is-saving",
"saveStatusError": ".t--save-status-error",
"selectWidgetVirtualList": ".menu-virtual-list div",

View File

@ -47,9 +47,15 @@ export const createModalAction = (
export const focusWidget = (
widgetId?: string,
): ReduxAction<{ widgetId?: string }> => ({
alt?: boolean,
): ReduxAction<{ widgetId?: string; alt?: boolean }> => ({
type: ReduxActionTypes.FOCUS_WIDGET,
payload: { widgetId },
payload: { widgetId, alt },
});
export const altFocusWidget = (alt: boolean) => ({
type: ReduxActionTypes.ALT_FOCUS_WIDGET,
payload: alt,
});
export const showModal = (id: string, shouldSelectModal = true) => {
@ -144,3 +150,10 @@ export const partialExportWidgets = (params: PartialExportParams) => {
payload: params,
};
};
export const setWidgetSelectionBlock = (payload: boolean) => {
return {
type: ReduxActionTypes.SET_WIDGET_SELECTION_BLOCK,
payload,
};
};

View File

@ -917,6 +917,8 @@ const ActionTypes = {
SET_API_PANE_DEBUGGER_STATE: "SET_API_PANE_DEBUGGER_STATE",
SET_JS_PANE_DEBUGGER_STATE: "SET_JS_PANE_DEBUGGER_STATE",
SET_CANVAS_DEBUGGER_STATE: "SET_CANVAS_DEBUGGER_STATE",
SET_WIDGET_SELECTION_BLOCK: "SET_WIDGET_SELECTION_BLOCK",
ALT_FOCUS_WIDGET: "ALT_FOCUS_WIDGET",
};
export const ReduxActionTypes = {

View File

@ -385,7 +385,8 @@ export type ONBOARDING_FLOW_EVENTS =
| "ONBOARDING_FLOW_CLICK_SKIP_BUTTON_START_FROM_DATA_PAGE"
| "ONBOARDING_FLOW_CLICK_SKIP_BUTTON_DATASOURCE_FORM_PAGE"
| "ONBOARDING_FLOW_CLICK_SKIP_BUTTON_START_FROM_TEMPLATE_PAGE"
| "ONBOARDING_FLOW_CLICK_SKIP_BUTTON_TEMPLATE_DETAILS_PAGE";
| "ONBOARDING_FLOW_CLICK_SKIP_BUTTON_TEMPLATE_DETAILS_PAGE"
| "CODE_MODE_WIDGET_SELECTION";
export type DATASOURCE_SCHEMA_EVENTS =
| "DATASOURCE_SCHEMA_SEARCH"

View File

@ -20,13 +20,13 @@ import { getShouldAllowDrag } from "selectors/widgetDragSelectors";
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
import { getAnvilSpaceDistributionStatus } from "layoutSystems/anvil/integrations/selectors";
const DraggableWrapper = styled.div`
const DraggableWrapper = styled.div<{ draggable: boolean }>`
display: block;
flex-direction: column;
width: 100%;
height: 100%;
user-select: none;
cursor: grab;
cursor: ${(props) => (props.draggable ? "grab" : "unset")};
`;
export interface DraggableComponentProps {
@ -37,7 +37,7 @@ export interface DraggableComponentProps {
type: string;
children: ReactNode;
generateDragState: (
e: React.DragEvent<Element>,
e: React.DragEvent,
draggableRef: HTMLElement,
) => SetDraggingStateActionPayload;
dragDisabled: boolean;
@ -53,7 +53,6 @@ const WidgetBoundaries = styled.div`
${(props) => getColorWithOpacity(props.theme.colors.textAnchor, 0.5)};
pointer-events: none;
top: 0;
position: absolute;
left: 0;
`;
@ -100,14 +99,14 @@ function DraggableComponent(props: DraggableComponentProps) {
!props.isFlexChild && (isCurrentWidgetDragging || isDraggingSibling);
// When mouse is over this draggable
const handleMouseOver = (e: any) => {
const handleMouseOver = (e: React.MouseEvent) => {
focusWidget &&
!isResizingOrDragging &&
!isFocused &&
!isDistributingSpace &&
!props.resizeDisabled &&
!isPreviewMode &&
focusWidget(props.widgetId);
focusWidget(props.widgetId, e.metaKey);
e.stopPropagation();
};

View File

@ -40,6 +40,7 @@ import { useCurrentAppState } from "pages/Editor/IDE/hooks";
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { getWidgetSelectionBlock } from "selectors/ui";
import {
isAutoHeightEnabledForWidget,
isAutoHeightEnabledForWidgetWithLimits,
@ -259,6 +260,7 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
);
// Are we changing the auto height limits by dragging the signifiers?
const { isAutoHeightWithLimitsChanging } = useAutoHeightUIState();
const isWidgetSelectionBlocked = useSelector(getWidgetSelectionBlock);
const dispatch = useDispatch();
@ -327,7 +329,12 @@ export function DropTargetComponent(props: DropTargetComponentProps) {
(e.target as HTMLDivElement).dataset.testid === selectionDiv ||
(e.target as HTMLDivElement).dataset.testid === mainCanvasId;
if (!isResizing && !isDragging && !isAutoHeightWithLimitsChanging) {
if (
!isResizing &&
!isDragging &&
!isAutoHeightWithLimitsChanging &&
!isWidgetSelectionBlocked
) {
// Check if Target is the MainCanvas
if (isTargetMainCanvas) {
goToWidgetAdd();

View File

@ -23,6 +23,7 @@ import {
combinedPreviewModeSelector,
snipingModeSelector,
} from "selectors/editorSelectors";
import { getWidgetSelectionBlock } from "../../../selectors/ui";
const minSize = 100;
/**
@ -101,7 +102,9 @@ export const ModalResizableLayer = ({
};
const isPreviewMode = useSelector(combinedPreviewModeSelector);
const isSnipingMode = useSelector(snipingModeSelector);
const enableResizing = !isSnipingMode && !isPreviewMode;
const isWidgetSelectionBlocked = useSelector(getWidgetSelectionBlock);
const enableResizing =
!isSnipingMode && !isPreviewMode && !isWidgetSelectionBlocked;
return (
<ModalResizable
allowResize

View File

@ -65,6 +65,10 @@ import {
computeFinalRowCols,
computeFinalAutoLayoutRowCols,
} from "layoutSystems/common/resizer/ResizableUtils";
import {
getAltBlockWidgetSelection,
getWidgetSelectionBlock,
} from "selectors/ui";
export type ResizableComponentProps = WidgetProps & {
paddingOffset: number;
@ -80,6 +84,8 @@ export const ResizableComponent = memo(function ResizableComponent(
const Resizable = isAutoLayout ? AutoLayoutResizable : FixedLayoutResizable;
const isSnipingMode = useSelector(snipingModeSelector);
const isPreviewMode = useSelector(combinedPreviewModeSelector);
const isWidgetSelectionBlock = useSelector(getWidgetSelectionBlock);
const isAltWidgetSelectionBlock = useSelector(getAltBlockWidgetSelection);
const isAppSettingsPaneWithNavigationTabOpen = useSelector(
getIsAppSettingsPaneWithNavigationTabOpen,
);
@ -325,6 +331,7 @@ export const ResizableComponent = memo(function ResizableComponent(
!props.resizeDisabled &&
!isSnipingMode &&
!isPreviewMode &&
!isWidgetSelectionBlock &&
!isAppSettingsPaneWithNavigationTabOpen;
const { updateDropTargetRows } = useContext(DropTargetContext);
@ -387,6 +394,7 @@ export const ResizableComponent = memo(function ResizableComponent(
!isPreviewMode &&
!isAppSettingsPaneWithNavigationTabOpen &&
!isDragging &&
!isAltWidgetSelectionBlock &&
(isHovered || isSelected);
return (

View File

@ -1,11 +1,11 @@
import { Classes, Tooltip } from "@blueprintjs/core";
import { Colors } from "constants/Colors";
import type { CSSProperties } from "react";
import React from "react";
import { useSelector } from "react-redux";
import { snipingModeSelector } from "selectors/editorSelectors";
import styled from "styled-components";
import { Icon } from "design-system";
import { Icon, Text, Tooltip } from "design-system";
// I honestly can't think of a better name for this enum
export enum Activities {
HOVERING,
@ -13,13 +13,7 @@ export enum Activities {
ACTIVE,
NONE,
}
const StyledTooltip = styled(Tooltip)<{
children?: React.ReactNode;
}>`
.${Classes.POPOVER_TARGET} {
height: 100%;
}
`;
const WidgetNameBoundary = 1;
const BORDER_RADIUS = 4;
const SettingsWrapper = styled.div<{ widgetWidth: number; inverted: boolean }>`
@ -60,10 +54,6 @@ const WidgetName = styled.span`
white-space: nowrap;
`;
const StyledErrorIcon = styled(Icon)`
margin-right: ${(props) => props.theme.spaces[1]}px;
`;
interface SettingsControlProps {
toggleSettings: (e: any) => void;
activity: Activities;
@ -111,17 +101,17 @@ const getStyles = (
export function SettingsControl(props: SettingsControlProps) {
const isSnipingMode = useSelector(snipingModeSelector);
const errorIcon = <StyledErrorIcon name="warning" size="sm" />;
const errorIcon = <Icon name="warning" size="sm" />;
return (
<StyledTooltip
<Tooltip
content={
isSnipingMode
? `Bind to widget ${props.name}`
: "Edit widget properties"
<Text color="var(--ads-v2-color-white)">
{isSnipingMode ? `Bind to widget ${props.name}` : `Edit widget`}
</Text>
}
hoverOpenDelay={500}
position="top-right"
mouseEnterDelay={0}
placement="topRight"
>
<SettingsWrapper
className="t--widget-propertypane-toggle"
@ -143,7 +133,7 @@ export function SettingsControl(props: SettingsControlProps) {
{isSnipingMode ? `Bind to ${props.name}` : props.name}
</WidgetName>
</SettingsWrapper>
</StyledTooltip>
</Tooltip>
);
}

View File

@ -30,6 +30,7 @@ import { getAbsolutePixels } from "utils/helpers";
import type { XYCord } from "layoutSystems/common/canvasArenas/ArenaTypes";
import { useCanvasDragToScroll } from "layoutSystems/common/canvasArenas/useCanvasDragToScroll";
import { StickyCanvasArena } from "layoutSystems/common/canvasArenas/StickyCanvasArena";
import { getWidgetSelectionBlock } from "../../../../selectors/ui";
export interface SelectedArenaDimensions {
top: number;
@ -71,6 +72,7 @@ export function CanvasSelectionArena({
);
const appMode = useSelector(getAppMode);
const isPreviewMode = useSelector(combinedPreviewModeSelector);
const isWidgetSelectionBlocked = useSelector(getWidgetSelectionBlock);
const isAppSettingsPaneWithNavigationTabOpen = useSelector(
getIsAppSettingsPaneWithNavigationTabOpen,
);
@ -501,6 +503,7 @@ export function CanvasSelectionArena({
!(
isDragging ||
isPreviewMode ||
isWidgetSelectionBlocked ||
isAppSettingsPaneWithNavigationTabOpen ||
dropDisabled
);

View File

@ -18,6 +18,7 @@ import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
import { renderAppsmithCanvas } from "layoutSystems/CanvasFactory";
import type { WidgetProps } from "widgets/BaseWidget";
import { getAppThemeSettings } from "@appsmith/selectors/applicationSelectors";
import CodeModeTooltip from "pages/Editor/WidgetsEditor/CodeModeTooltip";
interface CanvasProps {
widgetsStructure: CanvasWidgetStructure;
@ -83,20 +84,22 @@ const Canvas = (props: CanvasProps) => {
const renderChildren = () => {
return (
<Wrapper
$enableMainCanvasResizer={!!props.enableMainCanvasResizer}
background={isWDSEnabled ? "" : backgroundForCanvas}
className={`relative t--canvas-artboard ${paddingBottomClass} transition-all duration-400 ${marginHorizontalClass} ${getViewportClassName(
canvasWidth,
)}`}
data-testid={"t--canvas-artboard"}
id={CANVAS_ART_BOARD}
ref={isWDSEnabled ? undefined : focusRef}
width={canvasWidth}
>
{props.widgetsStructure.widgetId &&
renderAppsmithCanvas(props.widgetsStructure as WidgetProps)}
</Wrapper>
<CodeModeTooltip>
<Wrapper
$enableMainCanvasResizer={!!props.enableMainCanvasResizer}
background={isWDSEnabled ? "" : backgroundForCanvas}
className={`relative t--canvas-artboard ${paddingBottomClass} transition-all duration-400 ${marginHorizontalClass} ${getViewportClassName(
canvasWidth,
)}`}
data-testid={"t--canvas-artboard"}
id={CANVAS_ART_BOARD}
ref={isWDSEnabled ? undefined : focusRef}
width={canvasWidth}
>
{props.widgetsStructure.widgetId &&
renderAppsmithCanvas(props.widgetsStructure as WidgetProps)}
</Wrapper>
</CodeModeTooltip>
);
};

View File

@ -4,11 +4,13 @@ import { Route, Switch, useRouteMatch } from "react-router";
import * as Sentry from "@sentry/react";
import useRoutes from "@appsmith/pages/Editor/IDE/MainPane/useRoutes";
import EditorTabs from "pages/Editor/IDE/EditorTabs/FullScreenTabs";
import { useWidgetSelectionBlockListener } from "pages/Editor/IDE/hooks";
const SentryRoute = Sentry.withSentryRouting(Route);
export const MainPane = (props: { id: string }) => {
const { path } = useRouteMatch();
const routes = useRoutes(path);
useWidgetSelectionBlockListener();
return (
<div

View File

@ -7,7 +7,7 @@ import {
} from "@appsmith/entities/IDE/constants";
import { useLocation } from "react-router";
import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { getIDEViewMode, getIsSideBySideEnabled } from "selectors/ideSelectors";
import { getPropertyPaneWidth } from "selectors/propertyPaneSelectors";
import { getCurrentPageId } from "@appsmith/selectors/entitiesSelector";
@ -28,6 +28,8 @@ import {
} from "constants/AppConstants";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { getIsAltFocusWidget, getWidgetSelectionBlock } from "selectors/ui";
import { altFocusWidget, setWidgetSelectionBlock } from "actions/widgetActions";
export const useCurrentAppState = () => {
const [appState, setAppState] = useState(EditorState.EDITOR);
@ -215,3 +217,41 @@ export const useIsEditorPaneSegmentsEnabled = () => {
return isEditorSegmentsReleaseEnabled || isEditorSegmentsRolloutEnabled;
};
export function useWidgetSelectionBlockListener() {
const { pathname } = useLocation();
const dispatch = useDispatch();
const currentFocus = identifyEntityFromPath(pathname);
const isAltFocused = useSelector(getIsAltFocusWidget);
const widgetSelectionIsBlocked = useSelector(getWidgetSelectionBlock);
useEffect(() => {
const inUIMode = [
FocusEntity.CANVAS,
FocusEntity.PROPERTY_PANE,
FocusEntity.WIDGET_LIST,
].includes(currentFocus.entity);
dispatch(setWidgetSelectionBlock(!inUIMode));
}, [currentFocus]);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [isAltFocused, widgetSelectionIsBlocked]);
const handleKeyDown = (e: KeyboardEvent) => {
if (!isAltFocused && widgetSelectionIsBlocked && e.metaKey) {
dispatch(altFocusWidget(e.metaKey));
}
};
const handleKeyUp = (e: KeyboardEvent) => {
if (!e.metaKey && widgetSelectionIsBlocked) {
dispatch(altFocusWidget(e.metaKey));
}
};
}

View File

@ -0,0 +1,36 @@
import { Tooltip } from "design-system";
import React, { useEffect, useState } from "react";
import { modText } from "utils/helpers";
import { useSelector } from "react-redux";
import { getWidgetSelectionBlock } from "selectors/ui";
import { retrieveCodeWidgetNavigationUsed } from "utils/storage";
const CodeModeTooltip = (props: { children: React.ReactElement }) => {
const isWidgetSelectionBlock = useSelector(getWidgetSelectionBlock);
const [shouldShow, setShouldShow] = useState<boolean>(false);
useEffect(() => {
retrieveCodeWidgetNavigationUsed()
.then((timesUsed) => {
if (timesUsed < 2) {
setShouldShow(true);
}
})
.catch(() => {
setShouldShow(true);
});
}, [isWidgetSelectionBlock]);
if (!isWidgetSelectionBlock) return props.children;
return (
<Tooltip
content={`💡 ${modText()} click a widget to navigate to UI mode.`}
isDisabled={!shouldShow}
placement={"bottom"}
showArrow={false}
trigger={"hover"}
>
{props.children}
</Tooltip>
);
};
export default CodeModeTooltip;

View File

@ -22,6 +22,8 @@ const initialState: WidgetDragResizeState = {
isDistributingSpace: false,
},
isDraggingDisabled: false,
blockSelection: false,
altFocus: false,
};
export const widgetDraggingReducer = createImmerReducer(initialState, {
@ -100,11 +102,20 @@ export const widgetDraggingReducer = createImmerReducer(initialState, {
},
[ReduxActionTypes.FOCUS_WIDGET]: (
state: WidgetDragResizeState,
action: ReduxAction<{ widgetId?: string }>,
action: ReduxAction<{ widgetId?: string; alt?: boolean }>,
) => {
if (state.focusedWidget !== action.payload.widgetId) {
state.focusedWidget = action.payload.widgetId;
}
if (state.altFocus !== action.payload.alt) {
state.altFocus = !!action.payload.alt;
}
},
[ReduxActionTypes.ALT_FOCUS_WIDGET]: (
state: WidgetDragResizeState,
action: ReduxAction<boolean>,
) => {
state.altFocus = action.payload;
},
[ReduxActionTypes.SET_SELECTED_WIDGET_ANCESTRY]: (
state: WidgetDragResizeState,
@ -118,6 +129,12 @@ export const widgetDraggingReducer = createImmerReducer(initialState, {
) => {
state.entityExplorerAncestry = action.payload;
},
[ReduxActionTypes.SET_WIDGET_SELECTION_BLOCK]: (
state: WidgetDragResizeState,
action: ReduxAction<boolean>,
) => {
state.blockSelection = action.payload;
},
//space distribution redux
[AnvilReduxActionTypes.ANVIL_SPACE_DISTRIBUTION_START]: (
state: WidgetDragResizeState,
@ -166,6 +183,8 @@ export interface WidgetDragResizeState {
selectedWidgets: string[];
isAutoCanvasResizing: boolean;
isDraggingDisabled: boolean;
blockSelection: boolean;
altFocus: boolean;
}
export default widgetDraggingReducer;

View File

@ -24,12 +24,12 @@ import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidg
import { all, call, put, select, take, takeLatest } from "redux-saga/effects";
import type { SetSelectionResult } from "sagas/WidgetSelectUtils";
import {
SelectionRequestType,
assertParentId,
getWidgetAncestry,
isInvalidSelectionRequest,
pushPopWidgetSelection,
selectAllWidgetsInCanvasSaga,
SelectionRequestType,
selectMultipleWidgets,
selectOneWidget,
shiftSelectWidgets,
@ -41,7 +41,11 @@ import {
getIsFetchingPage,
snipingModeSelector,
} from "selectors/editorSelectors";
import { getLastSelectedWidget, getSelectedWidgets } from "selectors/ui";
import {
getLastSelectedWidget,
getSelectedWidgets,
getWidgetSelectionBlock,
} from "selectors/ui";
import { areArraysEqual } from "utils/AppsmithUtils";
import { quickScrollToWidget } from "utils/helpers";
import history, { NavigationMethod } from "utils/history";
@ -56,6 +60,11 @@ import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
import type { FeatureFlags } from "@appsmith/entities/FeatureFlag";
import { getWidgetSelectorByWidgetId } from "selectors/layoutSystemSelectors";
import { getAppViewerPageIdFromPath } from "@appsmith/pages/Editor/Explorer/helpers";
import AnalyticsUtil from "../utils/AnalyticsUtil";
import {
retrieveCodeWidgetNavigationUsed,
storeCodeWidgetNavigationUsed,
} from "../utils/storage";
// The following is computed to be used in the entity explorer
// Every time a widget is selected, we need to expand widget entities
@ -208,9 +217,16 @@ function* appendSelectedWidgetToUrlSaga(
invokedBy?: NavigationMethod,
) {
const isSnipingMode: boolean = yield select(snipingModeSelector);
const isWidgetSelectionBlocked: boolean = yield select(
getWidgetSelectionBlock,
);
const timesUsedCodeModeWidgetSelection: number = yield call(
retrieveCodeWidgetNavigationUsed,
);
const appMode: APP_MODE = yield select(getAppMode);
const viewMode = appMode === APP_MODE.PUBLISHED;
if (isSnipingMode || viewMode) return;
const { pathname } = window.location;
const currentPageId: string = yield select(getCurrentPageId);
const currentURL = pathname;
@ -225,6 +241,15 @@ function* appendSelectedWidgetToUrlSaga(
persistExistingParams: true,
selectedWidgets: [MAIN_CONTAINER_WIDGET_ID],
});
if (invokedBy === NavigationMethod.CanvasClick && isWidgetSelectionBlocked) {
AnalyticsUtil.logEvent("CODE_MODE_WIDGET_SELECTION");
if (timesUsedCodeModeWidgetSelection < 2) {
yield call(
storeCodeWidgetNavigationUsed,
timesUsedCodeModeWidgetSelection + 1,
);
}
}
if (currentURL !== newUrl) {
history.push(newUrl, { invokedBy });
}

View File

@ -68,3 +68,16 @@ export const getIsImportingCurl = (state: AppState) =>
export const getIsConsolidatedPageLoading = (state: AppState) =>
state.ui.consolidatedPageLoad.isLoading;
export const getIsAltFocusWidget = (state: AppState) =>
state.ui.widgetDragResize.altFocus;
export const getWidgetSelectionBlock = (state: AppState) =>
state.ui.widgetDragResize.blockSelection;
export const getAltBlockWidgetSelection = createSelector(
[getWidgetSelectionBlock, getIsAltFocusWidget],
(isWidgetSelectionBlock, isAltFocusWidget) => {
return isWidgetSelectionBlock ? !isAltFocusWidget : false;
},
);

View File

@ -5,6 +5,7 @@ import {
combinedPreviewModeSelector,
snipingModeSelector,
} from "./editorSelectors";
import { getWidgetSelectionBlock } from "./ui";
export const getIsDragging = (state: AppState) =>
state.ui.widgetDragResize.isDragging;
@ -25,6 +26,7 @@ export const getShouldAllowDrag = createSelector(
combinedPreviewModeSelector,
snipingModeSelector,
getIsAppSettingsPaneWithNavigationTabOpen,
getWidgetSelectionBlock,
(
isResizing,
isDragging,
@ -32,6 +34,7 @@ export const getShouldAllowDrag = createSelector(
isPreviewMode,
isSnipingMode,
isAppSettingsPaneWithNavigationTabOpen,
widgetSelectionIsBlocked,
) => {
return (
!isResizing &&
@ -39,7 +42,8 @@ export const getShouldAllowDrag = createSelector(
!isDraggingDisabled &&
!isSnipingMode &&
!isPreviewMode &&
!isAppSettingsPaneWithNavigationTabOpen
!isAppSettingsPaneWithNavigationTabOpen &&
!widgetSelectionIsBlocked
);
},
);

View File

@ -9,6 +9,7 @@ import { getNextEntityName } from "utils/AppsmithUtils";
import WidgetFactory from "WidgetProvider/factory";
import {
getAltBlockWidgetSelection,
getFocusedWidget,
getLastSelectedWidget,
getSelectedWidgets,
@ -179,6 +180,7 @@ export const shouldWidgetIgnoreClicksSelector = (widgetId: string) => {
getAppMode,
combinedPreviewModeSelector,
getIsAutoHeightWithLimitsChanging,
getAltBlockWidgetSelection,
(
focusedWidgetId,
isTableFilterPaneVisible,
@ -188,6 +190,7 @@ export const shouldWidgetIgnoreClicksSelector = (widgetId: string) => {
appMode,
isPreviewMode,
isAutoHeightWithLimitsChanging,
isWidgetSelectionBlock,
) => {
const isFocused = focusedWidgetId === widgetId;
@ -199,7 +202,8 @@ export const shouldWidgetIgnoreClicksSelector = (widgetId: string) => {
appMode !== APP_MODE.EDIT ||
!isFocused ||
isTableFilterPaneVisible ||
isAutoHeightWithLimitsChanging
isAutoHeightWithLimitsChanging ||
isWidgetSelectionBlock
);
},
);

View File

@ -7,6 +7,7 @@ import { useSelector } from "react-redux";
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
import { LayoutSystemTypes } from "layoutSystems/types";
import { getWidgetSelectionBlock } from "../../selectors/ui";
export const useAllowEditorDragToSelect = () => {
// This state tells us whether a `ResizableComponent` is resizing
@ -46,6 +47,8 @@ export const useAllowEditorDragToSelect = () => {
getIsAppSettingsPaneWithNavigationTabOpen,
);
const isWidgetSelectionBlocked = useSelector(getWidgetSelectionBlock);
return (
isFixedLayout &&
!isAutoCanvasResizing &&
@ -53,6 +56,7 @@ export const useAllowEditorDragToSelect = () => {
!isDraggingDisabled &&
!isSnipingMode &&
!isPreviewMode &&
!isAppSettingsPaneWithNavigationTabOpen
!isAppSettingsPaneWithNavigationTabOpen &&
!isWidgetSelectionBlocked
);
};

View File

@ -1,4 +1,4 @@
import { focusWidget } from "actions/widgetActions";
import { altFocusWidget, focusWidget } from "actions/widgetActions";
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import { useCallback } from "react";
@ -23,7 +23,8 @@ export const useWidgetSelection = () => {
[dispatch],
),
focusWidget: useCallback(
(widgetId?: string) => dispatch(focusWidget(widgetId)),
(widgetId?: string, altFocus?: boolean) =>
dispatch(focusWidget(widgetId, altFocus)),
[dispatch],
),
deselectAll: useCallback(
@ -38,5 +39,8 @@ export const useWidgetSelection = () => {
[dispatch],
),
goToWidgetAdd: useCallback(() => history.push(builderURL({})), []),
altFocus: useCallback((alt: boolean) => {
dispatch(altFocusWidget(alt));
}, []),
};
};

View File

@ -38,6 +38,7 @@ export const STORAGE_KEYS: {
AI_KNOWLEDGE_BASE: "AI_KNOWLEDGE_BASE",
PARTNER_PROGRAM_CALLOUT: "PARTNER_PROGRAM_CALLOUT",
IDE_VIEW_MODE: "IDE_VIEW_MODE",
CODE_WIDGET_NAVIGATION_USED: "CODE_WIDGET_NAVIGATION_USED",
};
const store = localforage.createInstance({
@ -882,3 +883,26 @@ export const retrieveIDEViewMode = async (): Promise<
log.error(error);
}
};
export const storeCodeWidgetNavigationUsed = async (count: number) => {
try {
await store.setItem(STORAGE_KEYS.CODE_WIDGET_NAVIGATION_USED, count);
return true;
} catch (error) {
log.error("An error occurred while setting CODE_WIDGET_NAVIGATION_USED");
log.error(error);
}
};
export const retrieveCodeWidgetNavigationUsed = async (): Promise<number> => {
try {
const mode = (await store.getItem(
STORAGE_KEYS.CODE_WIDGET_NAVIGATION_USED,
)) as number;
return mode || 0;
} catch (error) {
log.error("An error occurred while fetching CODE_WIDGET_NAVIGATION_USED");
log.error(error);
return 0;
}
};

View File

@ -19,8 +19,6 @@ import { getTableFilterState } from "selectors/tableFilterSelectors";
import { getWidgetMetaProps } from "sagas/selectors";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type { WidgetProps } from "widgets/BaseWidget";
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { importSvg } from "design-system-old";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
@ -155,14 +153,12 @@ const mapDispatchToProps = (dispatch: any) => {
position,
},
});
dispatch(selectWidgetInitAction(SelectionRequestType.One, [widgetId]));
},
hideFilterPane: (widgetId: string) => {
dispatch({
type: ReduxActionTypes.HIDE_TABLE_FILTER_PANE,
payload: { widgetId },
});
dispatch(selectWidgetInitAction(SelectionRequestType.One, [widgetId]));
},
};
};