Skip to content

Commit

Permalink
ref: add onRouteLeave shim (#75685)
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasBa authored and evanpurkhiser committed Aug 19, 2024
1 parent ce2d798 commit 6aef004
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 34 deletions.
48 changes: 47 additions & 1 deletion static/app/utils/reactRouter6Compat.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {Children, isValidElement} from 'react';
import type {InjectedRouter, PlainRoute} from 'react-router';
import {
generatePath,
type Location,
type Location as Location6,
Navigate,
type NavigateProps,
Outlet,
type RouteObject,
type To,
unstable_usePrompt,
useOutletContext,
} from 'react-router-dom';
import type {Location as Location3, LocationDescriptor, Query} from 'history';
import * as qs from 'query-string';

import {USING_CUSTOMER_DOMAIN} from 'sentry/constants';
import {USING_CUSTOMER_DOMAIN, USING_REACT_ROUTER_SIX} from 'sentry/constants';

import {useLocation} from './useLocation';
import {useParams} from './useParams';
Expand Down Expand Up @@ -227,3 +230,46 @@ export function location6ToLocation3<Q extends Query = DefaultQuery>(
action: 'POP',
} satisfies Location3<Q>;
}

// Shims useRouteLeave between react router versions
export function useRouteLeave() {
if (USING_REACT_ROUTER_SIX) {
unstable_usePrompt({
message: 'Are you sure?',
when: ({currentLocation, nextLocation}) =>
currentLocation.pathname !== nextLocation.pathname,
});
}
}

type ReactRouterV6RouteLeaveCallback = (state: {
currentLocation: Location;
nextLocation: Location;
}) => boolean;
type ReactRouterV3RouteLeaveCallback = () => string | undefined;

interface OnRouteLeaveProps {
legacyWhen: ReactRouterV3RouteLeaveCallback;
message: string;
route: PlainRoute;
router: InjectedRouter<any, any>;
when: ReactRouterV6RouteLeaveCallback;
}

export function OnRouteLeave(props: OnRouteLeaveProps) {
if (USING_REACT_ROUTER_SIX) {
unstable_usePrompt({
message: props.message,
when: state =>
props.when({
currentLocation: state.currentLocation,
nextLocation: state.nextLocation,
}),
});

return null;
}

props.router.setRouteLeaveHook(props.route, props.legacyWhen);
return null;
}
64 changes: 41 additions & 23 deletions static/app/views/dashboards/detail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {cloneElement, Component, isValidElement} from 'react';
import type {PlainRoute, RouteComponentProps} from 'react-router';
import type {Location} from 'react-router-dom';
import styled from '@emotion/styled';
import isEqual from 'lodash/isEqual';
import isEqualWith from 'lodash/isEqualWith';
Expand Down Expand Up @@ -37,6 +38,7 @@ import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metr
import {MetricsResultsMetaProvider} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {OnDemandControlProvider} from 'sentry/utils/performance/contexts/onDemandControl';
import {OnRouteLeave} from 'sentry/utils/reactRouter6Compat';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import withApi from 'sentry/utils/withApi';
import withOrganization from 'sentry/utils/withOrganization';
Expand Down Expand Up @@ -175,8 +177,6 @@ class DashboardDetail extends Component<Props, State> {
};

componentDidMount() {
const {route, router} = this.props;
router.setRouteLeaveHook(route, this.onRouteLeave);
window.addEventListener('beforeunload', this.onUnload);
this.checkIfShouldMountWidgetViewerModal();
}
Expand All @@ -194,6 +194,24 @@ class DashboardDetail extends Component<Props, State> {
window.removeEventListener('beforeunload', this.onUnload);
}

onUnload = (event: BeforeUnloadEvent) => {
const {dashboard} = this.props;
const {modifiedDashboard} = this.state;

if (
[
DashboardState.VIEW,
DashboardState.PENDING_DELETE,
DashboardState.PREVIEW,
].includes(this.state.dashboardState) ||
isEqual(modifiedDashboard, dashboard)
) {
return;
}
event.preventDefault();
event.returnValue = UNSAVED_MESSAGE;
};

checkIfShouldMountWidgetViewerModal() {
const {
params: {widgetId, dashboardId},
Expand Down Expand Up @@ -343,39 +361,25 @@ class DashboardDetail extends Component<Props, State> {
});
};

onRouteLeave = () => {
const {dashboard} = this.props;
const {modifiedDashboard} = this.state;

onLegacyRouteLeave = () => {
if (
![
DashboardState.VIEW,
DashboardState.PENDING_DELETE,
DashboardState.PREVIEW,
].includes(this.state.dashboardState) &&
!isEqual(modifiedDashboard, dashboard)
!isEqual(this.state.modifiedDashboard, this.props.dashboard)
) {
return UNSAVED_MESSAGE;
}
return undefined;
};

onUnload = (event: BeforeUnloadEvent) => {
const {dashboard} = this.props;
const {modifiedDashboard} = this.state;

if (
[
DashboardState.VIEW,
DashboardState.PENDING_DELETE,
DashboardState.PREVIEW,
].includes(this.state.dashboardState) ||
isEqual(modifiedDashboard, dashboard)
) {
return;
}
event.preventDefault();
event.returnValue = UNSAVED_MESSAGE;
onRouteLeave = (state: {currentLocation: Location; nextLocation: Location}) => {
return (
state.currentLocation.pathname !== state.nextLocation.pathname &&
!!this.onLegacyRouteLeave()
);
};

onDelete = (dashboard: State['modifiedDashboard']) => () => {
Expand Down Expand Up @@ -760,6 +764,13 @@ class DashboardDetail extends Component<Props, State> {
},
}}
>
<OnRouteLeave
router={this.props.router}
route={this.props.route}
message={UNSAVED_MESSAGE}
legacyWhen={this.onLegacyRouteLeave}
when={this.onRouteLeave}
/>
<Layout.Page withPadding>
<OnDemandControlProvider location={location}>
<MetricsResultsMetaProvider>
Expand Down Expand Up @@ -887,6 +898,13 @@ class DashboardDetail extends Component<Props, State> {
},
}}
>
<OnRouteLeave
router={this.props.router}
route={this.props.route}
message={UNSAVED_MESSAGE}
legacyWhen={this.onLegacyRouteLeave}
when={this.onRouteLeave}
/>
<Layout.Page>
<OnDemandControlProvider location={location}>
<MetricsResultsMetaProvider>
Expand Down
36 changes: 26 additions & 10 deletions static/app/views/dashboards/widgetBuilder/widgetBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useEffect, useMemo, useRef, useState} from 'react';
import type {RouteComponentProps} from 'react-router';
import type {Location} from 'react-router-dom';
import styled from '@emotion/styled';
import cloneDeep from 'lodash/cloneDeep';
import omit from 'lodash/omit';
Expand Down Expand Up @@ -44,6 +45,7 @@ import {
isOnDemandMetricWidget,
OnDemandControlProvider,
} from 'sentry/utils/performance/contexts/onDemandControl';
import {OnRouteLeave} from 'sentry/utils/reactRouter6Compat';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import useApi from 'sentry/utils/useApi';
import withPageFilters from 'sentry/utils/withPageFilters';
Expand Down Expand Up @@ -99,6 +101,9 @@ import {
} from './utils';
import {WidgetLibrary} from './widgetLibrary';

const UNSAVED_CHANGES_MESSAGE = t(
'You have unsaved changes, are you sure you want to leave?'
);
const WIDGET_TYPE_TO_DATA_SET = {
[WidgetType.DISCOVER]: DataSet.EVENTS,
[WidgetType.ISSUE]: DataSet.ISSUES,
Expand Down Expand Up @@ -366,17 +371,21 @@ function WidgetBuilder({
fetchOrgMembers(api, organization.slug, selection.projects?.map(String));
}, [selection.projects, api, organization.slug]);

useEffect(() => {
const onUnload = () => {
if (!isSubmittingRef.current && state.userHasModified) {
return t('You have unsaved changes, are you sure you want to leave?');
}
return undefined;
};

router.setRouteLeaveHook(route, onUnload);
}, [state.userHasModified, route, router]);
function onLegacyRouteLeave(): string | undefined {
return !isSubmittingRef.current && state.userHasModified
? UNSAVED_CHANGES_MESSAGE
: undefined;
}

function onRouteLeave(locationChange: {
currentLocation: Location;
nextLocation: Location;
}): boolean {
return (
locationChange.currentLocation.pathname !== locationChange.nextLocation.pathname &&
!!onLegacyRouteLeave()
);
}
const widgetType = DATA_SET_TO_WIDGET_TYPE[state.dataSet];

const currentWidget = {
Expand Down Expand Up @@ -1103,6 +1112,13 @@ function WidgetBuilder({
datetime: {start: null, end: null, utc: null, period: DEFAULT_STATS_PERIOD},
}}
>
<OnRouteLeave
message={UNSAVED_CHANGES_MESSAGE}
when={onRouteLeave}
legacyWhen={onLegacyRouteLeave}
route={route}
router={router}
/>
<CustomMeasurementsProvider organization={organization} selection={selection}>
<OnDemandControlProvider location={location}>
<MetricsResultsMetaProvider>
Expand Down

0 comments on commit 6aef004

Please sign in to comment.