Skip to content

MM-69893: Mount the host WYSIWYG editor at the page route with draft autosave - #13

Draft
nang2049 wants to merge 3 commits into
masterfrom
MM-69893-page-editor-autosave
Draft

MM-69893: Mount the host WYSIWYG editor at the page route with draft autosave#13
nang2049 wants to merge 3 commits into
masterfrom
MM-69893-page-editor-autosave

Conversation

@nang2049

Copy link
Copy Markdown
Contributor

Summary

POC for the Docs page editor. Mounts the core webapp's WysiwygEditor at the page route in document mode and wires it to the draft API added in MM-69271.

Built on the plugin surface published in MM-69912 (core). Uses all five additions from that contract: contentType='json', extensions, onContentError, getEditor() and hasContentError().

  • Editor mount: host editor in JSON mode, 700px centred column, title block and byline per Figma. Composer-specific host styles (46px min-height) are overridden for document editing.
  • Draft autosave: 1s debounce, patches coalesced, base_edit_at sent on every write for optimistic locking. Writes are serialized so publish cannot race an in-flight save, and a pending patch is flushed to the page you are leaving rather than dropped.
  • Publish flow: three-option exit dialog (publish / save as draft / discard) and a conflict dialog distinguishing concurrent_edit from concurrent_autosave.
  • Presence : active editors from REST plus page_presence_updated websocket events, currently rendered as a count.
  • Toolbar : host FormattingBar, pinned to the top by default with a toggle to a floating bar over the selection.
  • Callout extension : five types, matching the callout node already on the server allowlist.

** Not in this PR **
Read/view mode, the in-page outline sidebar, presence avatars, and the text-colour / comment / AI toolbar controls.
are untouched.

Testing

Requires MM_FEATUREFLAGS_ENABLEDOCS=true, otherwise every plugin API call is a 501.

webapp/src/hooks/draft_autosave.test.tsx covers debouncing, coalescing, base_edit_at, cancellation, in-flight flushing, and failure reporting.

Manually: type and confirm the indicator settles on Saved, go offline and publish, confirming the editor stays open with an error rather than closing and losing the text; type and immediately switch pages, confirming the text is on the page you left.

Open questions for the team

  1. Toolbar defaults to pinned, with the floating bar behind the toggle. Spec 3.1 implies pinned is primary. The preference persists in localStorage, which is per-browser, should it be a real user preference?
  2. Exit prompts with three options. Should closing with an unpublished draft prompt at all, or silently keep the draft?
  3. Callout types are info / note / success / warning / error. Confirm the set and the labels against design.
  4. Autosave debounce is 1s on top of the host's 100ms serialize.
  5. Slash commands. Core's suggestion list runs CommandProvider on any text starting with /, so typing / in a page currently autocompletes channel slash commands. Should / instead open a docs block-insert menu? This needs a decision before it can be fixed properly in core.
  6. Read vs edit mode. Currently the route is always editable for anyone with access. Is an explicit view mode with an Edit button expected?

Known issues that need fixing in Core

  • Enter inside a heading nested in a block wrapper threw an unguarded ProseMirror RangeError from wysiwyg_editor.tsx and crashed the whole webapp. Guarded locally.
  • The suggestion popup cannot be dismissed as Escape sets isOpen false but the next keystroke re-runs the providers and reopens it.
  • WysiwygSuggestionList hardcodes position='top', worked around here with a MutationObserver that re-anchors the popup to the caret.

Ticket Link

https://mattermost.atlassian.net/browse/MM-69893

Screenshot 2026-07-31 at 15 30 38 Screenshot 2026-07-31 at 15 30 44 Screenshot 2026-07-31 at 15 31 01 Screenshot 2026-07-31 at 15 31 07 Screenshot 2026-07-31 at 15 31 16

@nang2049
nang2049 requested a review from calebroseland July 31, 2026 09:38
@nang2049
nang2049 marked this pull request as draft July 31, 2026 09:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/components/page_editor/page_editor.module.scss (1)

1-10: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the blank line inside .root.

Stylelint reports declaration-empty-line-before at Line 7. The blank Line 6 between min-width: 0; and min-height: 0; triggers this.

🧹 Proposed fix
 .root {
     display: flex;
     flex: 1 1 0;
     flex-direction: column;
     min-width: 0;
-
     min-height: 0;
     height: 100%;
     background: var(--center-channel-bg);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/page_editor.module.scss` around lines 1 -
10, Remove the empty line between the min-width and min-height declarations in
the .root style rule, leaving the declaration order and values unchanged.

Source: Linters/SAST tools

🧹 Nitpick comments (8)
webapp/src/client/presence_events.ts (1)

21-23: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Isolate listener errors during publish.

publishPagePresence calls each listener directly. If one listener throws, the remaining listeners never receive the event, and the exception propagates into the WebSocket handler registered in webapp/src/index.tsx. Wrap each call so one failing subscriber cannot block delivery.

♻️ Proposed isolation of listener errors
 export function publishPagePresence(event: PagePresenceEvent): void {
-    listeners.forEach((listener) => listener(event));
+    listeners.forEach((listener) => {
+        try {
+            listener(event);
+        } catch {
+            // A single subscriber must not block delivery to the others.
+        }
+    });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/client/presence_events.ts` around lines 21 - 23, Update
publishPagePresence so each listener invocation is isolated with per-listener
error handling, ensuring an exception from one subscriber does not stop
iteration or propagate into the WebSocket handler; preserve delivery of the
event to all remaining listeners.
webapp/src/hooks/draft_autosave.ts (1)

112-124: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The debounce has no maximum wait, so continuous typing never saves.

Every queue call clears the timer and starts a new one. While a user types without a pause of AUTOSAVE_DEBOUNCE_MS, no write occurs. A long uninterrupted editing session therefore holds all content in memory. A tab crash or a forced reload loses that work.

Consider tracking the time of the first pending edit and forcing a write once a maximum interval elapses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/hooks/draft_autosave.ts` around lines 112 - 124, The queue
function’s debounce can be postponed indefinitely during continuous edits. Add
tracking for when the current pending batch first starts, and in queue enforce a
maximum wait interval that triggers write even when the debounce timer keeps
resetting; reset that tracking when the pending changes are written or cleared,
while preserving the existing debounce behavior for shorter pauses.
webapp/src/hooks/draft_autosave.test.tsx (1)

266-280: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add baseline coverage to the page-change test.

This test omits baseEditAt, so it asserts only that the patch reaches page1. It cannot detect which base_edit_at accompanies that patch. Two gaps remain untested, and both correspond to issues raised on webapp/src/hooks/draft_autosave.ts:

  1. Rerender with a different baseEditAt together with the new pageId, then assert the flushed patch carries the previous page's base_edit_at.
  2. Add a case with baseEditAt: 0 and assert base_edit_at: 0 is still sent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/hooks/draft_autosave.test.tsx` around lines 266 - 280, Expand the
page-change coverage around setup and the “flushes the pending patch…” test to
provide an initial baseEditAt, rerender with both a new pageId and different
baseEditAt, and assert the flushed page1 patch includes the previous page’s
base_edit_at. Add a separate case using baseEditAt: 0 and verify the emitted
patch preserves base_edit_at: 0.
webapp/src/components/page_editor/toolbar_controls.tsx (2)

138-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move CALLOUT_LABELS above CalloutControl and store plain message descriptors.

CalloutControl reads CALLOUT_LABELS at line 128, but the constant is declared here at line 140. The reference resolves at render time, so it works today. Declaring the constant before its use removes the forward reference. Storing MessageDescriptor values instead of functions also removes the Formatter indirection.

♻️ Proposed refactor
-type Formatter = ReturnType<typeof useIntl>['formatMessage'];
-
-const CALLOUT_LABELS: Record<CalloutType, (f: Formatter) => string> = {
-    info: (f) => f({id: 'docs.editor.calloutInfo', defaultMessage: 'Info'}),
-    note: (f) => f({id: 'docs.editor.calloutNote', defaultMessage: 'Note'}),
-    success: (f) => f({id: 'docs.editor.calloutSuccess', defaultMessage: 'Success'}),
-    warning: (f) => f({id: 'docs.editor.calloutWarning', defaultMessage: 'Warning'}),
-    error: (f) => f({id: 'docs.editor.calloutError', defaultMessage: 'Error'}),
-};

Add above CalloutControl:

const CALLOUT_LABELS: Record<CalloutType, MessageDescriptor> = {
    info: {id: 'docs.editor.calloutInfo', defaultMessage: 'Info'},
    note: {id: 'docs.editor.calloutNote', defaultMessage: 'Note'},
    success: {id: 'docs.editor.calloutSuccess', defaultMessage: 'Success'},
    warning: {id: 'docs.editor.calloutWarning', defaultMessage: 'Warning'},
    error: {id: 'docs.editor.calloutError', defaultMessage: 'Error'},
};

Then render with {formatMessage(CALLOUT_LABELS[type])}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/toolbar_controls.tsx` around lines 138 -
146, Move CALLOUT_LABELS above CalloutControl and change its values from
formatter functions to MessageDescriptor objects. Remove the Formatter
indirection, and update CalloutControl to pass CALLOUT_LABELS[type] directly to
formatMessage when rendering the label.

112-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add arrow-key navigation to the callout menu.

The container declares role='menu' and the items declare role='menuitem'. A screen reader then announces a menu, and the user expects arrow keys to move between items. The current code provides no arrow-key handling and no roving tabindex. Tab still reaches each button, so the task stays completable, but the announced interaction model does not match the behaviour.

Either implement arrow-key navigation with a roving tabindex, or drop the role='menu' and role='menuitem' attributes and let the buttons present as a plain group.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/toolbar_controls.tsx` around lines 112 -
133, Align the callout menu’s accessibility semantics with its behavior in the
toolbar component: either implement arrow-key navigation and roving tabindex for
the `menu` and `menuitem` elements around `CALLOUT_TYPES.map`, or remove those
role attributes so the controls remain a plain button group. Preserve the
existing `insert(type)` activation behavior.
webapp/src/hooks/caret_anchored_suggestions.ts (1)

6-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The .suggestion-list selector couples this hook to a host-owned class name.

The hook positions a DOM node that the host web application renders. If the host renames the class, then this code silently stops positioning the list and no error appears. Add a comment that records the host version this selector targets, so a future reader can trace the dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/hooks/caret_anchored_suggestions.ts` around lines 6 - 8, Add a
concise comment next to the SELECTOR constant documenting the host version
associated with the `.suggestion-list` class, preserving the existing selector
and positioning behavior.
webapp/src/components/page_editor/publish_conflict_dialog.tsx (1)

17-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compare the conflict reason exactly instead of by substring.

Line 23 uses reason.includes('concurrent_autosave'). Substring matching also matches an unrelated future reason such as not_concurrent_autosave or a reason that embeds the token in a longer message. Type reason as a union of the server reason codes and compare with ===. The only consequence today is the wrong explanatory paragraph, so this is a robustness improvement rather than a defect.

#!/bin/bash
# Find the reason values that the server and the draft client produce.
rg -n 'concurrent_autosave|PublishConflictError|reason' --type=ts --type=tsx -g '!**/*.test.*'
rg -n 'concurrent_autosave' --type=go
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/publish_conflict_dialog.tsx` around lines
17 - 23, Update isConcurrentAutosave to accept the server’s reason-code union
rather than a generic string, and compare the value exactly with ===
'concurrent_autosave'. Reuse the existing reason-code type or define the union
from the server/draft-client reason values, preserving the current
explanatory-paragraph behavior for the exact concurrent_autosave code.
webapp/src/components/page_editor/page_editor.module.scss (1)

28-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider consolidating the repeated .column selector.

.column is declared three times (Lines 28-32, 62-73, 88-153). Merging these into a single block groups the editor-surface and callout styling with the layout rules, and makes future edits less likely to miss one of the declarations.

Also applies to: 62-73, 88-153

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/page_editor.module.scss` around lines 28 -
32, Consolidate the three `.column` declarations into one selector block,
combining the layout, editor-surface, and callout rules while preserving all
existing properties and responsive behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@webapp/src/components/docs_root/docs_main_content.tsx`:
- Around line 24-44: Update DocsMainContent and the PageEditor loading flow to
explicitly validate that the requested space exists before rendering an editor
for spaceId/pageId. Preserve the existing 404 handling that allows valid new
drafts, but prevent invalid spaceId requests from rendering an empty editor when
both requests return 404; reuse the existing space state and error-handling
symbols.

In `@webapp/src/components/page_editor/apply_formatting.ts`:
- Around line 21-43: The selectWordUnderCaret function uses textContent, which
omits inline leaf nodes while parentOffset counts them. Replace the parent text
extraction with parent.textBetween(0, parent.content.size, undefined, '\ufffc')
so text indexing and caret offsets share one coordinate space; preserve the
existing word-boundary and selection logic.

In `@webapp/src/components/page_editor/callout_extension.ts`:
- Around line 48-53: Update toggleCallout in addCommands so an active callout
with a different type uses commands.updateAttributes(this.name, {type}) instead
of nesting via wrapIn; retain commands.toggleWrap(this.name, {type}) for all
other cases.

In `@webapp/src/components/page_editor/floating_formatting_bar.module.scss`:
- Around line 6-9: Remove the empty line between the z-index declaration and the
width declaration in the floating formatting bar styles so the declarations are
contiguous and satisfy Stylelint.

In `@webapp/src/components/page_editor/page_byline.tsx`:
- Around line 22-27: Update the component’s missing-author branch around getUser
and the author null check to dispatch getMissingProfilesByIds([userId]) when
author is unavailable before returning null. Add useEffect to the React imports
and use it to trigger this profile-loading dispatch when userId or author state
requires it, while preserving the existing rendering behavior once the profile
is available.

In `@webapp/src/components/page_editor/page_editor.tsx`:
- Around line 154-156: Update the early-return paths in the publish and
save-draft flows around autosave.flush() to call setActionError before returning
when the flush resolves false, using the existing error message pattern. Ensure
saveDraftAndLeave also catches rejected flush promises and sets actionError so
the exit dialog reports the failure instead of leaving an unhandled rejection.
- Around line 88-92: Update the page-change reset effect keyed by spaceId and
pageId to also clear conflict and showExitDialog, ensuring any publish-conflict
or exit dialog closes before actions can target the newly selected page.
- Around line 147-209: Replace the callback-captured busy guard in publish,
discard, and saveDraftAndLeave with a shared synchronous in-flight ref that is
checked and set before any await, then cleared when each action finishes.
Continue updating the existing busy state for rendering, and ensure all early
returns and finally paths release the ref.

In `@webapp/src/components/page_editor/toolbar_controls.tsx`:
- Around line 86-90: Guard the toolbar_controls.tsx insert callback so it
verifies the editor chain provides toggleCallout before invoking it, while
preserving focus, execution, and menu-closing behavior when supported. In
webapp/src/webapp_globals.ts lines 82-87, document the host build that
introduced contentType, extensions, and onContentError, or add a JSON-content
capability probe that page_editor.tsx can use for the legacy notice instead of
relying solely on hostSupportsDocumentEditor/getEditor.

In `@webapp/src/hooks/caret_anchored_suggestions.ts`:
- Around line 85-95: Update the effect around the MutationObserver in the
caret-anchored suggestions hook to schedule repositioning when the editor’s
data-docs-scroll container scrolls and when the window resizes. Register both
listeners while enabled, reuse the existing schedule callback, and remove them
in the cleanup alongside the observer and selectionchange listener.
- Around line 30-38: Update the caret-anchored positioning logic to clamp the
computed left offset before assigning `list.style.left`, using the surface width
and suggestion-list width so the list remains within the surface. Follow the
existing `maxLeft` clamping approach in `floating_formatting_bar.tsx` while
preserving the current caret-relative positioning when it fits.

In `@webapp/src/hooks/draft_autosave.ts`:
- Around line 140-143: Update the autosave flow around the Pending type, write,
and error-requeue logic to store baseEditAt alongside each queued patch, using
the value captured for that page rather than reading latest.current during
cleanup. Preserve the stored baseEditAt when requeueing failed patches, and add
coverage that switches between pages with different baselines and verifies each
patch uses its own baseline.

In `@webapp/src/hooks/page_draft.ts`:
- Around line 70-71: Update the draft field initialization in the page draft
hook to use nullish coalescing for both title and body, preserving empty-string
values while still falling back to page values only when the draft fields are
nullish.

In `@webapp/src/hooks/page_presence.ts`:
- Around line 54-57: Update the expired-snapshot branch in the page presence
effect to refresh the `now` state before exiting, rather than returning with the
mount-time value. Keep the existing timer scheduling for unexpired snapshots
unchanged so the memoized active-editor calculation receives the current
timestamp and clears stale editors.

---

Outside diff comments:
In `@webapp/src/components/page_editor/page_editor.module.scss`:
- Around line 1-10: Remove the empty line between the min-width and min-height
declarations in the .root style rule, leaving the declaration order and values
unchanged.

---

Nitpick comments:
In `@webapp/src/client/presence_events.ts`:
- Around line 21-23: Update publishPagePresence so each listener invocation is
isolated with per-listener error handling, ensuring an exception from one
subscriber does not stop iteration or propagate into the WebSocket handler;
preserve delivery of the event to all remaining listeners.

In `@webapp/src/components/page_editor/page_editor.module.scss`:
- Around line 28-32: Consolidate the three `.column` declarations into one
selector block, combining the layout, editor-surface, and callout rules while
preserving all existing properties and responsive behavior.

In `@webapp/src/components/page_editor/publish_conflict_dialog.tsx`:
- Around line 17-23: Update isConcurrentAutosave to accept the server’s
reason-code union rather than a generic string, and compare the value exactly
with === 'concurrent_autosave'. Reuse the existing reason-code type or define
the union from the server/draft-client reason values, preserving the current
explanatory-paragraph behavior for the exact concurrent_autosave code.

In `@webapp/src/components/page_editor/toolbar_controls.tsx`:
- Around line 138-146: Move CALLOUT_LABELS above CalloutControl and change its
values from formatter functions to MessageDescriptor objects. Remove the
Formatter indirection, and update CalloutControl to pass CALLOUT_LABELS[type]
directly to formatMessage when rendering the label.
- Around line 112-133: Align the callout menu’s accessibility semantics with its
behavior in the toolbar component: either implement arrow-key navigation and
roving tabindex for the `menu` and `menuitem` elements around
`CALLOUT_TYPES.map`, or remove those role attributes so the controls remain a
plain button group. Preserve the existing `insert(type)` activation behavior.

In `@webapp/src/hooks/caret_anchored_suggestions.ts`:
- Around line 6-8: Add a concise comment next to the SELECTOR constant
documenting the host version associated with the `.suggestion-list` class,
preserving the existing selector and positioning behavior.

In `@webapp/src/hooks/draft_autosave.test.tsx`:
- Around line 266-280: Expand the page-change coverage around setup and the
“flushes the pending patch…” test to provide an initial baseEditAt, rerender
with both a new pageId and different baseEditAt, and assert the flushed page1
patch includes the previous page’s base_edit_at. Add a separate case using
baseEditAt: 0 and verify the emitted patch preserves base_edit_at: 0.

In `@webapp/src/hooks/draft_autosave.ts`:
- Around line 112-124: The queue function’s debounce can be postponed
indefinitely during continuous edits. Add tracking for when the current pending
batch first starts, and in queue enforce a maximum wait interval that triggers
write even when the debounce timer keeps resetting; reset that tracking when the
pending changes are written or cleared, while preserving the existing debounce
behavior for shorter pauses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b73604e5-f7a9-4c16-9257-acc82cd46454

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf4f9a and 8aac409.

⛔ Files ignored due to path filters (1)
  • webapp/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (37)
  • webapp/i18n/en.json
  • webapp/package.json
  • webapp/src/client/drafts.ts
  • webapp/src/client/pages.ts
  • webapp/src/client/presence_events.ts
  • webapp/src/client/rest.ts
  • webapp/src/components/docs_root/docs_main_content.tsx
  • webapp/src/components/page_editor/apply_formatting.ts
  • webapp/src/components/page_editor/autosave_indicator.module.scss
  • webapp/src/components/page_editor/autosave_indicator.tsx
  • webapp/src/components/page_editor/callout_extension.ts
  • webapp/src/components/page_editor/docs_extensions.ts
  • webapp/src/components/page_editor/exit_editor_dialog.module.scss
  • webapp/src/components/page_editor/exit_editor_dialog.tsx
  • webapp/src/components/page_editor/floating_formatting_bar.module.scss
  • webapp/src/components/page_editor/floating_formatting_bar.tsx
  • webapp/src/components/page_editor/page_byline.module.scss
  • webapp/src/components/page_editor/page_byline.tsx
  • webapp/src/components/page_editor/page_editor.module.scss
  • webapp/src/components/page_editor/page_editor.tsx
  • webapp/src/components/page_editor/publish_conflict_dialog.module.scss
  • webapp/src/components/page_editor/publish_conflict_dialog.tsx
  • webapp/src/components/page_editor/toolbar_controls.module.scss
  • webapp/src/components/page_editor/toolbar_controls.tsx
  • webapp/src/data/fixtures.ts
  • webapp/src/hooks/caret_anchored_suggestions.ts
  • webapp/src/hooks/draft_autosave.test.tsx
  • webapp/src/hooks/draft_autosave.ts
  • webapp/src/hooks/page_draft.ts
  • webapp/src/hooks/page_presence.ts
  • webapp/src/hooks/pinned_toolbar.ts
  • webapp/src/hooks/user.ts
  • webapp/src/index.tsx
  • webapp/src/store/test_fixtures.ts
  • webapp/src/types/docs.ts
  • webapp/src/types/drafts.ts
  • webapp/src/webapp_globals.ts

Comment thread webapp/src/components/docs_root/docs_main_content.tsx
Comment thread webapp/src/components/page_editor/apply_formatting.ts
Comment thread webapp/src/components/page_editor/callout_extension.ts
Comment thread webapp/src/components/page_editor/page_byline.tsx
Comment thread webapp/src/hooks/caret_anchored_suggestions.ts Outdated
Comment thread webapp/src/hooks/caret_anchored_suggestions.ts
Comment thread webapp/src/hooks/draft_autosave.ts
Comment thread webapp/src/hooks/page_draft.ts
Comment thread webapp/src/hooks/page_presence.ts
@nang2049

nang2049 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces the Docs page placeholder with a functional Tiptap editor. It adds draft APIs, autosave, publishing, conflict handling, presence updates, formatting controls, editor dialogs, page metadata, and localized strings.

Changes

Docs editor

Layer / File(s) Summary
Draft API and data contracts
webapp/src/client/rest.ts, webapp/src/client/drafts.ts, webapp/src/client/pages.ts, webapp/src/types/*, webapp/src/data/fixtures.ts, webapp/src/store/test_fixtures.ts, webapp/package.json, webapp/src/webapp_globals.ts
The REST client, draft operations, page retrieval, draft types, conflict errors, editor contracts, and page fixtures define the editor data flow.
Draft loading, autosave, and presence
webapp/src/hooks/page_draft.ts, webapp/src/hooks/draft_autosave.ts, webapp/src/hooks/draft_autosave.test.tsx, webapp/src/hooks/page_presence.ts, webapp/src/hooks/user.ts, webapp/src/client/presence_events.ts, webapp/src/index.tsx
Hooks load drafts, serialize and retry autosave writes, track active editors, filter the current user, and forward WebSocket presence events.
Tiptap host and formatting integration
webapp/src/components/page_editor/callout_extension.ts, webapp/src/components/page_editor/docs_extensions.ts, webapp/src/components/page_editor/apply_formatting.ts, webapp/src/components/page_editor/floating_formatting_bar.*, webapp/src/hooks/caret_anchored_suggestions.ts, webapp/src/hooks/pinned_toolbar.ts, webapp/src/components/page_editor/toolbar_controls.*
Tiptap extensions, callouts, formatting, suggestions, floating controls, toolbar pinning, and toolbar controls implement editor interactions.
Page editor interface
webapp/src/components/docs_root/docs_main_content.tsx, webapp/src/components/page_editor/page_editor.tsx, webapp/src/components/page_editor/page_editor.module.scss, webapp/src/components/page_editor/autosave_indicator.*, webapp/src/components/page_editor/exit_editor_dialog.*, webapp/src/components/page_editor/publish_conflict_dialog.*, webapp/src/components/page_editor/page_byline.*, webapp/i18n/en.json
Routing now opens the page editor. The editor renders loading, editing, autosave, presence, publishing, conflict, exit, error, and page-author states with localized text and styles.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: 2: Dev Review

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary objective: mounting the WYSIWYG editor at the page route with draft autosave integration.
Description check ✅ Passed The description comprehensively explains the PR scope, implementation details, testing approach, and open questions related to the Docs page editor feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MM-69893-page-editor-autosave

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
webapp/src/hooks/page_draft.ts (1)

73-74: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve empty draft fields with nullish coalescing.

Draft.title and Draft.body are required strings, and autosave sends empty strings. With ||, a cleared title or a cleared body falls back to the published page content on the next load. The user then sees content that the draft no longer contains.

Use ?? so the fallback applies only when the draft field is null or undefined.

🐛 Proposed fix to preserve cleared draft fields
-                title: draft?.title || page?.title || '',
-                body: draft?.body || page?.body || '',
+                title: draft?.title ?? page?.title ?? '',
+                body: draft?.body ?? page?.body ?? '',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/hooks/page_draft.ts` around lines 73 - 74, Update the draft
initialization around title and body to use nullish coalescing instead of falsy
coalescing, so empty-string Draft.title and Draft.body values are preserved
while fallback to page values occurs only for null or undefined.
webapp/src/components/page_editor/page_editor.tsx (1)

159-162: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A failed autosave.flush() still produces no user-visible message.

Line 160 and Line 211 return early when flush() resolves false. Neither path calls setActionError. The finally block clears busy, so the button becomes active and nothing else changes. The user sees no result and no reason.

saveDraftAndLeave at Line 203 also has no catch. If flush() rejects, then the rejection is unhandled and the exit dialog shows no error, because failed={actionError != null} stays false at Line 418.

🐛 Proposed fix
         try {
             if (!await autosave.flush()) {
+                setActionError(new Error('autosave_flush_failed'));
                 return;
             }

Apply the same change at Line 211, and add a catch block to saveDraftAndLeave that calls setActionError(error).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/page_editor.tsx` around lines 159 - 162,
Update both autosave.flush() call paths to surface failures through
setActionError: when flush() resolves false, set the action error before
returning, and add a catch block to saveDraftAndLeave that passes the rejected
error to setActionError. Preserve the existing finally cleanup and successful
save/navigation behavior.
🧹 Nitpick comments (9)
webapp/src/hooks/draft_autosave.test.tsx (1)

172-189: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an assertion for recovery after a failed save.

The test verifies the retry patch content, but it does not verify that the status returns to saved after the retry succeeds. That transition drives the autosave indicator in page_editor.tsx.

Add the assertion at the end of the test.

♻️ Proposed additional assertion
         await act(async () => {
             await result.current.flush();
         });
         expect(patchesSent()[1]).toEqual({body: 'lost'});
+        expect(result.current.status).toBe('saved');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/hooks/draft_autosave.test.tsx` around lines 172 - 189, Add an
assertion at the end of the “keeps the patch for retry when a save fails” test
verifying that result.current.status transitions to “saved” after flush()
successfully retries the preserved patch.
webapp/src/hooks/pinned_toolbar.ts (2)

6-6: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Scope the storage key to the user.

STORAGE_KEY is a single global key. If two accounts use the same browser profile, they share one toolbar preference. The PR description lists the toolbar preference storage as an open question, so this choice is worth confirming now.

Two options exist:

  1. Append the current user id to the key. useCurrentUserId in webapp/src/hooks/user.ts already provides the id.
  2. Store the preference as a Mattermost user preference, so it follows the user across devices.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/hooks/pinned_toolbar.ts` at line 6, Update the pinned-toolbar
persistence around STORAGE_KEY to scope the stored preference to the
authenticated user, using the existing useCurrentUserId symbol from the user
hook when constructing the key. Ensure different users receive separate storage
entries and preserve the existing toolbar preference behavior.

30-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

writeStored returns a value that no caller uses.

writeStored returns true or false, but line 36 discards the result. A failed write is therefore silent, and the toolbar state and the stored state diverge without any signal.

Either drop the return type, or use the result to surface the failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/hooks/pinned_toolbar.ts` around lines 30 - 37, Update the pinned
toolbar effect around writeStored so its boolean result is no longer silently
discarded: either remove the unused return value from writeStored or handle
false by surfacing the storage failure, while preserving the first-render skip
behavior.
webapp/src/client/presence_events.ts (1)

21-23: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Isolate listener failures during dispatch.

If one listener throws, publishPagePresence stops and the exception propagates to the caller. The caller is the WebSocket handler registered in webapp/src/index.tsx, so a single failing subscriber can affect host event dispatch and can block the remaining subscribers.

Wrap each invocation so that one failure does not stop the others.

♻️ Proposed fix to isolate listener errors
 export function publishPagePresence(event: PagePresenceEvent): void {
-    listeners.forEach((listener) => listener(event));
+    listeners.forEach((listener) => {
+        try {
+            listener(event);
+        } catch {
+            // A failing subscriber must not block the remaining subscribers.
+        }
+    });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/client/presence_events.ts` around lines 21 - 23, Update
publishPagePresence to invoke each listener within its own failure boundary,
ensuring an exception from one listener is contained and does not propagate to
the WebSocket caller or prevent remaining listeners from receiving the event.
webapp/src/components/page_editor/exit_editor_dialog.module.scss (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add the copyright header.

floating_formatting_bar.module.scss and toolbar_controls.module.scss in the same directory both start with the two-line Mattermost copyright comment. This file does not.

♻️ Proposed addition
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
 .actions {
     display: flex;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/exit_editor_dialog.module.scss` at line 1,
Add the standard two-line Mattermost copyright comment at the beginning of the
stylesheet containing the .actions rule, matching the header used by
floating_formatting_bar.module.scss and toolbar_controls.module.scss.
webapp/src/components/page_editor/toolbar_controls.module.scss (1)

66-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused .swatch class.

toolbar_controls.tsx uses control, active, menuWrapper, menu, and menuItem. It does not use swatch. The menu items render Compass icons, not colour swatches.

♻️ Proposed removal
-
-.swatch {
-    width: 12px;
-    height: 12px;
-    border-radius: 2px;
-    flex-shrink: 0;
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/toolbar_controls.module.scss` around lines
66 - 71, Remove the unused .swatch style rule from the toolbar controls
stylesheet; retain the styles for the classes used by toolbar_controls.tsx,
including control, active, menuWrapper, menu, and menuItem.
webapp/src/components/page_editor/floating_formatting_bar.tsx (1)

85-100: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The scroll listener attaches only if editorRef.current is set on the first effect run.

Line 86 resolves the scroll container once. The effect depends on [schedule, editorRef]. editorRef is a stable ref object, so the effect does not re-run when editorRef.current changes later. If the referenced element mounts after this effect runs, scroller stays null and the bar never repositions on scroll.

Bind the scroll listener at the document level with capture, which does not depend on resolving the container.

♻️ Proposed change
     useEffect(() => {
-        const scroller = editorRef.current?.closest('[data-docs-scroll]');
-
         document.addEventListener('selectionchange', schedule);
         window.addEventListener('resize', schedule);
-        scroller?.addEventListener('scroll', schedule);
+        document.addEventListener('scroll', schedule, true);
         return () => {
             document.removeEventListener('selectionchange', schedule);
             window.removeEventListener('resize', schedule);
-            scroller?.removeEventListener('scroll', schedule);
+            document.removeEventListener('scroll', schedule, true);
             if (frameRef.current) {
                 cancelAnimationFrame(frameRef.current);
                 frameRef.current = 0;
             }
         };
     }, [schedule, editorRef]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/floating_formatting_bar.tsx` around lines
85 - 100, Update the scroll handling in the useEffect so it no longer resolves a
container from editorRef.current or attaches to scroller; register the scroll
listener on document with capture enabled and remove it using the same capture
configuration during cleanup, while preserving the existing selection, resize,
and animation-frame cleanup behavior.
webapp/src/components/page_editor/page_editor.module.scss (1)

62-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Merge the duplicate .column selectors.

.column is declared at Line 28, Line 62, and Line 88. Three separate blocks for the same class make the cascade harder to follow. Merge the two :global blocks into one block under a single .column rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/page_editor.module.scss` around lines 62 -
73, Merge the duplicate .column selectors in the stylesheet into one rule,
combining both :global blocks under it while preserving all existing
declarations and cascade behavior. Use the existing .column rule as the single
location for these styles.
webapp/src/components/page_editor/publish_conflict_dialog.tsx (1)

23-27: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Compare the conflict reason exactly instead of using a substring match.

reason is a discrete server reason code. includes matches any future code that embeds the concurrent_autosave token and selects the wrong message. Use an exact comparison against a shared constant.

♻️ Proposed change
-const isConcurrentAutosave = (reason: string): boolean => reason.includes('concurrent_autosave');
+const CONCURRENT_AUTOSAVE = 'concurrent_autosave';
+
+const isConcurrentAutosave = (reason: string): boolean => reason === CONCURRENT_AUTOSAVE;

Export the constant from client/drafts if the reason codes are already defined there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/publish_conflict_dialog.tsx` around lines
23 - 27, Update isConcurrentAutosave to compare reason by exact equality with
the shared concurrent-autosave reason constant, reusing and exporting that
constant from client/drafts if the reason codes are defined there; remove the
substring-based includes check while preserving the existing autosave conflict
selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@webapp/i18n/en.json`:
- Around line 27-31: Update the user-facing strings in the English translations
so docs.editor.autosave.saving, docs.editor.bodyPlaceholder, and the
corresponding string at line 49 use the same ellipsis convention, and change
docs.editor.callout to use “Callout” as the noun while preserving the existing
meaning.

In `@webapp/src/components/page_editor/page_editor.module.scss`:
- Around line 6-7: Remove the blank line immediately before the min-height
declaration in the SCSS file. Update the Stylelint configuration used for CSS
Modules files to ignore the global pseudo-class, reusing or extending the
existing .module.scss-specific rules after confirming whether other module
styles are already covered.

In `@webapp/src/components/page_editor/page_editor.tsx`:
- Around line 394-406: Update the WysiwygEditor usage in the page editor to set
useCtrlSend={true}, ensuring onPublish is triggered only through the intended
modified-key shortcut rather than an unmodified Enter in a paragraph.

In `@webapp/src/components/page_editor/publish_conflict_dialog.tsx`:
- Around line 39-54: The force-publish action in the conflict dialog lacks
loading and failure feedback. Update the dialog component’s props and the
`PrimaryButton` using `onForcePublish` to accept and mirror the `busy` and
`failed` handling used by `ExitEditorDialog`, so failed forced publishes are
surfaced within the open dialog.

In `@webapp/src/components/page_editor/toolbar_controls.tsx`:
- Around line 114-135: Update the menu item selection handler in the
CALLOUT_TYPES map to restore focus to triggerRef after insert(type) closes the
menu, unless the successful editor command has already moved focus to the editor
surface. Match the existing Escape-path focus behavior while preserving the
insert command flow.

---

Duplicate comments:
In `@webapp/src/components/page_editor/page_editor.tsx`:
- Around line 159-162: Update both autosave.flush() call paths to surface
failures through setActionError: when flush() resolves false, set the action
error before returning, and add a catch block to saveDraftAndLeave that passes
the rejected error to setActionError. Preserve the existing finally cleanup and
successful save/navigation behavior.

In `@webapp/src/hooks/page_draft.ts`:
- Around line 73-74: Update the draft initialization around title and body to
use nullish coalescing instead of falsy coalescing, so empty-string Draft.title
and Draft.body values are preserved while fallback to page values occurs only
for null or undefined.

---

Nitpick comments:
In `@webapp/src/client/presence_events.ts`:
- Around line 21-23: Update publishPagePresence to invoke each listener within
its own failure boundary, ensuring an exception from one listener is contained
and does not propagate to the WebSocket caller or prevent remaining listeners
from receiving the event.

In `@webapp/src/components/page_editor/exit_editor_dialog.module.scss`:
- Line 1: Add the standard two-line Mattermost copyright comment at the
beginning of the stylesheet containing the .actions rule, matching the header
used by floating_formatting_bar.module.scss and toolbar_controls.module.scss.

In `@webapp/src/components/page_editor/floating_formatting_bar.tsx`:
- Around line 85-100: Update the scroll handling in the useEffect so it no
longer resolves a container from editorRef.current or attaches to scroller;
register the scroll listener on document with capture enabled and remove it
using the same capture configuration during cleanup, while preserving the
existing selection, resize, and animation-frame cleanup behavior.

In `@webapp/src/components/page_editor/page_editor.module.scss`:
- Around line 62-73: Merge the duplicate .column selectors in the stylesheet
into one rule, combining both :global blocks under it while preserving all
existing declarations and cascade behavior. Use the existing .column rule as the
single location for these styles.

In `@webapp/src/components/page_editor/publish_conflict_dialog.tsx`:
- Around line 23-27: Update isConcurrentAutosave to compare reason by exact
equality with the shared concurrent-autosave reason constant, reusing and
exporting that constant from client/drafts if the reason codes are defined
there; remove the substring-based includes check while preserving the existing
autosave conflict selection.

In `@webapp/src/components/page_editor/toolbar_controls.module.scss`:
- Around line 66-71: Remove the unused .swatch style rule from the toolbar
controls stylesheet; retain the styles for the classes used by
toolbar_controls.tsx, including control, active, menuWrapper, menu, and
menuItem.

In `@webapp/src/hooks/draft_autosave.test.tsx`:
- Around line 172-189: Add an assertion at the end of the “keeps the patch for
retry when a save fails” test verifying that result.current.status transitions
to “saved” after flush() successfully retries the preserved patch.

In `@webapp/src/hooks/pinned_toolbar.ts`:
- Line 6: Update the pinned-toolbar persistence around STORAGE_KEY to scope the
stored preference to the authenticated user, using the existing useCurrentUserId
symbol from the user hook when constructing the key. Ensure different users
receive separate storage entries and preserve the existing toolbar preference
behavior.
- Around line 30-37: Update the pinned toolbar effect around writeStored so its
boolean result is no longer silently discarded: either remove the unused return
value from writeStored or handle false by surfacing the storage failure, while
preserving the first-render skip behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d019f109-4109-4213-a227-e285f66c7f60

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf4f9a and 51ed93f.

⛔ Files ignored due to path filters (1)
  • webapp/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (37)
  • webapp/i18n/en.json
  • webapp/package.json
  • webapp/src/client/drafts.ts
  • webapp/src/client/pages.ts
  • webapp/src/client/presence_events.ts
  • webapp/src/client/rest.ts
  • webapp/src/components/docs_root/docs_main_content.tsx
  • webapp/src/components/page_editor/apply_formatting.ts
  • webapp/src/components/page_editor/autosave_indicator.module.scss
  • webapp/src/components/page_editor/autosave_indicator.tsx
  • webapp/src/components/page_editor/callout_extension.ts
  • webapp/src/components/page_editor/docs_extensions.ts
  • webapp/src/components/page_editor/exit_editor_dialog.module.scss
  • webapp/src/components/page_editor/exit_editor_dialog.tsx
  • webapp/src/components/page_editor/floating_formatting_bar.module.scss
  • webapp/src/components/page_editor/floating_formatting_bar.tsx
  • webapp/src/components/page_editor/page_byline.module.scss
  • webapp/src/components/page_editor/page_byline.tsx
  • webapp/src/components/page_editor/page_editor.module.scss
  • webapp/src/components/page_editor/page_editor.tsx
  • webapp/src/components/page_editor/publish_conflict_dialog.module.scss
  • webapp/src/components/page_editor/publish_conflict_dialog.tsx
  • webapp/src/components/page_editor/toolbar_controls.module.scss
  • webapp/src/components/page_editor/toolbar_controls.tsx
  • webapp/src/data/fixtures.ts
  • webapp/src/hooks/caret_anchored_suggestions.ts
  • webapp/src/hooks/draft_autosave.test.tsx
  • webapp/src/hooks/draft_autosave.ts
  • webapp/src/hooks/page_draft.ts
  • webapp/src/hooks/page_presence.ts
  • webapp/src/hooks/pinned_toolbar.ts
  • webapp/src/hooks/user.ts
  • webapp/src/index.tsx
  • webapp/src/store/test_fixtures.ts
  • webapp/src/types/docs.ts
  • webapp/src/types/drafts.ts
  • webapp/src/webapp_globals.ts

Comment thread webapp/i18n/en.json Outdated
Comment thread webapp/src/components/page_editor/page_editor.module.scss
Comment thread webapp/src/components/page_editor/page_editor.tsx
Comment thread webapp/src/components/page_editor/publish_conflict_dialog.tsx
Comment thread webapp/src/components/page_editor/toolbar_controls.tsx
@catalintomai

Copy link
Copy Markdown
Collaborator

@asaadmahmood - for (some of) the "Open Questions" in #13 (comment)

Comment thread webapp/src/components/page_editor/floating_formatting_bar.tsx
Comment thread webapp/src/components/page_editor/page_editor.tsx
Comment thread webapp/src/hooks/page_draft.ts Outdated
Comment thread webapp/src/hooks/draft_autosave.ts Outdated
Comment thread webapp/src/components/page_editor/page_editor.tsx Outdated
calebroseland added a commit that referenced this pull request Aug 3, 2026
Pre-resolves the parts of the PR #13 (MM-69893) merge that are worth keeping
whichever order the two PRs land in, so the editor branch can be merged, reverted
and re-merged without redoing them.

- webapp_globals: adopt the document-mode editor contract — contentType,
  extensions, onContentError, plus getEditor/hasContentError on the handle and a
  hostSupportsDocumentEditor probe. A Docs page body is structured content, so
  the editor is useless to us in markdown mode. Also onChange's param rename and
  ReactNodeArray -> ReactNode[], which drops a deprecation warning.
- rest: thrown errors become RestError, a ClientError subclass that keeps the
  parsed payload and the server error id. Draft publish answers a 409 with the
  current page in the body, which ClientError alone discards. Existing
  instanceof ClientError / status_code callers are unaffected.
- rest: add a path-relative doFetch over the same request path, for client
  modules that describe a whole endpoint family.
- types: drop the stale note on Page — user_id and last_modified_by are here.

The editor branch's client layer compiles against this without edits, so it no
longer needs its own transport (which hand-rolled headers and relied on the
deprecated XMLHttpRequest CSRF fallback).

Prompt: merge PR#13 for preview with a revert-marked commit; conflict resolution
in a separate commit that stays, to reduce conflicts when both PRs merge
@nang2049

nang2049 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/components/page_editor/page_editor.tsx (1)

153-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Ignore action results after the route changes.

If a publish for page A remains in flight after navigation to page B, Line 175 can open page A's conflict dialog on page B. The dialog then calls the current publish callback and force-publishes page B.

discard and saveDraftAndLeave can also call leave() for page A after navigation to page B.

Capture a route generation or immutable page key when each action starts. Before each post-await state update, navigation, or conflict update, return when that key no longer matches the current route.

Proposed guard
+    const routeKeyRef = useRef(`${spaceId}:${pageId}`);
+
     useEffect(() => {
+        routeKeyRef.current = `${spaceId}:${pageId}`;
         setContentError(false);
         // ...
     }, [spaceId, pageId]);

     const publish = useCallback(async (force: boolean, exitAfter = false) => {
+        const actionRouteKey = `${spaceId}:${pageId}`;
         // ...
         try {
             if (!await autosave.flush()) {
                 return;
             }
+            if (routeKeyRef.current !== actionRouteKey) {
+                return;
+            }

             const published = await publishPageDraft(spaceId, pageId, force);
+            if (routeKeyRef.current !== actionRouteKey) {
+                return;
+            }
             // state updates and leave()

Also applies to: 185-222

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/page_editor/page_editor.tsx` around lines 153 - 183,
Guard publish, discard, and saveDraftAndLeave against stale results after
navigation by capturing the current route generation or immutable page key when
each action begins. Before every post-await state update, conflict/dialog
update, or leave() call, verify the captured key still matches the current route
and return otherwise; ensure stale page A actions cannot affect page B.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@webapp/src/components/page_editor/page_editor.tsx`:
- Around line 153-183: Guard publish, discard, and saveDraftAndLeave against
stale results after navigation by capturing the current route generation or
immutable page key when each action begins. Before every post-await state
update, conflict/dialog update, or leave() call, verify the captured key still
matches the current route and return otherwise; ensure stale page A actions
cannot affect page B.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d83bd77-8081-44df-b8b8-f242e7a2d7c4

📥 Commits

Reviewing files that changed from the base of the PR and between 51ed93f and 3a894db.

📒 Files selected for processing (13)
  • webapp/i18n/en.json
  • webapp/src/components/page_editor/apply_formatting.ts
  • webapp/src/components/page_editor/autosave_indicator.tsx
  • webapp/src/components/page_editor/floating_formatting_bar.module.scss
  • webapp/src/components/page_editor/floating_formatting_bar.tsx
  • webapp/src/components/page_editor/page_editor.tsx
  • webapp/src/components/page_editor/publish_conflict_dialog.module.scss
  • webapp/src/components/page_editor/publish_conflict_dialog.tsx
  • webapp/src/components/page_editor/toolbar_controls.tsx
  • webapp/src/hooks/draft_autosave.test.tsx
  • webapp/src/hooks/draft_autosave.ts
  • webapp/src/hooks/page_draft.test.tsx
  • webapp/src/hooks/page_draft.ts
💤 Files with no reviewable changes (1)
  • webapp/src/components/page_editor/floating_formatting_bar.module.scss
🚧 Files skipped from review as they are similar to previous changes (9)
  • webapp/src/components/page_editor/publish_conflict_dialog.module.scss
  • webapp/src/components/page_editor/autosave_indicator.tsx
  • webapp/src/hooks/page_draft.ts
  • webapp/i18n/en.json
  • webapp/src/components/page_editor/toolbar_controls.tsx
  • webapp/src/components/page_editor/floating_formatting_bar.tsx
  • webapp/src/hooks/draft_autosave.test.tsx
  • webapp/src/hooks/draft_autosave.ts
  • webapp/src/components/page_editor/apply_formatting.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants