mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
feat(core): outline viewer (quick toc) (#7614)
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
This commit is contained in:
@@ -33,6 +33,7 @@ export type AppSetting = {
|
||||
autoDownloadUpdate: boolean;
|
||||
enableMultiView: boolean;
|
||||
enableTelemetry: boolean;
|
||||
enableOutlineViewer: boolean;
|
||||
editorFlags: Partial<Omit<BlockSuiteFlags, 'readonly'>>;
|
||||
};
|
||||
export const windowFrameStyleOptions: AppSetting['windowFrameStyle'][] = [
|
||||
@@ -74,6 +75,7 @@ const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
|
||||
autoDownloadUpdate: true,
|
||||
enableTelemetry: true,
|
||||
enableMultiView: false,
|
||||
enableOutlineViewer: false,
|
||||
editorFlags: {},
|
||||
});
|
||||
|
||||
|
||||
+24
@@ -80,11 +80,13 @@ const ExperimentalFeaturesItem = ({
|
||||
isMutating,
|
||||
checked,
|
||||
onChange,
|
||||
testId,
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
isMutating?: boolean;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
testId?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.switchRow}>
|
||||
@@ -93,6 +95,7 @@ const ExperimentalFeaturesItem = ({
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className={isMutating ? styles.switchDisabled : ''}
|
||||
data-testid={testId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -121,6 +124,26 @@ const SplitViewSettingRow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const OutlineViewerSettingRow = () => {
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
|
||||
const onToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
updateSettings('enableOutlineViewer', checked);
|
||||
},
|
||||
[updateSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExperimentalFeaturesItem
|
||||
title="Outline Viewer"
|
||||
checked={appSettings.enableOutlineViewer}
|
||||
onChange={onToggle}
|
||||
testId="outline-viewer-switch"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// feature flag -> display name
|
||||
const blocksuiteFeatureFlags: Partial<Record<keyof BlockSuiteFlags, string>> = {
|
||||
enable_expand_database_block: 'Enable Expand Database Block',
|
||||
@@ -177,6 +200,7 @@ const ExperimentalFeaturesMain = () => {
|
||||
>
|
||||
<SplitViewSettingRow />
|
||||
<BlocksuiteFeatureFlagSettings />
|
||||
<OutlineViewerSettingRow />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<ViewHeader>
|
||||
@@ -239,6 +245,12 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
</AffineErrorBoundary>
|
||||
{isInTrash ? <TrashPageFooter /> : null}
|
||||
</div>
|
||||
{appSettings.enableOutlineViewer && (
|
||||
<EditorOutlineViewer
|
||||
editor={editor}
|
||||
toggleOutlinePanel={openOutlinePanel}
|
||||
/>
|
||||
)}
|
||||
</ViewBody>
|
||||
|
||||
<ViewSidebarTab tabId="chat" icon={<AiIcon />} unmountOnInactive={false}>
|
||||
@@ -250,7 +262,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
</ViewSidebarTab>
|
||||
|
||||
<ViewSidebarTab tabId="outline" icon={<TocIcon />}>
|
||||
<EditorOutline editor={editor} />
|
||||
<EditorOutlinePanel editor={editor} />
|
||||
</ViewSidebarTab>
|
||||
|
||||
<ViewSidebarTab tabId="frame" icon={<FrameIcon />}>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
position: 'fixed',
|
||||
top: 256,
|
||||
right: 22,
|
||||
maxHeight: 'calc(100% - 16px)',
|
||||
});
|
||||
@@ -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<OutlineViewer | null>(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 <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user