Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `isContentResolved` as datasource validity in `withDatasourceCheck()` ([#2219](https://github.com/Sitecore/jss/pull/2219))

### 🐛 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))
Expand Down
213 changes: 213 additions & 0 deletions packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.innerHTML).to.be.empty;
});

it('should return wrapped component when isContentResolved is true', () => {
const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
const props = {
rendering: {
componentName: 'TestComponent',
dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}',
isContentResolved: true,
},
};

const wrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName);
expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource);
});

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}',
isContentResolved: true,
},
};

const wrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(true)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName);
expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource);
});

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}',
isContentResolved: false,
},
};

const wrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.innerHTML).to.be.empty;
});

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}',
isContentResolved: false,
},
};

const wrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(true)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.querySelectorAll('div.sc-jss-editing-error')).to.have.length(1);
});

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}',
isContentResolved: false,
},
};

const wrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.innerHTML).to.be.empty;
});

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}',
isContentResolved: false,
},
};

const wrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.innerHTML).to.be.empty;
});

it('should preserve existing missing-datasource behavior when isContentResolved is true', () => {
const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
const props = {
rendering: {
componentName: 'TestComponent',
dataSource: '',
isContentResolved: true,
},
};

const wrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...props} />
</SitecoreContextReactContext.Provider>
);

expect(wrapper.container.innerHTML).to.be.empty;
});

it('should preserve existing behavior when isContentResolved 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(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...propsWithDatasource} />
</SitecoreContextReactContext.Provider>
);
const hidden = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck {...propsWithoutDatasource} />
</SitecoreContextReactContext.Provider>
);

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}',
isContentResolved: false,
};
const parentRendering = {
componentName: 'ParentComponent',
dataSource: '{PARENT-DATASOURCE-ID}',
isContentResolved: true,
placeholders: {
nested: [nestedChild],
},
};

const parentWrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck rendering={parentRendering} />
</SitecoreContextReactContext.Provider>
);
const childWrapper = render(
<SitecoreContextReactContext.Provider value={mockContext(false)}>
<TestComponentWithDatasourceCheck rendering={nestedChild} />
</SitecoreContextReactContext.Provider>
);

expect(parentWrapper.container.innerHTML).to.contain(parentRendering.componentName);
expect(childWrapper.container.innerHTML).to.be.empty;
});
});
32 changes: 27 additions & 5 deletions packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown> | React.FC<unknown>;
}

/**
* 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 unresolved content.
* @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.isContentResolved !== false;
}

/**
* Checks whether a Sitecore datasource is present and valid, then renders appropriately depending on page mode (normal vs editing).
* `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.
* 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 (isContentResolved: false) use the same
* // fallback as a missing datasource: hide in normal mode, show an editing error in editing mode.
* const ContentBlock = (props) => <div>{props.fields.heading}</div>;
* export default withDatasourceCheck()(ContentBlock);
*
* // Layout Service: { componentName: 'ContentBlock', dataSource: '{id}', isContentResolved: false }
* // → ContentBlock is not rendered; no extra app-level check is required.
*/
export function withDatasourceCheck(options?: WithDatasourceCheckOptions) {
return function withDatasourceCheckHoc<ComponentProps extends WithDatasourceCheckProps>(
Expand All @@ -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) ? (
<Component {...props} />
) : sitecoreContext.pageEditing ? (
<EditingError />
Expand Down
6 changes: 6 additions & 0 deletions packages/sitecore-jss/src/layout/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ export interface ComponentParams {
export interface ComponentRendering<T = ComponentFields> {
componentName: string;
dataSource?: string;
/**
* `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.
*/
isContentResolved?: boolean;
uid?: string;
placeholders?: PlaceholdersData;
fields?: T;
Expand Down
Loading