Skip to content

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

Open
logelog wants to merge 6 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 6 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: #2421

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 plugin panels for the content editor sidebar #2421 (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: ceb2e5d

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: ceb2e5d

@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
@github-actions github-actions Bot removed the review/needs-rereview Author pushed changes since the last review label Aug 10, 2026
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from 60deb9c to 5a3a1ec Compare August 10, 2026 09:50
@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 labels Aug 10, 2026
@logelog logelog closed this Aug 10, 2026
@logelog logelog reopened this Aug 10, 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 summary

This PR continues to implement the right extension point for #2227: a trusted-plugin React export contract, EmDash-owned sidebar sections/ordering, manifest lifecycle filtering, deterministic tie-breaking, and per-panel error boundaries all fit the admin architecture. The code I re-read is clean and I found no new blocking defects.

Status of prior findings:

  • ✅ The Kumo Button import in ContentEditorPanelBoundary is correct.
  • ✅ The Retry/remount concern is resolved; the integration test demonstrates that a transient panel failure recovers via a boundary remount on Retry, and a deterministic render error would re-trip regardless of any internal key.
  • ⚠️ The feature still lacks an approved Discussion. The PR body still lists Discussion #2105 as “awaiting maintainer review” and the corresponding checklist item is unchecked. Per the project process for features, maintainer approval of that Discussion must be confirmed before merge. I cannot verify GitHub state from the repo, so please confirm the Discussion is approved before merging.

What I checked:

  • The contentEditorPanels resolver (packages/admin/src/lib/content-editor-panels.tsx) — validation, collection/role filtering, manifest enablement checks, deterministic ordering, and swallowed-predicate error handling all look correct.
  • Integration into ContentSettingsPanel/ContentEditormanifest pass-through, per-panel error boundary keyed on content identity, and rendering only for saved entries.
  • Sortable section support for dynamic plugin sections via content-settings-layout.ts and SortableContentSettingsSections.
  • Lingui wrapping for host-rendered strings (Plugin panel unavailable., Retry), RTL-safe end-/inlineSize classes, and comment discipline on changed hunks.
  • New resolver + integration tests covering filtering, ordering, lifecycle, disabled plugins, error isolation, and remount behavior.
  • Changeset and docs update.
  • No server routes, SQL, content-table queries, or logged-out hot-path queries are affected.

I did not run pnpm test/lint/typecheck — I reviewed statically as instructed.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Aug 10, 2026
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from 5a3a1ec to dcc7c14 Compare August 10, 2026 17:29
@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 labels Aug 10, 2026
@logelog logelog closed this Aug 10, 2026
@logelog logelog reopened this Aug 10, 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 summary

This PR continues the right extension point for #2227. The design—trusted React plugin exports, EmDash-owned sidebar sections/ordering, manifest lifecycle filtering, deterministic tie-breaking, and per-panel error boundaries—fits the admin architecture. The resolver tests and the new ContentSettingsPanel integration tests cover filtering, ordering, disabled plugins, error isolation, and content-identity remounts. No new server routes, SQL, content-table queries, or logged-out hot-path queries are involved.

Status of prior findings:

  • ✅ The Kumo Button import in ContentEditorPanelBoundary is correct.
  • ✅ Retry/remount behavior is resolved; the integration test demonstrates that a transient failure recovers via boundary remount on Retry, and a deterministic error re-trips cleanly.
  • ⚠️ The approved Discussion requirement is still unverified. The PR body still lists Discussion #2105 as “awaiting maintainer review” and the corresponding checklist item is unchecked. Per the project process for features, maintainer approval of that Discussion must be confirmed before merge. I cannot verify GitHub state from the repo, so please confirm it before merging.

New issue this pass:

  • The host renders the plugin-provided panel title as a raw string (extension.title). Every user-facing admin string must go through Lingui so a plugin catalog can localize it. The existing Sidebar.tsx plugin page labels already use the same i18n._(id) pattern.

I did not run pnpm test/lint/typecheck—this review is static as instructed.

@@ -385,6 +393,19 @@ export const ContentSettingsPanel = React.memo(function ContentSettingsPanel({
}: ContentSettingsPanelProps) {
const { t } = useLingui();

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.

[needs fixing] The panel section heading and drag-handle label are plugin-provided, user-facing strings, but this component only destructures t from useLingui(). To follow the admin localization convention (see Sidebar.tsx plugin page labels), also expose i18n so extension.title can be passed through i18n._() before rendering.

Suggested change
const { t } = useLingui();
const { t, i18n } = useLingui();

Comment on lines +717 to +721
label={extension.title}
>
<div className="min-w-0 p-4">
<Text bold as="h3" DANGEROUS_className="mb-4">
{extension.title}

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.

[needs fixing] The plugin-supplied title is rendered raw in both the sortable section label and the section heading. Because the host owns these strings, they should be run through Lingui so a plugin that loads its catalog into the shared i18n instance gets translated headings (mirroring how plugin page labels are handled).

Suggested change
label={extension.title}
>
<div className="min-w-0 p-4">
<Text bold as="h3" DANGEROUS_className="mb-4">
{extension.title}
const title = i18n._({ id: extension.title, message: extension.title });
return (
<SortableContentSettingsSection
key={sectionId}
id={sectionId}
label={title}
>
<div className="min-w-0 p-4">
<Text bold as="h3" DANGEROUS_className="mb-4">
{title}
</Text>

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Aug 10, 2026
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from dcc7c14 to d8cbce8 Compare August 11, 2026 02:49
@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 labels Aug 11, 2026
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch 3 times, most recently from e4b17ce to 07d0434 Compare August 12, 2026 14:16
The boundary kept its error state across entries, so a panel that threw on
one entry stayed replaced by the fallback after navigating to another. Keying
it on collection and entry id rebuilds it with the content it renders.

Retry already recovers: React unmounts the subtree when the boundary catches,
so clearing the flag mounts the panel fresh. The added test asserts that
mount count rather than assuming it.
@logelog
logelog force-pushed the feat/plugin-content-editor-sections branch from 07d0434 to ceb2e5d Compare August 12, 2026 17:29
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