PromucFlow_constructor/app/client/src/ce/sagas/NavigationSagas.ts
Hetu Nandu 44d2b7e912
fix: Refresh breaks open tabs (#39021)
## Description

Fixes a focus retention issue where on refresh, the tabs are lost


EE Shadow: https://github.com/appsmithorg/appsmith-ee/pull/6106


## Automation

/ok-to-test tags="@tag.IDE"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/13151485815>
> Commit: e7ed2c3c16b1673b46a3bec68a7317e51bfbd575
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13151485815&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.IDE`
> Spec:
> <hr>Wed, 05 Feb 2025 06:53:31 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

This update delivers enhanced navigation stability and focus management
for a smoother browsing experience.

- **Bug Fixes**
- Improved focus handling during navigation transitions by ensuring
adjustments occur only when returning from a valid page. This refinement
minimizes unintended focus shifts, providing a more consistent and
user-friendly interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-05 12:23:42 +05:30

157 lines
5.2 KiB
TypeScript

import { fork, put, select, call, take } from "redux-saga/effects";
import type { RouteChangeActionPayload } from "actions/focusHistoryActions";
import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity";
import log from "loglevel";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { getRecentEntityIds } from "selectors/globalSearchSelectors";
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import { type ReduxAction } from "actions/ReduxActionTypes";
import { getCurrentThemeDetails } from "selectors/themeSelectors";
import type { BackgroundTheme } from "sagas/ThemeSaga";
import { changeAppBackground } from "sagas/ThemeSaga";
import { updateRecentEntitySaga } from "sagas/GlobalSearchSagas";
import {
setLastSelectedWidget,
setSelectedWidgets,
} from "actions/widgetSelectionActions";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import FocusRetention from "sagas/FocusRetentionSaga";
import { getSafeCrash } from "selectors/errorSelectors";
import { flushErrors } from "actions/errorActions";
import type { NavigationMethod } from "utils/history";
import UsagePulse from "usagePulse";
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
import type { EditorViewMode } from "ee/entities/IDE/constants";
import { IDE_TYPE } from "ee/entities/IDE/constants";
import { updateIDETabsOnRouteChangeSaga } from "sagas/IDESaga";
import { getIDEViewMode } from "selectors/ideSelectors";
let previousPath: string;
export function* handleRouteChange(
action: ReduxAction<RouteChangeActionPayload>,
) {
const { pathname, state } = action.payload.location;
try {
yield fork(clearErrors);
yield fork(watchForTrackableUrl, action.payload);
const IDEType = getIDETypeByUrl(pathname);
if (previousPath) {
yield fork(
FocusRetention.onRouteChange.bind(FocusRetention),
pathname,
previousPath,
state,
);
}
if (IDEType === IDE_TYPE.App) {
yield fork(logNavigationAnalytics, action.payload);
yield fork(appBackgroundHandler);
const entityInfo = identifyEntityFromPath(pathname);
yield fork(updateRecentEntitySaga, entityInfo);
yield fork(updateIDETabsOnRouteChangeSaga, entityInfo);
yield fork(setSelectedWidgetsSaga, state?.invokedBy);
}
} catch (e) {
log.error("Error in focus change", e);
} finally {
previousPath = pathname;
}
}
function* appBackgroundHandler() {
const currentTheme: BackgroundTheme = yield select(getCurrentThemeDetails);
changeAppBackground(currentTheme);
}
/**
* When an error occurs, we take over the whole router and keep it the error
* state till the errors are flushed. By default, we will flush out the
* error state when a CTA on the page is clicked but in case the
* user navigates via the browser buttons, this will ensure
* the errors are flushed
* */
function* clearErrors() {
const isCrashed: boolean = yield select(getSafeCrash);
if (isCrashed) {
yield put(flushErrors());
}
}
function* watchForTrackableUrl(payload: RouteChangeActionPayload) {
yield take([
ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS,
ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS,
]);
const oldPathname = payload.prevLocation.pathname;
const newPathname = payload.location.pathname;
const isOldPathTrackable: boolean = yield call(
UsagePulse.isTrackableUrl,
oldPathname,
);
const isNewPathTrackable: boolean = yield call(
UsagePulse.isTrackableUrl,
newPathname,
);
// Trackable to Trackable URL -> No pulse
// Non-Trackable to Non-Trackable URL -> No pulse
// Trackable to Non-Trackable -> No Pulse
// Non-Trackable to Trackable URL -> Send Pulse
if (!isOldPathTrackable && isNewPathTrackable) {
yield call(UsagePulse.sendPulseAndScheduleNext);
}
}
function* logNavigationAnalytics(payload: RouteChangeActionPayload) {
const {
location: { pathname, state },
} = payload;
const recentEntityIds: Array<string> = yield select(getRecentEntityIds);
const currentEntity = identifyEntityFromPath(pathname);
const previousEntity = identifyEntityFromPath(previousPath);
const isRecent = recentEntityIds.some(
(entityId) => entityId === currentEntity.id,
);
const ideViewMode: EditorViewMode = yield select(getIDEViewMode);
const { height, width } = window.screen;
AnalyticsUtil.logEvent("ROUTE_CHANGE", {
toPath: pathname,
fromPath: previousPath || undefined,
navigationMethod: state?.invokedBy,
isRecent,
recentLength: recentEntityIds.length,
toType: currentEntity.entity,
fromType: previousEntity.entity,
screenHeight: height,
screenWidth: width,
editorMode: ideViewMode,
});
}
function* setSelectedWidgetsSaga(invokedBy?: NavigationMethod) {
const pathname = window.location.pathname;
const entityInfo = identifyEntityFromPath(pathname);
let widgets: string[] = [];
let lastSelectedWidget = MAIN_CONTAINER_WIDGET_ID;
if (entityInfo.entity === FocusEntity.WIDGET) {
widgets = entityInfo.id.split(",");
if (widgets.length) {
lastSelectedWidget = widgets[widgets.length - 1];
}
}
yield put(setSelectedWidgets(widgets, invokedBy));
yield put(setLastSelectedWidget(lastSelectedWidget));
}