fix(core): update outline viewer style (#7641)

## What changes
- Update responsive style and fix some bug of outline viewer (https://github.com/toeverything/blocksuite/pull/7759)
- Change left and right padding of full-width editor from `15px` to `72px`
- Hide outline viewer when side outline panel is opened ([BS-987](https://linear.app/affine-design/issue/BS-987/逻辑-bug-toc-入口和-toc-侧边栏共存))
- Add entries of outline panel and frame panel in more menu of detail page header ( [BS-996](https://linear.app/affine-design/issue/BS-996/page-mode-下的-page-option-缺少-view-table-of-contents-的入口) , [BS-1006](https://linear.app/affine-design/issue/BS-1006/edgeless-mode-的-page-options-里缺少-view-all-frames))
- Add outline viewer to dock peek preview ( [BS-995](https://linear.app/affine-design/issue/BS-995/center-peek-里缺少-quick-toc-的入口) )
- Add more e2e tests for outline viewer
This commit is contained in:
L-Sun 2024-08-05 03:57:48 +00:00
parent 545bd032a7
commit bd31c8388c
No known key found for this signature in database
GPG Key ID: D5C252102D2B576F
18 changed files with 264 additions and 81 deletions

View File

@ -27,4 +27,4 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"version": "0.15.0" "version": "0.15.0"
} }

View File

@ -74,4 +74,4 @@
} }
}, },
"version": "0.15.0" "version": "0.15.0"
} }

View File

@ -111,4 +111,4 @@
"vitest": "1.6.0" "vitest": "1.6.0"
}, },
"version": "0.15.0" "version": "0.15.0"
} }

View File

@ -116,4 +116,4 @@
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"vitest": "1.6.0" "vitest": "1.6.0"
} }
} }

View File

@ -20,6 +20,7 @@ import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-h
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { mixpanel } from '@affine/core/mixpanel'; import { mixpanel } from '@affine/core/mixpanel';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view';
import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive'; import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@ -29,6 +30,7 @@ import {
EditIcon, EditIcon,
FavoritedIcon, FavoritedIcon,
FavoriteIcon, FavoriteIcon,
FrameIcon,
HistoryIcon, HistoryIcon,
ImportIcon, ImportIcon,
InformationIcon, InformationIcon,
@ -36,6 +38,7 @@ import {
PageIcon, PageIcon,
ShareIcon, ShareIcon,
SplitViewIcon, SplitViewIcon,
TocIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import { import {
@ -84,6 +87,24 @@ export const PageHeaderMenuButton = ({
const { importFile } = usePageHelper(docCollection); const { importFile } = usePageHelper(docCollection);
const { setTrashModal } = useTrashModalHelper(docCollection); const { setTrashModal } = useTrashModalHelper(docCollection);
const view = useService(ViewService).view;
const openSidePanel = useCallback(
(id: string) => {
workbench.openSidebar();
view.activeSidebarTab(id);
},
[workbench, view]
);
const openAllFrames = useCallback(() => {
openSidePanel('frame');
}, [openSidePanel]);
const openOutlinePanel = useCallback(() => {
openSidePanel('outline');
}, [openSidePanel]);
const [historyModalOpen, setHistoryModalOpen] = useState(false); const [historyModalOpen, setHistoryModalOpen] = useState(false);
const setOpenHistoryTipsModal = useSetAtom(openHistoryTipsModalAtom); const setOpenHistoryTipsModal = useSetAtom(openHistoryTipsModalAtom);
@ -297,6 +318,33 @@ export const PageHeaderMenuButton = ({
{t['com.affine.page-properties.page-info.view']()} {t['com.affine.page-properties.page-info.view']()}
</MenuItem> </MenuItem>
)} )}
{currentMode === 'page' ? (
<MenuItem
preFix={
<MenuIcon>
<TocIcon />
</MenuIcon>
}
data-testid="editor-option-toc"
onSelect={openOutlinePanel}
style={menuItemStyle}
>
{t['com.affine.header.option.view-toc']()}
</MenuItem>
) : (
<MenuItem
preFix={
<MenuIcon>
<FrameIcon />
</MenuIcon>
}
data-testid="editor-option-frame"
onSelect={openAllFrames}
style={menuItemStyle}
>
{t['com.affine.header.option.view-frame']()}
</MenuItem>
)}
<MenuItem <MenuItem
preFix={ preFix={
<MenuIcon> <MenuIcon>

View File

@ -6,10 +6,12 @@ import * as styles from './outline-viewer.css';
export const EditorOutlineViewer = ({ export const EditorOutlineViewer = ({
editor, editor,
toggleOutlinePanel, show,
openOutlinePanel,
}: { }: {
editor: AffineEditorContainer | null; editor: AffineEditorContainer | null;
toggleOutlinePanel: () => void; show: boolean;
openOutlinePanel: () => void;
}) => { }) => {
const outlineViewerRef = useRef<OutlineViewer | null>(null); const outlineViewerRef = useRef<OutlineViewer | null>(null);
@ -24,15 +26,16 @@ export const EditorOutlineViewer = ({
} }
}, []); }, []);
if (!editor) { if (!editor || !show) return;
return;
}
if (!outlineViewerRef.current) { if (!outlineViewerRef.current) {
outlineViewerRef.current = new OutlineViewer(); outlineViewerRef.current = new OutlineViewer();
(outlineViewerRef.current as OutlineViewer).editor = editor; }
(outlineViewerRef.current as OutlineViewer).toggleOutlinePanel = if (outlineViewerRef.current.editor !== editor) {
toggleOutlinePanel; outlineViewerRef.current.editor = editor;
}
if (outlineViewerRef.current.toggleOutlinePanel !== openOutlinePanel) {
outlineViewerRef.current.toggleOutlinePanel = openOutlinePanel;
} }
return <div className={styles.root} ref={onRefChange} />; return <div className={styles.root} ref={onRefChange} />;

View File

@ -0,0 +1,17 @@
import { style } from '@vanilla-extract/css';
const top = 256 - 52; // 52 is the height of the header
const bottom = 76;
export const root = style({
position: 'absolute',
top,
right: 22,
maxHeight: `calc(100% - ${top}px - ${bottom}px)`,
display: 'flex',
'@container': {
'(width <= 640px)': {
display: 'none',
},
},
});

View File

@ -5,7 +5,7 @@ export const editor = style({
'&.full-screen': { '&.full-screen': {
vars: { vars: {
'--affine-editor-width': '100%', '--affine-editor-width': '100%',
'--affine-editor-side-padding': '15px', '--affine-editor-side-padding': '72px',
}, },
}, },
}, },

View File

@ -1,6 +1,10 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const root = style({
containerType: 'inline-size',
});
export const editor = style({ export const editor = style({
vars: { vars: {
'--affine-editor-width': '100%', '--affine-editor-width': '100%',

View File

@ -3,6 +3,8 @@ import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { AIProvider } from '@affine/core/blocksuite/presets/ai'; import { AIProvider } from '@affine/core/blocksuite/presets/ai';
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary'; import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
import { BlockSuiteEditor } from '@affine/core/components/blocksuite/block-suite-editor'; import { BlockSuiteEditor } from '@affine/core/components/blocksuite/block-suite-editor';
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { PageNotFound } from '@affine/core/pages/404'; import { PageNotFound } from '@affine/core/pages/404';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
@ -12,7 +14,7 @@ import type { AffineEditorContainer } from '@blocksuite/presets';
import type { DocMode } from '@toeverything/infra'; import type { DocMode } from '@toeverything/infra';
import { DocsService, FrameworkScope, useService } from '@toeverything/infra'; import { DocsService, FrameworkScope, useService } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { WorkbenchService } from '../../../workbench'; import { WorkbenchService } from '../../../workbench';
import { PeekViewService } from '../../services/peek-view'; import { PeekViewService } from '../../services/peek-view';
@ -73,6 +75,7 @@ export function DocPeekPreview({
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;
const peekView = useService(PeekViewService).peekView; const peekView = useService(PeekViewService).peekView;
const [editor, setEditor] = useState<AffineEditorContainer | null>(null); const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
const { appSettings } = useAppSettingHelper();
const onRef = (editor: AffineEditorContainer) => { const onRef = (editor: AffineEditorContainer) => {
setEditor(editor); setEditor(editor);
@ -143,6 +146,13 @@ export function DocPeekPreview({
}; };
}, [editor, jumpToTag, peekView, workspace.id]); }, [editor, jumpToTag, peekView, workspace.id]);
const openOutlinePanel = useCallback(() => {
workbench.openDoc(docId);
workbench.openSidebar();
workbench.activeView$.value.activeSidebarTab('outline');
peekView.close();
}, [docId, peekView, workbench]);
// if sync engine has been synced and the page is null, show 404 page. // if sync engine has been synced and the page is null, show 404 page.
if (!doc || !resolvedMode) { if (!doc || !resolvedMode) {
return loading || !resolvedMode ? ( return loading || !resolvedMode ? (
@ -167,7 +177,15 @@ export function DocPeekPreview({
page={doc.blockSuiteDoc} page={doc.blockSuiteDoc}
/> />
</FrameworkScope> </FrameworkScope>
{appSettings.enableOutlineViewer && (
<EditorOutlineViewer
editor={editor}
show={resolvedMode === 'page'}
openOutlinePanel={openOutlinePanel}
/>
)}
</Scrollable.Viewport> </Scrollable.Viewport>
<Scrollable.Scrollbar /> <Scrollable.Scrollbar />
</Scrollable.Root> </Scrollable.Root>
</AffineErrorBoundary> </AffineErrorBoundary>

View File

@ -2,6 +2,7 @@ import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const mainContainer = style({ export const mainContainer = style({
containerType: 'inline-size',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flex: 1, flex: 1,

View File

@ -3,6 +3,7 @@ import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import type { ChatPanel } from '@affine/core/blocksuite/presets/ai'; import type { ChatPanel } from '@affine/core/blocksuite/presets/ai';
import { AIProvider } from '@affine/core/blocksuite/presets/ai'; import { AIProvider } from '@affine/core/blocksuite/presets/ai';
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding'; import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { RecentDocsService } from '@affine/core/modules/quicksearch'; import { RecentDocsService } from '@affine/core/modules/quicksearch';
@ -64,7 +65,6 @@ import { performanceRenderLogger } from '../../../shared';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import * as styles from './detail-page.css'; import * as styles from './detail-page.css';
import { DetailPageHeader } from './detail-page-header'; import { DetailPageHeader } from './detail-page-header';
import { EditorOutlineViewer } from './outline-viewer';
import { EditorChatPanel } from './tabs/chat'; import { EditorChatPanel } from './tabs/chat';
import { EditorFramePanel } from './tabs/frame'; import { EditorFramePanel } from './tabs/frame';
import { EditorJournalPanel } from './tabs/journal'; import { EditorJournalPanel } from './tabs/journal';
@ -83,6 +83,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const globalContext = useService(GlobalContextService).globalContext; const globalContext = useService(GlobalContextService).globalContext;
const docCollection = workspace.docCollection; const docCollection = workspace.docCollection;
const mode = useLiveData(doc.mode$); const mode = useLiveData(doc.mode$);
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const chatPanelRef = useRef<ChatPanel | null>(null); const chatPanelRef = useRef<ChatPanel | null>(null);
const { setDocReadonly } = useDocMetaHelper(workspace.docCollection); const { setDocReadonly } = useDocMetaHelper(workspace.docCollection);
@ -226,14 +227,14 @@ const DetailPageImpl = memo(function DetailPageImpl() {
[jumpToPageBlock, docCollection.id, openPage, jumpToTag, workspace.id] [jumpToPageBlock, docCollection.id, openPage, jumpToTag, workspace.id]
); );
const [refCallback, hasScrollTop] = useHasScrollTop();
const dynamicTopBorder = environment.isDesktop;
const openOutlinePanel = useCallback(() => { const openOutlinePanel = useCallback(() => {
workbench.openSidebar(); workbench.openSidebar();
view.activeSidebarTab('outline'); view.activeSidebarTab('outline');
}, [workbench, view]); }, [workbench, view]);
const [refCallback, hasScrollTop] = useHasScrollTop();
const dynamicTopBorder = environment.isDesktop;
return ( return (
<> <>
<ViewHeader> <ViewHeader>
@ -269,15 +270,16 @@ const DetailPageImpl = memo(function DetailPageImpl() {
})} })}
/> />
</Scrollable.Root> </Scrollable.Root>
{appSettings.enableOutlineViewer && (
<EditorOutlineViewer
editor={editor}
show={mode === 'page' && !isSideBarOpen}
openOutlinePanel={openOutlinePanel}
/>
)}
</AffineErrorBoundary> </AffineErrorBoundary>
{isInTrash ? <TrashPageFooter /> : null} {isInTrash ? <TrashPageFooter /> : null}
</div> </div>
{appSettings.enableOutlineViewer && (
<EditorOutlineViewer
editor={editor}
toggleOutlinePanel={openOutlinePanel}
/>
)}
</ViewBody> </ViewBody>
<ViewSidebarTab tabId="chat" icon={<AiIcon />} unmountOnInactive={false}> <ViewSidebarTab tabId="chat" icon={<AiIcon />} unmountOnInactive={false}>

View File

@ -1,8 +0,0 @@
import { style } from '@vanilla-extract/css';
export const root = style({
position: 'fixed',
top: 256,
right: 22,
maxHeight: 'calc(100% - 16px)',
});

View File

@ -95,4 +95,4 @@
"peerDependencies": { "peerDependencies": {
"ts-node": "*" "ts-node": "*"
} }
} }

View File

@ -719,6 +719,8 @@
"com.affine.filterList.button.add": "Add Filter", "com.affine.filterList.button.add": "Add Filter",
"com.affine.header.option.add-tag": "Add Tag", "com.affine.header.option.add-tag": "Add Tag",
"com.affine.header.option.duplicate": "Duplicate", "com.affine.header.option.duplicate": "Duplicate",
"com.affine.header.option.view-toc": "View table of contents",
"com.affine.header.option.view-frame": "View all frames",
"com.affine.helpIsland.contactUs": "Contact us", "com.affine.helpIsland.contactUs": "Contact us",
"com.affine.helpIsland.gettingStarted": "Getting started", "com.affine.helpIsland.gettingStarted": "Getting started",
"com.affine.helpIsland.helpAndFeedback": "Help and Feedback", "com.affine.helpIsland.helpAndFeedback": "Help and Feedback",

View File

@ -5,11 +5,6 @@ import {
getBlockSuiteEditorTitle, getBlockSuiteEditorTitle,
waitForEditorLoad, waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import {
confirmExperimentalPrompt,
openExperimentalFeaturesPanel,
openSettingModal,
} from '@affine-test/kit/utils/setting';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
@ -78,45 +73,3 @@ test('link page is useable', async ({ page }) => {
page.locator('.doc-title-container:has-text("page1")') page.locator('.doc-title-container:has-text("page1")')
).toBeVisible(); ).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({ force: true });
await expect(viewer).toBeVisible();
});

View File

@ -0,0 +1,143 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickPageModeButton,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
getBlockSuiteEditorTitle,
waitForEditorLoad,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import {
confirmExperimentalPrompt,
openExperimentalFeaturesPanel,
openSettingModal,
} from '@affine-test/kit/utils/setting';
import { expect, type Page } from '@playwright/test';
async function enableOutlineViewer(page: 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);
}
test('outline viewer is useable', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await enableOutlineViewer(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await title.pressSequentially('Title');
await expect(title).toContainText('Title');
await page.keyboard.press('Enter');
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({ force: true });
await expect(viewer).toBeVisible();
});
test('outline viewer should hide in edgeless mode', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await enableOutlineViewer(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await title.pressSequentially('Title');
await page.keyboard.press('Enter');
await expect(title).toHaveText('Title');
await page.keyboard.type('# ');
await page.keyboard.type('Heading 1');
const indicators = page.locator('.outline-heading-indicator');
await expect(indicators).toHaveCount(1);
await clickEdgelessModeButton(page);
await expect(indicators).toHaveCount(0);
await clickPageModeButton(page);
await expect(indicators).toHaveCount(1);
});
test('outline viewer should be useable in doc peek preview', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await enableOutlineViewer(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
await page.locator('affine-reference').hover();
await expect(
page.locator('.affine-reference-popover-container')
).toBeVisible();
await page
.locator('editor-menu-button editor-icon-button[aria-label="Open doc"]')
.click();
await page
.locator('editor-menu-action:has-text("Open in center peek")')
.click();
const peekView = page.getByTestId('peek-view-modal');
await expect(peekView).toBeVisible();
const title = peekView.locator('doc-title .inline-editor');
await title.click();
await page.keyboard.press('Enter');
await page.keyboard.type('# Heading 1');
const indicators = peekView.locator('.outline-heading-indicator');
await expect(indicators).toHaveCount(1);
await expect(indicators).toBeVisible();
await indicators.first().hover({ force: true });
const viewer = peekView.locator('affine-outline-panel-body');
await expect(viewer).toBeVisible();
const toggleButton = peekView.locator(
'.outline-viewer-header-container edgeless-tool-icon-button'
);
await toggleButton.click();
await page.waitForTimeout(500);
await expect(peekView).toBeHidden();
await expect(viewer).toBeHidden();
await expect(page.locator('affine-outline-panel')).toBeVisible();
});

View File

@ -46,4 +46,4 @@
"dev": "node --loader ts-node/esm/transpile-only.mjs ./src/bin/dev.ts" "dev": "node --loader ts-node/esm/transpile-only.mjs ./src/bin/dev.ts"
}, },
"version": "0.15.0" "version": "0.15.0"
} }