Skip to content

feat(admin): allow trusted plugins to add editor panels - #2187

Open
logelog wants to merge 4 commits into
emdash-cms:mainfrom
logelog:feat/plugin-content-editor-sections
Open

feat(admin): allow trusted plugins to add editor panels#2187
logelog wants to merge 4 commits into
emdash-cms:mainfrom
logelog:feat/plugin-content-editor-sections

Conversation

@logelog

@logelog logelog commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a focused contentEditorPanels extension contract for trusted React plugins. Plugins can contribute host-framed panels to the saved-entry settings sidebar while EmDash retains layout ownership, collection and role filtering, disabled-plugin lifecycle handling, deterministic ordering, and render-error isolation.

New entries keep the native editor unchanged. A failing contribution cannot take down the editor.

Discussion: #2105

Addresses #2227. That issue asks for a plugin-rendered editor panel that can read the rest of the document rather than a single field's value. A contributed panel receives the whole entry, so it can read the title, slug, body, and every other field. The panel is passed the saved entry, so it refreshes on save/autosave rather than on every keystroke.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs - a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: Trusted native content editor extension panels #2105 (opened, awaiting maintainer review)

AI-generated code disclosure

  • This PR includes AI generated code: Claude Opus 5, GPT-5.6 (via private dev orchestra)

Screenshots / test output

  • pnpm typecheck
  • pnpm lint - 0 warnings, 0 errors
  • pnpm format
  • 25 focused resolver and editor integration tests passed

@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a9614d9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Major
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 548 lines across 10 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@pkg-pr-new

pkg-pr-new Bot commented Jul 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2187

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2187

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2187

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2187

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2187

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2187

emdash

npm i https://pkg.pr.new/emdash@2187

create-emdash

npm i https://pkg.pr.new/create-emdash@2187

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2187

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2187

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2187

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2187

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2187

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2187

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2187

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2187

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2187

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2187

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2187

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2187

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2187

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2187

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2187

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2187

commit: a9614d9

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change is a focused, well-scoped feature that fits EmDash's trusted-plugin model: it adds a contentEditorPanels export contract, resolves/filters panels using PluginAdminContext and the admin manifest, renders them in host-owned settings-sidebar sections, and isolates failures with a React error boundary. The implementation is clean, focused tests and docs are present, and the changeset is appropriate.

One process note: the PR checklist item "New features link to an approved Discussion" is left unchecked even though the description links to Discussion #2105. That box should be ticked, or the approval status confirmed, before merge per the feature PR policy.

One code finding: the new error boundary's retry button is a raw <button> despite the AGENTS.md rule to use Kumo components. It mirrors the existing PluginFieldErrorBoundary pattern, but new code should migrate to Kumo Button instead. Otherwise the changed files are in good shape and the new conventions (Lingui wrapping, RTL-safe logical classes, manifest lifecycle filtering) are followed.

Comment on lines +193 to +199
<button
type="button"
className="mt-1 text-xs font-medium text-kumo-brand underline"
onClick={() => this.setState({ hasError: false })}
>
<Trans>Retry</Trans>
</button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The fallback UI uses a raw <button> with hand-rolled Tailwind classes (text-kumo-brand underline). AGENTS.md requires using Kumo components rather than custom buttons so the control gets Kumo focus rings, sizing, and dark-mode/RTL behavior for free. Import Button from @cloudflare/kumo and use it here (and, separately, migrate the identical pattern in the pre-existing PluginFieldErrorBoundary).

Suggested change
<button
type="button"
className="mt-1 text-xs font-medium text-kumo-brand underline"
onClick={() => this.setState({ hasError: false })}
>
<Trans>Retry</Trans>
</button>
<Button
type="button"
variant="ghost"
size="sm"
className="mt-1"
onClick={() => this.setState({ hasError: false })}
>
<Trans>Retry</Trans>
</Button>

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review overlap and removed review/needs-review No maintainer or bot review yet labels Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@swissky swissky added the bot:review Trigger an emdashbot code review on this PR label Jul 22, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature is a focused, well-scoped extension that fits EmDash's trusted-plugin model: it adds a contentEditorPanels export contract, resolves/filters panels via PluginAdminContext and the admin manifest, renders them in host-owned settings-sidebar sections, and isolates failures with a React error boundary. The approach is right for the problem.

Status of prior findings:

  • ✅ The previous Kumo Button finding is resolved — ContentEditorPanelBoundary now imports and uses Button from @cloudflare/kumo instead of a raw <button>.
  • ⚠️ The process note from the last review remains: the PR checklist item "New features link to an approved Discussion" is still unchecked. The description links to Discussion #2105, but per the feature PR policy the box should be ticked (or the approval status explicitly confirmed) before merge.

What I checked:

  • Panel contract, validation, manifest lifecycle filtering, collection/role filtering, deterministic ordering, and render-error recovery in packages/admin/src/lib/content-editor-panels.tsx.
  • Integration into ContentSettingsPanel and the manifest pass-through from ContentEditor.
  • RTL/Tailwind classes, Lingui wrapping, Kumo component use, and comment discipline against AGENTS.md.
  • Test coverage in ContentSettingsPanel.test.tsx and the new content-editor-panels.test.tsx.
  • Changeset quality and documentation in react-admin.mdx.
  • No changes touch server routes, SQL, content-table queries, or logged-out query counts.

Headline conclusion: The implementation is clean, the new tests and docs are appropriate, and the changeset is user-facing. No code defects were found. Once the Discussion checkbox is confirmed/ticked, this is ready to merge.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 22, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond needs-rebase and removed review/needs-rereview Author pushed changes since the last review labels Jul 22, 2026
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from c090a03 to 28139f7 Compare July 22, 2026 12:54
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond needs-rebase labels Jul 22, 2026
@github-actions github-actions Bot mentioned this pull request Jul 22, 2026
18 tasks
@logelog
logelog marked this pull request as draft July 23, 2026 16:17
@github-actions github-actions Bot removed the review/needs-rereview Author pushed changes since the last review label Jul 23, 2026
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from 28139f7 to 18cad00 Compare July 24, 2026 19:27
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from 18cad00 to 2ccfeaa Compare August 3, 2026 00:23
@logelog
logelog marked this pull request as ready for review August 3, 2026 00:24
@github-actions github-actions Bot added the review/needs-rereview Author pushed changes since the last review label Aug 3, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of PR #2187.

Approach: This remains the right, well-scoped extension for the problem in #2227. A trusted-plugin React export contract, host-owned sidebar sections, manifest lifecycle filtering, collection/role filtering, deterministic ordering, and render-error isolation all fit EmDash's architecture.

Status of prior findings:

  • ✅ The Kumo Button finding from the last review is resolved — ContentEditorPanelBoundary now imports Button from @cloudflare/kumo.
  • ⚠️ The process note remains open: the PR checklist item "New features link to an approved Discussion" is still unchecked. The description links to Discussion #2105, but per AGENTS.md a maintainer-approved Discussion is required before merging a feature.

What I checked:

  • Panel contract, runtime validation, and resolver behavior in packages/admin/src/lib/content-editor-panels.tsx.
  • Integration into ContentSettingsPanel and the manifest pass-through from ContentEditor.
  • Dynamic section ordering in SortableContentSettingsSections and content-settings-layout.ts.
  • Lingui wrapping, Kumo component use, RTL-safe Tailwind classes, and comment discipline against AGENTS.md.
  • Test coverage for filtering, ordering, deduplication, error containment, and ContentSettingsPanel integration.
  • Changeset quality and the react-admin.mdx documentation update.
  • No server routes, SQL, content-table queries, or logged-out query counts are affected.

Headline conclusion: The implementation is clean and the new tests/docs are appropriate. One minor code suggestion: the error-boundary Retry button currently only resets the error flag and does not remount the failed subtree, so deterministic render errors do not actually clear on retry. The fix is cheap (add a resetKey and key the children). Once that is addressed and the Discussion checkbox is confirmed/ticked, this is ready to merge.

Comment on lines +197 to +199
size="sm"
className="mt-1"
onClick={() => this.setState({ hasError: false })}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Clicking Retry only flips hasError back to false. React error boundaries unmount the failed subtree, so rendering the same children again without a changed key will usually recreate the same panel and hit the same deterministic render error, making the button a no-op for most failures.

Add a resetKey to the boundary state and use it to key the children so Retry actually remounts the panel:

Suggested change
size="sm"
className="mt-1"
onClick={() => this.setState({ hasError: false })}
interface ContentEditorPanelBoundaryState {
hasError: boolean;
resetKey: number;
}
/** Prevents one faulty trusted panel from unmounting the content editor. */
export class ContentEditorPanelBoundary extends React.Component<
ContentEditorPanelBoundaryProps,
ContentEditorPanelBoundaryState
> {
override state: ContentEditorPanelBoundaryState = { hasError: false, resetKey: 0 };
static getDerivedStateFromError(): ContentEditorPanelBoundaryState {
return { hasError: true, resetKey: 0 };
}
override componentDidCatch(error: Error, info: React.ErrorInfo): void {
console.error(
`Plugin "${this.props.pluginId}" failed while rendering content editor panel "${this.props.panelId}".`,
error,
info,
);
}
override render(): React.ReactNode {
if (!this.state.hasError) {
return (
<React.Fragment key={this.state.resetKey}>
{this.props.children}
</React.Fragment>
);
}
return (
<div role="alert" className="text-sm text-kumo-subtle">
<p>
<Trans>Plugin panel unavailable.</Trans>
</p>
<Button
type="button"
variant="ghost"
size="sm"
className="mt-1"
onClick={() =>
this.setState((prev) => ({
hasError: false,
resetKey: prev.resetKey + 1,
}))
}
>
<Trans>Retry</Trans>
</Button>
</div>
);
}
}

@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from 70ba489 to b160e7e Compare August 3, 2026 12:24
@logelog

logelog commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Added gives a panel that throws after mounting a fresh instance on Retry in ContentSettingsPanel.test.tsx.

It covers the gap behind the resetKey suggestion. The existing Retry test only had a panel failing on its first render, so it never mounted. The new one mounts, renders, then throws: on Retry the panel returns as a second instance with its own state reset, because React discards the subtree when the boundary catches. There is no retained instance left for a key to replace.

Both Retry tests fail if the button is made a no-op.

@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from 77f1ca6 to b160e7e Compare August 3, 2026 18:00
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from b160e7e to a9614d9 Compare August 7, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants