mirror of
https://github.com/strapi/strapi.git
synced 2025-09-01 04:42:58 +00:00
feat: preview endpoint (#21574)
* feat: base files for static preview * feat: preview config * Update packages/core/content-manager/server/src/preview/routes/index.ts Co-authored-by: Rémi de Juvigny <8087692+remidej@users.noreply.github.com> * chore: empty handler * chore: comment controllers type * fix: remove is enabled check from load * feat: test preview config * chore: refactor type * feat: preview endpoint * feat: preview test * fix: tests * fix: api test * chore: comment --------- Co-authored-by: Rémi de Juvigny <8087692+remidej@users.noreply.github.com>
This commit is contained in:
parent
52742fc75d
commit
002fc78b3c
@ -1,14 +1,37 @@
|
|||||||
import type { Core } from '@strapi/types';
|
import type { Core, UID } from '@strapi/types';
|
||||||
|
|
||||||
|
import { Preview } from '../../../../shared/contracts';
|
||||||
|
|
||||||
|
import { getService } from '../utils';
|
||||||
|
import { validatePreviewUrl } from './validation/preview';
|
||||||
|
|
||||||
const createPreviewController = () => {
|
const createPreviewController = () => {
|
||||||
return {
|
return {
|
||||||
async getPreviewURL(ctx) {
|
/**
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
* Transforms an entry into a preview URL, so that it can be previewed
|
||||||
const request = ctx.request;
|
* in the Content Manager.
|
||||||
|
*/
|
||||||
|
async getPreviewUrl(ctx) {
|
||||||
|
const uid = ctx.params.contentType as UID.ContentType;
|
||||||
|
const query = ctx.request.query as Preview.GetPreviewUrl.Request['query'];
|
||||||
|
|
||||||
|
// Validate the request parameters
|
||||||
|
const params = await validatePreviewUrl(strapi, uid, query);
|
||||||
|
|
||||||
|
// TODO: Permissions to preview content
|
||||||
|
|
||||||
|
// Get the preview URL by using the user-defined config handler
|
||||||
|
const previewService = getService(strapi, 'preview');
|
||||||
|
const url = await previewService.getPreviewUrl(uid, params);
|
||||||
|
|
||||||
|
// If no url is found, set status to 204
|
||||||
|
if (!url) {
|
||||||
|
ctx.status = 204;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: { url: '' },
|
data: { url },
|
||||||
};
|
} satisfies Preview.GetPreviewUrl.Response;
|
||||||
},
|
},
|
||||||
} satisfies Core.Controller;
|
} satisfies Core.Controller;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import * as yup from 'yup';
|
||||||
|
import { pick } from 'lodash/fp';
|
||||||
|
|
||||||
|
import type { Core, UID } from '@strapi/types';
|
||||||
|
import { validateYupSchema, errors } from '@strapi/utils';
|
||||||
|
|
||||||
|
import { Preview } from '../../../../../shared/contracts';
|
||||||
|
import type { HandlerParams } from '../../services/preview-config';
|
||||||
|
|
||||||
|
const getPreviewUrlSchema = yup
|
||||||
|
.object()
|
||||||
|
.shape({
|
||||||
|
// Will be undefined for single types
|
||||||
|
documentId: yup.string(),
|
||||||
|
locale: yup.string().nullable(),
|
||||||
|
status: yup.string(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const validatePreviewUrl = async (
|
||||||
|
strapi: Core.Strapi,
|
||||||
|
uid: UID.ContentType,
|
||||||
|
params: Preview.GetPreviewUrl.Request['query']
|
||||||
|
): Promise<HandlerParams> => {
|
||||||
|
// Validate the request parameters format
|
||||||
|
await validateYupSchema(getPreviewUrlSchema)(params);
|
||||||
|
|
||||||
|
const newParams = pick(['documentId', 'locale', 'status'], params) as HandlerParams;
|
||||||
|
const model = strapi.getModel(uid);
|
||||||
|
|
||||||
|
// If it's not a collection type or single type
|
||||||
|
if (!model || model.modelType !== 'contentType') {
|
||||||
|
throw new errors.ValidationError('Invalid content type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document id is not required for single types
|
||||||
|
const isSingleType = model?.kind === 'singleType';
|
||||||
|
if (!isSingleType && !params.documentId) {
|
||||||
|
throw new errors.ValidationError('documentId is required for Collection Types');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the documentId if it's a single type
|
||||||
|
if (isSingleType) {
|
||||||
|
const doc = await strapi.documents(uid).findFirst();
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
throw new errors.NotFoundError('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
newParams.documentId = doc?.documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newParams;
|
||||||
|
};
|
@ -9,7 +9,7 @@ const previewRouter: Plugin.LoadedPlugin['routes'][string] = {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
info,
|
info,
|
||||||
path: '/preview/url/:contentType',
|
path: '/preview/url/:contentType',
|
||||||
handler: 'preview.getPreviewURL',
|
handler: 'preview.getPreviewUrl',
|
||||||
config: {
|
config: {
|
||||||
policies: ['admin::isAuthenticatedAdmin'],
|
policies: ['admin::isAuthenticatedAdmin'],
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,31 @@
|
|||||||
|
import type { Core, UID } from '@strapi/types';
|
||||||
|
import { errors } from '@strapi/utils';
|
||||||
|
|
||||||
|
import { getService } from '../utils';
|
||||||
|
import type { HandlerParams } from './preview-config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible of routing an entry to a preview URL.
|
* Responsible of routing an entry to a preview URL.
|
||||||
*/
|
*/
|
||||||
const createPreviewService = () => {};
|
const createPreviewService = ({ strapi }: { strapi: Core.Strapi }) => {
|
||||||
|
const config = getService(strapi, 'preview-config');
|
||||||
|
|
||||||
|
return {
|
||||||
|
async getPreviewUrl(uid: UID.ContentType, params: HandlerParams) {
|
||||||
|
const handler = config.getPreviewHandler();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to get the preview URL from the user-defined handler
|
||||||
|
return handler(uid, params);
|
||||||
|
} catch (error) {
|
||||||
|
// Log the error and throw a generic error
|
||||||
|
strapi.log.error(`Failed to get preview URL: ${error}`);
|
||||||
|
throw new errors.ApplicationError('Failed to get preview URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export { createPreviewService };
|
export { createPreviewService };
|
||||||
|
@ -7,3 +7,4 @@ export * as SingleTypes from './single-types';
|
|||||||
export * as UID from './uid';
|
export * as UID from './uid';
|
||||||
export * as ReviewWorkflows from './review-workflows';
|
export * as ReviewWorkflows from './review-workflows';
|
||||||
export * as HistoryVersions from './history-versions';
|
export * as HistoryVersions from './history-versions';
|
||||||
|
export * as Preview from './preview';
|
||||||
|
31
packages/core/content-manager/shared/contracts/preview.ts
Normal file
31
packages/core/content-manager/shared/contracts/preview.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { Data, UID } from '@strapi/types';
|
||||||
|
import { type errors } from '@strapi/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /content-manager/preview/url/:uid
|
||||||
|
*/
|
||||||
|
export declare namespace GetPreviewUrl {
|
||||||
|
export interface Request {
|
||||||
|
params: {
|
||||||
|
contentType: UID.ContentType;
|
||||||
|
};
|
||||||
|
query: {
|
||||||
|
documentId?: Data.DocumentID;
|
||||||
|
locale?: string;
|
||||||
|
status?: 'published' | 'draft';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Response status will be 204 if no URL is found
|
||||||
|
export type Response =
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
url: string | undefined;
|
||||||
|
};
|
||||||
|
error?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data?: never;
|
||||||
|
error: errors.ApplicationError | errors.ValidationError | errors.NotFoundError;
|
||||||
|
};
|
||||||
|
}
|
133
tests/api/core/content-manager/preview/preview.test.api.ts
Normal file
133
tests/api/core/content-manager/preview/preview.test.api.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { createStrapiInstance } from 'api-tests/strapi';
|
||||||
|
import { createAuthRequest } from 'api-tests/request';
|
||||||
|
import { describeOnCondition } from 'api-tests/utils';
|
||||||
|
import { createTestBuilder } from 'api-tests/builder';
|
||||||
|
|
||||||
|
const collectionTypeUid = 'api::product.product';
|
||||||
|
const collectionTypeModel = {
|
||||||
|
singularName: 'product',
|
||||||
|
pluralName: 'products',
|
||||||
|
displayName: 'Product',
|
||||||
|
kind: 'collectionType',
|
||||||
|
draftAndPublish: true,
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const singleTypeUid = 'api::homepage.homepage';
|
||||||
|
const singleTypeModel = {
|
||||||
|
singularName: 'homepage',
|
||||||
|
pluralName: 'homepages',
|
||||||
|
displayName: 'Homepage',
|
||||||
|
kind: 'singleType',
|
||||||
|
draftAndPublish: true,
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||||
|
|
||||||
|
// TODO: Remove skip when future flag is removed
|
||||||
|
// describeOnCondition(edition === 'EE')('Preview', () => {
|
||||||
|
describeOnCondition(false)('Preview', () => {
|
||||||
|
const builder = createTestBuilder();
|
||||||
|
let strapi;
|
||||||
|
let rq;
|
||||||
|
let singleTypeEntry;
|
||||||
|
|
||||||
|
const updateEntry = async ({ uid, documentId, data, locale }) => {
|
||||||
|
const type = documentId ? 'collection-types' : 'single-types';
|
||||||
|
const params = documentId ? `${type}/${uid}/${documentId}` : `${type}/${uid}`;
|
||||||
|
|
||||||
|
const { body } = await rq({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/content-manager/${params}`,
|
||||||
|
body: data,
|
||||||
|
qs: { locale },
|
||||||
|
});
|
||||||
|
|
||||||
|
return body.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewUrl = async ({ uid, documentId, locale, status }) => {
|
||||||
|
return rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/preview/url/${uid}`,
|
||||||
|
qs: { documentId, locale, status },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await builder.addContentTypes([collectionTypeModel, singleTypeModel]).build();
|
||||||
|
|
||||||
|
strapi = await createStrapiInstance();
|
||||||
|
rq = await createAuthRequest({ strapi });
|
||||||
|
|
||||||
|
// Update the single type to create an initial history version
|
||||||
|
singleTypeEntry = await updateEntry({
|
||||||
|
uid: singleTypeUid,
|
||||||
|
documentId: undefined,
|
||||||
|
locale: 'en',
|
||||||
|
data: {
|
||||||
|
title: 'Welcome',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure the preview URL handler
|
||||||
|
strapi.config.set('admin.preview', {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
handler: (uid, { documentId, locale, status }) => {
|
||||||
|
return `/preview/${uid}/${documentId}?locale=${locale}&status=${status}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await strapi.destroy();
|
||||||
|
await builder.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get preview URL for collection type', async () => {
|
||||||
|
const { body, statusCode } = await getPreviewUrl({
|
||||||
|
uid: collectionTypeUid,
|
||||||
|
documentId: '1',
|
||||||
|
locale: 'en',
|
||||||
|
status: 'draft',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(statusCode).toBe(200);
|
||||||
|
expect(body.data.url).toEqual(`/preview/${collectionTypeUid}/1?locale=en&status=draft`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get preview URL for single type', async () => {
|
||||||
|
const { body, statusCode } = await getPreviewUrl({
|
||||||
|
uid: singleTypeUid,
|
||||||
|
documentId: undefined,
|
||||||
|
locale: 'en',
|
||||||
|
status: 'draft',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(statusCode).toBe(200);
|
||||||
|
expect(body.data.url).toEqual(
|
||||||
|
`/preview/${singleTypeUid}/${singleTypeEntry.documentId}?locale=en&status=draft`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user