mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08:00
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:
@@ -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 { 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 { PlusIcon } from '@blocksuite/icons';
|
||||
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 = ({
|
||||
showCreateNew,
|
||||
@@ -15,33 +13,18 @@ export const AllCollectionHeader = ({
|
||||
showCreateNew: boolean;
|
||||
onCreateCollection?: () => void;
|
||||
}) => {
|
||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||
|
||||
const renderRightItem = useMemo(() => {
|
||||
return (
|
||||
<IconButton
|
||||
type="default"
|
||||
icon={<PlusIcon fontSize={16} />}
|
||||
onClick={onCreateCollection}
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
styles.headerCreateNewCollectionIconButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}, [onCreateCollection, showCreateNew]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
right={
|
||||
<div
|
||||
className={styles.headerRightWindows}
|
||||
data-is-windows-desktop={isWindowsDesktop}
|
||||
>
|
||||
{renderRightItem}
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
<IconButton
|
||||
type="default"
|
||||
icon={<PlusIcon fontSize={16} />}
|
||||
onClick={onCreateCollection}
|
||||
className={clsx(
|
||||
styles.headerCreateNewCollectionIconButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
/>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { HubIsland } from '@affine/core/components/affine/hub-island';
|
||||
import {
|
||||
CollectionListHeader,
|
||||
type CollectionMeta,
|
||||
@@ -16,7 +15,7 @@ import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
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 { AllCollectionHeader } from './header';
|
||||
|
||||
@@ -58,33 +57,35 @@ export const AllCollection = () => {
|
||||
}, [collectionService, currentWorkspace, navigateHelper, open]);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<AllCollectionHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
onCreateCollection={handleCreateCollection}
|
||||
/>
|
||||
{collectionMetas.length > 0 ? (
|
||||
<VirtualizedCollectionList
|
||||
collections={collections}
|
||||
collectionMetas={collectionMetas}
|
||||
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
|
||||
node={node}
|
||||
config={config}
|
||||
handleCreateCollection={handleCreateCollection}
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<AllCollectionHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
onCreateCollection={handleCreateCollection}
|
||||
/>
|
||||
) : (
|
||||
<EmptyCollectionList
|
||||
heading={
|
||||
<CollectionListHeader
|
||||
node={node}
|
||||
onCreate={handleCreateCollection}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HubIsland />
|
||||
</div>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
{collectionMetas.length > 0 ? (
|
||||
<VirtualizedCollectionList
|
||||
collections={collections}
|
||||
collectionMetas={collectionMetas}
|
||||
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
|
||||
node={node}
|
||||
config={config}
|
||||
handleCreateCollection={handleCreateCollection}
|
||||
/>
|
||||
) : (
|
||||
<EmptyCollectionList
|
||||
heading={
|
||||
<CollectionListHeader
|
||||
node={node}
|
||||
onCreate={handleCreateCollection}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ViewBodyIsland>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,17 +3,14 @@ import {
|
||||
PageListNewPageButton,
|
||||
} from '@affine/core/components/page-list';
|
||||
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 type { Filter } from '@affine/env/filter';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { Workspace } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './all-page.css';
|
||||
import { FilterContainer } from './all-page-filter';
|
||||
|
||||
export const AllPageHeader = ({
|
||||
showCreateNew,
|
||||
@@ -25,44 +22,28 @@ export const AllPageHeader = ({
|
||||
onChangeFilters: (filters: Filter[]) => void;
|
||||
}) => {
|
||||
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 (
|
||||
<>
|
||||
<Header
|
||||
left={
|
||||
<AllPageListOperationsMenu
|
||||
filterList={filters}
|
||||
onChangeFilterList={onChangeFilters}
|
||||
propertiesMeta={workspace.blockSuiteWorkspace.meta.properties}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<div
|
||||
className={styles.headerRightWindows}
|
||||
data-is-windows-desktop={isWindowsDesktop}
|
||||
>
|
||||
{renderRightItem}
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
|
||||
/>
|
||||
<FilterContainer filters={filters} onChangeFilters={onChangeFilters} />
|
||||
</>
|
||||
<Header
|
||||
left={
|
||||
<AllPageListOperationsMenu
|
||||
filterList={filters}
|
||||
onChangeFilterList={onChangeFilters}
|
||||
propertiesMeta={workspace.blockSuiteWorkspace.meta.properties}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<PageListNewPageButton
|
||||
size="small"
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
>
|
||||
<PlusIcon />
|
||||
</PageListNewPageButton>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
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({
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
@@ -26,13 +18,11 @@ export const headerCreateNewButtonHidden = style({
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
export const headerRightWindows = style({
|
||||
|
||||
export const body = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
selectors: {
|
||||
'&[data-is-windows-desktop="true"]': {
|
||||
transform: 'translateX(16px)',
|
||||
},
|
||||
},
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { HubIsland } from '@affine/core/components/affine/hub-island';
|
||||
import {
|
||||
PageListHeader,
|
||||
useFilteredPageMetas,
|
||||
@@ -12,8 +11,10 @@ import { useService } from '@toeverything/infra';
|
||||
import { Workspace } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||
import { EmptyPageList } from '../page-list-empty';
|
||||
import * as styles from './all-page.css';
|
||||
import { FilterContainer } from './all-page-filter';
|
||||
import { AllPageHeader } from './all-page-header';
|
||||
|
||||
export const AllPage = () => {
|
||||
@@ -27,26 +28,32 @@ export const AllPage = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<AllPageHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
filters={filters}
|
||||
onChangeFilters={setFilters}
|
||||
/>
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedPageList
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<AllPageHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
filters={filters}
|
||||
onChangeFilters={setFilters}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList
|
||||
type="all"
|
||||
heading={<PageListHeader />}
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
)}
|
||||
<HubIsland />
|
||||
</div>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
<div className={styles.body}>
|
||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedPageList
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
filters={filters}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList
|
||||
type="all"
|
||||
heading={<PageListHeader />}
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
|
||||
export const body = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
minWidth: '320px',
|
||||
});
|
||||
@@ -1,23 +1,6 @@
|
||||
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 * as styles from '../all-page/all-page.css';
|
||||
|
||||
export const AllTagHeader = () => {
|
||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||
|
||||
return (
|
||||
<Header
|
||||
right={
|
||||
<div
|
||||
className={styles.headerRightWindows}
|
||||
data-is-windows-desktop={isWindowsDesktop}
|
||||
>
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
|
||||
/>
|
||||
);
|
||||
return <Header 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 {
|
||||
TagListHeader,
|
||||
@@ -8,8 +7,9 @@ import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-me
|
||||
import { useService } 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 * as styles from './all-tag.css';
|
||||
import { AllTagHeader } from './header';
|
||||
|
||||
export const AllTag = () => {
|
||||
@@ -22,19 +22,24 @@ export const AllTag = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<AllTagHeader />
|
||||
{tags.length > 0 ? (
|
||||
<VirtualizedTagList
|
||||
tags={tags}
|
||||
tagMetas={tagMetas}
|
||||
onTagDelete={deleteTags}
|
||||
/>
|
||||
) : (
|
||||
<EmptyTagList heading={<TagListHeader />} />
|
||||
)}
|
||||
<HubIsland />
|
||||
</div>
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<AllTagHeader />
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
<div className={styles.body}>
|
||||
{tags.length > 0 ? (
|
||||
<VirtualizedTagList
|
||||
tags={tags}
|
||||
tagMetas={tagMetas}
|
||||
onTagDelete={deleteTags}
|
||||
/>
|
||||
) : (
|
||||
<EmptyTagList heading={<TagListHeader />} />
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -23,3 +23,17 @@ export const button = style({
|
||||
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 { 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 { PlusIcon } from '@blocksuite/icons';
|
||||
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 = ({
|
||||
showCreateNew,
|
||||
@@ -15,33 +13,19 @@ export const CollectionDetailHeader = ({
|
||||
showCreateNew: boolean;
|
||||
onCreate: () => void;
|
||||
}) => {
|
||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||
|
||||
const renderRightItem = useMemo(() => {
|
||||
return (
|
||||
<IconButton
|
||||
type="default"
|
||||
icon={<PlusIcon fontSize={16} />}
|
||||
onClick={onCreate}
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
styles.headerCreateNewCollectionIconButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}, [onCreate, showCreateNew]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
right={
|
||||
<div
|
||||
className={styles.headerRightWindows}
|
||||
data-is-windows-desktop={isWindowsDesktop}
|
||||
>
|
||||
{renderRightItem}
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
<IconButton
|
||||
type="default"
|
||||
icon={<PlusIcon fontSize={16} />}
|
||||
onClick={onCreate}
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
styles.headerCreateNewCollectionIconButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
/>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import {
|
||||
appSidebarOpenAtom,
|
||||
SidebarSwitch,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { HubIsland } from '@affine/core/components/affine/hub-island';
|
||||
import {
|
||||
AffineShapeIcon,
|
||||
useEditCollection,
|
||||
VirtualizedPageList,
|
||||
} 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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
@@ -25,14 +19,13 @@ import {
|
||||
import { Workspace } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useLiveData } from '@toeverything/infra/livedata';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||
import { WorkspaceSubPath } from '../../../shared';
|
||||
import * as allPageStyles from '../all-page/all-page.css';
|
||||
import * as styles from './collection.css';
|
||||
import { CollectionDetailHeader } from './header';
|
||||
|
||||
@@ -52,19 +45,22 @@ export const CollectionDetail = ({
|
||||
}, [collection, collectionService, open]);
|
||||
|
||||
return (
|
||||
<div className={allPageStyles.root}>
|
||||
<CollectionDetailHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
onCreate={handleEditCollection}
|
||||
/>
|
||||
<VirtualizedPageList
|
||||
collection={collection}
|
||||
config={config}
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
/>
|
||||
<HubIsland />
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<CollectionDetailHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
onCreate={handleEditCollection}
|
||||
/>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
<VirtualizedPageList
|
||||
collection={collection}
|
||||
config={config}
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
/>
|
||||
</ViewBodyIsland>
|
||||
{node}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -115,8 +111,6 @@ export const Component = function CollectionPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||
|
||||
const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
const workspace = useService(Workspace);
|
||||
const collectionService = useService(CollectionService);
|
||||
@@ -139,199 +133,191 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
localStorage.setItem('hide-empty-collection-help-info', 'true');
|
||||
}, []);
|
||||
const t = useAFFiNEI18N();
|
||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
|
||||
const handleJumpToCollections = useCallback(() => {
|
||||
jumpToCollections(workspace.id);
|
||||
}, [jumpToCollections, workspace]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
height: 52,
|
||||
paddingLeft: '16px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
['WebkitAppRegion' as string]: 'drag',
|
||||
}}
|
||||
>
|
||||
<SidebarSwitch show={!leftSidebarOpen} />
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
}}
|
||||
onClick={handleJumpToCollections}
|
||||
>
|
||||
<ViewLayersIcon
|
||||
style={{ color: 'var(--affine-icon-color)' }}
|
||||
fontSize={14}
|
||||
/>
|
||||
{t['com.affine.collection.allCollections']()}
|
||||
<div>/</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="collection-name"
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
gap: 8,
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
}}
|
||||
>
|
||||
{collection.name}
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
{isWindowsDesktop && <WindowsAppControls />}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 64,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 432,
|
||||
marginTop: 118,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 18,
|
||||
margin: '118px 12px 0',
|
||||
}}
|
||||
>
|
||||
<AffineShapeIcon />
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
}}
|
||||
>
|
||||
{t['com.affine.collection.emptyCollection']()}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{t['com.affine.collection.emptyCollectionDescription']()}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px 32px',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
}}
|
||||
onClick={handleJumpToCollections}
|
||||
>
|
||||
<ViewLayersIcon
|
||||
style={{ color: 'var(--affine-icon-color)' }}
|
||||
fontSize={14}
|
||||
/>
|
||||
{t['com.affine.collection.allCollections']()}
|
||||
<div>/</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="collection-name"
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
}}
|
||||
>
|
||||
<div onClick={openPageEdit} className={styles.placeholderButton}>
|
||||
<PageIcon
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: 'var(--affine-icon-color)',
|
||||
}}
|
||||
/>
|
||||
<span style={{ padding: '0 4px' }}>
|
||||
{t['com.affine.collection.addPages']()}
|
||||
</span>
|
||||
</div>
|
||||
<div onClick={openRuleEdit} className={styles.placeholderButton}>
|
||||
<FilterIcon
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: 'var(--affine-icon-color)',
|
||||
}}
|
||||
/>
|
||||
<span style={{ padding: '0 4px' }}>
|
||||
{t['com.affine.collection.addRules']()}
|
||||
</span>
|
||||
</div>
|
||||
{collection.name}
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
</div>
|
||||
{showTips ? (
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 64,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 452,
|
||||
borderRadius: 8,
|
||||
maxWidth: 432,
|
||||
marginTop: 118,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
padding: 10,
|
||||
gap: 14,
|
||||
margin: '0 12px',
|
||||
alignItems: 'center',
|
||||
gap: 18,
|
||||
margin: '118px 12px 0',
|
||||
}}
|
||||
>
|
||||
<AffineShapeIcon />
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
}}
|
||||
>
|
||||
{t['com.affine.collection.emptyCollection']()}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div>{t['com.affine.collection.helpInfo']()}</div>
|
||||
<CloseIcon
|
||||
className={styles.button}
|
||||
style={{ width: 16, height: 16 }}
|
||||
onClick={hideTips}
|
||||
/>
|
||||
{t['com.affine.collection.emptyCollectionDescription']()}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
alignItems: 'center',
|
||||
gap: '12px 32px',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Trans i18nKey="com.affine.collection.addPages.tips">
|
||||
<span style={{ fontWeight: 600 }}>Add pages:</span> You can
|
||||
freely select pages and add them to the collection.
|
||||
</Trans>
|
||||
<div onClick={openPageEdit} className={styles.placeholderButton}>
|
||||
<PageIcon
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: 'var(--affine-icon-color)',
|
||||
}}
|
||||
/>
|
||||
<span style={{ padding: '0 4px' }}>
|
||||
{t['com.affine.collection.addPages']()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<Trans i18nKey="com.affine.collection.addRules.tips">
|
||||
<span style={{ fontWeight: 600 }}>Add rules:</span> Rules are
|
||||
based on filtering. After adding rules, pages that meet the
|
||||
requirements will be automatically added to the current
|
||||
collection.
|
||||
</Trans>
|
||||
<div onClick={openRuleEdit} className={styles.placeholderButton}>
|
||||
<FilterIcon
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: 'var(--affine-icon-color)',
|
||||
}}
|
||||
/>
|
||||
<span style={{ padding: '0 4px' }}>
|
||||
{t['com.affine.collection.addRules']()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{showTips ? (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 452,
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
padding: 10,
|
||||
gap: 14,
|
||||
margin: '0 12px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div>{t['com.affine.collection.helpInfo']()}</div>
|
||||
<CloseIcon
|
||||
className={styles.button}
|
||||
style={{ width: 16, height: 16 }}
|
||||
onClick={hideTips}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Trans i18nKey="com.affine.collection.addPages.tips">
|
||||
<span style={{ fontWeight: 600 }}>Add pages:</span> You can
|
||||
freely select pages and add them to the collection.
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<Trans i18nKey="com.affine.collection.addRules.tips">
|
||||
<span style={{ fontWeight: 600 }}>Add rules:</span> Rules
|
||||
are based on filtering. After adding rules, pages that meet
|
||||
the requirements will be automatically added to the current
|
||||
collection.
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
{node}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,59 +1,16 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const header = style({
|
||||
display: 'flex',
|
||||
height: '52px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
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,
|
||||
},
|
||||
]);
|
||||
export const sidebarHeader = style([
|
||||
header,
|
||||
{
|
||||
padding: '0 16px',
|
||||
gap: '12px',
|
||||
},
|
||||
]);
|
||||
export const mainHeaderRight = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
gap: 12,
|
||||
});
|
||||
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 journalWeekPicker = style({
|
||||
minWidth: 100,
|
||||
flexGrow: 1,
|
||||
|
||||
@@ -1,85 +1,20 @@
|
||||
import type { InlineEditHandle } from '@affine/component';
|
||||
import { IconButton } from '@affine/component';
|
||||
import {
|
||||
appSidebarFloatingAtom,
|
||||
appSidebarOpenAtom,
|
||||
SidebarSwitch,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { appSidebarFloatingAtom } from '@affine/component/app-sidebar';
|
||||
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 { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
||||
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||
import { RightSidebarIcon } from '@blocksuite/icons';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
||||
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
|
||||
import { HeaderDivider } from '../../../components/pure/header';
|
||||
import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls';
|
||||
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({
|
||||
children,
|
||||
@@ -106,21 +41,10 @@ function Header({
|
||||
interface PageHeaderProps {
|
||||
page: Doc;
|
||||
workspace: Workspace;
|
||||
showSidebarSwitch?: boolean;
|
||||
}
|
||||
const RightHeader = isWindowsDesktop
|
||||
? WindowsMainPageHeaderRight
|
||||
: NonWindowsMainPageHeaderRight;
|
||||
export function JournalPageHeader({
|
||||
page,
|
||||
workspace,
|
||||
showSidebarSwitch = true,
|
||||
}: PageHeaderProps) {
|
||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
return (
|
||||
<Header className={styles.mainHeader}>
|
||||
<SidebarSwitch show={!leftSidebarOpen} />
|
||||
{!leftSidebarOpen ? <HeaderDivider /> : null}
|
||||
<Header className={styles.header}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={page?.id}
|
||||
@@ -137,26 +61,18 @@ export function JournalPageHeader({
|
||||
{page ? (
|
||||
<SharePageButton isJournal workspace={workspace} page={page} />
|
||||
) : null}
|
||||
<RightHeader showSidebarSwitch={showSidebarSwitch} />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
export function NormalPageHeader({
|
||||
page,
|
||||
workspace,
|
||||
showSidebarSwitch = true,
|
||||
}: PageHeaderProps) {
|
||||
export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
|
||||
const onRename = useCallback(() => {
|
||||
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
||||
}, []);
|
||||
return (
|
||||
<Header className={styles.mainHeader}>
|
||||
<SidebarSwitch show={!leftSidebarOpen} />
|
||||
{!leftSidebarOpen ? <HeaderDivider /> : null}
|
||||
<Header className={styles.header}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={page?.id}
|
||||
@@ -170,7 +86,6 @@ export function NormalPageHeader({
|
||||
<FavoriteButton pageId={page?.id} />
|
||||
<div className={styles.spacer} />
|
||||
{page ? <SharePageButton workspace={workspace} page={page} /> : null}
|
||||
<RightHeader showSidebarSwitch={showSidebarSwitch} />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
@@ -186,36 +101,3 @@ export function DetailPageHeader(props: PageHeaderProps) {
|
||||
<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 { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const mainContainer = style({
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
background: cssVar('backgroundPrimaryColor'),
|
||||
selectors: {
|
||||
[`${root}[data-client-border=true] &`]: {
|
||||
borderRadius: '4px',
|
||||
},
|
||||
},
|
||||
borderTop: `1px solid ${cssVar('borderColor')}`,
|
||||
});
|
||||
|
||||
export const editorContainer = style({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
@@ -28,29 +16,3 @@ export const editorContainer = style({
|
||||
flex: 1,
|
||||
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 { 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 {
|
||||
BookmarkService,
|
||||
@@ -20,13 +19,12 @@ import {
|
||||
ServiceProviderContext,
|
||||
useLiveData,
|
||||
} from '@toeverything/infra';
|
||||
import { appSettingAtom, Workspace } from '@toeverything/infra';
|
||||
import { Workspace } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import {
|
||||
memo,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -37,84 +35,55 @@ import type { Map as YMap } from 'yjs';
|
||||
|
||||
import { recentPageIdsBaseAtom } from '../../../atoms';
|
||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||
import { HubIsland } from '../../../components/affine/hub-island';
|
||||
import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal';
|
||||
import { ImagePreviewModal } from '../../../components/image-preview';
|
||||
import { PageDetailEditor } from '../../../components/page-detail-editor';
|
||||
import { TrashPageFooter } from '../../../components/pure/trash-page-footer';
|
||||
import { TopTip } from '../../../components/top-tip';
|
||||
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 { 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 { PageNotFound } from '../../404';
|
||||
import * as styles from './detail-page.css';
|
||||
import { DetailPageHeader, RightSidebarHeader } 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>
|
||||
);
|
||||
};
|
||||
import { DetailPageHeader } from './detail-page-header';
|
||||
|
||||
const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const page = useService(Doc);
|
||||
const pageRecordList = useService(PageRecordList);
|
||||
const currentPageId = page.id;
|
||||
const { openPage, jumpToTag } = useNavigateHelper();
|
||||
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
|
||||
const currentWorkspace = useService(Workspace);
|
||||
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(
|
||||
meta => meta.id === page.id
|
||||
);
|
||||
@@ -184,6 +153,9 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const disposeTagClick = editor.slots.tagClicked.on(({ tagId }) => {
|
||||
jumpToTag(currentWorkspace.id, tagId);
|
||||
});
|
||||
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
disposeTagClick.dispose();
|
||||
@@ -200,24 +172,23 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
]
|
||||
);
|
||||
|
||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailPageLayout
|
||||
header={
|
||||
<>
|
||||
<DetailPageHeader
|
||||
page={page.blockSuiteDoc}
|
||||
workspace={currentWorkspace}
|
||||
showSidebarSwitch={!isInTrash}
|
||||
/>
|
||||
<TopTip pageId={currentPageId} workspace={currentWorkspace} />
|
||||
</>
|
||||
}
|
||||
main={
|
||||
// Add a key to force rerender when page changed, to avoid error boundary persisting.
|
||||
<ViewHeaderIsland>
|
||||
<DetailPageHeader
|
||||
page={page.blockSuiteDoc}
|
||||
workspace={currentWorkspace}
|
||||
/>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
<div className={styles.mainContainer}>
|
||||
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
||||
<AffineErrorBoundary key={currentPageId}>
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport className={styles.editorContainer}>
|
||||
<TopTip pageId={currentPageId} workspace={currentWorkspace} />
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onLoad={onLoad}
|
||||
@@ -226,25 +197,42 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
<HubIsland />
|
||||
</AffineErrorBoundary>
|
||||
}
|
||||
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>
|
||||
{isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
|
||||
<RightSidebarViewIsland
|
||||
active={isActiveView}
|
||||
header={
|
||||
!isWindowsDesktop ? (
|
||||
<MultiTabSidebarHeaderSwitcher
|
||||
activeTabName={activeTabName ?? sidebarTabs[0]?.name}
|
||||
setActiveTabName={setActiveTabName}
|
||||
tabs={sidebarTabs}
|
||||
/>
|
||||
) : 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
|
||||
pageId={currentPageId}
|
||||
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 };
|
||||
});
|
||||
}
|
||||
);
|
||||
-16
@@ -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>
|
||||
);
|
||||
};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { AiIcon } from '@blocksuite/icons';
|
||||
import { CopilotPanel } from '@blocksuite/presets';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import type { EditorExtension } from '../types';
|
||||
import * as styles from './outline.css';
|
||||
|
||||
// A wrapper for CopilotPanel
|
||||
const EditorCopilotPanel = () => {
|
||||
const copilotPanelRef = useRef<CopilotPanel | null>(null);
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
assertExists(
|
||||
copilotPanelRef.current,
|
||||
'copilot panel should be initialized'
|
||||
);
|
||||
container.append(copilotPanelRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!copilotPanelRef.current) {
|
||||
copilotPanelRef.current = new CopilotPanel();
|
||||
}
|
||||
|
||||
if (editor !== copilotPanelRef.current?.editor) {
|
||||
(copilotPanelRef.current as CopilotPanel).editor = editor;
|
||||
// (copilotPanelRef.current as CopilotPanel).fitPadding = [20, 20, 20, 20];
|
||||
}
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
|
||||
export const copilotExtension: EditorExtension = {
|
||||
name: 'copilot',
|
||||
icon: <AiIcon />,
|
||||
Component: EditorCopilotPanel,
|
||||
};
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
export const activeIdx = createVar();
|
||||
export const switchRoot = style({
|
||||
vars: {
|
||||
[activeIdx]: '0',
|
||||
},
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
height: '32px',
|
||||
borderRadius: '12px',
|
||||
padding: '4px',
|
||||
position: 'relative',
|
||||
background: cssVar('backgroundSecondaryColor'),
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
background: cssVar('backgroundPrimaryColor'),
|
||||
boxShadow: cssVar('shadow1'),
|
||||
borderRadius: '8px',
|
||||
position: 'absolute',
|
||||
transform: `translateX(calc(${activeIdx} * 32px))`,
|
||||
transition: 'all .15s',
|
||||
},
|
||||
});
|
||||
export const button = style({
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '8px',
|
||||
color: cssVar('iconColor'),
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
selectors: {
|
||||
'&[data-active=true]': {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
-69
@@ -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>
|
||||
);
|
||||
};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { FrameIcon } from '@blocksuite/icons';
|
||||
import { FramePanel } from '@blocksuite/presets';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import type { EditorExtension } from '../types';
|
||||
import * as styles from './frame.css';
|
||||
|
||||
// A wrapper for FramePanel
|
||||
const EditorFramePanel = () => {
|
||||
const framePanelRef = useRef<FramePanel | null>(null);
|
||||
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
assertExists(framePanelRef.current, 'frame panel should be initialized');
|
||||
container.append(framePanelRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!framePanelRef.current) {
|
||||
framePanelRef.current = new FramePanel();
|
||||
}
|
||||
|
||||
if (editor !== framePanelRef.current?.editor) {
|
||||
(framePanelRef.current as FramePanel).editor = editor;
|
||||
(framePanelRef.current as FramePanel).fitPadding = [20, 20, 20, 20];
|
||||
}
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
|
||||
export const framePanelExtension: EditorExtension = {
|
||||
name: 'frame',
|
||||
icon: <FrameIcon />,
|
||||
Component: EditorFramePanel,
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export * from './extensions';
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const interactive = style({
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0,
|
||||
borderRadius: 'inherit',
|
||||
boxShadow: `0 0 0 3px ${cssVar('primaryColor')}`,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
boxShadow: `0 0 0 0px ${cssVar('primaryColor')}`,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&:focus-visible::before': {
|
||||
opacity: 0.2,
|
||||
},
|
||||
'&:focus-visible::after': {
|
||||
boxShadow: `0 0 0 1px ${cssVar('primaryColor')}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const calendar = style({
|
||||
padding: '16px',
|
||||
});
|
||||
export const journalPanel = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const dailyCount = style({
|
||||
height: 0,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
});
|
||||
export const dailyCountHeader = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 16px',
|
||||
gap: 8,
|
||||
});
|
||||
export const dailyCountNav = style([
|
||||
interactive,
|
||||
{
|
||||
height: 28,
|
||||
width: 0,
|
||||
flex: 1,
|
||||
fontWeight: 500,
|
||||
fontSize: 14,
|
||||
padding: '4px 8px',
|
||||
whiteSpace: 'nowrap',
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
transition: 'all .3s',
|
||||
selectors: {
|
||||
'&[aria-selected="true"]': {
|
||||
backgroundColor: cssVar('backgroundTertiaryColor'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const dailyCountContainer = style({
|
||||
height: 0,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
width: `calc(var(--item-count) * 100%)`,
|
||||
transition: 'transform .15s ease',
|
||||
transform:
|
||||
'translateX(calc(var(--active-index) * 100% / var(--item-count) * -1))',
|
||||
});
|
||||
export const dailyCountItem = style({
|
||||
width: 'calc(100% / var(--item-count))',
|
||||
height: '100%',
|
||||
});
|
||||
export const dailyCountContent = style({
|
||||
padding: '8px 16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
});
|
||||
export const dailyCountEmpty = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxHeight: 220,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: '24px',
|
||||
fontSize: 15,
|
||||
color: cssVar('textSecondaryColor'),
|
||||
textAlign: 'center',
|
||||
padding: '0 70px',
|
||||
fontWeight: 400,
|
||||
});
|
||||
|
||||
// page item
|
||||
export const pageItem = style([
|
||||
interactive,
|
||||
{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
padding: '0 4px',
|
||||
gap: 8,
|
||||
height: 30,
|
||||
selectors: {
|
||||
'&[aria-selected="true"]': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const pageItemIcon = style({
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: cssVar('iconColor'),
|
||||
});
|
||||
export const pageItemLabel = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
textAlign: 'left',
|
||||
selectors: {
|
||||
'[aria-selected="true"] &': {
|
||||
// TODO: wait for design
|
||||
color: cssVar('primaryColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// conflict
|
||||
export const journalConflictBlock = style({
|
||||
padding: '0 16px 16px 16px',
|
||||
});
|
||||
export const journalConflictWrapper = style({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(170px, 1fr))',
|
||||
rowGap: 4,
|
||||
columnGap: 8,
|
||||
});
|
||||
export const journalConflictMoreTrigger = style([
|
||||
interactive,
|
||||
{
|
||||
color: cssVar('textSecondaryColor'),
|
||||
height: 30,
|
||||
borderRadius: 4,
|
||||
padding: '0px 8px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
]);
|
||||
|
||||
// customize date-picker cell
|
||||
export const journalDateCell = style([
|
||||
interactive,
|
||||
{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontWeight: 400,
|
||||
position: 'relative',
|
||||
|
||||
selectors: {
|
||||
'&[data-is-today="true"]': {
|
||||
fontWeight: 600,
|
||||
color: cssVar('brandColor'),
|
||||
},
|
||||
'&[data-not-current-month="true"]': {
|
||||
color: cssVar('black10'),
|
||||
},
|
||||
'&[data-selected="true"]': {
|
||||
backgroundColor: cssVar('brandColor'),
|
||||
fontWeight: 500,
|
||||
color: cssVar('pureWhite'),
|
||||
},
|
||||
'&[data-is-journal="false"][data-selected="true"]': {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: 500,
|
||||
border: `1px solid ${cssVar('primaryColor')}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const journalDateCellDot = style({
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: cssVar('primaryColor'),
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
});
|
||||
-382
@@ -1,382 +0,0 @@
|
||||
import {
|
||||
type DateCell,
|
||||
DatePicker,
|
||||
IconButton,
|
||||
Menu,
|
||||
Scrollable,
|
||||
} from '@affine/component';
|
||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||
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 {
|
||||
useJournalHelper,
|
||||
useJournalInfoHelper,
|
||||
useJournalRouteHelper,
|
||||
} from '@affine/core/hooks/use-journal';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
MoreHorizontalIcon,
|
||||
PageIcon,
|
||||
TodayIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { Doc, DocMeta } from '@blocksuite/store';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { EditorExtension, EditorExtensionProps } from '..';
|
||||
import * as styles from './journal.css';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const CountDisplay = ({
|
||||
count,
|
||||
max = 99,
|
||||
...attrs
|
||||
}: { count: number; max?: number } & HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span {...attrs}>{count > max ? `${max}+` : count}</span>;
|
||||
};
|
||||
interface PageItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
pageMeta: DocMeta;
|
||||
workspace: BlockSuiteWorkspace;
|
||||
right?: ReactNode;
|
||||
}
|
||||
const PageItem = ({
|
||||
pageMeta,
|
||||
workspace,
|
||||
right,
|
||||
className,
|
||||
...attrs
|
||||
}: PageItemProps) => {
|
||||
const { isJournal } = useJournalInfoHelper(workspace, pageMeta.id);
|
||||
const title = useBlockSuiteWorkspacePageTitle(workspace, pageMeta.id);
|
||||
|
||||
const Icon = isJournal
|
||||
? TodayIcon
|
||||
: pageMeta.mode === 'edgeless'
|
||||
? EdgelessIcon
|
||||
: PageIcon;
|
||||
return (
|
||||
<div
|
||||
aria-label={pageMeta.title}
|
||||
className={clsx(className, styles.pageItem)}
|
||||
{...attrs}
|
||||
>
|
||||
<div className={styles.pageItemIcon}>
|
||||
<Icon width={20} height={20} />
|
||||
</div>
|
||||
<span className={styles.pageItemLabel}>{title}</span>
|
||||
{right}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type NavItemName = 'createdToday' | 'updatedToday';
|
||||
interface NavItem {
|
||||
name: NavItemName;
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
interface JournalBlockProps extends EditorExtensionProps {
|
||||
date: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
const EditorJournalPanel = (props: EditorExtensionProps) => {
|
||||
const { workspace, page } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const { journalDate, isJournal } = useJournalInfoHelper(
|
||||
page.workspace,
|
||||
page.id
|
||||
);
|
||||
const { openJournal } = useJournalRouteHelper(workspace);
|
||||
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
|
||||
useEffect(() => {
|
||||
journalDate && setDate(journalDate.format('YYYY-MM-DD'));
|
||||
}, [journalDate]);
|
||||
|
||||
const onDateSelect = useCallback(
|
||||
(date: string) => {
|
||||
if (journalDate && dayjs(date).isSame(dayjs(journalDate))) return;
|
||||
openJournal(date);
|
||||
},
|
||||
[journalDate, openJournal]
|
||||
);
|
||||
|
||||
const customDayRenderer = useCallback(
|
||||
(cell: DateCell) => {
|
||||
// TODO: add a dot to indicate journal
|
||||
// has performance issue for now, better to calculate it in advance
|
||||
// const hasJournal = !!getJournalsByDate(cell.date.format('YYYY-MM-DD'))?.length;
|
||||
const hasJournal = false;
|
||||
return (
|
||||
<button
|
||||
className={styles.journalDateCell}
|
||||
data-is-date-cell
|
||||
tabIndex={cell.focused ? 0 : -1}
|
||||
data-is-today={cell.isToday}
|
||||
data-not-current-month={cell.notCurrentMonth}
|
||||
data-selected={cell.selected}
|
||||
data-is-journal={isJournal}
|
||||
data-has-journal={hasJournal}
|
||||
>
|
||||
{cell.label}
|
||||
{hasJournal && !cell.selected ? (
|
||||
<div className={styles.journalDateCellDot} />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
[isJournal]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.journalPanel} data-is-journal={isJournal}>
|
||||
<div className={styles.calendar}>
|
||||
<DatePicker
|
||||
weekDays={t['com.affine.calendar-date-picker.week-days']()}
|
||||
monthNames={t['com.affine.calendar-date-picker.month-names']()}
|
||||
todayLabel={t['com.affine.calendar-date-picker.today']()}
|
||||
customDayRenderer={customDayRenderer}
|
||||
value={date}
|
||||
onChange={onDateSelect}
|
||||
/>
|
||||
</div>
|
||||
<JournalConflictBlock date={dayjs(date)} {...props} />
|
||||
<JournalDailyCountBlock date={dayjs(date)} {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const sortPagesByDate = (
|
||||
pages: DocMeta[],
|
||||
field: 'updatedDate' | 'createDate',
|
||||
order: 'asc' | 'desc' = 'desc'
|
||||
) => {
|
||||
return [...pages].sort((a, b) => {
|
||||
return (order === 'asc' ? 1 : -1) * dayjs(b[field]).diff(dayjs(a[field]));
|
||||
});
|
||||
};
|
||||
|
||||
const DailyCountEmptyFallback = ({ name }: { name: NavItemName }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div className={styles.dailyCountEmpty}>
|
||||
{name === 'createdToday'
|
||||
? t['com.affine.journal.daily-count-created-empty-tips']()
|
||||
: t['com.affine.journal.daily-count-updated-empty-tips']()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => {
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
const t = useAFFiNEI18N();
|
||||
const [activeItem, setActiveItem] = useState<NavItemName>('createdToday');
|
||||
const pageMetas = useBlockSuiteDocMeta(workspace);
|
||||
|
||||
const navigateHelper = useNavigateHelper();
|
||||
|
||||
const getTodaysPages = useCallback(
|
||||
(field: 'createDate' | 'updatedDate') => {
|
||||
return sortPagesByDate(
|
||||
pageMetas.filter(pageMeta => {
|
||||
if (pageMeta.trash) return false;
|
||||
return pageMeta[field] && dayjs(pageMeta[field]).isSame(date, 'day');
|
||||
}),
|
||||
field
|
||||
);
|
||||
},
|
||||
[date, pageMetas]
|
||||
);
|
||||
|
||||
const createdToday = useMemo(
|
||||
() => getTodaysPages('createDate'),
|
||||
[getTodaysPages]
|
||||
);
|
||||
const updatedToday = useMemo(
|
||||
() => getTodaysPages('updatedDate'),
|
||||
[getTodaysPages]
|
||||
);
|
||||
|
||||
const headerItems = useMemo<NavItem[]>(
|
||||
() => [
|
||||
{
|
||||
name: 'createdToday',
|
||||
label: t['com.affine.journal.created-today'](),
|
||||
count: createdToday.length,
|
||||
},
|
||||
{
|
||||
name: 'updatedToday',
|
||||
label: t['com.affine.journal.updated-today'](),
|
||||
count: updatedToday.length,
|
||||
},
|
||||
],
|
||||
[createdToday.length, t, updatedToday.length]
|
||||
);
|
||||
|
||||
const activeIndex = headerItems.findIndex(({ name }) => name === activeItem);
|
||||
|
||||
const vars = assignInlineVars({
|
||||
'--active-index': String(activeIndex),
|
||||
'--item-count': String(headerItems.length),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.dailyCount} style={vars}>
|
||||
<header className={styles.dailyCountHeader}>
|
||||
{headerItems.map(({ label, count, name }, index) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setActiveItem(name)}
|
||||
aria-selected={activeItem === name}
|
||||
className={styles.dailyCountNav}
|
||||
key={index}
|
||||
>
|
||||
{label}
|
||||
|
||||
<CountDisplay count={count} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</header>
|
||||
|
||||
<main className={styles.dailyCountContainer} data-active={activeItem}>
|
||||
{headerItems.map(({ name }) => {
|
||||
const renderList =
|
||||
name === 'createdToday' ? createdToday : updatedToday;
|
||||
if (renderList.length === 0)
|
||||
return (
|
||||
<div key={name} className={styles.dailyCountItem}>
|
||||
<DailyCountEmptyFallback name={name} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Scrollable.Root key={name} className={styles.dailyCountItem}>
|
||||
<Scrollable.Scrollbar />
|
||||
<Scrollable.Viewport>
|
||||
<div className={styles.dailyCountContent} ref={nodeRef}>
|
||||
{renderList.map((pageMeta, index) => (
|
||||
<PageItem
|
||||
onClick={() =>
|
||||
navigateHelper.openPage(workspace.id, pageMeta.id)
|
||||
}
|
||||
tabIndex={name === activeItem ? 0 : -1}
|
||||
key={index}
|
||||
pageMeta={pageMeta}
|
||||
workspace={workspace}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Scrollable.Viewport>
|
||||
</Scrollable.Root>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MAX_CONFLICT_COUNT = 5;
|
||||
interface ConflictListProps
|
||||
extends JournalBlockProps,
|
||||
PropsWithChildren,
|
||||
HTMLAttributes<HTMLDivElement> {
|
||||
pages: Doc[];
|
||||
}
|
||||
const ConflictList = ({
|
||||
page: currentPage,
|
||||
pages,
|
||||
workspace,
|
||||
children,
|
||||
className,
|
||||
...attrs
|
||||
}: ConflictListProps) => {
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const { setTrashModal } = useTrashModalHelper(workspace);
|
||||
|
||||
const handleOpenTrashModal = useCallback(
|
||||
(page: Doc) => {
|
||||
if (!page.meta) return;
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [page.id],
|
||||
pageTitles: [page.meta.title],
|
||||
});
|
||||
},
|
||||
[setTrashModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.journalConflictWrapper, className)} {...attrs}>
|
||||
{pages.map(page => {
|
||||
const isCurrent = page.id === currentPage.id;
|
||||
return (
|
||||
<PageItem
|
||||
aria-label={page.meta?.title}
|
||||
aria-selected={isCurrent}
|
||||
pageMeta={page.meta as DocMeta}
|
||||
workspace={workspace}
|
||||
key={page.id}
|
||||
right={
|
||||
<Menu
|
||||
items={
|
||||
<MoveToTrash onSelect={() => handleOpenTrashModal(page)} />
|
||||
}
|
||||
>
|
||||
<IconButton type="plain">
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
}
|
||||
onClick={() => navigateHelper.openPage(workspace.id, page.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const JournalConflictBlock = (props: JournalBlockProps) => {
|
||||
const { workspace, date } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const journalHelper = useJournalHelper(workspace);
|
||||
const pages = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD'));
|
||||
|
||||
if (pages.length <= 1) return null;
|
||||
|
||||
return (
|
||||
<ConflictList
|
||||
className={styles.journalConflictBlock}
|
||||
pages={pages.slice(0, MAX_CONFLICT_COUNT)}
|
||||
{...props}
|
||||
>
|
||||
{pages.length > MAX_CONFLICT_COUNT ? (
|
||||
<Menu
|
||||
items={
|
||||
<ConflictList pages={pages.slice(MAX_CONFLICT_COUNT)} {...props} />
|
||||
}
|
||||
>
|
||||
<div className={styles.journalConflictMoreTrigger}>
|
||||
{t['com.affine.journal.conflict-show-more']({
|
||||
count: (pages.length - MAX_CONFLICT_COUNT).toFixed(0),
|
||||
})}
|
||||
</div>
|
||||
</Menu>
|
||||
) : null}
|
||||
</ConflictList>
|
||||
);
|
||||
};
|
||||
|
||||
export const journalExtension: EditorExtension = {
|
||||
name: 'journal',
|
||||
icon: <TodayIcon />,
|
||||
Component: EditorJournalPanel,
|
||||
};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { TocIcon } from '@blocksuite/icons';
|
||||
import { OutlinePanel } from '@blocksuite/presets';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import type { EditorExtension } from '../types';
|
||||
import * as styles from './outline.css';
|
||||
|
||||
// A wrapper for TOCNotesPanel
|
||||
const EditorOutline = () => {
|
||||
const outlinePanelRef = useRef<OutlinePanel | null>(null);
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
assertExists(outlinePanelRef.current, 'toc panel should be initialized');
|
||||
container.append(outlinePanelRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!outlinePanelRef.current) {
|
||||
outlinePanelRef.current = new OutlinePanel();
|
||||
}
|
||||
|
||||
if (editor !== outlinePanelRef.current?.editor) {
|
||||
(outlinePanelRef.current as OutlinePanel).editor = editor;
|
||||
(outlinePanelRef.current as OutlinePanel).fitPadding = [20, 20, 20, 20];
|
||||
}
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
|
||||
export const outlineExtension: EditorExtension = {
|
||||
name: 'outline',
|
||||
icon: <TocIcon />,
|
||||
Component: EditorOutline,
|
||||
};
|
||||
@@ -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 { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
import { HubIsland } from '../../components/affine/hub-island';
|
||||
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 { performanceRenderLogger } from '../../shared';
|
||||
import { PageNotFound } from '../404';
|
||||
@@ -115,6 +117,8 @@ export const Component = (): ReactElement => {
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout>
|
||||
<WorkbenchRoot />
|
||||
<RightSidebarContainer />
|
||||
<HubIsland />
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
</Suspense>
|
||||
|
||||
@@ -76,6 +76,7 @@ export const EmptyCollectionList = ({ heading }: { heading: ReactNode }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyTagList = ({ heading }: { heading: ReactNode }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
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 * as styles from '../all-page/all-page.css';
|
||||
|
||||
export const TagDetailHeader = () => {
|
||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||
|
||||
return (
|
||||
<Header
|
||||
right={
|
||||
<div
|
||||
className={styles.headerRightWindows}
|
||||
data-is-windows-desktop={isWindowsDesktop}
|
||||
>
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
|
||||
/>
|
||||
);
|
||||
return <Header center={<WorkspaceModeFilterTab activeFilter={'tags'} />} />;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { HubIsland } from '@affine/core/components/affine/hub-island';
|
||||
import {
|
||||
PageListHeader,
|
||||
useTagMetas,
|
||||
@@ -11,7 +10,6 @@ import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { PageNotFound } from '../../404';
|
||||
import * as styles from '../all-page/all-page.css';
|
||||
import { EmptyPageList } from '../page-list-empty';
|
||||
import { TagDetailHeader } from './header';
|
||||
|
||||
@@ -40,7 +38,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<>
|
||||
<TagDetailHeader />
|
||||
{tagPageMetas.length > 0 ? (
|
||||
<VirtualizedPageList tag={currentTag} listItem={tagPageMetas} />
|
||||
@@ -51,8 +49,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
)}
|
||||
<HubIsland />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export { root } from './all-page/all-page.css';
|
||||
export const trashTitle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -8,6 +7,13 @@ export const trashTitle = style({
|
||||
padding: '0 8px',
|
||||
fontWeight: 600,
|
||||
});
|
||||
export const body = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
export const trashIcon = style({
|
||||
color: cssVar('iconColor'),
|
||||
fontSize: cssVar('fontH5'),
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Workspace } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../modules/workbench';
|
||||
import { EmptyPageList } from './page-list-empty';
|
||||
import * as styles from './trash-page.css';
|
||||
|
||||
@@ -94,26 +95,32 @@ export const TrashPage = () => {
|
||||
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
||||
}, []);
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<TrashHeader />
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedList
|
||||
items={filteredPageMetas}
|
||||
rowAsLink
|
||||
groupBy={false}
|
||||
isPreferredEdgeless={isPreferredEdgeless}
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
operationsRenderer={pageOperationsRenderer}
|
||||
itemRenderer={pageItemRenderer}
|
||||
headerRenderer={pageHeaderRenderer}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList
|
||||
type="trash"
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<TrashHeader />
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
<div className={styles.body}>
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedList
|
||||
items={filteredPageMetas}
|
||||
rowAsLink
|
||||
groupBy={false}
|
||||
isPreferredEdgeless={isPreferredEdgeless}
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
operationsRenderer={pageOperationsRenderer}
|
||||
itemRenderer={pageItemRenderer}
|
||||
headerRenderer={pageHeaderRenderer}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList
|
||||
type="trash"
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user