mirror of
https://github.com/strapi/strapi.git
synced 2025-06-27 00:41:25 +00:00
feat(preview): refresh iframe when strapi updates (#23024)
This commit is contained in:
parent
99da359006
commit
85d3789768
@ -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>
|
||||
|
@ -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> {
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 };
|
||||
|
@ -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')}
|
||||
|
@ -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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
Loading…
x
Reference in New Issue
Block a user