mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 11:58:41 +00:00
feat: support for view management (#2892)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './collections-list';
|
||||
export { Page } from './page';
|
||||
export { PageOperations } from './page';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)',
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -3,3 +3,7 @@ import type { AllWorkspace } from '../../../shared';
|
||||
export type FavoriteListProps = {
|
||||
currentWorkspace: AllWorkspace;
|
||||
};
|
||||
|
||||
export type CollectionsListProps = {
|
||||
currentWorkspace: AllWorkspace;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
27
apps/web/src/hooks/use-get-page-info.ts
Normal file
27
apps/web/src/hooks/use-get-page-info.ts
Normal 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',
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ const AllPage: NextPageWithLayout = () => {
|
||||
}}
|
||||
/>
|
||||
<PageList
|
||||
view={setting.currentView}
|
||||
collection={setting.currentCollection}
|
||||
onOpenPage={onClickPage}
|
||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||
/>
|
||||
|
||||
17
apps/web/src/utils/filter.ts
Normal file
17
apps/web/src/utils/filter.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user