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])); }, }; };