feat: support for view management (#2892)

This commit is contained in:
3720 2023-06-30 13:40:00 +08:00 committed by GitHub
parent d3393cb0fc
commit 9d0db78f64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1936 additions and 477 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/global": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/icons": "^2.1.23",
"@blocksuite/lit": "0.0.0-20230629103121-76e6587d-nightly",
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
"@types/react": "^18.2.14",

View File

@ -10,6 +10,7 @@ export const root = style({
cursor: 'pointer',
padding: '0 8px 0 12px',
fontSize: 'var(--affine-font-sm)',
margin: '2px 0',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
@ -22,11 +23,12 @@ export const root = style({
color: 'var(--affine-text-secondary-color)',
pointerEvents: 'none',
},
'&[data-active="true"]:hover': {
background:
// 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);',
},
// this is not visible in dark mode
// '&[data-active="true"]:hover': {
// background:
// // 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"]': {
width: 'calc(100% + 8px)',
transform: 'translateX(-8px)',
@ -39,6 +41,7 @@ export const content = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
});
export const icon = style({

View File

@ -12,6 +12,7 @@ export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
disabled?: boolean;
collapsed?: boolean; // true, false, undefined. undefined means no collapse
onCollapsedChange?: (collapsed: boolean) => void;
postfix?: React.ReactElement;
}
export interface MenuLinkItemProps
@ -28,6 +29,7 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
disabled,
collapsed,
onCollapsedChange,
postfix,
...props
},
ref
@ -43,7 +45,6 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
ref={ref}
{...props}
className={clsx([styles.root, props.className])}
onClick={onClick}
data-active={active}
data-disabled={disabled}
data-collapsible={collapsible}
@ -68,11 +69,15 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
)}
{React.cloneElement(icon, {
className: clsx([styles.icon, icon.props.className]),
onClick: onClick,
})}
</div>
)}
<div className={styles.content}>{children}</div>
<div onClick={onClick} className={styles.content}>
{children}
</div>
{postfix}
</div>
);
}

View File

@ -4,7 +4,6 @@
import 'fake-indexeddb/auto';
import { renderHook } from '@testing-library/react';
import { RESET } from 'jotai/utils';
import { expect, test } from 'vitest';
import { createDefaultFilter, vars } from '../filter/vars';
@ -12,22 +11,22 @@ import { useAllPageSetting } from '../use-all-page-setting';
test('useAllPageSetting', async () => {
const settingHook = renderHook(() => useAllPageSetting());
const prevView = settingHook.result.current.currentView;
expect(settingHook.result.current.savedViews).toEqual([]);
settingHook.result.current.setCurrentView(view => ({
...view,
const prevCollection = settingHook.result.current.currentCollection;
expect(settingHook.result.current.savedCollections).toEqual([]);
await settingHook.result.current.updateCollection({
...settingHook.result.current.currentCollection,
filterList: [createDefaultFilter(vars[0])],
}));
});
settingHook.rerender();
const nextView = settingHook.result.current.currentView;
expect(nextView).not.toBe(prevView);
expect(nextView.filterList).toEqual([createDefaultFilter(vars[0])]);
settingHook.result.current.setCurrentView(RESET);
await settingHook.result.current.createView({
...settingHook.result.current.currentView,
const nextCollection = settingHook.result.current.currentCollection;
expect(nextCollection).not.toBe(prevCollection);
expect(nextCollection.filterList).toEqual([createDefaultFilter(vars[0])]);
settingHook.result.current.backToAll();
await settingHook.result.current.saveCollection({
...settingHook.result.current.currentCollection,
id: '1',
});
settingHook.rerender();
expect(settingHook.result.current.savedViews.length).toBe(1);
expect(settingHook.result.current.savedViews[0].id).toBe('1');
expect(settingHook.result.current.savedCollections.length).toBe(1);
expect(settingHook.result.current.savedCollections[0].id).toBe('1');
});

View File

@ -1,4 +1,6 @@
import { CollectionBar } from '@affine/component/page-list';
import { DEFAULT_SORT_KEY } from '@affine/env/constant';
import type { GetPageInfoById } from '@affine/env/page-info';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material';
@ -31,12 +33,14 @@ const AllPagesHead = ({
createNewPage,
createNewEdgeless,
importFile,
getPageInfo,
}: {
isPublicWorkspace: boolean;
sorter: ReturnType<typeof useSorter<ListData>>;
createNewPage: () => void;
createNewEdgeless: () => void;
importFile: () => void;
getPageInfo: GetPageInfoById;
}) => {
const t = useAFFiNEI18N();
const titleList = [
@ -72,7 +76,6 @@ const AllPagesHead = ({
} satisfies CSSProperties,
},
];
return (
<TableHead>
<TableHeadRow>
@ -107,6 +110,7 @@ const AllPagesHead = ({
</TableCell>
))}
</TableHeadRow>
<CollectionBar getPageInfo={getPageInfo} />
</TableHead>
);
};
@ -118,6 +122,7 @@ export const PageList = ({
onCreateNewEdgeless,
onImportFile,
fallback,
getPageInfo,
}: PageListProps) => {
const sorter = useSorter<ListData>({
data: list,
@ -160,6 +165,7 @@ export const PageList = ({
createNewPage={onCreateNewPage}
createNewEdgeless={onCreateNewEdgeless}
importFile={onImportFile}
getPageInfo={getPageInfo}
/>
<AllPagesBody
isPublicWorkspace={isPublicWorkspace}

View File

@ -5,6 +5,7 @@ import { Menu } from '../../..';
import { Condition } from './condition';
import * as styles from './index.css';
import { CreateFilterMenu } from './vars';
export const FilterList = ({
value,
onChange,
@ -13,7 +14,13 @@ export const FilterList = ({
onChange: (value: Filter[]) => void;
}) => {
return (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 10,
}}
>
{value.map((filter, i) => {
return (
<div className={styles.filterItemStyle} key={i}>

View File

@ -28,7 +28,6 @@ export const filterItemStyle = style({
border: '1px solid var(--affine-border-color)',
borderRadius: '8px',
background: 'var(--affine-white)',
margin: '4px',
padding: '4px 8px',
});

View File

@ -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
*
@ -45,6 +47,7 @@ export type PageListProps = {
onCreateNewPage: () => void;
onCreateNewEdgeless: () => void;
onImportFile: () => void;
getPageInfo: GetPageInfoById;
};
export type DraggableTitleCellData = {

View File

@ -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 { openDB } from 'idb';
import type { IDBPDatabase } from 'idb/build/entry';
import { useAtom } from 'jotai';
import { atomWithReset } from 'jotai/utils';
import { atomWithReset, RESET } from 'jotai/utils';
import { useCallback } from 'react';
import useSWRImmutable from 'swr/immutable';
import { NIL } from 'uuid';
import { evalFilterList } from './filter';
type PersistenceView = View;
type PersistenceCollection = Collection;
export interface PageViewDBV1 extends DBSchema {
export interface PageCollectionDBV1 extends DBSchema {
view: {
key: PersistenceView['id'];
value: PersistenceView;
key: PersistenceCollection['id'];
value: PersistenceCollection;
};
}
const pageViewDBPromise: Promise<IDBPDatabase<PageViewDBV1>> =
const pageCollectionDBPromise: Promise<IDBPDatabase<PageCollectionDBV1>> =
typeof window === 'undefined'
? // never resolve in SSR
new Promise<any>(() => {})
: openDB<PageViewDBV1>('page-view', 1, {
: openDB<PageCollectionDBV1>('page-view', 1, {
upgrade(database) {
database.createObjectStore('view', {
keyPath: 'id',
@ -31,18 +31,24 @@ const pageViewDBPromise: Promise<IDBPDatabase<PageViewDBV1>> =
},
});
const currentViewAtom = atomWithReset<View>({
name: 'default',
id: NIL,
filterList: [],
const collectionAtom = atomWithReset<{
currentId: string;
defaultCollection: Collection;
}>({
currentId: NIL,
defaultCollection: {
id: NIL,
name: 'All',
filterList: [],
},
});
export const useAllPageSetting = () => {
const { data: savedViews, mutate } = useSWRImmutable(
['affine', 'page-view'],
export const useSavedCollections = () => {
const { data: savedCollections, mutate } = useSWRImmutable<Collection[]>(
['affine', 'page-collection'],
{
fetcher: async () => {
const db = await pageViewDBPromise;
const db = await pageCollectionDBPromise;
const t = db.transaction('view').objectStore('view');
return await t.getAll();
},
@ -51,29 +57,98 @@ export const useAllPageSetting = () => {
revalidateOnMount: true,
}
);
const [currentView, setCurrentView] = useAtom(currentViewAtom);
const createView = useCallback(
async (view: View) => {
if (view.id === NIL) {
const saveCollection = useCallback(
async (collection: Collection) => {
if (collection.id === NIL) {
return;
}
const db = await pageViewDBPromise;
const db = await pageCollectionDBPromise;
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.put(view);
await t.put(collection);
await 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 {
currentView,
savedViews: savedViews as View[],
savedCollections: savedCollections ?? [],
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
createView,
setCurrentView,
saveCollection,
updateCollection,
selectCollection,
backToAll,
deleteCollection,
addPage,
};
};
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>

View File

@ -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',
},
},
});

View File

@ -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;
};

View File

@ -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)',
},
});

View File

@ -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>
);
};

View File

@ -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)}
/>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -1,2 +1,3 @@
export * from './create-view';
export * from './view-list';
export * from './collection-bar';
export * from './collection-list';
export * from './create-collection';

View File

@ -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',
});

View File

@ -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>
);
};

View File

@ -8,22 +8,28 @@ import * as styles from './index.css';
export type ScrollableContainerProps = {
showScrollTopBorder?: boolean;
inTableView?: boolean;
className?: string;
viewPortClassName?: string;
};
export const ScrollableContainer = ({
children,
showScrollTopBorder = false,
inTableView = false,
className,
viewPortClassName,
}: PropsWithChildren<ScrollableContainerProps>) => {
const [hasScrollTop, ref] = useHasScrollTop();
return (
<ScrollArea.Root className={styles.scrollableContainerRoot}>
<ScrollArea.Root
className={clsx(styles.scrollableContainerRoot, className)}
>
<div
data-has-scroll-top={hasScrollTop}
className={clsx({ [styles.scrollTopBorder]: showScrollTopBorder })}
/>
<ScrollArea.Viewport
className={clsx([styles.scrollableViewport])}
className={clsx([styles.scrollableViewport, viewPortClassName])}
ref={ref}
>
<div className={styles.scrollableContainer}>{children}</div>

View File

@ -25,8 +25,11 @@ export type Filter = {
args: Literal[];
};
export type View = {
export type Collection = {
id: string;
name: string;
pinned?: boolean;
filterList: Filter[];
allowList?: string[];
excludeList?: string[];
};

7
packages/env/src/page-info.ts vendored Normal file
View File

@ -0,0 +1,7 @@
export type PageInfo = {
isEdgeless: boolean;
title: string;
id: string;
};
export type GetPageInfoById = (id: string) => PageInfo | undefined;

View File

@ -7,7 +7,7 @@ import type {
} from '@blocksuite/store';
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';
export enum WorkspaceVersion {
@ -185,7 +185,7 @@ type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
type PageListProps<_Flavour extends keyof WorkspaceRegistry> = {
blockSuiteWorkspace: BlockSuiteWorkspace;
onOpenPage: (pageId: string, newTab?: boolean) => void;
view: View;
collection: Collection;
};
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {

View File

@ -193,6 +193,7 @@
"Check Our Docs": "Check Our Docs",
"Get in touch! Join our communities": "Get in touch! Join our communities.",
"Favorites": "Favourites",
"Collections": "Collections",
"Download data": "Download {{CoreOrAll}} data",
"Back Home": "Back Home",
"Set a Workspace name": "Set a Workspace name",

View File

@ -49,3 +49,9 @@ export async function clickPageMoreActions(page: Page) {
.getByTestId('editor-option-menu')
.click();
}
export const closeDownloadTip = async (page: Page) => {
await page
.locator('[data-testid="download-client-tip-close-button"]')
.click();
};

View File

@ -3,7 +3,11 @@ import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
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';
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();
});
const closeDownloadTip = async (page: Page) => {
await page
.locator('[data-testid="download-client-tip-close-button"]')
.click();
};
const createFirstFilter = async (page: Page, name: string) => {
await page
.locator('[data-testid="editor-header-items"]')

View 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');
});

View File

@ -89,7 +89,7 @@ __metadata:
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/global": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/icons": ^2.1.21
"@blocksuite/icons": ^2.1.23
"@blocksuite/lit": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
"@dnd-kit/core": ^6.0.8
@ -455,7 +455,7 @@ __metadata:
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/global": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/icons": ^2.1.21
"@blocksuite/icons": ^2.1.23
"@blocksuite/lit": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
"@storybook/addon-actions": ^7.0.23
@ -513,7 +513,7 @@ __metadata:
"@blocksuite/blocks": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/editor": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/global": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/icons": ^2.1.21
"@blocksuite/icons": ^2.1.23
"@blocksuite/lit": 0.0.0-20230629103121-76e6587d-nightly
"@blocksuite/store": 0.0.0-20230629103121-76e6587d-nightly
"@dnd-kit/core": ^6.0.8
@ -3967,13 +3967,13 @@ __metadata:
languageName: node
linkType: hard
"@blocksuite/icons@npm:^2.1.21":
version: 2.1.21
resolution: "@blocksuite/icons@npm:2.1.21"
"@blocksuite/icons@npm:^2.1.23":
version: 2.1.24
resolution: "@blocksuite/icons@npm:2.1.24"
peerDependencies:
"@types/react": ^18.0.25
react: ^18.2.0
checksum: ade86c53243691da1aae2bf2abca88b0d9594590a59cf30ec361cba8cb4268737e7129fc0a61ad87e610d709e3eb3d10c8fea3bb76beeeebb334dd14f1001ea1
checksum: 170d060e194a923edc5733563fee54475b088235a344073c57608883c48c54d647bbcb33635dd3d816c4a498468e30acc16c9bb5dc5d515050a5636ead5f7679
languageName: node
linkType: hard