mirror of
https://github.com/strapi/strapi.git
synced 2025-07-24 17:40:18 +00:00
enhancement: create dedicated preview page (#21965)
* enh: add preview page and navbar * chore: remove header * chore: update e2e tests * chore: remove getDocumentStatus export * fix: error state
This commit is contained in:
parent
c5ae9675f5
commit
6d1431fe2a
@ -226,5 +226,5 @@ const BackButton = React.forwardRef<HTMLAnchorElement, BackButtonProps>(({ disab
|
||||
);
|
||||
});
|
||||
|
||||
export { BackButton, HistoryProvider };
|
||||
export { BackButton, HistoryProvider, useHistory };
|
||||
export type { BackButtonProps, HistoryProviderProps, HistoryContextValue, HistoryState };
|
||||
|
@ -37,6 +37,7 @@ export {
|
||||
} from './features/Notifications';
|
||||
export { useAppInfo, type AppInfoContextValue } from './features/AppInfo';
|
||||
export { type Permission, useAuth, type AuthContextValue } from './features/Auth';
|
||||
export { useHistory } from './features/BackButton';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
|
@ -14,7 +14,6 @@ import { stringify } from 'qs';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink, useNavigate, useParams, type To } from 'react-router-dom';
|
||||
|
||||
import { COLLECTION_TYPES } from '../../constants/collections';
|
||||
import { PERMISSIONS } from '../../constants/plugin';
|
||||
import { useHistoryContext } from '../pages/History';
|
||||
import { useRestoreVersionMutation } from '../services/historyVersion';
|
||||
@ -48,13 +47,6 @@ export const VersionHeader = ({ headerId }: VersionHeaderProps) => {
|
||||
const getNextNavigation = (): To => {
|
||||
const pluginsQueryParams = stringify({ plugins: query.plugins }, { encode: false });
|
||||
|
||||
if (collectionType === COLLECTION_TYPES) {
|
||||
return {
|
||||
pathname: '..',
|
||||
search: pluginsQueryParams,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pathname: '..',
|
||||
search: pluginsQueryParams,
|
||||
|
@ -1,12 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
useQueryParams,
|
||||
Page,
|
||||
createContext,
|
||||
useRBAC,
|
||||
BackButton,
|
||||
} from '@strapi/admin/strapi-admin';
|
||||
import { useQueryParams, Page, createContext, useRBAC } from '@strapi/admin/strapi-admin';
|
||||
import { Box, Flex, FocusTrap, Main, Portal, Link } from '@strapi/design-system';
|
||||
import { stringify } from 'qs';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
@ -1,9 +1,9 @@
|
||||
/* eslint-disable check-file/filename-naming-convention */
|
||||
import { lazy } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { type PathRouteProps } from 'react-router-dom';
|
||||
|
||||
const ProtectedHistoryPage = lazy(() =>
|
||||
const ProtectedHistoryPage = React.lazy(() =>
|
||||
import('./pages/History').then((mod) => ({ default: mod.ProtectedHistoryPage }))
|
||||
);
|
||||
|
||||
|
@ -149,7 +149,7 @@ const EditViewPage = () => {
|
||||
|
||||
return (
|
||||
<Main paddingLeft={10} paddingRight={10}>
|
||||
<Page.Title>{`${documentTitle}`}</Page.Title>
|
||||
<Page.Title>{documentTitle}</Page.Title>
|
||||
<Form
|
||||
disabled={hasDraftAndPublished && status === 'published'}
|
||||
initialValues={initialValues}
|
||||
@ -228,7 +228,7 @@ const StatusTab = styled(Tabs.Trigger)`
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @description Returns the status of the document where it's latest state takes priority,
|
||||
* @description Returns the status of the document where its latest state takes priority,
|
||||
* this typically will be "published" unless a user has edited their draft in which we should
|
||||
* display "modified".
|
||||
*/
|
||||
|
@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useClipboard, useNotification, useTracking } from '@strapi/admin/strapi-admin';
|
||||
import { Button, Flex, IconButton } from '@strapi/design-system';
|
||||
import { Link as LinkIcon } from '@strapi/icons';
|
||||
import { useQueryParams, useTracking } from '@strapi/admin/strapi-admin';
|
||||
import { Button, Flex } from '@strapi/design-system';
|
||||
import { UID } from '@strapi/types';
|
||||
import { stringify } from 'qs';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@ -13,9 +13,14 @@ import type { PanelComponent } from '@strapi/content-manager/strapi-admin';
|
||||
|
||||
const PreviewSidePanel: PanelComponent = ({ model, documentId, document }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { toggleNotification } = useNotification();
|
||||
const { copy } = useClipboard();
|
||||
const { trackUsage } = useTracking();
|
||||
const [{ query }] = useQueryParams();
|
||||
|
||||
/**
|
||||
* The preview URL isn't used in this component, we just fetch it to know if preview is enabled
|
||||
* for the content type. If it's not, the panel is not displayed. If it is, we display a link to
|
||||
* /preview, and the URL will already be loaded in the RTK query cache.
|
||||
*/
|
||||
const { data, error } = useGetPreviewUrlQuery({
|
||||
params: {
|
||||
contentType: model as UID.ContentType,
|
||||
@ -31,20 +36,8 @@ const PreviewSidePanel: PanelComponent = ({ model, documentId, document }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { url } = data.data;
|
||||
|
||||
const handleCopyLink = () => {
|
||||
copy(url);
|
||||
toggleNotification({
|
||||
message: formatMessage({
|
||||
id: 'content-manager.preview.copy.success',
|
||||
defaultMessage: 'Copied preview link',
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
// TODO: delete this event and use willNavigate instead
|
||||
trackUsage('willOpenPreview');
|
||||
};
|
||||
|
||||
@ -55,9 +48,8 @@ const PreviewSidePanel: PanelComponent = ({ model, documentId, document }) => {
|
||||
<Button
|
||||
variant="tertiary"
|
||||
tag={Link}
|
||||
to={url}
|
||||
to={{ pathname: 'preview', search: stringify(query, { encode: false }) }}
|
||||
onClick={handleClick}
|
||||
target="_blank"
|
||||
flex="auto"
|
||||
>
|
||||
{formatMessage({
|
||||
@ -65,16 +57,6 @@ const PreviewSidePanel: PanelComponent = ({ model, documentId, document }) => {
|
||||
defaultMessage: 'Open preview',
|
||||
})}
|
||||
</Button>
|
||||
<IconButton
|
||||
type="button"
|
||||
label={formatMessage({
|
||||
id: 'preview.copy.label',
|
||||
defaultMessage: 'Copy preview link',
|
||||
})}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
@ -0,0 +1,200 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Page, useQueryParams, useRBAC, createContext } from '@strapi/admin/strapi-admin';
|
||||
import { Box, FocusTrap, Portal, Typography } from '@strapi/design-system';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { GetPreviewUrl } from '../../../../shared/contracts/preview';
|
||||
import { COLLECTION_TYPES } from '../../constants/collections';
|
||||
import { DocumentRBAC } from '../../features/DocumentRBAC';
|
||||
import { type UseDocument, useDocument } from '../../hooks/useDocument';
|
||||
import { useDocumentLayout } from '../../hooks/useDocumentLayout';
|
||||
import { buildValidParams } from '../../utils/api';
|
||||
import { useGetPreviewUrlQuery } from '../services/preview';
|
||||
|
||||
import type { UID } from '@strapi/types';
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* PreviewProvider
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface PreviewContextValue {
|
||||
url: string;
|
||||
mainField: string;
|
||||
document: NonNullable<ReturnType<UseDocument>['document']>;
|
||||
meta: NonNullable<ReturnType<UseDocument>['meta']>;
|
||||
schema: NonNullable<ReturnType<UseDocument>['schema']>;
|
||||
}
|
||||
|
||||
const [PreviewProvider, usePreviewContext] = createContext<PreviewContextValue>('PreviewPage');
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* PreviewPage
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const PreviewPage = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
// Read all the necessary data from the URL to find the right preview URL
|
||||
const {
|
||||
slug: model,
|
||||
id: documentId,
|
||||
collectionType,
|
||||
} = useParams<{
|
||||
slug: UID.ContentType;
|
||||
id: string;
|
||||
collectionType: string;
|
||||
}>();
|
||||
const [{ query }] = useQueryParams<{
|
||||
plugins?: Record<string, unknown>;
|
||||
}>();
|
||||
const params = React.useMemo(() => buildValidParams(query), [query]);
|
||||
|
||||
if (!collectionType) {
|
||||
throw new Error('Could not find collectionType in url params');
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
throw new Error('Could not find model in url params');
|
||||
}
|
||||
|
||||
// Only collection types must have a documentId
|
||||
if (collectionType === COLLECTION_TYPES && !documentId) {
|
||||
throw new Error('Could not find documentId in url params');
|
||||
}
|
||||
|
||||
const previewUrlResponse = useGetPreviewUrlQuery({
|
||||
params: {
|
||||
contentType: model,
|
||||
},
|
||||
query: {
|
||||
documentId,
|
||||
locale: params.locale,
|
||||
status: params.status as GetPreviewUrl.Request['query']['status'],
|
||||
},
|
||||
});
|
||||
|
||||
const documentResponse = useDocument({
|
||||
model,
|
||||
collectionType,
|
||||
documentId,
|
||||
params,
|
||||
});
|
||||
|
||||
const documentLayoutResponse = useDocumentLayout(model);
|
||||
|
||||
if (
|
||||
documentResponse.isLoading ||
|
||||
previewUrlResponse.isLoading ||
|
||||
documentLayoutResponse.isLoading
|
||||
) {
|
||||
return <Page.Loading />;
|
||||
}
|
||||
|
||||
if (
|
||||
previewUrlResponse.error ||
|
||||
documentLayoutResponse.error ||
|
||||
!documentResponse.document ||
|
||||
!documentResponse.meta ||
|
||||
!documentResponse.schema
|
||||
) {
|
||||
return <Page.Error />;
|
||||
}
|
||||
|
||||
if (!previewUrlResponse.data?.data?.url) {
|
||||
return <Page.NoData />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page.Title>
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.preview.page-title',
|
||||
defaultMessage: '{contentType} preview',
|
||||
},
|
||||
{
|
||||
contentType: documentLayoutResponse.edit.settings.displayName,
|
||||
}
|
||||
)}
|
||||
</Page.Title>
|
||||
<PreviewProvider
|
||||
url={previewUrlResponse.data.data.url}
|
||||
mainField={documentLayoutResponse.edit.settings.mainField}
|
||||
document={documentResponse.document}
|
||||
meta={documentResponse.meta}
|
||||
schema={documentResponse.schema}
|
||||
>
|
||||
<Typography>Preview will go here!</Typography>
|
||||
</PreviewProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* ProtectedPreviewPage
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const ProtectedPreviewPageImpl = () => {
|
||||
const { slug: model } = useParams<{
|
||||
slug: string;
|
||||
}>();
|
||||
const {
|
||||
permissions = [],
|
||||
isLoading,
|
||||
error,
|
||||
} = useRBAC([{ action: 'plugin::content-manager.explorer.read', subject: model }]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Page.Loading />;
|
||||
}
|
||||
|
||||
if (error || !model) {
|
||||
return (
|
||||
<Box
|
||||
height="100vh"
|
||||
width="100vw"
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
zIndex={2}
|
||||
background="neutral0"
|
||||
>
|
||||
<Page.Error />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
height="100vh"
|
||||
width="100vw"
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
zIndex={2}
|
||||
background="neutral0"
|
||||
>
|
||||
<Page.Protect permissions={permissions}>
|
||||
{({ permissions }) => (
|
||||
<DocumentRBAC permissions={permissions}>
|
||||
<PreviewPage />
|
||||
</DocumentRBAC>
|
||||
)}
|
||||
</Page.Protect>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ProtectedPreviewPage = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<FocusTrap>
|
||||
<ProtectedPreviewPageImpl />
|
||||
</FocusTrap>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProtectedPreviewPage, usePreviewContext };
|
21
packages/core/content-manager/admin/src/preview/routes.tsx
Normal file
21
packages/core/content-manager/admin/src/preview/routes.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
/* eslint-disable check-file/filename-naming-convention */
|
||||
import * as React from 'react';
|
||||
|
||||
import type { PathRouteProps } from 'react-router-dom';
|
||||
|
||||
const ProtectedPreviewPage = React.lazy(() =>
|
||||
import('./pages/Preview').then((mod) => ({ default: mod.ProtectedPreviewPage }))
|
||||
);
|
||||
|
||||
const routes: PathRouteProps[] = [
|
||||
{
|
||||
path: ':collectionType/:slug/:id/preview',
|
||||
Component: ProtectedPreviewPage,
|
||||
},
|
||||
{
|
||||
path: ':collectionType/:slug/preview',
|
||||
Component: ProtectedPreviewPage,
|
||||
},
|
||||
];
|
||||
|
||||
export { routes };
|
@ -5,6 +5,7 @@ import { Navigate, PathRouteProps, useParams } from 'react-router-dom';
|
||||
|
||||
import { COLLECTION_TYPES, SINGLE_TYPES } from './constants/collections';
|
||||
import { routes as historyRoutes } from './history/routes';
|
||||
import { routes as previewRoutes } from './preview/routes';
|
||||
|
||||
const ProtectedEditViewPage = lazy(() =>
|
||||
import('./pages/EditView/EditViewPage').then((mod) => ({ default: mod.ProtectedEditViewPage }))
|
||||
@ -90,6 +91,7 @@ const routes: PathRouteProps[] = [
|
||||
Component: NoContentType,
|
||||
},
|
||||
...historyRoutes,
|
||||
...previewRoutes,
|
||||
];
|
||||
|
||||
export { routes, CLONE_PATH, LIST_PATH };
|
||||
|
@ -233,6 +233,8 @@
|
||||
"popover.display-relations.label": "Display relations",
|
||||
"preview.panel.title": "Preview",
|
||||
"preview.panel.button": "Open preview",
|
||||
"preview.page-title": "{contentType} preview",
|
||||
"preview.header.close": "Close preview",
|
||||
"preview.copy.label": "Copy preview link",
|
||||
"preview.copy.success": "Copied preview link",
|
||||
"relation.add": "Add relation",
|
||||
|
@ -16,36 +16,15 @@ describeOnCondition(edition === 'EE')('Preview', () => {
|
||||
await page.waitForURL('/admin');
|
||||
});
|
||||
|
||||
test('Preview button should appear for configured content types', async ({
|
||||
page,
|
||||
context,
|
||||
browser,
|
||||
}) => {
|
||||
test('Preview button should appear for configured content types', async ({ page, context }) => {
|
||||
// Open an edit view for a content type that has preview
|
||||
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
|
||||
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
|
||||
await clickAndWait(page, page.getByRole('gridcell', { name: /west ham post match/i }));
|
||||
|
||||
// Copy the preview link
|
||||
await page.getByRole('button', { name: /copy preview link/i }).click();
|
||||
await findAndClose(page, 'Copied preview link');
|
||||
|
||||
// Check that preview opens in a new tab
|
||||
const newTabPromiseDraft = page.waitForEvent('popup');
|
||||
await page.getByRole('link', { name: /open preview/i }).click();
|
||||
const newTab = await newTabPromiseDraft;
|
||||
expect(newTab.url()).toMatch(/^https:\/\/strapi\.io\/preview\/api::article\.article.*\/draft$/);
|
||||
|
||||
// Check that preview link reflects the publication status
|
||||
await page.getByRole('button', { name: /publish/i }).click();
|
||||
await findAndClose(page, 'Published document');
|
||||
await page.getByRole('tab', { name: /published/i }).click();
|
||||
const newTabPromisePublished = page.waitForEvent('popup');
|
||||
await page.getByRole('link', { name: /open preview/i }).click();
|
||||
const newTab2 = await newTabPromisePublished;
|
||||
expect(newTab2.url()).toMatch(
|
||||
/^https:\/\/strapi\.io\/preview\/api::article\.article.*\/published$/
|
||||
);
|
||||
// Check that preview opens in its own page
|
||||
await clickAndWait(page, page.getByRole('link', { name: /open preview/i }));
|
||||
await expect(page.getByText('Preview will go here!')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Preview button should not appear for content types without preview config', async ({
|
||||
|
Loading…
x
Reference in New Issue
Block a user