From 67e4ae7a1935a49a5b4b63e32a31ca199cabf9fe Mon Sep 17 00:00:00 2001 From: sc-naveenhedallaarachchi Date: Wed, 19 Aug 2026 18:31:27 +0530 Subject: [PATCH 1/3] Add dataSourceResolveFailed to ComponentRendering and update withDatasourceCheck behavior --- CHANGELOG.md | 4 + .../enhancers/withDatasourceCheck.test.tsx | 213 ++++++++++++++++++ .../src/enhancers/withDatasourceCheck.tsx | 32 ++- packages/sitecore-jss/src/layout/models.ts | 6 + 4 files changed, 250 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e179bbf896..2f03273079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ Our versioning strategy is as follows: > ⚠️ **JSS 23 supports Sitecore XP 10.5 only. Sitecore AI is not supported - use Sitecore Content SDK for that scenario.** +### 🎉 New Features & Improvements + +* `[sitecore-jss]` `[sitecore-jss-react]` Treat Layout Service `dataSourceResolveFailed` as an invalid datasource in `withDatasourceCheck()` + ### 🐛 Bug Fixes * `[sitecore-jss-react]` Suppress hydration mismatch warnings in Experience Editor by adding `suppressHydrationWarning` to SDK chrome paths (ErrorBoundary, placeholders, field components)([#2218](https://github.com/Sitecore/jss/pull/2218)) diff --git a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx index f2d81852c5..d151cc06b6 100644 --- a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx +++ b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx @@ -146,4 +146,217 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName); expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource); }); + + it('should return null if no datasource is configured on the rendering', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.innerHTML).to.be.empty; + }); + + it('should return wrapped component when dataSourceResolveFailed is false', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', + dataSourceResolveFailed: false, + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName); + expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource); + }); + + it('should return wrapped component when dataSourceResolveFailed is false in editing mode', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', + dataSourceResolveFailed: false, + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName); + expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource); + }); + + it('should return null when dataSourceResolveFailed is true in normal mode', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', + dataSourceResolveFailed: true, + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.innerHTML).to.be.empty; + }); + + it('should return default error component when dataSourceResolveFailed is true in editing mode', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', + dataSourceResolveFailed: true, + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.querySelectorAll('div.sc-jss-editing-error')).to.have.length(1); + }); + + it('should not render when the datasource item was deleted and dataSourceResolveFailed is true', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + dataSource: '{DELETED-DATASOURCE-ID}', + dataSourceResolveFailed: true, + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.innerHTML).to.be.empty; + }); + + it('should not render when the datasource item was archived and dataSourceResolveFailed is true', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + dataSource: '{ARCHIVED-DATASOURCE-ID}', + dataSourceResolveFailed: true, + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.innerHTML).to.be.empty; + }); + + it('should preserve existing missing-datasource behavior when dataSourceResolveFailed is false', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const props = { + rendering: { + componentName: 'TestComponent', + dataSource: '', + dataSourceResolveFailed: false, + }, + }; + + const wrapper = render( + + + + ); + + expect(wrapper.container.innerHTML).to.be.empty; + }); + + it('should preserve existing behavior when dataSourceResolveFailed is absent', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const propsWithDatasource = { + rendering: { + componentName: 'TestComponent', + dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', + }, + }; + const propsWithoutDatasource = { + rendering: { + componentName: 'TestComponent', + dataSource: '', + }, + }; + + const rendered = render( + + + + ); + const hidden = render( + + + + ); + + expect(rendered.container.innerHTML).to.contain(propsWithDatasource.rendering.componentName); + expect(hidden.container.innerHTML).to.be.empty; + }); + + it('should evaluate nested placeholder renderings independently', () => { + const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); + const nestedChild = { + componentName: 'ChildComponent', + dataSource: '{CHILD-DATASOURCE-ID}', + dataSourceResolveFailed: true, + }; + const parentRendering = { + componentName: 'ParentComponent', + dataSource: '{PARENT-DATASOURCE-ID}', + dataSourceResolveFailed: false, + placeholders: { + nested: [nestedChild], + }, + }; + + const parentWrapper = render( + + + + ); + const childWrapper = render( + + + + ); + + expect(parentWrapper.container.innerHTML).to.contain(parentRendering.componentName); + expect(childWrapper.container.innerHTML).to.be.empty; + }); }); diff --git a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx index 390ecaf91c..3ee7bfb5df 100644 --- a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx +++ b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx @@ -14,18 +14,40 @@ export interface WithDatasourceCheckProps { export interface WithDatasourceCheckOptions { /** - * A component that is rendered when a datasource is missing during editing. + * A component that is rendered when a datasource is missing or failed to resolve during editing. * If unspecified, a default component with message is displayed. */ editingErrorComponent?: React.ComponentClass | React.FC; } /** - * Checks whether a Sitecore datasource is present and renders appropriately depending on page mode (normal vs editing). + * Returns true when the rendering has a datasource and Layout Service did not report a resolve failure. + * @param {ComponentRendering} [rendering] rendering data from Layout Service + * @returns {boolean} whether the datasource is present and valid + */ +function hasValidDatasource(rendering?: ComponentRendering): boolean { + if (!rendering?.dataSource) { + return false; + } + + return rendering.dataSourceResolveFailed !== true; +} + +/** + * Checks whether a Sitecore datasource is present and valid, then renders appropriately depending on page mode (normal vs editing). + * `dataSourceResolveFailed: true` is treated the same as a missing datasource. If the property is omitted, the original presence check is used. * @param {WithDatasourceCheckOptions} [options] * @returns - * The wrapped component, if a datasource is present. - * A null component (in normal mode) or an error component (in editing mode), if a datasource is not present. + * The wrapped component, if a datasource is present and valid. + * A null component (in normal mode) or an error component (in editing mode), if a datasource is missing or failed to resolve. + * @example + * // Wrap once. Deleted/archived datasources (dataSourceResolveFailed: true) use the same + * // fallback as a missing datasource: hide in normal mode, show an editing error in editing mode. + * const ContentBlock = (props) =>
{props.fields.heading}
; + * export default withDatasourceCheck()(ContentBlock); + * + * // Layout Service: { componentName: 'ContentBlock', dataSource: '{id}', dataSourceResolveFailed: true } + * // → ContentBlock is not rendered; no extra app-level check is required. */ export function withDatasourceCheck(options?: WithDatasourceCheckOptions) { return function withDatasourceCheckHoc( @@ -35,7 +57,7 @@ export function withDatasourceCheck(options?: WithDatasourceCheckOptions) { const { sitecoreContext } = useSitecoreContext(); const EditingError = options?.editingErrorComponent ?? DefaultEditingError; - return props.rendering?.dataSource ? ( + return hasValidDatasource(props.rendering) ? ( ) : sitecoreContext.pageEditing ? ( diff --git a/packages/sitecore-jss/src/layout/models.ts b/packages/sitecore-jss/src/layout/models.ts index a5d70b304f..0d9ad40448 100644 --- a/packages/sitecore-jss/src/layout/models.ts +++ b/packages/sitecore-jss/src/layout/models.ts @@ -85,6 +85,12 @@ export interface ComponentParams { export interface ComponentRendering { componentName: string; dataSource?: string; + /** + * `true` when Layout Service failed to resolve this rendering's datasource item + * (for example because the item was deleted or archived). + * Omitted by older Layout Service versions; absence preserves existing behavior. + */ + dataSourceResolveFailed?: boolean; uid?: string; placeholders?: PlaceholdersData; fields?: T; From 319ee07cb3fa5c4f36f59c3e6ce25a6df90e7afb Mon Sep 17 00:00:00 2001 From: sc-naveenhedallaarachchi Date: Wed, 19 Aug 2026 19:07:33 +0530 Subject: [PATCH 2/3] chore: Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f03273079..957e29ddd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Our versioning strategy is as follows: ### 🎉 New Features & Improvements -* `[sitecore-jss]` `[sitecore-jss-react]` Treat Layout Service `dataSourceResolveFailed` as an invalid datasource in `withDatasourceCheck()` +* `[sitecore-jss]` `[sitecore-jss-react]` Treat Layout Service `dataSourceResolveFailed` as an invalid datasource in `withDatasourceCheck()` ([#2219](https://github.com/Sitecore/jss/pull/2219)) ### 🐛 Bug Fixes From 8b01450d95763acae56254220f4c9e1445ef003f Mon Sep 17 00:00:00 2001 From: sc-naveenhedallaarachchi Date: Fri, 21 Aug 2026 12:04:08 +0530 Subject: [PATCH 3/3] Refactor: replace dataSourceResolveFailed with isContentResolved in withDatasourceCheck --- CHANGELOG.md | 2 +- .../enhancers/withDatasourceCheck.test.tsx | 34 +++++++++---------- .../src/enhancers/withDatasourceCheck.tsx | 10 +++--- packages/sitecore-jss/src/layout/models.ts | 6 ++-- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 957e29ddd1..0efb60825e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Our versioning strategy is as follows: ### 🎉 New Features & Improvements -* `[sitecore-jss]` `[sitecore-jss-react]` Treat Layout Service `dataSourceResolveFailed` as an invalid datasource in `withDatasourceCheck()` ([#2219](https://github.com/Sitecore/jss/pull/2219)) +* `[sitecore-jss]` `[sitecore-jss-react]` Treat Layout Service `isContentResolved` as datasource validity in `withDatasourceCheck()` ([#2219](https://github.com/Sitecore/jss/pull/2219)) ### 🐛 Bug Fixes diff --git a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx index d151cc06b6..296d1675ba 100644 --- a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx +++ b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx @@ -164,13 +164,13 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.be.empty; }); - it('should return wrapped component when dataSourceResolveFailed is false', () => { + it('should return wrapped component when isContentResolved is true', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const props = { rendering: { componentName: 'TestComponent', dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', - dataSourceResolveFailed: false, + isContentResolved: true, }, }; @@ -184,13 +184,13 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource); }); - it('should return wrapped component when dataSourceResolveFailed is false in editing mode', () => { + it('should return wrapped component when isContentResolved is true in editing mode', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const props = { rendering: { componentName: 'TestComponent', dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', - dataSourceResolveFailed: false, + isContentResolved: true, }, }; @@ -204,13 +204,13 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource); }); - it('should return null when dataSourceResolveFailed is true in normal mode', () => { + it('should return null when isContentResolved is false in normal mode', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const props = { rendering: { componentName: 'TestComponent', dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', - dataSourceResolveFailed: true, + isContentResolved: false, }, }; @@ -223,13 +223,13 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.be.empty; }); - it('should return default error component when dataSourceResolveFailed is true in editing mode', () => { + it('should return default error component when isContentResolved is false in editing mode', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const props = { rendering: { componentName: 'TestComponent', dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}', - dataSourceResolveFailed: true, + isContentResolved: false, }, }; @@ -242,13 +242,13 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.querySelectorAll('div.sc-jss-editing-error')).to.have.length(1); }); - it('should not render when the datasource item was deleted and dataSourceResolveFailed is true', () => { + it('should not render when the datasource item was deleted and isContentResolved is false', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const props = { rendering: { componentName: 'TestComponent', dataSource: '{DELETED-DATASOURCE-ID}', - dataSourceResolveFailed: true, + isContentResolved: false, }, }; @@ -261,13 +261,13 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.be.empty; }); - it('should not render when the datasource item was archived and dataSourceResolveFailed is true', () => { + it('should not render when the datasource item was archived and isContentResolved is false', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const props = { rendering: { componentName: 'TestComponent', dataSource: '{ARCHIVED-DATASOURCE-ID}', - dataSourceResolveFailed: true, + isContentResolved: false, }, }; @@ -280,13 +280,13 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.be.empty; }); - it('should preserve existing missing-datasource behavior when dataSourceResolveFailed is false', () => { + it('should preserve existing missing-datasource behavior when isContentResolved is true', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const props = { rendering: { componentName: 'TestComponent', dataSource: '', - dataSourceResolveFailed: false, + isContentResolved: true, }, }; @@ -299,7 +299,7 @@ describe('withDatasourceCheck', () => { expect(wrapper.container.innerHTML).to.be.empty; }); - it('should preserve existing behavior when dataSourceResolveFailed is absent', () => { + it('should preserve existing behavior when isContentResolved is absent', () => { const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent); const propsWithDatasource = { rendering: { @@ -334,12 +334,12 @@ describe('withDatasourceCheck', () => { const nestedChild = { componentName: 'ChildComponent', dataSource: '{CHILD-DATASOURCE-ID}', - dataSourceResolveFailed: true, + isContentResolved: false, }; const parentRendering = { componentName: 'ParentComponent', dataSource: '{PARENT-DATASOURCE-ID}', - dataSourceResolveFailed: false, + isContentResolved: true, placeholders: { nested: [nestedChild], }, diff --git a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx index 3ee7bfb5df..6430f9ce65 100644 --- a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx +++ b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx @@ -21,7 +21,7 @@ export interface WithDatasourceCheckOptions { } /** - * Returns true when the rendering has a datasource and Layout Service did not report a resolve failure. + * Returns true when the rendering has a datasource and Layout Service did not report unresolved content. * @param {ComponentRendering} [rendering] rendering data from Layout Service * @returns {boolean} whether the datasource is present and valid */ @@ -30,23 +30,23 @@ function hasValidDatasource(rendering?: ComponentRendering): boolean { return false; } - return rendering.dataSourceResolveFailed !== true; + return rendering.isContentResolved !== false; } /** * Checks whether a Sitecore datasource is present and valid, then renders appropriately depending on page mode (normal vs editing). - * `dataSourceResolveFailed: true` is treated the same as a missing datasource. If the property is omitted, the original presence check is used. + * `isContentResolved: false` is treated the same as a missing datasource. If the property is omitted, the original presence check is used. * @param {WithDatasourceCheckOptions} [options] * @returns * The wrapped component, if a datasource is present and valid. * A null component (in normal mode) or an error component (in editing mode), if a datasource is missing or failed to resolve. * @example - * // Wrap once. Deleted/archived datasources (dataSourceResolveFailed: true) use the same + * // Wrap once. Deleted/archived datasources (isContentResolved: false) use the same * // fallback as a missing datasource: hide in normal mode, show an editing error in editing mode. * const ContentBlock = (props) =>
{props.fields.heading}
; * export default withDatasourceCheck()(ContentBlock); * - * // Layout Service: { componentName: 'ContentBlock', dataSource: '{id}', dataSourceResolveFailed: true } + * // Layout Service: { componentName: 'ContentBlock', dataSource: '{id}', isContentResolved: false } * // → ContentBlock is not rendered; no extra app-level check is required. */ export function withDatasourceCheck(options?: WithDatasourceCheckOptions) { diff --git a/packages/sitecore-jss/src/layout/models.ts b/packages/sitecore-jss/src/layout/models.ts index 0d9ad40448..d38c90c8a1 100644 --- a/packages/sitecore-jss/src/layout/models.ts +++ b/packages/sitecore-jss/src/layout/models.ts @@ -86,11 +86,11 @@ export interface ComponentRendering { componentName: string; dataSource?: string; /** - * `true` when Layout Service failed to resolve this rendering's datasource item - * (for example because the item was deleted or archived). + * `true` when Layout Service resolved this rendering's datasource content. + * `false` when resolution failed (for example because the item was deleted or archived). * Omitted by older Layout Service versions; absence preserves existing behavior. */ - dataSourceResolveFailed?: boolean; + isContentResolved?: boolean; uid?: string; placeholders?: PlaceholdersData; fields?: T;