Skip to content

Commit af7e4e2

Browse files
authored
wip: static preview draft & publish tabs (strapi#22025)
* feat: preview tabs * fix: re organize imports * fix: tabs label text * fix: import * fix: preview close button (strapi#22027) * fix: preview close button * chore: remove function * fix: typescript error * fix: pr suggestions * fix: padding * fix: use paddings * fix: pr suggestions * fix: align to end * chore: e2e test * chore: remove margin bottom from tab * chore: do not use style tag * fix: remove unused align * chore: update component locator to getByRole
1 parent fed2139 commit af7e4e2

File tree

4 files changed

+128
-55
lines changed

4 files changed

+128
-55
lines changed
Lines changed: 102 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import * as React from 'react';
22

3+
import { useClipboard, useNotification, useQueryParams } from '@strapi/admin/strapi-admin';
34
import {
4-
useClipboard,
5-
useHistory,
6-
useNotification,
7-
useQueryParams,
8-
} from '@strapi/admin/strapi-admin';
9-
import { Flex, IconButton, Typography } from '@strapi/design-system';
5+
Box,
6+
type BoxProps,
7+
Flex,
8+
IconButton,
9+
Tabs,
10+
Typography,
11+
Grid,
12+
} from '@strapi/design-system';
1013
import { Cross, Link as LinkIcon } from '@strapi/icons';
1114
import { stringify } from 'qs';
12-
import { useIntl } from 'react-intl';
13-
import { Link, type To, useNavigate } from 'react-router-dom';
15+
import { type MessageDescriptor, useIntl } from 'react-intl';
16+
import { Link } from 'react-router-dom';
17+
import { styled } from 'styled-components';
1418

1519
import { DocumentStatus } from '../../pages/EditView/components/DocumentStatus';
1620
import { getDocumentStatus } from '../../pages/EditView/EditViewPage';
@@ -21,39 +25,19 @@ import { usePreviewContext } from '../pages/Preview';
2125
* -----------------------------------------------------------------------------------------------*/
2226

2327
const ClosePreviewButton = () => {
24-
const [{ query }] = useQueryParams();
25-
const navigate = useNavigate();
28+
const [{ query }] = useQueryParams<{
29+
plugins?: Record<string, unknown>;
30+
}>();
2631
const { formatMessage } = useIntl();
2732

28-
const canGoBack = useHistory('BackButton', (state) => state.canGoBack);
29-
const goBack = useHistory('BackButton', (state) => state.goBack);
30-
const history = useHistory('BackButton', (state) => state.history);
31-
32-
const fallbackUrl: To = {
33-
pathname: '..',
34-
search: stringify(query, { encode: false }),
35-
};
36-
37-
const handleClick = (e: React.MouseEvent) => {
38-
/**
39-
* Prevent normal link behavior. We only make it an achor for accessibility reasons.
40-
* The point of this logic is to act as the browser's back button when possible, and to fallback
41-
* to a link behavior to the edit view when no history is available.
42-
* */
43-
e.preventDefault();
44-
45-
if (canGoBack) {
46-
goBack();
47-
} else {
48-
navigate(fallbackUrl);
49-
}
50-
};
51-
5233
return (
5334
<IconButton
5435
tag={Link}
55-
to={history.at(-1) ?? fallbackUrl}
56-
onClick={handleClick}
36+
relative="path"
37+
to={{
38+
pathname: '..',
39+
search: stringify({ plugins: query.plugins }, { encode: false }),
40+
}}
5741
label={formatMessage({
5842
id: 'content-manager.preview.header.close',
5943
defaultMessage: 'Close preview',
@@ -84,6 +68,56 @@ const Status = () => {
8468
return <DocumentStatus status={status} size="XS" />;
8569
};
8670

71+
const PreviewTabs = () => {
72+
const { formatMessage } = useIntl();
73+
74+
// URL query params
75+
const [{ query }, setQuery] = useQueryParams<{ status: 'draft' | 'published' }>();
76+
77+
// Get status
78+
const document = usePreviewContext('PreviewHeader', (state) => state.document);
79+
const schema = usePreviewContext('PreviewHeader', (state) => state.schema);
80+
const meta = usePreviewContext('PreviewHeader', (state) => state.meta);
81+
const hasDraftAndPublish = schema?.options?.draftAndPublish ?? false;
82+
const documentStatus = getDocumentStatus(document, meta);
83+
84+
const handleTabChange = (status: string) => {
85+
if (status === 'published' || status === 'draft') {
86+
setQuery({ status }, 'push', true);
87+
}
88+
};
89+
90+
if (!hasDraftAndPublish) {
91+
return null;
92+
}
93+
94+
return (
95+
<>
96+
<Tabs.Root variant="simple" value={query.status || 'draft'} onValueChange={handleTabChange}>
97+
<Tabs.List
98+
aria-label={formatMessage({
99+
id: 'preview.tabs.label',
100+
defaultMessage: 'Document status',
101+
})}
102+
>
103+
<StatusTab value="draft">
104+
{formatMessage({
105+
id: 'content-manager.containers.List.draft',
106+
defaultMessage: 'draft',
107+
})}
108+
</StatusTab>
109+
<StatusTab value="published" disabled={documentStatus === 'draft'}>
110+
{formatMessage({
111+
id: 'content-manager.containers.List.published',
112+
defaultMessage: 'published',
113+
})}
114+
</StatusTab>
115+
</Tabs.List>
116+
</Tabs.Root>
117+
</>
118+
);
119+
};
120+
87121
/* -------------------------------------------------------------------------------------------------
88122
* PreviewHeader
89123
* -----------------------------------------------------------------------------------------------*/
@@ -110,32 +144,46 @@ const PreviewHeader = () => {
110144
};
111145

112146
return (
113-
<Flex
114-
justifyContent="space-between"
147+
<Grid.Root
148+
gap={3}
149+
gridCols={3}
150+
paddingLeft={2}
151+
paddingRight={2}
115152
background="neutral0"
116-
padding={2}
117153
borderColor="neutral150"
118154
tag="header"
119155
>
120-
<Flex gap={3}>
156+
{/* Title and status */}
157+
<Grid.Item xs={1} paddingTop={2} paddingBottom={2} gap={3}>
121158
<ClosePreviewButton />
122-
<Typography tag="h1" fontWeight={600} fontSize={2}>
159+
<Typography tag="h1" fontWeight={600} fontSize={2} maxWidth="200px">
123160
{title}
124161
</Typography>
125-
<Status />
126-
</Flex>
127-
<IconButton
128-
type="button"
129-
label={formatMessage({
130-
id: 'preview.copy.label',
131-
defaultMessage: 'Copy preview link',
132-
})}
133-
onClick={handleCopyLink}
134-
>
135-
<LinkIcon />
136-
</IconButton>
137-
</Flex>
162+
<DocumentStatus />
163+
</Grid.Item>
164+
{/* Tabs */}
165+
<Grid.Item xs={1} marginBottom="-1px" alignItems="end" margin="auto">
166+
<PreviewTabs />
167+
</Grid.Item>
168+
{/* Copy link */}
169+
<Grid.Item xs={1} justifyContent="end" paddingTop={2} paddingBottom={2}>
170+
<IconButton
171+
type="button"
172+
label={formatMessage({
173+
id: 'preview.copy.label',
174+
defaultMessage: 'Copy preview link',
175+
})}
176+
onClick={handleCopyLink}
177+
>
178+
<LinkIcon />
179+
</IconButton>
180+
</Grid.Item>
181+
</Grid.Root>
138182
);
139183
};
140184

185+
const StatusTab = styled(Tabs.Trigger)`
186+
text-transform: uppercase;
187+
`;
188+
141189
export { PreviewHeader };

packages/core/content-manager/admin/src/preview/pages/Preview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const PreviewPage = () => {
5151
const [{ query }] = useQueryParams<{
5252
plugins?: Record<string, unknown>;
5353
}>();
54+
5455
const params = React.useMemo(() => buildValidParams(query), [query]);
5556

5657
if (!collectionType) {

packages/core/content-manager/admin/src/translations/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@
240240
"preview.header.close": "Close preview",
241241
"preview.copy.label": "Copy preview link",
242242
"preview.copy.success": "Copied preview link",
243+
"preview.tabs.label": "Preview status",
243244
"relation.add": "Add relation",
244245
"relation.disconnect": "Remove",
245246
"relation.error-adding-relation": "An error occurred while trying to add the relation.",

tests/e2e/tests/content-manager/preview.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ describeOnCondition(edition === 'EE')('Preview', () => {
2424

2525
// Check that preview opens in its own page
2626
await clickAndWait(page, page.getByRole('link', { name: /open preview/i }));
27-
await expect(page.getByText(/^Draft$/)).toBeVisible();
27+
// Draft status is visible (second Draft text is the draft tab)
28+
await expect(page.getByText(/^Draft$/).nth(0)).toBeVisible();
2829
await expect(page.getByRole('heading', { name: /west ham post match/i })).toBeVisible();
2930

3031
// Copies the link of the page
@@ -46,4 +47,26 @@ describeOnCondition(edition === 'EE')('Preview', () => {
4647

4748
await expect(page.getByRole('link', { name: /open preview/i })).not.toBeVisible();
4849
});
50+
51+
test('Tabs for Draft and Publish should be visible for content type with D&P enabled', async ({
52+
page,
53+
}) => {
54+
// Navigate to the Content Manager and open the edit view of a content type with D&P enabled
55+
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
56+
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
57+
await clickAndWait(page, page.getByRole('gridcell', { name: /west ham post match/i }));
58+
59+
// Check that preview opens in its own page
60+
await clickAndWait(page, page.getByRole('link', { name: /open preview/i }));
61+
// Draft status is visible (second Draft text is the draft tab)
62+
await expect(page.getByText(/^Draft$/).nth(0)).toBeVisible();
63+
await expect(page.getByRole('heading', { name: /west ham post match/i })).toBeVisible();
64+
65+
// Verify that Draft and Publish tabs are visible
66+
await expect(page.getByRole('tab', { name: /^Draft$/ })).toBeVisible();
67+
await expect(page.getByRole('tab', { name: /^Published$/ })).toBeVisible();
68+
69+
// Expect the preview tab to be disabled (since the document is in draft status)
70+
await expect(page.getByText(/^Published$/)).toBeDisabled();
71+
});
4972
});

0 commit comments

Comments
 (0)