Skip to content

Commit 2221db5

Browse files
feat: modernize ExO example apps — canvas terminology, app-sdk 4.67.0, auto-resize [AIS-223] (#11112)
* docs: rename Experience Editor to experience canvas in ExO example apps [AIS-223] Update user-facing copy in the experience-toolbar and experience-auditor example apps to use "experience canvas" (and "Experience Canvas toolbar" for the location) in place of "Experience Editor". Also refresh the README verification notes to describe each example against the published app-sdk contract without time-bound rollout claims. TypeScript identifiers, imports, and the ExperienceEditorToolbarAppSDK type are left unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: bump app-sdk to 4.67.0 and auto-resize ExO example apps [AIS-223] Bumps @contentful/app-sdk to ^4.67.0 in both ExO example apps and wires the new window API on the experience-toolbar location so each app reports its height into the host's stacked Apps panel via startAutoResizer. The call is guarded on sdk.window so hosts that don't serve it degrade to a no-op. Also awaits the now-async getRootNodes() and strengthens the README verification sections to state the apps are verified end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a7af0b6 commit 2221db5

16 files changed

Lines changed: 83 additions & 41 deletions

File tree

examples/experience-auditor/README.md

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Experience Auditor
22

3-
A polished, real-world example app for the **Experience Editor toolbar** — the
3+
A polished, real-world example app for the **Experience Canvas toolbar** — the
44
`experience-toolbar` location introduced in
55
[`@contentful/app-sdk@4.60.0`](https://www.npmjs.com/package/@contentful/app-sdk).
66

7-
Experience Auditor runs alongside the Experience Editor and continuously audits
7+
Experience Auditor runs alongside the experience canvas and continuously audits
88
the experience you are editing for **accessibility, SEO, and
99
content-completeness** issues. It demonstrates the standout capability of the
1010
toolbar location: **live, selection-aware tooling that reads the experience tree
@@ -170,24 +170,23 @@ npm run create-app-definition
170170
selecting the **App configuration screen** and **Experience toolbar** locations,
171171
pointing the app at `http://localhost:3000`.
172172

173-
## A note on verification
173+
## Verification
174174

175-
This app is built against the published `@contentful/app-sdk@4.60.0` types,
176-
which are the contract for the toolbar location. The host renderer that serves
177-
`sdk.experiences` at runtime is still rolling out, so the app is **type-verified and
178-
unit-tested against a mocked SDK** — 36 tests cover the audit rules, scoring,
175+
This app is verified working end-to-end against a live experience canvas — the
176+
host renderer serving `sdk.experiences` — in addition to being **type-checked
177+
and unit-tested against a mocked SDK**. 38 tests cover the audit rules, scoring,
179178
the collector and its binding resolution, capability detection, the suggested-
180-
fix derivation, and the toolbar's locate / fix / publish-gate behavior. It is
181-
not yet verified end-to-end inside a live Experience Editor; that live
182-
verification is tracked separately as the host renderer rolls out. The API
183-
shapes used here match the published types exactly.
179+
fix derivation, the auto-resize wiring, and the toolbar's locate / fix /
180+
publish-gate behavior. The API shapes used here match the published
181+
`@contentful/app-sdk` types exactly, so the audit you run against the canvas is
182+
the same code these tests exercise.
184183

185184
> **Note on `getRootNodes()`.** The audit traversal starts from
186-
> `sdk.experiences.experience.getRootNodes()`, which currently resolves to an
187-
> empty list until the host wires up experience-tree sync. Against a live host
188-
> today the toolbar shows a distinct "no components to audit yet" state rather
189-
> than the all-clear celebration; demo mode (`/?demo`) exercises the full loop
190-
> against a seeded in-memory experience.
185+
> `sdk.experiences.experience.getRootNodes()`. When it returns an empty list
186+
> — an experience with nothing on the canvas yet — the toolbar shows a distinct
187+
> "no components to audit yet" state rather than the all-clear celebration;
188+
> demo mode (`/?demo`) exercises the full loop against a seeded in-memory
189+
> experience.
191190
192191
## Available scripts
193192

examples/experience-auditor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6-
"@contentful/app-sdk": "^4.63.1",
6+
"@contentful/app-sdk": "^4.67.0",
77
"@contentful/f36-components": "4.81.1",
88
"@contentful/f36-icons": "^4.28.0",
99
"@contentful/f36-tokens": "4.2.0",

examples/experience-auditor/src/audit/collect.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('collectNodes', () => {
77
const experience: any = {
88
getRootNodes: vi
99
.fn()
10-
.mockReturnValue([
10+
.mockResolvedValue([
1111
makeMockNode('a', 'Component', [{ key: 'heading', area: 'content', value: 'Hi' }]),
1212
makeMockNode('b', 'Component', [{ key: 'body', area: 'content', value: 'There' }]),
1313
]),
@@ -27,7 +27,7 @@ describe('collectNodes', () => {
2727
const experience: any = {
2828
getRootNodes: vi
2929
.fn()
30-
.mockReturnValue([
30+
.mockResolvedValue([
3131
broken,
3232
makeMockNode('ok', 'Component', [{ key: 'heading', area: 'content', value: 'Hi' }]),
3333
]),

examples/experience-auditor/src/audit/collect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { CollectedNode } from './types';
1111
* the same `getNode` + `getProperties` pattern, applied recursively.
1212
*/
1313
export async function collectNodes(experience: ExperienceAPI): Promise<CollectedNode[]> {
14-
const roots = experience.getRootNodes();
14+
const roots = await experience.getRootNodes();
1515

1616
const collected = await Promise.all(
1717
roots.map(async (node) => {

examples/experience-auditor/src/components/FindingList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const FindingList = ({ findings, nodeCount, canLocate, onLocate }: FindingListPr
8383
title={
8484
canLocate
8585
? undefined
86-
: 'Locate on canvas is not available — the experience editor does not expose selection yet'
86+
: 'Locate on canvas is not available — the experience canvas does not expose selection yet'
8787
}>
8888
Locate
8989
</Button>

examples/experience-auditor/src/demo/mockExperiences.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const demoSdk = {
6464
save: async () => {},
6565
publish: async () => {},
6666
getNode: (id: string) => nodes.find((n) => n.id === id) ?? null,
67-
getRootNodes: () => nodes,
67+
getRootNodes: async () => nodes,
6868
// selection intentionally omitted (see above)
6969
},
7070
},

examples/experience-auditor/src/locations/ConfigScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const ConfigScreen = () => {
3939
<Form>
4040
<Heading>Experience Auditor</Heading>
4141
<Paragraph>
42-
Experience Auditor runs inside the Experience Editor toolbar and continuously checks the
42+
Experience Auditor runs inside the Experience Canvas toolbar and continuously checks the
4343
experience you are editing for accessibility, SEO, and content-completeness issues.
4444
</Paragraph>
4545
<Note variant="primary">

examples/experience-auditor/src/locations/ExperienceToolbar.spec.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ describe('ExperienceToolbar (Experience Auditor)', () => {
1919
set: vi.fn(),
2020
highlight: vi.fn(),
2121
};
22-
mockSdk.experiences.experience.getRootNodes.mockReturnValue(defaultNodes);
22+
mockSdk.experiences.experience.getRootNodes.mockResolvedValue(defaultNodes);
2323
mockSdk.experiences.experience.getNode.mockImplementation(
2424
(id: string) => defaultNodes.find((n) => n.id === id) ?? null
2525
);
2626
});
2727

28+
it('starts the auto resizer so the host sizes the toolbar panel', () => {
29+
render(<ExperienceToolbar />);
30+
31+
expect(mockSdk.window.startAutoResizer).toHaveBeenCalledOnce();
32+
});
33+
2834
it('runs an audit on mount and renders findings with a score', async () => {
2935
const { getByTestId, getAllByTestId } = render(<ExperienceToolbar />);
3036

@@ -60,7 +66,7 @@ describe('ExperienceToolbar (Experience Auditor)', () => {
6066
});
6167

6268
it('shows a no-tree-data state when the host returns no root nodes', async () => {
63-
mockSdk.experiences.experience.getRootNodes.mockReturnValue([]);
69+
mockSdk.experiences.experience.getRootNodes.mockResolvedValue([]);
6470
const { getByTestId } = render(<ExperienceToolbar />);
6571
await waitFor(() => expect(getByTestId('no-tree-data')).toBeInTheDocument());
6672
});
@@ -73,7 +79,7 @@ describe('ExperienceToolbar (Experience Auditor)', () => {
7379
{ key: 'image', area: 'content', value: { sys: { id: 'asset-1' } } },
7480
{ key: 'altText', area: 'content', value: ' spaced alt ' },
7581
]);
76-
mockSdk.experiences.experience.getRootNodes.mockReturnValue([fixNode]);
82+
mockSdk.experiences.experience.getRootNodes.mockResolvedValue([fixNode]);
7783
mockSdk.experiences.experience.getNode.mockReturnValue(fixNode);
7884

7985
const { getAllByTestId, getByTestId } = render(<ExperienceToolbar />);
@@ -92,7 +98,7 @@ describe('ExperienceToolbar (Experience Auditor)', () => {
9298
{ key: 'heading', area: 'content', value: 'Spring Sale' },
9399
{ key: 'metaTitle', area: 'content', value: '' },
94100
]);
95-
mockSdk.experiences.experience.getRootNodes.mockReturnValue([metaNode]);
101+
mockSdk.experiences.experience.getRootNodes.mockResolvedValue([metaNode]);
96102
mockSdk.experiences.experience.getNode.mockReturnValue(metaNode);
97103

98104
const { getByTestId, queryByText } = render(<ExperienceToolbar />);

examples/experience-auditor/src/locations/ExperienceToolbar.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ const ExperienceToolbar = () => {
6262
}
6363
}, [sdk]);
6464

65+
// Report the panel's height so the host sizes the toolbar app to its content
66+
// in the stacked Apps panel. sdk.window on the toolbar location arrived in
67+
// app-sdk 4.67.0; guard it so hosts that don't serve it degrade to a no-op.
68+
useEffect(() => {
69+
if (!sdk.window) {
70+
return;
71+
}
72+
sdk.window.startAutoResizer();
73+
return () => sdk.window.stopAutoResizer();
74+
}, [sdk]);
75+
6576
// Keep context in sync.
6677
useEffect(() => sdk.experiences.onContextChanged(setContext), [sdk]);
6778

examples/experience-auditor/test/mocks/mockSdk.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ const mockSdk: any = {
3838
setReady: vi.fn(),
3939
getCurrentState: vi.fn().mockResolvedValue(null),
4040
},
41+
window: {
42+
startAutoResizer: vi.fn(),
43+
stopAutoResizer: vi.fn(),
44+
updateHeight: vi.fn(),
45+
},
4146
access: {
4247
can: vi.fn().mockResolvedValue(true),
4348
},
@@ -55,7 +60,7 @@ const mockSdk: any = {
5560
save: vi.fn().mockResolvedValue(undefined),
5661
publish: vi.fn().mockResolvedValue(undefined),
5762
getNode: vi.fn((id: string) => defaultNodes.find((n) => n.id === id) ?? null),
58-
getRootNodes: vi.fn().mockReturnValue(defaultNodes),
63+
getRootNodes: vi.fn().mockResolvedValue(defaultNodes),
5964
selection: {
6065
get: vi.fn().mockReturnValue({ nodeId: null }),
6166
onChange: vi.fn().mockReturnValue(noopUnsubscribe),

0 commit comments

Comments
 (0)