feat: support for view management (#2892)

This commit is contained in:
3720
2023-06-30 13:40:00 +08:00
committed by GitHub
parent d3393cb0fc
commit 9d0db78f64
45 changed files with 1936 additions and 477 deletions

View File

@@ -34,7 +34,7 @@
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/global": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/icons": "^2.1.23",
"@blocksuite/lit": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
"react": "18.3.0-canary-8ec962d82-20230623",

View File

@@ -23,7 +23,7 @@
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/global": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/icons": "^2.1.23",
"@blocksuite/lit": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
"@dnd-kit/core": "^6.0.8",

View File

@@ -336,10 +336,10 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, view }) => {
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
view={view}
collection={collection}
listType="all"
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}

View File

@@ -95,11 +95,11 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, view }) => {
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
listType="all"
view={view}
collection={collection}
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>

View File

@@ -1,11 +1,7 @@
import { Empty } from '@affine/component';
import type { ListData, TrashListData } from '@affine/component/page-list';
import {
filterByFilterList,
PageList,
PageListTrashView,
} from '@affine/component/page-list';
import type { View } from '@affine/env/filter';
import { PageList, PageListTrashView } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
@@ -18,8 +14,10 @@ import { useMemo } from 'react';
import { allPageModeSelectAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useGetPageInfoById } from '../../../hooks/use-get-page-info';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
import { filterPage } from '../../../utils/filter';
import { emptyDescButton, emptyDescKbd, pageListEmptyStyle } from './index.css';
import { usePageHelper } from './utils';
@@ -28,7 +26,7 @@ export type BlockSuitePageListProps = {
listType: 'all' | 'trash' | 'shared' | 'public';
isPublic?: true;
onOpenPage: (pageId: string, newTab?: boolean) => void;
view?: View;
collection?: Collection;
};
const filter = {
@@ -97,7 +95,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
onOpenPage,
listType,
isPublic = false,
view,
collection,
}) => {
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const {
@@ -111,6 +109,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
const { createPage, createEdgeless, importFile, isPreferredEdgeless } =
usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const getPageInfo = useGetPageInfoById();
const list = useMemo(
() =>
pageMetas
@@ -131,16 +130,12 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
if (!filter[listType](pageMeta, pageMetas)) {
return false;
}
if (!view) {
if (!collection) {
return true;
}
return filterByFilterList(view.filterList, {
'Is Favourited': !!pageMeta.favorite,
Created: pageMeta.createDate,
Updated: pageMeta.updatedDate ?? pageMeta.createDate,
});
return filterPage(collection, pageMeta);
}),
[pageMetas, filterMode, isPreferredEdgeless, listType, view]
[pageMetas, filterMode, isPreferredEdgeless, listType, collection]
);
if (listType === 'trash') {
@@ -222,9 +217,9 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
},
};
});
return (
<PageList
getPageInfo={getPageInfo}
onCreateNewPage={createPage}
onCreateNewEdgeless={createEdgeless}
onImportFile={importFile}

View File

@@ -0,0 +1,274 @@
import { Menu } from '@affine/component';
import { MenuItem } from '@affine/component/app-sidebar';
import {
EditCollectionModel,
useAllPageSetting,
useSavedCollections,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import {
DeleteIcon,
FilterIcon,
MoreHorizontalIcon,
UnpinIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import type { DragEndEvent } from '@dnd-kit/core';
import { useDroppable } from '@dnd-kit/core';
import * as Collapsible from '@radix-ui/react-collapsible';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
import type { AllWorkspace } from '../../../../shared';
import { filterPage } from '../../../../utils/filter';
import type { CollectionsListProps } from '../index';
import { Page } from './page';
import * as styles from './styles.css';
const Collections_DROP_AREA_PREFIX = 'collections-';
const isCollectionsDropArea = (id?: string | number) => {
return typeof id === 'string' && id.startsWith(Collections_DROP_AREA_PREFIX);
};
export const processCollectionsDrag = (e: DragEndEvent) => {
if (
isCollectionsDropArea(e.over?.id) &&
String(e.active.id).startsWith('page-list-item-')
) {
e.over?.data.current?.addToCollection?.(e.active.data.current?.pageId);
}
};
const CollectionOperations = ({
view,
showUpdateCollection,
setting,
}: {
view: Collection;
showUpdateCollection: () => void;
setting: ReturnType<typeof useAllPageSetting>;
}) => {
const actions = useMemo<
Array<
| {
icon: ReactElement;
name: string;
click: () => void;
className?: string;
element?: undefined;
}
| {
element: ReactElement;
}
>
>(
() => [
{
icon: <FilterIcon />,
name: 'Edit Filter',
click: showUpdateCollection,
},
{
icon: <UnpinIcon />,
name: 'Unpin',
click: () => {
return setting.updateCollection({
...view,
pinned: false,
});
},
},
{
element: <div key="divider" className={styles.menuDividerStyle}></div>,
},
{
icon: <DeleteIcon style={{ color: 'var(--affine-warning-color)' }} />,
name: 'Delete',
click: () => {
return setting.deleteCollection(view.id);
},
className: styles.deleteFolder,
},
],
[setting, showUpdateCollection, view]
);
return (
<div style={{ minWidth: 150 }}>
{actions.map(action => {
if (action.element) {
return action.element;
}
return (
<MenuItem
data-testid="collection-option"
key={action.name}
className={action.className}
icon={action.icon}
onClick={action.click}
>
{action.name}
</MenuItem>
);
})}
</div>
);
};
const CollectionRenderer = ({
collection,
pages,
workspace,
getPageInfo,
}: {
collection: Collection;
pages: PageMeta[];
workspace: AllWorkspace;
getPageInfo: GetPageInfoById;
}) => {
const [collapsed, setCollapsed] = React.useState(true);
const setting = useAllPageSetting();
const router = useRouter();
const clickCollection = useCallback(() => {
router
.push(`/workspace/${workspace.id}/all`)
.then(() => {
setting.selectCollection(collection.id);
})
.catch(err => {
console.error(err);
});
}, [router, workspace.id, setting, collection.id]);
const { setNodeRef, isOver } = useDroppable({
id: `${Collections_DROP_AREA_PREFIX}${collection.id}`,
data: {
addToCollection: (id: string) => {
setting.addPage(collection.id, id).catch(err => {
console.error(err);
});
},
},
});
const allPagesMeta = useMemo(
() => Object.fromEntries(pages.map(v => [v.id, v])),
[pages]
);
const [show, showUpdateCollection] = useState(false);
const allowList = useMemo(
() => new Set(collection.allowList),
[collection.allowList]
);
const excludeList = useMemo(
() => new Set(collection.excludeList),
[collection.excludeList]
);
const removeFromAllowList = useCallback(
(id: string) => {
return setting.updateCollection({
...collection,
allowList: collection.allowList?.filter(v => v != id),
});
},
[collection, setting]
);
const addToExcludeList = useCallback(
(id: string) => {
return setting.updateCollection({
...collection,
excludeList: [id, ...(collection.excludeList ?? [])],
});
},
[collection, setting]
);
const pagesToRender = pages.filter(
page => filterPage(collection, page) && !page.trash
);
return (
<Collapsible.Root open={!collapsed}>
<EditCollectionModel
getPageInfo={getPageInfo}
init={collection}
onConfirm={setting.saveCollection}
open={show}
onClose={() => showUpdateCollection(false)}
/>
<MenuItem
data-testid="collection-item"
ref={setNodeRef}
onCollapsedChange={setCollapsed}
active={isOver}
icon={<ViewLayersIcon />}
postfix={
<Menu
trigger="click"
placement="bottom-start"
content={
<CollectionOperations
view={collection}
showUpdateCollection={() => showUpdateCollection(true)}
setting={setting}
/>
}
>
<div data-testid="collection-options" className={styles.more}>
<MoreHorizontalIcon></MoreHorizontalIcon>
</div>
</Menu>
}
collapsed={pagesToRender.length > 0 ? collapsed : undefined}
onClick={clickCollection}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>{collection.name}</div>
</div>
</MenuItem>
<Collapsible.Content>
<div style={{ marginLeft: 8 }}>
{pagesToRender.map(page => {
return (
<Page
inAllowList={allowList.has(page.id)}
removeFromAllowList={removeFromAllowList}
inExcludeList={excludeList.has(page.id)}
addToExcludeList={addToExcludeList}
allPageMeta={allPagesMeta}
page={page}
key={page.id}
workspace={workspace}
/>
);
})}
</div>
</Collapsible.Content>
</Collapsible.Root>
);
};
export const CollectionsList = ({ currentWorkspace }: CollectionsListProps) => {
const metas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const { savedCollections } = useSavedCollections();
const getPageInfo = useGetPageInfoById();
return (
<div data-testid="collections" className={styles.wrapper}>
{savedCollections
.filter(v => v.pinned)
.map(view => {
return (
<CollectionRenderer
getPageInfo={getPageInfo}
key={view.id}
collection={view}
pages={metas}
workspace={currentWorkspace}
/>
);
})}
</div>
);
};

View File

@@ -0,0 +1,3 @@
export * from './collections-list';
export { Page } from './page';
export { PageOperations } from './page';

View File

@@ -0,0 +1,200 @@
import { Menu } from '@affine/component';
import { MenuItem } from '@affine/component/app-sidebar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteIcon,
EdgelessIcon,
FilterIcon,
MoreHorizontalIcon,
PageIcon,
} from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
import { useAtomValue } from 'jotai/index';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import React, { useCallback, useMemo } from 'react';
import { pageSettingFamily } from '../../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import type { AllWorkspace } from '../../../../shared';
import { ReferencePage } from '../components/reference-page';
import * as styles from './styles.css';
export const PageOperations = ({
page,
inAllowList,
addToExcludeList,
removeFromAllowList,
inExcludeList,
workspace,
}: {
workspace: Workspace;
page: PageMeta;
inAllowList: boolean;
removeFromAllowList: (id: string) => void;
inExcludeList: boolean;
addToExcludeList: (id: string) => void;
}) => {
const { removeToTrash } = useBlockSuiteMetaHelper(workspace);
const actions = useMemo<
Array<
| {
icon: ReactElement;
name: string;
click: () => void;
className?: string;
element?: undefined;
}
| {
element: ReactElement;
}
>
>(
() => [
...(inAllowList
? [
{
icon: <FilterIcon />,
name: 'Remove special filter',
click: () => removeFromAllowList(page.id),
},
]
: []),
...(!inExcludeList
? [
{
icon: <FilterIcon />,
name: 'Exclude from filter',
click: () => addToExcludeList(page.id),
},
]
: []),
{
element: <div key="divider" className={styles.menuDividerStyle}></div>,
},
{
icon: <DeleteIcon style={{ color: 'var(--affine-warning-color)' }} />,
name: 'Delete',
click: () => {
removeToTrash(page.id);
},
className: styles.deleteFolder,
},
],
[
inAllowList,
inExcludeList,
page.id,
removeFromAllowList,
addToExcludeList,
removeToTrash,
]
);
return (
<>
{actions.map(action => {
if (action.element) {
return action.element;
}
return (
<MenuItem
data-testid="collection-page-option"
key={action.name}
className={action.className}
icon={action.icon}
onClick={action.click}
>
{action.name}
</MenuItem>
);
})}
</>
);
};
export const Page = ({
page,
workspace,
allPageMeta,
inAllowList,
inExcludeList,
removeFromAllowList,
addToExcludeList,
}: {
page: PageMeta;
inAllowList: boolean;
removeFromAllowList: (id: string) => void;
inExcludeList: boolean;
addToExcludeList: (id: string) => void;
workspace: AllWorkspace;
allPageMeta: Record<string, PageMeta>;
}) => {
const [collapsed, setCollapsed] = React.useState(true);
const router = useRouter();
const t = useAFFiNEI18N();
const pageId = page.id;
const active = router.query.pageId === pageId;
const setting = useAtomValue(pageSettingFamily(pageId));
const icon = setting?.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
const references = useBlockSuitePageReferences(
workspace.blockSuiteWorkspace,
pageId
);
const clickPage = useCallback(() => {
return router.push(`/workspace/${workspace.id}/${page.id}`);
}, [page.id, router, workspace.id]);
const referencesToRender = references.filter(id => !allPageMeta[id]?.trash);
return (
<Collapsible.Root open={!collapsed}>
<MenuItem
data-testid="collection-page"
icon={icon}
onClick={clickPage}
className={styles.title}
active={active}
collapsed={referencesToRender.length > 0 ? collapsed : undefined}
onCollapsedChange={setCollapsed}
postfix={
<Menu
trigger="click"
placement="bottom-start"
content={
<div style={{ width: 220 }}>
<PageOperations
inAllowList={inAllowList}
removeFromAllowList={removeFromAllowList}
inExcludeList={inExcludeList}
addToExcludeList={addToExcludeList}
page={page}
workspace={workspace.blockSuiteWorkspace}
/>
</div>
}
>
<div data-testid="collection-page-options" className={styles.more}>
<MoreHorizontalIcon></MoreHorizontalIcon>
</div>
</Menu>
}
>
{page.title || t['Untitled']()}
</MenuItem>
<Collapsible.Content>
<div style={{ marginLeft: 8 }}>
{referencesToRender.map(id => {
return (
<ReferencePage
key={id}
workspace={workspace.blockSuiteWorkspace}
pageId={id}
metaMapping={allPageMeta}
parentIds={new Set([pageId])}
/>
);
})}
</div>
</Collapsible.Content>
</Collapsible.Root>
);
};

View File

@@ -0,0 +1,51 @@
import { style } from '@vanilla-extract/css';
export const wrapper = style({
userSelect: 'none',
// marginLeft:8,
});
export const collapsedIcon = style({
transition: 'transform 0.2s ease-in-out',
selectors: {
'&[data-collapsed="true"]': {
transform: 'rotate(-90deg)',
},
},
});
export const view = style({
display: 'flex',
alignItems: 'center',
});
export const viewTitle = style({
display: 'flex',
alignItems: 'center',
});
export const title = style({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
export const more = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
padding: 4,
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
});
export const deleteFolder = style({
color: 'var(--affine-warning-color)',
':hover': {
backgroundColor: 'var(--affine-background-warning-color)',
},
});
export const menuDividerStyle = style({
marginTop: '2px',
marginBottom: '2px',
marginLeft: '12px',
marginRight: '8px',
height: '1px',
background: 'var(--affine-border-color)',
});

View File

@@ -0,0 +1,81 @@
import { MenuLinkItem } from '@affine/component/app-sidebar';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
import { useAtomValue } from 'jotai/index';
import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';
import { pageSettingFamily } from '../../../../atoms';
import * as styles from '../favorite/styles.css';
interface ReferencePageProps {
workspace: Workspace;
pageId: string;
metaMapping: Record<string, PageMeta>;
parentIds: Set<string>;
}
export const ReferencePage = ({
workspace,
pageId,
metaMapping,
parentIds,
}: ReferencePageProps) => {
const router = useRouter();
const setting = useAtomValue(pageSettingFamily(pageId));
const active = router.query.pageId === pageId;
const icon = setting?.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
const references = useBlockSuitePageReferences(workspace, pageId);
const referencesToShow = useMemo(() => {
return [
...new Set(
references.filter(
ref => !parentIds.has(ref) && !metaMapping[ref]?.trash
)
),
];
}, [references, parentIds, metaMapping]);
const [collapsed, setCollapsed] = useState(true);
const collapsible = referencesToShow.length > 0;
const nestedItem = parentIds.size > 0;
const untitled = !metaMapping[pageId]?.title;
return (
<Collapsible.Root
className={styles.favItemWrapper}
data-nested={nestedItem}
open={!collapsed}
>
<MenuLinkItem
data-type="favorite-list-item"
data-testid={`favorite-list-item-${pageId}`}
active={active}
href={`/workspace/${workspace.id}/${pageId}`}
icon={icon}
collapsed={collapsible ? collapsed : undefined}
onCollapsedChange={setCollapsed}
>
<span className={styles.label} data-untitled={untitled}>
{metaMapping[pageId]?.title || 'Untitled'}
</span>
</MenuLinkItem>
{collapsible && (
<Collapsible.Content className={styles.collapsibleContent}>
<div className={styles.collapsibleContentInner}>
{referencesToShow.map(ref => {
return (
<ReferencePage
key={ref}
workspace={workspace}
pageId={ref}
metaMapping={metaMapping}
parentIds={new Set([...parentIds, pageId])}
/>
);
})}
</div>
</Collapsible.Content>
)}
</Collapsible.Root>
);
};

View File

@@ -1,88 +1,10 @@
import { MenuLinkItem } from '@affine/component/app-sidebar';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { pageSettingFamily } from '../../../../atoms';
import { ReferencePage } from '../components/reference-page';
import type { FavoriteListProps } from '../index';
import EmptyItem from './empty-item';
import * as styles from './styles.css';
interface FavoriteMenuItemProps {
workspace: Workspace;
pageId: string;
metaMapping: Record<string, PageMeta>;
parentIds: Set<string>;
}
function FavoriteMenuItem({
workspace,
pageId,
metaMapping,
parentIds,
}: FavoriteMenuItemProps) {
const router = useRouter();
const setting = useAtomValue(pageSettingFamily(pageId));
const active = router.query.pageId === pageId;
const icon = setting?.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
const references = useBlockSuitePageReferences(workspace, pageId);
const referencesToShow = useMemo(() => {
return [
...new Set(
references.filter(
ref => !parentIds.has(ref) && !metaMapping[ref]?.trash
)
),
];
}, [references, parentIds, metaMapping]);
const [collapsed, setCollapsed] = useState(true);
const collapsible = referencesToShow.length > 0;
const nestedItem = parentIds.size > 0;
const untitled = !metaMapping[pageId]?.title;
return (
<Collapsible.Root
className={styles.favItemWrapper}
data-nested={nestedItem}
open={!collapsed}
>
<MenuLinkItem
data-type="favorite-list-item"
data-testid={`favorite-list-item-${pageId}`}
active={active}
href={`/workspace/${workspace.id}/${pageId}`}
icon={icon}
collapsed={collapsible ? collapsed : undefined}
onCollapsedChange={setCollapsed}
>
<span className={styles.label} data-untitled={untitled}>
{metaMapping[pageId]?.title || 'Untitled'}
</span>
</MenuLinkItem>
{collapsible && (
<Collapsible.Content className={styles.collapsibleContent}>
<div className={styles.collapsibleContentInner}>
{referencesToShow.map(ref => {
return (
<FavoriteMenuItem
key={ref}
workspace={workspace}
pageId={ref}
metaMapping={metaMapping}
parentIds={new Set([...parentIds, pageId])}
/>
);
})}
</div>
</Collapsible.Content>
)}
</Collapsible.Root>
);
}
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
const metas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
@@ -105,7 +27,7 @@ export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
<>
{favoriteList.map((pageMeta, index) => {
return (
<FavoriteMenuItem
<ReferencePage
key={`${pageMeta}-${index}`}
metaMapping={metaMapping}
pageId={pageMeta.id}

View File

@@ -3,3 +3,7 @@ import type { AllWorkspace } from '../../../shared';
export type FavoriteListProps = {
currentWorkspace: AllWorkspace;
};
export type CollectionsListProps = {
currentWorkspace: AllWorkspace;
};

View File

@@ -29,6 +29,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import { useHistoryAtom } from '../../atoms/history';
import { useAppSetting } from '../../atoms/settings';
import type { AllWorkspace } from '../../shared';
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
@@ -225,7 +226,10 @@ export const RootAppSidebar = ({
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
</RouteMenuLinkItem>
))}
<CategoryDivider label={t['Collections']()} />
{blockSuiteWorkspace && (
<CollectionsList currentWorkspace={currentWorkspace} />
)}
<CategoryDivider label={t['others']()} />
<RouteMenuLinkItem
ref={trashDroppable.setNodeRef}

View File

@@ -1,18 +1,19 @@
import { Button } from '@affine/component';
import {
CollectionList,
FilterList,
SaveViewButton,
SaveCollectionButton,
useAllPageSetting,
ViewList,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import type { WorkspaceHeaderProps } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SettingsIcon } from '@blocksuite/icons';
import { RESET } from 'jotai/utils';
import { uuidv4 } from '@blocksuite/store';
import type { ReactElement } from 'react';
import { NIL } from 'uuid';
import { useCallback } from 'react';
import { useGetPageInfoById } from '../hooks/use-get-page-info';
import { BlockSuiteEditorHeader } from './blocksuite/workspace-header';
import { filterContainerStyle } from './filter-container.css';
import { WorkspaceModeFilterTab, WorkspaceTitle } from './pure/workspace-title';
@@ -23,40 +24,51 @@ export function WorkspaceHeader({
}: WorkspaceHeaderProps<WorkspaceFlavour>): ReactElement {
const setting = useAllPageSetting();
const t = useAFFiNEI18N();
const saveToCollection = useCallback(
async (collection: Collection) => {
await setting.saveCollection(collection);
setting.selectCollection(collection.id);
},
[setting]
);
const getPageInfoById = useGetPageInfoById();
if ('subPath' in currentEntry) {
if (currentEntry.subPath === WorkspaceSubPath.ALL) {
const leftSlot = <ViewList setting={setting}></ViewList>;
const filterContainer = setting.currentView.filterList.length > 0 && (
<div className={filterContainerStyle}>
<div style={{ flex: 1 }}>
<FilterList
value={setting.currentView.filterList}
onChange={filterList => {
setting.setCurrentView(view => ({
...view,
filterList,
}));
}}
/>
</div>
{runtimeConfig.enableAllPageSaving && (
<div>
{setting.currentView.id !== NIL ||
(setting.currentView.id === NIL &&
setting.currentView.filterList.length > 0) ? (
<SaveViewButton
init={setting.currentView.filterList}
onConfirm={setting.createView}
></SaveViewButton>
) : (
<Button onClick={() => setting.setCurrentView(RESET)}>
Back to all
</Button>
)}
</div>
)}
</div>
const leftSlot = (
<CollectionList
setting={setting}
getPageInfo={getPageInfoById}
></CollectionList>
);
const filterContainer =
setting.isDefault && setting.currentCollection.filterList.length > 0 ? (
<div className={filterContainerStyle}>
<div style={{ flex: 1 }}>
<FilterList
value={setting.currentCollection.filterList}
onChange={filterList => {
return setting.updateCollection({
...setting.currentCollection,
filterList,
});
}}
/>
</div>
<div>
{setting.currentCollection.filterList.length > 0 ? (
<SaveCollectionButton
getPageInfo={getPageInfoById}
init={{
id: uuidv4(),
name: '',
filterList: setting.currentCollection.filterList,
}}
onConfirm={saveToCollection}
></SaveCollectionButton>
) : null}
</div>
</div>
) : null;
return (
<>
<WorkspaceModeFilterTab

View File

@@ -0,0 +1,27 @@
import type { GetPageInfoById } from '@affine/env/page-info';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { pageSettingsAtom } from '../atoms';
import { rootCurrentWorkspaceAtom } from '../atoms/root';
export const useGetPageInfoById = (): GetPageInfoById => {
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const pageMap = useMemo(
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
[pageMetas]
);
const pageSettings = useAtomValue(pageSettingsAtom);
return (id: string) => {
const page = pageMap[id];
if (!page) {
return;
}
return {
...page,
isEdgeless: pageSettings[id]?.mode === 'edgeless',
};
};
};

View File

@@ -50,6 +50,7 @@ import {
import { AppContainer } from '../components/affine/app-container';
import type { IslandItemNames } from '../components/pure/help-island';
import { HelpIsland } from '../components/pure/help-island';
import { processCollectionsDrag } from '../components/pure/workspace-slider-bar/collections';
import {
DROPPABLE_SIDEBAR_TRASH,
RootAppSidebar,
@@ -393,6 +394,8 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
moveToTrash(pageId);
toast(t['Successfully deleted']());
}
// Drag page into Collections
processCollectionsDrag(e);
},
[moveToTrash, t]
);

View File

@@ -51,7 +51,7 @@ const AllPage: NextPageWithLayout = () => {
}}
/>
<PageList
view={setting.currentView}
collection={setting.currentCollection}
onOpenPage={onClickPage}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>

View File

@@ -0,0 +1,17 @@
import { filterByFilterList } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import type { PageMeta } from '@blocksuite/store';
export const filterPage = (collection: Collection, page: PageMeta) => {
if (collection.excludeList?.includes(page.id)) {
return false;
}
if (collection.allowList?.includes(page.id)) {
return true;
}
return filterByFilterList(collection.filterList, {
'Is Favourited': !!page.favorite,
Created: page.createDate,
Updated: page.updatedDate ?? page.createDate,
});
};