Repository navigation
Suggest mode 1/9: editor intent (edit/suggest/view) and experiment gate - #80427
adamsilverstein wants to merge 72 commits into
Conversation
Adds the session-scoped editor intent state (private setEditorIntent action / getEditorIntent selector), the Edit / Suggest / View switcher in the editor options menu, Google Docs convention keyboard shortcuts, and a read-only view intent via isPreviewMode. The Suggestion Mode experiment flag and the gate helpers (isSuggestionModeEnabled / useCanSuggest) land here so every later layer gates on the same predicate. No suggestion capture or storage yet.
|
Size Change: +1.92 kB (+0.02%) Total Size: 7.91 MB 📦 View Changed
|
|
Flaky tests detected in 5df3684. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/33116177758 refuses the drop and uploads nothing in
|
# Conflicts: # packages/editor/src/components/global-keyboard-shortcuts/index.js # packages/editor/src/store/private-selectors.js
# Conflicts: # packages/editor/CHANGELOG.md # packages/editor/src/store/private-actions.js # packages/editor/src/store/private-selectors.js
…spec The new @wordpress/dependency-group rule runs in 'never' mode, so the header comment left in this spec still failed lint after the last pass.
The guard skipped the mode snackbar and a11y announcement when the previous intent was undefined, but the reducer defaults the slice to `edit` and the selector falls back to `EDITOR_INTENT_EDIT`, so that value never occurs. The comment promised boot silence for any initial intent while only `edit` was ever silent, which is a trap for the next reader. Keep the equality check, which is what actually suppresses the notice, and say so.
New React files must use .tsx and new source should be TypeScript, so the intent switcher, the Suggest mode gate and constants, and their tests move to .ts/.tsx with real annotations instead of JSDoc types. MenuItemsChoice's onHover is typed as required even though the component defaults it to a noop and its own usage example omits it; typing the first TypeScript consumer surfaced that, so mark the prop optional.
# Conflicts: # packages/editor/CHANGELOG.md
# Conflicts: # packages/editor/CHANGELOG.md
|
Testing just this one in isolation, I get the same error noted here. Let me know if I'm doing something wrong. |
Trunk's Menu now throws in development builds when a Menu.Group sits inside a Menu.RadioGroup (#83792), which crashed the editor as soon as the Options menu opened. Put the Mode label and radio items directly in the RadioGroup, the same shape trunk gave ModeSwitcher, and cover it with a jsdom test since production e2e builds never hit the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTRGFetxgiXPJC9a91DMTL
# Conflicts: # packages/editor/CHANGELOG.md
this should be fixed now @jasmussen - thanks for letting me know it was broken! |
|
Took it for a spin:
Bolding doesn't create a note. I understand that's part of a separate PR in the stack, that's fine, the focus of this one is the dropdown, this one:
I think it makes sense insofar as we refactored appearance and panels to make room for this. If that becomes controversial once landed, we can invert that: restore appearance toggles direcly, and put suggest modes inside a flyout. I prefer what this branch does, but I always want to think of contingencies. The larger motivation: we are still doing that careful dance of adding features but balancing prominence with readiness. The truth is that longer term, edit/suggest/view probably deserve their own dropdown, a la this:
But that's not near term. One thing though: we could start without view. There's an aspect of user roles in WordPress, if you're just a contributor you might have view. Or if in the future we build a "share" link, we could assign roles to the shared URL. That's all part of the collaborative future, and what I'm suggesting is that if "view" in its present state is low value, we can postpone it. |
|
Thanks as always for testing and the feedback @jasmussen.
Interesting idea. Viewing mode is fairly distinct from suggesting mode and removing it might significantly reduce complexity of the overall stack. Let me research that further and see what the impact of removing it would be. |
|
I wanted to see how much it would actually save so I asked Claude to review: Claude went through the stack to size up what View costs:
If others agree this is a good idea, I'll pull View out of this PR and cascade the change through the stack. |
Only the post status was locked while suggesting, so the excerpt, featured image, slug, terms, meta and every other post setting changed the saved post directly (tyxla on #73411). Most sidebar panels and plugins write the entity through `useEntityProp`, which never passes through `editPost`, so the status lock could not catch them. Guard the current post at three seams: `editPost` sorts its edits into content (passes), proposable (Phase 2) and refused; a wrap of the core-data `editEntityRecord` action refuses direct writes to the current post while suggesting; and `savePost` strips every post-level edit from a save made while suggesting, so an edit staged in Editing is never persisted from Suggesting. Settings that cannot be suggested are shown read-only with the reason as their description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
Phase 1 refuses every post-level edit while suggesting; the excerpt, featured image and slug are fields a suggester should be able to propose, the way the title already is. Generalize the title proposal into post field proposals held in the editor store rather than the session, so `editPost` can divert a proposable edit into a proposal and `getEditedPostAttribute` can show the proposed value while suggesting: every panel that reads the post through the editor store shows the proposal with no change of its own. The title moves onto the same path. Each proposal is saved as its own note with a `post-attribute-set` op; accept applies it past the guard, reject drops it, and undo withdraws the newest one. The note names the field and the summary shows before and after (thumbnails for the featured image). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
Categories, tags and custom taxonomies changed the saved post directly while suggesting. Treat each of the post type's taxonomies (by the `rest_base` its term ids are edited under) as a proposable field, so picking terms becomes one note per taxonomy whose summary lists the terms added and removed. Creating a term is a real write to the taxonomy that no reviewer could take back, so the term pickers stop offering to create one while suggesting; only existing terms can be suggested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
Post meta written through `editPost` (the classic pattern for a plugin's meta field) changed the saved post directly while suggesting. Treat a meta key the post's REST record carries as proposable, one note per key, shown in the plugin's field while suggesting. Plugins that write the entity directly stay refused by the Phase 1 guard. A reviewer's accept writes whatever the note holds, so the server now validates every post-level op before a note is saved: only the fields Suggestion mode proposes, a REST-exposed taxonomy of the post type, or a meta key registered with `show_in_rest` that the suggester could edit (`edit_post_meta`). Anything else is refused with a 400. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
Trashing the post is not a change a reviewer could accept or reject, so a suggester must not be able to do it. The sidebar button is disabled with the reason as its description, the post actions menu stops offering Trash and the other delete or site-setting actions, and the store refuses trashPost and a core-data deleteEntityRecord of the current post with a refusal notice, so plugins and the console are covered too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
The template summary in the post editor edits site settings (posts per page, the site's discussion default) and the posts page title (the blog title). None of them belong to the post, so a suggestion cannot propose them, yet they were written directly while suggesting. The guard now refuses editEntityRecord on the site entity and on the posts page, and refuses saveEntityRecord and saveEditedEntityRecord on the site entity so a setting staged in Editing is not saved from Suggesting either. The rows, legacy and DataForm, are read-only while suggesting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
The trashPost mock gains the intent selector the action now reads, and two cases cover the refusals: trashPost and deleteEntityRecord of the current post send no DELETE while suggesting, and a site entity edit is dropped with the refusal announced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
Post field proposals live in the editor store, which a reload empties, so editing a field again after a reload opened a second note beside the pending one, and editing it back to the post's value left the note behind. The current user's pending post-level notes now restore their proposals when the notes load (the note's before is the baseline), linked to their note. The field shows the proposal again while suggesting, the auto-saver updates that note instead of creating one, and going back to the post's value trashes it. Undo after a re-edit returns to what the note holds rather than withdrawing a suggestion made in an earlier session. Other authors' notes are never restored, and a decision on one of them no longer clears the user's own proposal for the same field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
The term pickers hid "Add Category" and "Create:" while suggesting,
because creating a term is a real write to the taxonomy. A suggester
could only pick terms that already existed.
A new term now rides on the terms proposal as { name, parent? } beside
the term ids, and nothing is written to the taxonomy while suggesting.
The pickers show it like a proposed existing term (a chip, a checked
checkbox), getEditedPostAttribute keeps returning term ids only, and the
note summary reads "New tag: foo". Accepting creates the term as the
reviewer, reusing one with the same name that appeared meanwhile, then
assigns it; when the reviewer may not create terms the accept fails with
a notice and the note stays pending. Rejecting writes nothing.
The server validates the entries: each is a term id or a new term with a
sanitized, non-empty name, and a parent only for a hierarchical taxonomy,
as an existing term of it. The suggester needs no capability to create
terms, since nothing is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
getEditedPostAttribute is public, and while suggesting it returns a field's pending suggested value rather than the post's. That is what lets every panel and plugin show the proposal, so it stays, but its docblock now says so and where to read the post's own value. The architecture doc and the changelog cover it along with new term suggestions, restoring proposals after a reload, and the trash and site settings locks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
The code editor guard reads saved content back for pending markers. Markers now carry one class per kind, so the guard has to find all three and stop treating the old single class as a marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
…tent SUGGESTION_MARKER_CLASS becomes SUGGESTION_MARKER_CLASSES, the store's copy of the three class tokens, so the code editor guard still finds a pending marker of any kind and no longer matches the retired class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x
Brings the branch current with trunk so the PR stops reporting as conflicting and gets a real CI run. The only conflict was the editor changelog: the intent entry stays under New Features in Unreleased, trunk's new Enhancements entry follows it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GLh3wdWJSgU3NLobNDk6x



Part of #73411
What's in this PR
Adds the session-scoped editor intent state (private setEditorIntent
action / getEditorIntent selector), the Edit / Suggest / View switcher in
the editor options menu, Google Docs convention keyboard shortcuts, and a
read-only view intent via isPreviewMode. The Suggestion Mode experiment
flag and the gate helpers (isSuggestionModeEnabled / useCanSuggest) land
here so every later layer gates on the same predicate. No suggestion
capture or storage yet.
The
viewintent's read-only contract is enforced here too, in the layerthat promises it: preview mode is added to the memoized insertion
selectors' dependants so flipping it at runtime invalidates their cached
answers, the writing flow's cross-block input handling bails out while
the canvas is a preview, and the Block Inserter toggle is disabled (an
open inserter is closed on the way in). This was originally #81661 on top
of the stack and moved down here so it can be reviewed next to the intent
it belongs to.
Screenshot
The Edit / Suggest / View switcher in the editor Options menu, with the Google Docs style shortcuts alongside each mode:
Mode group in the editor Options menu showing Editing, Suggesting and Viewing
Testing
This is one layer of the stack. To exercise the whole feature, #78994 bundles every layer into one branch and builds it in Playground:
👉 https://playground.wordpress.net/gutenberg.html?pr=78994
Enable Gutenberg > Experiments > Collaboration > Suggestion Mode, then follow the walkthrough in #73411, which also explains how to review the stack layer by layer.
Suggest mode stack
This rebuilds the manually-stacked Suggest mode work (#73411) as a GitHub Stack of 9 small, independently reviewable PRs, each building on the one below it:
Each follow up fix now sits in the layer that owns the code it changes, rather than piling onto the top of the stack. The whole feature can be exercised end-to-end via the combined testing branch #78994 (Playground). Behind the "Suggestion Mode" experiment (Gutenberg > Experiments).
AI Use
Code and description both written with 🤖 Claude Code. I will review and test.