diff --git a/packages/core/content-manager/admin/src/preview/components/PreviewSidePanel.tsx b/packages/core/content-manager/admin/src/preview/components/PreviewSidePanel.tsx index b96543a841..ceb17ef49d 100644 --- a/packages/core/content-manager/admin/src/preview/components/PreviewSidePanel.tsx +++ b/packages/core/content-manager/admin/src/preview/components/PreviewSidePanel.tsx @@ -31,22 +31,59 @@ const PreviewSidePanel: PanelComponent = ({ model, documentId, document }) => { const { pathname } = useLocation(); const [{ query }] = useQueryParams(); const isModified = useForm('PreviewSidePanel', (state) => state.modified); + const isUnsaved = Boolean(!document || !document.id); + + const title = formatMessage({ + id: 'content-manager.preview.panel.title', + defaultMessage: 'Preview', + }); /** * 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, + const { data, error } = useGetPreviewUrlQuery( + { + params: { + contentType: model as UID.ContentType, + }, + query: { + documentId, + locale: document?.locale, + status: document?.status, + }, }, - query: { - documentId, - locale: document?.locale, - status: document?.status, - }, - }); + // Don't bother making the request since we won't show any UI + { skip: isUnsaved } + ); + + if (isUnsaved) { + return null; + } + + // Preview was not configured but not disabled either (otherwise it would be a success 204). + // So we encourage the user to set it up. + if (error && error.name === 'NotFoundError') { + return { + title, + content: ( + + ), + }; + } if (!data?.data?.url || error) { return null; @@ -59,7 +96,7 @@ const PreviewSidePanel: PanelComponent = ({ model, documentId, document }) => { }; return { - title: formatMessage({ id: 'content-manager.preview.panel.title', defaultMessage: 'Preview' }), + title, content: ( { expect(createPreviewConfigService({ strapi }).isEnabled()).toBe(true); }); - describe('Validation', () => { + describe('isConfigured', () => { + test('Is configured when preview is explicitly disabled', () => { + const strapi = { + config: { + get: () => ({ enabled: false }), + }, + } as any; + + expect(createPreviewConfigService({ strapi }).isConfigured()).toBe(true); + }); + + test('Is configured when handler is configured', () => { + const strapi = { + config: { + get: () => getConfig(true, () => {}), + }, + } as any; + + expect(createPreviewConfigService({ strapi }).isConfigured()).toBe(true); + }); + + test('Is not configured when preview is neither disabled nor configured', () => { + const strapi = { + config: { + get: () => ({ enabled: true }), + }, + } as any; + + expect(createPreviewConfigService({ strapi }).isConfigured()).toBe(false); + }); + + test('Is not configured when no config is provided', () => { + const strapi = { + config: { + get: () => undefined, + }, + } as any; + + expect(createPreviewConfigService({ strapi }).isConfigured()).toBe(false); + }); + }); + + describe('validate', () => { test('Passes on valid configuration', () => { const strapi = { config: { diff --git a/packages/core/content-manager/server/src/preview/services/__tests__/preview.test.ts b/packages/core/content-manager/server/src/preview/services/__tests__/preview.test.ts new file mode 100644 index 0000000000..701a20af79 --- /dev/null +++ b/packages/core/content-manager/server/src/preview/services/__tests__/preview.test.ts @@ -0,0 +1,58 @@ +import { errors } from '@strapi/utils'; +import { createPreviewService } from '../preview'; + +const mockConfig = { + isConfigured: jest.fn(), + getPreviewHandler: jest.fn(), +}; + +const mockStrapi = { + log: { + error: jest.fn(), + }, +} as any; + +const mockGetService = jest.fn().mockReturnValue(mockConfig); + +jest.mock('../../utils', () => ({ + getService: jest.fn().mockImplementation(() => mockGetService()), +})); + +describe('Preview Service', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('Throws 404 when preview is not configured', async () => { + mockConfig.isConfigured.mockReturnValue(false); + + const previewService = createPreviewService({ strapi: mockStrapi }); + + await expect( + previewService.getPreviewUrl('api::article.article', { + documentId: '', + locale: '', + status: 'published', + }) + ).rejects.toThrow(new errors.NotFoundError('Preview config not found')); + + expect(mockConfig.isConfigured).toHaveBeenCalled(); + expect(mockConfig.getPreviewHandler).not.toHaveBeenCalled(); + }); + + test('Calls handler when preview is configured', async () => { + const mockHandler = jest.fn().mockResolvedValue('http://preview.example.com'); + mockConfig.isConfigured.mockReturnValue(true); + mockConfig.getPreviewHandler.mockReturnValue(mockHandler); + + const previewService = createPreviewService({ strapi: mockStrapi }); + const params = { documentId: '1', locale: 'en', status: 'published' as const }; + + const result = await previewService.getPreviewUrl('api::article.article', params); + + expect(result).toBe('http://preview.example.com'); + expect(mockConfig.isConfigured).toHaveBeenCalled(); + expect(mockConfig.getPreviewHandler).toHaveBeenCalled(); + expect(mockHandler).toHaveBeenCalledWith('api::article.article', params); + }); +}); diff --git a/packages/core/content-manager/server/src/preview/services/preview-config.ts b/packages/core/content-manager/server/src/preview/services/preview-config.ts index 82a6d5e26a..fc64479749 100644 --- a/packages/core/content-manager/server/src/preview/services/preview-config.ts +++ b/packages/core/content-manager/server/src/preview/services/preview-config.ts @@ -81,6 +81,11 @@ const createPreviewConfigService = ({ strapi }: { strapi: Core.Strapi }) => { } }, + isConfigured() { + const config = strapi.config.get('admin.preview') as PreviewConfig; + return config?.enabled === false || config?.config?.handler != null; + }, + isEnabled() { const config = strapi.config.get('admin.preview') as PreviewConfig; @@ -113,8 +118,6 @@ const createPreviewConfigService = ({ strapi }: { strapi: Core.Strapi }) => { * Utility to get the preview handler from the configuration */ getPreviewHandler(): PreviewConfig['config']['handler'] { - const config = strapi.config.get('admin.preview') as PreviewConfig; - const emptyHandler = () => { return undefined; }; @@ -123,6 +126,8 @@ const createPreviewConfigService = ({ strapi }: { strapi: Core.Strapi }) => { return emptyHandler; } + const config = strapi.config.get('admin.preview') as PreviewConfig; + return config?.config?.handler || emptyHandler; }, }; diff --git a/packages/core/content-manager/server/src/preview/services/preview.ts b/packages/core/content-manager/server/src/preview/services/preview.ts index d572f9c0d8..caba90c9c9 100644 --- a/packages/core/content-manager/server/src/preview/services/preview.ts +++ b/packages/core/content-manager/server/src/preview/services/preview.ts @@ -12,6 +12,12 @@ const createPreviewService = ({ strapi }: { strapi: Core.Strapi }) => { return { async getPreviewUrl(uid: UID.ContentType, params: HandlerParams) { + const isConfigured = config.isConfigured(); + + if (!isConfigured) { + throw new errors.NotFoundError('Preview config not found'); + } + const handler = config.getPreviewHandler(); try {