Skip to content

Commit fed2139

Browse files
authored
enhancement: add back button fallback support (strapi#21970)
* enhancement: add back button fallback support * enhancement: add fallback urls * fix: feedback and fixes
1 parent b0db564 commit fed2139

File tree

11 files changed

+80
-46
lines changed

11 files changed

+80
-46
lines changed

packages/core/admin/admin/src/features/BackButton.tsx

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Link, LinkProps } from '@strapi/design-system';
44
import { ArrowLeft } from '@strapi/icons';
55
import { produce } from 'immer';
66
import { useIntl } from 'react-intl';
7-
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
7+
import { NavLink, type To, useLocation, useNavigate } from 'react-router-dom';
88

99
import { createContext } from '../components/Context';
1010

@@ -188,43 +188,61 @@ const reducer = (state: HistoryState, action: HistoryActions) =>
188188
/* -------------------------------------------------------------------------------------------------
189189
* BackButton
190190
* -----------------------------------------------------------------------------------------------*/
191-
interface BackButtonProps extends Pick<LinkProps, 'disabled'> {}
191+
interface BackButtonProps extends Pick<LinkProps, 'disabled'> {
192+
fallback?: To;
193+
}
192194

193195
/**
194196
* @beta
195197
* @description The universal back button for the Strapi application. This uses the internal history
196198
* context to navigate the user back to the previous location. It can be completely disabled in a
197-
* specific user case.
199+
* specific user case. When no history is available, you can provide a fallback destination,
200+
* otherwise the link will be disabled.
198201
*/
199-
const BackButton = React.forwardRef<HTMLAnchorElement, BackButtonProps>(({ disabled }, ref) => {
200-
const { formatMessage } = useIntl();
201-
202-
const canGoBack = useHistory('BackButton', (state) => state.canGoBack);
203-
const goBack = useHistory('BackButton', (state) => state.goBack);
204-
const history = useHistory('BackButton', (state) => state.history);
205-
206-
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
207-
e.preventDefault();
208-
goBack();
209-
};
202+
const BackButton = React.forwardRef<HTMLAnchorElement, BackButtonProps>(
203+
({ disabled, fallback = '' }, ref) => {
204+
const { formatMessage } = useIntl();
205+
const navigate = useNavigate();
206+
207+
const canGoBack = useHistory('BackButton', (state) => state.canGoBack);
208+
const goBack = useHistory('BackButton', (state) => state.goBack);
209+
const history = useHistory('BackButton', (state) => state.history);
210+
const hasFallback = fallback !== '';
211+
const shouldBeDisabled = disabled || (!canGoBack && !hasFallback);
212+
213+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
214+
e.preventDefault();
215+
216+
if (canGoBack) {
217+
goBack();
218+
} else if (hasFallback) {
219+
navigate(fallback);
220+
}
221+
};
210222

211-
return (
212-
<Link
213-
ref={ref}
214-
tag={NavLink}
215-
to={history.at(-1) ?? ''}
216-
onClick={handleClick}
217-
disabled={disabled || !canGoBack}
218-
aria-disabled={disabled || !canGoBack}
219-
startIcon={<ArrowLeft />}
220-
>
221-
{formatMessage({
222-
id: 'global.back',
223-
defaultMessage: 'Back',
224-
})}
225-
</Link>
226-
);
227-
});
223+
// The link destination from the history. Undefined if there is only 1 location in the history.
224+
const historyTo = canGoBack ? history.at(-1) : undefined;
225+
// If no link destination from the history, use the fallback.
226+
const toWithFallback = historyTo ?? fallback;
227+
228+
return (
229+
<Link
230+
ref={ref}
231+
tag={NavLink}
232+
to={toWithFallback}
233+
onClick={handleClick}
234+
disabled={shouldBeDisabled}
235+
aria-disabled={shouldBeDisabled}
236+
startIcon={<ArrowLeft />}
237+
>
238+
{formatMessage({
239+
id: 'global.back',
240+
defaultMessage: 'Back',
241+
})}
242+
</Link>
243+
);
244+
}
245+
);
228246

229247
export { BackButton, HistoryProvider, useHistory };
230248
export type { BackButtonProps, HistoryProviderProps, HistoryContextValue, HistoryState };

packages/core/admin/admin/src/features/tests/BackButton.test.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useMemo } from 'react';
33
import { render as renderRTL, screen, waitFor } from '@tests/utils';
44
import { NavLink, useLocation } from 'react-router-dom';
55

6-
import { BackButton, HistoryProvider } from '../BackButton';
6+
import { BackButton, type BackButtonProps, HistoryProvider } from '../BackButton';
77

88
const LocationDisplay = () => {
99
const location = useLocation();
@@ -19,8 +19,8 @@ const RandomNavLink = () => {
1919
return <NavLink to={to}>Navigate</NavLink>;
2020
};
2121

22-
const render = () =>
23-
renderRTL(<BackButton />, {
22+
const render = (props: BackButtonProps = {}) =>
23+
renderRTL(<BackButton {...props} />, {
2424
renderOptions: {
2525
wrapper({ children }) {
2626
return (
@@ -35,12 +35,18 @@ const render = () =>
3535
});
3636

3737
describe('BackButton', () => {
38-
it('should be disabled if there is no history', () => {
38+
it('should be disabled if there is no history and no fallback', () => {
3939
render();
4040

4141
expect(screen.getByRole('link', { name: 'Back' })).toHaveAttribute('aria-disabled', 'true');
4242
});
4343

44+
it('should be enabled if there is a fallback', () => {
45+
render({ fallback: '..' });
46+
47+
expect(screen.getByRole('link', { name: 'Back' })).toHaveAttribute('aria-disabled', 'false');
48+
});
49+
4450
it('should be enabled if there is history', async () => {
4551
const { user } = render();
4652

packages/core/admin/admin/src/pages/Settings/pages/Roles/CreatePage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ const CreatePage = () => {
220220
id: 'Settings.roles.create.description',
221221
defaultMessage: 'Define the rights given to the role',
222222
})}
223-
navigationAction={<BackButton />}
223+
navigationAction={<BackButton fallback="../roles" />}
224224
/>
225225
<Layouts.Content>
226226
<Flex direction="column" alignItems="stretch" gap={6}>

packages/core/admin/admin/src/pages/Settings/pages/Roles/EditPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ const EditPage = () => {
211211
id: 'Settings.roles.create.description',
212212
defaultMessage: 'Define the rights given to the role',
213213
})}
214-
navigationAction={<BackButton />}
214+
navigationAction={<BackButton fallback="../roles" />}
215215
/>
216216
<Layouts.Content>
217217
<Flex direction="column" alignItems="stretch" gap={6}>

packages/core/admin/admin/src/pages/Settings/pages/Users/EditPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ const EditPage = () => {
205205
name: getDisplayName(initialData),
206206
}
207207
)}
208-
navigationAction={<BackButton />}
208+
navigationAction={<BackButton fallback="../users" />}
209209
/>
210210
<Layouts.Content>
211211
{user?.registrationToken && (

packages/core/admin/admin/src/pages/Settings/pages/Webhooks/components/WebhookForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const WebhookForm = ({
125125
})
126126
: data?.name
127127
}
128-
navigationAction={<BackButton />}
128+
navigationAction={<BackButton fallback="../webhooks" />}
129129
/>
130130
<Layouts.Content>
131131
<Flex direction="column" alignItems="stretch" gap={4}>

packages/core/content-manager/admin/src/pages/EditView/components/Header.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from '@strapi/design-system';
2020
import { ListPlus, Pencil, Trash, WarningCircle } from '@strapi/icons';
2121
import { useIntl } from 'react-intl';
22-
import { useMatch, useNavigate } from 'react-router-dom';
22+
import { useMatch, useNavigate, useParams } from 'react-router-dom';
2323

2424
import { RelativeTime } from '../../../components/RelativeTime';
2525
import {
@@ -30,7 +30,7 @@ import {
3030
UPDATED_AT_ATTRIBUTE_NAME,
3131
UPDATED_BY_ATTRIBUTE_NAME,
3232
} from '../../../constants/attributes';
33-
import { SINGLE_TYPES } from '../../../constants/collections';
33+
import { COLLECTION_TYPES, SINGLE_TYPES } from '../../../constants/collections';
3434
import { useDocumentRBAC } from '../../../features/DocumentRBAC';
3535
import { useDoc } from '../../../hooks/useDocument';
3636
import { useDocumentActions } from '../../../hooks/useDocumentActions';
@@ -55,6 +55,7 @@ interface HeaderProps {
5555
const Header = ({ isCreating, status, title: documentTitle = 'Untitled' }: HeaderProps) => {
5656
const { formatMessage } = useIntl();
5757
const isCloning = useMatch(CLONE_PATH) !== null;
58+
const params = useParams<{ collectionType: string; slug: string }>();
5859

5960
const title = isCreating
6061
? formatMessage({
@@ -65,7 +66,13 @@ const Header = ({ isCreating, status, title: documentTitle = 'Untitled' }: Heade
6566

6667
return (
6768
<Flex direction="column" alignItems="flex-start" paddingTop={6} paddingBottom={4} gap={2}>
68-
<BackButton />
69+
<BackButton
70+
fallback={
71+
params.collectionType === SINGLE_TYPES
72+
? undefined
73+
: `../${COLLECTION_TYPES}/${params.slug}`
74+
}
75+
/>
6976
<Flex width="100%" justifyContent="space-between" gap="80px" alignItems="flex-start">
7077
<Typography variant="alpha" tag="h1">
7178
{title}

packages/core/content-manager/admin/src/pages/ListConfiguration/components/Header.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useForm, BackButton, Layouts } from '@strapi/admin/strapi-admin';
22
import { Button } from '@strapi/design-system';
33
import { useIntl } from 'react-intl';
4+
import { useParams } from 'react-router-dom';
45

6+
import { COLLECTION_TYPES } from '../../../constants/collections';
57
import { capitalise } from '../../../utils/strings';
68
import { getTranslation } from '../../../utils/translations';
79

@@ -13,13 +15,14 @@ interface HeaderProps {
1315

1416
const Header = ({ name }: HeaderProps) => {
1517
const { formatMessage } = useIntl();
18+
const params = useParams<{ slug: string }>();
1619

1720
const modified = useForm('Header', (state) => state.modified);
1821
const isSubmitting = useForm('Header', (state) => state.isSubmitting);
1922

2023
return (
2124
<Layouts.Header
22-
navigationAction={<BackButton />}
25+
navigationAction={<BackButton fallback={`../${COLLECTION_TYPES}/${params.slug}`} />}
2326
primaryAction={
2427
<Button size="S" disabled={!modified} type="submit" loading={isSubmitting}>
2528
{formatMessage({ id: 'global.save', defaultMessage: 'Save' })}

packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ const ReleaseDetailsLayout = ({
271271
<Badge {...getBadgeProps(release.status)}>{release.status}</Badge>
272272
</Flex>
273273
}
274-
navigationAction={<BackButton />}
274+
navigationAction={<BackButton fallback=".." />}
275275
primaryAction={
276276
!release.releasedAt && (
277277
<Flex gap={2}>

packages/core/review-workflows/admin/src/routes/settings/id.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ const EditPage = () => {
347347
{({ modified, isSubmitting, values, setErrors }) => (
348348
<>
349349
<Layout.Header
350-
navigationAction={<BackButton />}
350+
navigationAction={<BackButton fallback=".." />}
351351
primaryAction={
352352
canUpdate || canCreate ? (
353353
<Button

0 commit comments

Comments
 (0)