From 64149d909aa85f6e954e6608b7ef81b574ebc713 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Sun, 5 Apr 2026 20:43:09 +0800 Subject: [PATCH] feat: follow publish mode if no params --- .../core/src/__tests__/share-page.spec.ts | 77 ++++++++++++++++--- .../pages/workspace/share/share-page.tsx | 49 +++++++++++- .../pages/workspace/share/share-page.utils.ts | 58 +++++++++++++- tests/affine-cloud/e2e/share-page-1.spec.ts | 9 ++- 4 files changed, 173 insertions(+), 20 deletions(-) diff --git a/packages/frontend/core/src/__tests__/share-page.spec.ts b/packages/frontend/core/src/__tests__/share-page.spec.ts index 0ff672c79b..0fd9c3c32f 100644 --- a/packages/frontend/core/src/__tests__/share-page.spec.ts +++ b/packages/frontend/core/src/__tests__/share-page.spec.ts @@ -1,24 +1,21 @@ -import { PublicDocMode } from '@affine/graphql'; -import { describe, expect, test } from 'vitest'; +import { afterEach, describe, expect, test, vi } from 'vitest'; import { + fetchSharedPublishMode, getResolvedPublishMode, getSearchWithMode, + parsePublishMode, } 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'); + expect(getResolvedPublishMode('edgeless', 'page')).toBe('edgeless'); + expect(getResolvedPublishMode('page', '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'); + expect(getResolvedPublishMode(null, 'edgeless')).toBe('edgeless'); + expect(getResolvedPublishMode(null, 'page')).toBe('page'); }); test('defaults to page when no mode is available', () => { @@ -27,6 +24,66 @@ describe('getResolvedPublishMode', () => { }); }); +describe('parsePublishMode', () => { + test('accepts valid publish modes only', () => { + expect(parsePublishMode('page')).toBe('page'); + expect(parsePublishMode('edgeless')).toBe('edgeless'); + expect(parsePublishMode('invalid')).toBeNull(); + expect(parsePublishMode(null)).toBeNull(); + }); +}); + +describe('fetchSharedPublishMode', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('reads publish mode from the HEAD response', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(null, { + status: 200, + headers: { 'publish-mode': 'edgeless' }, + }) + ); + + await expect( + fetchSharedPublishMode({ + serverBaseUrl: 'https://app.affine.pro', + workspaceId: 'workspace-id', + docId: 'doc-id', + }) + ).resolves.toBe('edgeless'); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + new URL( + '/api/workspaces/workspace-id/docs/doc-id', + 'https://app.affine.pro' + ), + expect.objectContaining({ method: 'HEAD' }) + ); + }); + + test('falls back to GET when HEAD misses the header', async () => { + vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(new Response(null, { status: 200 })) + .mockResolvedValueOnce( + new Response(null, { + status: 200, + headers: { 'publish-mode': 'page' }, + }) + ); + + await expect( + fetchSharedPublishMode({ + serverBaseUrl: 'https://app.affine.pro', + workspaceId: 'workspace-id', + docId: 'doc-id', + }) + ).resolves.toBe('page'); + expect(globalThis.fetch).toHaveBeenCalledTimes(2); + }); +}); + describe('getSearchWithMode', () => { test('adds mode to an empty search string', () => { expect(getSearchWithMode('', 'edgeless')).toBe('?mode=edgeless'); 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 4e6f5e40f3..da6e6d8af3 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,10 @@ import { PageNotFound } from '../../404'; import { ShareFooter } from './share-footer'; import { ShareHeader } from './share-header'; import * as styles from './share-page.css'; +import { + fetchSharedPublishMode, + getResolvedPublishMode, +} from './share-page.utils'; import { useSharedModeQuerySync } from './use-shared-mode-query-sync'; const useUpdateBasename = (workspace: Workspace | null) => { @@ -127,19 +131,58 @@ const SharePageInner = ({ const [page, setPage] = useState(null); const [editor, setEditor] = useState(null); const [noPermission, setNoPermission] = useState(false); + const [fetchedPublishMode, setFetchedPublishMode] = useState< + DocMode | null | undefined + >(() => (publishMode === undefined ? undefined : null)); const [editorContainer, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor(); - const resolvedPublishMode = publishMode ?? null; + const resolvedPublishMode = + publishMode !== undefined + ? publishMode + : fetchedPublishMode === undefined + ? null + : getResolvedPublishMode(null, fetchedPublishMode); const currentPublishMode = useSharedModeQuerySync({ editor, resolvedPublishMode, }); useEffect(() => { - if (editor || workspace || page) { + if (publishMode !== undefined) { + setFetchedPublishMode(null); return; } + const abortController = new AbortController(); + setFetchedPublishMode(undefined); + + void fetchSharedPublishMode({ + serverBaseUrl: serverService.server.baseUrl, + workspaceId, + docId, + signal: abortController.signal, + }) + .then(mode => { + if (!abortController.signal.aborted) { + setFetchedPublishMode(mode); + } + }) + .catch(err => { + if (!abortController.signal.aborted) { + console.error(err); + setFetchedPublishMode(null); + } + }); + + return () => { + abortController.abort(); + }; + }, [docId, publishMode, serverService.server.baseUrl, workspaceId]); + + useEffect(() => { + if (resolvedPublishMode === null) return; + if (editor || workspace || page) return; + // create a workspace for share page const { workspace: sharedWorkspace } = workspacesService.open( { @@ -188,7 +231,7 @@ const SharePageInner = ({ setPage(doc); const editor = doc.scope.get(EditorsService).createEditor(); - editor.setMode(resolvedPublishMode ?? doc.getPrimaryMode() ?? 'page'); + editor.setMode(resolvedPublishMode); if (selector) { editor.setSelector(selector); 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 index 9ce7e76a9f..23cc88ab25 100644 --- 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 @@ -1,15 +1,67 @@ -import { PublicDocMode } from '@affine/graphql'; import { type DocMode, DocModes } from '@blocksuite/affine/model'; export const getResolvedPublishMode = ( queryMode: DocMode | null, - publicMode?: PublicDocMode | null + publishMode?: DocMode | null ): DocMode => { if (queryMode && DocModes.includes(queryMode)) { return queryMode; } - return publicMode === PublicDocMode.Edgeless ? 'edgeless' : 'page'; + return publishMode === 'edgeless' ? 'edgeless' : 'page'; +}; + +export const parsePublishMode = ( + publishMode: string | null | undefined +): DocMode | null => { + if (!publishMode) { + return null; + } + + return DocModes.includes(publishMode as DocMode) + ? (publishMode as DocMode) + : null; +}; + +export const fetchSharedPublishMode = async ({ + serverBaseUrl, + workspaceId, + docId, + signal, +}: { + serverBaseUrl: string; + workspaceId: string; + docId: string; + signal?: AbortSignal; +}): Promise => { + const url = new URL( + `/api/workspaces/${encodeURIComponent(workspaceId)}/docs/${encodeURIComponent(docId)}`, + serverBaseUrl + ); + const headers = { + Accept: 'application/octet-stream', + 'x-affine-version': BUILD_CONFIG.appVersion, + }; + const headResponse = await globalThis.fetch(url, { + method: 'HEAD', + headers, + signal, + }); + const headMode = parsePublishMode(headResponse.headers.get('publish-mode')); + + if (headMode || headResponse.status === 404 || !headResponse.ok) { + return headMode; + } + + const getResponse = await globalThis.fetch(url, { + headers, + signal, + }); + try { + return parsePublishMode(getResponse.headers.get('publish-mode')); + } finally { + await getResponse.body?.cancel(); + } }; export const getSearchWithMode = (search: string, mode: DocMode) => { diff --git a/tests/affine-cloud/e2e/share-page-1.spec.ts b/tests/affine-cloud/e2e/share-page-1.spec.ts index 827ab4d9b4..789c851fbe 100644 --- a/tests/affine-cloud/e2e/share-page-1.spec.ts +++ b/tests/affine-cloud/e2e/share-page-1.spec.ts @@ -200,13 +200,14 @@ test('share page with default edgeless', async ({ page, browser }) => { { const context = await browser.newContext(); await skipOnboarding(context); - const url: string = await page.evaluate(() => - navigator.clipboard.readText() + const url = new URL( + await page.evaluate(() => navigator.clipboard.readText()) ); + url.searchParams.delete('mode'); const page2 = await context.newPage(); - await page2.goto(url); + await page2.goto(url.toString()); await waitForEditorLoad(page2); - await expect(page.locator('affine-edgeless-root')).toBeVisible({ + await expect(page2.locator('affine-edgeless-root')).toBeVisible({ timeout: 1000, }); await expect(page2.locator('affine-paragraph').first()).toContainText(