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 {