Skip to content

Commit 36090b8

Browse files
NikschavanCopilotdognose24
authored
STATS-69: Navigation: Allow go back to Posts & Pages Summary page (#103494)
Co-authored-by: Copilot <[email protected]> Co-authored-by: Dognose <[email protected]>
1 parent df80a74 commit 36090b8

File tree

7 files changed

+297
-20
lines changed

7 files changed

+297
-20
lines changed

client/components/navigation-header/navigation-header.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
text-decoration: none;
5050
font-size: $default-font-size;
5151
transition: color 0.2s ease;
52+
cursor: pointer;
5253

5354
&:hover {
5455
color: var(--wp-components-color-gray-900, $gray-900);

client/components/navigation-header/navigation-header.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import page from '@automattic/calypso-router';
12
import clsx from 'clsx';
23
import { translate } from 'i18n-calypso';
34
import { ReactNode } from 'react';
5+
import { popCurrentScreenFromHistory } from 'calypso/my-sites/stats/hooks/use-stats-navigation-history';
46
import './navigation-header.scss';
57

68
// Type definitions for the props
79
interface BackLinkProps {
810
url?: string;
911
text?: string;
10-
onBackClick?: ( e: React.MouseEvent< HTMLAnchorElement > ) => void;
12+
onBackClick?: () => void;
1113
}
1214

1315
export interface HeaderProps extends React.HTMLAttributes< HTMLElement > {
@@ -42,18 +44,22 @@ const NavigationHeader: React.FC< HeaderProps > = ( {
4244
backLinkProps,
4345
titleElement,
4446
headElement = backLinkProps?.url && (
45-
<a
47+
<button
4648
className="calypso-navigation-header__back-link"
47-
href={ backLinkProps?.url }
48-
onClick={ ( e ) => {
49+
type="button"
50+
aria-label={ backLinkProps?.text || translate( 'Back' ) }
51+
onClick={ () => {
52+
popCurrentScreenFromHistory();
53+
4954
if ( backLinkProps?.onBackClick ) {
50-
e.preventDefault();
51-
backLinkProps.onBackClick( e );
55+
backLinkProps.onBackClick();
56+
} else if ( backLinkProps?.url ) {
57+
page( backLinkProps.url );
5258
}
5359
} }
5460
>
5561
{ backLinkProps?.text ?? translate( 'Back' ) }
56-
</a>
62+
</button>
5763
),
5864
rightSection,
5965
hasScreenOptionsTab,

client/components/navigation-header/stories/navigation-header.stories.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ export const WithScreenOptions: Story = {
9090
backLinkProps: {
9191
url: '/dashboard',
9292
text: 'Back to Dashboard',
93-
onBackClick: ( e ) => {
94-
e.preventDefault();
93+
onBackClick: () => {
9594
alert( 'Back button clicked!' );
9695
},
9796
},
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { useEffect, useMemo, useState } from '@wordpress/element';
2+
import { buildQueryString } from '@wordpress/url';
3+
import { translate } from 'i18n-calypso';
4+
import { useSelector } from 'calypso/state';
5+
import { getSiteSlug } from 'calypso/state/sites/selectors';
6+
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
7+
8+
type QueryArgs = Record< string, string | null >;
9+
10+
const STORAGE_KEY = 'jp-stats-navigation';
11+
12+
const possibleBackLinks: { [ key: string ]: string | null } = {
13+
traffic: '/stats/{period}/',
14+
insights: '/stats/insights/',
15+
store: '/stats/store/',
16+
ads: '/stats/ads/',
17+
subscribers: '/stats/subscribers/{period}/',
18+
posts: '/stats/{period}/posts/',
19+
authors: '/stats/{period}/authors/',
20+
postDetails: null, // Last item in the history, the text is not displayed anywhere but this is used to track the item in history stack.
21+
};
22+
23+
const SUPPORTED_QUERY_PARAMS: string[] = [
24+
'startDate',
25+
'endDate',
26+
'num',
27+
'summarize',
28+
'chartStart',
29+
'chartEnd',
30+
'shortcut',
31+
];
32+
33+
const defaultLastScreen = 'traffic';
34+
35+
const getFilteredQueryParams = ( queryParams: QueryArgs ): QueryArgs => {
36+
return Object.fromEntries(
37+
Object.entries( queryParams ).filter( ( [ key ] ) => SUPPORTED_QUERY_PARAMS.includes( key ) )
38+
);
39+
};
40+
41+
/**
42+
* Hook for managing stats navigation state
43+
* Supports reading/writing from sessionStorage and initializing from query params
44+
* @returns { { text: string; url: string | null } }
45+
*/
46+
export const useStatsNavigationHistory = (): { text: string; url: string | null } => {
47+
const localizedTabNames: { [ key: string ]: string | null } = useMemo(
48+
() => ( {
49+
traffic: translate( 'Traffic' ),
50+
insights: translate( 'Insights' ),
51+
store: translate( 'Store' ),
52+
ads: translate( 'Ads' ),
53+
subscribers: translate( 'Subscribers' ),
54+
posts: translate( 'Posts & pages' ),
55+
authors: translate( 'Authors' ),
56+
postDetails: null,
57+
} ),
58+
[]
59+
);
60+
61+
const [ lastScreen, setLastScreen ] = useState< {
62+
screen: string;
63+
queryParams: QueryArgs;
64+
period: string | null;
65+
} >( {
66+
screen: defaultLastScreen,
67+
queryParams: {},
68+
period: 'day',
69+
} );
70+
const siteId = useSelector( getSelectedSiteId );
71+
const siteSlug = useSelector( ( state ) => getSiteSlug( state, siteId ) );
72+
73+
useEffect( () => {
74+
try {
75+
const navState = JSON.parse( sessionStorage.getItem( STORAGE_KEY ) || '[]' );
76+
77+
// Select the second last item from the history stack as the back link.
78+
// The last item in the stack if the current screen.
79+
const lastItem =
80+
Array.isArray( navState ) && navState.length >= 2 ? navState[ navState.length - 2 ] : {};
81+
82+
// Make sure it's array and select last item
83+
if ( lastItem && lastItem.screen ) {
84+
setLastScreen( lastItem );
85+
} else {
86+
setLastScreen( {
87+
screen: defaultLastScreen,
88+
queryParams: {},
89+
period: 'day',
90+
} );
91+
}
92+
} catch ( e ) {}
93+
}, [] );
94+
95+
const backLink = useMemo( () => {
96+
if ( ! siteSlug ) {
97+
return null;
98+
}
99+
100+
let backLink = possibleBackLinks[ lastScreen.screen ];
101+
102+
if ( ! backLink ) {
103+
return null;
104+
}
105+
106+
if ( backLink.includes( '{period}' ) && lastScreen.period ) {
107+
backLink = backLink.replace( '{period}', lastScreen.period );
108+
}
109+
110+
const queryParams = buildQueryString( getFilteredQueryParams( lastScreen.queryParams ) );
111+
112+
return backLink + siteSlug + ( queryParams ? '?' + queryParams : '' );
113+
}, [ lastScreen, siteSlug ] );
114+
115+
return {
116+
text: localizedTabNames[ lastScreen.screen ] || '',
117+
url: backLink,
118+
};
119+
};
120+
121+
/**
122+
* Utility to record the current screen for back navigation
123+
* @param {string} screen - Current screen identifier
124+
* @param {Object} args - Arguments for the screen
125+
* @param {Object} args.queryParams - Query parameters for the screen
126+
* @param {string} args.period - Period for the screen
127+
* @param {boolean} reset - Whether to reset the navigation history
128+
*/
129+
export const recordCurrentScreen = (
130+
screen: string,
131+
args: {
132+
queryParams: QueryArgs;
133+
period: string | null;
134+
} = {
135+
queryParams: {},
136+
period: null,
137+
},
138+
reset: boolean = false
139+
): void => {
140+
try {
141+
if ( ! screen || ! ( screen in possibleBackLinks ) ) {
142+
return;
143+
}
144+
145+
const filteredQueryParams = getFilteredQueryParams( args.queryParams );
146+
const currentEntry = {
147+
screen,
148+
queryParams: filteredQueryParams,
149+
period: args.period,
150+
};
151+
152+
// Get current navigation history array
153+
let navigationHistory = reset
154+
? []
155+
: JSON.parse( sessionStorage.getItem( STORAGE_KEY ) || '[]' );
156+
157+
// Ensure navigationHistory is an array
158+
if ( ! Array.isArray( navigationHistory ) ) {
159+
navigationHistory = [];
160+
}
161+
162+
// If the history already has the same screen, remove it
163+
if (
164+
navigationHistory.some(
165+
( entry: { screen: string } ) => entry.screen === currentEntry.screen
166+
)
167+
) {
168+
navigationHistory = navigationHistory.filter(
169+
( entry: { screen: string } ) => entry.screen !== currentEntry.screen
170+
);
171+
}
172+
173+
navigationHistory.push( currentEntry );
174+
sessionStorage.setItem( STORAGE_KEY, JSON.stringify( navigationHistory ) );
175+
} catch ( e ) {}
176+
};
177+
178+
/**
179+
* Utility to pop the current screen from the navigation history
180+
*/
181+
export const popCurrentScreenFromHistory = (): void => {
182+
try {
183+
const navigationHistory = JSON.parse( sessionStorage.getItem( STORAGE_KEY ) || '[]' );
184+
navigationHistory.pop();
185+
sessionStorage.setItem( STORAGE_KEY, JSON.stringify( navigationHistory ) );
186+
} catch ( e ) {}
187+
};

client/my-sites/stats/site.jsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { getMomentSiteZone } from 'calypso/my-sites/stats/hooks/use-moment-site-zone';
3939
import useNoticeVisibilityMutation from 'calypso/my-sites/stats/hooks/use-notice-visibility-mutation';
4040
import { useNoticeVisibilityQuery } from 'calypso/my-sites/stats/hooks/use-notice-visibility-query';
41+
import { recordCurrentScreen } from 'calypso/my-sites/stats/hooks/use-stats-navigation-history';
4142
import { getChartRangeParams } from 'calypso/my-sites/stats/utils';
4243
import {
4344
recordGoogleEvent,
@@ -899,7 +900,11 @@ const StatsBodyAccessCheck = ( props ) => {
899900
};
900901

901902
const StatsSite = ( props ) => {
902-
const { period } = props.period;
903+
const {
904+
context,
905+
period: { period },
906+
} = props;
907+
903908
const isOdysseyStats = config.isEnabled( 'is_running_in_jetpack_site' );
904909
const siteId = useSelector( getSelectedSiteId );
905910
const isJetpack = useSelector( ( state ) => isJetpackSite( state, siteId ) );
@@ -911,6 +916,17 @@ const StatsSite = ( props ) => {
911916
[]
912917
); // Track the last viewed tab.
913918

919+
useEffect( () => {
920+
recordCurrentScreen(
921+
'traffic',
922+
{
923+
queryParams: context.query,
924+
period: period,
925+
},
926+
true
927+
);
928+
}, [ context.query, period ] );
929+
914930
return (
915931
<Main fullWidthLayout ariaLabel={ STATS_PRODUCT_NAME }>
916932
{ /* Odyssey: Google Business Profile pages are currently unsupported. */ }

client/my-sites/stats/stats-post-detail/index.jsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { localizeUrl } from '@automattic/i18n-utils';
44
import { Button as CoreButton } from '@wordpress/components';
55
import clsx from 'clsx';
66
import { localize } from 'i18n-calypso';
7-
import { flowRight } from 'lodash';
7+
import { isEqual, flowRight } from 'lodash';
88
import PropTypes from 'prop-types';
99
import { Component } from 'react';
1010
import { connect } from 'react-redux';
@@ -19,6 +19,10 @@ import WebPreview from 'calypso/components/web-preview';
1919
import { decodeEntities, stripHTML } from 'calypso/lib/formatting';
2020
import PageHeader from 'calypso/my-sites/stats/components/headers/page-header';
2121
import Main from 'calypso/my-sites/stats/components/stats-main';
22+
import {
23+
useStatsNavigationHistory,
24+
recordCurrentScreen,
25+
} from 'calypso/my-sites/stats/hooks/use-stats-navigation-history';
2226
import StatsDetailsNavigation from 'calypso/my-sites/stats/stats-details-navigation';
2327
import { getSitePost, getPostPreviewUrl } from 'calypso/state/posts/selectors';
2428
import { countPostLikes } from 'calypso/state/posts/selectors/count-post-likes';
@@ -51,6 +55,10 @@ class StatsPostDetail extends Component {
5155
siteSlug: PropTypes.string,
5256
showViewLink: PropTypes.bool,
5357
previewUrl: PropTypes.string,
58+
lastScreen: PropTypes.shape( {
59+
text: PropTypes.string,
60+
url: PropTypes.string,
61+
} ),
5462
};
5563

5664
state = {
@@ -92,6 +100,22 @@ class StatsPostDetail extends Component {
92100

93101
componentDidMount() {
94102
window.scrollTo( 0, 0 );
103+
104+
const { context } = this.props;
105+
recordCurrentScreen( 'postDetails', {
106+
queryParams: context.query,
107+
period: null,
108+
} );
109+
}
110+
111+
componentDidUpdate( prevProps ) {
112+
const { context } = this.props;
113+
if ( ! isEqual( prevProps.context, this.props.context ) ) {
114+
recordCurrentScreen( 'postDetails', {
115+
queryParams: context.query,
116+
period: null,
117+
} );
118+
}
95119
}
96120

97121
openPreview = () => {
@@ -181,6 +205,7 @@ class StatsPostDetail extends Component {
181205
isSubscriptionsModuleActive,
182206
supportsEmailStats,
183207
isSimple,
208+
lastScreen,
184209
} = this.props;
185210

186211
const isLoading = isRequestingStats && ! countViews;
@@ -211,9 +236,10 @@ class StatsPostDetail extends Component {
211236
const navigationItems = this.getNavigationItemsWithTitle( this.getTitle() );
212237

213238
const backLinkProps = {
214-
text: navigationItems[ 0 ].label,
215-
url: navigationItems[ 0 ].href,
239+
text: lastScreen.text,
240+
url: lastScreen.url,
216241
};
242+
217243
const titleProps = {
218244
title: navigationItems[ 1 ].label,
219245
// Remove the default logo for Odyssey stats.
@@ -319,6 +345,11 @@ class StatsPostDetail extends Component {
319345
}
320346
}
321347

348+
const StatsPostDetailWrapper = ( props ) => {
349+
const lastScreen = useStatsNavigationHistory();
350+
return <StatsPostDetail { ...props } lastScreen={ lastScreen } />;
351+
};
352+
322353
const connectComponent = connect( ( state, { postId } ) => {
323354
const siteId = getSelectedSiteId( state );
324355
const isJetpack = isJetpackSite( state, siteId );
@@ -347,4 +378,4 @@ const connectComponent = connect( ( state, { postId } ) => {
347378
};
348379
} );
349380

350-
export default flowRight( connectComponent, localize )( StatsPostDetail );
381+
export default flowRight( connectComponent, localize )( StatsPostDetailWrapper );

0 commit comments

Comments
 (0)