diff --git a/packages/frontend/core/src/__tests__/share-page.spec.ts b/packages/frontend/core/src/__tests__/share-page.spec.ts new file mode 100644 index 0000000000..0ff672c79b --- /dev/null +++ b/packages/frontend/core/src/__tests__/share-page.spec.ts @@ -0,0 +1,40 @@ +import { PublicDocMode } from '@affine/graphql'; +import { describe, expect, test } from 'vitest'; + +import { + getResolvedPublishMode, + getSearchWithMode, +} from '../desktop/pages/workspace/share/share-page.utils'; + +describe('getResolvedPublishMode', () => { + test('prefers the query mode when it is present', () => { + expect(getResolvedPublishMode('edgeless', PublicDocMode.Page)).toBe( + 'edgeless' + ); + expect(getResolvedPublishMode('page', PublicDocMode.Edgeless)).toBe('page'); + }); + + test('falls back to the published public mode for shared docs', () => { + expect(getResolvedPublishMode(null, PublicDocMode.Edgeless)).toBe( + 'edgeless' + ); + expect(getResolvedPublishMode(null, PublicDocMode.Page)).toBe('page'); + }); + + test('defaults to page when no mode is available', () => { + expect(getResolvedPublishMode(null, null)).toBe('page'); + expect(getResolvedPublishMode(null, undefined)).toBe('page'); + }); +}); + +describe('getSearchWithMode', () => { + test('adds mode to an empty search string', () => { + expect(getSearchWithMode('', 'edgeless')).toBe('?mode=edgeless'); + }); + + test('replaces an existing mode and preserves other params', () => { + expect(getSearchWithMode('?foo=1&mode=page&bar=2', 'edgeless')).toBe( + '?foo=1&mode=edgeless&bar=2' + ); + }); +}); diff --git a/packages/frontend/core/src/__tests__/use-share-url.utils.spec.ts b/packages/frontend/core/src/__tests__/use-share-url.utils.spec.ts new file mode 100644 index 0000000000..119ea3c53e --- /dev/null +++ b/packages/frontend/core/src/__tests__/use-share-url.utils.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from 'vitest'; + +import { getDefaultShareMode } from '../components/hooks/affine/use-share-url.utils'; + +describe('getDefaultShareMode', () => { + test('returns edgeless when the current mode is edgeless', () => { + expect(getDefaultShareMode('edgeless')).toBe('edgeless'); + }); + + test('returns undefined for page mode or an unset mode', () => { + expect(getDefaultShareMode('page')).toBeUndefined(); + expect(getDefaultShareMode(undefined)).toBeUndefined(); + }); +}); diff --git a/packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx b/packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx index 641cbcf6f3..4b594322a6 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-mode-switch/index.tsx @@ -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 ( { unsubs.forEach(unsub => unsub()); }; - }, [docId, isActiveView, isCloud, onClickCopyLink]); + }, [currentMode, docId, isActiveView, isCloud, onClickCopyLink]); } diff --git a/packages/frontend/core/src/components/hooks/affine/use-share-url.utils.ts b/packages/frontend/core/src/components/hooks/affine/use-share-url.utils.ts new file mode 100644 index 0000000000..039de4e0a9 --- /dev/null +++ b/packages/frontend/core/src/components/hooks/affine/use-share-url.utils.ts @@ -0,0 +1,7 @@ +import type { DocMode } from '@blocksuite/affine/model'; + +export const getDefaultShareMode = ( + currentMode?: DocMode +): DocMode | undefined => { + return currentMode === 'edgeless' ? 'edgeless' : undefined; +}; diff --git a/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx index 857f3d6acb..4e6f5e40f3 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx @@ -37,6 +37,7 @@ import { PageNotFound } from '../../404'; import { ShareFooter } from './share-footer'; import { ShareHeader } from './share-header'; import * as styles from './share-page.css'; +import { useSharedModeQuerySync } from './use-shared-mode-query-sync'; const useUpdateBasename = (workspace: Workspace | null) => { const location = useLocation(); @@ -106,7 +107,7 @@ export const SharePage = ({ const SharePageInner = ({ workspaceId, docId, - publishMode = 'page', + publishMode, selector, isTemplate, templateName, @@ -128,10 +129,19 @@ const SharePageInner = ({ const [noPermission, setNoPermission] = useState(false); const [editorContainer, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor(); + const resolvedPublishMode = publishMode ?? null; + const currentPublishMode = useSharedModeQuerySync({ + editor, + resolvedPublishMode, + }); useEffect(() => { + if (editor || workspace || page) { + return; + } + // create a workspace for share page - const { workspace } = workspacesService.open( + const { workspace: sharedWorkspace } = workspacesService.open( { metadata: { id: workspaceId, @@ -160,16 +170,16 @@ const SharePageInner = ({ } ); - setWorkspace(workspace); + setWorkspace(sharedWorkspace); - workspace.engine.doc - .waitForDocLoaded(workspace.id) + sharedWorkspace.engine.doc + .waitForDocLoaded(sharedWorkspace.id) .then(async () => { - const { doc } = workspace.scope.get(DocsService).open(docId); + const { doc } = sharedWorkspace.scope.get(DocsService).open(docId); doc.blockSuiteDoc.load(); doc.blockSuiteDoc.readonly = true; - await workspace.engine.doc.waitForDocLoaded(docId); + await sharedWorkspace.engine.doc.waitForDocLoaded(docId); if (!doc.blockSuiteDoc.root) { throw new Error('Doc is empty'); @@ -178,7 +188,7 @@ const SharePageInner = ({ setPage(doc); const editor = doc.scope.get(EditorsService).createEditor(); - editor.setMode(publishMode); + editor.setMode(resolvedPublishMode ?? doc.getPrimaryMode() ?? 'page'); if (selector) { editor.setSelector(selector); @@ -192,13 +202,24 @@ const SharePageInner = ({ }); }, [ docId, - workspaceId, - workspacesService, - publishMode, + editor, + page, + resolvedPublishMode, selector, + workspaceId, + workspace, + workspacesService, serverService.server.baseUrl, ]); + useEffect(() => { + if (!editor) { + return; + } + + editor.setSelector(selector); + }, [editor, selector]); + const t = useI18n(); const pageTitle = useLiveData(page?.title$); const { jumpToPageBlock, openPage } = useNavigateHelper(); @@ -244,7 +265,7 @@ const SharePageInner = ({ return ; } - if (!workspace || !page || !editor) { + if (!workspace || !page || !editor || !currentPublishMode) { return null; } @@ -252,13 +273,13 @@ const SharePageInner = ({ - +
- {publishMode === 'page' && !BUILD_CONFIG.isElectron ? ( + {currentPublishMode === 'page' && !BUILD_CONFIG.isElectron ? ( ) : null} @@ -279,7 +300,7 @@ const SharePageInner = ({ {!BUILD_CONFIG.isElectron && }
diff --git a/packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts b/packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts new file mode 100644 index 0000000000..9ce7e76a9f --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/share/share-page.utils.ts @@ -0,0 +1,21 @@ +import { PublicDocMode } from '@affine/graphql'; +import { type DocMode, DocModes } from '@blocksuite/affine/model'; + +export const getResolvedPublishMode = ( + queryMode: DocMode | null, + publicMode?: PublicDocMode | null +): DocMode => { + if (queryMode && DocModes.includes(queryMode)) { + return queryMode; + } + + return publicMode === PublicDocMode.Edgeless ? 'edgeless' : 'page'; +}; + +export const getSearchWithMode = (search: string, mode: DocMode) => { + const searchParams = new URLSearchParams(search); + searchParams.set('mode', mode); + + const nextSearch = searchParams.toString(); + return nextSearch ? `?${nextSearch}` : ''; +}; diff --git a/packages/frontend/core/src/desktop/pages/workspace/share/use-shared-mode-query-sync.ts b/packages/frontend/core/src/desktop/pages/workspace/share/use-shared-mode-query-sync.ts new file mode 100644 index 0000000000..24dcafd3f2 --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/share/use-shared-mode-query-sync.ts @@ -0,0 +1,60 @@ +import type { Editor } from '@affine/core/modules/editor'; +import type { DocMode } from '@blocksuite/affine/model'; +import { useLiveData } from '@toeverything/infra'; +import { useEffect, useRef } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { getSearchWithMode } from './share-page.utils'; + +export const useSharedModeQuerySync = ({ + editor, + resolvedPublishMode, +}: { + editor: Editor | null; + resolvedPublishMode: DocMode | null; +}) => { + const location = useLocation(); + const navigate = useNavigate(); + const currentPublishMode = useLiveData(editor?.mode$) ?? resolvedPublishMode; + const previousPublishModeRef = useRef(null); + + useEffect(() => { + if (!editor || !resolvedPublishMode) { + return; + } + + if (editor.mode$.value !== resolvedPublishMode) { + editor.setMode(resolvedPublishMode); + } + }, [editor, resolvedPublishMode]); + + useEffect(() => { + if (!currentPublishMode) { + return; + } + + if (previousPublishModeRef.current === null) { + previousPublishModeRef.current = currentPublishMode; + return; + } + + if (previousPublishModeRef.current === currentPublishMode) { + return; + } + + previousPublishModeRef.current = currentPublishMode; + + const nextSearch = getSearchWithMode(location.search, currentPublishMode); + if (nextSearch !== location.search) { + navigate( + { + pathname: location.pathname, + search: nextSearch, + }, + { replace: true } + ); + } + }, [currentPublishMode, location.pathname, location.search, navigate]); + + return currentPublishMode; +}; diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx index 0ded6696cd..290ee42bd5 100644 --- a/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx +++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx @@ -3,6 +3,7 @@ import { getSelectedNodes, useSharingUrl, } from '@affine/core/components/hooks/affine/use-share-url'; +import { getDefaultShareMode } from '@affine/core/components/hooks/affine/use-share-url.utils'; import { EditorService } from '@affine/core/modules/editor'; import { useI18n } from '@affine/i18n'; import type { DocMode } from '@blocksuite/affine/model'; @@ -46,8 +47,8 @@ export const CopyLinkButton = ({ }, [onClickCopyLink, currentMode, blockIds, elementIds]); const onCopyLink = useCallback(() => { - onClickCopyLink(); - }, [onClickCopyLink]); + onClickCopyLink(getDefaultShareMode(currentMode)); + }, [currentMode, onClickCopyLink]); return (