diff --git a/app/client/src/IDE/hooks/index.ts b/app/client/src/IDE/hooks/index.ts new file mode 100644 index 000000000000..50897137a667 --- /dev/null +++ b/app/client/src/IDE/hooks/index.ts @@ -0,0 +1 @@ +export { useIsInSideBySideEditor } from "./useIsInSideBySideEditor"; diff --git a/app/client/src/IDE/hooks/useIsInSideBySideEditor.test.tsx b/app/client/src/IDE/hooks/useIsInSideBySideEditor.test.tsx new file mode 100644 index 000000000000..04c50c5cf4b6 --- /dev/null +++ b/app/client/src/IDE/hooks/useIsInSideBySideEditor.test.tsx @@ -0,0 +1,147 @@ +import React from "react"; +import { renderHook, act } from "@testing-library/react-hooks/dom"; +import { Provider } from "react-redux"; +import { EditorViewMode } from "@appsmith/entities/IDE/constants"; +import { useIsInSideBySideEditor } from "./useIsInSideBySideEditor"; +import { getIDETestState } from "test/factories/AppIDEFactoryUtils"; +import type { AppState } from "@appsmith/reducers"; + +import { createMemoryHistory, type MemoryHistory } from "history"; +import { Router } from "react-router"; + +import { getIDEViewMode } from "../../selectors/ideSelectors"; +import { setIdeEditorViewMode } from "../../actions/ideActions"; +import { testStore } from "../../store"; +import type { Store } from "redux"; + +const JS_COLLECTION_EDITOR_PATH = + "/app/app-name/page-665dd1103e4483728c9ed11a/edit/jsObjects"; +const NON_JS_COLLECTION_EDITOR_PATH = "/some-other-path"; +const FEATURE_FLAGS = { + rollout_side_by_side_enabled: true, +}; + +const renderUseIsInSideBySideEditor = ( + history: MemoryHistory, + store: Store, +) => { + const wrapper: React.FC = ({ children }) => ( + + {children} + + ); + return renderHook(() => useIsInSideBySideEditor(), { + wrapper, + }); +}; + +describe("useIsInSideBySideEditor", () => { + it("should enter into split screen mode", () => { + const store = testStore( + getIDETestState({ + ideView: EditorViewMode.SplitScreen, + featureFlags: FEATURE_FLAGS, + }), + ); + + const ideViewMode = getIDEViewMode(store.getState()); + expect(ideViewMode).toBe(EditorViewMode.SplitScreen); + }); + + it("should return false when on correct path but not in SplitScreen mode", () => { + const store = testStore( + getIDETestState({ + ideView: EditorViewMode.FullScreen, + featureFlags: FEATURE_FLAGS, + }), + ); + + const history = createMemoryHistory({ + initialEntries: [JS_COLLECTION_EDITOR_PATH], + }); + + const { result } = renderUseIsInSideBySideEditor(history, store); + expect(result.current).toBe(false); + }); + + it("should return false when pathname does not satisfy JS_COLLECTION_EDITOR_PATH", () => { + const store = testStore( + getIDETestState({ + ideView: EditorViewMode.SplitScreen, + featureFlags: FEATURE_FLAGS, + }), + ); + + const history = createMemoryHistory({ + initialEntries: [NON_JS_COLLECTION_EDITOR_PATH], + }); + + const { result } = renderUseIsInSideBySideEditor(history, store); + + expect(result.current).toBe(false); + }); + + it("should return true when in SplitScreen mode and pathname satisfies JS_COLLECTION_EDITOR_PATH", () => { + const store = testStore( + getIDETestState({ + ideView: EditorViewMode.SplitScreen, + featureFlags: FEATURE_FLAGS, + }), + ); + + const history = createMemoryHistory({ + initialEntries: [JS_COLLECTION_EDITOR_PATH], + }); + + const { result } = renderUseIsInSideBySideEditor(history, store); + expect(result.current).toBe(true); + }); + + it("should update when ideViewMode changes", () => { + const store = testStore( + getIDETestState({ + ideView: EditorViewMode.SplitScreen, + featureFlags: FEATURE_FLAGS, + }), + ); + + const history = createMemoryHistory({ + initialEntries: [JS_COLLECTION_EDITOR_PATH], + }); + + const { rerender, result } = renderUseIsInSideBySideEditor(history, store); + + expect(result.current).toBe(true); + + act(() => { + store.dispatch(setIdeEditorViewMode(EditorViewMode.FullScreen)); + rerender(); + }); + + expect(getIDEViewMode(store.getState())).toBe(EditorViewMode.FullScreen); + expect(result.current).toBe(false); + }); + + it("should update when pathname changes", () => { + const store = testStore( + getIDETestState({ + ideView: EditorViewMode.SplitScreen, + featureFlags: FEATURE_FLAGS, + }), + ); + + const history = createMemoryHistory({ + initialEntries: [NON_JS_COLLECTION_EDITOR_PATH], + }); + + const { rerender, result } = renderUseIsInSideBySideEditor(history, store); + expect(result.current).toBe(false); + + act(() => { + history.push(JS_COLLECTION_EDITOR_PATH); + rerender(); + }); + + expect(result.current).toBe(true); + }); +}); diff --git a/app/client/src/IDE/hooks/useIsInSideBySideEditor.ts b/app/client/src/IDE/hooks/useIsInSideBySideEditor.ts new file mode 100644 index 000000000000..8ceb91ba95a0 --- /dev/null +++ b/app/client/src/IDE/hooks/useIsInSideBySideEditor.ts @@ -0,0 +1,21 @@ +import { useSelector } from "react-redux"; +import { useLocation } from "react-router"; + +import { getIDEViewMode } from "../../selectors/ideSelectors"; +import { identifyEntityFromPath } from "../../navigation/FocusEntity"; +import { + getCurrentEntityInfo, + isInSideBySideEditor, +} from "../../pages/Editor/utils"; + +/** + * Checks if current component is in side-by-side editor mode. + */ +export const useIsInSideBySideEditor = () => { + const { pathname } = useLocation(); + const viewMode = useSelector(getIDEViewMode); + const { appState, entity } = identifyEntityFromPath(pathname); + const { segment } = getCurrentEntityInfo(entity); + + return isInSideBySideEditor({ appState, segment, viewMode }); +}; diff --git a/app/client/src/actions/ideActions.ts b/app/client/src/actions/ideActions.ts index ac44769645d2..000da12c6b37 100644 --- a/app/client/src/actions/ideActions.ts +++ b/app/client/src/actions/ideActions.ts @@ -35,3 +35,22 @@ export const setShowQueryCreateNewModal = (payload: boolean) => { payload, }; }; + +export const recordAnalyticsForSideBySideWidgetHover = ( + widgetType: string, +) => ({ + type: ReduxActionTypes.RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_WIDGET_HOVER, + payload: widgetType, +}); + +export const sendAnalyticsForSideBySideHover = () => ({ + type: ReduxActionTypes.SEND_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER, +}); + +export const recordAnalyticsForSideBySideNavigation = () => ({ + type: ReduxActionTypes.RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_NAVIGATION, +}); + +export const resetAnalyticsForSideBySideHover = () => ({ + type: ReduxActionTypes.RESET_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER, +}); diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 8d49cfbc7b36..008daa1e60ed 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -929,6 +929,14 @@ const ActionTypes = { RESET_BUILDING_BLOCK_DRAG_START_TIME: "RESET_BUILDING_BLOCK_DRAG_START_TIME", CLOSE_QUERY_ACTION_TAB_SUCCESS: "CLOSE_QUERY_ACTION_TAB_SUCCESS", LINT_SETUP: "LINT_SETUP", + SEND_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER: + "SEND_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER", + RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_WIDGET_HOVER: + "RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_WIDGET_HOVER", + RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_NAVIGATION: + "RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_NAVIGATION", + RESET_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER: + "RESET_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER", }; export const ReduxActionTypes = { diff --git a/app/client/src/ce/sagas/index.tsx b/app/client/src/ce/sagas/index.tsx index 54daa5ccc22a..c1cb60a4e060 100644 --- a/app/client/src/ce/sagas/index.tsx +++ b/app/client/src/ce/sagas/index.tsx @@ -51,6 +51,7 @@ import entityNavigationSaga from "sagas/NavigationSagas"; import communityTemplateSagas from "sagas/CommunityTemplatesSagas"; import anvilSagas from "layoutSystems/anvil/integrations/sagas"; import ideSagas from "sagas/IDESaga"; +import sendSideBySideWidgetHoverAnalyticsEventSaga from "sagas/AnalyticsSaga"; /* Sagas that are registered by a module that is designed to be independent of the core platform */ import ternSagas from "sagas/TernSaga"; @@ -110,4 +111,5 @@ export const sagas = [ anvilSagas, ternSagas, ideSagas, + sendSideBySideWidgetHoverAnalyticsEventSaga, ]; diff --git a/app/client/src/ce/utils/analyticsUtilTypes.ts b/app/client/src/ce/utils/analyticsUtilTypes.ts index 36f1c025f6b6..988306feb3d7 100644 --- a/app/client/src/ce/utils/analyticsUtilTypes.ts +++ b/app/client/src/ce/utils/analyticsUtilTypes.ts @@ -354,7 +354,8 @@ export type EventName = | HOMEPAGE_CREATE_APP_FROM_TEMPLATE_EVENTS | "EDITOR_MODE_CHANGE" | BUILDING_BLOCKS_EVENTS - | "VISIT_SELF_HOST_DOCS"; + | "VISIT_SELF_HOST_DOCS" + | "CANVAS_HOVER"; type HOMEPAGE_CREATE_APP_FROM_TEMPLATE_EVENTS = | "TEMPLATE_DROPDOWN_CLICK" diff --git a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx index db005c914fba..c33e3ea1eeb5 100644 --- a/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx +++ b/app/client/src/layoutSystems/anvil/editor/AnvilWidgetName/AnvilWidgetNameComponent.tsx @@ -10,6 +10,7 @@ import { import { createMessage } from "@appsmith/constants/messages"; import { debugWidget } from "layoutSystems/anvil/integrations/actions"; import { useDispatch } from "react-redux"; +import { NavigationMethod } from "utils/history"; /** * Floating UI doesn't seem to respect initial styles from styled components or modules @@ -63,7 +64,11 @@ export function _AnvilWidgetNameComponent( }, [parentId]); const handleSelectWidget = useCallback(() => { - selectWidget(SelectionRequestType.One, [props.widgetId]); + selectWidget( + SelectionRequestType.One, + [props.widgetId], + NavigationMethod.CanvasClick, + ); }, [props.widgetId]); const handleDebugClick = useCallback(() => { diff --git a/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx b/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx index 67319c5c0976..a8c6f026cb05 100644 --- a/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx +++ b/app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx @@ -7,6 +7,7 @@ import type { AnvilGlobalDnDStates } from "./hooks/useAnvilGlobalDnDStates"; import { useAnvilGlobalDnDStates } from "./hooks/useAnvilGlobalDnDStates"; import { AnvilDragPreview } from "../canvasArenas/AnvilDragPreview"; import { AnvilWidgetElevationProvider } from "./providers/AnvilWidgetElevationProvider"; +import { AnalyticsWrapper } from "../../../common/AnalyticsWrapper"; export const AnvilDnDStatesContext = React.createContext< AnvilGlobalDnDStates | undefined @@ -58,7 +59,9 @@ export const AnvilEditorCanvas = (props: BaseWidgetProps) => { return ( - + + + { + const dispatch = useDispatch(); + const isInSideBySideEditor = useIsInSideBySideEditor(); + const layoutSystemType = useSelector(getLayoutSystemType); + const isAnvil = layoutSystemType === LayoutSystemTypes.ANVIL; + const className = isAnvil ? "contents" : "h-full"; + + const handleMouseLeave: React.MouseEventHandler = (e) => { + const wrapperElement = document.getElementById(LAYOUT_WRAPPER_ID); + + if ( + isInSideBySideEditor && + (isAnvil || (wrapperElement && wrapperElement === e.target)) + ) { + dispatch(sendAnalyticsForSideBySideHover()); + } + }; + return ( +
+ {children} +
+ ); +}; diff --git a/app/client/src/layoutSystems/common/AnalyticsWrapper/constants.ts b/app/client/src/layoutSystems/common/AnalyticsWrapper/constants.ts new file mode 100644 index 000000000000..cf13dc70f369 --- /dev/null +++ b/app/client/src/layoutSystems/common/AnalyticsWrapper/constants.ts @@ -0,0 +1,3 @@ +import { v4 as uuid } from "uuid"; + +export const LAYOUT_WRAPPER_ID = uuid(); diff --git a/app/client/src/layoutSystems/common/AnalyticsWrapper/index.ts b/app/client/src/layoutSystems/common/AnalyticsWrapper/index.ts new file mode 100644 index 000000000000..ce37b5f888ca --- /dev/null +++ b/app/client/src/layoutSystems/common/AnalyticsWrapper/index.ts @@ -0,0 +1 @@ +export { AnalyticsWrapper } from "./AnalyticsWrapper"; diff --git a/app/client/src/layoutSystems/constants.ts b/app/client/src/layoutSystems/constants.ts new file mode 100644 index 000000000000..cf13dc70f369 --- /dev/null +++ b/app/client/src/layoutSystems/constants.ts @@ -0,0 +1,3 @@ +import { v4 as uuid } from "uuid"; + +export const LAYOUT_WRAPPER_ID = uuid(); diff --git a/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutEditorCanvas.tsx b/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutEditorCanvas.tsx index 025531c9f8b3..b52dfbb1f2be 100644 --- a/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutEditorCanvas.tsx +++ b/app/client/src/layoutSystems/fixedlayout/canvas/FixedLayoutEditorCanvas.tsx @@ -14,6 +14,7 @@ import { FixedCanvasDraggingArena } from "../editor/FixedLayoutCanvasArenas/Fixe import { compact, sortBy } from "lodash"; import { Positioning } from "layoutSystems/common/utils/constants"; import type { DSLWidget } from "WidgetProvider/constants"; +import { AnalyticsWrapper } from "../../common/AnalyticsWrapper"; export type CanvasProps = DSLWidget; /** @@ -112,7 +113,7 @@ export const FixedLayoutEditorCanvas = (props: BaseWidgetProps) => { widgetId={props.widgetId} widgetType={props.type} /> - {canvasChildren} + {canvasChildren} ); diff --git a/app/client/src/pages/Editor/IDE/hooks.ts b/app/client/src/pages/Editor/IDE/hooks.ts index cd2b4e9a2db5..ef4639e90119 100644 --- a/app/client/src/pages/Editor/IDE/hooks.ts +++ b/app/client/src/pages/Editor/IDE/hooks.ts @@ -34,6 +34,7 @@ import { createEditorFocusInfoKey } from "@appsmith/navigation/FocusStrategy/App import { FocusElement } from "navigation/FocusElements"; import { closeJSActionTab } from "actions/jsActionActions"; import { closeQueryActionTab } from "actions/pluginActionActions"; +import { getCurrentEntityInfo } from "../utils"; export const useCurrentAppState = () => { const [appState, setAppState] = useState(EditorState.EDITOR); @@ -60,52 +61,10 @@ export const useCurrentEditorState = () => { * */ useEffect(() => { - const currentEntityInfo = identifyEntityFromPath(location.pathname); - switch (currentEntityInfo.entity) { - case FocusEntity.QUERY: - case FocusEntity.API: - case FocusEntity.QUERY_MODULE_INSTANCE: - setSelectedSegment(EditorEntityTab.QUERIES); - setSelectedSegmentState(EditorEntityTabState.Edit); - break; - case FocusEntity.QUERY_LIST: - setSelectedSegment(EditorEntityTab.QUERIES); - setSelectedSegmentState(EditorEntityTabState.List); - break; - case FocusEntity.QUERY_ADD: - setSelectedSegment(EditorEntityTab.QUERIES); - setSelectedSegmentState(EditorEntityTabState.Add); - break; - case FocusEntity.JS_OBJECT: - case FocusEntity.JS_MODULE_INSTANCE: - setSelectedSegment(EditorEntityTab.JS); - setSelectedSegmentState(EditorEntityTabState.Edit); - break; - case FocusEntity.JS_OBJECT_ADD: - setSelectedSegment(EditorEntityTab.JS); - setSelectedSegmentState(EditorEntityTabState.Add); - break; - case FocusEntity.JS_OBJECT_LIST: - setSelectedSegment(EditorEntityTab.JS); - setSelectedSegmentState(EditorEntityTabState.List); - break; - case FocusEntity.CANVAS: - setSelectedSegment(EditorEntityTab.UI); - setSelectedSegmentState(EditorEntityTabState.Add); - break; - case FocusEntity.PROPERTY_PANE: - setSelectedSegment(EditorEntityTab.UI); - setSelectedSegmentState(EditorEntityTabState.Edit); - break; - case FocusEntity.WIDGET_LIST: - setSelectedSegment(EditorEntityTab.UI); - setSelectedSegmentState(EditorEntityTabState.List); - break; - default: - setSelectedSegment(EditorEntityTab.UI); - setSelectedSegmentState(EditorEntityTabState.Add); - break; - } + const { entity } = identifyEntityFromPath(location.pathname); + const { segment, segmentMode } = getCurrentEntityInfo(entity); + setSelectedSegment(segment); + setSelectedSegmentState(segmentMode); }, [location.pathname]); return { diff --git a/app/client/src/pages/Editor/utils.tsx b/app/client/src/pages/Editor/utils.tsx index fab1158cc45d..e47970482238 100644 --- a/app/client/src/pages/Editor/utils.tsx +++ b/app/client/src/pages/Editor/utils.tsx @@ -33,6 +33,13 @@ import { getAssetUrl } from "@appsmith/utils/airgapHelpers"; import type { Plugin } from "api/PluginApi"; import ImageAlt from "assets/images/placeholder-image.svg"; import { Icon } from "design-system"; +import { + EditorEntityTab, + EditorEntityTabState, + EditorState, + EditorViewMode, +} from "@appsmith/entities/IDE/constants"; +import { FocusEntity } from "navigation/FocusEntity"; export const draggableElement = ( id: string, @@ -415,3 +422,83 @@ export function getPluginImagesFromPlugins(plugins: Plugin[]) { }); return pluginImages; } + +/** + * Resolve segment and segmentMode based on entity type. + */ +export function getCurrentEntityInfo(entity: FocusEntity) { + switch (entity) { + case FocusEntity.QUERY: + case FocusEntity.API: + case FocusEntity.QUERY_MODULE_INSTANCE: + return { + segment: EditorEntityTab.QUERIES, + segmentMode: EditorEntityTabState.Edit, + }; + case FocusEntity.QUERY_LIST: + return { + segment: EditorEntityTab.QUERIES, + segmentMode: EditorEntityTabState.List, + }; + case FocusEntity.QUERY_ADD: + return { + segment: EditorEntityTab.QUERIES, + segmentMode: EditorEntityTabState.Add, + }; + case FocusEntity.JS_OBJECT: + case FocusEntity.JS_MODULE_INSTANCE: + return { + segment: EditorEntityTab.JS, + segmentMode: EditorEntityTabState.Edit, + }; + case FocusEntity.JS_OBJECT_ADD: + return { + segment: EditorEntityTab.JS, + segmentMode: EditorEntityTabState.Add, + }; + case FocusEntity.JS_OBJECT_LIST: + return { + segment: EditorEntityTab.JS, + segmentMode: EditorEntityTabState.List, + }; + case FocusEntity.CANVAS: + return { + segment: EditorEntityTab.UI, + segmentMode: EditorEntityTabState.Add, + }; + case FocusEntity.PROPERTY_PANE: + return { + segment: EditorEntityTab.UI, + segmentMode: EditorEntityTabState.Edit, + }; + case FocusEntity.WIDGET_LIST: + return { + segment: EditorEntityTab.UI, + segmentMode: EditorEntityTabState.List, + }; + default: + return { + segment: EditorEntityTab.UI, + segmentMode: EditorEntityTabState.Add, + }; + } +} + +/** + * Check if use is currently working is side-by-side editor mode. + */ +export function isInSideBySideEditor({ + appState, + segment, + viewMode, +}: { + viewMode: EditorViewMode; + appState: EditorState; + segment: EditorEntityTab; +}) { + return ( + viewMode === EditorViewMode.SplitScreen && + appState === EditorState.EDITOR && + segment !== EditorEntityTab.UI + ); +} diff --git a/app/client/src/reducers/uiReducers/ideReducer.ts b/app/client/src/reducers/uiReducers/ideReducer.ts index c155157b6ed4..8226ad605649 100644 --- a/app/client/src/reducers/uiReducers/ideReducer.ts +++ b/app/client/src/reducers/uiReducers/ideReducer.ts @@ -17,6 +17,10 @@ const initialState: IDEState = { view: EditorViewMode.FullScreen, tabs: {}, showCreateModal: false, + ideCanvasSideBySideHover: { + navigated: false, + widgetTypes: [], + }, }; const ideReducer = createImmerReducer(initialState, { @@ -80,12 +84,31 @@ const ideReducer = createImmerReducer(initialState, { ); remove(tabs, (tab) => tab === action.payload.id); }, + [ReduxActionTypes.RESET_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER]: ( + state: IDEState, + ) => { + state.ideCanvasSideBySideHover = klona( + initialState.ideCanvasSideBySideHover, + ); + }, + [ReduxActionTypes.RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_NAVIGATION]: ( + state: IDEState, + ) => { + state.ideCanvasSideBySideHover.navigated = true; + }, + [ReduxActionTypes.RECORD_ANALYTICS_FOR_SIDE_BY_SIDE_WIDGET_HOVER]: ( + state: IDEState, + action: ReduxAction, + ) => { + state.ideCanvasSideBySideHover.widgetTypes.push(action.payload); + }, }); export interface IDEState { view: EditorViewMode; tabs: ParentEntityIDETabs; showCreateModal: boolean; + ideCanvasSideBySideHover: IDECanvasSideBySideHover; } export interface ParentEntityIDETabs { @@ -97,4 +120,9 @@ export interface IDETabs { [EditorEntityTab.QUERIES]: string[]; } +export interface IDECanvasSideBySideHover { + navigated: boolean; + widgetTypes: string[]; +} + export default ideReducer; diff --git a/app/client/src/sagas/AnalyticsSaga.ts b/app/client/src/sagas/AnalyticsSaga.ts index 1790faaad832..940f59c063e5 100644 --- a/app/client/src/sagas/AnalyticsSaga.ts +++ b/app/client/src/sagas/AnalyticsSaga.ts @@ -11,6 +11,29 @@ import { } from "./helper"; import get from "lodash/get"; import log from "loglevel"; +import { all, put, select, takeEvery } from "redux-saga/effects"; +import { getIdeCanvasSideBySideHoverState } from "selectors/ideSelectors"; + +import { EditorViewMode } from "@appsmith/entities/IDE/constants"; +import { + recordAnalyticsForSideBySideNavigation, + recordAnalyticsForSideBySideWidgetHover, + resetAnalyticsForSideBySideHover, +} from "actions/ideActions"; + +import type { routeChanged } from "actions/focusHistoryActions"; +import { NavigationMethod } from "utils/history"; +import { getIDEViewMode } from "selectors/ideSelectors"; + +import { + JS_COLLECTION_EDITOR_PATH, + QUERIES_EDITOR_BASE_PATH, + WIDGETS_EDITOR_BASE_PATH, +} from "constants/routes"; +import type { focusWidget } from "actions/widgetActions"; +import { getCanvasWidgets } from "@appsmith/selectors/entitiesSelector"; +import { identifyEntityFromPath } from "navigation/FocusEntity"; +import { getCurrentEntityInfo, isInSideBySideEditor } from "pages/Editor/utils"; export function* sendAnalyticsEventSaga( type: ReduxActionType, @@ -50,3 +73,86 @@ export function* sendAnalyticsEventSaga( log.error("Failed to send analytics event"); } } + +function* sendSideBySideWidgetHoverAnalyticsEventSaga() { + const { + navigated, + widgetTypes, + }: ReturnType = yield select( + getIdeCanvasSideBySideHoverState, + ); + + const payload = { + navigated, + widgetHover: widgetTypes.length > 0, + widgetTypes: Array.from(new Set(widgetTypes)), + }; + + yield put(resetAnalyticsForSideBySideHover()); + + AnalyticsUtil.logEvent("CANVAS_HOVER", payload); +} + +function* routeChangeInSideBySideModeSaga({ + payload, +}: ReturnType) { + const viewMode: ReturnType = + yield select(getIDEViewMode); + + const { + location: { pathname: pathName, state }, + prevLocation: { pathname: prevPathName }, + } = payload; + + const invokedBy = state?.invokedBy; + + if ( + invokedBy === NavigationMethod.CanvasClick && + viewMode === EditorViewMode.SplitScreen && + pathName.includes(WIDGETS_EDITOR_BASE_PATH) && + (prevPathName.includes(JS_COLLECTION_EDITOR_PATH) || + prevPathName.includes(QUERIES_EDITOR_BASE_PATH)) + ) { + yield put(recordAnalyticsForSideBySideNavigation()); + yield sendSideBySideWidgetHoverAnalyticsEventSaga(); + } +} + +function* focusWidgetInSideBySideModeSaga({ + payload, +}: ReturnType) { + const { widgetId } = payload; + + if (widgetId) { + const viewMode: ReturnType = + yield select(getIDEViewMode); + + const { appState, entity } = identifyEntityFromPath( + window.location.pathname, + ); + + const { segment } = getCurrentEntityInfo(entity); + + if (isInSideBySideEditor({ appState, segment, viewMode })) { + const widgets: ReturnType = + yield select(getCanvasWidgets); + const widget = widgets[widgetId]; + + if (widget) { + yield put(recordAnalyticsForSideBySideWidgetHover(widget.type)); + } + } + } +} + +export default function* root() { + yield all([ + takeEvery( + ReduxActionTypes.SEND_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER, + sendSideBySideWidgetHoverAnalyticsEventSaga, + ), + + takeEvery(ReduxActionTypes.ROUTE_CHANGED, routeChangeInSideBySideModeSaga), + takeEvery(ReduxActionTypes.FOCUS_WIDGET, focusWidgetInSideBySideModeSaga), + ]); +} diff --git a/app/client/src/selectors/ideSelectors.tsx b/app/client/src/selectors/ideSelectors.tsx index 697e833f79f1..6bb835c1322d 100644 --- a/app/client/src/selectors/ideSelectors.tsx +++ b/app/client/src/selectors/ideSelectors.tsx @@ -60,3 +60,6 @@ export const getQueryTabs = createSelector( export const getShowCreateNewModal = (state: AppState) => state.ui.ide.showCreateModal; + +export const getIdeCanvasSideBySideHoverState = (state: AppState) => + state.ui.ide.ideCanvasSideBySideHover; diff --git a/app/client/test/factories/AppIDEFactoryUtils.ts b/app/client/test/factories/AppIDEFactoryUtils.ts index 28b65206af47..60c925159ee7 100644 --- a/app/client/test/factories/AppIDEFactoryUtils.ts +++ b/app/client/test/factories/AppIDEFactoryUtils.ts @@ -12,6 +12,7 @@ import { IDETabsDefaultValue } from "reducers/uiReducers/ideReducer"; import type { JSCollection } from "entities/JSCollection"; import type { FocusHistory } from "reducers/uiReducers/focusHistoryReducer"; import type { Datasource } from "entities/Datasource"; +import type { FeatureFlags } from "@appsmith/entities/FeatureFlag"; interface IDEStateArgs { ideView?: EditorViewMode; @@ -22,12 +23,14 @@ interface IDEStateArgs { branch?: string; focusHistory?: FocusHistory; datasources?: Datasource[]; + featureFlags?: Partial; } export const getIDETestState = ({ actions = [], branch, datasources = [], + featureFlags, focusHistory = {}, ideView = EditorViewMode.FullScreen, js = [], @@ -44,14 +47,15 @@ export const getIDETestState = ({ defaultPageId: pages[0]?.pageId, loading: {}, }; + let ideTabs: ParentEntityIDETabs = {}; if (pageList.currentPageId) { ideTabs = { [pageList.currentPageId]: tabs }; } const actionData = actions.map((a) => ({ isLoading: false, config: a })); - const jsData = js.map((a) => ({ isLoading: false, config: a })); + const featureFlag = featureFlags ? { data: featureFlags } : {}; return { ...initialState, @@ -68,6 +72,10 @@ export const getIDETestState = ({ }, ui: { ...initialState.ui, + users: { + ...initialState.ui.users, + featureFlag, + }, ide: { ...initialState.ui.ide, view: ideView,