Skip to content

Commit

Permalink
[sitecore-jss] fix isEditorActive for XMCloud Pages (#1912)
Browse files Browse the repository at this point in the history
* [sitecore-jss] fix isEditorActive for XMCloud Pages
* render JSS-specific element in Pages editing
  • Loading branch information
art-alexeyenko authored Aug 30, 2024
1 parent 210939b commit a9b094d
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 94 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Our versioning strategy is as follows:

### 🐛 Bug Fixes

* `[sitecore-jss]` Fix isEditorActive returning false in XMCloud Pages ([#1912](https://github.com/Sitecore/jss/pull/1912))
* `[templates/nextjs]` `[XM Cloud]` FEAAS / BYOC Components are not visible on the page with running A/B test ([#1914](https://github.com/Sitecore/jss/pull/1914))
* Make sure to update the _PagePropsFactory_ plugins *order*, these plugins should be executed after the _page-props-factory\plugins\personalize.ts_ plugin to ensure that personalized layout data is used:
- _page-props-factory/plugins/component-themes.ts_
Expand Down
133 changes: 68 additions & 65 deletions packages/sitecore-jss-react/src/components/EditingScripts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { EditingScripts } from './EditingScripts';
import { SitecoreContext } from './SitecoreContext';
import { ComponentFactory } from './sharedTypes';
import { getJssPagesClientData } from '@sitecore-jss/sitecore-jss/editing';

describe('<EditingScripts />', () => {
const mockComponentFactory: ComponentFactory = () => null;
Expand Down Expand Up @@ -75,11 +76,11 @@ describe('<EditingScripts />', () => {
expect(scripts.find('script')).to.have.length(0);
});

['Preview', 'Edit'].forEach((pageState) => {
it(`should render nothing when ${pageState} and edit mode is Chromes`, () => {
['Edit', 'Preview'].forEach((pageState) => {
it(`should render nothing when in ${pageState} pageState and Chromes editmode`, () => {
const layoutData = getLayoutData({
editMode: EditMode.Chromes,
pageState: LayoutServicePageState[pageState],
pageState: LayoutServicePageState.Preview,
pageEditing: true,
});

Expand All @@ -94,72 +95,74 @@ describe('<EditingScripts />', () => {
expect(scripts.html()).to.be.null;
expect(scripts.find('script')).to.have.length(0);
});
});
describe('should render Pages scripts when in Metadata mode', () => {
it('should render scripts', () => {
const layoutData = getLayoutData({
editMode: EditMode.Metadata,
pageState: LayoutServicePageState.Edit,
pageEditing: true,
});

const component = mount(
<SitecoreContext componentFactory={mockComponentFactory} layoutData={layoutData}>
<EditingScripts />
</SitecoreContext>
);

const scripts = component.find('EditingScripts');
const jssScriptsLength = Object.keys(getJssPagesClientData()).length;

describe(`should render scripts when ${pageState} and edit mode is Metadata`, () => {
it('should render scripts', () => {
const layoutData = getLayoutData({
editMode: EditMode.Metadata,
pageState: LayoutServicePageState[pageState],
pageEditing: true,
});

const component = mount(
<SitecoreContext componentFactory={mockComponentFactory} layoutData={layoutData}>
<EditingScripts />
</SitecoreContext>
);

const scripts = component.find('EditingScripts');

expect(scripts.find('script')).to.have.length(4);

const script1 = scripts.find('script').at(0);
expect(script1.prop('src')).to.equal('http://test.foo/script1.js');

const script2 = scripts.find('script').at(1);
expect(script2.prop('src')).to.equal('http://test.foo/script2.js');

const script3 = scripts.find('script').at(2);
expect(script3.prop('id')).to.equal('foo');
expect(script3.prop('type')).to.equal('application/json');
expect(script3.prop('dangerouslySetInnerHTML')).to.deep.equal({
__html: '{"x":1,"y":"1","z":true}',
});
expect(script3.html()).to.equal(
'<script id="foo" type="application/json">{"x":1,"y":"1","z":true}</script>'
);

const script4 = scripts.find('script').at(3);
expect(script4.prop('id')).to.equal('bar');
expect(script4.prop('type')).to.equal('application/json');
expect(script4.prop('dangerouslySetInnerHTML')).to.deep.equal({
__html: '{"a":2,"b":"2","c":false}',
});
expect(script4.html()).to.equal(
'<script id="bar" type="application/json">{"a":2,"b":"2","c":false}</script>'
);
expect(scripts.find('script')).to.have.length(4 + jssScriptsLength);

const script1 = scripts.find('script').at(0);
expect(script1.prop('src')).to.equal('http://test.foo/script1.js');

const script2 = scripts.find('script').at(1);
expect(script2.prop('src')).to.equal('http://test.foo/script2.js');

const script3 = scripts.find('script').at(2);
expect(script3.prop('id')).to.equal('foo');
expect(script3.prop('type')).to.equal('application/json');
expect(script3.prop('dangerouslySetInnerHTML')).to.deep.equal({
__html: '{"x":1,"y":"1","z":true}',
});
expect(script3.html()).to.equal(
'<script id="foo" type="application/json">{"x":1,"y":"1","z":true}</script>'
);

it('should render nothing when data is not provided', () => {
const layoutData = getLayoutData({
editMode: EditMode.Metadata,
pageState: LayoutServicePageState[pageState],
pageEditing: true,
clientData: {},
clientScripts: [],
});

const component = mount(
<SitecoreContext componentFactory={mockComponentFactory} layoutData={layoutData}>
<EditingScripts />
</SitecoreContext>
);

const scripts = component.find('EditingScripts');

expect(scripts.html()).to.equal('');
expect(scripts.find('script')).to.have.length(0);
const script4 = scripts.find('script').at(3);
expect(script4.prop('id')).to.equal('bar');
expect(script4.prop('type')).to.equal('application/json');
expect(script4.prop('dangerouslySetInnerHTML')).to.deep.equal({
__html: '{"a":2,"b":"2","c":false}',
});
expect(script4.html()).to.equal(
'<script id="bar" type="application/json">{"a":2,"b":"2","c":false}</script>'
);
});

it('should render jss pages script elements when data is not provided', () => {
const layoutData = getLayoutData({
editMode: EditMode.Metadata,
pageState: LayoutServicePageState.Edit,
pageEditing: true,
clientData: {},
clientScripts: [],
});

const component = mount(
<SitecoreContext componentFactory={mockComponentFactory} layoutData={layoutData}>
<EditingScripts />
</SitecoreContext>
);

const scripts = component.find('EditingScripts');
const ids = Object.keys(getJssPagesClientData());
ids.forEach((id) => {
expect(scripts.exists(`#${id}`)).to.equal(true);
});
expect(scripts.find('script')).to.have.length(ids.length);
});
});
});
20 changes: 11 additions & 9 deletions packages/sitecore-jss-react/src/components/EditingScripts.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { EditMode, LayoutServicePageState } from '@sitecore-jss/sitecore-jss/layout';
import { useSitecoreContext } from '../enhancers/withSitecoreContext';
import { getJssPagesClientData } from '@sitecore-jss/sitecore-jss/editing';

/**
* Renders client scripts and data for editing/preview mode in Pages.
Expand All @@ -15,20 +16,21 @@ export const EditingScripts = (): JSX.Element => {
if (pageState === LayoutServicePageState.Normal) return <></>;

if (editMode === EditMode.Metadata) {
const jssClientData = { ...clientData, ...getJssPagesClientData() };

return (
<>
{clientScripts?.map((src, index) => (
<script src={src} key={index} />
))}
{clientData &&
Object.keys(clientData).map((id) => (
<script
key={id}
id={id}
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(clientData[id]) }}
/>
))}
{Object.keys(jssClientData).map((id) => (
<script
key={id}
id={id}
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jssClientData[id]) }}
/>
))}
</>
);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/sitecore-jss/src/editing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ export {
resetEditorChromes,
handleEditorAnchors,
Metadata,
getJssPagesClientData,
EDITING_ALLOWED_ORIGINS,
QUERY_PARAM_EDITING_SECRET,
PAGES_EDITING_MARKER,
} from './utils';
export {
DefaultEditFrameButton,
Expand Down
48 changes: 35 additions & 13 deletions packages/sitecore-jss/src/editing/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/* eslint-disable no-unused-expressions */
import { expect, spy } from 'chai';
import { isEditorActive, resetEditorChromes, ChromeRediscoveryGlobalFunctionName } from './utils';
import {
isEditorActive,
resetEditorChromes,
ChromeRediscoveryGlobalFunctionName,
PAGES_EDITING_MARKER,
} from './utils';

// must make TypeScript happy with `global` variable modification
interface CustomWindow {
Expand All @@ -15,32 +20,49 @@ interface Global {
declare const global: Global;

describe('utils', () => {
const pagesEditingDocument = {
getElementById: (id: unknown) => (id === PAGES_EDITING_MARKER ? 'present' : null),
};

const nonPagesEditingDocument = {
getElementById: (id: unknown) => (id === PAGES_EDITING_MARKER ? null : 'present'),
};

describe('isEditorActive', () => {
it('should return false when invoked on server', () => {
expect(isEditorActive()).to.be.false;
});

it('should return true when EE is active', () => {
global.window = {
document: {},
document: nonPagesEditingDocument,
location: { search: '' },
Sitecore: { PageModes: { ChromeManager: {} } },
};
expect(isEditorActive()).to.be.true;
});

it('should return true when Horizon is active', () => {
it('should return true when XMC Pages edit mode is active', () => {
global.window = {
document: {},
location: { search: '?sc_horizon=editor' },
document: pagesEditingDocument,
location: { search: '' },
Sitecore: null,
};
expect(isEditorActive()).to.be.true;
});

it('should return false when EE and Horizon are not active', () => {
it('should return false when XMC Pages preview mode is active', () => {
global.window = {
document: nonPagesEditingDocument,
location: { search: '?sc_horizon=preview' },
Sitecore: null,
};
expect(isEditorActive()).to.be.false;
});

it('should return false when EE and XMC Pages are not active', () => {
global.window = {
document: {},
document: nonPagesEditingDocument,
location: { search: '' },
Sitecore: null,
};
Expand All @@ -60,29 +82,29 @@ describe('utils', () => {
it('should reset chromes when EE is active', () => {
const resetChromes = spy();
global.window = {
document: {},
document: nonPagesEditingDocument,
location: { search: '' },
Sitecore: { PageModes: { ChromeManager: { resetChromes } } },
};
resetEditorChromes();
expect(resetChromes).to.have.been.called.once;
});

it('should reset chromes when Horizon is active', () => {
it('should reset chromes when XMC Pages edit mode is active', () => {
const resetChromes = spy();
global.window = {
document: {},
location: { search: '?sc_horizon=editor' },
document: pagesEditingDocument,
location: { search: '' },
Sitecore: null,
};
global.window[ChromeRediscoveryGlobalFunctionName.name] = resetChromes;
resetEditorChromes();
expect(resetChromes).to.have.been.called.once;
});

it('should not throw when EE and Horizon are not active', () => {
it('should not throw when EE and XMC Pages are not active', () => {
global.window = {
document: {},
document: nonPagesEditingDocument,
location: { search: '' },
Sitecore: null,
};
Expand Down
33 changes: 26 additions & 7 deletions packages/sitecore-jss/src/editing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import isServer from '../utils/is-server';
*/
export const QUERY_PARAM_EDITING_SECRET = 'secret';

/**
* ID to be used as a marker for a script rendered in XMC Pages
* Should identify app is in XM Cloud Pages editing mode
*/
export const PAGES_EDITING_MARKER = 'jss-hrz-editing';

/**
* Default allowed origins for editing requests. This is used to enforce CORS, CSP headers.
*/
Expand Down Expand Up @@ -62,26 +68,28 @@ export const ChromeRediscoveryGlobalFunctionName = {
};

/**
* Static utility class for Sitecore Horizon Editor
* Static utility class for Sitecore Pages Editor (ex-Horizon)
*/
export class HorizonEditor {
/**
* Determines whether the current execution context is within a Horizon Editor.
* Horizon Editor environment can be identified only in the browser
* @returns true if executing within a Horizon Editor
* Determines whether the current execution context is within a Pages Editor.
* Pages Editor environment can be identified only in the browser
* @returns true if executing within a Pages Editor
*/
static isActive(): boolean {
if (isServer()) {
return false;
}
// Horizon will add "sc_horizon=editor" query string parameter for the editor and "sc_horizon=simulator" for the preview
return window.location.search.indexOf('sc_horizon=editor') > -1;
// Check for Chromes mode
const chromesCheck = window.location.search.indexOf('sc_headless_mode=edit') > -1;
// JSS will render a jss-exclusive script element in Metadata mode to indicate edit mode in Pages
return chromesCheck || !!window.document.getElementById(PAGES_EDITING_MARKER);
}
static resetChromes(): void {
if (isServer()) {
return;
}
// Reset chromes in Horizon
// Reset chromes in Pages
(window as ExtendedWindow)[ChromeRediscoveryGlobalFunctionName.name] &&
((window as ExtendedWindow)[ChromeRediscoveryGlobalFunctionName.name] as () => void)();
}
Expand Down Expand Up @@ -145,3 +153,14 @@ export const handleEditorAnchors = () => {
observer.observe(targetNode, observerOptions);
}
};

/**
* Gets extra JSS clientData scripts to render in XMC Pages in addition to clientData from Pages itself
* @returns {Record} collection of clientData
*/
export const getJssPagesClientData = () => {
const clientData: Record<string, Record<string, unknown>> = {};
clientData[PAGES_EDITING_MARKER] = {};

return clientData;
};

0 comments on commit a9b094d

Please sign in to comment.