feat(core): split right sidebar (#5971)

https://github.com/toeverything/AFFiNE/assets/13579374/c846c069-aa32-445d-b59b-b773a9b05ced

Now each view has a general container, the yellow area is the general container part, and the green part is the routing specific part.

![CleanShot 2024-03-01 at 11.47.35@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/g3jz87HxbjOJpXV3FPT7/9a9f6ad6-2207-42e5-ae66-f7426bc9f3fc.png)
This commit is contained in:
EYHN
2024-03-04 06:42:12 +00:00
parent e2a31ea1fc
commit c599715963
88 changed files with 1393 additions and 1238 deletions

View File

@@ -1,5 +1,6 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SidebarIcon } from '@blocksuite/icons'; import { SidebarIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { IconButton } from '../../../ui/button'; import { IconButton } from '../../../ui/button';
@@ -7,7 +8,13 @@ import { Tooltip } from '../../../ui/tooltip';
import { appSidebarOpenAtom } from '../index.jotai'; import { appSidebarOpenAtom } from '../index.jotai';
import * as styles from './sidebar-switch.css'; import * as styles from './sidebar-switch.css';
export const SidebarSwitch = ({ show }: { show: boolean }) => { export const SidebarSwitch = ({
show = true,
className,
}: {
show?: boolean;
className?: string;
}) => {
const [open, setOpen] = useAtom(appSidebarOpenAtom); const [open, setOpen] = useAtom(appSidebarOpenAtom);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const tooltipContent = open const tooltipContent = open
@@ -22,7 +29,7 @@ export const SidebarSwitch = ({ show }: { show: boolean }) => {
side={open ? 'bottom' : 'right'} side={open ? 'bottom' : 'right'}
> >
<IconButton <IconButton
className={styles.sidebarSwitch} className={clsx(styles.sidebarSwitch, className)}
data-show={show} data-show={show}
size="large" size="large"
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`} data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}

View File

@@ -3,11 +3,11 @@ import { style } from '@vanilla-extract/css';
export const notFoundPageContainer = style({ export const notFoundPageContainer = style({
fontSize: cssVar('fontBase'), fontSize: cssVar('fontBase'),
color: cssVar('textPrimaryColor'), color: cssVar('textPrimaryColor'),
height: '100vh', height: '100%',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: '100vw', width: '100%',
padding: '0 20px', padding: '0 20px',
}); });
export const wrapper = style({ export const wrapper = style({

View File

@@ -13,7 +13,7 @@ export const root = style({
}, },
position: 'relative', position: 'relative',
width: panelWidthVar, width: panelWidthVar,
minWidth: 0, minWidth: panelWidthVar,
height: '100%', height: '100%',
selectors: { selectors: {
'&[data-is-floating="true"]': { '&[data-is-floating="true"]': {

View File

@@ -48,13 +48,12 @@ export const mainContainerStyle = style({
zIndex: 0, zIndex: 0,
// it will create stacking context to limit layer of child elements and be lower than after auto zIndex // it will create stacking context to limit layer of child elements and be lower than after auto zIndex
width: 0, width: 0,
display: 'flex',
flex: 1, flex: 1,
maxWidth: '100%', maxWidth: '100%',
backgroundColor: cssVar('backgroundPrimaryColor'),
selectors: { selectors: {
'&[data-show-padding="true"]': { '&[data-show-padding="true"]': {
margin: '8px', margin: '8px',
borderRadius: '5px',
overflow: 'hidden', overflow: 'hidden',
// todo: is this performance intensive? // todo: is this performance intensive?
filter: 'drop-shadow(0px 0px 4px rgba(66,65,73,.14))', filter: 'drop-shadow(0px 0px 4px rgba(66,65,73,.14))',
@@ -66,9 +65,6 @@ export const mainContainerStyle = style({
}, },
}, },
}, },
'&[data-show-padding="true"][data-is-macos="true"]': {
borderRadius: '6px',
},
'&[data-show-padding="true"]:before': { '&[data-show-padding="true"]:before': {
content: '""', content: '""',
position: 'absolute', position: 'absolute',

View File

@@ -266,7 +266,6 @@ affine-block-hub {
} }
} }
button,
input, input,
select, select,
textarea textarea

View File

@@ -6,7 +6,7 @@ export const scrollableContainerRoot = style({
'--scrollbar-width': '8px', '--scrollbar-width': '8px',
}, },
height: '100%', height: '100%',
overflow: 'hidden', overflowY: 'hidden',
}); });
export const scrollTopBorder = style({ export const scrollTopBorder = style({
position: 'absolute', position: 'absolute',

View File

@@ -10,6 +10,7 @@ import clsx from 'clsx';
import type React from 'react'; import type React from 'react';
import { import {
forwardRef, forwardRef,
type RefObject,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
@@ -62,7 +63,7 @@ function findBlockElementById(container: HTMLElement, blockId: string) {
// a workaround for returning the webcomponent for the given block id // a workaround for returning the webcomponent for the given block id
// by iterating over the children of the rendered dom tree // by iterating over the children of the rendered dom tree
const useBlockElementById = ( const useBlockElementById = (
container: HTMLElement | null, containerRef: RefObject<HTMLElement | null>,
blockId: string | undefined, blockId: string | undefined,
timeout = 1000 timeout = 1000
) => { ) => {
@@ -74,10 +75,10 @@ const useBlockElementById = (
let canceled = false; let canceled = false;
const start = Date.now(); const start = Date.now();
function run() { function run() {
if (canceled || !container || !blockId) { if (canceled || !containerRef.current || !blockId) {
return; return;
} }
const element = findBlockElementById(container, blockId); const element = findBlockElementById(containerRef.current, blockId);
if (element) { if (element) {
setBlockElement(element); setBlockElement(element);
} else if (Date.now() - start < timeout) { } else if (Date.now() - start < timeout) {
@@ -88,7 +89,7 @@ const useBlockElementById = (
return () => { return () => {
canceled = true; canceled = true;
}; };
}, [container, blockId, timeout]); }, [blockId, containerRef, timeout]);
return blockElement; return blockElement;
}; };
@@ -200,10 +201,7 @@ export const BlocksuiteEditorContainer = forwardRef<
} }
}, [affineEditorContainerProxy, ref]); }, [affineEditorContainerProxy, ref]);
const blockElement = useBlockElementById( const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId);
rootRef.current,
defaultSelectedBlockId
);
useEffect(() => { useEffect(() => {
if (blockElement) { if (blockElement) {

View File

@@ -3,4 +3,10 @@ import { style } from '@vanilla-extract/css';
export const title = style({ export const title = style({
fontWeight: 500, fontWeight: 500,
color: cssVar('textPrimaryColor'), color: cssVar('textPrimaryColor'),
selectors: {
'&[data-editing="true"]': {
['WebkitAppRegion' as string]: 'no-drag',
flexGrow: 1,
},
},
}); });

View File

@@ -1,6 +1,5 @@
import './page-detail-editor.css'; import './page-detail-editor.css';
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page'; import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
import { assertExists, DisposableGroup } from '@blocksuite/global/utils'; import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '@blocksuite/presets'; import type { AffineEditorContainer } from '@blocksuite/presets';
@@ -70,14 +69,12 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
return fontStyle.value; return fontStyle.value;
}, [appSettings.fontStyle]); }, [appSettings.fontStyle]);
const [, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor();
const blockId = useRouterHash(); const blockId = useRouterHash();
const onLoadEditor = useCallback( const onLoadEditor = useCallback(
(editor: AffineEditorContainer) => { (editor: AffineEditorContainer) => {
// debug current detail editor // debug current detail editor
globalThis.currentEditor = editor; globalThis.currentEditor = editor;
setActiveBlocksuiteEditor(editor);
const disposableGroup = new DisposableGroup(); const disposableGroup = new DisposableGroup();
disposableGroup.add( disposableGroup.add(
page.slots.blockUpdated.once(() => { page.slots.blockUpdated.once(() => {
@@ -99,10 +96,9 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
return () => { return () => {
disposableGroup.dispose(); disposableGroup.dispose();
setActiveBlocksuiteEditor(null);
}; };
}, },
[onLoad, page, setActiveBlocksuiteEditor] [onLoad, page]
); );
return ( return (

View File

@@ -1,9 +1,8 @@
import { Checkbox } from '@affine/component'; import { Checkbox } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useDraggable } from '@dnd-kit/core'; import { useDraggable } from '@dnd-kit/core';
import { type PropsWithChildren, useCallback, useMemo } from 'react'; import { type PropsWithChildren, useCallback, useMemo } from 'react';
import { WorkbenchLink } from '../../../modules/workbench/workbench-link'; import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
import type { DraggableTitleCellData, PageListItemProps } from '../types'; import type { DraggableTitleCellData, PageListItemProps } from '../types';
import { ColWrapper, formatDate, stopPropagation } from '../utils'; import { ColWrapper, formatDate, stopPropagation } from '../utils';
import * as styles from './page-list-item.css'; import * as styles from './page-list-item.css';
@@ -13,14 +12,13 @@ const ListTitleCell = ({
title, title,
preview, preview,
}: Pick<PageListItemProps, 'title' | 'preview'>) => { }: Pick<PageListItemProps, 'title' | 'preview'>) => {
const t = useAFFiNEI18N();
return ( return (
<div data-testid="page-list-item-title" className={styles.titleCell}> <div data-testid="page-list-item-title" className={styles.titleCell}>
<div <div
data-testid="page-list-item-title-text" data-testid="page-list-item-title-text"
className={styles.titleCellMain} className={styles.titleCellMain}
> >
{title || t['Untitled']()} {title}
</div> </div>
{preview ? ( {preview ? (
<div <div

View File

@@ -1,4 +1,3 @@
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import type { Tag } from '@affine/env/filter'; import type { Tag } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -12,6 +11,9 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { DocMeta, Workspace } from '@blocksuite/store'; import type { DocMeta, Workspace } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra';
import { PageRecordList } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { selectAtom } from 'jotai/utils'; import { selectAtom } from 'jotai/utils';
import { type MouseEventHandler, useCallback, useMemo, useState } from 'react'; import { type MouseEventHandler, useCallback, useMemo, useState } from 'react';
@@ -271,9 +273,15 @@ function tagIdToTagOption(
); );
} }
const PageTitle = ({ id, workspace }: { id: string; workspace: Workspace }) => { const PageTitle = ({ id }: { id: string }) => {
const title = useBlockSuiteWorkspacePageTitle(workspace, id); const page = useLiveData(
return title; useService(PageRecordList).records.map(record => {
return record.find(p => p.id === id);
})
);
const title = useLiveData(page?.title);
const t = useAFFiNEI18N();
return title || t['Untitled']();
}; };
const UnifiedPageIcon = ({ const UnifiedPageIcon = ({
@@ -315,7 +323,7 @@ function pageMetaToListItemProp(
: undefined; : undefined;
const itemProps: PageListItemProps = { const itemProps: PageListItemProps = {
pageId: item.id, pageId: item.id,
title: <PageTitle id={item.id} workspace={props.blockSuiteWorkspace} />, title: <PageTitle id={item.id} />,
preview: ( preview: (
<PagePreview workspace={props.blockSuiteWorkspace} pageId={item.id} /> <PagePreview workspace={props.blockSuiteWorkspace} pageId={item.id} />
), ),

View File

@@ -1,13 +1,10 @@
import { import {
appSidebarFloatingAtom, appSidebarFloatingAtom,
appSidebarOpenAtom, appSidebarOpenAtom,
SidebarSwitch,
} from '@affine/component/app-sidebar'; } from '@affine/component/app-sidebar';
import { useIsTinyScreen } from '@affine/core/hooks/use-is-tiny-screen';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback, useRef, useState } from 'react';
import * as style from './style.css'; import * as style from './style.css';
@@ -16,86 +13,35 @@ interface HeaderPros {
right?: ReactNode; right?: ReactNode;
center?: ReactNode; center?: ReactNode;
bottomBorder?: boolean; bottomBorder?: boolean;
isFloat?: boolean;
} }
// The Header component is used to solve the following problems // The Header component is used to solve the following problems
// 1. Manage layout issues independently of page or business logic // 1. Manage layout issues independently of page or business logic
// 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed // 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed
export const Header = ({ export const Header = ({ left, center, right }: HeaderPros) => {
left,
center,
right,
bottomBorder,
isFloat,
}: HeaderPros) => {
const sidebarSwitchRef = useRef<HTMLDivElement | null>(null);
const leftSlotRef = useRef<HTMLDivElement | null>(null);
const centerSlotRef = useRef<HTMLDivElement | null>(null);
const rightSlotRef = useRef<HTMLDivElement | null>(null);
const [headerRoot, setHeaderRoot] = useState<HTMLDivElement | null>(null);
const onSetHeaderRoot = useCallback((node: HTMLDivElement | null) => {
setHeaderRoot(node);
}, []);
const isTinyScreen = useIsTinyScreen({
container: headerRoot,
leftStatic: sidebarSwitchRef,
leftSlot: [leftSlotRef],
centerDom: centerSlotRef,
rightSlot: [rightSlotRef],
});
const open = useAtomValue(appSidebarOpenAtom); const open = useAtomValue(appSidebarOpenAtom);
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
return ( return (
<div <div
className={clsx(style.header, bottomBorder && style.bottomBorder, { className={clsx(style.header)}
[style.headerFloat]: isFloat,
})}
data-open={open} data-open={open}
data-sidebar-floating={appSidebarFloating} data-sidebar-floating={appSidebarFloating}
data-testid="header" data-testid="header"
ref={onSetHeaderRoot}
> >
<div <div className={clsx(style.headerSideContainer)}>
className={clsx(style.headerSideContainer, {
block: isTinyScreen,
})}
>
<div
className={clsx(
style.headerItem,
'top-item',
!open ? 'top-item-visible' : ''
)}
>
<div ref={sidebarSwitchRef}>
<SidebarSwitch show={!open} />
</div>
</div>
<div className={clsx(style.headerItem, 'left')}> <div className={clsx(style.headerItem, 'left')}>
<div ref={leftSlotRef}>{left}</div> <div>{left}</div>
</div> </div>
</div> </div>
<div <div
className={clsx({ className={clsx({
[style.headerCenter]: center, [style.headerCenter]: center,
})} })}
ref={centerSlotRef}
> >
{center} {center}
</div> </div>
<div <div className={clsx(style.headerSideContainer, 'right')}>
className={clsx(style.headerSideContainer, 'right', { <div className={clsx(style.headerItem, 'right')}>{right}</div>
block: isTinyScreen,
})}
>
<div ref={rightSlotRef} className={clsx(style.headerItem, 'right')}>
{right}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -3,48 +3,23 @@ import { style } from '@vanilla-extract/css';
export const header = style({ export const header = style({
display: 'flex', display: 'flex',
flex: 1,
justifyContent: 'space-between', justifyContent: 'space-between',
position: 'relative', position: 'relative',
padding: '0 16px',
minHeight: '52px',
background: 'var(--affine-background-primary-color)',
zIndex: 2, zIndex: 2,
selectors: {
'&[data-sidebar-floating="false"]': {
WebkitAppRegion: 'drag',
},
},
'@media': { '@media': {
print: { print: {
display: 'none', display: 'none',
}, },
}, },
':has([data-popper-placement])': {
WebkitAppRegion: 'no-drag',
},
} as ComplexStyleRule); } as ComplexStyleRule);
export const headerFloat = style({
position: 'absolute',
width: '100%',
});
export const bottomBorder = style({
borderBottom: '1px solid var(--affine-border-color)',
});
export const headerItem = style({ export const headerItem = style({
minHeight: '32px', minHeight: '32px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexShrink: 0, flexShrink: 0,
selectors: { selectors: {
'&.top-item': {
height: '52px',
},
'&.top-item-visible': {
marginRight: '20px',
},
'&.left': { '&.left': {
justifyContent: 'left', justifyContent: 'left',
}, },
@@ -66,12 +41,6 @@ export const headerCenter = style({
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
left: '50%', left: '50%',
zIndex: 1, zIndex: 1,
selectors: {
'&.shadow': {
position: 'static',
visibility: 'hidden',
},
},
}); });
export const headerSideContainer = style({ export const headerSideContainer = style({
@@ -82,10 +51,6 @@ export const headerSideContainer = style({
'&.right': { '&.right': {
flexDirection: 'row-reverse', flexDirection: 'row-reverse',
}, },
'&.block': {
display: 'block',
paddingBottom: '10px',
},
}, },
}); });

View File

@@ -22,7 +22,7 @@ import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag'; import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta'; import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
import { Workbench } from '../../../../modules/workbench'; import { Workbench } from '../../../../modules/workbench';
import { WorkbenchLink } from '../../../../modules/workbench/workbench-link'; import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
import type { CollectionsListProps } from '../index'; import type { CollectionsListProps } from '../index';
import { Page } from './page'; import { Page } from './page';
import * as styles from './styles.css'; import * as styles from './styles.css';

View File

@@ -1,73 +0,0 @@
import 'foxact/use-debounced-state';
import { debounce } from 'lodash-es';
import { type RefObject, useEffect, useState } from 'react';
export function useIsTinyScreen({
container,
leftStatic,
leftSlot,
centerDom,
rightSlot,
}: {
container: HTMLElement | null;
leftStatic: RefObject<HTMLElement>;
leftSlot: RefObject<HTMLElement>[];
centerDom: RefObject<HTMLElement>;
rightSlot: RefObject<HTMLElement>[];
}) {
const [isTinyScreen, setIsTinyScreen] = useState(false);
useEffect(() => {
if (!container) {
return;
}
const handleResize = debounce(() => {
if (!centerDom.current) {
return;
}
const leftStaticWidth = leftStatic.current?.clientWidth || 0;
const leftSlotWidth = leftSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
const rightSlotWidth = rightSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
if (!leftSlotWidth && !rightSlotWidth) {
if (isTinyScreen) {
setIsTinyScreen(false);
}
return;
}
const containerRect = container.getBoundingClientRect();
const centerRect = centerDom.current.getBoundingClientRect();
if (
leftStaticWidth + leftSlotWidth + containerRect.left >=
centerRect.left ||
containerRect.right - centerRect.right <= rightSlotWidth
) {
setIsTinyScreen(true);
} else {
setIsTinyScreen(false);
}
}, 100);
handleResize();
const resizeObserver = new ResizeObserver(() => {
handleResize();
});
resizeObserver.observe(container);
return () => {
resizeObserver.unobserve(container);
};
}, [centerDom, isTinyScreen, leftSlot, leftStatic, container, rightSlot]);
return isTinyScreen;
}

View File

@@ -0,0 +1,13 @@
import type { AffineEditorContainer } from '@blocksuite/presets';
export type SidebarTabName = 'outline' | 'frame' | 'copilot' | 'journal';
export interface SidebarTabProps {
editor: AffineEditorContainer | null;
}
export interface SidebarTab {
name: SidebarTabName;
icon: React.ReactNode;
Component: React.ComponentType<SidebarTabProps>;
}

View File

@@ -0,0 +1,14 @@
import type { SidebarTab } from './sidebar-tab';
import { copilotTab } from './tabs/copilot';
import { framePanelTab } from './tabs/frame';
import { journalTab } from './tabs/journal';
import { outlineTab } from './tabs/outline';
// the list of all possible tabs in affine.
// order matters (determines the order of the tabs)
export const sidebarTabs: SidebarTab[] = [
journalTab,
outlineTab,
framePanelTab,
copilotTab,
];

View File

@@ -1,16 +1,14 @@
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { AiIcon } from '@blocksuite/icons'; import { AiIcon } from '@blocksuite/icons';
import { CopilotPanel } from '@blocksuite/presets'; import { CopilotPanel } from '@blocksuite/presets';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { EditorExtension } from '../types'; import type { SidebarTab, SidebarTabProps } from '../sidebar-tab';
import * as styles from './outline.css'; import * as styles from './outline.css';
// A wrapper for CopilotPanel // A wrapper for CopilotPanel
const EditorCopilotPanel = () => { const EditorCopilotPanel = ({ editor }: SidebarTabProps) => {
const copilotPanelRef = useRef<CopilotPanel | null>(null); const copilotPanelRef = useRef<CopilotPanel | null>(null);
const [editor] = useActiveBlocksuiteEditor();
const onRefChange = useCallback((container: HTMLDivElement | null) => { const onRefChange = useCallback((container: HTMLDivElement | null) => {
if (container) { if (container) {
@@ -38,7 +36,7 @@ const EditorCopilotPanel = () => {
return <div className={styles.root} ref={onRefChange} />; return <div className={styles.root} ref={onRefChange} />;
}; };
export const copilotExtension: EditorExtension = { export const copilotTab: SidebarTab = {
name: 'copilot', name: 'copilot',
icon: <AiIcon />, icon: <AiIcon />,
Component: EditorCopilotPanel, Component: EditorCopilotPanel,

View File

@@ -1,18 +1,15 @@
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { FrameIcon } from '@blocksuite/icons'; import { FrameIcon } from '@blocksuite/icons';
import { FramePanel } from '@blocksuite/presets'; import { FramePanel } from '@blocksuite/presets';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { EditorExtension } from '../types'; import type { SidebarTab, SidebarTabProps } from '../sidebar-tab';
import * as styles from './frame.css'; import * as styles from './frame.css';
// A wrapper for FramePanel // A wrapper for FramePanel
const EditorFramePanel = () => { const EditorFramePanel = ({ editor }: SidebarTabProps) => {
const framePanelRef = useRef<FramePanel | null>(null); const framePanelRef = useRef<FramePanel | null>(null);
const [editor] = useActiveBlocksuiteEditor();
const onRefChange = useCallback((container: HTMLDivElement | null) => { const onRefChange = useCallback((container: HTMLDivElement | null) => {
if (container) { if (container) {
assertExists(framePanelRef.current, 'frame panel should be initialized'); assertExists(framePanelRef.current, 'frame panel should be initialized');
@@ -36,7 +33,7 @@ const EditorFramePanel = () => {
return <div className={styles.root} ref={onRefChange} />; return <div className={styles.root} ref={onRefChange} />;
}; };
export const framePanelExtension: EditorExtension = { export const framePanelTab: SidebarTab = {
name: 'frame', name: 'frame',
icon: <FrameIcon />, icon: <FrameIcon />,
Component: EditorFramePanel, Component: EditorFramePanel,

View File

@@ -7,15 +7,12 @@ import {
} from '@affine/component'; } from '@affine/component';
import { MoveToTrash } from '@affine/core/components/page-list'; import { MoveToTrash } from '@affine/core/components/page-list';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { import {
useJournalHelper, useJournalHelper,
useJournalInfoHelper, useJournalInfoHelper,
useJournalRouteHelper, useJournalRouteHelper,
} from '@affine/core/hooks/use-journal'; } from '@affine/core/hooks/use-journal';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import type { BlockSuiteWorkspace } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import {
EdgelessIcon, EdgelessIcon,
@@ -23,14 +20,21 @@ import {
PageIcon, PageIcon,
TodayIcon, TodayIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { Doc, DocMeta } from '@blocksuite/store'; import type { PageRecord } from '@toeverything/infra';
import {
Doc,
PageRecordList,
useLiveData,
Workspace,
} from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { assignInlineVars } from '@vanilla-extract/dynamic'; import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx'; import clsx from 'clsx';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { EditorExtension, EditorExtensionProps } from '..'; import type { SidebarTab } from '../sidebar-tab';
import * as styles from './journal.css'; import * as styles from './journal.css';
/** /**
@@ -44,28 +48,31 @@ const CountDisplay = ({
return <span {...attrs}>{count > max ? `${max}+` : count}</span>; return <span {...attrs}>{count > max ? `${max}+` : count}</span>;
}; };
interface PageItemProps extends HTMLAttributes<HTMLDivElement> { interface PageItemProps extends HTMLAttributes<HTMLDivElement> {
pageMeta: DocMeta; pageRecord: PageRecord;
workspace: BlockSuiteWorkspace;
right?: ReactNode; right?: ReactNode;
} }
const PageItem = ({ const PageItem = ({
pageMeta, pageRecord,
workspace,
right, right,
className, className,
...attrs ...attrs
}: PageItemProps) => { }: PageItemProps) => {
const { isJournal } = useJournalInfoHelper(workspace, pageMeta.id); const title = useLiveData(pageRecord.title);
const title = useBlockSuiteWorkspacePageTitle(workspace, pageMeta.id); const mode = useLiveData(pageRecord.mode);
const workspace = useService(Workspace);
const { isJournal } = useJournalInfoHelper(
workspace.blockSuiteWorkspace,
pageRecord.id
);
const Icon = isJournal const Icon = isJournal
? TodayIcon ? TodayIcon
: pageMeta.mode === 'edgeless' : mode === 'edgeless'
? EdgelessIcon ? EdgelessIcon
: PageIcon; : PageIcon;
return ( return (
<div <div
aria-label={pageMeta.title} aria-label={title}
className={clsx(className, styles.pageItem)} className={clsx(className, styles.pageItem)}
{...attrs} {...attrs}
> >
@@ -84,18 +91,19 @@ interface NavItem {
label: string; label: string;
count: number; count: number;
} }
interface JournalBlockProps extends EditorExtensionProps { interface JournalBlockProps {
date: dayjs.Dayjs; date: dayjs.Dayjs;
} }
const EditorJournalPanel = (props: EditorExtensionProps) => { const EditorJournalPanel = () => {
const { workspace, page } = props;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const doc = useService(Doc);
const workspace = useService(Workspace);
const { journalDate, isJournal } = useJournalInfoHelper( const { journalDate, isJournal } = useJournalInfoHelper(
page.workspace, workspace.blockSuiteWorkspace,
page.id doc.id
); );
const { openJournal } = useJournalRouteHelper(workspace); const { openJournal } = useJournalRouteHelper(workspace.blockSuiteWorkspace);
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD')); const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
useEffect(() => { useEffect(() => {
@@ -149,19 +157,22 @@ const EditorJournalPanel = (props: EditorExtensionProps) => {
onChange={onDateSelect} onChange={onDateSelect}
/> />
</div> </div>
<JournalConflictBlock date={dayjs(date)} {...props} /> <JournalConflictBlock date={dayjs(date)} />
<JournalDailyCountBlock date={dayjs(date)} {...props} /> <JournalDailyCountBlock date={dayjs(date)} />
</div> </div>
); );
}; };
const sortPagesByDate = ( const sortPagesByDate = (
pages: DocMeta[], pages: PageRecord[],
field: 'updatedDate' | 'createDate', field: 'updatedDate' | 'createDate',
order: 'asc' | 'desc' = 'desc' order: 'asc' | 'desc' = 'desc'
) => { ) => {
return [...pages].sort((a, b) => { return [...pages].sort((a, b) => {
return (order === 'asc' ? 1 : -1) * dayjs(b[field]).diff(dayjs(a[field])); return (
(order === 'asc' ? 1 : -1) *
dayjs(b.meta.value[field]).diff(dayjs(a.meta.value[field]))
);
}); });
}; };
@@ -176,25 +187,30 @@ const DailyCountEmptyFallback = ({ name }: { name: NavItemName }) => {
</div> </div>
); );
}; };
const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => { const JournalDailyCountBlock = ({ date }: JournalBlockProps) => {
const workspace = useService(Workspace);
const nodeRef = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [activeItem, setActiveItem] = useState<NavItemName>('createdToday'); const [activeItem, setActiveItem] = useState<NavItemName>('createdToday');
const pageMetas = useBlockSuiteDocMeta(workspace); const pageRecordList = useService(PageRecordList);
const pageRecords = useLiveData(pageRecordList.records);
const navigateHelper = useNavigateHelper(); const navigateHelper = useNavigateHelper();
const getTodaysPages = useCallback( const getTodaysPages = useCallback(
(field: 'createDate' | 'updatedDate') => { (field: 'createDate' | 'updatedDate') => {
return sortPagesByDate( return sortPagesByDate(
pageMetas.filter(pageMeta => { pageRecords.filter(pageRecord => {
if (pageMeta.trash) return false; if (pageRecord.meta.value.trash) return false;
return pageMeta[field] && dayjs(pageMeta[field]).isSame(date, 'day'); return (
pageRecord.meta.value[field] &&
dayjs(pageRecord.meta.value[field]).isSame(date, 'day')
);
}), }),
field field
); );
}, },
[date, pageMetas] [date, pageRecords]
); );
const createdToday = useMemo( const createdToday = useMemo(
@@ -263,15 +279,14 @@ const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => {
<Scrollable.Scrollbar /> <Scrollable.Scrollbar />
<Scrollable.Viewport> <Scrollable.Viewport>
<div className={styles.dailyCountContent} ref={nodeRef}> <div className={styles.dailyCountContent} ref={nodeRef}>
{renderList.map((pageMeta, index) => ( {renderList.map((pageRecord, index) => (
<PageItem <PageItem
onClick={() => onClick={() =>
navigateHelper.openPage(workspace.id, pageMeta.id) navigateHelper.openPage(workspace.id, pageRecord.id)
} }
tabIndex={name === activeItem ? 0 : -1} tabIndex={name === activeItem ? 0 : -1}
key={index} key={index}
pageMeta={pageMeta} pageRecord={pageRecord}
workspace={workspace}
/> />
))} ))}
</div> </div>
@@ -286,29 +301,27 @@ const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => {
const MAX_CONFLICT_COUNT = 5; const MAX_CONFLICT_COUNT = 5;
interface ConflictListProps interface ConflictListProps
extends JournalBlockProps, extends PropsWithChildren,
PropsWithChildren,
HTMLAttributes<HTMLDivElement> { HTMLAttributes<HTMLDivElement> {
pages: Doc[]; pageRecords: PageRecord[];
} }
const ConflictList = ({ const ConflictList = ({
page: currentPage, pageRecords,
pages,
workspace,
children, children,
className, className,
...attrs ...attrs
}: ConflictListProps) => { }: ConflictListProps) => {
const navigateHelper = useNavigateHelper(); const navigateHelper = useNavigateHelper();
const { setTrashModal } = useTrashModalHelper(workspace); const workspace = useService(Workspace);
const currentDoc = useService(Doc);
const { setTrashModal } = useTrashModalHelper(workspace.blockSuiteWorkspace);
const handleOpenTrashModal = useCallback( const handleOpenTrashModal = useCallback(
(page: Doc) => { (pageRecord: PageRecord) => {
if (!page.meta) return;
setTrashModal({ setTrashModal({
open: true, open: true,
pageIds: [page.id], pageIds: [pageRecord.id],
pageTitles: [page.meta.title], pageTitles: [pageRecord.meta.value.title],
}); });
}, },
[setTrashModal] [setTrashModal]
@@ -316,19 +329,19 @@ const ConflictList = ({
return ( return (
<div className={clsx(styles.journalConflictWrapper, className)} {...attrs}> <div className={clsx(styles.journalConflictWrapper, className)} {...attrs}>
{pages.map(page => { {pageRecords.map(pageRecord => {
const isCurrent = page.id === currentPage.id; const isCurrent = pageRecord.id === currentDoc.id;
return ( return (
<PageItem <PageItem
aria-label={page.meta?.title}
aria-selected={isCurrent} aria-selected={isCurrent}
pageMeta={page.meta as DocMeta} pageRecord={pageRecord}
workspace={workspace} key={pageRecord.id}
key={page.id}
right={ right={
<Menu <Menu
items={ items={
<MoveToTrash onSelect={() => handleOpenTrashModal(page)} /> <MoveToTrash
onSelect={() => handleOpenTrashModal(pageRecord)}
/>
} }
> >
<IconButton type="plain"> <IconButton type="plain">
@@ -336,7 +349,7 @@ const ConflictList = ({
</IconButton> </IconButton>
</Menu> </Menu>
} }
onClick={() => navigateHelper.openPage(workspace.id, page.id)} onClick={() => navigateHelper.openPage(workspace.id, pageRecord.id)}
/> />
); );
})} })}
@@ -344,29 +357,32 @@ const ConflictList = ({
</div> </div>
); );
}; };
const JournalConflictBlock = (props: JournalBlockProps) => { const JournalConflictBlock = ({ date }: JournalBlockProps) => {
const { workspace, date } = props;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const journalHelper = useJournalHelper(workspace); const workspace = useService(Workspace);
const pages = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD')); const pageRecordList = useService(PageRecordList);
const journalHelper = useJournalHelper(workspace.blockSuiteWorkspace);
const docs = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD'));
const pageRecords = useLiveData(pageRecordList.records).filter(v => {
return docs.some(doc => doc.id === v.id);
});
if (pages.length <= 1) return null; if (docs.length <= 1) return null;
return ( return (
<ConflictList <ConflictList
className={styles.journalConflictBlock} className={styles.journalConflictBlock}
pages={pages.slice(0, MAX_CONFLICT_COUNT)} pageRecords={pageRecords.slice(0, MAX_CONFLICT_COUNT)}
{...props}
> >
{pages.length > MAX_CONFLICT_COUNT ? ( {docs.length > MAX_CONFLICT_COUNT ? (
<Menu <Menu
items={ items={
<ConflictList pages={pages.slice(MAX_CONFLICT_COUNT)} {...props} /> <ConflictList pageRecords={pageRecords.slice(MAX_CONFLICT_COUNT)} />
} }
> >
<div className={styles.journalConflictMoreTrigger}> <div className={styles.journalConflictMoreTrigger}>
{t['com.affine.journal.conflict-show-more']({ {t['com.affine.journal.conflict-show-more']({
count: (pages.length - MAX_CONFLICT_COUNT).toFixed(0), count: (pageRecords.length - MAX_CONFLICT_COUNT).toFixed(0),
})} })}
</div> </div>
</Menu> </Menu>
@@ -375,7 +391,7 @@ const JournalConflictBlock = (props: JournalBlockProps) => {
); );
}; };
export const journalExtension: EditorExtension = { export const journalTab: SidebarTab = {
name: 'journal', name: 'journal',
icon: <TodayIcon />, icon: <TodayIcon />,
Component: EditorJournalPanel, Component: EditorJournalPanel,

View File

@@ -1,16 +1,14 @@
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { TocIcon } from '@blocksuite/icons'; import { TocIcon } from '@blocksuite/icons';
import { OutlinePanel } from '@blocksuite/presets'; import { OutlinePanel } from '@blocksuite/presets';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { EditorExtension } from '../types'; import type { SidebarTab, SidebarTabProps } from '../sidebar-tab';
import * as styles from './outline.css'; import * as styles from './outline.css';
// A wrapper for TOCNotesPanel // A wrapper for TOCNotesPanel
const EditorOutline = () => { const EditorOutline = ({ editor }: SidebarTabProps) => {
const outlinePanelRef = useRef<OutlinePanel | null>(null); const outlinePanelRef = useRef<OutlinePanel | null>(null);
const [editor] = useActiveBlocksuiteEditor();
const onRefChange = useCallback((container: HTMLDivElement | null) => { const onRefChange = useCallback((container: HTMLDivElement | null) => {
if (container) { if (container) {
@@ -35,7 +33,7 @@ const EditorOutline = () => {
return <div className={styles.root} ref={onRefChange} />; return <div className={styles.root} ref={onRefChange} />;
}; };
export const outlineExtension: EditorExtension = { export const outlineTab: SidebarTab = {
name: 'outline', name: 'outline',
icon: <TocIcon />, icon: <TocIcon />,
Component: EditorOutline, Component: EditorOutline,

View File

@@ -0,0 +1,4 @@
export type { SidebarTabName } from './entities/sidebar-tab';
export { sidebarTabs } from './entities/sidebar-tabs';
export { MultiTabSidebarBody } from './view/body';
export { MultiTabSidebarHeaderSwitcher } from './view/header-switcher';

View File

@@ -0,0 +1,13 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
flexDirection: 'column',
flex: 1,
width: '100%',
height: '100%',
minWidth: '320px',
overflow: 'hidden',
alignItems: 'center',
borderTop: `1px solid ${cssVar('borderColor')}`,
});

View File

@@ -0,0 +1,17 @@
import type { PropsWithChildren } from 'react';
import type { SidebarTab, SidebarTabProps } from '../entities/sidebar-tab';
import * as styles from './body.css';
export const MultiTabSidebarBody = (
props: PropsWithChildren<SidebarTabProps & { tab?: SidebarTab | null }>
) => {
const Component = props.tab?.Component;
return (
<div className={styles.root}>
{props.children}
{Component ? <Component {...props} /> : null}
</div>
);
};

View File

@@ -1,6 +1,11 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { createVar, style } from '@vanilla-extract/css'; import { createVar, style } from '@vanilla-extract/css';
export const activeIdx = createVar(); export const activeIdx = createVar();
export const switchRootWrapper = style({
height: '52px',
display: 'flex',
alignItems: 'center',
});
export const switchRoot = style({ export const switchRoot = style({
vars: { vars: {
[activeIdx]: '0', [activeIdx]: '0',

View File

@@ -0,0 +1,77 @@
import { IconButton } from '@affine/component';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { useWorkspaceEnabledFeatures } from '@affine/core/hooks/use-workspace-features';
import { FeatureType } from '@affine/graphql';
import { Doc, useService, Workspace } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useEffect, useMemo } from 'react';
import type { SidebarTab, SidebarTabName } from '../entities/sidebar-tab';
import * as styles from './header-switcher.css';
export interface MultiTabSidebarHeaderSwitcherProps {
tabs: SidebarTab[];
activeTabName: SidebarTabName | null;
setActiveTabName: (ext: SidebarTabName) => void;
}
// provide a switcher for active extensions
// will be used in global top header (MacOS) or sidebar (Windows)
export const MultiTabSidebarHeaderSwitcher = ({
tabs,
activeTabName,
setActiveTabName,
}: MultiTabSidebarHeaderSwitcherProps) => {
const workspace = useService(Workspace);
const doc = useService(Doc);
const copilotEnabled = useWorkspaceEnabledFeatures(workspace.meta).includes(
FeatureType.Copilot
);
const { isJournal } = useJournalInfoHelper(
workspace.blockSuiteWorkspace,
doc.id
);
const exts = useMemo(
() =>
tabs.filter(ext => {
if (ext.name === 'copilot' && !copilotEnabled) return false;
return true;
}),
[copilotEnabled, tabs]
);
const activeExtension = exts.find(ext => ext.name === activeTabName);
// if journal is active, set selected to journal
useEffect(() => {
const journalExtension = tabs.find(ext => ext.name === 'journal');
isJournal && journalExtension && setActiveTabName('journal');
}, [tabs, isJournal, setActiveTabName]);
const vars = assignInlineVars({
[styles.activeIdx]: String(
exts.findIndex(ext => ext.name === activeExtension?.name) ?? 0
),
});
return (
<div className={styles.switchRootWrapper}>
<div className={styles.switchRoot} style={vars}>
{exts.map(extension => {
return (
<IconButton
onClick={() => setActiveTabName(extension.name)}
key={extension.name}
data-active={activeExtension === extension}
className={styles.button}
>
{extension.icon}
</IconButton>
);
})}
</div>
</div>
);
};

View File

@@ -1,3 +0,0 @@
export class PageListView {
constructor() {}
}

View File

@@ -0,0 +1,6 @@
import { createIsland } from '../../../utils/island';
export class RightSidebarView {
readonly body = createIsland();
readonly header = createIsland();
}

View File

@@ -0,0 +1,47 @@
import { LiveData } from '@toeverything/infra/livedata';
import type { RightSidebarView } from './right-sidebar-view';
export class RightSidebar {
readonly isOpen = new LiveData(false);
readonly views = new LiveData<RightSidebarView[]>([]);
readonly front = this.views.map(
stack => stack[0] as RightSidebarView | undefined
);
readonly hasViews = this.views.map(stack => stack.length > 0);
open() {
this.isOpen.next(true);
}
toggle() {
this.isOpen.next(!this.isOpen.value);
}
close() {
this.isOpen.next(false);
}
/**
* @private use `RightSidebarViewIsland` instead
*/
_append(view: RightSidebarView) {
this.views.next([...this.views.value, view]);
}
/**
* @private use `RightSidebarViewIsland` instead
*/
_moveToFront(view: RightSidebarView) {
if (this.views.value.includes(view)) {
this.views.next([view, ...this.views.value.filter(v => v !== view)]);
}
}
/**
* @private use `RightSidebarViewIsland` instead
*/
_remove(view: RightSidebarView) {
this.views.next(this.views.value.filter(v => v !== view));
}
}

View File

@@ -0,0 +1,3 @@
export { RightSidebar } from './entities/right-sidebar';
export { RightSidebarContainer } from './view/container';
export { RightSidebarViewIsland } from './view/view-island';

View File

@@ -0,0 +1,40 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const sidebarContainerInner = style({
display: 'flex',
background: cssVar('backgroundPrimaryColor'),
flexDirection: 'column',
overflow: 'hidden',
height: '100%',
width: '100%',
});
export const sidebarContainer = style({
display: 'flex',
flexShrink: 0,
height: '100%',
selectors: {
[`&[data-client-border=true]`]: {
paddingLeft: 9,
},
[`&[data-client-border=false]`]: {
borderLeft: `1px solid ${cssVar('borderColor')}`,
},
},
});
export const sidebarBodyTarget = style({
flex: 1,
width: '100%',
minWidth: '320px',
overflow: 'hidden',
selectors: {
[`&[data-client-border=true]`]: {
paddingLeft: 9,
},
[`&[data-client-border=false]`]: {
borderLeft: `1px solid ${cssVar('borderColor')}`,
},
},
});

View File

@@ -0,0 +1,68 @@
import { ResizePanel } from '@affine/component/resize-panel';
import { appSettingAtom } from '@toeverything/infra/atom';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react';
import { RightSidebar } from '../entities/right-sidebar';
import * as styles from './container.css';
import { Header } from './header';
const MIN_SIDEBAR_WIDTH = 320;
const MAX_SIDEBAR_WIDTH = 800;
export const RightSidebarContainer = () => {
const { clientBorder } = useAtomValue(appSettingAtom);
const [width, setWidth] = useState(300);
const [resizing, setResizing] = useState(false);
const rightSidebar = useService(RightSidebar);
const frontView = useLiveData(rightSidebar.front);
const open = useLiveData(rightSidebar.isOpen) && frontView !== undefined;
const handleOpenChange = useCallback(
(open: boolean) => {
if (open) {
rightSidebar.open();
} else {
rightSidebar.close();
}
},
[rightSidebar]
);
const handleToggleOpen = useCallback(() => {
rightSidebar.toggle();
}, [rightSidebar]);
return (
<ResizePanel
resizeHandlePos="left"
resizeHandleOffset={clientBorder ? 4 : 0}
width={width}
resizing={resizing}
onResizing={setResizing}
className={styles.sidebarContainer}
data-client-border={clientBorder && open}
open={open}
onOpen={handleOpenChange}
onWidthChange={setWidth}
minWidth={MIN_SIDEBAR_WIDTH}
maxWidth={MAX_SIDEBAR_WIDTH}
>
{frontView && (
<div className={styles.sidebarContainerInner}>
<Header
floating={false}
onToggle={handleToggleOpen}
view={frontView}
/>
<frontView.body.Target
className={styles.sidebarBodyTarget}
></frontView.body.Target>
</div>
)}
</ResizePanel>
);
};

View File

@@ -0,0 +1,43 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const header = style({
display: 'flex',
height: '52px',
width: '100%',
alignItems: 'center',
flexShrink: 0,
padding: '0 16px',
gap: '12px',
background: cssVar('backgroundPrimaryColor'),
selectors: {
'&[data-sidebar-floating="false"]': {
['WebkitAppRegion' as string]: 'drag',
},
},
'@media': {
print: {
display: 'none',
},
},
});
export const spacer = style({
flexGrow: 1,
minWidth: 12,
});
export const standaloneExtensionSwitcherWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: '52px',
position: 'relative',
});
export const windowsAppControlsContainer = style({
display: 'flex',
height: '100%',
marginRight: '-16px',
});

View File

@@ -0,0 +1,69 @@
import { IconButton } from '@affine/component';
import { RightSidebarIcon } from '@blocksuite/icons';
import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls';
import type { RightSidebarView } from '../entities/right-sidebar-view';
import * as styles from './header.css';
export type HeaderProps = {
floating: boolean;
onToggle?: () => void;
view: RightSidebarView;
};
function Container({
children,
style,
className,
floating,
}: {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
floating?: boolean;
}) {
return (
<div
data-testid="header"
style={style}
className={className}
data-sidebar-floating={floating}
>
{children}
</div>
);
}
const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => {
return (
<IconButton size="large" onClick={onToggle}>
<RightSidebarIcon />
</IconButton>
);
};
const Windows = ({ floating, onToggle, view }: HeaderProps) => {
return (
<Container className={styles.header} floating={floating}>
<view.header.Target></view.header.Target>
<div className={styles.spacer} />
<ToggleButton onToggle={onToggle} />
<div className={styles.windowsAppControlsContainer}>
<WindowsAppControls />
</div>
</Container>
);
};
const NonWindows = ({ floating, view, onToggle }: HeaderProps) => {
return (
<Container className={styles.header} floating={floating}>
<view.header.Target></view.header.Target>
<div className={styles.spacer} />
<ToggleButton onToggle={onToggle} />
</Container>
);
};
export const Header =
environment.isDesktop && environment.isWindows ? Windows : NonWindows;

View File

@@ -0,0 +1,42 @@
import { useService } from '@toeverything/infra';
import { useEffect, useMemo } from 'react';
import { RightSidebar } from '../entities/right-sidebar';
import { RightSidebarView } from '../entities/right-sidebar-view';
export interface RightSidebarViewProps {
body: JSX.Element;
header?: JSX.Element | null;
name?: string;
active?: boolean;
}
export const RightSidebarViewIsland = ({
body,
header,
active,
}: RightSidebarViewProps) => {
const rightSidebar = useService(RightSidebar);
const view = useMemo(() => new RightSidebarView(), []);
useEffect(() => {
rightSidebar._append(view);
return () => {
rightSidebar._remove(view);
};
}, [rightSidebar, view]);
useEffect(() => {
if (active) {
rightSidebar._moveToFront(view);
}
}, [active, rightSidebar, view]);
return (
<>
<view.header.Provider>{header}</view.header.Provider>
<view.body.Provider>{body}</view.body.Provider>
</>
);
};

View File

@@ -11,6 +11,7 @@ import {
LocalStorageGlobalCache, LocalStorageGlobalCache,
LocalStorageGlobalState, LocalStorageGlobalState,
} from './infra-web/storage'; } from './infra-web/storage';
import { RightSidebar } from './right-sidebar/entities/right-sidebar';
import { Workbench } from './workbench'; import { Workbench } from './workbench';
import { import {
CurrentWorkspaceService, CurrentWorkspaceService,
@@ -23,6 +24,7 @@ export function configureBusinessServices(services: ServiceCollection) {
services services
.scope(WorkspaceScope) .scope(WorkspaceScope)
.add(Workbench) .add(Workbench)
.add(RightSidebar)
.add(WorkspacePropertiesAdapter, [Workspace]) .add(WorkspacePropertiesAdapter, [Workspace])
.add(CollectionService, [Workspace]) .add(CollectionService, [Workspace])
.add(WorkspaceLegacyProperties, [Workspace]); .add(WorkspaceLegacyProperties, [Workspace]);

View File

@@ -4,6 +4,8 @@ import { createMemoryHistory } from 'history';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { createIsland } from '../../../utils/island';
export class View { export class View {
id = nanoid(); id = nanoid();
@@ -19,6 +21,9 @@ export class View {
this.history.location this.history.location
); );
header = createIsland();
body = createIsland();
push(path: To) { push(path: To) {
this.history.push(path); this.history.push(path);
} }

View File

@@ -1,2 +1,7 @@
export * from './view'; export { View } from './entities/view';
export * from './workbench'; export { Workbench } from './entities/workbench';
export { useIsActiveView } from './view/use-is-active-view';
export { ViewBodyIsland } from './view/view-body-island';
export { ViewHeaderIsland } from './view/view-header-island';
export { WorkbenchLink } from './view/workbench-link';
export { WorkbenchRoot } from './view/workbench-root';

View File

@@ -4,7 +4,7 @@ import { useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import type { Workbench } from './workbench'; import type { Workbench } from '../entities/workbench';
/** /**
* This hook binds the workbench to the browser router. * This hook binds the workbench to the browser router.

View File

@@ -3,7 +3,7 @@ import { useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import type { Workbench } from './workbench'; import type { Workbench } from '../entities/workbench';
/** /**
* This hook binds the workbench to the browser router. * This hook binds the workbench to the browser router.

View File

@@ -1 +0,0 @@
export * from './view';

View File

@@ -0,0 +1,57 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
height: '100%',
overflow: 'hidden',
width: '100%',
position: 'relative',
flexDirection: 'column',
minWidth: 0,
background: cssVar('backgroundPrimaryColor'),
});
export const header = style({
display: 'flex',
height: '52px',
width: '100%',
alignItems: 'center',
flexShrink: 0,
background: cssVar('backgroundPrimaryColor'),
padding: '0 16px',
['WebkitAppRegion' as string]: 'drag',
'@media': {
print: {
display: 'none',
},
},
});
export const viewBodyContainer = style({
display: 'flex',
flex: 1,
overflow: 'hidden',
});
export const leftSidebarButton = style({
margin: '0 16px 0 0',
});
export const rightSidebarButton = style({
margin: '0 0 0 16px',
});
export const viewHeaderContainer = style({
display: 'flex',
height: '100%',
flexGrow: 1,
minWidth: 12,
});
export const windowsAppControlsContainer = style({
display: 'flex',
height: '100%',
marginRight: '-16px',
paddingLeft: '16px',
});

View File

@@ -0,0 +1,76 @@
import { IconButton } from '@affine/component';
import {
appSidebarOpenAtom,
SidebarSwitch,
} from '@affine/component/app-sidebar';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { RightSidebarIcon } from '@blocksuite/icons';
import { useLiveData } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai';
import { Suspense, useCallback } from 'react';
import { RightSidebar } from '../../right-sidebar';
import * as styles from './route-container.css';
import { useView } from './use-view';
import { useViewPosition } from './use-view-position';
export interface Props {
route: {
Component: React.ComponentType;
};
}
const ToggleButton = ({
onToggle,
className,
}: {
onToggle?: () => void;
className: string;
}) => {
return (
<IconButton size="large" onClick={onToggle} className={className}>
<RightSidebarIcon />
</IconButton>
);
};
export const RouteContainer = ({ route }: Props) => {
const view = useView();
const viewPosition = useViewPosition();
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
const rightSidebar = useService(RightSidebar);
const rightSidebarOpen = useLiveData(rightSidebar.isOpen);
const rightSidebarHasViews = useLiveData(rightSidebar.hasViews);
const handleToggleRightSidebar = useCallback(() => {
rightSidebar.toggle();
}, [rightSidebar]);
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
return (
<div className={styles.root}>
<div className={styles.header}>
{viewPosition.isFirst && !leftSidebarOpen && (
<SidebarSwitch className={styles.leftSidebarButton} />
)}
<view.header.Target className={styles.viewHeaderContainer} />
{viewPosition.isLast && !rightSidebarOpen && rightSidebarHasViews && (
<>
<ToggleButton
className={styles.rightSidebarButton}
onToggle={handleToggleRightSidebar}
/>
{isWindowsDesktop && (
<div className={styles.windowsAppControlsContainer}>
<WindowsAppControls />
</div>
)}
</>
)}
</div>
<view.body.Target className={styles.viewBodyContainer} />
<Suspense fallback={<>loading</>}>
<route.Component />
</Suspense>
</div>
);
};

View File

@@ -0,0 +1,12 @@
import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra';
import { Workbench } from '../entities/workbench';
import { useView } from './use-view';
export function useIsActiveView() {
const workbench = useService(Workbench);
const currentView = useView();
const activeView = useLiveData(workbench.activeView);
return currentView === activeView;
}

View File

@@ -0,0 +1,35 @@
import { useService } from '@toeverything/infra/di';
import { useEffect, useState } from 'react';
import type { View } from '../entities/view';
import { Workbench } from '../entities/workbench';
import { useView } from './use-view';
export const useViewPosition = () => {
const workbench = useService(Workbench);
const view = useView();
const [position, setPosition] = useState(() =>
calcPosition(view, workbench.views.value)
);
useEffect(() => {
const subscription = workbench.views.subscribe(views => {
setPosition(calcPosition(view, views));
});
return () => {
subscription.unsubscribe();
};
}, [view, workbench]);
return position;
};
function calcPosition(view: View, viewList: View[]) {
const index = viewList.indexOf(view);
return {
index: index,
isFirst: index === 0,
isLast: index === viewList.length - 1,
};
}

View File

@@ -0,0 +1,15 @@
import { createContext, useContext } from 'react';
import type { View } from '../entities/view';
export const ViewContext = createContext<View | null>(null);
export const useView = () => {
const view = useContext(ViewContext);
if (!view) {
throw new Error(
'No view found in context. Make sure you are rendering inside a ViewRoot.'
);
}
return view;
};

View File

@@ -0,0 +1,6 @@
import { useView } from './use-view';
export const ViewBodyIsland = ({ children }: React.PropsWithChildren) => {
const view = useView();
return <view.body.Provider>{children}</view.body.Provider>;
};

View File

@@ -0,0 +1,6 @@
import { useView } from './use-view';
export const ViewHeaderIsland = ({ children }: React.PropsWithChildren) => {
const view = useView();
return <view.header.Provider>{children}</view.header.Provider>;
};

View File

@@ -1,5 +1,5 @@
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { useEffect, useMemo } from 'react'; import { lazy as reactLazy, useEffect, useMemo } from 'react';
import { import {
createMemoryRouter, createMemoryRouter,
RouterProvider, RouterProvider,
@@ -8,10 +8,30 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import { viewRoutes } from '../../../router'; import { viewRoutes } from '../../../router';
import type { View } from './view'; import type { View } from '../entities/view';
import { RouteContainer } from './route-container';
import { ViewContext } from './use-view';
const warpedRoutes = viewRoutes.map(({ path, lazy }) => {
const Component = reactLazy(() =>
lazy().then(m => ({
default: m.Component as React.ComponentType,
}))
);
const route = {
Component,
};
return {
path,
Component: () => {
return <RouteContainer route={route} />;
},
};
});
export const ViewRoot = ({ view }: { view: View }) => { export const ViewRoot = ({ view }: { view: View }) => {
const viewRouter = useMemo(() => createMemoryRouter(viewRoutes), []); const viewRouter = useMemo(() => createMemoryRouter(warpedRoutes), []);
const location = useLiveData(view.location); const location = useLiveData(view.location);
@@ -23,6 +43,7 @@ export const ViewRoot = ({ view }: { view: View }) => {
// https://github.com/remix-run/react-router/issues/7375#issuecomment-975431736 // https://github.com/remix-run/react-router/issues/7375#issuecomment-975431736
return ( return (
<ViewContext.Provider value={view}>
<UNSAFE_LocationContext.Provider value={null as any}> <UNSAFE_LocationContext.Provider value={null as any}>
<UNSAFE_RouteContext.Provider <UNSAFE_RouteContext.Provider
value={{ value={{
@@ -34,5 +55,6 @@ export const ViewRoot = ({ view }: { view: View }) => {
<RouterProvider router={viewRouter} /> <RouterProvider router={viewRouter} />
</UNSAFE_RouteContext.Provider> </UNSAFE_RouteContext.Provider>
</UNSAFE_LocationContext.Provider> </UNSAFE_LocationContext.Provider>
</ViewContext.Provider>
); );
}; };

View File

@@ -2,7 +2,7 @@ import { useService } from '@toeverything/infra/di';
import type { To } from 'history'; import type { To } from 'history';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Workbench } from './workbench'; import { Workbench } from '../entities/workbench';
export const WorkbenchLink = ({ export const WorkbenchLink = ({
to, to,
@@ -17,11 +17,15 @@ export const WorkbenchLink = ({
(event: React.MouseEvent<HTMLAnchorElement>) => { (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault(); event.preventDefault();
// TODO: open this when multi view control is implemented // TODO: open this when multi view control is implemented
// if (environment.isDesktop && (event.ctrlKey || event.metaKey)) { if (
// workbench.open(to, { at: 'beside' }); (window as any).enableMultiView &&
// } else { environment.isDesktop &&
(event.ctrlKey || event.metaKey)
) {
workbench.open(to, { at: 'beside' });
} else {
workbench.open(to); workbench.open(to);
// } }
onClick?.(event); onClick?.(event);
}, },

View File

@@ -3,8 +3,17 @@ import { style } from '@vanilla-extract/css';
export const workbenchRootContainer = style({ export const workbenchRootContainer = style({
display: 'flex', display: 'flex',
height: '100%', height: '100%',
flex: 1,
overflow: 'hidden',
selectors: {
[`&[data-client-border="true"]`]: {
gap: '8px',
},
},
}); });
export const workbenchViewContainer = style({ export const workbenchViewContainer = style({
flex: 1, flex: 1,
overflow: 'hidden',
height: '100%',
}); });

View File

@@ -1,17 +1,16 @@
import { appSettingAtom } from '@toeverything/infra/atom';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { useCallback } from 'react'; import { useAtomValue } from 'jotai';
import { useCallback, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import type { View } from '../entities/view';
import { Workbench } from '../entities/workbench';
import { useBindWorkbenchToBrowserRouter } from './browser-adapter'; import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter'; import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
import type { View } from './view'; import { ViewRoot } from './view-root';
import { ViewRoot } from './view/view-root'; import * as styles from './workbench-root.css';
import { Workbench } from './workbench';
import {
workbenchRootContainer,
workbenchViewContainer,
} from './workbench-root.css';
const useAdapter = environment.isDesktop const useAdapter = environment.isDesktop
? useBindWorkbenchToDesktopRouter ? useBindWorkbenchToDesktopRouter
@@ -30,8 +29,13 @@ export const WorkbenchRoot = () => {
useAdapter(workbench, basename); useAdapter(workbench, basename);
const { clientBorder } = useAtomValue(appSettingAtom);
return ( return (
<div className={workbenchRootContainer}> <div
className={styles.workbenchRootContainer}
data-client-border={!!clientBorder}
>
{views.map((view, index) => ( {views.map((view, index) => (
<WorkbenchView key={view.id} view={view} index={index} /> <WorkbenchView key={view.id} view={view} index={index} />
))} ))}
@@ -46,8 +50,25 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
workbench.active(index); workbench.active(index);
}, [workbench, index]); }, [workbench, index]);
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (containerRef.current) {
const element = containerRef.current;
element.addEventListener('mousedown', handleOnFocus, {
capture: true,
});
return () => {
element.removeEventListener('mousedown', handleOnFocus, {
capture: true,
});
};
}
return;
}, [handleOnFocus]);
return ( return (
<div className={workbenchViewContainer} onMouseDownCapture={handleOnFocus}> <div className={styles.workbenchViewContainer} ref={containerRef}>
<ViewRoot key={view.id} view={view} /> <ViewRoot key={view.id} view={view} />
</div> </div>
); );

View File

@@ -0,0 +1,14 @@
import { style } from '@vanilla-extract/css';
export const headerCreateNewCollectionIconButton = style({
padding: '4px 8px',
fontSize: '16px',
width: '32px',
height: '28px',
borderRadius: '8px',
transition: 'opacity 0.1s ease-in-out',
});
export const headerCreateNewButtonHidden = style({
opacity: 0,
pointerEvents: 'none',
});

View File

@@ -1,12 +1,10 @@
import { IconButton } from '@affine/component'; import { IconButton } from '@affine/component';
import { Header } from '@affine/core/components/pure/header'; import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { PlusIcon } from '@blocksuite/icons'; import { PlusIcon } from '@blocksuite/icons';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import * as styles from '../all-page/all-page.css'; import * as styles from './header.css';
export const AllCollectionHeader = ({ export const AllCollectionHeader = ({
showCreateNew, showCreateNew,
@@ -15,33 +13,18 @@ export const AllCollectionHeader = ({
showCreateNew: boolean; showCreateNew: boolean;
onCreateCollection?: () => void; onCreateCollection?: () => void;
}) => { }) => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const renderRightItem = useMemo(() => {
return ( return (
<Header
right={
<IconButton <IconButton
type="default" type="default"
icon={<PlusIcon fontSize={16} />} icon={<PlusIcon fontSize={16} />}
onClick={onCreateCollection} onClick={onCreateCollection}
className={clsx( className={clsx(
styles.headerCreateNewButton,
styles.headerCreateNewCollectionIconButton, styles.headerCreateNewCollectionIconButton,
!showCreateNew && styles.headerCreateNewButtonHidden !showCreateNew && styles.headerCreateNewButtonHidden
)} )}
/> />
);
}, [onCreateCollection, showCreateNew]);
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{renderRightItem}
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
} }
center={<WorkspaceModeFilterTab activeFilter={'collections'} />} center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
/> />

View File

@@ -1,4 +1,3 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { import {
CollectionListHeader, CollectionListHeader,
type CollectionMeta, type CollectionMeta,
@@ -16,7 +15,7 @@ import { nanoid } from 'nanoid';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { CollectionService } from '../../../modules/collection'; import { CollectionService } from '../../../modules/collection';
import * as styles from '../all-page/all-page.css'; import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
import { EmptyCollectionList } from '../page-list-empty'; import { EmptyCollectionList } from '../page-list-empty';
import { AllCollectionHeader } from './header'; import { AllCollectionHeader } from './header';
@@ -58,11 +57,14 @@ export const AllCollection = () => {
}, [collectionService, currentWorkspace, navigateHelper, open]); }, [collectionService, currentWorkspace, navigateHelper, open]);
return ( return (
<div className={styles.root}> <>
<ViewHeaderIsland>
<AllCollectionHeader <AllCollectionHeader
showCreateNew={!hideHeaderCreateNew} showCreateNew={!hideHeaderCreateNew}
onCreateCollection={handleCreateCollection} onCreateCollection={handleCreateCollection}
/> />
</ViewHeaderIsland>
<ViewBodyIsland>
{collectionMetas.length > 0 ? ( {collectionMetas.length > 0 ? (
<VirtualizedCollectionList <VirtualizedCollectionList
collections={collections} collections={collections}
@@ -82,9 +84,8 @@ export const AllCollection = () => {
} }
/> />
)} )}
</ViewBodyIsland>
<HubIsland /> </>
</div>
); );
}; };

View File

@@ -3,17 +3,14 @@ import {
PageListNewPageButton, PageListNewPageButton,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header'; import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import type { Filter } from '@affine/env/filter'; import type { Filter } from '@affine/env/filter';
import { PlusIcon } from '@blocksuite/icons'; import { PlusIcon } from '@blocksuite/icons';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import * as styles from './all-page.css'; import * as styles from './all-page.css';
import { FilterContainer } from './all-page-filter';
export const AllPageHeader = ({ export const AllPageHeader = ({
showCreateNew, showCreateNew,
@@ -25,24 +22,8 @@ export const AllPageHeader = ({
onChangeFilters: (filters: Filter[]) => void; onChangeFilters: (filters: Filter[]) => void;
}) => { }) => {
const workspace = useService(Workspace); const workspace = useService(Workspace);
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const renderRightItem = useMemo(() => {
return (
<PageListNewPageButton
size="small"
className={clsx(
styles.headerCreateNewButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
>
<PlusIcon />
</PageListNewPageButton>
);
}, [showCreateNew]);
return ( return (
<>
<Header <Header
left={ left={
<AllPageListOperationsMenu <AllPageListOperationsMenu
@@ -52,17 +33,17 @@ export const AllPageHeader = ({
/> />
} }
right={ right={
<div <PageListNewPageButton
className={styles.headerRightWindows} size="small"
data-is-windows-desktop={isWindowsDesktop} className={clsx(
styles.headerCreateNewButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
> >
{renderRightItem} <PlusIcon />
{isWindowsDesktop ? <WindowsAppControls /> : null} </PageListNewPageButton>
</div>
} }
center={<WorkspaceModeFilterTab activeFilter={'docs'} />} center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
/> />
<FilterContainer filters={filters} onChangeFilters={onChangeFilters} />
</>
); );
}; };

View File

@@ -1,12 +1,4 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const root = style({
height: '100%',
width: '100%',
display: 'flex',
flexFlow: 'column',
background: cssVar('backgroundPrimaryColor'),
});
export const scrollContainer = style({ export const scrollContainer = style({
flex: 1, flex: 1,
width: '100%', width: '100%',
@@ -26,13 +18,11 @@ export const headerCreateNewButtonHidden = style({
opacity: 0, opacity: 0,
pointerEvents: 'none', pointerEvents: 'none',
}); });
export const headerRightWindows = style({
export const body = style({
display: 'flex', display: 'flex',
alignItems: 'center', flexDirection: 'column',
gap: 8, flex: 1,
selectors: { height: '100%',
'&[data-is-windows-desktop="true"]': { width: '100%',
transform: 'translateX(16px)',
},
},
}); });

View File

@@ -1,4 +1,3 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { import {
PageListHeader, PageListHeader,
useFilteredPageMetas, useFilteredPageMetas,
@@ -12,8 +11,10 @@ import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
import { EmptyPageList } from '../page-list-empty'; import { EmptyPageList } from '../page-list-empty';
import * as styles from './all-page.css'; import * as styles from './all-page.css';
import { FilterContainer } from './all-page-filter';
import { AllPageHeader } from './all-page-header'; import { AllPageHeader } from './all-page-header';
export const AllPage = () => { export const AllPage = () => {
@@ -27,12 +28,17 @@ export const AllPage = () => {
}); });
return ( return (
<div className={styles.root}> <>
<ViewHeaderIsland>
<AllPageHeader <AllPageHeader
showCreateNew={!hideHeaderCreateNew} showCreateNew={!hideHeaderCreateNew}
filters={filters} filters={filters}
onChangeFilters={setFilters} onChangeFilters={setFilters}
/> />
</ViewHeaderIsland>
<ViewBodyIsland>
<div className={styles.body}>
<FilterContainer filters={filters} onChangeFilters={setFilters} />
{filteredPageMetas.length > 0 ? ( {filteredPageMetas.length > 0 ? (
<VirtualizedPageList <VirtualizedPageList
setHideHeaderCreateNewPage={setHideHeaderCreateNew} setHideHeaderCreateNewPage={setHideHeaderCreateNew}
@@ -45,8 +51,9 @@ export const AllPage = () => {
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/> />
)} )}
<HubIsland />
</div> </div>
</ViewBodyIsland>
</>
); );
}; };

View File

@@ -1,9 +1,9 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const root = style({
export const body = style({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flex: 1, flex: 1,
overflow: 'auto', height: '100%',
width: '100%', width: '100%',
minWidth: '320px',
}); });

View File

@@ -1,23 +1,6 @@
import { Header } from '@affine/core/components/pure/header'; import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import * as styles from '../all-page/all-page.css';
export const AllTagHeader = () => { export const AllTagHeader = () => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows; return <Header center={<WorkspaceModeFilterTab activeFilter={'tags'} />} />;
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
/>
);
}; };

View File

@@ -1,4 +1,3 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { useTagMetas } from '@affine/core/components/page-list'; import { useTagMetas } from '@affine/core/components/page-list';
import { import {
TagListHeader, TagListHeader,
@@ -8,8 +7,9 @@ import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-me
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import * as styles from '../all-page/all-page.css'; import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
import { EmptyTagList } from '../page-list-empty'; import { EmptyTagList } from '../page-list-empty';
import * as styles from './all-tag.css';
import { AllTagHeader } from './header'; import { AllTagHeader } from './header';
export const AllTag = () => { export const AllTag = () => {
@@ -22,8 +22,12 @@ export const AllTag = () => {
); );
return ( return (
<div className={styles.root}> <>
<ViewHeaderIsland>
<AllTagHeader /> <AllTagHeader />
</ViewHeaderIsland>
<ViewBodyIsland>
<div className={styles.body}>
{tags.length > 0 ? ( {tags.length > 0 ? (
<VirtualizedTagList <VirtualizedTagList
tags={tags} tags={tags}
@@ -33,8 +37,9 @@ export const AllTag = () => {
) : ( ) : (
<EmptyTagList heading={<TagListHeader />} /> <EmptyTagList heading={<TagListHeader />} />
)} )}
<HubIsland />
</div> </div>
</ViewBodyIsland>
</>
); );
}; };

View File

@@ -23,3 +23,17 @@ export const button = style({
backgroundColor: cssVar('hoverColor'), backgroundColor: cssVar('hoverColor'),
}, },
}); });
export const headerCreateNewButton = style({
transition: 'opacity 0.1s ease-in-out',
});
export const headerCreateNewCollectionIconButton = style({
padding: '4px 8px',
fontSize: '16px',
width: '32px',
height: '28px',
borderRadius: '8px',
});
export const headerCreateNewButtonHidden = style({
opacity: 0,
pointerEvents: 'none',
});

View File

@@ -1,12 +1,10 @@
import { IconButton } from '@affine/component'; import { IconButton } from '@affine/component';
import { Header } from '@affine/core/components/pure/header'; import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { PlusIcon } from '@blocksuite/icons'; import { PlusIcon } from '@blocksuite/icons';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import * as styles from '../all-page/all-page.css'; import * as styles from './collection.css';
export const CollectionDetailHeader = ({ export const CollectionDetailHeader = ({
showCreateNew, showCreateNew,
@@ -15,10 +13,9 @@ export const CollectionDetailHeader = ({
showCreateNew: boolean; showCreateNew: boolean;
onCreate: () => void; onCreate: () => void;
}) => { }) => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const renderRightItem = useMemo(() => {
return ( return (
<Header
right={
<IconButton <IconButton
type="default" type="default"
icon={<PlusIcon fontSize={16} />} icon={<PlusIcon fontSize={16} />}
@@ -29,19 +26,6 @@ export const CollectionDetailHeader = ({
!showCreateNew && styles.headerCreateNewButtonHidden !showCreateNew && styles.headerCreateNewButtonHidden
)} )}
/> />
);
}, [onCreate, showCreateNew]);
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{renderRightItem}
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
} }
center={<WorkspaceModeFilterTab activeFilter={'collections'} />} center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
/> />

View File

@@ -1,15 +1,9 @@
import {
appSidebarOpenAtom,
SidebarSwitch,
} from '@affine/component/app-sidebar';
import { pushNotificationAtom } from '@affine/component/notification-center'; import { pushNotificationAtom } from '@affine/component/notification-center';
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { import {
AffineShapeIcon, AffineShapeIcon,
useEditCollection, useEditCollection,
VirtualizedPageList, VirtualizedPageList,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config'; import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { CollectionService } from '@affine/core/modules/collection'; import { CollectionService } from '@affine/core/modules/collection';
@@ -25,14 +19,13 @@ import {
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
import { WorkspaceSubPath } from '../../../shared'; import { WorkspaceSubPath } from '../../../shared';
import * as allPageStyles from '../all-page/all-page.css';
import * as styles from './collection.css'; import * as styles from './collection.css';
import { CollectionDetailHeader } from './header'; import { CollectionDetailHeader } from './header';
@@ -52,19 +45,22 @@ export const CollectionDetail = ({
}, [collection, collectionService, open]); }, [collection, collectionService, open]);
return ( return (
<div className={allPageStyles.root}> <>
<ViewHeaderIsland>
<CollectionDetailHeader <CollectionDetailHeader
showCreateNew={!hideHeaderCreateNew} showCreateNew={!hideHeaderCreateNew}
onCreate={handleEditCollection} onCreate={handleEditCollection}
/> />
</ViewHeaderIsland>
<ViewBodyIsland>
<VirtualizedPageList <VirtualizedPageList
collection={collection} collection={collection}
config={config} config={config}
setHideHeaderCreateNewPage={setHideHeaderCreateNew} setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/> />
<HubIsland /> </ViewBodyIsland>
{node} {node}
</div> </>
); );
}; };
@@ -115,8 +111,6 @@ export const Component = function CollectionPage() {
); );
}; };
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const Placeholder = ({ collection }: { collection: Collection }) => { const Placeholder = ({ collection }: { collection: Collection }) => {
const workspace = useService(Workspace); const workspace = useService(Workspace);
const collectionService = useService(CollectionService); const collectionService = useService(CollectionService);
@@ -139,32 +133,22 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
localStorage.setItem('hide-empty-collection-help-info', 'true'); localStorage.setItem('hide-empty-collection-help-info', 'true');
}, []); }, []);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
const handleJumpToCollections = useCallback(() => { const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspace.id); jumpToCollections(workspace.id);
}, [jumpToCollections, workspace]); }, [jumpToCollections, workspace]);
return ( return (
<div <>
style={{ <ViewHeaderIsland>
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
height: 52,
paddingLeft: '16px',
fontSize: 'var(--affine-font-xs)', fontSize: 'var(--affine-font-xs)',
['WebkitAppRegion' as string]: 'drag',
}} }}
> >
<SidebarSwitch show={!leftSidebarOpen} />
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -194,8 +178,9 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
{collection.name} {collection.name}
</div> </div>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
{isWindowsDesktop && <WindowsAppControls />}
</div> </div>
</ViewHeaderIsland>
<ViewBodyIsland>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -320,9 +305,9 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
</div> </div>
<div> <div>
<Trans i18nKey="com.affine.collection.addRules.tips"> <Trans i18nKey="com.affine.collection.addRules.tips">
<span style={{ fontWeight: 600 }}>Add rules:</span> Rules are <span style={{ fontWeight: 600 }}>Add rules:</span> Rules
based on filtering. After adding rules, pages that meet the are based on filtering. After adding rules, pages that meet
requirements will be automatically added to the current the requirements will be automatically added to the current
collection. collection.
</Trans> </Trans>
</div> </div>
@@ -330,8 +315,9 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
</div> </div>
) : null} ) : null}
</div> </div>
</ViewBodyIsland>
{node} {node}
</div> </>
); );
}; };

View File

@@ -1,59 +1,16 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const header = style({ export const header = style({
display: 'flex', display: 'flex',
height: '52px', height: '100%',
width: '100%', width: '100%',
alignItems: 'center', alignItems: 'center',
flexShrink: 0,
background: cssVar('backgroundPrimaryColor'),
borderBottom: `1px solid ${cssVar('borderColor')}`,
selectors: {
'&[data-sidebar-floating="false"]': {
['WebkitAppRegion' as string]: 'drag',
},
'&:has([data-popper-placement])': {
['WebkitAppRegion' as string]: 'no-drag',
},
},
'@media': {
print: {
display: 'none',
},
},
});
export const mainHeader = style([
header,
{
padding: '0 16px',
gap: 12, gap: 12,
},
]);
export const sidebarHeader = style([
header,
{
padding: '0 16px',
gap: '12px',
},
]);
export const mainHeaderRight = style({
display: 'flex',
alignItems: 'center',
gap: '8px',
}); });
export const spacer = style({ export const spacer = style({
flexGrow: 1, flexGrow: 1,
minWidth: 12, minWidth: 12,
}); });
export const standaloneExtensionSwitcherWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: '52px',
position: 'relative',
});
export const journalWeekPicker = style({ export const journalWeekPicker = style({
minWidth: 100, minWidth: 100,
flexGrow: 1, flexGrow: 1,

View File

@@ -1,85 +1,20 @@
import type { InlineEditHandle } from '@affine/component'; import type { InlineEditHandle } from '@affine/component';
import { IconButton } from '@affine/component'; import { appSidebarFloatingAtom } from '@affine/component/app-sidebar';
import {
appSidebarFloatingAtom,
appSidebarOpenAtom,
SidebarSwitch,
} from '@affine/component/app-sidebar';
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite'; import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker'; import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button'; import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu'; import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { RightSidebarIcon } from '@blocksuite/icons';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra'; import type { Workspace } from '@toeverything/infra';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue } from 'jotai';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { SharePageButton } from '../../../components/affine/share-page-modal'; import { SharePageButton } from '../../../components/affine/share-page-modal';
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index'; import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
import { HeaderDivider } from '../../../components/pure/header'; import { HeaderDivider } from '../../../components/pure/header';
import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls';
import * as styles from './detail-page-header.css'; import * as styles from './detail-page-header.css';
import { ExtensionTabs } from './editor-sidebar';
import {
editorSidebarOpenAtom,
editorSidebarToggleAtom,
} from './editor-sidebar/atoms';
interface PageHeaderRightProps {
showSidebarSwitch?: boolean;
}
const ToggleSidebarButton = () => {
const toggle = useSetAtom(editorSidebarToggleAtom);
return (
<IconButton size="large" onClick={toggle}>
<RightSidebarIcon />
</IconButton>
);
};
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const WindowsMainPageHeaderRight = ({
showSidebarSwitch,
}: PageHeaderRightProps) => {
const editorSidebarOpen = useAtomValue(editorSidebarOpenAtom);
if (editorSidebarOpen) {
return null;
}
return (
<>
<HeaderDivider />
<div className={styles.mainHeaderRight} style={{ marginRight: -16 }}>
{showSidebarSwitch ? <ToggleSidebarButton /> : null}
<WindowsAppControls />
</div>
</>
);
};
const NonWindowsMainPageHeaderRight = ({
showSidebarSwitch,
}: PageHeaderRightProps) => {
const editorSidebarOpen = useAtomValue(editorSidebarOpenAtom);
if (editorSidebarOpen || !showSidebarSwitch) {
return null;
}
return (
<>
<HeaderDivider />
<div className={styles.mainHeaderRight}>
<ToggleSidebarButton />
</div>
</>
);
};
function Header({ function Header({
children, children,
@@ -106,21 +41,10 @@ function Header({
interface PageHeaderProps { interface PageHeaderProps {
page: Doc; page: Doc;
workspace: Workspace; workspace: Workspace;
showSidebarSwitch?: boolean;
} }
const RightHeader = isWindowsDesktop export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
? WindowsMainPageHeaderRight
: NonWindowsMainPageHeaderRight;
export function JournalPageHeader({
page,
workspace,
showSidebarSwitch = true,
}: PageHeaderProps) {
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
return ( return (
<Header className={styles.mainHeader}> <Header className={styles.header}>
<SidebarSwitch show={!leftSidebarOpen} />
{!leftSidebarOpen ? <HeaderDivider /> : null}
<EditorModeSwitch <EditorModeSwitch
blockSuiteWorkspace={workspace.blockSuiteWorkspace} blockSuiteWorkspace={workspace.blockSuiteWorkspace}
pageId={page?.id} pageId={page?.id}
@@ -137,26 +61,18 @@ export function JournalPageHeader({
{page ? ( {page ? (
<SharePageButton isJournal workspace={workspace} page={page} /> <SharePageButton isJournal workspace={workspace} page={page} />
) : null} ) : null}
<RightHeader showSidebarSwitch={showSidebarSwitch} />
</Header> </Header>
); );
} }
export function NormalPageHeader({ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
page,
workspace,
showSidebarSwitch = true,
}: PageHeaderProps) {
const titleInputHandleRef = useRef<InlineEditHandle>(null); const titleInputHandleRef = useRef<InlineEditHandle>(null);
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
const onRename = useCallback(() => { const onRename = useCallback(() => {
setTimeout(() => titleInputHandleRef.current?.triggerEdit()); setTimeout(() => titleInputHandleRef.current?.triggerEdit());
}, []); }, []);
return ( return (
<Header className={styles.mainHeader}> <Header className={styles.header}>
<SidebarSwitch show={!leftSidebarOpen} />
{!leftSidebarOpen ? <HeaderDivider /> : null}
<EditorModeSwitch <EditorModeSwitch
blockSuiteWorkspace={workspace.blockSuiteWorkspace} blockSuiteWorkspace={workspace.blockSuiteWorkspace}
pageId={page?.id} pageId={page?.id}
@@ -170,7 +86,6 @@ export function NormalPageHeader({
<FavoriteButton pageId={page?.id} /> <FavoriteButton pageId={page?.id} />
<div className={styles.spacer} /> <div className={styles.spacer} />
{page ? <SharePageButton workspace={workspace} page={page} /> : null} {page ? <SharePageButton workspace={workspace} page={page} /> : null}
<RightHeader showSidebarSwitch={showSidebarSwitch} />
</Header> </Header>
); );
} }
@@ -186,36 +101,3 @@ export function DetailPageHeader(props: PageHeaderProps) {
<NormalPageHeader {...props} /> <NormalPageHeader {...props} />
); );
} }
interface SidebarHeaderProps {
workspace: Workspace;
page: Doc;
}
function WindowsSidebarHeader(props: SidebarHeaderProps) {
return (
<>
<Header className={styles.sidebarHeader} style={{ paddingRight: 0 }}>
<div className={styles.spacer} />
<ToggleSidebarButton />
<WindowsAppControls />
</Header>
<div className={styles.standaloneExtensionSwitcherWrapper}>
<ExtensionTabs {...props} />
</div>
</>
);
}
function NonWindowsSidebarHeader(props: SidebarHeaderProps) {
return (
<Header className={styles.sidebarHeader}>
<ExtensionTabs {...props} />
<div className={styles.spacer} />
<ToggleSidebarButton />
</Header>
);
}
export const RightSidebarHeader = isWindowsDesktop
? WindowsSidebarHeader
: NonWindowsSidebarHeader;

View File

@@ -1,26 +1,14 @@
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({
display: 'flex',
height: '100%',
overflow: 'hidden',
width: '100%',
});
export const mainContainer = style({ export const mainContainer = style({
display: 'flex', display: 'flex',
flex: 1,
height: '100%',
position: 'relative',
flexDirection: 'column', flexDirection: 'column',
minWidth: 0, flex: 1,
overflow: 'hidden', overflow: 'hidden',
background: cssVar('backgroundPrimaryColor'), borderTop: `1px solid ${cssVar('borderColor')}`,
selectors: {
[`${root}[data-client-border=true] &`]: {
borderRadius: '4px',
},
},
}); });
export const editorContainer = style({ export const editorContainer = style({
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
@@ -28,29 +16,3 @@ export const editorContainer = style({
flex: 1, flex: 1,
zIndex: 0, zIndex: 0,
}); });
export const sidebarContainer = style({
display: 'flex',
flexShrink: 0,
height: '100%',
selectors: {
[`${root}[data-client-border=true] &`]: {
paddingLeft: 9,
},
[`${root}[data-client-border=false] &`]: {
borderLeft: `1px solid ${cssVar('borderColor')}`,
},
},
});
export const sidebarContainerInner = style({
display: 'flex',
background: cssVar('backgroundPrimaryColor'),
flexDirection: 'column',
overflow: 'hidden',
height: '100%',
width: '100%',
selectors: {
[`${root}[data-client-border=true] &`]: {
borderRadius: '4px',
},
},
});

View File

@@ -1,6 +1,5 @@
import { Scrollable } from '@affine/component'; import { Scrollable } from '@affine/component';
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { ResizePanel } from '@affine/component/resize-panel';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { import {
BookmarkService, BookmarkService,
@@ -20,13 +19,12 @@ import {
ServiceProviderContext, ServiceProviderContext,
useLiveData, useLiveData,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { appSettingAtom, Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { import {
memo, memo,
type ReactElement, type ReactElement,
type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -37,84 +35,55 @@ import type { Map as YMap } from 'yjs';
import { recentPageIdsBaseAtom } from '../../../atoms'; import { recentPageIdsBaseAtom } from '../../../atoms';
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary'; import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
import { HubIsland } from '../../../components/affine/hub-island';
import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal'; import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal';
import { ImagePreviewModal } from '../../../components/image-preview'; import { ImagePreviewModal } from '../../../components/image-preview';
import { PageDetailEditor } from '../../../components/page-detail-editor'; import { PageDetailEditor } from '../../../components/page-detail-editor';
import { TrashPageFooter } from '../../../components/pure/trash-page-footer'; import { TrashPageFooter } from '../../../components/pure/trash-page-footer';
import { TopTip } from '../../../components/top-tip'; import { TopTip } from '../../../components/top-tip';
import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands'; import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands';
import { useActiveBlocksuiteEditor } from '../../../hooks/use-block-suite-editor';
import { usePageDocumentTitle } from '../../../hooks/use-global-state'; import { usePageDocumentTitle } from '../../../hooks/use-global-state';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import {
MultiTabSidebarBody,
MultiTabSidebarHeaderSwitcher,
type SidebarTabName,
} from '../../../modules/multi-tab-sidebar';
import { sidebarTabs } from '../../../modules/multi-tab-sidebar';
import { RightSidebarViewIsland } from '../../../modules/right-sidebar';
import {
useIsActiveView,
ViewBodyIsland,
ViewHeaderIsland,
} from '../../../modules/workbench';
import { performanceRenderLogger } from '../../../shared'; 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, RightSidebarHeader } from './detail-page-header'; import { DetailPageHeader } from './detail-page-header';
import {
EditorSidebar,
editorSidebarOpenAtom,
editorSidebarResizingAtom,
editorSidebarWidthAtom,
} from './editor-sidebar';
interface DetailPageLayoutProps {
main: ReactNode;
header: ReactNode;
footer: ReactNode;
sidebar: ReactNode;
}
const MIN_SIDEBAR_WIDTH = 320;
const MAX_SIDEBAR_WIDTH = 800;
// todo: consider move to a shared place if we also want to reuse the layout for other routes
const DetailPageLayout = ({
main,
header,
footer,
sidebar,
}: DetailPageLayoutProps): ReactElement => {
const [width, setWidth] = useAtom(editorSidebarWidthAtom);
const { clientBorder } = useAtomValue(appSettingAtom);
const [resizing, setResizing] = useAtom(editorSidebarResizingAtom);
const [open, setOpen] = useAtom(editorSidebarOpenAtom);
return (
<div className={styles.root} data-client-border={clientBorder && open}>
<div className={styles.mainContainer}>
{header}
{main}
{footer}
</div>
{sidebar ? (
<ResizePanel
resizeHandlePos="left"
resizeHandleOffset={clientBorder ? 4 : 0}
width={width}
className={styles.sidebarContainer}
onResizing={setResizing}
resizing={resizing}
open={open}
onOpen={setOpen}
onWidthChange={setWidth}
minWidth={MIN_SIDEBAR_WIDTH}
maxWidth={MAX_SIDEBAR_WIDTH}
>
{sidebar}
</ResizePanel>
) : null}
</div>
);
};
const DetailPageImpl = memo(function DetailPageImpl() { const DetailPageImpl = memo(function DetailPageImpl() {
const page = useService(Doc); const page = useService(Doc);
const pageRecordList = useService(PageRecordList); const pageRecordList = useService(PageRecordList);
const currentPageId = page.id; const currentPageId = page.id;
const { openPage, jumpToTag } = useNavigateHelper(); const { openPage, jumpToTag } = useNavigateHelper();
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
const currentWorkspace = useService(Workspace); const currentWorkspace = useService(Workspace);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const isActiveView = useIsActiveView();
// TODO: remove jotai here
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
useEffect(() => {
if (isActiveView) {
setActiveBlockSuiteEditor(editor);
}
}, [editor, isActiveView, setActiveBlockSuiteEditor]);
const [activeTabName, setActiveTabName] = useState<SidebarTabName | null>(
null
);
const pageMeta = useBlockSuiteDocMeta(blockSuiteWorkspace).find( const pageMeta = useBlockSuiteDocMeta(blockSuiteWorkspace).find(
meta => meta.id === page.id meta => meta.id === page.id
); );
@@ -184,6 +153,9 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const disposeTagClick = editor.slots.tagClicked.on(({ tagId }) => { const disposeTagClick = editor.slots.tagClicked.on(({ tagId }) => {
jumpToTag(currentWorkspace.id, tagId); jumpToTag(currentWorkspace.id, tagId);
}); });
setEditor(editor);
return () => { return () => {
dispose.dispose(); dispose.dispose();
disposeTagClick.dispose(); disposeTagClick.dispose();
@@ -200,24 +172,23 @@ const DetailPageImpl = memo(function DetailPageImpl() {
] ]
); );
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
return ( return (
<> <>
<DetailPageLayout <ViewHeaderIsland>
header={
<>
<DetailPageHeader <DetailPageHeader
page={page.blockSuiteDoc} page={page.blockSuiteDoc}
workspace={currentWorkspace} workspace={currentWorkspace}
showSidebarSwitch={!isInTrash}
/> />
<TopTip pageId={currentPageId} workspace={currentWorkspace} /> </ViewHeaderIsland>
</> <ViewBodyIsland>
} <div className={styles.mainContainer}>
main={ {/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
// Add a key to force rerender when page changed, to avoid error boundary persisting.
<AffineErrorBoundary key={currentPageId}> <AffineErrorBoundary key={currentPageId}>
<Scrollable.Root> <Scrollable.Root>
<Scrollable.Viewport className={styles.editorContainer}> <Scrollable.Viewport className={styles.editorContainer}>
<TopTip pageId={currentPageId} workspace={currentWorkspace} />
<PageDetailEditor <PageDetailEditor
pageId={currentPageId} pageId={currentPageId}
onLoad={onLoad} onLoad={onLoad}
@@ -226,25 +197,42 @@ const DetailPageImpl = memo(function DetailPageImpl() {
</Scrollable.Viewport> </Scrollable.Viewport>
<Scrollable.Scrollbar /> <Scrollable.Scrollbar />
</Scrollable.Root> </Scrollable.Root>
<HubIsland />
</AffineErrorBoundary> </AffineErrorBoundary>
} {isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
footer={isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
sidebar={
!isInTrash ? (
<div className={styles.sidebarContainerInner}>
<RightSidebarHeader
workspace={currentWorkspace}
page={page.blockSuiteDoc}
/>
<EditorSidebar
workspace={blockSuiteWorkspace}
page={page.blockSuiteDoc}
/>
</div> </div>
</ViewBodyIsland>
<RightSidebarViewIsland
active={isActiveView}
header={
!isWindowsDesktop ? (
<MultiTabSidebarHeaderSwitcher
activeTabName={activeTabName ?? sidebarTabs[0]?.name}
setActiveTabName={setActiveTabName}
tabs={sidebarTabs}
/>
) : null ) : null
} }
body={
<MultiTabSidebarBody
editor={editor}
tab={
sidebarTabs.find(ext => ext.name === activeTabName) ??
sidebarTabs[0]
}
>
{/* Show switcher in body for windows desktop */}
{isWindowsDesktop && (
<MultiTabSidebarHeaderSwitcher
activeTabName={activeTabName ?? sidebarTabs[0]?.name}
setActiveTabName={setActiveTabName}
tabs={sidebarTabs}
/> />
)}
</MultiTabSidebarBody>
}
/>
<ImagePreviewModal <ImagePreviewModal
pageId={currentPageId} pageId={currentPageId}
workspace={blockSuiteWorkspace} workspace={blockSuiteWorkspace}

View File

@@ -1,99 +0,0 @@
// main editor sidebar states
import { assertExists, isEqual } from '@blocksuite/global/utils';
import { atom } from 'jotai';
import { selectAtom } from 'jotai/utils';
import { copilotExtension } from './extensions/copilot';
import { framePanelExtension } from './extensions/frame';
import { journalExtension } from './extensions/journal';
import { outlineExtension } from './extensions/outline';
import type { EditorExtension, EditorExtensionName } from './types';
// the list of all possible extensions in affine.
// order matters (determines the order of the tabs)
export const extensions: EditorExtension[] = [
journalExtension,
outlineExtension,
framePanelExtension,
copilotExtension,
];
export interface EditorSidebarState {
isOpen: boolean;
width: number;
resizing: boolean;
activeExtension?: EditorExtension;
extensions: EditorExtension[];
}
const baseStateAtom = atom<EditorSidebarState>({
isOpen: false,
resizing: false,
width: 300, // todo: should be resizable
activeExtension: extensions[0],
extensions: extensions, // todo: maybe should be dynamic (by feature flag?)
});
const isOpenAtom = selectAtom(baseStateAtom, state => state.isOpen);
const resizingAtom = selectAtom(baseStateAtom, state => state.resizing);
const activeExtensionAtom = selectAtom(
baseStateAtom,
state => state.activeExtension
);
const widthAtom = selectAtom(baseStateAtom, state => state.width);
export const editorExtensionsAtom = selectAtom(
baseStateAtom,
state => state.extensions,
isEqual
);
// get/set sidebar open state
export const editorSidebarOpenAtom = atom(
get => get(isOpenAtom),
(_, set, isOpen: boolean) => {
set(baseStateAtom, prev => {
return { ...prev, isOpen };
});
}
);
// get/set sidebar resizing state
export const editorSidebarResizingAtom = atom(
get => get(resizingAtom),
(_, set, resizing: boolean) => {
set(baseStateAtom, prev => {
return { ...prev, resizing };
});
}
);
// get/set active extension
export const editorSidebarActiveExtensionAtom = atom(
get => get(activeExtensionAtom),
(_, set, extension: EditorExtensionName) => {
set(baseStateAtom, prev => {
const extensions = prev.extensions;
const newExtension = extensions.find(e => e.name === extension);
assertExists(newExtension, `extension ${extension} not found`);
return { ...prev, activeExtension: newExtension };
});
}
);
// toggle sidebar (write only)
export const editorSidebarToggleAtom = atom(null, (_, set) => {
set(baseStateAtom, prev => {
return { ...prev, isOpen: !prev.isOpen };
});
});
// get/set sidebar width
export const editorSidebarWidthAtom = atom(
get => get(widthAtom),
(_, set, width: number) => {
set(baseStateAtom, prev => {
return { ...prev, width };
});
}
);

View File

@@ -1,16 +0,0 @@
import { useAtomValue } from 'jotai';
import type { EditorExtensionProps } from '.';
import { editorSidebarActiveExtensionAtom } from './atoms';
import * as styles from './editor-sidebar.css';
export const EditorSidebar = (props: EditorExtensionProps) => {
const activeExtension = useAtomValue(editorSidebarActiveExtensionAtom);
const Component = activeExtension?.Component;
return (
<div className={styles.root}>
{Component ? <Component {...props} /> : null}
</div>
);
};

View File

@@ -1,69 +0,0 @@
import { IconButton } from '@affine/component';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { useWorkspaceEnabledFeatures } from '@affine/core/hooks/use-workspace-features';
import { FeatureType } from '@affine/graphql';
import type { Doc } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useAtom, useAtomValue } from 'jotai';
import { useEffect } from 'react';
import {
editorExtensionsAtom,
editorSidebarActiveExtensionAtom,
} from '../atoms';
import * as styles from './extensions.css';
export interface ExtensionTabsProps {
workspace: Workspace;
page: Doc;
}
// provide a switcher for active extensions
// will be used in global top header (MacOS) or sidebar (Windows)
export const ExtensionTabs = ({ page, workspace }: ExtensionTabsProps) => {
// todo: filter in editorExtensionsAtom instead?
const copilotEnabled = useWorkspaceEnabledFeatures(workspace.meta).includes(
FeatureType.Copilot
);
const { isJournal } = useJournalInfoHelper(page.workspace, page.id);
const exts = useAtomValue(editorExtensionsAtom).filter(ext => {
if (ext.name === 'copilot' && !copilotEnabled) return false;
return true;
});
const [selected, setSelected] = useAtom(editorSidebarActiveExtensionAtom);
// if journal is active, set selected to journal
useEffect(() => {
isJournal && setSelected('journal');
}, [isJournal, setSelected]);
const vars = assignInlineVars({
[styles.activeIdx]: String(
exts.findIndex(ext => ext.name === selected?.name) ?? 0
),
});
useEffect(() => {
if (!selected || !exts.some(e => selected.name === e.name)) {
setSelected(exts[0].name);
}
}, [exts, selected, setSelected]);
return (
<div className={styles.switchRoot} style={vars}>
{exts.map(extension => {
return (
<IconButton
onClick={() => setSelected(extension.name)}
key={extension.name}
data-active={selected?.name === extension.name}
className={styles.button}
>
{extension.icon}
</IconButton>
);
})}
</div>
);
};

View File

@@ -1,4 +0,0 @@
export * from './atoms';
export * from './editor-sidebar';
export * from './extensions';
export * from './types';

View File

@@ -1,15 +0,0 @@
import type { BlockSuiteWorkspace } from '@affine/core/shared';
import type { Doc } from '@blocksuite/store';
export type EditorExtensionName = 'outline' | 'frame' | 'copilot' | 'journal';
export interface EditorExtensionProps {
workspace: BlockSuiteWorkspace;
page: Doc;
}
export interface EditorExtension {
name: EditorExtensionName;
icon: React.ReactNode;
Component: React.ComponentType<EditorExtensionProps>;
}

View File

@@ -21,8 +21,10 @@ import {
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary'; import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
import { HubIsland } from '../../components/affine/hub-island';
import { WorkspaceLayout } from '../../layouts/workspace-layout'; import { WorkspaceLayout } from '../../layouts/workspace-layout';
import { WorkbenchRoot } from '../../modules/workbench/workbench-root'; import { RightSidebarContainer } from '../../modules/right-sidebar';
import { WorkbenchRoot } from '../../modules/workbench';
import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace'; import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace';
import { performanceRenderLogger } from '../../shared'; import { performanceRenderLogger } from '../../shared';
import { PageNotFound } from '../404'; import { PageNotFound } from '../404';
@@ -115,6 +117,8 @@ export const Component = (): ReactElement => {
<AffineErrorBoundary height="100vh"> <AffineErrorBoundary height="100vh">
<WorkspaceLayout> <WorkspaceLayout>
<WorkbenchRoot /> <WorkbenchRoot />
<RightSidebarContainer />
<HubIsland />
</WorkspaceLayout> </WorkspaceLayout>
</AffineErrorBoundary> </AffineErrorBoundary>
</Suspense> </Suspense>

View File

@@ -76,6 +76,7 @@ export const EmptyCollectionList = ({ heading }: { heading: ReactNode }) => {
</div> </div>
); );
}; };
export const EmptyTagList = ({ heading }: { heading: ReactNode }) => { export const EmptyTagList = ({ heading }: { heading: ReactNode }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (

View File

@@ -1,23 +1,6 @@
import { Header } from '@affine/core/components/pure/header'; import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import * as styles from '../all-page/all-page.css';
export const TagDetailHeader = () => { export const TagDetailHeader = () => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows; return <Header center={<WorkspaceModeFilterTab activeFilter={'tags'} />} />;
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
/>
);
}; };

View File

@@ -1,4 +1,3 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { import {
PageListHeader, PageListHeader,
useTagMetas, useTagMetas,
@@ -11,7 +10,6 @@ import { useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import * as styles from '../all-page/all-page.css';
import { EmptyPageList } from '../page-list-empty'; import { EmptyPageList } from '../page-list-empty';
import { TagDetailHeader } from './header'; import { TagDetailHeader } from './header';
@@ -40,7 +38,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
} }
return ( return (
<div className={styles.root}> <>
<TagDetailHeader /> <TagDetailHeader />
{tagPageMetas.length > 0 ? ( {tagPageMetas.length > 0 ? (
<VirtualizedPageList tag={currentTag} listItem={tagPageMetas} /> <VirtualizedPageList tag={currentTag} listItem={tagPageMetas} />
@@ -51,8 +49,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/> />
)} )}
<HubIsland /> </>
</div>
); );
}; };

View File

@@ -1,6 +1,5 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export { root } from './all-page/all-page.css';
export const trashTitle = style({ export const trashTitle = style({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -8,6 +7,13 @@ export const trashTitle = style({
padding: '0 8px', padding: '0 8px',
fontWeight: 600, fontWeight: 600,
}); });
export const body = style({
display: 'flex',
flexDirection: 'column',
flex: 1,
height: '100%',
width: '100%',
});
export const trashIcon = style({ export const trashIcon = style({
color: cssVar('iconColor'), color: cssVar('iconColor'),
fontSize: cssVar('fontH5'), fontSize: cssVar('fontH5'),

View File

@@ -21,6 +21,7 @@ import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { ViewBodyIsland, ViewHeaderIsland } from '../../modules/workbench';
import { EmptyPageList } from './page-list-empty'; import { EmptyPageList } from './page-list-empty';
import * as styles from './trash-page.css'; import * as styles from './trash-page.css';
@@ -94,8 +95,12 @@ export const TrashPage = () => {
return <ListTableHeader headerCols={pageHeaderColsDef} />; return <ListTableHeader headerCols={pageHeaderColsDef} />;
}, []); }, []);
return ( return (
<div className={styles.root}> <>
<ViewHeaderIsland>
<TrashHeader /> <TrashHeader />
</ViewHeaderIsland>
<ViewBodyIsland>
<div className={styles.body}>
{filteredPageMetas.length > 0 ? ( {filteredPageMetas.length > 0 ? (
<VirtualizedList <VirtualizedList
items={filteredPageMetas} items={filteredPageMetas}
@@ -114,6 +119,8 @@ export const TrashPage = () => {
/> />
)} )}
</div> </div>
</ViewBodyIsland>
</>
); );
}; };

View File

@@ -0,0 +1,41 @@
import { LiveData, useLiveData } from '@toeverything/infra/livedata';
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
export const createIsland = () => {
const targetLiveData = new LiveData<HTMLDivElement | null>(null);
let mounted = false;
let provided = false;
return {
Target: ({ ...other }: React.HTMLProps<HTMLDivElement>) => {
const target = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (mounted === true) {
throw new Error('Island should not be mounted more than once');
}
mounted = true;
targetLiveData.next(target.current);
return () => {
mounted = false;
targetLiveData.next(null);
};
}, []);
return <div {...other} ref={target}></div>;
},
Provider: ({ children }: React.PropsWithChildren) => {
const target = useLiveData(targetLiveData);
useEffect(() => {
if (provided === true && process.env.NODE_ENV !== 'production') {
throw new Error('Island should not be provided more than once');
}
provided = true;
return () => {
provided = false;
};
}, []);
return target ? createPortal(children, target) : null;
},
};
};
export type Island = ReturnType<typeof createIsland>;

View File

@@ -1,4 +0,0 @@
{
"id": "709a97a0-aa01-4d0b-b29a-f3ed2f63033c",
"mainDBPath": "/Users/eyhn/AFFiNE/packages/frontend/electron/test/db/tmp/app-data/workspaces/709a97a0-aa01-4d0b-b29a-f3ed2f63033c/storage.db"
}