Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sitecore-jss] fix isEditorActive for XMCloud Pages #1912

Merged
merged 10 commits into from
Aug 30, 2024
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))
* `[sitecore-jss-nextjs]` Resolved an issue with redirects that was caused by the x-middleware-next header in Next.js. This header prevented the flow from being interrupted properly, resulting in redirects not functioning correctly in certain cases. ([#1899](https://github.com/Sitecore/jss/pull/1899))

## 22.1.1
Expand Down
42 changes: 31 additions & 11 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 { getJssHorizonClientData } from '@sitecore-jss/sitecore-jss/editing';

describe('<EditingScripts />', () => {
const mockComponentFactory: ComponentFactory = () => null;
Expand Down Expand Up @@ -75,11 +76,20 @@ 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`, () => {
[
{
pageState: 'Edit',
jssData: getJssHorizonClientData(),
},
{
pageState: 'Preview',
jssData: {},
},
].forEach((scenario) => {
it(`should render nothing when in ${scenario.pageState} pageState and Chromes editmode`, () => {
const layoutData = getLayoutData({
editMode: EditMode.Chromes,
pageState: LayoutServicePageState[pageState],
pageState: LayoutServicePageState.Preview,
pageEditing: true,
});

Expand All @@ -95,11 +105,11 @@ describe('<EditingScripts />', () => {
expect(scripts.find('script')).to.have.length(0);
});

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

Expand All @@ -110,8 +120,9 @@ describe('<EditingScripts />', () => {
);

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

expect(scripts.find('script')).to.have.length(4);
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');
Expand Down Expand Up @@ -140,10 +151,12 @@ describe('<EditingScripts />', () => {
);
});

it('should render nothing when data is not provided', () => {
it(`should render ${scenario.jssData ? 'jss client scripts' : 'nothing'} when edit mode is ${
scenario.pageState
} and data is not provided`, () => {
const layoutData = getLayoutData({
editMode: EditMode.Metadata,
pageState: LayoutServicePageState[pageState],
pageState: LayoutServicePageState[scenario.pageState],
pageEditing: true,
clientData: {},
clientScripts: [],
Expand All @@ -156,9 +169,16 @@ describe('<EditingScripts />', () => {
);

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

expect(scripts.html()).to.equal('');
expect(scripts.find('script')).to.have.length(0);
if (scenario.jssData) {
const ids = Object.keys(scenario.jssData);
ids.forEach((id) => {
expect(scripts.exists(`#${id}`)).to.equal(true);
});
expect(scripts.find('script')).to.have.length(ids.length);
} else {
expect(scripts.html()).to.equal('');
expect(scripts.find('script')).to.have.length(0);
}
});
});
});
Expand Down
23 changes: 14 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 { getJssHorizonClientData } from '@sitecore-jss/sitecore-jss/editing';

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

if (editMode === EditMode.Metadata) {
let jssClientData: Record<string, unknown> = clientData;
if (pageState === LayoutServicePageState.Edit) {
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
jssClientData = { ...jssClientData, ...getJssHorizonClientData() };
}

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,
getJssHorizonClientData,
EDITING_ALLOWED_ORIGINS,
QUERY_PARAM_EDITING_SECRET,
PAGES_EDITING_MARKER,
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
} 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 getJssHorizonClientData = () => {
illiakovalenko marked this conversation as resolved.
Show resolved Hide resolved
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
const clientData: Record<string, Record<string, unknown>> = {};
clientData[PAGES_EDITING_MARKER] = {};

return clientData;
};