Skip to content

fix: shared page mode syncing#14756

Open
ibex088 wants to merge 6 commits intotoeverything:canaryfrom
ibex088:fix/shared-mode-link-sync
Open

fix: shared page mode syncing#14756
ibex088 wants to merge 6 commits intotoeverything:canaryfrom
ibex088:fix/shared-mode-link-sync

Conversation

@ibex088
Copy link
Copy Markdown
Contributor

@ibex088 ibex088 commented Mar 31, 2026

Summary

This fixes a few inconsistencies in shared page behavior:
fixes #14751

  • shared pages now open in the correct published mode when the URL does not already include ?mode=...
  • switching between page and edgeless in shared mode now keeps the URL query param in sync
  • the default Copy Link action now follows the current editor mode
  • shared viewers can toggle between page and edgeless mode in readonly share pages

What Changed

  • updated shared page mode resolution to prefer URL mode, with backend publish mode as fallback
  • added query-param syncing for shared page mode changes
  • made the default share link copy use:
    • page link in page mode
    • edgeless link in edgeless mode
  • allowed EditorModeSwitch to toggle both ways in shared mode
  • extracted shared-mode behavior into small hooks to keep share-page.tsx cleaner

Demo

https://www.loom.com/share/a287172321fb4fc5b94f7c67a39298a9

Summary by CodeRabbit

  • New Features

    • Mode switching between page and edgeless no longer blocked by shared gating; shared pages initialize and respect the resolved editor mode.
    • Shared page URLs stay in sync with editor mode and copy-link actions include/preserve the selected mode.
  • Tests

    • Added tests for publish-mode resolution, query-string mode handling, and default share-mode behavior.
  • Bug Fixes

    • Updated shared-page “not found” UI text to match new messaging.

@ibex088 ibex088 requested a review from a team as a code owner March 31, 2026 15:35
@github-actions github-actions bot added test Related to test cases app:core labels Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e54f16ee-ce33-4bf3-9e9a-00b01bd78975

📥 Commits

Reviewing files that changed from the base of the PR and between 7cbe17c and 48d61f6.

📒 Files selected for processing (3)
  • packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx
  • packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx
  • tests/affine-cloud/e2e/share-page-2.spec.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/affine-cloud/e2e/share-page-2.spec.ts
  • packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx

📝 Walkthrough

Walkthrough

Adds utilities and hooks to resolve a shared document's publish mode from URL, server, and editor state; syncs editor mode with the URL; removes shared-mode gating from mode-switch logic; propagates resolved mode into share-page initialization and copy-link flows; and adds tests for the new utilities and default-mode helper.

Changes

Cohort / File(s) Summary
Utilities & Tests
packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts, packages/frontend/core/src/__tests__/share-page.spec.ts, packages/frontend/core/src/__tests__/use-share-url.utils.spec.ts
Added getResolvedPublishMode and getSearchWithMode; tests verifying those utilities and getDefaultShareMode.
Shared Mode Hooks
packages/frontend/core/src/desktop/pages/workspace/share/use-shared-publish-mode.ts, packages/frontend/core/src/desktop/pages/workspace/share/use-shared-mode-query-sync.ts
New hooks: useSharedPublishMode (fetches/derives resolved publish mode, handles errors) and useSharedModeQuerySync (keeps editor mode and URL query param synchronized).
Share-page integration
packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx
Defer mode resolution to hooks; wait for resolvedPublishMode/currentPublishMode before rendering; initialize editor with resolved mode; add editor selector sync and early error handling.
Mode switch behavior
packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx
Removed isSharedMode gating from toggle callbacks, hide logic, and related effect dependencies.
Copy-link flows
packages/frontend/core/src/components/hooks/affine/use-register-copy-link-commands.tsx, packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx, packages/frontend/core/src/components/hooks/affine/use-share-url.utils.ts
Added/exported getDefaultShareMode; copy-link handlers now compute and pass default share mode (derived from editor mode) into link-copy callbacks; hook subscribes to live editor mode.
E2E & Text update
tests/affine-cloud/e2e/share-page-2.spec.ts
Updated expected UI text in an E2E test to match changed message.

Sequence Diagram

sequenceDiagram
    participant User
    participant SharePage as "Share Page"
    participant Hooks as "Mode Hooks"
    participant Server as "Server/GraphQL"
    participant Editor as "Editor Service"
    participant URL as "Browser / Location"

    User->>SharePage: Open shared page link (may include ?mode=)

    rect rgba(100,150,200,0.5)
    SharePage->>Hooks: useSharedPublishMode(docId, publishMode?, workspaceId)
    Hooks->>Server: Fetch page public mode if publishMode absent
    Server-->>Hooks: Return public mode
    Hooks->>Hooks: getResolvedPublishMode(queryMode, publicMode)
    Hooks-->>SharePage: resolvedPublishMode
    end

    rect rgba(150,100,200,0.5)
    SharePage->>Editor: Initialize / setMode(resolvedPublishMode)
    Editor->>Editor: editor.mode$ = resolvedPublishMode
    end

    rect rgba(100,200,150,0.5)
    SharePage->>Hooks: useSharedModeQuerySync(editor, resolvedPublishMode)
    Hooks->>Editor: subscribe to editor.mode$
    Editor-->>Hooks: currentMode
    Hooks->>Hooks: getSearchWithMode(location.search, currentMode)
    Hooks->>URL: replaceState with updated search (if changed)
    end

    User->>Editor: Toggle mode (page ↔ edgeless)
    Editor->>Hooks: Emit mode change
    Hooks->>URL: Sync mode to query param
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I nibble query strings and modes with glee,

server whispers, editor hums back to me.
No shared-gates to stall the hop,
links now carry the mode on top.
A tiny rabbit cheers the synced spree.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: shared page mode syncing' accurately describes the main change: addressing mode synchronization issues in shared pages across editor state and URL query parameters.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #14751: ensuring shared Edgeless/Board pages render correctly by resolving the correct publish mode, syncing editor mode with URL parameters, and enabling mode switching for shared viewers.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing shared page mode behavior. The modifications include new utility functions, hooks, updated components, and test files—all directly supporting the objective of proper mode syncing in shared pages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts (1)

4-8: Accept raw query values here.

URLSearchParams.get('mode') yields string | null, so callers currently have to cast arbitrary input to DocMode before this helper can validate it. Widening the parameter keeps that unsafe cast out of the caller and makes DocModes.includes(...) the single validation point.

Suggested refactor
 export const getResolvedPublishMode = (
-  queryMode: DocMode | null,
+  queryMode: string | null | undefined,
   publicMode?: PublicDocMode | null
 ): DocMode => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts`
around lines 4 - 8, The getResolvedPublishMode function should accept raw query
input instead of forcing callers to cast: change the queryMode parameter type
from DocMode | null to string | null and update its validation to use
DocModes.includes(queryMode as DocMode) (or similar runtime check) so
DocModes.includes(...) is the single validation point; ensure existing logic
that checks publicMode (PublicDocMode) remains unchanged and that any returned
DocMode values are still typed correctly by narrowing after the includes check.
packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx (1)

219-225: Avoid replaying the same selector on first load.

The editor already receives selector during bootstrap at Line 198, so this effect immediately reapplies the same setSelector once setEditor commits. If setSelector scrolls or focuses, shared deep links will jump twice. Consider skipping the initial run or comparing the previous selector before reapplying.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx`
around lines 219 - 225, The effect re-applies the same selector on mount because
editor was already initialized with selector; change the useEffect that calls
editor.setSelector(selector) so it skips the initial replay: introduce a ref
like hasAppliedSelectorRef or prevSelectorRef, on first run avoid calling
editor.setSelector if prevSelectorRef.current === selector (or if
hasAppliedSelectorRef is false and editor was bootstrapped), and only call
editor.setSelector when selector differs from prevSelectorRef.current; after
calling, update the ref. Target the useEffect with dependencies [editor,
selector] and the editor.setSelector call to implement this guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/frontend/core/src/components/hooks/affine/use-share-url.utils.ts`:
- Around line 3-4: The helper getDefaultShareMode currently coerces an absent
currentMode into 'page', which breaks the intended fallback in onClickCopyLink;
change getDefaultShareMode (signature and implementation) to return undefined
when currentMode is not 'edgeless' so callers can treat an omitted mode as "use
current view" (i.e., make the return type DocMode | undefined and return
'edgeless' only when currentMode === 'edgeless', otherwise return undefined).

In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx`:
- Around line 133-145: The component is treating a coerced default of 'page'
from useSharedPublishMode as authoritative (via resolvedPublishMode), which lets
transient getWorkspacePageByIdQuery failures boot an empty page; change
useSharedPublishMode (and/or getWorkspacePageByIdQuery) to avoid falling back to
the literal 'page' on query errors — instead return a tri-state (loading | error
| value) or undefined for unresolved mode so failures are distinguishable, and
update the caller in share-page.tsx (the useEffect that checks
resolvedPublishMode, editor, workspace, page and the later application at Line
~196) to keep the UI in loading state until resolvedPublishMode is a reliable
value or to attempt resolving from the public-share source; reference hooks:
useSharedPublishMode, getWorkspacePageByIdQuery, and useSharedModeQuerySync, and
ensure no implicit coercion to 'page' occurs on query errors.

In
`@packages/frontend/core/src/desktop/pages/workspace/share/use-shared-publish-mode.ts`:
- Around line 19-27: The hook initializes resolvedPublishMode from publishMode
only on mount, so when docId changes and publishMode is undefined it retains the
previous doc's mode; update the effect that runs when docId or publishMode
change (the useEffect around AbortController and fetching) to explicitly reset
resolvedPublishMode to null when docId changes and publishMode is not provided,
e.g. call setResolvedPublishMode(null) at the start of that effect before
issuing the GraphQL fetch; ensure the effect dependencies include docId and
publishMode so the reset happens on navigation and the subsequent fetch sets the
correct mode.

---

Nitpick comments:
In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx`:
- Around line 219-225: The effect re-applies the same selector on mount because
editor was already initialized with selector; change the useEffect that calls
editor.setSelector(selector) so it skips the initial replay: introduce a ref
like hasAppliedSelectorRef or prevSelectorRef, on first run avoid calling
editor.setSelector if prevSelectorRef.current === selector (or if
hasAppliedSelectorRef is false and editor was bootstrapped), and only call
editor.setSelector when selector differs from prevSelectorRef.current; after
calling, update the ref. Target the useEffect with dependencies [editor,
selector] and the editor.setSelector call to implement this guard.

In
`@packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts`:
- Around line 4-8: The getResolvedPublishMode function should accept raw query
input instead of forcing callers to cast: change the queryMode parameter type
from DocMode | null to string | null and update its validation to use
DocModes.includes(queryMode as DocMode) (or similar runtime check) so
DocModes.includes(...) is the single validation point; ensure existing logic
that checks publicMode (PublicDocMode) remains unchanged and that any returned
DocMode values are still typed correctly by narrowing after the includes check.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44e385ab-a889-44ff-8c3b-cb7387c42788

📥 Commits

Reviewing files that changed from the base of the PR and between 91ad783 and 784e271.

📒 Files selected for processing (10)
  • packages/frontend/core/src/__tests__/share-page.spec.ts
  • packages/frontend/core/src/__tests__/use-share-url.utils.spec.ts
  • packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx
  • packages/frontend/core/src/components/hooks/affine/use-register-copy-link-commands.tsx
  • packages/frontend/core/src/components/hooks/affine/use-share-url.utils.ts
  • packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx
  • packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts
  • packages/frontend/core/src/desktop/pages/workspace/share/use-shared-mode-query-sync.ts
  • packages/frontend/core/src/desktop/pages/workspace/share/use-shared-publish-mode.ts
  • packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx

@ibex088 ibex088 force-pushed the fix/shared-mode-link-sync branch from 784e271 to d3fd46d Compare March 31, 2026 17:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx (1)

143-217: Cancel the async share-page bootstrap when inputs change.

This effect never cleans up its promise chain, so a route or mode change while either waitForDocLoaded(...) call is pending can still run the old setPage / setEditor path and apply a stale mode at Line 196. Add a cancelled flag and bail out before each state mutation; if Workspace/Editor has explicit disposal APIs, use them in the cleanup too.

Possible guard
  useEffect(() => {
+    let cancelled = false;
+
     if (!resolvedPublishMode || editor || workspace || page) {
       return;
     }

     // create a workspace for share page
     const { workspace: sharedWorkspace } = workspacesService.open(
@@
-    setWorkspace(sharedWorkspace);
+    setWorkspace(sharedWorkspace);

     sharedWorkspace.engine.doc
       .waitForDocLoaded(sharedWorkspace.id)
       .then(async () => {
+        if (cancelled) {
+          return;
+        }
+
         const { doc } = sharedWorkspace.scope.get(DocsService).open(docId);
         doc.blockSuiteDoc.load();
         doc.blockSuiteDoc.readonly = true;

         await sharedWorkspace.engine.doc.waitForDocLoaded(docId);
+
+        if (cancelled) {
+          return;
+        }

         if (!doc.blockSuiteDoc.root) {
           throw new Error('Doc is empty');
         }

         setPage(doc);

         const editor = doc.scope.get(EditorsService).createEditor();
         editor.setMode(resolvedPublishMode);

         if (selector) {
           editor.setSelector(selector);
         }

-        setEditor(editor);
+        if (!cancelled) {
+          setEditor(editor);
+        }
       })
       .catch(err => {
+        if (cancelled) {
+          return;
+        }
+
         console.error(err);
         setNoPermission(true);
       });
+
+    return () => {
+      cancelled = true;
+    };
   }, [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx`
around lines 143 - 217, The effect that opens a shared workspace and awaits
sharedWorkspace.engine.doc.waitForDocLoaded(...) lacks cleanup, so add a
cancellation guard and dispose logic: introduce a local "cancelled" boolean (or
AbortController) at the top of the useEffect and in the returned cleanup set
cancelled = true and call any relevant disposal methods (e.g.,
sharedWorkspace.close()/dispose() or editor.dispose()/workspace.dispose() if
available) to prevent stale mutations; before every setWorkspace, setPage,
setEditor, editor.setMode, editor.setSelector and setNoPermission call inside
the .then/.catch handlers check the cancelled flag and bail out if true so stale
async completions don't apply old state. Ensure you reference the same symbols:
sharedWorkspace, sharedWorkspace.engine.doc.waitForDocLoaded, doc.blockSuiteDoc,
setWorkspace, setPage, setEditor, and editor (and dispose APIs if present).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx`:
- Around line 268-277: The current guards turn publish-mode lookup failures into
a blank page; update the conditions so hasPublishModeError does not fall through
to the generic null loading guard: remove hasPublishModeError from the early
"return null" check and add an explicit branch that when hasPublishModeError &&
!currentPublishMode renders an error UI (e.g., a PublishModeError component or
reuse PageNotFound with an error message) instead of returning null; adjust the
three-way guard that checks workspace, page, editor, currentPublishMode so only
genuine loading states return null while publish-mode failures are handled by
the new error branch (refer to hasPublishModeError, currentPublishMode,
workspace, page, editor, and PageNotFound/use-shared-publish-mode.ts).

---

Nitpick comments:
In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx`:
- Around line 143-217: The effect that opens a shared workspace and awaits
sharedWorkspace.engine.doc.waitForDocLoaded(...) lacks cleanup, so add a
cancellation guard and dispose logic: introduce a local "cancelled" boolean (or
AbortController) at the top of the useEffect and in the returned cleanup set
cancelled = true and call any relevant disposal methods (e.g.,
sharedWorkspace.close()/dispose() or editor.dispose()/workspace.dispose() if
available) to prevent stale mutations; before every setWorkspace, setPage,
setEditor, editor.setMode, editor.setSelector and setNoPermission call inside
the .then/.catch handlers check the cancelled flag and bail out if true so stale
async completions don't apply old state. Ensure you reference the same symbols:
sharedWorkspace, sharedWorkspace.engine.doc.waitForDocLoaded, doc.blockSuiteDoc,
setWorkspace, setPage, setEditor, and editor (and dispose APIs if present).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: a94a0427-3738-41fa-a7de-ddf013050921

📥 Commits

Reviewing files that changed from the base of the PR and between 784e271 and d3fd46d.

📒 Files selected for processing (2)
  • packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx
  • packages/frontend/core/src/desktop/pages/workspace/share/use-shared-publish-mode.ts

Comment on lines +268 to 277
if (hasPublishModeError && !workspace && !page && !editor) {
return null;
}

if (noPermission) {
return <PageNotFound noPermission />;
}

if (!workspace || !page || !editor) {
if (!workspace || !page || !editor || !currentPublishMode) {
return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t turn publish-mode lookup failures into a blank page.

When packages/frontend/core/src/desktop/pages/workspace/share/use-shared-publish-mode.ts sets hasPublishModeError at Lines 54-60, this component still renders null here, and the loading guard below does the same. A failed mode lookup now looks like an infinite blank screen instead of a recoverable error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx`
around lines 268 - 277, The current guards turn publish-mode lookup failures
into a blank page; update the conditions so hasPublishModeError does not fall
through to the generic null loading guard: remove hasPublishModeError from the
early "return null" check and add an explicit branch that when
hasPublishModeError && !currentPublishMode renders an error UI (e.g., a
PublishModeError component or reuse PageNotFound with an error message) instead
of returning null; adjust the three-way guard that checks workspace, page,
editor, currentPublishMode so only genuine loading states return null while
publish-mode failures are handled by the new error branch (refer to
hasPublishModeError, currentPublishMode, workspace, page, editor, and
PageNotFound/use-shared-publish-mode.ts).

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 57.34%. Comparing base (64f3858) to head (48d61f6).
⚠️ Report is 4 commits behind head on canary.

Files with missing lines Patch % Lines
.../desktop/pages/workspace/share/share-page.utils.ts 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           canary   #14756      +/-   ##
==========================================
- Coverage   57.83%   57.34%   -0.50%     
==========================================
  Files        2959     2962       +3     
  Lines      165709   165746      +37     
  Branches    24418    24294     -124     
==========================================
- Hits        95843    95045     -798     
- Misses      66816    67648     +832     
- Partials     3050     3053       +3     
Flag Coverage Δ
server-test 76.25% <ø> (-0.88%) ⬇️
unittest 34.62% <90.90%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

app:core test Related to test cases

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Shared Edgeless/Board pages render login page for unauthenticated visitors (self-hosted)

2 participants