From e99cc39e47d6caa4bd8d2b10d447889d8232ac5f Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Wed, 13 Mar 2024 11:53:49 +0530 Subject: [PATCH] chore: Block Selections when Canvas is in Side by Side mode (#31587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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" > [!IMPORTANT] > Workflow run: > Commit: `15e1cf937a9d15adaea68e16a55006d993a07cbf` > Cypress dashboard url: Click here! > All cypress tests have passed 🎉🎉🎉 ## 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. --- .../ClientSide/Widgets/ContainerTest2_spec.ts | 10 ++--- .../CurrencyInput/CurrencyInput_spec.js | 2 +- .../Widgets/ListV2/ListV2_NestedList_spec.ts | 1 + ...Listv2_BasicChildWidgetInteraction_spec.js | 14 +++---- .../ClientSide/Widgets/ListV2/Listv2_spec.js | 6 ++- .../TableV2_Button_Icon_validation_spec.js | 4 +- .../cypress/locators/commonlocators.json | 1 - app/client/src/actions/widgetActions.tsx | 17 +++++++- .../src/ce/constants/ReduxActionConstants.tsx | 2 + app/client/src/ce/utils/analyticsUtilTypes.ts | 3 +- .../common/draggable/DraggableComponent.tsx | 11 +++-- .../common/dropTarget/DropTargetComponent.tsx | 9 +++- .../common/resizer/ModalResizableLayer.tsx | 5 ++- .../common/resizer/ResizableComponent.tsx | 8 ++++ .../common/widgetName/SettingsControl.tsx | 32 +++++--------- .../CanvasSelectionArena.tsx | 3 ++ app/client/src/pages/Editor/Canvas.tsx | 31 +++++++------- .../src/pages/Editor/IDE/MainPane/index.tsx | 2 + app/client/src/pages/Editor/IDE/hooks.ts | 42 ++++++++++++++++++- .../Editor/WidgetsEditor/CodeModeTooltip.tsx | 36 ++++++++++++++++ .../reducers/uiReducers/dragResizeReducer.ts | 21 +++++++++- app/client/src/sagas/WidgetSelectionSagas.ts | 29 ++++++++++++- app/client/src/selectors/ui.tsx | 13 ++++++ .../src/selectors/widgetDragSelectors.ts | 6 ++- app/client/src/selectors/widgetSelectors.ts | 6 ++- .../utils/hooks/useAllowEditorDragToSelect.ts | 6 ++- .../src/utils/hooks/useWidgetSelection.ts | 8 +++- app/client/src/utils/storage.ts | 24 +++++++++++ .../header/actions/filter/FilterPane.tsx | 4 -- 29 files changed, 276 insertions(+), 80 deletions(-) create mode 100644 app/client/src/pages/Editor/WidgetsEditor/CodeModeTooltip.tsx diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ContainerTest2_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ContainerTest2_spec.ts index 27c8963d7c..9045781906 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ContainerTest2_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ContainerTest2_spec.ts @@ -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", () => { diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/CurrencyInput/CurrencyInput_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/CurrencyInput/CurrencyInput_spec.js index 221c984351..e9f9e34752 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/CurrencyInput/CurrencyInput_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/CurrencyInput/CurrencyInput_spec.js @@ -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"); }); }); }, diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/ListV2_NestedList_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/ListV2_NestedList_spec.ts index 266064d28d..2ce68154fb 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/ListV2_NestedList_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/ListV2_NestedList_spec.ts @@ -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, diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicChildWidgetInteraction_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicChildWidgetInteraction_spec.js index bcd2f9ecd9..7b9fae85d3 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicChildWidgetInteraction_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicChildWidgetInteraction_spec.js @@ -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, diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_spec.js index 4b0c2e1f6b..afebdc4abf 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_spec.js @@ -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); }); }, ); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2_Button_Icon_validation_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2_Button_Icon_validation_spec.js index 955087e218..83ba744fa2 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2_Button_Icon_validation_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/TableV2_Button_Icon_validation_spec.js @@ -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 () { diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 7443ea1aaf..cd6f50d6e6 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -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", diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index b67ca7ef21..0d16591262 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -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, + }; +}; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 4077a4ad75..de8c869fad 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -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 = { diff --git a/app/client/src/ce/utils/analyticsUtilTypes.ts b/app/client/src/ce/utils/analyticsUtilTypes.ts index 6fdfc40af1..2d4a0dd553 100644 --- a/app/client/src/ce/utils/analyticsUtilTypes.ts +++ b/app/client/src/ce/utils/analyticsUtilTypes.ts @@ -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" diff --git a/app/client/src/layoutSystems/common/draggable/DraggableComponent.tsx b/app/client/src/layoutSystems/common/draggable/DraggableComponent.tsx index 86d78353ef..05f4725940 100644 --- a/app/client/src/layoutSystems/common/draggable/DraggableComponent.tsx +++ b/app/client/src/layoutSystems/common/draggable/DraggableComponent.tsx @@ -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, + 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(); }; diff --git a/app/client/src/layoutSystems/common/dropTarget/DropTargetComponent.tsx b/app/client/src/layoutSystems/common/dropTarget/DropTargetComponent.tsx index 9639be084c..846dc4c1a8 100644 --- a/app/client/src/layoutSystems/common/dropTarget/DropTargetComponent.tsx +++ b/app/client/src/layoutSystems/common/dropTarget/DropTargetComponent.tsx @@ -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(); diff --git a/app/client/src/layoutSystems/common/resizer/ModalResizableLayer.tsx b/app/client/src/layoutSystems/common/resizer/ModalResizableLayer.tsx index ffe88063ef..ad5b71af17 100644 --- a/app/client/src/layoutSystems/common/resizer/ModalResizableLayer.tsx +++ b/app/client/src/layoutSystems/common/resizer/ModalResizableLayer.tsx @@ -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 ( ` - .${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 = ; + const errorIcon = ; return ( - + {isSnipingMode ? `Bind to widget ${props.name}` : `Edit widget`} + } - hoverOpenDelay={500} - position="top-right" + mouseEnterDelay={0} + placement="topRight" > - + ); } diff --git a/app/client/src/layoutSystems/fixedlayout/editor/FixedLayoutCanvasArenas/CanvasSelectionArena.tsx b/app/client/src/layoutSystems/fixedlayout/editor/FixedLayoutCanvasArenas/CanvasSelectionArena.tsx index ccde7e6ba3..3bb3653d1f 100644 --- a/app/client/src/layoutSystems/fixedlayout/editor/FixedLayoutCanvasArenas/CanvasSelectionArena.tsx +++ b/app/client/src/layoutSystems/fixedlayout/editor/FixedLayoutCanvasArenas/CanvasSelectionArena.tsx @@ -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 ); diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index 1b024f7939..87faf621f3 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -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 ( - - {props.widgetsStructure.widgetId && - renderAppsmithCanvas(props.widgetsStructure as WidgetProps)} - + + + {props.widgetsStructure.widgetId && + renderAppsmithCanvas(props.widgetsStructure as WidgetProps)} + + ); }; diff --git a/app/client/src/pages/Editor/IDE/MainPane/index.tsx b/app/client/src/pages/Editor/IDE/MainPane/index.tsx index bd2a5040b5..2f679302f5 100644 --- a/app/client/src/pages/Editor/IDE/MainPane/index.tsx +++ b/app/client/src/pages/Editor/IDE/MainPane/index.tsx @@ -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 (
{ 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)); + } + }; +} diff --git a/app/client/src/pages/Editor/WidgetsEditor/CodeModeTooltip.tsx b/app/client/src/pages/Editor/WidgetsEditor/CodeModeTooltip.tsx new file mode 100644 index 0000000000..9c1e2e8a17 --- /dev/null +++ b/app/client/src/pages/Editor/WidgetsEditor/CodeModeTooltip.tsx @@ -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(false); + useEffect(() => { + retrieveCodeWidgetNavigationUsed() + .then((timesUsed) => { + if (timesUsed < 2) { + setShouldShow(true); + } + }) + .catch(() => { + setShouldShow(true); + }); + }, [isWidgetSelectionBlock]); + if (!isWidgetSelectionBlock) return props.children; + return ( + + {props.children} + + ); +}; + +export default CodeModeTooltip; diff --git a/app/client/src/reducers/uiReducers/dragResizeReducer.ts b/app/client/src/reducers/uiReducers/dragResizeReducer.ts index fb41c9872f..dd52089a0c 100644 --- a/app/client/src/reducers/uiReducers/dragResizeReducer.ts +++ b/app/client/src/reducers/uiReducers/dragResizeReducer.ts @@ -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, + ) => { + 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, + ) => { + 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; diff --git a/app/client/src/sagas/WidgetSelectionSagas.ts b/app/client/src/sagas/WidgetSelectionSagas.ts index ecaf44e9e2..1fe5dbba35 100644 --- a/app/client/src/sagas/WidgetSelectionSagas.ts +++ b/app/client/src/sagas/WidgetSelectionSagas.ts @@ -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 }); } diff --git a/app/client/src/selectors/ui.tsx b/app/client/src/selectors/ui.tsx index 435410addc..2dfd0bef01 100644 --- a/app/client/src/selectors/ui.tsx +++ b/app/client/src/selectors/ui.tsx @@ -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; + }, +); diff --git a/app/client/src/selectors/widgetDragSelectors.ts b/app/client/src/selectors/widgetDragSelectors.ts index c962998b55..54343cb1cd 100644 --- a/app/client/src/selectors/widgetDragSelectors.ts +++ b/app/client/src/selectors/widgetDragSelectors.ts @@ -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 ); }, ); diff --git a/app/client/src/selectors/widgetSelectors.ts b/app/client/src/selectors/widgetSelectors.ts index 20e64217bb..04144e6586 100644 --- a/app/client/src/selectors/widgetSelectors.ts +++ b/app/client/src/selectors/widgetSelectors.ts @@ -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 ); }, ); diff --git a/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts b/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts index 6c1d2e734c..3d36241f26 100644 --- a/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts +++ b/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts @@ -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 ); }; diff --git a/app/client/src/utils/hooks/useWidgetSelection.ts b/app/client/src/utils/hooks/useWidgetSelection.ts index 2d275fd021..23b6a145af 100644 --- a/app/client/src/utils/hooks/useWidgetSelection.ts +++ b/app/client/src/utils/hooks/useWidgetSelection.ts @@ -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)); + }, []), }; }; diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index a0aaac610e..42914ece99 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -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 => { + 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; + } +}; diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPane.tsx b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPane.tsx index 4da3d76e0f..c7a4e5f028 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPane.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPane.tsx @@ -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])); }, }; };