Skip to content

fix: auto-save not detecting plugin block field changes#1119

Open
adentdk wants to merge 5 commits into
emdash-cms:mainfrom
adentdk:fix/autosave-block-fields
Open

fix: auto-save not detecting plugin block field changes#1119
adentdk wants to merge 5 commits into
emdash-cms:mainfrom
adentdk:fix/autosave-block-fields

Conversation

@adentdk
Copy link
Copy Markdown

@adentdk adentdk commented May 20, 2026

What does this PR do?

Fixes auto-save not detecting changes when editing plugin block fields via the Block Kit modal. When a user edits an existing block's attributes through the modal, the change was silently lost unless they also made a separate text edit.

Closes #1118

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: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.6 via OpenCode

Root Cause

When editing an existing plugin block's attributes through the Block Kit modal, handlePluginBlockInsert in PortableTextEditor.tsx used editor.view.dispatch(tr) with setNodeMarkup to update the node. For atom nodes (atom: true) with attribute-only changes, this bypasses TipTap's command pipeline and does not reliably trigger the onUpdate callback. As a result:

  1. PortableTextEditor's onChange never fires
  2. ContentEditor's formData never updates
  3. serializeEditorState() produces the same string as lastSavedData
  4. Auto-save's isDirty stays false
  5. The change is silently lost

Fix

Replace editor.view.dispatch(tr) with editor.chain().command(({ tr }) => { ... }).run(). The chain API goes through TipTap's full command pipeline, which guarantees onUpdate fires and propagates the change through onChangehandleFieldChangesetFormData → auto-save dirty detection.

How to test

  1. Run demos/simple with a plugin that has Block Kit fields (e.g., forms plugin)
  2. Add a plugin block to a portable text field
  3. Click the edit (pencil) icon on the block
  4. Change field values in the Block Kit modal and save
  5. Before fix: Auto-save does not trigger; navigating away loses the change
  6. After fix: Auto-save triggers within 2 seconds; change is persisted

Screenshots / test output

N/A — behavioral fix, no visual change.

When editing plugin block attributes via the Block Kit modal, the
previous code used raw editor.view.dispatch(tr) with setNodeMarkup.
For atom nodes with attribute-only changes, this may not reliably
trigger TipTap's onUpdate callback, which means the PortableTextEditor
onChange never fires and the parent ContentEditor never sees the
change — breaking auto-save dirty detection.

Switch to editor.chain().command() which goes through TipTap's
command pipeline and guarantees onUpdate fires, propagating the
change to formData and triggering auto-save.

Fixes emdash-cms#1118
Copilot AI review requested due to automatic review settings May 20, 2026 14:52
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

🦋 Changeset detected

Latest commit: 3d78cb1

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

This PR includes changesets to release 14 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@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

github-actions Bot commented May 20, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an auto-save reliability issue in the admin Portable Text editor where edits to existing plugin block attributes (via the Block Kit modal) were not consistently detected as document updates, preventing dirty-state tracking and auto-save from triggering.

Changes:

  • Switch plugin block attribute updates from editor.view.dispatch(tr) to a TipTap editor.chain().command(...).run() transaction path.
  • Ensure attribute-only updates on pluginBlock atom nodes propagate through TipTap’s update pipeline so onChange can fire reliably.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2256 to +2260
// Editing an existing block — update its attributes in place.
// Use the chain API so TipTap's onUpdate fires reliably
// (raw view.dispatch may not trigger onUpdate for attribute-only
// changes on atom nodes in some TipTap versions).
editor
@adentdk
Copy link
Copy Markdown
Author

adentdk commented May 20, 2026

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request May 20, 2026
@ascorbic
Copy link
Copy Markdown
Collaborator

/review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

emdash

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

create-emdash

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

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

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 3d78cb1

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Now I have a clear picture. Let me assess this:

Review assessment:

  1. Root cause is plausible. TipTap's onUpdate is invoked via the Editor.dispatchTransaction wrapper, not via view.dispatch directly. When the editor's internal dispatchTransaction is the view's dispatchTransaction prop, view.dispatch(tr) should still flow through it — but in some TipTap versions/configurations, attribute-only setNodeMarkup changes on atom nodes are dropped or do not fire onUpdate. The author's fix (chain API) is a well-known correct workaround.

  2. The fix is correct and minimal. Using editor.chain().command(...).run() routes through TipTap's command manager, which dispatches through editor.view.dispatch but more importantly returns updated state correctly and ensures onUpdate fires. The same pattern is used elsewhere in the file (e.g. line 2230, line 2278).

  3. Closure of editPos. The editPos value is captured at the start of handlePluginBlockInsert. The command(({ tr }) => ...) runs synchronously inside .run(), so this is fine.

  4. editingBlockPosRef.current = null is set after the chain call regardless of outcome. The chain's .run() is synchronous, so the ordering is fine.

  5. No tests added. Copilot already flagged this. Worth seconding given the regression involves a subtle TipTap behavior.

  6. Minor nit: The original code returned silently if the node wasn't a pluginBlock. The new code returns false from the command, which causes the chain to fail — same effective behavior (no transaction). OK.

  7. Behavior parity: Both branches now use .chain().run(), which is a stylistic improvement (consistency).

The fix is small, targeted, correctly diagnosed, and uses a known-safe pattern. The main weakness is the missing regression test, already raised by Copilot. I won't pile on with a duplicate inline comment — I'll skip posting a review.

LGTM!

github run

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.

Auto-save not triggered when editing Portable Text block fields

3 participants