feat(preview): refresh iframe when strapi updates (#23024)

This commit is contained in:
markkaylor 2025-03-03 16:54:22 +01:00 committed by GitHub
parent 99da359006
commit 85d3789768
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 117 additions and 92 deletions

View File

@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import { useParams } from 'react-router-dom';
// @ts-ignore
@ -8,13 +8,27 @@ import { Grid, Flex, Typography, JSONInput } from '@strapi/design-system';
const PreviewComponent = () => {
const { uid: model, documentId, locale, status, collectionType } = useParams();
const { document } = useDocument({
const { document, refetch } = useDocument({
model,
documentId,
params: { locale, status },
collectionType,
});
React.useEffect(() => {
const handleStrapiUpdate = (event) => {
if (event.data?.type === 'strapiUpdate') {
refetch();
}
};
window.addEventListener('message', handleStrapiUpdate);
return () => {
window.removeEventListener('message', handleStrapiUpdate);
};
}, []);
return (
<Layouts.Root>
<Page.Main>

View File

@ -80,7 +80,9 @@ interface PanelComponent extends DescriptionComponent<PanelComponentProps, Panel
type?: 'actions' | 'releases';
}
interface DocumentActionProps extends EditViewContext {}
interface DocumentActionProps extends EditViewContext {
onPreview?: () => void;
}
interface DocumentActionComponent
extends DescriptionComponent<DocumentActionProps, DocumentActionDescription> {

View File

@ -63,6 +63,7 @@ type UseDocument = (
schema?: Schema;
schemas?: Schema[];
hasError?: boolean;
refetch: () => void;
validate: (document: Document) => null | FormErrors;
/**
* Get the document's title
@ -115,6 +116,7 @@ const useDocument: UseDocument = (args, opts) => {
isLoading: isLoadingDocument,
isFetching: isFetchingDocument,
error,
refetch,
} = useGetDocumentQuery(args, {
...opts,
skip: (!args.documentId && args.collectionType !== SINGLE_TYPES) || opts?.skip,
@ -227,6 +229,7 @@ const useDocument: UseDocument = (args, opts) => {
validate,
getTitle,
getInitialFormValues,
refetch,
} satisfies ReturnType<UseDocument>;
};

View File

@ -516,6 +516,7 @@ const PublishAction: DocumentActionComponent = ({
collectionType,
meta,
document,
onPreview,
}) => {
const { schema } = useDoc();
const navigate = useNavigate();
@ -678,6 +679,10 @@ const PublishAction: DocumentActionComponent = ({
}
} finally {
setSubmitting(false);
if (onPreview) {
onPreview();
}
}
};
@ -755,6 +760,7 @@ const UpdateAction: DocumentActionComponent = ({
documentId,
model,
collectionType,
onPreview,
}) => {
const navigate = useNavigate();
const { toggleNotification } = useNotification();
@ -867,6 +873,9 @@ const UpdateAction: DocumentActionComponent = ({
}
} finally {
setSubmitting(false);
if (onPreview) {
onPreview();
}
}
}, [
clone,

View File

@ -1,89 +1,8 @@
import * as React from 'react';
import { Box, Flex, IconButton } from '@strapi/design-system';
import { ArrowLeft } from '@strapi/icons';
import { Box } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { styled } from 'styled-components';
import { FormLayout } from '../../pages/EditView/components/FormLayout';
import { usePreviewContext } from '../pages/Preview';
// TODO use ArrowLineLeft once it's available in the DS
const AnimatedArrow = styled(ArrowLeft)<{ isSideEditorOpen: boolean }>`
will-change: transform;
rotate: ${(props) => (props.isSideEditorOpen ? '0deg' : '180deg')};
transition: rotate 0.2s ease-in-out;
`;
const UnstablePreviewContent = () => {
const previewUrl = usePreviewContext('PreviewContent', (state) => state.url);
const layout = usePreviewContext('PreviewContent', (state) => state.layout);
const { formatMessage } = useIntl();
const [isSideEditorOpen, setIsSideEditorOpen] = React.useState(true);
return (
<Flex flex={1} overflow="auto" alignItems="stretch">
<Box
overflow="auto"
width={isSideEditorOpen ? '50%' : 0}
borderWidth="0 1px 0 0"
borderColor="neutral150"
paddingTop={6}
paddingBottom={6}
// Remove horizontal padding when the editor is closed or it won't fully disappear
paddingLeft={isSideEditorOpen ? 6 : 0}
paddingRight={isSideEditorOpen ? 6 : 0}
transition="all 0.2s ease-in-out"
>
<FormLayout layout={layout.layout} hasBackground />
</Box>
<Box position="relative" flex={1} height="100%" overflow="hidden">
<Box
src={previewUrl}
/**
* For some reason, changing an iframe's src tag causes the browser to add a new item in the
* history stack. This is an issue for us as it means clicking the back button will not let us
* go back to the edit view. To fix it, we need to trick the browser into thinking this is a
* different iframe when the preview URL changes. So we set a key prop to force React
* to mount a different node when the src changes.
*/
key={previewUrl}
title={formatMessage({
id: 'content-manager.preview.panel.title',
defaultMessage: 'Preview',
})}
width="100%"
height="100%"
borderWidth={0}
tag="iframe"
/>
<IconButton
variant="tertiary"
label={formatMessage(
isSideEditorOpen
? {
id: 'content-manager.preview.content.close-editor',
defaultMessage: 'Close editor',
}
: {
id: 'content-manager.preview.content.open-editor',
defaultMessage: 'Open editor',
}
)}
onClick={() => setIsSideEditorOpen((prev) => !prev)}
position="absolute"
top={2}
left={2}
>
<AnimatedArrow isSideEditorOpen={isSideEditorOpen} />
</IconButton>
</Box>
</Flex>
);
};
const PreviewContent = () => {
const previewUrl = usePreviewContext('PreviewContent', (state) => state.url);
@ -112,4 +31,4 @@ const PreviewContent = () => {
);
};
export { PreviewContent, UnstablePreviewContent };
export { PreviewContent };

View File

@ -156,6 +156,7 @@ const UnstablePreviewHeader = () => {
const schema = usePreviewContext('PreviewHeader', (state) => state.schema);
const meta = usePreviewContext('PreviewHeader', (state) => state.meta);
const plugins = useStrapiApp('PreviewHeader', (state) => state.plugins);
const iframeRef = usePreviewContext('PreviewHeader', (state) => state.iframeRef);
const [{ query }] = useQueryParams<{
status?: 'draft' | 'published';
@ -176,13 +177,16 @@ const UnstablePreviewHeader = () => {
};
const hasDraftAndPublish = schema.options?.draftAndPublish ?? false;
const props = {
const documentActionProps = {
activeTab: query.status ?? null,
collectionType: schema.kind === 'collectionType' ? 'collection-types' : 'single-types',
model: schema.uid,
documentId: document.documentId,
document,
meta,
onPreview: () => {
iframeRef?.current?.contentWindow?.postMessage({ type: 'strapiUpdate' });
},
} satisfies DocumentActionProps;
return (
@ -227,7 +231,7 @@ const UnstablePreviewHeader = () => {
</IconButton>
<InjectionZone area="preview.actions" />
<DescriptionComponentRenderer
props={props}
props={documentActionProps}
descriptions={(
plugins['content-manager'].apis as ContentManagerPlugin['config']['apis']
).getDocumentActions('preview')}

View File

@ -7,18 +7,21 @@ import {
createContext,
Form as FormContext,
} from '@strapi/admin/strapi-admin';
import { Box, Flex, FocusTrap, Portal } from '@strapi/design-system';
import { Box, Flex, FocusTrap, IconButton, Portal } from '@strapi/design-system';
import { ArrowLeft } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { useLocation, useParams } from 'react-router-dom';
import { styled } from 'styled-components';
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 { type EditLayout, useDocumentLayout } from '../../hooks/useDocumentLayout';
import { FormLayout } from '../../pages/EditView/components/FormLayout';
import { buildValidParams } from '../../utils/api';
import { createYupSchema } from '../../utils/validation';
import { PreviewContent, UnstablePreviewContent } from '../components/PreviewContent';
import { PreviewContent } from '../components/PreviewContent';
import { PreviewHeader, UnstablePreviewHeader } from '../components/PreviewHeader';
import { useGetPreviewUrlQuery } from '../services/preview';
@ -35,6 +38,7 @@ interface PreviewContextValue {
meta: NonNullable<ReturnType<UseDocument>['meta']>;
schema: NonNullable<ReturnType<UseDocument>['schema']>;
layout: EditLayout;
iframeRef?: React.RefObject<HTMLIFrameElement>;
}
const [PreviewProvider, usePreviewContext] = createContext<PreviewContextValue>('PreviewPage');
@ -43,10 +47,19 @@ const [PreviewProvider, usePreviewContext] = createContext<PreviewContextValue>(
* PreviewPage
* -----------------------------------------------------------------------------------------------*/
const AnimatedArrow = styled(ArrowLeft)<{ isSideEditorOpen: boolean }>`
will-change: transform;
rotate: ${(props) => (props.isSideEditorOpen ? '0deg' : '180deg')};
transition: rotate 0.2s ease-in-out;
`;
const PreviewPage = () => {
const location = useLocation();
const { formatMessage } = useIntl();
const iframeRef = React.useRef<HTMLIFrameElement>(null);
const [isSideEditorOpen, setIsSideEditorOpen] = React.useState(true);
// Read all the necessary data from the URL to find the right preview URL
const {
slug: model,
@ -133,6 +146,8 @@ const PreviewPage = () => {
return yupSchema.validateSync(values, { abortEarly: false });
};
const previewUrl = previewUrlResponse.data.data.url;
return (
<>
<Page.Title>
@ -147,12 +162,13 @@ const PreviewPage = () => {
)}
</Page.Title>
<PreviewProvider
url={previewUrlResponse.data.data.url}
url={previewUrl}
document={documentResponse.document}
title={documentTitle}
meta={documentResponse.meta}
schema={documentResponse.schema}
layout={documentLayoutResponse.edit}
iframeRef={iframeRef}
>
<FormContext
method="PUT"
@ -181,7 +197,65 @@ const PreviewPage = () => {
{window.strapi.future.isEnabled('unstablePreviewSideEditor') ? (
<>
<UnstablePreviewHeader />
<UnstablePreviewContent />
<Flex flex={1} overflow="auto" alignItems="stretch">
<Box
overflow="auto"
width={isSideEditorOpen ? '50%' : 0}
borderWidth="0 1px 0 0"
borderColor="neutral150"
paddingTop={6}
paddingBottom={6}
// Remove horizontal padding when the editor is closed or it won't fully disappear
paddingLeft={isSideEditorOpen ? 6 : 0}
paddingRight={isSideEditorOpen ? 6 : 0}
transition="all 0.2s ease-in-out"
>
<FormLayout layout={documentLayoutResponse.edit.layout} hasBackground />
</Box>
<Box position="relative" flex={1} height="100%" overflow="hidden">
<Box
data-testid="preview-iframe"
ref={iframeRef}
src={previewUrl}
/**
* For some reason, changing an iframe's src tag causes the browser to add a new item in the
* history stack. This is an issue for us as it means clicking the back button will not let us
* go back to the edit view. To fix it, we need to trick the browser into thinking this is a
* different iframe when the preview URL changes. So we set a key prop to force React
* to mount a different node when the src changes.
*/
key={previewUrl}
title={formatMessage({
id: 'content-manager.preview.panel.title',
defaultMessage: 'Preview',
})}
width="100%"
height="100%"
borderWidth={0}
tag="iframe"
/>
<IconButton
variant="tertiary"
label={formatMessage(
isSideEditorOpen
? {
id: 'content-manager.preview.content.close-editor',
defaultMessage: 'Close editor',
}
: {
id: 'content-manager.preview.content.open-editor',
defaultMessage: 'Open editor',
}
)}
onClick={() => setIsSideEditorOpen((prev) => !prev)}
position="absolute"
top={2}
left={2}
>
<AnimatedArrow isSideEditorOpen={isSideEditorOpen} />
</IconButton>
</Box>
</Flex>
</>
) : (
<>