diff --git a/packages/core/admin/admin/src/features/BackButton.tsx b/packages/core/admin/admin/src/features/BackButton.tsx index 7227f3db89..16ce339770 100644 --- a/packages/core/admin/admin/src/features/BackButton.tsx +++ b/packages/core/admin/admin/src/features/BackButton.tsx @@ -226,5 +226,5 @@ const BackButton = React.forwardRef(({ disab ); }); -export { BackButton, HistoryProvider }; +export { BackButton, HistoryProvider, useHistory }; export type { BackButtonProps, HistoryProviderProps, HistoryContextValue, HistoryState }; diff --git a/packages/core/admin/admin/src/index.ts b/packages/core/admin/admin/src/index.ts index 118d29bc70..def361ac8c 100644 --- a/packages/core/admin/admin/src/index.ts +++ b/packages/core/admin/admin/src/index.ts @@ -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 diff --git a/packages/core/content-manager/admin/src/history/components/VersionHeader.tsx b/packages/core/content-manager/admin/src/history/components/VersionHeader.tsx index 842f986ad4..1f1f92d2cc 100644 --- a/packages/core/content-manager/admin/src/history/components/VersionHeader.tsx +++ b/packages/core/content-manager/admin/src/history/components/VersionHeader.tsx @@ -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, diff --git a/packages/core/content-manager/admin/src/history/pages/History.tsx b/packages/core/content-manager/admin/src/history/pages/History.tsx index 2c50adfcab..b3b743580e 100644 --- a/packages/core/content-manager/admin/src/history/pages/History.tsx +++ b/packages/core/content-manager/admin/src/history/pages/History.tsx @@ -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'; diff --git a/packages/core/content-manager/admin/src/history/routes.tsx b/packages/core/content-manager/admin/src/history/routes.tsx index 705ff3f835..2e342618d9 100644 --- a/packages/core/content-manager/admin/src/history/routes.tsx +++ b/packages/core/content-manager/admin/src/history/routes.tsx @@ -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 })) ); diff --git a/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx b/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx index f93e7f279c..637c14a717 100644 --- a/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx +++ b/packages/core/content-manager/admin/src/pages/EditView/EditViewPage.tsx @@ -149,7 +149,7 @@ const EditViewPage = () => { return (
- {`${documentTitle}`} + {documentTitle}
{ 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 }) => { - - - ), }; diff --git a/packages/core/content-manager/admin/src/preview/pages/Preview.tsx b/packages/core/content-manager/admin/src/preview/pages/Preview.tsx new file mode 100644 index 0000000000..4cad3bb361 --- /dev/null +++ b/packages/core/content-manager/admin/src/preview/pages/Preview.tsx @@ -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['document']>; + meta: NonNullable['meta']>; + schema: NonNullable['schema']>; +} + +const [PreviewProvider, usePreviewContext] = createContext('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; + }>(); + 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 ; + } + + if ( + previewUrlResponse.error || + documentLayoutResponse.error || + !documentResponse.document || + !documentResponse.meta || + !documentResponse.schema + ) { + return ; + } + + if (!previewUrlResponse.data?.data?.url) { + return ; + } + + return ( + <> + + {formatMessage( + { + id: 'content-manager.preview.page-title', + defaultMessage: '{contentType} preview', + }, + { + contentType: documentLayoutResponse.edit.settings.displayName, + } + )} + + + Preview will go here! + + + ); +}; + +/* ------------------------------------------------------------------------------------------------- + * 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 ; + } + + if (error || !model) { + return ( + + + + ); + } + + return ( + + + {({ permissions }) => ( + + + + )} + + + ); +}; + +const ProtectedPreviewPage = () => { + return ( + + + + + + ); +}; + +export { ProtectedPreviewPage, usePreviewContext }; diff --git a/packages/core/content-manager/admin/src/preview/routes.tsx b/packages/core/content-manager/admin/src/preview/routes.tsx new file mode 100644 index 0000000000..08015a14e8 --- /dev/null +++ b/packages/core/content-manager/admin/src/preview/routes.tsx @@ -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 }; diff --git a/packages/core/content-manager/admin/src/router.tsx b/packages/core/content-manager/admin/src/router.tsx index 0a2c83f8c8..9f3edf08bf 100644 --- a/packages/core/content-manager/admin/src/router.tsx +++ b/packages/core/content-manager/admin/src/router.tsx @@ -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 }; diff --git a/packages/core/content-manager/admin/src/translations/en.json b/packages/core/content-manager/admin/src/translations/en.json index b3e3134049..29b0bc83b7 100644 --- a/packages/core/content-manager/admin/src/translations/en.json +++ b/packages/core/content-manager/admin/src/translations/en.json @@ -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", diff --git a/tests/e2e/tests/content-manager/preview.spec.ts b/tests/e2e/tests/content-manager/preview.spec.ts index c044ce5a4c..44534872ef 100644 --- a/tests/e2e/tests/content-manager/preview.spec.ts +++ b/tests/e2e/tests/content-manager/preview.spec.ts @@ -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 ({