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';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -8,13 +8,27 @@ import { Grid, Flex, Typography, JSONInput } from '@strapi/design-system';
|
|||||||
|
|
||||||
const PreviewComponent = () => {
|
const PreviewComponent = () => {
|
||||||
const { uid: model, documentId, locale, status, collectionType } = useParams();
|
const { uid: model, documentId, locale, status, collectionType } = useParams();
|
||||||
const { document } = useDocument({
|
const { document, refetch } = useDocument({
|
||||||
model,
|
model,
|
||||||
documentId,
|
documentId,
|
||||||
params: { locale, status },
|
params: { locale, status },
|
||||||
collectionType,
|
collectionType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleStrapiUpdate = (event) => {
|
||||||
|
if (event.data?.type === 'strapiUpdate') {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleStrapiUpdate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleStrapiUpdate);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layouts.Root>
|
<Layouts.Root>
|
||||||
<Page.Main>
|
<Page.Main>
|
||||||
|
@ -80,7 +80,9 @@ interface PanelComponent extends DescriptionComponent<PanelComponentProps, Panel
|
|||||||
type?: 'actions' | 'releases';
|
type?: 'actions' | 'releases';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DocumentActionProps extends EditViewContext {}
|
interface DocumentActionProps extends EditViewContext {
|
||||||
|
onPreview?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface DocumentActionComponent
|
interface DocumentActionComponent
|
||||||
extends DescriptionComponent<DocumentActionProps, DocumentActionDescription> {
|
extends DescriptionComponent<DocumentActionProps, DocumentActionDescription> {
|
||||||
|
@ -63,6 +63,7 @@ type UseDocument = (
|
|||||||
schema?: Schema;
|
schema?: Schema;
|
||||||
schemas?: Schema[];
|
schemas?: Schema[];
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
|
refetch: () => void;
|
||||||
validate: (document: Document) => null | FormErrors;
|
validate: (document: Document) => null | FormErrors;
|
||||||
/**
|
/**
|
||||||
* Get the document's title
|
* Get the document's title
|
||||||
@ -115,6 +116,7 @@ const useDocument: UseDocument = (args, opts) => {
|
|||||||
isLoading: isLoadingDocument,
|
isLoading: isLoadingDocument,
|
||||||
isFetching: isFetchingDocument,
|
isFetching: isFetchingDocument,
|
||||||
error,
|
error,
|
||||||
|
refetch,
|
||||||
} = useGetDocumentQuery(args, {
|
} = useGetDocumentQuery(args, {
|
||||||
...opts,
|
...opts,
|
||||||
skip: (!args.documentId && args.collectionType !== SINGLE_TYPES) || opts?.skip,
|
skip: (!args.documentId && args.collectionType !== SINGLE_TYPES) || opts?.skip,
|
||||||
@ -227,6 +229,7 @@ const useDocument: UseDocument = (args, opts) => {
|
|||||||
validate,
|
validate,
|
||||||
getTitle,
|
getTitle,
|
||||||
getInitialFormValues,
|
getInitialFormValues,
|
||||||
|
refetch,
|
||||||
} satisfies ReturnType<UseDocument>;
|
} satisfies ReturnType<UseDocument>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -516,6 +516,7 @@ const PublishAction: DocumentActionComponent = ({
|
|||||||
collectionType,
|
collectionType,
|
||||||
meta,
|
meta,
|
||||||
document,
|
document,
|
||||||
|
onPreview,
|
||||||
}) => {
|
}) => {
|
||||||
const { schema } = useDoc();
|
const { schema } = useDoc();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -678,6 +679,10 @@ const PublishAction: DocumentActionComponent = ({
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
|
if (onPreview) {
|
||||||
|
onPreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -755,6 +760,7 @@ const UpdateAction: DocumentActionComponent = ({
|
|||||||
documentId,
|
documentId,
|
||||||
model,
|
model,
|
||||||
collectionType,
|
collectionType,
|
||||||
|
onPreview,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toggleNotification } = useNotification();
|
const { toggleNotification } = useNotification();
|
||||||
@ -867,6 +873,9 @@ const UpdateAction: DocumentActionComponent = ({
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
if (onPreview) {
|
||||||
|
onPreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
clone,
|
clone,
|
||||||
|
@ -1,89 +1,8 @@
|
|||||||
import * as React from 'react';
|
import { Box } from '@strapi/design-system';
|
||||||
|
|
||||||
import { Box, Flex, IconButton } from '@strapi/design-system';
|
|
||||||
import { ArrowLeft } from '@strapi/icons';
|
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { styled } from 'styled-components';
|
|
||||||
|
|
||||||
import { FormLayout } from '../../pages/EditView/components/FormLayout';
|
|
||||||
import { usePreviewContext } from '../pages/Preview';
|
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 PreviewContent = () => {
|
||||||
const previewUrl = usePreviewContext('PreviewContent', (state) => state.url);
|
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 schema = usePreviewContext('PreviewHeader', (state) => state.schema);
|
||||||
const meta = usePreviewContext('PreviewHeader', (state) => state.meta);
|
const meta = usePreviewContext('PreviewHeader', (state) => state.meta);
|
||||||
const plugins = useStrapiApp('PreviewHeader', (state) => state.plugins);
|
const plugins = useStrapiApp('PreviewHeader', (state) => state.plugins);
|
||||||
|
const iframeRef = usePreviewContext('PreviewHeader', (state) => state.iframeRef);
|
||||||
|
|
||||||
const [{ query }] = useQueryParams<{
|
const [{ query }] = useQueryParams<{
|
||||||
status?: 'draft' | 'published';
|
status?: 'draft' | 'published';
|
||||||
@ -176,13 +177,16 @@ const UnstablePreviewHeader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasDraftAndPublish = schema.options?.draftAndPublish ?? false;
|
const hasDraftAndPublish = schema.options?.draftAndPublish ?? false;
|
||||||
const props = {
|
const documentActionProps = {
|
||||||
activeTab: query.status ?? null,
|
activeTab: query.status ?? null,
|
||||||
collectionType: schema.kind === 'collectionType' ? 'collection-types' : 'single-types',
|
collectionType: schema.kind === 'collectionType' ? 'collection-types' : 'single-types',
|
||||||
model: schema.uid,
|
model: schema.uid,
|
||||||
documentId: document.documentId,
|
documentId: document.documentId,
|
||||||
document,
|
document,
|
||||||
meta,
|
meta,
|
||||||
|
onPreview: () => {
|
||||||
|
iframeRef?.current?.contentWindow?.postMessage({ type: 'strapiUpdate' });
|
||||||
|
},
|
||||||
} satisfies DocumentActionProps;
|
} satisfies DocumentActionProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -227,7 +231,7 @@ const UnstablePreviewHeader = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<InjectionZone area="preview.actions" />
|
<InjectionZone area="preview.actions" />
|
||||||
<DescriptionComponentRenderer
|
<DescriptionComponentRenderer
|
||||||
props={props}
|
props={documentActionProps}
|
||||||
descriptions={(
|
descriptions={(
|
||||||
plugins['content-manager'].apis as ContentManagerPlugin['config']['apis']
|
plugins['content-manager'].apis as ContentManagerPlugin['config']['apis']
|
||||||
).getDocumentActions('preview')}
|
).getDocumentActions('preview')}
|
||||||
|
@ -7,18 +7,21 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
Form as FormContext,
|
Form as FormContext,
|
||||||
} from '@strapi/admin/strapi-admin';
|
} 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 { useIntl } from 'react-intl';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
import { styled } from 'styled-components';
|
||||||
|
|
||||||
import { GetPreviewUrl } from '../../../../shared/contracts/preview';
|
import { GetPreviewUrl } from '../../../../shared/contracts/preview';
|
||||||
import { COLLECTION_TYPES } from '../../constants/collections';
|
import { COLLECTION_TYPES } from '../../constants/collections';
|
||||||
import { DocumentRBAC } from '../../features/DocumentRBAC';
|
import { DocumentRBAC } from '../../features/DocumentRBAC';
|
||||||
import { type UseDocument, useDocument } from '../../hooks/useDocument';
|
import { type UseDocument, useDocument } from '../../hooks/useDocument';
|
||||||
import { type EditLayout, useDocumentLayout } from '../../hooks/useDocumentLayout';
|
import { type EditLayout, useDocumentLayout } from '../../hooks/useDocumentLayout';
|
||||||
|
import { FormLayout } from '../../pages/EditView/components/FormLayout';
|
||||||
import { buildValidParams } from '../../utils/api';
|
import { buildValidParams } from '../../utils/api';
|
||||||
import { createYupSchema } from '../../utils/validation';
|
import { createYupSchema } from '../../utils/validation';
|
||||||
import { PreviewContent, UnstablePreviewContent } from '../components/PreviewContent';
|
import { PreviewContent } from '../components/PreviewContent';
|
||||||
import { PreviewHeader, UnstablePreviewHeader } from '../components/PreviewHeader';
|
import { PreviewHeader, UnstablePreviewHeader } from '../components/PreviewHeader';
|
||||||
import { useGetPreviewUrlQuery } from '../services/preview';
|
import { useGetPreviewUrlQuery } from '../services/preview';
|
||||||
|
|
||||||
@ -35,6 +38,7 @@ interface PreviewContextValue {
|
|||||||
meta: NonNullable<ReturnType<UseDocument>['meta']>;
|
meta: NonNullable<ReturnType<UseDocument>['meta']>;
|
||||||
schema: NonNullable<ReturnType<UseDocument>['schema']>;
|
schema: NonNullable<ReturnType<UseDocument>['schema']>;
|
||||||
layout: EditLayout;
|
layout: EditLayout;
|
||||||
|
iframeRef?: React.RefObject<HTMLIFrameElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [PreviewProvider, usePreviewContext] = createContext<PreviewContextValue>('PreviewPage');
|
const [PreviewProvider, usePreviewContext] = createContext<PreviewContextValue>('PreviewPage');
|
||||||
@ -43,10 +47,19 @@ const [PreviewProvider, usePreviewContext] = createContext<PreviewContextValue>(
|
|||||||
* PreviewPage
|
* 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 PreviewPage = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { formatMessage } = useIntl();
|
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
|
// Read all the necessary data from the URL to find the right preview URL
|
||||||
const {
|
const {
|
||||||
slug: model,
|
slug: model,
|
||||||
@ -133,6 +146,8 @@ const PreviewPage = () => {
|
|||||||
return yupSchema.validateSync(values, { abortEarly: false });
|
return yupSchema.validateSync(values, { abortEarly: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const previewUrl = previewUrlResponse.data.data.url;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Page.Title>
|
<Page.Title>
|
||||||
@ -147,12 +162,13 @@ const PreviewPage = () => {
|
|||||||
)}
|
)}
|
||||||
</Page.Title>
|
</Page.Title>
|
||||||
<PreviewProvider
|
<PreviewProvider
|
||||||
url={previewUrlResponse.data.data.url}
|
url={previewUrl}
|
||||||
document={documentResponse.document}
|
document={documentResponse.document}
|
||||||
title={documentTitle}
|
title={documentTitle}
|
||||||
meta={documentResponse.meta}
|
meta={documentResponse.meta}
|
||||||
schema={documentResponse.schema}
|
schema={documentResponse.schema}
|
||||||
layout={documentLayoutResponse.edit}
|
layout={documentLayoutResponse.edit}
|
||||||
|
iframeRef={iframeRef}
|
||||||
>
|
>
|
||||||
<FormContext
|
<FormContext
|
||||||
method="PUT"
|
method="PUT"
|
||||||
@ -181,7 +197,65 @@ const PreviewPage = () => {
|
|||||||
{window.strapi.future.isEnabled('unstablePreviewSideEditor') ? (
|
{window.strapi.future.isEnabled('unstablePreviewSideEditor') ? (
|
||||||
<>
|
<>
|
||||||
<UnstablePreviewHeader />
|
<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