Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6863aa8
feat: add redux for canvas analytics
alex-golovanov Jun 11, 2024
8e68494
feat: add analytics canvas hover saga
alex-golovanov Jun 11, 2024
3136546
feat: add hook to verify side-by-side is active and present
alex-golovanov Jun 11, 2024
e360f95
feat: add hover logging in side-by-side mode
alex-golovanov Jun 11, 2024
c40e83f
feat: add mouseleave handler for canvas hover analytics
alex-golovanov Jun 11, 2024
cd1bc5a
Merge branch 'release' into feat/33159-side-by-side-analytics
alex-golovanov Jun 11, 2024
87c76b1
chore: revert widget name changes
alex-golovanov Jun 13, 2024
83c60d7
perf: move entity segment resolution to a util
alex-golovanov Jun 13, 2024
4e92f5c
chore: reverted canvas factory changes
alex-golovanov Jun 13, 2024
adf5f26
perf: add canvas click navigation method
alex-golovanov Jun 13, 2024
94d0281
perf: move analytics wrapper to a component and attach to editor canvas
alex-golovanov Jun 13, 2024
934df57
perf: add util for in side-by-side
alex-golovanov Jun 13, 2024
8d7824d
perf: add saga for widget hover
alex-golovanov Jun 13, 2024
cdc6bce
fix: add wrapper element check
alex-golovanov Jun 13, 2024
8727305
perf: add segment dependency & move logic to utility
alex-golovanov Jun 14, 2024
e2eca7d
chore: move hook to ide folder
alex-golovanov Jun 14, 2024
fd98592
test: add side-by-side hook tests
alex-golovanov Jun 14, 2024
49382da
chore: move analytics redux branch to ide
alex-golovanov Jun 17, 2024
8a693f8
perf: use canvas widgets selector
alex-golovanov Jun 17, 2024
e6ff7e1
perf: tweak event handler for fixed layout
alex-golovanov Jun 19, 2024
9cec247
feat: added queries editor path
alex-golovanov Jun 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/client/src/IDE/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useIsInSideBySideEditor } from "./useIsInSideBySideEditor";
147 changes: 147 additions & 0 deletions app/client/src/IDE/hooks/useIsInSideBySideEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -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<AppState>,
) => {
const wrapper: React.FC = ({ children }) => (
<Provider store={store}>
<Router history={history}>{children}</Router>
</Provider>
);
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);
});
});
21 changes: 21 additions & 0 deletions app/client/src/IDE/hooks/useIsInSideBySideEditor.ts
Original file line number Diff line number Diff line change
@@ -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 });
};
19 changes: 19 additions & 0 deletions app/client/src/actions/ideActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
8 changes: 8 additions & 0 deletions app/client/src/ce/constants/ReduxActionConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 2 additions & 0 deletions app/client/src/ce/sagas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -110,4 +111,5 @@ export const sagas = [
anvilSagas,
ternSagas,
ideSagas,
sendSideBySideWidgetHoverAnalyticsEventSaga,
];
3 changes: 2 additions & 1 deletion app/client/src/ce/utils/analyticsUtilTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,7 +59,9 @@ export const AnvilEditorCanvas = (props: BaseWidgetProps) => {
return (
<AnvilWidgetElevationProvider>
<AnvilDnDStatesContext.Provider value={anvilGlobalDnDStates}>
<AnvilViewerCanvas {...props} ref={canvasRef} />
<AnalyticsWrapper>
<AnvilViewerCanvas {...props} ref={canvasRef} />
</AnalyticsWrapper>
<AnvilDragPreview
dragDetails={anvilGlobalDnDStates.dragDetails}
draggedBlocks={anvilGlobalDnDStates.draggedBlocks}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { LAYOUT_WRAPPER_ID } from "./constants";
import { sendAnalyticsForSideBySideHover } from "actions/ideActions";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
import { LayoutSystemTypes } from "layoutSystems/types";
import { useIsInSideBySideEditor } from "IDE/hooks";

export const AnalyticsWrapper: React.FC = ({ children }) => {
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<HTMLDivElement> = (e) => {
const wrapperElement = document.getElementById(LAYOUT_WRAPPER_ID);

if (
isInSideBySideEditor &&
(isAnvil || (wrapperElement && wrapperElement === e.target))
) {
dispatch(sendAnalyticsForSideBySideHover());
}
};
return (
<div
className={className}
id={LAYOUT_WRAPPER_ID}
onMouseLeave={handleMouseLeave}
>
{children}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { v4 as uuid } from "uuid";

export const LAYOUT_WRAPPER_ID = uuid();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AnalyticsWrapper } from "./AnalyticsWrapper";
3 changes: 3 additions & 0 deletions app/client/src/layoutSystems/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { v4 as uuid } from "uuid";

export const LAYOUT_WRAPPER_ID = uuid();
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down Expand Up @@ -112,7 +113,7 @@ export const FixedLayoutEditorCanvas = (props: BaseWidgetProps) => {
widgetId={props.widgetId}
widgetType={props.type}
/>
{canvasChildren}
<AnalyticsWrapper>{canvasChildren}</AnalyticsWrapper>
</ContainerComponent>
</DropTargetComponentWrapper>
);
Expand Down
Loading