diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f62cabba6..3b4800dfa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,18 @@ Our versioning strategy is as follows: ### ๐Ÿงน Chores +## 22.1.1 + +### ๐ŸŽ‰ New Features & Improvements + +* `[XM Cloud]` `[Metadata Mode]` `[Next.js]` API was changed, next.js preview data provides a new parameter `layoutKind` therefore, please make the necessary updates in the app in `plugins/preview-mode.ts` as shown in the following PR to experience a smooth upgrade. +* `[sitecore-jss]` `[sitecore-jss-nextjs]` Pass `sc_layoutKind` to GraphQLEditingService request header to support shared/final editing layouts ([#1907](https://github.com/Sitecore/jss/pull/1907)) +* `[sitecore-jss]` `GraphQLRequestClient`'s `request` method now supports dynamic headers based on specific request ([#1907](https://github.com/Sitecore/jss/pull/1907)) + +``` + client.request(query, variables, { headers }) +``` + ## 22.1.0 ### ๐Ÿ› Bug Fixes diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/preview-mode.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/preview-mode.ts index 4bc6f1ed8a..f9b3125b7e 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/preview-mode.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/page-props-factory/plugins/preview-mode.ts @@ -20,13 +20,14 @@ class PreviewModePlugin implements Plugin { // If we're in Pages preview (editing) Metadata Edit Mode, prefetch the editing data if (isEditingMetadataPreviewData(context.previewData)) { - const { site, itemId, language, version, variantIds } = context.previewData; + const { site, itemId, language, version, variantIds, layoutKind } = context.previewData; const data = await graphQLEditingService.fetchEditingData({ siteName: site, itemId, language, version, + layoutKind, }); if (!data) { diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts index 59975eb428..71c6c06b3b 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts @@ -181,6 +181,7 @@ describe('EditingRenderMiddleware', () => { sc_variant: 'dev', sc_version: 'latest', secret: secret, + sc_layoutKind: 'shared', } as MetadataQueryParams; it('should handle request', async () => { @@ -200,6 +201,7 @@ describe('EditingRenderMiddleware', () => { version: 'latest', editMode: 'metadata', pageState: 'edit', + layoutKind: 'shared', }); expect(res.redirect).to.have.been.calledOnce; @@ -237,6 +239,7 @@ describe('EditingRenderMiddleware', () => { version: undefined, editMode: 'metadata', pageState: 'edit', + layoutKind: undefined, }); }); @@ -265,6 +268,7 @@ describe('EditingRenderMiddleware', () => { version: undefined, editMode: 'metadata', pageState: 'edit', + layoutKind: undefined, }); expect(res.redirect).to.have.been.calledOnce; @@ -297,6 +301,7 @@ describe('EditingRenderMiddleware', () => { version: 'latest', editMode: 'metadata', pageState: 'edit', + layoutKind: 'shared', }); expect(res.redirect).to.have.been.calledOnce; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts index ec5ded6c1a..49dcf8f6c4 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts @@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { STATIC_PROPS_ID, SERVER_PROPS_ID } from 'next/constants'; import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss'; import { EditMode, LayoutServicePageState } from '@sitecore-jss/sitecore-jss/layout'; +import { LayoutKind } from '@sitecore-jss/sitecore-jss/editing'; import { QUERY_PARAM_EDITING_SECRET, EDITING_ALLOWED_ORIGINS, @@ -277,6 +278,7 @@ export type MetadataQueryParams = { mode: Exclude; sc_variant?: string; sc_version?: string; + sc_layoutKind?: LayoutKind; }; /** @@ -297,6 +299,7 @@ export type EditingMetadataPreviewData = { pageState: Exclude; variantIds: string[]; version?: string; + layoutKind?: LayoutKind; }; /** @@ -358,6 +361,7 @@ export class MetadataHandler { version: query.sc_version, editMode: EditMode.Metadata, pageState: query.mode, + layoutKind: query.sc_layoutKind, } as EditingMetadataPreviewData, // Cache the preview data for 3 seconds to ensure the page is rendered with the correct preview data not the cached one { diff --git a/packages/sitecore-jss/src/editing/graphql-editing-service.test.ts b/packages/sitecore-jss/src/editing/graphql-editing-service.test.ts index e2d889a5da..d7ec53338c 100644 --- a/packages/sitecore-jss/src/editing/graphql-editing-service.test.ts +++ b/packages/sitecore-jss/src/editing/graphql-editing-service.test.ts @@ -15,6 +15,7 @@ import { mockEditingServiceResponse, } from '../test-data/mockEditingServiceResponse'; import { EditMode } from '../layout'; +import { LayoutKind } from './models'; import debug from '../debug'; use(spies); @@ -91,12 +92,20 @@ describe('GraphQLEditingService', () => { }) ).to.be.true; expect(clientFactorySpy.returnValues[0].request).to.be.called.exactly(1); - expect(clientFactorySpy.returnValues[0].request).to.be.called.with(query, { - language, - version, - itemId, - siteName, - }); + expect(clientFactorySpy.returnValues[0].request).to.be.called.with( + query, + { + language, + version, + itemId, + siteName, + }, + { + headers: { + sc_layoutKind: 'final', + }, + } + ); expect(result).to.deep.equal({ layoutData: layoutDataResponse, @@ -148,12 +157,20 @@ describe('GraphQLEditingService', () => { }) ).to.be.true; expect(clientFactorySpy.returnValues[0].request).to.be.called.exactly(1); - expect(clientFactorySpy.returnValues[0].request).to.be.called.with(query, { - language, - version, - itemId, - siteName, - }); + expect(clientFactorySpy.returnValues[0].request).to.be.called.with( + query, + { + language, + version, + itemId, + siteName, + }, + { + headers: { + sc_layoutKind: 'final', + }, + } + ); expect(result).to.deep.equal({ layoutData: { @@ -215,6 +232,55 @@ describe('GraphQLEditingService', () => { spy.restore(clientFactorySpy); }); + it('should fetch shared layout editing data', async () => { + nock(hostname, { reqheaders: { sc_editMode: 'true', sc_layoutKind: 'shared' } }) + .post(endpointPath, /EditingQuery/gi) + .reply(200, editingData); + + const clientFactorySpy = sinon.spy(clientFactory); + + const service = new GraphQLEditingService({ + clientFactory: clientFactorySpy, + }); + + spy.on(clientFactorySpy.returnValues[0], 'request'); + + const result = await service.fetchEditingData({ + language, + version, + itemId, + siteName, + layoutKind: LayoutKind.Shared, + }); + + expect(clientFactorySpy.calledOnce).to.be.true; + expect(clientFactorySpy.returnValues[0].request).to.be.called.exactly(1); + expect(clientFactorySpy.returnValues[0].request).to.be.called.with( + query, + { + language, + version, + itemId, + siteName, + }, + { + headers: { + sc_layoutKind: 'shared', + }, + } + ); + + expect(result).to.deep.equal({ + layoutData: layoutDataResponse, + dictionary: { + foo: 'foo-phrase', + bar: 'bar-phrase', + }, + }); + + spy.restore(clientFactorySpy); + }); + it('should fetch editing data when dicionary has multiple pages', async () => { nock(hostname, { reqheaders: { sc_editMode: 'true' } }) .post(endpointPath, /EditingQuery/gi) diff --git a/packages/sitecore-jss/src/editing/graphql-editing-service.ts b/packages/sitecore-jss/src/editing/graphql-editing-service.ts index 791e40ee6c..93ae040065 100644 --- a/packages/sitecore-jss/src/editing/graphql-editing-service.ts +++ b/packages/sitecore-jss/src/editing/graphql-editing-service.ts @@ -3,6 +3,7 @@ import { PageInfo } from '../graphql'; import { GraphQLClient, GraphQLRequestClientFactory } from '../graphql-request-client'; import { DictionaryPhrases } from '../i18n'; import { EditMode, LayoutServiceData } from '../layout'; +import { LayoutKind } from './models'; /** * The dictionary query default page size. @@ -115,6 +116,7 @@ export class GraphQLEditingService { * @param {string} variables.itemId - The item id (path) to fetch layout data for. * @param {string} variables.language - The language to fetch layout data for. * @param {string} [variables.version] - The version of the item (optional). + * @param {LayoutKind} [variables.layoutKind] - The final or shared layout variant. * @returns {Promise} The layout data and dictionary phrases. */ async fetchEditingData({ @@ -122,13 +124,22 @@ export class GraphQLEditingService { itemId, language, version, + layoutKind = LayoutKind.Final, }: { siteName: string; itemId: string; language: string; version?: string; + layoutKind?: LayoutKind; }) { - debug.editing('fetching editing data for %s %s %s %s', siteName, itemId, language, version); + debug.editing( + 'fetching editing data for %s %s %s %s', + siteName, + itemId, + language, + version, + layoutKind + ); if (!siteName) { throw new RangeError('The site name must be a non-empty string'); @@ -143,12 +154,20 @@ export class GraphQLEditingService { let hasNext = true; let after = ''; - const editingData = await this.graphQLClient.request(query, { - siteName, - itemId, - version, - language, - }); + const editingData = await this.graphQLClient.request( + query, + { + siteName, + itemId, + version, + language, + }, + { + headers: { + sc_layoutKind: layoutKind, + }, + } + ); if (editingData?.site?.siteInfo?.dictionary) { dictionaryResults = editingData.site.siteInfo.dictionary.results; diff --git a/packages/sitecore-jss/src/editing/index.ts b/packages/sitecore-jss/src/editing/index.ts index 600ffaee70..e3febbe38a 100644 --- a/packages/sitecore-jss/src/editing/index.ts +++ b/packages/sitecore-jss/src/editing/index.ts @@ -20,3 +20,4 @@ export { EditButtonTypes, mapButtonToCommand, } from './edit-frame'; +export { LayoutKind } from './models'; diff --git a/packages/sitecore-jss/src/editing/models.ts b/packages/sitecore-jss/src/editing/models.ts new file mode 100644 index 0000000000..2abad5dc3f --- /dev/null +++ b/packages/sitecore-jss/src/editing/models.ts @@ -0,0 +1,9 @@ +๏ปฟ/** + * Represents the Editing Layout variant. + * - shared - shared layout + * - final - final layout + */ +export enum LayoutKind { + Final = 'final', + Shared = 'shared', +} diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index 82a858fdad..1777c24ae0 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -68,7 +68,41 @@ describe('GraphQLRequestClient', () => { }); const graphQLClient = new GraphQLRequestClient(endpoint, { apiKey }); - await graphQLClient.request('test'); + const result = await graphQLClient.request('test'); + + expect(result).to.deep.equal({ result: 'Hello world...' }); + }); + + it('should send additional request headers configured through options', async () => { + const apiKey = 'cjhNRWNVOHRFTklwUjhYa0RSTnBhSStIam1mNE1KN1pyeW13c3FnRVExTT18bXRzdC1kLTAxOQ=='; + const customHeader = 'Custom-Header-Value'; + nock('http://jssnextweb', { + reqheaders: { + sc_apikey: apiKey, + }, + }) + .post('/graphql') + .reply(200, function() { + const receivedHeaders = this.req.headers; + + expect(receivedHeaders['sc_apikey']).to.deep.equal([apiKey]); + expect(receivedHeaders['custom-header']).to.deep.equal([customHeader]); + + return { + data: { + result: 'Hello world...', + }, + }; + }); + + const graphQLClient = new GraphQLRequestClient(endpoint, { apiKey }); + const result = await graphQLClient.request('test', undefined, { + headers: { + 'Custom-Header': customHeader, + }, + }); + + expect(result).to.deep.equal({ result: 'Hello world...' }); }); it('should debug log request and response', async () => { diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index 15925e52b7..b537ea9d90 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -4,6 +4,13 @@ import { DocumentNode } from 'graphql'; import debuggers, { Debugger } from './debug'; import TimeoutPromise from './utils/timeout-promise'; +/** + * Options for configuring a GraphQL request. + */ +interface RequestOptions { + headers?: Record; +} + /** * An interface for GraphQL clients for Sitecore APIs */ @@ -11,9 +18,14 @@ export interface GraphQLClient { /** * Execute graphql request * @param {string | DocumentNode} query graphql query - * @param {Object} variables graphql variables + * @param {Object} [variables] graphql variables + * @param {RequestOptions} [options] options for configuring a GraphQL request. */ - request(query: string | DocumentNode, variables?: { [key: string]: unknown }): Promise; + request( + query: string | DocumentNode, + variables?: { [key: string]: unknown }, + options?: RequestOptions + ): Promise; } /** @@ -206,11 +218,13 @@ export class GraphQLRequestClient implements GraphQLClient { /** * Execute graphql request * @param {string | DocumentNode} query graphql query - * @param {Object} variables graphql variables + * @param {Object} [variables] graphql variables + * @param {RequestOptions} [options] Options for configuring a GraphQL request. */ async request( query: string | DocumentNode, - variables?: { [key: string]: unknown } + variables?: { [key: string]: unknown }, + options?: RequestOptions ): Promise { let attempt = 1; @@ -219,12 +233,12 @@ export class GraphQLRequestClient implements GraphQLClient { // (or nice hooks like we have with Axios), but we should log whatever we have. this.debug('request: %o', { url: this.endpoint, - headers: this.headers, + headers: { ...this.headers, ...options?.headers }, query, variables, }); const startTimestamp = Date.now(); - const fetchWithOptionalTimeout = [this.client.request(query, variables)]; + const fetchWithOptionalTimeout = [this.client.request(query, variables, options?.headers)]; if (this.timeout) { this.abortTimeout = new TimeoutPromise(this.timeout); fetchWithOptionalTimeout.push(this.abortTimeout.start);