Skip to content

feat: add experience-toolbar starter example [EXT-7365]#11009

Open
Jared Jolton (jjolton-contentful) wants to merge 5 commits into
masterfrom
feat/experience-toolbar-example
Open

feat: add experience-toolbar starter example [EXT-7365]#11009
Jared Jolton (jjolton-contentful) wants to merge 5 commits into
masterfrom
feat/experience-toolbar-example

Conversation

@jjolton-contentful

@jjolton-contentful Jared Jolton (jjolton-contentful) commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

EXT-7365

Summary

  • Adds a Create Contentful App example at examples/experience-toolbar for the new ExO toolbar App SDK location (LOCATION_EXPERIENCE_TOOLBAR), scaffoldable via npx create-contentful-app --example experience-toolbar.
  • Typed with ExperienceEditorToolbarAppSDK from @contentful/app-sdk@4.58.0 (first example to pin 4.58.0). Demonstrates every pattern in the ticket AC:
    • location detection — sdk.location.is(locations.LOCATION_EXPERIENCE_TOOLBAR) in App.tsx
    • reading sdk.exo.context to tell experience vs. fragment apart (+ onContextChanged)
    • subscribing to sdk.exo.experience.selection.onChange()
    • resolving the selected node via sdk.exo.experience.getNode(nodeId) and reading its properties
    • reacting to sdk.exo.onUiModeChanged() (form/visual), with graceful form-mode degradation
  • React-only; no Data Assembly or save()/publish() (per ticket excluded scope). Mirrors examples/typescript conventions (Vite + Vitest + Forma 36).

Two deviations from the AC (intentional — flagging for review)

  1. --example, not --template. The AC says --template experience-toolbar, but the CLI scaffolds repo examples via --example <name> (resolved from apps/examples/ via the GitHub API on master). --template is a fixed allowlist of framework starters (IGNORED_EXAMPLE_FOLDERS). Delivered as an example, matching how editor-assignment, page-location, etc. ship. The ticket hedges with "(or equivalent)".
  2. No app-definition.json. No example in the repo ships a manifest, and the toolbar location is not an EditorInterface target state — visibility is determined solely by whether the location is registered on the app definition. Documented registration via create-app-definition / add-locations in the README and a ConfigScreen note instead of inventing a manifest the CLI would strip.

Verification status

  • ✅ Type-verified: tsc --noEmit clean against the published 4.58.0 types.
  • vite build clean.
  • ✅ 7/7 Vitest specs pass (mocked SDK drives selection / ui-mode / context subscriptions).
  • ⚠️ Not runtime-verified inside a live ExO editor — the host renderer serving sdk.exo is still rolling out (EXT-7363/EXT-7364). The README states this explicitly. API shapes match the published types exactly.

Test plan

  • npm install && npm run build in examples/experience-toolbar succeeds
  • npm run test:ci passes
  • npx create-contentful-app --example experience-toolbar scaffolds a working project (once merged to master, since the CLI reads examples from the GitHub API)
  • (When host renderer lands) install into a space, register the experience-toolbar location, open an experience, and confirm context/selection/ui-mode/properties render live

Generated with Claude Code

Add a Create Contentful App example demonstrating the new ExO toolbar
App SDK location (LOCATION_EXPERIENCE_TOOLBAR), scaffoldable via
`npx create-contentful-app --example experience-toolbar`.

Typed with ExperienceEditorToolbarAppSDK (@contentful/app-sdk@4.58.0),
it demonstrates location detection, reading sdk.exo.context
(experience vs. fragment), subscribing to selection changes, reading
the selected node's properties via getNode, and reacting to UI mode
changes. React-only; no Data Assembly or save/publish (per ticket scope).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lewisjcs

Josh Lewis (lewisjcs) commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

⚔️ The Gauntlet — 🛡️ Clean · 4 advisory

🤖 AI-powered · your code, through the lanes

gauntlet verdict

🛑 Blockers 0
⚠️ Concerns 2
💡 Nits 2
🔒 Security clean ✓
🧪 Lanes code-quality · adversarial · doc-review (PR body) · security

No blockers. Security lane is clear, and both AC deviations (--example vs --template, no app-definition.json) hold up under scrutiny. Four advisory items, all about what the template teaches the next developer rather than correctness:

Findings

Finding Location Status
⚠️ The mock's getProperties always resolves instantly and never rejects, so the loading-spinner and error paths are never tested. test/mocks/mockSdk.ts:16 Open
⚠️ The binding branch of formatPropertyValue — arguably the most ExO-specific display logic — is never exercised; no mock property carries a binding. src/locations/ExperienceToolbar.tsx:222 Open
💡 The PR description has an orphaned [EXT-7365]: …?atlOrigin=… reference-link carrying an Atlassian smartlink token; the inline link at the top already covers it. PR description footer Open
💡 The initial getProperties().catch() resets to empty state with no log — worth a comment or a small UI note so the template doesn't model silent error-swallowing. src/locations/ExperienceToolbar.tsx:101 Open
🤖 Machine-readable findings (for agents)
{
  "tool": "gauntlet",
  "schema": "v1",
  "reviewed_ref": "8d22108f4",
  "verdict": { "blockers": 0, "concerns": 2, "nits": 2, "security": "clean" },
  "findings": [
    { "severity": "concern", "location": "test/mocks/mockSdk.ts:16", "lens": "adversarial-review / Hidden Assumptions", "confidence": 93, "claim": "getProperties always resolves immediately and never rejects; loading-spinner and error UI paths untested.", "recommendation": "Add a deferred-promise spec for the spinner and a mockRejectedValueOnce for the error path." },
    { "severity": "concern", "location": "src/locations/ExperienceToolbar.tsx:222", "lens": "code-quality-audit / Gap", "confidence": 80, "claim": "binding branch of formatPropertyValue never exercised; no mock property has a binding.", "recommendation": "Add a mock property with binding {sourceType:'entry',entryId} and assert the rendered 'entry -> ...'." },
    { "severity": "nit", "location": "PR description footer", "lens": "doc-review / Memory rules — no local paths", "confidence": 88, "claim": "Orphaned [EXT-7365]: ?atlOrigin= reference-link leaks an Atlassian smartlink token; inline top link suffices.", "recommendation": "Delete the trailing reference-style definition." },
    { "severity": "nit", "location": "src/locations/ExperienceToolbar.tsx:101", "lens": "code-quality-standards / Rule 5", "confidence": 70, "claim": "Initial getProperties().catch() resets state silently with no log; models silent swallowing in a template.", "recommendation": "Add a code comment or surface a small error note." }
  ]
}

🎮 The Gauntlet · an AI review harness built by Josh C.S. Lewis · reviewed at 8d22108f4. Everything but the 🛑 is advisory.

…-7365]

Address code review on the experience-toolbar starter:
- add a deferred-promise spec asserting the loading spinner
- add a spec for the getProperties rejection path (spinner clears, no table)
- cover the formatPropertyValue binding branch ("entry -> entryId")
- comment the previously-silent getProperties().catch() state reset

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lewisjcs Josh Lewis (lewisjcs) force-pushed the feat/experience-toolbar-example branch from eba492e to 45989e8 Compare June 5, 2026 01:58
…on [EXT-7365]

4.58.1 reshaped ComponentPropertyDescriptor.binding to the typed `Binding`
union (EntryBinding | ManualBinding), replacing `{ sourceType, entryId }`.

- formatPropertyValue narrows on `binding.type === 'entry'` to read entryId
  off the EntryBinding arm (was destructuring the removed `sourceType`).
- Spec uses the EntryBinding fixture shape `{ type, entryId, fieldId }`.
- `area` rendering is unchanged — the discriminator is retained in the SDK.
- Bump @contentful/app-sdk 4.58.0 -> 4.58.1.

Verified: tsc --noEmit clean, 12 tests passing against 4.58.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Track the latest published ExO surface (DA definition-only trim landed in
ui-extensions-sdk#2592, widget-renderer synced in experience-packages#3544).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants