From 622715d2f367d292d0af5553b28abc17fdf1d896 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Mon, 29 Jul 2024 10:19:57 +0000 Subject: [PATCH] feat(core): outline viewer (quick toc) (#7614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close: [BS-949](https://linear.app/affine-design/issue/BS-949/outline-viewer-加入到affine) Details are in this PR: https://github.com/toeverything/blocksuite/pull/7704 --- packages/common/infra/src/atom/settings.ts | 2 + .../experimental-features/index.tsx | 24 ++++++++++ .../workspace/detail-page/detail-page.tsx | 16 ++++++- .../detail-page/outline-viewer.css.ts | 8 ++++ .../workspace/detail-page/outline-viewer.tsx | 39 +++++++++++++++ .../workspace/detail-page/tabs/outline.tsx | 8 ++-- .../e2e/blocksuite/editor.spec.ts | 47 +++++++++++++++++++ 7 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.css.ts create mode 100644 packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.tsx diff --git a/packages/common/infra/src/atom/settings.ts b/packages/common/infra/src/atom/settings.ts index 37df9ef8c..e0ce75301 100644 --- a/packages/common/infra/src/atom/settings.ts +++ b/packages/common/infra/src/atom/settings.ts @@ -33,6 +33,7 @@ export type AppSetting = { autoDownloadUpdate: boolean; enableMultiView: boolean; enableTelemetry: boolean; + enableOutlineViewer: boolean; editorFlags: Partial>; }; export const windowFrameStyleOptions: AppSetting['windowFrameStyle'][] = [ @@ -74,6 +75,7 @@ const appSettingBaseAtom = atomWithStorage('affine-settings', { autoDownloadUpdate: true, enableTelemetry: true, enableMultiView: false, + enableOutlineViewer: false, editorFlags: {}, }); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx index 74a75aca3..e4400f03f 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx @@ -80,11 +80,13 @@ const ExperimentalFeaturesItem = ({ isMutating, checked, onChange, + testId, }: { title: React.ReactNode; isMutating?: boolean; checked: boolean; onChange: (checked: boolean) => void; + testId?: string; }) => { return (
@@ -93,6 +95,7 @@ const ExperimentalFeaturesItem = ({ checked={checked} onChange={onChange} className={isMutating ? styles.switchDisabled : ''} + data-testid={testId} />
); @@ -121,6 +124,26 @@ const SplitViewSettingRow = () => { ); }; +const OutlineViewerSettingRow = () => { + const { appSettings, updateSettings } = useAppSettingHelper(); + + const onToggle = useCallback( + (checked: boolean) => { + updateSettings('enableOutlineViewer', checked); + }, + [updateSettings] + ); + + return ( + + ); +}; + // feature flag -> display name const blocksuiteFeatureFlags: Partial> = { enable_expand_database_block: 'Enable Expand Database Block', @@ -177,6 +200,7 @@ const ExperimentalFeaturesMain = () => { > + ); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index a7047b8c2..5e6ba728d 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -63,10 +63,11 @@ import { performanceRenderLogger } from '../../../shared'; import { PageNotFound } from '../../404'; import * as styles from './detail-page.css'; import { DetailPageHeader } from './detail-page-header'; +import { EditorOutlineViewer } from './outline-viewer'; import { EditorChatPanel } from './tabs/chat'; import { EditorFramePanel } from './tabs/frame'; import { EditorJournalPanel } from './tabs/journal'; -import { EditorOutline } from './tabs/outline'; +import { EditorOutlinePanel } from './tabs/outline'; const DetailPageImpl = memo(function DetailPageImpl() { const workbench = useService(WorkbenchService).workbench; @@ -206,6 +207,11 @@ const DetailPageImpl = memo(function DetailPageImpl() { [jumpToPageBlock, docCollection.id, openPage, jumpToTag, workspace.id] ); + const openOutlinePanel = useCallback(() => { + workbench.openSidebar(); + view.activeSidebarTab('outline'); + }, [workbench, view]); + return ( <> @@ -239,6 +245,12 @@ const DetailPageImpl = memo(function DetailPageImpl() { {isInTrash ? : null} + {appSettings.enableOutlineViewer && ( + + )} } unmountOnInactive={false}> @@ -250,7 +262,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { }> - + }> diff --git a/packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.css.ts b/packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.css.ts new file mode 100644 index 000000000..d58b79f3d --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; + +export const root = style({ + position: 'fixed', + top: 256, + right: 22, + maxHeight: 'calc(100% - 16px)', +}); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.tsx b/packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.tsx new file mode 100644 index 000000000..ca760f693 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/detail-page/outline-viewer.tsx @@ -0,0 +1,39 @@ +import type { AffineEditorContainer } from '@blocksuite/presets'; +import { OutlineViewer } from '@blocksuite/presets'; +import { useCallback, useRef } from 'react'; + +import * as styles from './outline-viewer.css'; + +export const EditorOutlineViewer = ({ + editor, + toggleOutlinePanel, +}: { + editor: AffineEditorContainer | null; + toggleOutlinePanel: () => void; +}) => { + const outlineViewerRef = useRef(null); + + const onRefChange = useCallback((container: HTMLDivElement | null) => { + if (container) { + if (outlineViewerRef.current === null) { + console.error('outline viewer should be initialized'); + return; + } + + container.append(outlineViewerRef.current); + } + }, []); + + if (!editor) { + return; + } + + if (!outlineViewerRef.current) { + outlineViewerRef.current = new OutlineViewer(); + (outlineViewerRef.current as OutlineViewer).editor = editor; + (outlineViewerRef.current as OutlineViewer).toggleOutlinePanel = + toggleOutlinePanel; + } + + return
; +}; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.tsx b/packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.tsx index 2ed0d7f76..999d134ff 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/tabs/outline.tsx @@ -1,4 +1,3 @@ -import { assertExists } from '@blocksuite/global/utils'; import type { AffineEditorContainer } from '@blocksuite/presets'; import { OutlinePanel } from '@blocksuite/presets'; import { useCallback, useRef } from 'react'; @@ -6,7 +5,7 @@ import { useCallback, useRef } from 'react'; import * as styles from './outline.css'; // A wrapper for TOCNotesPanel -export const EditorOutline = ({ +export const EditorOutlinePanel = ({ editor, }: { editor: AffineEditorContainer | null; @@ -15,7 +14,10 @@ export const EditorOutline = ({ const onRefChange = useCallback((container: HTMLDivElement | null) => { if (container) { - assertExists(outlinePanelRef.current, 'toc panel should be initialized'); + if (outlinePanelRef.current === null) { + console.error('outline panel should be initialized'); + return; + } container.append(outlinePanelRef.current); } }, []); diff --git a/tests/affine-local/e2e/blocksuite/editor.spec.ts b/tests/affine-local/e2e/blocksuite/editor.spec.ts index 40b4e0568..ea461ea79 100644 --- a/tests/affine-local/e2e/blocksuite/editor.spec.ts +++ b/tests/affine-local/e2e/blocksuite/editor.spec.ts @@ -5,6 +5,11 @@ import { getBlockSuiteEditorTitle, waitForEditorLoad, } from '@affine-test/kit/utils/page-logic'; +import { + confirmExperimentalPrompt, + openExperimentalFeaturesPanel, + openSettingModal, +} from '@affine-test/kit/utils/setting'; import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; @@ -73,3 +78,45 @@ test('link page is useable', async ({ page }) => { page.locator('.doc-title-container:has-text("page1")') ).toBeVisible(); }); + +test('outline viewer is useable', async ({ page }) => { + await openHomePage(page); + await waitForEditorLoad(page); + await clickNewPageButton(page); + await waitForEditorLoad(page); + + await openSettingModal(page); + await openExperimentalFeaturesPanel(page); + const prompt = page.getByTestId('experimental-prompt'); + await expect(prompt).toBeVisible(); + await confirmExperimentalPrompt(page); + const settings = page.getByTestId('experimental-settings'); + const enableOutlineViewerSetting = settings.getByTestId( + 'outline-viewer-switch' + ); + await expect(enableOutlineViewerSetting).toBeVisible(); + await enableOutlineViewerSetting.click(); + await page.waitForTimeout(500); + await page.getByTestId('modal-close-button').click(); + await page.waitForTimeout(500); + + const title = getBlockSuiteEditorTitle(page); + await title.pressSequentially('Title'); + await page.keyboard.press('Enter'); + expect(await title.innerText()).toBe('Title'); + await page.keyboard.type('# '); + await page.keyboard.type('Heading 1'); + await page.keyboard.press('Enter'); + await page.keyboard.type('## '); + await page.keyboard.type('Heading 2'); + await page.keyboard.press('Enter'); + + const indicators = page.locator('.outline-heading-indicator'); + await expect(indicators).toHaveCount(2); + await expect(indicators.nth(0)).toBeVisible(); + await expect(indicators.nth(1)).toBeVisible(); + + const viewer = page.locator('affine-outline-panel-body'); + await indicators.first().hover(); + await expect(viewer).toBeVisible(); +});