mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +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/blocks": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/global": "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/lit": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"react": "18.3.0-canary-8ec962d82-20230623",
|
"react": "18.3.0-canary-8ec962d82-20230623",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/global": "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/lit": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@dnd-kit/core": "^6.0.8",
|
"@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 (
|
return (
|
||||||
<BlockSuitePageList
|
<BlockSuitePageList
|
||||||
view={view}
|
collection={collection}
|
||||||
listType="all"
|
listType="all"
|
||||||
onOpenPage={onOpenPage}
|
onOpenPage={onOpenPage}
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||||
|
|||||||
@@ -95,11 +95,11 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
PageList: ({ blockSuiteWorkspace, onOpenPage, view }) => {
|
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||||
return (
|
return (
|
||||||
<BlockSuitePageList
|
<BlockSuitePageList
|
||||||
listType="all"
|
listType="all"
|
||||||
view={view}
|
collection={collection}
|
||||||
onOpenPage={onOpenPage}
|
onOpenPage={onOpenPage}
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Empty } from '@affine/component';
|
import { Empty } from '@affine/component';
|
||||||
import type { ListData, TrashListData } from '@affine/component/page-list';
|
import type { ListData, TrashListData } from '@affine/component/page-list';
|
||||||
import {
|
import { PageList, PageListTrashView } from '@affine/component/page-list';
|
||||||
filterByFilterList,
|
import type { Collection } from '@affine/env/filter';
|
||||||
PageList,
|
|
||||||
PageListTrashView,
|
|
||||||
} from '@affine/component/page-list';
|
|
||||||
import type { View } from '@affine/env/filter';
|
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
@@ -18,8 +14,10 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { allPageModeSelectAtom } from '../../../atoms';
|
import { allPageModeSelectAtom } from '../../../atoms';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||||
|
import { useGetPageInfoById } from '../../../hooks/use-get-page-info';
|
||||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
|
import { filterPage } from '../../../utils/filter';
|
||||||
import { emptyDescButton, emptyDescKbd, pageListEmptyStyle } from './index.css';
|
import { emptyDescButton, emptyDescKbd, pageListEmptyStyle } from './index.css';
|
||||||
import { usePageHelper } from './utils';
|
import { usePageHelper } from './utils';
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ export type BlockSuitePageListProps = {
|
|||||||
listType: 'all' | 'trash' | 'shared' | 'public';
|
listType: 'all' | 'trash' | 'shared' | 'public';
|
||||||
isPublic?: true;
|
isPublic?: true;
|
||||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||||
view?: View;
|
collection?: Collection;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
@@ -97,7 +95,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
onOpenPage,
|
onOpenPage,
|
||||||
listType,
|
listType,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
view,
|
collection,
|
||||||
}) => {
|
}) => {
|
||||||
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||||
const {
|
const {
|
||||||
@@ -111,6 +109,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
const { createPage, createEdgeless, importFile, isPreferredEdgeless } =
|
const { createPage, createEdgeless, importFile, isPreferredEdgeless } =
|
||||||
usePageHelper(blockSuiteWorkspace);
|
usePageHelper(blockSuiteWorkspace);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const getPageInfo = useGetPageInfoById();
|
||||||
const list = useMemo(
|
const list = useMemo(
|
||||||
() =>
|
() =>
|
||||||
pageMetas
|
pageMetas
|
||||||
@@ -131,16 +130,12 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
if (!filter[listType](pageMeta, pageMetas)) {
|
if (!filter[listType](pageMeta, pageMetas)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!view) {
|
if (!collection) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return filterByFilterList(view.filterList, {
|
return filterPage(collection, pageMeta);
|
||||||
'Is Favourited': !!pageMeta.favorite,
|
|
||||||
Created: pageMeta.createDate,
|
|
||||||
Updated: pageMeta.updatedDate ?? pageMeta.createDate,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
[pageMetas, filterMode, isPreferredEdgeless, listType, view]
|
[pageMetas, filterMode, isPreferredEdgeless, listType, collection]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (listType === 'trash') {
|
if (listType === 'trash') {
|
||||||
@@ -222,9 +217,9 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageList
|
<PageList
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
onCreateNewPage={createPage}
|
onCreateNewPage={createPage}
|
||||||
onCreateNewEdgeless={createEdgeless}
|
onCreateNewEdgeless={createEdgeless}
|
||||||
onImportFile={importFile}
|
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 type { PageMeta } from '@blocksuite/store';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
|
||||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
|
import { useMemo } from 'react';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { pageSettingFamily } from '../../../../atoms';
|
import { ReferencePage } from '../components/reference-page';
|
||||||
import type { FavoriteListProps } from '../index';
|
import type { FavoriteListProps } from '../index';
|
||||||
import EmptyItem from './empty-item';
|
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) => {
|
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
|
||||||
const metas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
const metas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||||
@@ -105,7 +27,7 @@ export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
|
|||||||
<>
|
<>
|
||||||
{favoriteList.map((pageMeta, index) => {
|
{favoriteList.map((pageMeta, index) => {
|
||||||
return (
|
return (
|
||||||
<FavoriteMenuItem
|
<ReferencePage
|
||||||
key={`${pageMeta}-${index}`}
|
key={`${pageMeta}-${index}`}
|
||||||
metaMapping={metaMapping}
|
metaMapping={metaMapping}
|
||||||
pageId={pageMeta.id}
|
pageId={pageMeta.id}
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ import type { AllWorkspace } from '../../../shared';
|
|||||||
export type FavoriteListProps = {
|
export type FavoriteListProps = {
|
||||||
currentWorkspace: AllWorkspace;
|
currentWorkspace: AllWorkspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CollectionsListProps = {
|
||||||
|
currentWorkspace: AllWorkspace;
|
||||||
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
|
|||||||
import { useHistoryAtom } from '../../atoms/history';
|
import { useHistoryAtom } from '../../atoms/history';
|
||||||
import { useAppSetting } from '../../atoms/settings';
|
import { useAppSetting } from '../../atoms/settings';
|
||||||
import type { AllWorkspace } from '../../shared';
|
import type { AllWorkspace } from '../../shared';
|
||||||
|
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||||
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
||||||
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
||||||
|
|
||||||
@@ -225,7 +226,10 @@ export const RootAppSidebar = ({
|
|||||||
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
|
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
|
||||||
</RouteMenuLinkItem>
|
</RouteMenuLinkItem>
|
||||||
))}
|
))}
|
||||||
|
<CategoryDivider label={t['Collections']()} />
|
||||||
|
{blockSuiteWorkspace && (
|
||||||
|
<CollectionsList currentWorkspace={currentWorkspace} />
|
||||||
|
)}
|
||||||
<CategoryDivider label={t['others']()} />
|
<CategoryDivider label={t['others']()} />
|
||||||
<RouteMenuLinkItem
|
<RouteMenuLinkItem
|
||||||
ref={trashDroppable.setNodeRef}
|
ref={trashDroppable.setNodeRef}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { Button } from '@affine/component';
|
|
||||||
import {
|
import {
|
||||||
|
CollectionList,
|
||||||
FilterList,
|
FilterList,
|
||||||
SaveViewButton,
|
SaveCollectionButton,
|
||||||
useAllPageSetting,
|
useAllPageSetting,
|
||||||
ViewList,
|
|
||||||
} from '@affine/component/page-list';
|
} from '@affine/component/page-list';
|
||||||
|
import type { Collection } from '@affine/env/filter';
|
||||||
import type { WorkspaceHeaderProps } from '@affine/env/workspace';
|
import type { WorkspaceHeaderProps } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { SettingsIcon } from '@blocksuite/icons';
|
import { SettingsIcon } from '@blocksuite/icons';
|
||||||
import { RESET } from 'jotai/utils';
|
import { uuidv4 } from '@blocksuite/store';
|
||||||
import type { ReactElement } from 'react';
|
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 { BlockSuiteEditorHeader } from './blocksuite/workspace-header';
|
||||||
import { filterContainerStyle } from './filter-container.css';
|
import { filterContainerStyle } from './filter-container.css';
|
||||||
import { WorkspaceModeFilterTab, WorkspaceTitle } from './pure/workspace-title';
|
import { WorkspaceModeFilterTab, WorkspaceTitle } from './pure/workspace-title';
|
||||||
@@ -23,40 +24,51 @@ export function WorkspaceHeader({
|
|||||||
}: WorkspaceHeaderProps<WorkspaceFlavour>): ReactElement {
|
}: WorkspaceHeaderProps<WorkspaceFlavour>): ReactElement {
|
||||||
const setting = useAllPageSetting();
|
const setting = useAllPageSetting();
|
||||||
const t = useAFFiNEI18N();
|
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 ('subPath' in currentEntry) {
|
||||||
if (currentEntry.subPath === WorkspaceSubPath.ALL) {
|
if (currentEntry.subPath === WorkspaceSubPath.ALL) {
|
||||||
const leftSlot = <ViewList setting={setting}></ViewList>;
|
const leftSlot = (
|
||||||
const filterContainer = setting.currentView.filterList.length > 0 && (
|
<CollectionList
|
||||||
<div className={filterContainerStyle}>
|
setting={setting}
|
||||||
<div style={{ flex: 1 }}>
|
getPageInfo={getPageInfoById}
|
||||||
<FilterList
|
></CollectionList>
|
||||||
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 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<WorkspaceModeFilterTab
|
<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 { AppContainer } from '../components/affine/app-container';
|
||||||
import type { IslandItemNames } from '../components/pure/help-island';
|
import type { IslandItemNames } from '../components/pure/help-island';
|
||||||
import { HelpIsland } from '../components/pure/help-island';
|
import { HelpIsland } from '../components/pure/help-island';
|
||||||
|
import { processCollectionsDrag } from '../components/pure/workspace-slider-bar/collections';
|
||||||
import {
|
import {
|
||||||
DROPPABLE_SIDEBAR_TRASH,
|
DROPPABLE_SIDEBAR_TRASH,
|
||||||
RootAppSidebar,
|
RootAppSidebar,
|
||||||
@@ -393,6 +394,8 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
moveToTrash(pageId);
|
moveToTrash(pageId);
|
||||||
toast(t['Successfully deleted']());
|
toast(t['Successfully deleted']());
|
||||||
}
|
}
|
||||||
|
// Drag page into Collections
|
||||||
|
processCollectionsDrag(e);
|
||||||
},
|
},
|
||||||
[moveToTrash, t]
|
[moveToTrash, t]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const AllPage: NextPageWithLayout = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PageList
|
<PageList
|
||||||
view={setting.currentView}
|
collection={setting.currentCollection}
|
||||||
onOpenPage={onClickPage}
|
onOpenPage={onClickPage}
|
||||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/global": "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/lit": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
|
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const root = style({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '0 8px 0 12px',
|
padding: '0 8px 0 12px',
|
||||||
fontSize: 'var(--affine-font-sm)',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
|
margin: '2px 0',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'var(--affine-hover-color)',
|
background: 'var(--affine-hover-color)',
|
||||||
@@ -22,11 +23,12 @@ export const root = style({
|
|||||||
color: 'var(--affine-text-secondary-color)',
|
color: 'var(--affine-text-secondary-color)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
'&[data-active="true"]:hover': {
|
// this is not visible in dark mode
|
||||||
background:
|
// '&[data-active="true"]:hover': {
|
||||||
// make this a variable?
|
// background:
|
||||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), rgba(0, 0, 0, 0.04);',
|
// // make this a variable?
|
||||||
},
|
// 'linear-gradient(0deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), rgba(0, 0, 0, 0.04)',
|
||||||
|
// },
|
||||||
'&[data-collapsible="true"]': {
|
'&[data-collapsible="true"]': {
|
||||||
width: 'calc(100% + 8px)',
|
width: 'calc(100% + 8px)',
|
||||||
transform: 'translateX(-8px)',
|
transform: 'translateX(-8px)',
|
||||||
@@ -39,6 +41,7 @@ export const content = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
flex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const icon = style({
|
export const icon = style({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
collapsed?: boolean; // true, false, undefined. undefined means no collapse
|
collapsed?: boolean; // true, false, undefined. undefined means no collapse
|
||||||
onCollapsedChange?: (collapsed: boolean) => void;
|
onCollapsedChange?: (collapsed: boolean) => void;
|
||||||
|
postfix?: React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuLinkItemProps
|
export interface MenuLinkItemProps
|
||||||
@@ -28,6 +29,7 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
|||||||
disabled,
|
disabled,
|
||||||
collapsed,
|
collapsed,
|
||||||
onCollapsedChange,
|
onCollapsedChange,
|
||||||
|
postfix,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -43,7 +45,6 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
className={clsx([styles.root, props.className])}
|
className={clsx([styles.root, props.className])}
|
||||||
onClick={onClick}
|
|
||||||
data-active={active}
|
data-active={active}
|
||||||
data-disabled={disabled}
|
data-disabled={disabled}
|
||||||
data-collapsible={collapsible}
|
data-collapsible={collapsible}
|
||||||
@@ -68,11 +69,15 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
|||||||
)}
|
)}
|
||||||
{React.cloneElement(icon, {
|
{React.cloneElement(icon, {
|
||||||
className: clsx([styles.icon, icon.props.className]),
|
className: clsx([styles.icon, icon.props.className]),
|
||||||
|
onClick: onClick,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.content}>{children}</div>
|
<div onClick={onClick} className={styles.content}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{postfix}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import 'fake-indexeddb/auto';
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { RESET } from 'jotai/utils';
|
|
||||||
import { expect, test } from 'vitest';
|
import { expect, test } from 'vitest';
|
||||||
|
|
||||||
import { createDefaultFilter, vars } from '../filter/vars';
|
import { createDefaultFilter, vars } from '../filter/vars';
|
||||||
@@ -12,22 +11,22 @@ import { useAllPageSetting } from '../use-all-page-setting';
|
|||||||
|
|
||||||
test('useAllPageSetting', async () => {
|
test('useAllPageSetting', async () => {
|
||||||
const settingHook = renderHook(() => useAllPageSetting());
|
const settingHook = renderHook(() => useAllPageSetting());
|
||||||
const prevView = settingHook.result.current.currentView;
|
const prevCollection = settingHook.result.current.currentCollection;
|
||||||
expect(settingHook.result.current.savedViews).toEqual([]);
|
expect(settingHook.result.current.savedCollections).toEqual([]);
|
||||||
settingHook.result.current.setCurrentView(view => ({
|
await settingHook.result.current.updateCollection({
|
||||||
...view,
|
...settingHook.result.current.currentCollection,
|
||||||
filterList: [createDefaultFilter(vars[0])],
|
filterList: [createDefaultFilter(vars[0])],
|
||||||
}));
|
});
|
||||||
settingHook.rerender();
|
settingHook.rerender();
|
||||||
const nextView = settingHook.result.current.currentView;
|
const nextCollection = settingHook.result.current.currentCollection;
|
||||||
expect(nextView).not.toBe(prevView);
|
expect(nextCollection).not.toBe(prevCollection);
|
||||||
expect(nextView.filterList).toEqual([createDefaultFilter(vars[0])]);
|
expect(nextCollection.filterList).toEqual([createDefaultFilter(vars[0])]);
|
||||||
settingHook.result.current.setCurrentView(RESET);
|
settingHook.result.current.backToAll();
|
||||||
await settingHook.result.current.createView({
|
await settingHook.result.current.saveCollection({
|
||||||
...settingHook.result.current.currentView,
|
...settingHook.result.current.currentCollection,
|
||||||
id: '1',
|
id: '1',
|
||||||
});
|
});
|
||||||
settingHook.rerender();
|
settingHook.rerender();
|
||||||
expect(settingHook.result.current.savedViews.length).toBe(1);
|
expect(settingHook.result.current.savedCollections.length).toBe(1);
|
||||||
expect(settingHook.result.current.savedViews[0].id).toBe('1');
|
expect(settingHook.result.current.savedCollections[0].id).toBe('1');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { CollectionBar } from '@affine/component/page-list';
|
||||||
import { DEFAULT_SORT_KEY } from '@affine/env/constant';
|
import { DEFAULT_SORT_KEY } from '@affine/env/constant';
|
||||||
|
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
|
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
|
||||||
import { useMediaQuery, useTheme } from '@mui/material';
|
import { useMediaQuery, useTheme } from '@mui/material';
|
||||||
@@ -31,12 +33,14 @@ const AllPagesHead = ({
|
|||||||
createNewPage,
|
createNewPage,
|
||||||
createNewEdgeless,
|
createNewEdgeless,
|
||||||
importFile,
|
importFile,
|
||||||
|
getPageInfo,
|
||||||
}: {
|
}: {
|
||||||
isPublicWorkspace: boolean;
|
isPublicWorkspace: boolean;
|
||||||
sorter: ReturnType<typeof useSorter<ListData>>;
|
sorter: ReturnType<typeof useSorter<ListData>>;
|
||||||
createNewPage: () => void;
|
createNewPage: () => void;
|
||||||
createNewEdgeless: () => void;
|
createNewEdgeless: () => void;
|
||||||
importFile: () => void;
|
importFile: () => void;
|
||||||
|
getPageInfo: GetPageInfoById;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const titleList = [
|
const titleList = [
|
||||||
@@ -72,7 +76,6 @@ const AllPagesHead = ({
|
|||||||
} satisfies CSSProperties,
|
} satisfies CSSProperties,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableHeadRow>
|
<TableHeadRow>
|
||||||
@@ -107,6 +110,7 @@ const AllPagesHead = ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableHeadRow>
|
</TableHeadRow>
|
||||||
|
<CollectionBar getPageInfo={getPageInfo} />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -118,6 +122,7 @@ export const PageList = ({
|
|||||||
onCreateNewEdgeless,
|
onCreateNewEdgeless,
|
||||||
onImportFile,
|
onImportFile,
|
||||||
fallback,
|
fallback,
|
||||||
|
getPageInfo,
|
||||||
}: PageListProps) => {
|
}: PageListProps) => {
|
||||||
const sorter = useSorter<ListData>({
|
const sorter = useSorter<ListData>({
|
||||||
data: list,
|
data: list,
|
||||||
@@ -160,6 +165,7 @@ export const PageList = ({
|
|||||||
createNewPage={onCreateNewPage}
|
createNewPage={onCreateNewPage}
|
||||||
createNewEdgeless={onCreateNewEdgeless}
|
createNewEdgeless={onCreateNewEdgeless}
|
||||||
importFile={onImportFile}
|
importFile={onImportFile}
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
/>
|
/>
|
||||||
<AllPagesBody
|
<AllPagesBody
|
||||||
isPublicWorkspace={isPublicWorkspace}
|
isPublicWorkspace={isPublicWorkspace}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Menu } from '../../..';
|
|||||||
import { Condition } from './condition';
|
import { Condition } from './condition';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import { CreateFilterMenu } from './vars';
|
import { CreateFilterMenu } from './vars';
|
||||||
|
|
||||||
export const FilterList = ({
|
export const FilterList = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -13,7 +14,13 @@ export const FilterList = ({
|
|||||||
onChange: (value: Filter[]) => void;
|
onChange: (value: Filter[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{value.map((filter, i) => {
|
{value.map((filter, i) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.filterItemStyle} key={i}>
|
<div className={styles.filterItemStyle} key={i}>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export const filterItemStyle = style({
|
|||||||
border: '1px solid var(--affine-border-color)',
|
border: '1px solid var(--affine-border-color)',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
background: 'var(--affine-white)',
|
background: 'var(--affine-white)',
|
||||||
margin: '4px',
|
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the keys of an object type whose values are of a given type
|
* Get the keys of an object type whose values are of a given type
|
||||||
*
|
*
|
||||||
@@ -45,6 +47,7 @@ export type PageListProps = {
|
|||||||
onCreateNewPage: () => void;
|
onCreateNewPage: () => void;
|
||||||
onCreateNewEdgeless: () => void;
|
onCreateNewEdgeless: () => void;
|
||||||
onImportFile: () => void;
|
onImportFile: () => void;
|
||||||
|
getPageInfo: GetPageInfoById;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DraggableTitleCellData = {
|
export type DraggableTitleCellData = {
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import type { Filter, VariableMap, View } from '@affine/env/filter';
|
import type { Collection, Filter, VariableMap } from '@affine/env/filter';
|
||||||
import type { DBSchema } from 'idb';
|
import type { DBSchema } from 'idb';
|
||||||
import { openDB } from 'idb';
|
import { openDB } from 'idb';
|
||||||
import type { IDBPDatabase } from 'idb/build/entry';
|
import type { IDBPDatabase } from 'idb/build/entry';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { atomWithReset } from 'jotai/utils';
|
import { atomWithReset, RESET } from 'jotai/utils';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import useSWRImmutable from 'swr/immutable';
|
import useSWRImmutable from 'swr/immutable';
|
||||||
import { NIL } from 'uuid';
|
import { NIL } from 'uuid';
|
||||||
|
|
||||||
import { evalFilterList } from './filter';
|
import { evalFilterList } from './filter';
|
||||||
|
|
||||||
type PersistenceView = View;
|
type PersistenceCollection = Collection;
|
||||||
|
|
||||||
export interface PageViewDBV1 extends DBSchema {
|
export interface PageCollectionDBV1 extends DBSchema {
|
||||||
view: {
|
view: {
|
||||||
key: PersistenceView['id'];
|
key: PersistenceCollection['id'];
|
||||||
value: PersistenceView;
|
value: PersistenceCollection;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageViewDBPromise: Promise<IDBPDatabase<PageViewDBV1>> =
|
const pageCollectionDBPromise: Promise<IDBPDatabase<PageCollectionDBV1>> =
|
||||||
typeof window === 'undefined'
|
typeof window === 'undefined'
|
||||||
? // never resolve in SSR
|
? // never resolve in SSR
|
||||||
new Promise<any>(() => {})
|
new Promise<any>(() => {})
|
||||||
: openDB<PageViewDBV1>('page-view', 1, {
|
: openDB<PageCollectionDBV1>('page-view', 1, {
|
||||||
upgrade(database) {
|
upgrade(database) {
|
||||||
database.createObjectStore('view', {
|
database.createObjectStore('view', {
|
||||||
keyPath: 'id',
|
keyPath: 'id',
|
||||||
@@ -31,18 +31,24 @@ const pageViewDBPromise: Promise<IDBPDatabase<PageViewDBV1>> =
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentViewAtom = atomWithReset<View>({
|
const collectionAtom = atomWithReset<{
|
||||||
name: 'default',
|
currentId: string;
|
||||||
id: NIL,
|
defaultCollection: Collection;
|
||||||
filterList: [],
|
}>({
|
||||||
|
currentId: NIL,
|
||||||
|
defaultCollection: {
|
||||||
|
id: NIL,
|
||||||
|
name: 'All',
|
||||||
|
filterList: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAllPageSetting = () => {
|
export const useSavedCollections = () => {
|
||||||
const { data: savedViews, mutate } = useSWRImmutable(
|
const { data: savedCollections, mutate } = useSWRImmutable<Collection[]>(
|
||||||
['affine', 'page-view'],
|
['affine', 'page-collection'],
|
||||||
{
|
{
|
||||||
fetcher: async () => {
|
fetcher: async () => {
|
||||||
const db = await pageViewDBPromise;
|
const db = await pageCollectionDBPromise;
|
||||||
const t = db.transaction('view').objectStore('view');
|
const t = db.transaction('view').objectStore('view');
|
||||||
return await t.getAll();
|
return await t.getAll();
|
||||||
},
|
},
|
||||||
@@ -51,29 +57,98 @@ export const useAllPageSetting = () => {
|
|||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const saveCollection = useCallback(
|
||||||
const [currentView, setCurrentView] = useAtom(currentViewAtom);
|
async (collection: Collection) => {
|
||||||
|
if (collection.id === NIL) {
|
||||||
const createView = useCallback(
|
|
||||||
async (view: View) => {
|
|
||||||
if (view.id === NIL) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const db = await pageViewDBPromise;
|
const db = await pageCollectionDBPromise;
|
||||||
const t = db.transaction('view', 'readwrite').objectStore('view');
|
const t = db.transaction('view', 'readwrite').objectStore('view');
|
||||||
await t.put(view);
|
await t.put(collection);
|
||||||
await mutate();
|
await mutate();
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate]
|
||||||
);
|
);
|
||||||
|
const deleteCollection = useCallback(
|
||||||
|
async (id: string) => {
|
||||||
|
if (id === NIL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const db = await pageCollectionDBPromise;
|
||||||
|
const t = db.transaction('view', 'readwrite').objectStore('view');
|
||||||
|
await t.delete(id);
|
||||||
|
await mutate();
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
const addPage = useCallback(
|
||||||
|
async (collectionId: string, pageId: string) => {
|
||||||
|
const collection = savedCollections?.find(v => v.id === collectionId);
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await saveCollection({
|
||||||
|
...collection,
|
||||||
|
allowList: [pageId, ...(collection.allowList ?? [])],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[saveCollection, savedCollections]
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
currentView,
|
savedCollections: savedCollections ?? [],
|
||||||
savedViews: savedViews as View[],
|
saveCollection,
|
||||||
|
deleteCollection,
|
||||||
|
addPage,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAllPageSetting = () => {
|
||||||
|
const { savedCollections, saveCollection, deleteCollection, addPage } =
|
||||||
|
useSavedCollections();
|
||||||
|
const [collectionData, setCollectionData] = useAtom(collectionAtom);
|
||||||
|
|
||||||
|
const updateCollection = useCallback(
|
||||||
|
async (collection: Collection) => {
|
||||||
|
if (collection.id === NIL) {
|
||||||
|
setCollectionData({
|
||||||
|
...collectionData,
|
||||||
|
defaultCollection: collection,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await saveCollection(collection);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[collectionData, saveCollection, setCollectionData]
|
||||||
|
);
|
||||||
|
const selectCollection = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
setCollectionData({
|
||||||
|
...collectionData,
|
||||||
|
currentId: id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[collectionData, setCollectionData]
|
||||||
|
);
|
||||||
|
const backToAll = useCallback(() => {
|
||||||
|
setCollectionData(RESET);
|
||||||
|
}, [setCollectionData]);
|
||||||
|
const currentCollection =
|
||||||
|
collectionData.currentId === NIL
|
||||||
|
? collectionData.defaultCollection
|
||||||
|
: savedCollections.find(v => v.id === collectionData.currentId) ??
|
||||||
|
collectionData.defaultCollection;
|
||||||
|
return {
|
||||||
|
currentCollection: currentCollection,
|
||||||
|
savedCollections,
|
||||||
|
isDefault: currentCollection.id === NIL,
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
createView,
|
saveCollection,
|
||||||
setCurrentView,
|
updateCollection,
|
||||||
|
selectCollection,
|
||||||
|
backToAll,
|
||||||
|
deleteCollection,
|
||||||
|
addPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const view = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
height: '100%',
|
||||||
|
paddingLeft: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const option = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 4,
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
opacity: 0,
|
||||||
|
selectors: {
|
||||||
|
[`${view}:hover &`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const pin = style({
|
||||||
|
opacity: 1,
|
||||||
|
});
|
||||||
|
export const pinedIcon = style({
|
||||||
|
display: 'block',
|
||||||
|
selectors: {
|
||||||
|
[`${option}:hover &`]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const pinIcon = style({
|
||||||
|
display: 'none',
|
||||||
|
selectors: {
|
||||||
|
[`${option}:hover &`]: {
|
||||||
|
display: 'block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { EditCollectionModel } from '@affine/component/page-list';
|
||||||
|
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||||
|
import {
|
||||||
|
DeleteIcon,
|
||||||
|
FilterIcon,
|
||||||
|
PinedIcon,
|
||||||
|
PinIcon,
|
||||||
|
UnpinIcon,
|
||||||
|
ViewLayersIcon,
|
||||||
|
} from '@blocksuite/icons';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '../../../ui/button/button';
|
||||||
|
import { useAllPageSetting } from '../use-all-page-setting';
|
||||||
|
import * as styles from './collection-bar.css';
|
||||||
|
|
||||||
|
export const CollectionBar = ({
|
||||||
|
getPageInfo,
|
||||||
|
}: {
|
||||||
|
getPageInfo: GetPageInfoById;
|
||||||
|
}) => {
|
||||||
|
const setting = useAllPageSetting();
|
||||||
|
const collection = setting.currentCollection;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const actions: {
|
||||||
|
icon: ReactNode;
|
||||||
|
click: () => void;
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
}[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<>
|
||||||
|
{collection.pinned ? (
|
||||||
|
<PinedIcon className={styles.pinedIcon}></PinedIcon>
|
||||||
|
) : (
|
||||||
|
<PinIcon className={styles.pinedIcon}></PinIcon>
|
||||||
|
)}
|
||||||
|
{collection.pinned ? (
|
||||||
|
<UnpinIcon className={styles.pinIcon}></UnpinIcon>
|
||||||
|
) : (
|
||||||
|
<PinIcon className={styles.pinIcon}></PinIcon>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
name: 'pin',
|
||||||
|
className: styles.pin,
|
||||||
|
click: () => {
|
||||||
|
return setting.updateCollection({
|
||||||
|
...collection,
|
||||||
|
pinned: !collection.pinned,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FilterIcon />,
|
||||||
|
name: 'edit',
|
||||||
|
click: () => {
|
||||||
|
setOpen(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <DeleteIcon style={{ color: 'red' }} />,
|
||||||
|
name: 'delete',
|
||||||
|
click: () => {
|
||||||
|
setting.deleteCollection(collection.id).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[setting, collection]
|
||||||
|
);
|
||||||
|
const onClose = useCallback(() => setOpen(false), []);
|
||||||
|
return !setting.isDefault ? (
|
||||||
|
<tr style={{ userSelect: 'none' }}>
|
||||||
|
<td>
|
||||||
|
<div className={styles.view}>
|
||||||
|
<EditCollectionModel
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
|
init={collection}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
onConfirm={setting.updateCollection}
|
||||||
|
></EditCollectionModel>
|
||||||
|
<ViewLayersIcon
|
||||||
|
style={{
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ marginRight: 10 }}>
|
||||||
|
{setting.currentCollection.name}
|
||||||
|
</div>
|
||||||
|
{actions.map(action => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={action.name}
|
||||||
|
data-testid={`collection-bar-option-${action.name}`}
|
||||||
|
onClick={action.click}
|
||||||
|
className={clsx(styles.option, action.className)}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button style={{ border: 'none' }} onClick={() => setting.backToAll()}>
|
||||||
|
Back to all
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const menuTitleStyle = style({
|
||||||
|
marginLeft: '12px',
|
||||||
|
marginTop: '10px',
|
||||||
|
fontSize: 'var(--affine-font-xs)',
|
||||||
|
color: 'var(--affine-text-secondary-color)',
|
||||||
|
});
|
||||||
|
export const menuDividerStyle = style({
|
||||||
|
marginTop: '2px',
|
||||||
|
marginBottom: '2px',
|
||||||
|
marginLeft: '12px',
|
||||||
|
marginRight: '8px',
|
||||||
|
height: '1px',
|
||||||
|
background: 'var(--affine-border-color)',
|
||||||
|
});
|
||||||
|
export const viewButton = style({
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '100%',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: 'var(--affine-font-xs)',
|
||||||
|
background: 'var(--affine-white)',
|
||||||
|
color: 'var(--affine-text-secondary-color)',
|
||||||
|
border: '1px solid var(--affine-border-color)',
|
||||||
|
transition: 'margin-left 0.2s ease-in-out',
|
||||||
|
':hover': {
|
||||||
|
borderColor: 'var(--affine-border-color)',
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
marginRight: '20px',
|
||||||
|
});
|
||||||
|
export const viewMenu = style({});
|
||||||
|
export const viewOption = style({
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginLeft: 6,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
opacity: 0,
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
[`${viewMenu}:hover &`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const deleteOption = style({
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: '#FFEFE9',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const filterButton = style({
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '100%',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: 'var(--affine-font-xs)',
|
||||||
|
background: 'var(--affine-white)',
|
||||||
|
color: 'var(--affine-text-secondary-color)',
|
||||||
|
border: '1px solid var(--affine-border-color)',
|
||||||
|
transition: 'margin-left 0.2s ease-in-out',
|
||||||
|
':hover': {
|
||||||
|
borderColor: 'var(--affine-border-color)',
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const filterButtonCollapse = style({
|
||||||
|
marginLeft: '20px',
|
||||||
|
});
|
||||||
|
export const viewDivider = style({
|
||||||
|
'::after': {
|
||||||
|
content: '""',
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
height: '1px',
|
||||||
|
background: 'var(--affine-border-color)',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
margin: '0 1px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const saveButton = style({
|
||||||
|
marginTop: '4px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px 0',
|
||||||
|
':hover': {
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
border: '1px solid var(--affine-border-color)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const saveButtonContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
padding: '8px',
|
||||||
|
});
|
||||||
|
export const saveIcon = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 'var(--affine-font-sm)',
|
||||||
|
marginRight: '8px',
|
||||||
|
});
|
||||||
|
export const saveText = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: 'var(--affine-font-sm)',
|
||||||
|
});
|
||||||
|
export const cancelButton = style({
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
':hover': {
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
border: '1px solid var(--affine-border-color)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const saveTitle = style({
|
||||||
|
fontSize: 'var(--affine-font-h-6)',
|
||||||
|
fontWeight: '600',
|
||||||
|
lineHeight: '24px',
|
||||||
|
paddingBottom: 20,
|
||||||
|
});
|
||||||
|
export const allowList = style({});
|
||||||
|
|
||||||
|
export const allowTitle = style({
|
||||||
|
fontSize: 12,
|
||||||
|
margin: '20px 0',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const allowListContent = style({
|
||||||
|
margin: '8px 0',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const excludeList = style({
|
||||||
|
backgroundColor: 'var(--affine-background-warning-color)',
|
||||||
|
padding: 18,
|
||||||
|
borderRadius: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const excludeListContent = style({
|
||||||
|
margin: '8px 0',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const filterTitle = style({
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
marginBottom: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const excludeTitle = style({
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const excludeTip = style({
|
||||||
|
color: 'var(--affine-text-secondary-color)',
|
||||||
|
fontSize: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const scrollContainer = style({
|
||||||
|
overflow: 'hidden',
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
export const container = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
export const pageContainer = style({
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
height: 32,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 8,
|
||||||
|
paddingRight: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const pageIcon = style({
|
||||||
|
marginRight: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const pageTitle = style({
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
export const deleteIcon = style({
|
||||||
|
marginLeft: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { EditCollectionModel } from '@affine/component/page-list';
|
||||||
|
import type { Collection, Filter } from '@affine/env/filter';
|
||||||
|
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import {
|
||||||
|
DeleteIcon,
|
||||||
|
FilteredIcon,
|
||||||
|
FilterIcon,
|
||||||
|
FolderIcon,
|
||||||
|
PinIcon,
|
||||||
|
ViewLayersIcon,
|
||||||
|
} from '@blocksuite/icons';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import type { MouseEvent, ReactNode } from 'react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button, MenuItem } from '../../..';
|
||||||
|
import Menu from '../../../ui/menu/menu';
|
||||||
|
import { appSidebarOpenAtom } from '../../app-sidebar';
|
||||||
|
import { CreateFilterMenu } from '../filter/vars';
|
||||||
|
import type { useAllPageSetting } from '../use-all-page-setting';
|
||||||
|
import * as styles from './collection-list.css';
|
||||||
|
|
||||||
|
const CollectionOption = ({
|
||||||
|
collection,
|
||||||
|
setting,
|
||||||
|
updateCollection,
|
||||||
|
}: {
|
||||||
|
collection: Collection;
|
||||||
|
setting: ReturnType<typeof useAllPageSetting>;
|
||||||
|
updateCollection: (view: Collection) => void;
|
||||||
|
}) => {
|
||||||
|
const actions: {
|
||||||
|
icon: ReactNode;
|
||||||
|
click: () => void;
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
}[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
icon: <PinIcon />,
|
||||||
|
name: 'pin',
|
||||||
|
click: () => {
|
||||||
|
return setting.updateCollection({
|
||||||
|
...collection,
|
||||||
|
pinned: !collection.pinned,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FilterIcon />,
|
||||||
|
name: 'edit',
|
||||||
|
click: () => {
|
||||||
|
updateCollection(collection);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <DeleteIcon style={{ color: 'red' }} />,
|
||||||
|
name: 'delete',
|
||||||
|
click: () => {
|
||||||
|
setting.deleteCollection(collection.id).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[setting, updateCollection, collection]
|
||||||
|
);
|
||||||
|
const selectCollection = useCallback(
|
||||||
|
() => setting.selectCollection(collection.id),
|
||||||
|
[setting, collection.id]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
data-testid="collection-select-option"
|
||||||
|
icon={<ViewLayersIcon></ViewLayersIcon>}
|
||||||
|
onClick={selectCollection}
|
||||||
|
key={collection.id}
|
||||||
|
className={styles.viewMenu}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{collection.name}</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actions.map((v, i) => {
|
||||||
|
const onClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
v.click();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid={`collection-select-option-${v.name}`}
|
||||||
|
key={i}
|
||||||
|
onClick={onClick}
|
||||||
|
style={{ marginLeft: i === 0 ? 28 : undefined }}
|
||||||
|
className={clsx(styles.viewOption, v.className)}
|
||||||
|
>
|
||||||
|
{v.icon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const CollectionList = ({
|
||||||
|
setting,
|
||||||
|
getPageInfo,
|
||||||
|
}: {
|
||||||
|
setting: ReturnType<typeof useAllPageSetting>;
|
||||||
|
getPageInfo: GetPageInfoById;
|
||||||
|
}) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const [open] = useAtom(appSidebarOpenAtom);
|
||||||
|
const [collection, setCollection] = useState<Collection>();
|
||||||
|
const onChange = useCallback(
|
||||||
|
(filterList: Filter[]) => {
|
||||||
|
return setting.updateCollection({
|
||||||
|
...setting.currentCollection,
|
||||||
|
filterList,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setting]
|
||||||
|
);
|
||||||
|
const closeUpdateCollectionModal = useCallback(
|
||||||
|
() => setCollection(undefined),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const onConfirm = useCallback(
|
||||||
|
(view: Collection) => {
|
||||||
|
return setting.updateCollection(view).then(() => {
|
||||||
|
closeUpdateCollectionModal();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[closeUpdateCollectionModal, setting]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx({
|
||||||
|
[styles.filterButtonCollapse]: !open,
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
marginLeft: 4,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{setting.savedCollections.length > 0 && (
|
||||||
|
<Menu
|
||||||
|
trigger="click"
|
||||||
|
content={
|
||||||
|
<div style={{ minWidth: 150 }}>
|
||||||
|
<MenuItem
|
||||||
|
icon={<FolderIcon></FolderIcon>}
|
||||||
|
onClick={setting.backToAll}
|
||||||
|
className={styles.viewMenu}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>All</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
<div className={styles.menuTitleStyle}>Saved Collection</div>
|
||||||
|
<div className={styles.menuDividerStyle}></div>
|
||||||
|
{setting.savedCollections.map(view => (
|
||||||
|
<CollectionOption
|
||||||
|
key={view.id}
|
||||||
|
collection={view}
|
||||||
|
setting={setting}
|
||||||
|
updateCollection={setCollection}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className={clsx(styles.viewButton)}
|
||||||
|
hoverColor="var(--affine-icon-color)"
|
||||||
|
data-testid="collection-select"
|
||||||
|
>
|
||||||
|
{setting.currentCollection.name}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
<Menu
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom-start"
|
||||||
|
content={
|
||||||
|
<CreateFilterMenu
|
||||||
|
value={setting.currentCollection.filterList}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<FilteredIcon />}
|
||||||
|
className={clsx(styles.filterButton)}
|
||||||
|
size="small"
|
||||||
|
hoverColor="var(--affine-icon-color)"
|
||||||
|
data-testid="create-first-filter"
|
||||||
|
>
|
||||||
|
{t['com.affine.filter']()}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
<EditCollectionModel
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
|
init={collection}
|
||||||
|
open={!!collection}
|
||||||
|
onClose={closeUpdateCollectionModal}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
></EditCollectionModel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import type { Collection } from '@affine/env/filter';
|
||||||
|
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||||
|
import {
|
||||||
|
EdgelessIcon,
|
||||||
|
PageIcon,
|
||||||
|
RemoveIcon,
|
||||||
|
SaveIcon,
|
||||||
|
} from '@blocksuite/icons';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalWrapper,
|
||||||
|
ScrollableContainer,
|
||||||
|
} from '../../..';
|
||||||
|
import { FilterList } from '../filter';
|
||||||
|
import * as styles from './collection-list.css';
|
||||||
|
|
||||||
|
type CreateCollectionProps = {
|
||||||
|
title?: string;
|
||||||
|
init: Collection;
|
||||||
|
onConfirm: (collection: Collection) => void;
|
||||||
|
onConfirmText?: string;
|
||||||
|
getPageInfo: GetPageInfoById;
|
||||||
|
};
|
||||||
|
export const EditCollectionModel = ({
|
||||||
|
init,
|
||||||
|
onConfirm,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
getPageInfo,
|
||||||
|
}: {
|
||||||
|
init?: Collection;
|
||||||
|
onConfirm: (view: Collection) => void;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
getPageInfo: GetPageInfoById;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<ModalWrapper
|
||||||
|
width={600}
|
||||||
|
style={{
|
||||||
|
padding: '40px',
|
||||||
|
background: 'var(--affine-background-primary-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalCloseButton
|
||||||
|
top={12}
|
||||||
|
right={12}
|
||||||
|
onClick={onClose}
|
||||||
|
hoverColor="var(--affine-icon-color)"
|
||||||
|
/>
|
||||||
|
{init ? (
|
||||||
|
<EditCollection
|
||||||
|
title="Update Collection"
|
||||||
|
onConfirmText="Save"
|
||||||
|
init={init}
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
|
onCancel={onClose}
|
||||||
|
onConfirm={view => {
|
||||||
|
onConfirm(view);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</ModalWrapper>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Page = ({
|
||||||
|
id,
|
||||||
|
onClick,
|
||||||
|
getPageInfo,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
onClick: (id: string) => void;
|
||||||
|
getPageInfo: GetPageInfoById;
|
||||||
|
}) => {
|
||||||
|
const page = getPageInfo(id);
|
||||||
|
if (!page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const icon = page.isEdgeless ? (
|
||||||
|
<EdgelessIcon
|
||||||
|
style={{
|
||||||
|
width: 17.5,
|
||||||
|
height: 17.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PageIcon
|
||||||
|
style={{
|
||||||
|
width: 17.5,
|
||||||
|
height: 17.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const click = () => {
|
||||||
|
onClick(id);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={styles.pageContainer}>
|
||||||
|
<div className={styles.pageIcon}>{icon}</div>
|
||||||
|
<div className={styles.pageTitle}>{page.title}</div>
|
||||||
|
<div onClick={click} className={styles.deleteIcon}>
|
||||||
|
<RemoveIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const EditCollection = ({
|
||||||
|
title,
|
||||||
|
init,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
onConfirmText,
|
||||||
|
getPageInfo,
|
||||||
|
}: CreateCollectionProps & {
|
||||||
|
onCancel: () => void;
|
||||||
|
}) => {
|
||||||
|
const [value, onChange] = useState<Collection>(init);
|
||||||
|
const removeFromExcludeList = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
excludeList: value.excludeList?.filter(v => v !== id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
const removeFromAllowList = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
allowList: value.allowList?.filter(v => v !== id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: '90vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.saveTitle}>
|
||||||
|
{title ?? 'Save As New Collection'}
|
||||||
|
</div>
|
||||||
|
<ScrollableContainer
|
||||||
|
className={styles.scrollContainer}
|
||||||
|
viewPortClassName={styles.container}
|
||||||
|
>
|
||||||
|
<div className={styles.excludeList}>
|
||||||
|
<div className={styles.excludeTitle}>
|
||||||
|
Exclude from this collection
|
||||||
|
</div>
|
||||||
|
{value.excludeList ? (
|
||||||
|
<div className={styles.excludeListContent}>
|
||||||
|
{value.excludeList.map(id => {
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
id={id}
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
|
key={id}
|
||||||
|
onClick={removeFromExcludeList}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.excludeTip}>
|
||||||
|
These pages will never appear in the current collection
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 18,
|
||||||
|
marginTop: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.filterTitle}>Filters</div>
|
||||||
|
<FilterList
|
||||||
|
value={value.filterList}
|
||||||
|
onChange={list =>
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
filterList: list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></FilterList>
|
||||||
|
{value.allowList ? (
|
||||||
|
<div className={styles.allowList}>
|
||||||
|
<div className={styles.allowTitle}>With follow pages:</div>
|
||||||
|
<div className={styles.allowListContent}>
|
||||||
|
{value.allowList.map(id => {
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
|
onClick={removeFromAllowList}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<Input
|
||||||
|
data-testid="input-collection-title"
|
||||||
|
placeholder="Untitled Collection"
|
||||||
|
value={value.name}
|
||||||
|
onChange={text =>
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
name: text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScrollableContainer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: 40,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button className={styles.cancelButton} onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
marginLeft: 20,
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
data-testid="save-collection"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (value.name.trim().length > 0) {
|
||||||
|
onConfirm(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{onConfirmText ?? 'Create'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const SaveCollectionButton = ({
|
||||||
|
init,
|
||||||
|
onConfirm,
|
||||||
|
getPageInfo,
|
||||||
|
}: CreateCollectionProps) => {
|
||||||
|
const [show, changeShow] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className={styles.saveButton}
|
||||||
|
onClick={() => changeShow(true)}
|
||||||
|
size="middle"
|
||||||
|
data-testid="save-as-collection"
|
||||||
|
>
|
||||||
|
<div className={styles.saveButtonContainer}>
|
||||||
|
<div className={styles.saveIcon}>
|
||||||
|
<SaveIcon />
|
||||||
|
</div>
|
||||||
|
<div className={styles.saveText}>Save As Collection</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<EditCollectionModel
|
||||||
|
init={init}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
open={show}
|
||||||
|
getPageInfo={getPageInfo}
|
||||||
|
onClose={() => changeShow(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import type { Filter, View } from '@affine/env/filter';
|
|
||||||
import { SaveIcon } from '@blocksuite/icons';
|
|
||||||
import { uuidv4 } from '@blocksuite/store';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Button, Input, Modal, ModalCloseButton, ModalWrapper } from '../../..';
|
|
||||||
import { FilterList } from '../filter';
|
|
||||||
import * as styles from './view-list.css';
|
|
||||||
|
|
||||||
type CreateViewProps = {
|
|
||||||
init: Filter[];
|
|
||||||
onConfirm: (view: View) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateView = ({
|
|
||||||
init,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
}: CreateViewProps & { onCancel: () => void }) => {
|
|
||||||
const [value, onChange] = useState<View>({
|
|
||||||
name: '',
|
|
||||||
filterList: init,
|
|
||||||
id: uuidv4(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.saveTitle}>Save As New View</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'var(--affine-hover-color)',
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 20,
|
|
||||||
marginTop: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FilterList
|
|
||||||
value={value.filterList}
|
|
||||||
onChange={list => onChange({ ...value, filterList: list })}
|
|
||||||
></FilterList>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: 20 }}>
|
|
||||||
<Input
|
|
||||||
placeholder="Untitled View"
|
|
||||||
value={value.name}
|
|
||||||
onChange={text => onChange({ ...value, name: text })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 40 }}
|
|
||||||
>
|
|
||||||
<Button className={styles.cancelButton} onClick={onCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
style={{ marginLeft: 20, borderRadius: '8px' }}
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
if (value.name.trim().length > 0) {
|
|
||||||
onConfirm(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const SaveViewButton = ({ init, onConfirm }: CreateViewProps) => {
|
|
||||||
const [show, changeShow] = useState(false);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
className={styles.saveButton}
|
|
||||||
onClick={() => changeShow(true)}
|
|
||||||
size="middle"
|
|
||||||
>
|
|
||||||
<div className={styles.saveButtonContainer}>
|
|
||||||
<div className={styles.saveIcon}>
|
|
||||||
<SaveIcon />
|
|
||||||
</div>
|
|
||||||
<div className={styles.saveText}>Save View</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<Modal open={show} onClose={() => changeShow(false)}>
|
|
||||||
<ModalWrapper
|
|
||||||
width={560}
|
|
||||||
style={{
|
|
||||||
padding: '40px',
|
|
||||||
background: 'var(--affine-background-primary-color)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalCloseButton
|
|
||||||
top={12}
|
|
||||||
right={12}
|
|
||||||
onClick={() => changeShow(false)}
|
|
||||||
hoverColor="var(--affine-icon-color)"
|
|
||||||
/>
|
|
||||||
<CreateView
|
|
||||||
init={init}
|
|
||||||
onCancel={() => changeShow(false)}
|
|
||||||
onConfirm={view => {
|
|
||||||
onConfirm(view);
|
|
||||||
changeShow(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ModalWrapper>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './create-view';
|
export * from './collection-bar';
|
||||||
export * from './view-list';
|
export * from './collection-list';
|
||||||
|
export * from './create-collection';
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
|
|
||||||
export const filterButton = style({
|
|
||||||
borderRadius: '8px',
|
|
||||||
height: '100%',
|
|
||||||
padding: '4px 8px',
|
|
||||||
fontSize: 'var(--affine-font-xs)',
|
|
||||||
background: 'var(--affine-white)',
|
|
||||||
color: 'var(--affine-text-secondary-color)',
|
|
||||||
border: '1px solid var(--affine-border-color)',
|
|
||||||
transition: 'margin-left 0.2s ease-in-out',
|
|
||||||
':hover': {
|
|
||||||
borderColor: 'var(--affine-border-color)',
|
|
||||||
background: 'var(--affine-hover-color)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const filterButtonCollapse = style({
|
|
||||||
marginLeft: '20px',
|
|
||||||
});
|
|
||||||
export const viewDivider = style({
|
|
||||||
'::after': {
|
|
||||||
content: '""',
|
|
||||||
display: 'block',
|
|
||||||
width: '100%',
|
|
||||||
height: '1px',
|
|
||||||
background: 'var(--affine-border-color)',
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
margin: '0 1px',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const saveButton = style({
|
|
||||||
marginTop: '4px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '8px 0',
|
|
||||||
':hover': {
|
|
||||||
background: 'var(--affine-hover-color)',
|
|
||||||
color: 'var(--affine-text-primary-color)',
|
|
||||||
border: '1px solid var(--affine-border-color)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const saveButtonContainer = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
padding: '8px',
|
|
||||||
});
|
|
||||||
export const saveIcon = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
fontSize: 'var(--affine-font-sm)',
|
|
||||||
marginRight: '8px',
|
|
||||||
});
|
|
||||||
export const saveText = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: 'var(--affine-font-sm)',
|
|
||||||
});
|
|
||||||
export const cancelButton = style({
|
|
||||||
background: 'var(--affine-hover-color)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
':hover': {
|
|
||||||
background: 'var(--affine-hover-color)',
|
|
||||||
color: 'var(--affine-text-primary-color)',
|
|
||||||
border: '1px solid var(--affine-border-color)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export const saveTitle = style({
|
|
||||||
fontSize: 'var(--affine-font-h-6)',
|
|
||||||
fontWeight: '600',
|
|
||||||
lineHeight: '24px',
|
|
||||||
});
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
import { FilteredIcon } from '@blocksuite/icons';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
|
|
||||||
import { Button, MenuItem } from '../../..';
|
|
||||||
import Menu from '../../../ui/menu/menu';
|
|
||||||
import { appSidebarOpenAtom } from '../../app-sidebar';
|
|
||||||
import { CreateFilterMenu } from '../filter/vars';
|
|
||||||
import type { useAllPageSetting } from '../use-all-page-setting';
|
|
||||||
import * as styles from './view-list.css';
|
|
||||||
export const ViewList = ({
|
|
||||||
setting,
|
|
||||||
}: {
|
|
||||||
setting: ReturnType<typeof useAllPageSetting>;
|
|
||||||
}) => {
|
|
||||||
const [open] = useAtom(appSidebarOpenAtom);
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
return (
|
|
||||||
<div style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>
|
|
||||||
{setting.savedViews.length > 0 && (
|
|
||||||
<Menu
|
|
||||||
trigger="click"
|
|
||||||
content={
|
|
||||||
<div>
|
|
||||||
{setting.savedViews.map(view => {
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => setting.setCurrentView(view)}
|
|
||||||
key={view.id}
|
|
||||||
>
|
|
||||||
{view.name}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button style={{ marginRight: 12, cursor: 'pointer' }}>
|
|
||||||
{setting.currentView.name}
|
|
||||||
</Button>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
<Menu
|
|
||||||
trigger="click"
|
|
||||||
placement="bottom-start"
|
|
||||||
content={
|
|
||||||
<CreateFilterMenu
|
|
||||||
value={setting.currentView.filterList}
|
|
||||||
onChange={filterList => {
|
|
||||||
setting.setCurrentView(view => ({
|
|
||||||
...view,
|
|
||||||
filterList,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={<FilteredIcon />}
|
|
||||||
className={clsx(styles.filterButton, {
|
|
||||||
[styles.filterButtonCollapse]: !open,
|
|
||||||
})}
|
|
||||||
size="small"
|
|
||||||
hoverColor="var(--affine-icon-color)"
|
|
||||||
>
|
|
||||||
{t['com.affine.filter']()}
|
|
||||||
</Button>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,22 +8,28 @@ import * as styles from './index.css';
|
|||||||
export type ScrollableContainerProps = {
|
export type ScrollableContainerProps = {
|
||||||
showScrollTopBorder?: boolean;
|
showScrollTopBorder?: boolean;
|
||||||
inTableView?: boolean;
|
inTableView?: boolean;
|
||||||
|
className?: string;
|
||||||
|
viewPortClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScrollableContainer = ({
|
export const ScrollableContainer = ({
|
||||||
children,
|
children,
|
||||||
showScrollTopBorder = false,
|
showScrollTopBorder = false,
|
||||||
inTableView = false,
|
inTableView = false,
|
||||||
|
className,
|
||||||
|
viewPortClassName,
|
||||||
}: PropsWithChildren<ScrollableContainerProps>) => {
|
}: PropsWithChildren<ScrollableContainerProps>) => {
|
||||||
const [hasScrollTop, ref] = useHasScrollTop();
|
const [hasScrollTop, ref] = useHasScrollTop();
|
||||||
return (
|
return (
|
||||||
<ScrollArea.Root className={styles.scrollableContainerRoot}>
|
<ScrollArea.Root
|
||||||
|
className={clsx(styles.scrollableContainerRoot, className)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
data-has-scroll-top={hasScrollTop}
|
data-has-scroll-top={hasScrollTop}
|
||||||
className={clsx({ [styles.scrollTopBorder]: showScrollTopBorder })}
|
className={clsx({ [styles.scrollTopBorder]: showScrollTopBorder })}
|
||||||
/>
|
/>
|
||||||
<ScrollArea.Viewport
|
<ScrollArea.Viewport
|
||||||
className={clsx([styles.scrollableViewport])}
|
className={clsx([styles.scrollableViewport, viewPortClassName])}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<div className={styles.scrollableContainer}>{children}</div>
|
<div className={styles.scrollableContainer}>{children}</div>
|
||||||
|
|||||||
5
packages/env/src/filter.ts
vendored
5
packages/env/src/filter.ts
vendored
@@ -25,8 +25,11 @@ export type Filter = {
|
|||||||
args: Literal[];
|
args: Literal[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type View = {
|
export type Collection = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
pinned?: boolean;
|
||||||
filterList: Filter[];
|
filterList: Filter[];
|
||||||
|
allowList?: string[];
|
||||||
|
excludeList?: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
7
packages/env/src/page-info.ts
vendored
Normal file
7
packages/env/src/page-info.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type PageInfo = {
|
||||||
|
isEdgeless: boolean;
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetPageInfoById = (id: string) => PageInfo | undefined;
|
||||||
4
packages/env/src/workspace.ts
vendored
4
packages/env/src/workspace.ts
vendored
@@ -7,7 +7,7 @@ import type {
|
|||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
import type { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import type { View } from './filter';
|
import type { Collection } from './filter';
|
||||||
import type { Workspace as RemoteWorkspace } from './workspace/legacy-cloud';
|
import type { Workspace as RemoteWorkspace } from './workspace/legacy-cloud';
|
||||||
|
|
||||||
export enum WorkspaceVersion {
|
export enum WorkspaceVersion {
|
||||||
@@ -185,7 +185,7 @@ type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
|
|||||||
type PageListProps<_Flavour extends keyof WorkspaceRegistry> = {
|
type PageListProps<_Flavour extends keyof WorkspaceRegistry> = {
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||||
view: View;
|
collection: Collection;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
||||||
|
|||||||
@@ -193,6 +193,7 @@
|
|||||||
"Check Our Docs": "Check Our Docs",
|
"Check Our Docs": "Check Our Docs",
|
||||||
"Get in touch! Join our communities": "Get in touch! Join our communities.",
|
"Get in touch! Join our communities": "Get in touch! Join our communities.",
|
||||||
"Favorites": "Favourites",
|
"Favorites": "Favourites",
|
||||||
|
"Collections": "Collections",
|
||||||
"Download data": "Download {{CoreOrAll}} data",
|
"Download data": "Download {{CoreOrAll}} data",
|
||||||
"Back Home": "Back Home",
|
"Back Home": "Back Home",
|
||||||
"Set a Workspace name": "Set a Workspace name",
|
"Set a Workspace name": "Set a Workspace name",
|
||||||
|
|||||||
@@ -49,3 +49,9 @@ export async function clickPageMoreActions(page: Page) {
|
|||||||
.getByTestId('editor-option-menu')
|
.getByTestId('editor-option-menu')
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const closeDownloadTip = async (page: Page) => {
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="download-client-tip-close-button"]')
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import type { Page } from '@playwright/test';
|
|||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import { openHomePage } from '../libs/load-page';
|
import { openHomePage } from '../libs/load-page';
|
||||||
import { getBlockSuiteEditorTitle, waitEditorLoad } from '../libs/page-logic';
|
import {
|
||||||
|
closeDownloadTip,
|
||||||
|
getBlockSuiteEditorTitle,
|
||||||
|
waitEditorLoad,
|
||||||
|
} from '../libs/page-logic';
|
||||||
import { clickSideBarAllPageButton } from '../libs/sidebar';
|
import { clickSideBarAllPageButton } from '../libs/sidebar';
|
||||||
|
|
||||||
function getAllPage(page: Page) {
|
function getAllPage(page: Page) {
|
||||||
@@ -52,12 +56,6 @@ test('all page can create new edgeless page', async ({ page }) => {
|
|||||||
await expect(page.locator('affine-edgeless-page')).toBeVisible();
|
await expect(page.locator('affine-edgeless-page')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeDownloadTip = async (page: Page) => {
|
|
||||||
await page
|
|
||||||
.locator('[data-testid="download-client-tip-close-button"]')
|
|
||||||
.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFirstFilter = async (page: Page, name: string) => {
|
const createFirstFilter = async (page: Page, name: string) => {
|
||||||
await page
|
await page
|
||||||
.locator('[data-testid="editor-header-items"]')
|
.locator('[data-testid="editor-header-items"]')
|
||||||
|
|||||||
103
tests/parallels/local-first-collections-items.spec.ts
Normal file
103
tests/parallels/local-first-collections-items.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { openHomePage } from '../libs/load-page';
|
||||||
|
import {
|
||||||
|
closeDownloadTip,
|
||||||
|
getBlockSuiteEditorTitle,
|
||||||
|
newPage,
|
||||||
|
waitEditorLoad,
|
||||||
|
} from '../libs/page-logic';
|
||||||
|
|
||||||
|
const createAndPinCollection = async (
|
||||||
|
page: Page,
|
||||||
|
options?: {
|
||||||
|
collectionName?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await waitEditorLoad(page);
|
||||||
|
await newPage(page);
|
||||||
|
await getBlockSuiteEditorTitle(page).click();
|
||||||
|
await getBlockSuiteEditorTitle(page).fill('test page');
|
||||||
|
await page.getByTestId('all-pages').click();
|
||||||
|
const cell = page.getByRole('cell', {
|
||||||
|
name: 'test page',
|
||||||
|
});
|
||||||
|
await expect(cell).toBeVisible();
|
||||||
|
await closeDownloadTip(page);
|
||||||
|
await page.getByTestId('create-first-filter').click();
|
||||||
|
await page
|
||||||
|
.getByTestId('variable-select')
|
||||||
|
.locator('button', { hasText: 'Created' })
|
||||||
|
.click();
|
||||||
|
await page.getByTestId('save-as-collection').click();
|
||||||
|
const title = page.getByTestId('input-collection-title');
|
||||||
|
await title.isVisible();
|
||||||
|
await title.fill(options?.collectionName ?? 'test collection');
|
||||||
|
await page.getByTestId('save-collection').click();
|
||||||
|
await page.getByTestId('collection-bar-option-pin').click();
|
||||||
|
};
|
||||||
|
test('Show collections items in sidebar', async ({ page }) => {
|
||||||
|
await createAndPinCollection(page);
|
||||||
|
const collections = page.getByTestId('collections');
|
||||||
|
const items = collections.getByTestId('collection-item');
|
||||||
|
expect(await items.count()).toBe(1);
|
||||||
|
const first = items.first();
|
||||||
|
expect(await first.textContent()).toBe('test collection');
|
||||||
|
await first.getByTestId('fav-collapsed-button').click();
|
||||||
|
const collectionPage = collections.getByTestId('collection-page').nth(1);
|
||||||
|
expect(await collectionPage.textContent()).toBe('test page');
|
||||||
|
await collectionPage.getByTestId('collection-page-options').click();
|
||||||
|
const deletePage = page
|
||||||
|
.getByTestId('collection-page-option')
|
||||||
|
.getByText('Delete');
|
||||||
|
await deletePage.click();
|
||||||
|
expect(await collections.getByTestId('collection-page').count()).toBe(1);
|
||||||
|
await first.getByTestId('collection-options').click();
|
||||||
|
const deleteCollection = page
|
||||||
|
.getByTestId('collection-option')
|
||||||
|
.getByText('Delete');
|
||||||
|
await deleteCollection.click();
|
||||||
|
expect(await items.count()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pin and unpin collection', async ({ page }) => {
|
||||||
|
const name = 'asd';
|
||||||
|
await createAndPinCollection(page, { collectionName: name });
|
||||||
|
const collections = page.getByTestId('collections');
|
||||||
|
const items = collections.getByTestId('collection-item');
|
||||||
|
expect(await items.count()).toBe(1);
|
||||||
|
const first = items.first();
|
||||||
|
await first.getByTestId('collection-options').click();
|
||||||
|
const deleteCollection = page
|
||||||
|
.getByTestId('collection-option')
|
||||||
|
.getByText('Unpin');
|
||||||
|
await deleteCollection.click();
|
||||||
|
expect(await items.count()).toBe(0);
|
||||||
|
await page.getByTestId('collection-select').click();
|
||||||
|
const option = page.locator('[data-testid=collection-select-option]', {
|
||||||
|
hasText: name,
|
||||||
|
});
|
||||||
|
await option.hover();
|
||||||
|
await option.getByTestId('collection-select-option-pin').click();
|
||||||
|
expect(await items.count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edit collection', async ({ page }) => {
|
||||||
|
await createAndPinCollection(page);
|
||||||
|
const collections = page.getByTestId('collections');
|
||||||
|
const items = collections.getByTestId('collection-item');
|
||||||
|
expect(await items.count()).toBe(1);
|
||||||
|
const first = items.first();
|
||||||
|
await first.getByTestId('collection-options').click();
|
||||||
|
const editCollection = page
|
||||||
|
.getByTestId('collection-option')
|
||||||
|
.getByText('Edit Filter');
|
||||||
|
await editCollection.click();
|
||||||
|
const title = page.getByTestId('input-collection-title');
|
||||||
|
await title.fill('123');
|
||||||
|
await page.getByTestId('save-collection').click();
|
||||||
|
expect(await first.textContent()).toBe('123');
|
||||||
|
});
|
||||||
14
yarn.lock
14
yarn.lock
@@ -89,7 +89,7 @@ __metadata:
|
|||||||
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/global": 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/lit": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@dnd-kit/core": ^6.0.8
|
"@dnd-kit/core": ^6.0.8
|
||||||
@@ -455,7 +455,7 @@ __metadata:
|
|||||||
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/global": 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/lit": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@storybook/addon-actions": ^7.0.23
|
"@storybook/addon-actions": ^7.0.23
|
||||||
@@ -513,7 +513,7 @@ __metadata:
|
|||||||
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/global": 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/lit": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
|
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
|
||||||
"@dnd-kit/core": ^6.0.8
|
"@dnd-kit/core": ^6.0.8
|
||||||
@@ -3967,13 +3967,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@blocksuite/icons@npm:^2.1.21":
|
"@blocksuite/icons@npm:^2.1.23":
|
||||||
version: 2.1.21
|
version: 2.1.24
|
||||||
resolution: "@blocksuite/icons@npm:2.1.21"
|
resolution: "@blocksuite/icons@npm:2.1.24"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@types/react": ^18.0.25
|
"@types/react": ^18.0.25
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
checksum: ade86c53243691da1aae2bf2abca88b0d9594590a59cf30ec361cba8cb4268737e7129fc0a61ad87e610d709e3eb3d10c8fea3bb76beeeebb334dd14f1001ea1
|
checksum: 170d060e194a923edc5733563fee54475b088235a344073c57608883c48c54d647bbcb33635dd3d816c4a498468e30acc16c9bb5dc5d515050a5636ead5f7679
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user