fix(core): shared page mode syncing (#14756)

### Summary
This fixes a few inconsistencies in shared page behavior:
fixes https://github.com/toeverything/AFFiNE/issues/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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
This commit is contained in:
chauhan_s
2026-04-05 17:50:58 +05:30
committed by GitHub
parent aa48c1c18b
commit f81abe692d
10 changed files with 201 additions and 31 deletions
@@ -39,7 +39,6 @@ export const EditorModeSwitch = () => {
const t = useI18n();
const editor = useService(EditorService).editor;
const trash = useLiveData(editor.doc.trash$);
const isSharedMode = editor.isSharedMode;
const currentMode = useLiveData(editor.mode$);
const view = useServiceOptional(ViewService)?.view;
const workbench = useServiceOptional(WorkbenchService)?.workbench;
@@ -47,18 +46,18 @@ export const EditorModeSwitch = () => {
const isActiveView = activeView?.id && activeView?.id === view?.id;
const togglePage = useCallback(() => {
if (currentMode === 'page' || isSharedMode || trash) return;
if (currentMode === 'page' || trash) return;
editor.setMode('page');
editor.setSelector(undefined);
track.$.header.actions.switchPageMode({ mode: 'page' });
}, [currentMode, editor, isSharedMode, trash]);
}, [currentMode, editor, trash]);
const toggleEdgeless = useCallback(() => {
if (currentMode === 'edgeless' || isSharedMode || trash) return;
if (currentMode === 'edgeless' || trash) return;
editor.setMode('edgeless');
editor.setSelector(undefined);
track.$.header.actions.switchPageMode({ mode: 'edgeless' });
}, [currentMode, editor, isSharedMode, trash]);
}, [currentMode, editor, trash]);
const onModeChange = useCallback(
(mode: DocMode) => {
@@ -68,13 +67,12 @@ export const EditorModeSwitch = () => {
);
const shouldHide = useCallback(
(mode: DocMode) => (trash || isSharedMode) && currentMode !== mode,
[currentMode, isSharedMode, trash]
(mode: DocMode) => trash && currentMode !== mode,
[currentMode, trash]
);
useEffect(() => {
if (trash || isSharedMode || currentMode === undefined || !isActiveView)
return;
if (trash || currentMode === undefined || !isActiveView) return;
return registerAffineCommand({
id: 'affine:doc-mode-switch',
category: 'editor:page',
@@ -89,7 +87,7 @@ export const EditorModeSwitch = () => {
},
run: () => onModeChange(currentMode === 'edgeless' ? 'page' : 'edgeless'),
});
}, [currentMode, isActiveView, isSharedMode, onModeChange, t, trash]);
}, [currentMode, isActiveView, onModeChange, t, trash]);
return (
<PureEditorModeSwitch