Skip to content

Phase 3: Add suggestion diff preview, Apply, and Reject#77405

Open
adamsilverstein wants to merge 13 commits into
suggest-mode-phase-2from
suggest-mode-phase-3
Open

Phase 3: Add suggestion diff preview, Apply, and Reject#77405
adamsilverstein wants to merge 13 commits into
suggest-mode-phase-2from
suggest-mode-phase-3

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Apr 16, 2026

Copy link
Copy Markdown
Member

Overview

This is one of 5 stacked PRs implementing Suggest mode for the WordPress editor — a Google Docs–style workflow where reviewers can propose changes that the post author can Accept or Reject. Tracked in #73411, with design direction from #73410 and jasmussen's mockups.

This PR (Phase 3)

Adds the persistence layer and review UI:

  • _wp_suggestion comment meta — registered on note comments as a JSON payload (schemaVersion, blockName, baseRevision, operations). Payload size capped at 64 KB via a sanitize_callback.
  • _wp_suggestion_status comment meta — lifecycle flag (pending / applied / rejected) with a REST-exposed enum.
  • SuggestionsProvider interface (useSuggestionsProvider()): createSuggestion, updateSuggestion, deleteSuggestion, applySuggestion, rejectSuggestion. Implementations can swap in without touching the UI — the Yjs-backed provider drops in with the same methods.
  • applyOperations — pure function that applies an ordered list of attribute-set operations to a block's current attributes.
  • SuggestionDiff — word-level LCS diff rendered in the notes sidebar with green underlined insertions and red strikethrough deletions. Falls back to a compact before→after label for non-text attributes.
  • Accept / Reject icon buttons — checkmark and close icons slotted into the note header, right-aligned with the author info. The generic "Resolve" button is hidden on suggestion threads to avoid a duplicate checkmark affordance.

What's NOT in this PR

  • The Docs-style Add/Delete/Formatting summary (Phase 4) — this PR still renders the full word diff.
  • Per-attribute conflict scoping (Phase 4) — this PR flags staleness on every post-level revision bump, which is noisy once auto-save arrives in Phase 5.
  • Auto-save (Phase 5) — this PR expects an explicit trigger.

Phase 3 test plan

  • Suggest mode; edit a paragraph; trigger the provider's createSuggestion → confirm a POST /wp/v2/comments with type=note and a _wp_suggestion meta JSON body
  • Open the notes sidebar; confirm the word-level diff preview renders for a text change (green insertions, red strikethrough)
  • Click Accept (check icon) → confirm the block updates and _wp_suggestion_status is set to applied
  • Click Reject (close icon) → confirm the block stays unchanged and _wp_suggestion_status is set to rejected
  • Confirm the generic "Resolve" button no longer appears on a suggestion thread
  • Unit: npm run test:unit -- packages/editor/src/components/suggestion-mode/test/provider.js
  • Unit: npm run test:unit -- packages/editor/src/components/suggestion-mode/test/suggestion-diff.js

🗺️ PR Stack Navigation

# PR Phase
1 #77403 Intent scaffolding Edit / Suggest / View mode
2 #77404 Overlay capture In-memory suggestion overlay
3 #77405 Provider + Accept/Reject ← this PR _wp_suggestion meta, provider, sidebar actions
4 #77406 Summary + docs + attribute tests Add/Delete/Formatting summary, architecture stub, conflict scoping
5a #78351 REST permissions and PHP coverage Permissions, payload cap, PHP tests
5b #78352 Summary + attribute conflict + docs Renderer, per-attribute staleness, architecture docs
5c #78353 Surface Apply/Reject in the collaboration sidebar Icon buttons + e2e + sidebar wiring
6 #78308 Auto-save subsystem Background debounced save (replaces commit-bar)

📋 Tracking issue: #73411

@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown

Size Change: +2.19 kB (+0.03%)

Total Size: 7.51 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 480 kB +2.19 kB (+0.46%)

compressed-size-action

@adamsilverstein adamsilverstein force-pushed the suggest-mode-phase-3 branch 2 times, most recently from 7e2fe59 to 9debb13 Compare April 16, 2026 18:49
@adamsilverstein adamsilverstein force-pushed the suggest-mode-phase-3 branch 2 times, most recently from f63fe8c to 66f2542 Compare April 16, 2026 19:06
@adamsilverstein adamsilverstein force-pushed the suggest-mode-phase-2 branch 2 times, most recently from b1af20a to 6ae1fa6 Compare April 16, 2026 21:57
@github-actions

github-actions Bot commented Apr 17, 2026

Copy link
Copy Markdown

Flaky tests detected in e4204b1.
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/27779774618
📝 Reported issues:

@adamsilverstein adamsilverstein marked this pull request as ready for review April 29, 2026 16:08
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

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>

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

@adamsilverstein adamsilverstein added [Status] In Progress Tracking issues with work in progress [Type] Feature New feature to highlight in changelogs. [Feature] Client Side Media Media processing in the browser with WASM [Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting and removed [Feature] Client Side Media Media processing in the browser with WASM labels Apr 29, 2026
@adamsilverstein adamsilverstein added Backport from WordPress Core Pull request that needs to be backported to a Gutenberg release from WordPress Core [Type] Enhancement A suggestion for improvement. and removed [Status] In Progress Tracking issues with work in progress [Package] Editor /packages/editor [Type] Feature New feature to highlight in changelogs. labels May 18, 2026
@github-actions github-actions Bot added the [Package] Editor /packages/editor label Jun 17, 2026
Phase 3 of the Suggest mode effort. Implements the Apply/Reject
lifecycle and inline diff preview for suggestion notes.

New in provider.js:
- applySuggestion: applies operations to block attributes via
  updateBlockAttributes, marks note resolved with status 'applied'.
  Emits a warning snackbar when baseRevision differs from current
  post modified date (stale suggestion).
- rejectSuggestion: marks note resolved with status 'rejected'
  without changing content.
- applyOperations: pure function running attribute-set ops against
  current block attributes.
- parseSuggestionPayload: safe JSON parser for _wp_suggestion meta.

New SuggestionDiff component:
- Word-level LCS-based diff for text attributes (insertions green
  underline, deletions red strikethrough).
- Attribute label for non-text changes.

UI integration in collab-sidebar/comments.js:
- SuggestionActions component rendered inside CommentBoard when
  thread has _wp_suggestion meta. Shows diff + Apply/Reject buttons
  for pending suggestions, or Applied/Rejected label for resolved.

Tests: applyOperations, parseSuggestionPayload, wordDiff (25 total).

Refs #73411
- applySuggestion's catch path now restores exactly the keys the apply
  touched, with their original values (or undefined when the key was
  added by this apply). updateBlockAttributes is a partial merge —
  passing currentAttributes alone left newly-added keys stuck on the
  block at their applied value when the server rejected the status
  update.
- MAX_DIFF_LENGTH dropped from 5000 → 2000. Two 2KB strings already
  produce a 4M-cell DP table; 5000² is too costly for an interactive
  sidebar render.
- Add a wordDiff segmentation test covering the in-place replacement
  case.
The merge of trunk's Notes refactor (#77614) into phase-3 ported a
SuggestionActionButtons reference into the new note.js, but that named
export does not exist on phase-3 — only `SuggestionActions` (default)
ships in this phase. Header-slot icon buttons are a downstream
refinement and will arrive in a later phase.

Removes the unused named import and the header-slot usage so the
package build no longer fails with 'No matching export'.
Trunk's new `@wordpress/use-recommended-components` lint rule flags
`__experimentalHStack`/`__experimentalVStack` and (via
`@wordpress/use-import-as`) requires the `__experimentalText`
import to use the `WCText` alias.

Switch HStack/VStack/VisuallyHidden to the `@wordpress/ui` Stack and
VisuallyHidden, rename `Text` → `WCText`, and add the two
suggestion files to the lint suppressions baseline alongside
existing entries (e.g. pagination/index.js) so the deferred
`Text → @wordpress/ui` migration does not block this PR.
…estion

In Suggest mode, `applySuggestion` calls `updateBlockAttributes` to
write the proposed attribute values onto the live block. The store
interceptor was reverting that dispatch the same way it reverts a
user edit, so the comment status flipped to "applied" but the block
content never actually changed. Reject worked because it only saves
the comment status.

Add a ref-based bypass set on the overlay context that the apply
flow can opt into. The interceptor checks the set before diffing,
adopts the current attributes as its new snapshot baseline, and
skips the revert. Clearing the overlay entry resets the per-block
suggestion tracking so a subsequent edit re-baselines from the
post-apply attributes. Outside Suggest mode the interceptor isn't
running and the calls are no-ops.
…tData diffs are detected

Two equality helpers — `isAttributeEqual` in provider.js and
`shallowAttributeEquals` in store-interceptor.js — short-circuited
on `RichTextData` (and similar wrapper) values. RichTextData holds
its content in private class fields, so `Object.keys()` returns an
empty array and the structural compare was vacuously equal regardless
of the underlying text or formatting. The string/object branch also
returned false outright when one side was a plain string (the
JSON-deserialized 'before' from a stored suggestion) and the other a
RichTextData instance from the live store, so content-attribute
diffs registered as "changed" even when nothing had moved.

Fix: when both sides have zero enumerable keys, or when only one
side is an object, fall back to `String(a) === String(b)`. This
preserves equality across the wrapper/string boundary and across
RichTextData instances, so content suggestions are detected and
applied correctly.
…terceptor

In collaborative editing, an applied suggestion lands on the live block
on the accepter's tab and propagates through the sync layer to the
suggester. Without a complement to the accepter-side interceptor bypass,
the suggester's store interceptor sees the incoming attributes as a
drift from its snapshot, reverts them back to baseline, and that revert
round-trips back through sync — undoing the apply on the accepter's
screen a moment after they clicked.

Detect the case explicitly: when every changed attribute on a block
linked to one or more notes matches the after value declared in those
notes' suggestion payloads, adopt the change as the new baseline rather
than reverting. The check is purely local (no comment-status watcher,
no race with the apply dispatch) and only fires when the live store
exactly matches a known suggestion payload.

Tighten shallowAttributeEquals to compare across the wrapper/primitive
boundary so a JSON-decoded after string and a live RichTextData wrapper
read as equal — without it, content suggestions never match and the new
branch never fires.

The suppression entry mirrors the pattern from the broader react-hooks
sweep on add-suggestion-mode and covers four pre-existing ref-write
violations in this file.
…gestionDiff

Document the public action arguments and surface the suggestion-diff
component's input shape, edge cases (MAX_DIFF_LENGTH fallback, wrapper
values), and rendering semantics so reviewers don't have to trace the
helper functions to understand the contract.
…base

The react-hooks/refs ref-during-render pattern in store-interceptor.js now
has four occurrences; bump the suppression count to match so lint:js passes.
Remove 17 stale bulk-suppression entries for files that are byte-identical
to trunk (which carries no suppression for them), so the suppressed
violations no longer occur. Resolves the failing "All" lint job which
reported leftover suppressions via --prune-suppressions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backport from WordPress Core Pull request that needs to be backported to a Gutenberg release from WordPress Core [Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting [Package] Editor /packages/editor [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant