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. 
This commit is contained in:
@@ -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'}`}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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"]': {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -266,7 +266,6 @@ affine-block-hub {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea
|
textarea
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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} />
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
@@ -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,
|
||||||
@@ -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,
|
||||||
@@ -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,
|
||||||
@@ -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';
|
||||||
@@ -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')}`,
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export class PageListView {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createIsland } from '../../../utils/island';
|
||||||
|
|
||||||
|
export class RightSidebarView {
|
||||||
|
readonly body = createIsland();
|
||||||
|
readonly header = createIsland();
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { RightSidebar } from './entities/right-sidebar';
|
||||||
|
export { RightSidebarContainer } from './view/container';
|
||||||
|
export { RightSidebarViewIsland } from './view/view-island';
|
||||||
@@ -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')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './view';
|
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
@@ -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%',
|
||||||
});
|
});
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
@@ -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'} />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
|
||||||
});
|
});
|
||||||
@@ -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'} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
});
|
||||||
|
|||||||
@@ -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'} />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './extensions';
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './atoms';
|
|
||||||
export * from './editor-sidebar';
|
|
||||||
export * from './extensions';
|
|
||||||
export * from './types';
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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'} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
41
packages/frontend/core/src/utils/island.tsx
Normal file
41
packages/frontend/core/src/utils/island.tsx
Normal 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>;
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user