Skip to content

Editor: Preserve unsent note drafts when switching blocks - #80482

Closed
adamsilverstein wants to merge 13 commits into
trunkfrom
fix/preserve-note-drafts
Closed

adamsilverstein wants to merge 13 commits into
trunkfrom
fix/preserve-note-drafts

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jul 20, 2026 •

Copy link
Copy Markdown
Member

What?

Closes #73473.
Supersedes #75030 - rebuilt on top of the current notes architecture (floating notes, selectNote store state, useFocusOutside dismissal). Props @Mustafabharmal for the original approach.

Preserves unsent note drafts when focus moves away from a block, and caches one draft per block (plus one per reply thread) so multiple drafts can be kept at the same time, similar to Google Docs.

Why?

Starting a note and clicking another block silently discarded whatever was typed. As raised in this comment, a fix should also cache multiple drafts - one per block - rather than only the most recent one. (The other repro from #73473, losing the draft on window/tab switch, was already fixed on trunk by the useFocusOutside migration in #80222, which ignores window blur.)

How?

  • New noteDrafts state in the editor store (private setNoteDraft action, getNoteDraft selector): a map of draft key → content. New-note drafts are keyed per block (new-note-${clientId}), reply drafts per thread (reply-${noteId}), so every form caches its own draft independently.
  • NoteForm gains an optional draftKey prop: it restores the stashed draft on mount, stashes edits as you type, clears the draft on successful submit, and treats Cancel/Escape as an explicit discard. Dismissing the form implicitly (focus-out) keeps the draft.
  • Auto-reopen: when selection moves to a block that has no thread but carries an unsent draft, the block-transition logic in notes.js reopens the new-note form so the draft surfaces again, like a saved thread would. Stored drafts also keep the floating sidebar enabled (new hasNoteDrafts selector) so that transition logic stays mounted while a draft is waiting.
  • AddNote race fix: switching selection directly from an open form on one draft-carrying block to another produces a 'new' → 'new' note-selection transition, which the deferred useFocusOutside blur callback couldn't distinguish from a plain dismissal - it would instantly close the reopened form. The form now records which block owns focus at focus time and skips the deferred dismissal when block selection has moved on, leaving the decision to the block-transition logic. NoteForm is also keyed by clientId so each block's form remounts with its own draft.

Testing Instructions

Test in WordPress Playground

  1. Add two paragraph blocks.
  2. Start a note on the first block (Options menu → Add note) and type something without submitting.
  3. Select the second block - the form closes.
  4. Reselect the first block - the form reopens with your draft.
  5. Start a draft on the second block too, then switch between the blocks - each shows its own draft.
  6. Click Cancel on a draft - only that draft is discarded; reopening the form starts empty.
  7. Submit a draft - it posts as a normal note and the draft is cleared.
  8. Select an existing note thread, type a reply without submitting, click another block, then return - the reply draft is restored.

The new behavior is covered by a Draft persistence group in test/e2e/specs/editor/various/block-notes.spec.js and unit tests for the noteDrafts reducer.

Starting a note and selecting another block silently discarded the
draft. Stash unsent note content in a new noteDrafts map in the editor
store, keyed per block for new notes and per thread for replies, so
every form caches its own draft and multiple drafts can coexist.

Reopen the new note form when selection returns to a block with a
stashed draft, keep the floating sidebar mounted while drafts exist so
that transition logic stays alive, and skip the deferred focus-outside
dismissal when block selection has already moved on so it cannot close
a form the transition logic just reopened. Cancel and successful
submission discard the draft; implicit dismissal keeps it.

Claude-Session: https://claude.ai/code/session_01QenttpdKEur99wPv3D7iUy
@adamsilverstein adamsilverstein added the [Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting label Jul 20, 2026
@github-actions github-actions Bot added the [Package] Editor /packages/editor label Jul 20, 2026
@adamsilverstein adamsilverstein added the [Type] Bug An existing feature does not function as intended label Jul 20, 2026
@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Size Change: +312 B (0%)

Total Size: 7.91 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 581 kB +312 B (+0.05%)

compressed-size-action

@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Flaky tests detected in a3deb5c.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/31743411578
📝 Reported tests:

adds and activates a new tab when pressing Enter at the end of a tab label in /test/e2e/specs/editor/blocks/tabs.spec.js, passed after 2 failed attempts.
Error: expect(locator).toHaveCount(expected) failed

Locator:  locator('[name="editor-canvas"]').contentFrame().getByRole('tab')
Expected: 3
Received: 2
Timeout:  5000ms

Call log:
  - Expect "toHaveCount" with timeout 5000ms
  - waiting for locator('[name="editor-canvas"]').contentFrame().getByRole('tab')
    14 × locator resolved to 2 elements
       - unexpected value "2"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/tabs.spec.js:146:25
Error: expect(locator).toHaveCount(expected) failed

Locator:  locator('[name="editor-canvas"]').contentFrame().getByRole('tab')
Expected: 3
Received: 2
Timeout:  5000ms

Call log:
  - Expect "toHaveCount" with timeout 5000ms
  - waiting for locator('[name="editor-canvas"]').contentFrame().getByRole('tab')
    14 × locator resolved to 2 elements
       - unexpected value "2"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/tabs.spec.js:146:25

@noruzzamans

Copy link
Copy Markdown
Contributor

Tested! Confirms the fix works as expected. Draft note content is preserved when switching between blocks; returning to a block with an unsent draft reopens the form with the draft intact.

One thing I noticed: selecting a note shows a dimming overlay on the rest of the canvas. Not sure if this is pre-existing behavior unrelated to this PR, flagging in case it's useful.

Screen.Recording.2026-07-24.at.7.45.09.AM.mov

@adamsilverstein

Copy link
Copy Markdown
Member Author

Thanks for testing and capturing the screencast @noruzzamans - very helpful!

…afts

# Conflicts:
#	packages/editor/src/components/collab-sidebar/note-form.js
@Mamaduka

Mamaduka commented Aug 6, 2026

Copy link
Copy Markdown
Member

Let's punt this to 7.2.

@t-hamano t-hamano moved this from 🔎 Needs Review to 🦵 Punted to 7.2 in WordPress 7.1 Editor Tasks Aug 10, 2026
…afts

# Conflicts:
#	packages/editor/src/components/collab-sidebar/note-form.js
Trunk removed note-form.js's dataviews private-API usage and with it the
lock-unlock import, but this branch still unlocks the editor store to reach
the private setNoteDraft action and getNoteDraft selector.
The template registration tests reach for the "Template options" button
without ensuring the document settings sidebar is open, so they depend on
whatever sidebar state an earlier spec in the same shard happened to leave
behind. When shard packing puts a spec that closes the sidebar ahead of
this one, the button is never rendered and the tests time out.

Open the sidebar explicitly via the existing idempotent helper.
adamsilverstein and others added 3 commits August 13, 2026 13:57
Pick up three weeks of trunk so the branch builds and lints against the
current toolchain (Prettier 3.9.6, the Jest jsdom test split, and the new
changelog validator) and to clear the failing "Required changes from
trunk" check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XymJGmRuNKwf8srrtsrcLR
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: noruzzamans <noruzzaman@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: yashjawale <yashjawale@git.wordpress.org>
Co-authored-by: desrosj <desrosj@git.wordpress.org>
Co-authored-by: Mustafabharmal <mustafabharmal@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: +354 B (0%)

Total Size: 8.25 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 616 kB +354 B (+0.06%)

6401107 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 0ab17ee trunk % Change
timeToFirstByte 39.8 ms +4.77% -3.39% 41.55 ms +9.99% -5.42% -4.21%
largestContentfulPaint 66 ms +9.09% -3.03% 72 ms +0% -5.56% -8.33%
lcpMinusTtfb 26.4 ms +4.36% -7.58% 27.15 ms +3.87% -4.79% -2.76%
wpBeforeTemplate 19.43 ms +9.11% -3.29% 19.99 ms +10.26% -5.7% -2.8%
wpTemplate 17.35 ms +4.9% -4.03% 18.14 ms +4.36% -6.34% -4.36%
wpTotal 36.73 ms +5.01% -3.27% 38.29 ms +10.16% -5.98% -4.07%
wpMemoryUsage 7.62 MB +0% -0% 7.59 MB +0% -0% 0.46%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric 0ab17ee trunk % Change
timeToFirstByte 33.6 ms +4.46% -4.46% 34.1 ms +1.47% -2.05% -1.47%
largestContentfulPaint 72 ms +8.33% -0% 76 ms +0% -0% -5.26%
lcpMinusTtfb 40.3 ms +5.09% -3.85% 41.9 ms +2.51% -1.19% -3.82%
wpBeforeTemplate 17.2 ms +6.74% -2.15% 17.45 ms +3.9% -2.46% -1.43%
wpTemplate 13.59 ms +4.56% -6.62% 13.52 ms +4.81% -5.77% 0.52%
wpTotal 30.91 ms +4.66% -4.24% 31.34 ms +1.08% -2.71% -1.37%
wpMemoryUsage 6.25 MB +0% -0% 6.20 MB +0% -0% 0.71%
wpDbQueries 14 +0% -0% 14 +0% -0% 0%

media-processing

Metric 0ab17ee trunk % Change
mediaProcessingJpeg 287.22 ms +1.22% -0.78% 294.62 ms +2.37% -1.21% -2.51%
mediaProcessingAvif 4140.01 ms +6.08% -0.3% 4211.04 ms +3.54% -1.3% -1.69%
mediaProcessingJpegToAvif 2838.81 ms +0.51% -0.63% 2883.32 ms +0.86% -2.33% -1.54%

media-upload

Metric 0ab17ee trunk % Change
jpegUploadProcessing 1402.05 ms +1.61% -0.23% 1402.07 ms +0.45% -0.88% 0%
pngUploadProcessing 212.98 ms +13.91% -14.99% 175.99 ms +3.97% -10.86% 21.02%
largeJpegUploadProcessing 1410.92 ms +0.41% -0.94% 1392.64 ms +1.86% -0.62% 1.31%
multipleImageUploadProcessing 1454.76 ms +0.44% -0.41% 1470.16 ms +1.34% -0.88% -1.05%

post-editor

Metric 0ab17ee trunk % Change
serverResponse 311.87 ms +4.79% -2.07% 313.47 ms +1.43% -0.57% -0.51%
firstPaint 181.23 ms +26.14% -24.22% 190.09 ms +43.85% -5.42% -4.66%
domContentLoaded 797.46 ms +5.38% -4.48% 770.1 ms +5.06% -0.95% 3.55%
loaded 798.53 ms +5.38% -4.49% 771.13 ms +5.08% -0.9% 3.55%
firstContentfulPaint 336.92 ms +8.37% -0.15% 333.18 ms +0.91% -2.14% 1.12%
firstBlock 2294.46 ms +1.52% -0.59% 2278.82 ms +1.42% -1.82% 0.69%
type 16.9 ms +3.55% -5.68% 16.29 ms +6.88% -2.03% 3.74%
typeWithoutInspector 16.24 ms +2.16% -5.05% 16.55 ms +1.39% -3.93% -1.87%
typeWithTopToolbar 21.17 ms +4.82% -7.09% 20.47 ms +5.37% -1.81% 3.42%
typeContainer 7.81 ms +3.97% -4.61% 7.39 ms +2.71% -5.41% 5.68%
focus 59.47 ms +9.62% -6% 56.63 ms +13.97% -8.67% 5.02%
firstFocus 158.21 ms +0% -0% 140.07 ms +0% -0% 12.95%
selectAll 333.62 ms +5.79% -2.64% 366.37 ms +2.06% -0.57% -8.94%
listViewOpen 45.66 ms +6.72% -5.21% 48.14 ms +5.88% -13.52% -5.15%
inserterOpen 18.66 ms +22.51% -7.13% 19.64 ms +9.32% -6.92% -4.99%
inserterHover 2.06 ms +10.19% -10.68% 2.13 ms +8.45% -6.1% -3.29%
inserterSearch 7.64 ms +9.29% -5.76% 8.03 ms +5.85% -3.49% -4.86%
loadPatterns 531.89 ms +7.74% -6.48% 504.72 ms +3.14% -5.05% 5.38%
wpTotal 303.44 ms +5.15% -1.92% 305.16 ms +1.37% -0.38% -0.56%
wpMemoryUsage 13.17 MB +0% -0% 13.13 MB +0% -0% 0.28%
wpDbQueries 54 +0% -0% 54 +0% -0% 0%

site-editor

Metric 0ab17ee trunk % Change
serverResponse 502.9 ms +5.33% -5.86% 484.18 ms +4.96% -1.11% 3.87%
firstPaint 267.22 ms +12.5% -7.62% 254.5 ms +5.76% -5.36% 5%
domContentLoaded 1182.16 ms +1.88% -2.93% 1134.59 ms +1.53% -0.08% 4.19%
loaded 1183.38 ms +1.88% -2.94% 1135.82 ms +1.54% -0.09% 4.19%
firstContentfulPaint 469.47 ms +5.67% -4.51% 455.31 ms +2.7% -1.61% 3.11%
firstBlock 4249.8 ms +0.74% -0.74% 4143.8 ms +0.67% -0.47% 2.56%
type 18.69 ms +6.1% -7.33% 18.56 ms +3.61% -6.3% 0.7%
navigate 107.06 ms +14.22% -9.89% 107.36 ms +4.71% -1.16% -0.28%
loadPatterns 1392.19 ms +8.39% -3.14% 1373.75 ms +6.56% -8.86% 1.34%
loadPages 1047.26 ms +1.31% -0.81% 1071.11 ms +1.43% -0.71% -2.23%
wpTotal 493.16 ms +5.52% -6.07% 474.21 ms +5.06% -1.05% 4%
wpMemoryUsage 12.14 MB +0% -0% 12.09 MB +0% -0% 0.41%
wpDbQueries 43 +2.33% -0% 43.5 +1.15% -1.15% -1.15%

6401107 Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 2
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=254"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/254", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/252", "id": 252, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-254", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-01T16:01:52", "date_gmt": "2026-10-01T16:01:52", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png"}, "id": 254, "link": "http://localhost:8889/?attachment_id=254", "media_details": {"file": "2026/10/10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-01T16:01:52", "modified_gmt": "2026-10-01T16:01:52", "ping_status": "closed", "post": 252, "slug": "10x10_e2e_test_image_z9t8jk-2", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK-1.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}, {"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=253"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/253", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/252", "id": 252, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-253", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-01T16:01:51", "date_gmt": "2026-10-01T16:01:51", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png"}, "id": 253, "link": "http://localhost:8889/?attachment_id=253", "media_details": {"file": "2026/10/10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-01T16:01:51", "modified_gmt": "2026-10-01T16:01:51", "ping_status": "closed", "post": 252, "slug": "10x10_e2e_test_image_z9t8jk", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/10x10_e2e_test_image_z9T8jK.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45

6401107 Run

@yashjawale

Copy link
Copy Markdown
Contributor

Tested & can confirm that the patch still fixes the issue. Same behavior as described by #80482 (comment)
The merge conflicts appear pretty minimal too.

Screen.Recording.2026-09-30.at.5.19.04.PM.mov

…afts

# Conflicts:
#	packages/editor/src/components/collab-sidebar/index.jsx
#	packages/editor/src/components/collab-sidebar/notes.jsx
@adamsilverstein

Copy link
Copy Markdown
Member Author

Closing in favor of #84110. Thanks for the testing here.

@Mamaduka
Mamaduka deleted the fix/preserve-note-drafts branch October 6, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting [Package] Editor /packages/editor [Type] Bug An existing feature does not function as intended

Projects

Development

Successfully merging this pull request may close these issues.

Drafted notes are lost when focus is removed from block

5 participants