feat: new collections (#4530)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
3720
2023-10-27 17:06:59 +08:00
committed by GitHub
parent 9fc0152cb1
commit ef8024c657
133 changed files with 8382 additions and 3743 deletions
@@ -70,8 +70,8 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
const handleOpenTrashModal = useCallback(() => {
setTrashModal({
open: true,
pageId,
pageTitle: pageMeta.title,
pageIds: [pageId],
pageTitles: [pageMeta.title],
});
}, [pageId, pageMeta.title, setTrashModal]);
@@ -1,297 +0,0 @@
import { Empty } from '@affine/component';
import type { ListData, TrashListData } from '@affine/component/page-list';
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 { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { type PageMeta, type Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuitePagePreview } from '@toeverything/hooks/use-block-suite-page-preview';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useAtom, useAtomValue } from 'jotai';
import { Suspense, useCallback, useMemo } from 'react';
import { allPageModeSelectAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
import { useGetPageInfoById } from '../../../hooks/use-get-page-info';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
import { filterPage } from '../../../utils/filter';
import { currentCollectionsAtom } from '../../../utils/user-setting';
import { emptyDescButton, emptyDescKbd, pageListEmptyStyle } from './index.css';
import { usePageHelper } from './utils';
export interface BlockSuitePageListProps {
blockSuiteWorkspace: BlockSuiteWorkspace;
listType: 'all' | 'trash' | 'shared' | 'public';
isPublic?: boolean;
onOpenPage: (pageId: string, newTab?: boolean) => void;
collection?: Collection;
}
const filter = {
all: (pageMeta: PageMeta) => !pageMeta.trash,
public: (pageMeta: PageMeta) => !pageMeta.trash,
trash: (pageMeta: PageMeta, allMetas: PageMeta[]) => {
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
return !parentMeta?.trash && pageMeta.trash;
},
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
interface PagePreviewInnerProps {
workspace: Workspace;
pageId: string;
}
const PagePreviewInner = ({ workspace, pageId }: PagePreviewInnerProps) => {
const page = useBlockSuiteWorkspacePage(workspace, pageId);
assertExists(page);
const previewAtom = useBlockSuitePagePreview(page);
const preview = useAtomValue(previewAtom);
return preview;
};
interface PagePreviewProps {
workspace: Workspace;
pageId: string;
}
const PagePreview = ({ workspace, pageId }: PagePreviewProps) => {
return (
<Suspense>
<PagePreviewInner workspace={workspace} pageId={pageId} />
</Suspense>
);
};
interface PageListEmptyProps {
createPage?: ReturnType<typeof usePageHelper>['createPage'];
listType: BlockSuitePageListProps['listType'];
}
const PageListEmpty = (props: PageListEmptyProps) => {
const { listType, createPage } = props;
const t = useAFFiNEI18N();
const onCreatePage = useCallback(() => {
createPage?.();
}, [createPage]);
const getEmptyDescription = () => {
if (listType === 'all') {
const createNewPageButton = (
<button className={emptyDescButton} onClick={onCreatePage}>
New Page
</button>
);
if (environment.isDesktop) {
const shortcut = environment.isMacOs ? '⌘ + N' : 'Ctrl + N';
return (
<Trans i18nKey="emptyAllPagesClient">
Click on the {createNewPageButton} button Or press
<kbd className={emptyDescKbd}>{{ shortcut } as any}</kbd> to create
your first page.
</Trans>
);
}
return (
<Trans i18nKey="emptyAllPages">
Click on the
{createNewPageButton}
button to create your first page.
</Trans>
);
}
if (listType === 'trash') {
return t['emptyTrash']();
}
if (listType === 'shared') {
return t['emptySharedPages']();
}
return;
};
return (
<div className={pageListEmptyStyle}>
<Empty
title={t['com.affine.emptyDesc']()}
description={getEmptyDescription()}
/>
</div>
);
};
export const BlockSuitePageList = ({
blockSuiteWorkspace,
onOpenPage,
listType,
isPublic = false,
collection,
}: BlockSuitePageListProps) => {
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const {
toggleFavorite,
restoreFromTrash,
permanentlyDeletePage,
cancelPublicPage,
} = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const [filterMode] = useAtom(allPageModeSelectAtom);
const { createPage, createEdgeless, importFile, isPreferredEdgeless } =
usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const getPageInfo = useGetPageInfoById(blockSuiteWorkspace);
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
const tagOptionMap = useMemo(
() =>
Object.fromEntries(
(blockSuiteWorkspace.meta.properties.tags?.options ?? []).map(v => [
v.id,
v,
])
),
[blockSuiteWorkspace.meta.properties.tags?.options]
);
const list = useMemo(
() =>
pageMetas
.filter(pageMeta => {
if (filterMode === 'all') {
return true;
}
if (filterMode === 'edgeless') {
return isPreferredEdgeless(pageMeta.id);
}
if (filterMode === 'page') {
return !isPreferredEdgeless(pageMeta.id);
}
console.error('unknown filter mode', pageMeta, filterMode);
return true;
})
.filter(pageMeta => {
if (!filter[listType](pageMeta, pageMetas)) {
return false;
}
if (!collection) {
return true;
}
return filterPage(collection, pageMeta);
}),
[pageMetas, filterMode, isPreferredEdgeless, listType, collection]
);
if (listType === 'trash') {
const pageList: TrashListData[] = list.map(pageMeta => {
return {
icon: isPreferredEdgeless(pageMeta.id) ? (
<EdgelessIcon />
) : (
<PageIcon />
),
pageId: pageMeta.id,
title: pageMeta.title,
preview: (
<PagePreview workspace={blockSuiteWorkspace} pageId={pageMeta.id} />
),
createDate: new Date(pageMeta.createDate),
trashDate: pageMeta.trashDate
? new Date(pageMeta.trashDate)
: undefined,
onClickPage: () => onOpenPage(pageMeta.id),
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
},
onRestorePage: () => {
restoreFromTrash(pageMeta.id);
toast(
t['com.affine.toastMessage.restored']({
title: pageMeta.title || 'Untitled',
})
);
},
onPermanentlyDeletePage: () => {
permanentlyDeletePage(pageMeta.id);
toast(t['com.affine.toastMessage.permanentlyDeleted']());
},
};
});
return (
<PageListTrashView
list={pageList}
fallback={<PageListEmpty listType={listType} />}
/>
);
}
const pageList: ListData[] = list.map(pageMeta => {
const page = blockSuiteWorkspace.getPage(pageMeta.id);
return {
icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
pageId: pageMeta.id,
title: pageMeta.title,
preview: (
<PagePreview workspace={blockSuiteWorkspace} pageId={pageMeta.id} />
),
tags:
page?.meta.tags?.map(id => tagOptionMap[id]).filter(v => v != null) ??
[],
favorite: !!pageMeta.favorite,
isPublicPage: !!pageMeta.isPublic,
createDate: new Date(pageMeta.createDate),
updatedDate: new Date(pageMeta.updatedDate ?? pageMeta.createDate),
onClickPage: () => onOpenPage(pageMeta.id),
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
},
removeToTrash: () =>
setTrashModal({
open: true,
pageId: pageMeta.id,
pageTitle: pageMeta.title,
}),
onRestorePage: () => {
restoreFromTrash(pageMeta.id);
toast(
t['com.affine.toastMessage.restored']({
title: pageMeta.title || 'Untitled',
})
);
},
bookmarkPage: () => {
const status = pageMeta.favorite;
toggleFavorite(pageMeta.id);
toast(
status
? t['com.affine.toastMessage.removedFavorites']()
: t['com.affine.toastMessage.addedFavorites']()
);
},
onDisablePublicSharing: () => {
cancelPublicPage(pageMeta.id);
toast('Successfully disabled', {
portal: document.body,
});
},
};
});
return (
<PageList
collectionsAtom={currentCollectionsAtom}
propertiesMeta={blockSuiteWorkspace.meta.properties}
getPageInfo={getPageInfo}
onCreateNewPage={createPage}
onCreateNewEdgeless={createEdgeless}
onImportFile={importFile}
isPublicWorkspace={isPublic}
list={pageList}
fallback={<PageListEmpty createPage={createPage} listType={listType} />}
/>
);
};
@@ -1,18 +1,7 @@
import { style } from '@vanilla-extract/css';
export const filterContainerStyle = style({
padding: '12px',
padding: '0 16px',
display: 'flex',
position: 'relative',
'::after': {
content: '""',
display: 'block',
width: '100%',
height: '1px',
background: 'var(--affine-border-color)',
position: 'absolute',
bottom: 0,
left: 0,
margin: '0 1px',
},
});
@@ -25,17 +25,17 @@ import {
} from '@toeverything/infra/command';
import { atom, useAtomValue } from 'jotai';
import groupBy from 'lodash/groupBy';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import {
openQuickSearchModalAtom,
pageSettingsAtom,
recentPageIdsBaseAtom,
} from '../../../atoms';
import { collectionsCRUDAtom } from '../../../atoms/collections';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../../shared';
import { currentCollectionsAtom } from '../../../utils/user-setting';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import type { CMDKCommand, CommandContext } from './types';
@@ -295,7 +295,7 @@ export const collectionToCommand = (
collection: Collection,
store: ReturnType<typeof getCurrentStore>,
navigationHelper: ReturnType<typeof useNavigateHelper>,
selectCollection: ReturnType<typeof useCollectionManager>['selectCollection'],
selectCollection: (id: string) => void,
t: ReturnType<typeof useAFFiNEI18N>
): CMDKCommand => {
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
@@ -329,14 +329,18 @@ export const collectionToCommand = (
export const useCollectionsCommands = () => {
// todo: considering collections for searching pages
const { savedCollections, selectCollection } = useCollectionManager(
currentCollectionsAtom
);
const { savedCollections } = useCollectionManager(collectionsCRUDAtom);
const store = getCurrentStore();
const query = useAtomValue(cmdkQueryAtom);
const navigationHelper = useNavigateHelper();
const t = useAFFiNEI18N();
const [workspace] = useCurrentWorkspace();
const selectCollection = useCallback(
(id: string) => {
navigationHelper.jumpToCollection(workspace.id, id);
},
[navigationHelper, workspace.id]
);
return useMemo(() => {
let results: CMDKCommand[] = [];
if (query.trim() === '') {
@@ -6,7 +6,7 @@ import {
import { useIsTinyScreen } from '@toeverything/hooks/use-is-tiny-screen';
import clsx from 'clsx';
import { type Atom, useAtomValue } from 'jotai';
import type { ReactElement } from 'react';
import type { ReactNode } from 'react';
import { forwardRef, useRef } from 'react';
import * as style from './style.css';
@@ -14,17 +14,18 @@ import { TopTip } from './top-tip';
import { WindowsAppControls } from './windows-app-controls';
interface HeaderPros {
left?: ReactElement;
right?: ReactElement;
center?: ReactElement;
left?: ReactNode;
right?: ReactNode;
center?: ReactNode;
mainContainerAtom: Atom<HTMLDivElement | null>;
bottomBorder?: boolean;
}
// The Header component is used to solve the following problems
// 1. Manage layout issues independently of page or business logic
// 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed
export const Header = forwardRef<HTMLDivElement, HeaderPros>(function Header(
{ left, center, right, mainContainerAtom },
{ left, center, right, mainContainerAtom, bottomBorder },
ref
) {
const sidebarSwitchRef = useRef<HTMLDivElement | null>(null);
@@ -51,7 +52,7 @@ export const Header = forwardRef<HTMLDivElement, HeaderPros>(function Header(
<>
<TopTip />
<div
className={style.header}
className={clsx(style.header, bottomBorder && style.bottomBorder)}
// data-has-warning={showWarning}
data-open={open}
data-sidebar-floating={appSidebarFloating}
@@ -8,7 +8,6 @@ export const header = style({
padding: '0 16px',
minHeight: '52px',
background: 'var(--affine-background-primary-color)',
borderBottom: '1px solid var(--affine-border-color)',
zIndex: 2,
selectors: {
'&[data-sidebar-floating="false"]': {
@@ -25,6 +24,10 @@ export const header = style({
},
} as ComplexStyleRule);
export const bottomBorder = style({
borderBottom: '1px solid var(--affine-border-color)',
});
export const headerItem = style({
minHeight: '32px',
display: 'flex',
@@ -1,41 +1,41 @@
import {
EditCollectionModal,
createEmptyCollection,
useCollectionManager,
useEditCollectionName,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PlusIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { IconButton } from '@toeverything/components/button';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
import { currentCollectionsAtom } from '../../../../utils/user-setting';
import { collectionsCRUDAtom } from '../../../../atoms/collections';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
type AddCollectionButtonProps = {
workspace: Workspace;
};
export const AddCollectionButton = ({
workspace,
}: AddCollectionButtonProps) => {
const getPageInfo = useGetPageInfoById(workspace);
const setting = useCollectionManager(currentCollectionsAtom);
export const AddCollectionButton = () => {
const setting = useCollectionManager(collectionsCRUDAtom);
const t = useAFFiNEI18N();
const [show, showUpdateCollection] = useState(false);
const [defaultCollection, setDefaultCollection] = useState<Collection>();
const { node, open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,
});
const navigateHelper = useNavigateHelper();
const [workspace] = useCurrentWorkspace();
const handleClick = useCallback(() => {
showUpdateCollection(true);
setDefaultCollection({
id: nanoid(),
name: '',
pinned: true,
filterList: [],
workspaceId: workspace.id,
});
}, [showUpdateCollection, workspace.id]);
open('')
.then(name => {
const id = nanoid();
return setting
.createCollection(createEmptyCollection(id, { name }))
.then(() => {
navigateHelper.jumpToCollection(workspace.id, id);
});
})
.catch(err => {
console.error(err);
});
}, [navigateHelper, open, setting, workspace.id]);
return (
<>
<IconButton
@@ -45,16 +45,7 @@ export const AddCollectionButton = ({
>
<PlusIcon />
</IconButton>
<EditCollectionModal
propertiesMeta={workspace.meta.properties}
getPageInfo={getPageInfo}
onConfirm={setting.saveCollection}
open={show}
onOpenChange={showUpdateCollection}
title={t['com.affine.editCollection.saveCollection']()}
init={defaultCollection}
/>
{node}
</>
);
};
@@ -1,40 +1,29 @@
import { MenuItem as CollectionItem } from '@affine/component/app-sidebar';
import { AnimatedCollectionsIcon } from '@affine/component';
import {
EditCollectionModal,
MenuItem as SidebarMenuItem,
MenuLinkItem as SidebarMenuLinkItem,
} from '@affine/component/app-sidebar';
import {
CollectionOperations,
filterPage,
stopPropagation,
useCollectionManager,
useSavedCollections,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import { WorkspaceSubPath } from '@affine/env/workspace';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteIcon,
FilterIcon,
InformationIcon,
MoreHorizontalIcon,
UnpinIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import { InformationIcon, MoreHorizontalIcon } from '@blocksuite/icons';
import type { PageMeta, Workspace } 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 { IconButton } from '@toeverything/components/button';
import {
Menu,
MenuIcon,
MenuItem,
type MenuItemProps,
} from '@toeverything/components/menu';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import type { ReactElement } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
import { filterPage } from '../../../../utils/filter';
import { currentCollectionsAtom } from '../../../../utils/user-setting';
import { collectionsCRUDAtom } from '../../../../atoms/collections';
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
import type { CollectionsListProps } from '../index';
import { Page } from './page';
import * as styles from './styles.css';
@@ -51,111 +40,20 @@ export const processCollectionsDrag = (e: DragEndEvent) => {
e.over?.data.current?.addToCollection?.(e.active.data.current?.pageId);
}
};
const CollectionOperations = ({
view,
showUpdateCollection,
setting,
}: {
view: Collection;
showUpdateCollection: () => void;
setting: ReturnType<typeof useCollectionManager>;
}) => {
const t = useAFFiNEI18N();
const actions = useMemo<
Array<
| {
icon: ReactElement;
name: string;
click: () => void;
type?: MenuItemProps['type'];
element?: undefined;
}
| {
element: ReactElement;
}
>
>(
() => [
{
icon: (
<MenuIcon>
<FilterIcon />
</MenuIcon>
),
name: t['Edit Filter'](),
click: showUpdateCollection,
},
{
icon: (
<MenuIcon>
<UnpinIcon />
</MenuIcon>
),
name: t['Unpin'](),
click: () => {
return setting.updateCollection({
...view,
pinned: false,
});
},
},
{
element: <div key="divider" className={styles.menuDividerStyle}></div>,
},
{
icon: (
<MenuIcon>
<DeleteIcon />
</MenuIcon>
),
name: t['Delete'](),
click: () => {
return setting.deleteCollection(view.id);
},
type: 'danger',
},
],
[setting, showUpdateCollection, t, view]
);
return (
<div style={{ minWidth: 150 }}>
{actions.map(action => {
if (action.element) {
return action.element;
}
return (
<MenuItem
data-testid="collection-option"
key={action.name}
type={action.type}
preFix={action.icon}
onClick={action.click}
>
{action.name}
</MenuItem>
);
})}
</div>
);
};
const CollectionRenderer = ({
collection,
pages,
workspace,
getPageInfo,
info,
}: {
collection: Collection;
pages: PageMeta[];
workspace: Workspace;
getPageInfo: GetPageInfoById;
info: DeleteCollectionInfo;
}) => {
const [collapsed, setCollapsed] = useState(true);
const setting = useCollectionManager(currentCollectionsAtom);
const { jumpToSubPath } = useNavigateHelper();
const clickCollection = useCallback(() => {
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setting.selectCollection(collection.id);
}, [jumpToSubPath, workspace.id, setting, collection.id]);
const setting = useCollectionManager(collectionsCRUDAtom);
const { setNodeRef, isOver } = useDroppable({
id: `${Collections_DROP_AREA_PREFIX}${collection.id}`,
data: {
@@ -166,19 +64,15 @@ const CollectionRenderer = ({
},
},
});
const config = useAllPageListConfig();
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({
@@ -188,57 +82,41 @@ const CollectionRenderer = ({
},
[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
);
const location = useLocation();
const currentPath = location.pathname.split('?')[0];
const path = `/workspace/${workspace.id}/collection/${collection.id}`;
return (
<Collapsible.Root open={!collapsed}>
<EditCollectionModal
propertiesMeta={workspace.meta.properties}
getPageInfo={getPageInfo}
init={collection}
onConfirm={setting.saveCollection}
open={show}
onOpenChange={showUpdateCollection}
/>
<CollectionItem
<SidebarMenuLinkItem
data-testid="collection-item"
data-type="collection-list-item"
ref={setNodeRef}
onCollapsedChange={setCollapsed}
active={isOver}
icon={<ViewLayersIcon />}
active={isOver || currentPath === path}
icon={<AnimatedCollectionsIcon closed={isOver} />}
to={path}
postfix={
<Menu
items={
<CollectionOperations
view={collection}
showUpdateCollection={() => showUpdateCollection(true)}
setting={setting}
/>
}
>
<IconButton
data-testid="collection-options"
type="plain"
withoutHoverStyle
<div onClick={stopPropagation}>
<CollectionOperations
info={info}
collection={collection}
setting={setting}
config={config}
>
<MoreHorizontalIcon />
</IconButton>
</Menu>
<IconButton
data-testid="collection-options"
type="plain"
withoutHoverStyle
>
<MoreHorizontalIcon />
</IconButton>
</CollectionOperations>
</div>
}
collapsed={pagesToRender.length > 0 ? collapsed : undefined}
onClick={clickCollection}
>
<div
style={{
@@ -249,7 +127,7 @@ const CollectionRenderer = ({
>
<div>{collection.name}</div>
</div>
</CollectionItem>
</SidebarMenuLinkItem>
<Collapsible.Content className={styles.collapsibleContent}>
<div style={{ marginLeft: 20, marginTop: -4 }}>
{pagesToRender.map(page => {
@@ -257,8 +135,6 @@ const CollectionRenderer = ({
<Page
inAllowList={allowList.has(page.id)}
removeFromAllowList={removeFromAllowList}
inExcludeList={excludeList.has(page.id)}
addToExcludeList={addToExcludeList}
allPageMeta={allPagesMeta}
page={page}
key={page.id}
@@ -271,32 +147,27 @@ const CollectionRenderer = ({
</Collapsible.Root>
);
};
export const CollectionsList = ({ workspace }: CollectionsListProps) => {
export const CollectionsList = ({ workspace, info }: CollectionsListProps) => {
const metas = useBlockSuitePageMeta(workspace);
const { savedCollections } = useSavedCollections(currentCollectionsAtom);
const getPageInfo = useGetPageInfoById(workspace);
const pinedCollections = useMemo(
() => savedCollections.filter(v => v.pinned),
[savedCollections]
);
const { collections } = useSavedCollections(collectionsCRUDAtom);
const t = useAFFiNEI18N();
if (pinedCollections.length === 0) {
if (collections.length === 0) {
return (
<CollectionItem
<SidebarMenuItem
data-testid="slider-bar-collection-null-description"
icon={<InformationIcon />}
disabled
>
<span>{t['Create a collection']()}</span>
</CollectionItem>
</SidebarMenuItem>
);
}
return (
<div data-testid="collections" className={styles.wrapper}>
{pinedCollections.map(view => {
{collections.map(view => {
return (
<CollectionRenderer
getPageInfo={getPageInfo}
info={info}
key={view.id}
collection={view}
pages={metas}
@@ -4,7 +4,6 @@ import {
DeleteIcon,
EdgelessIcon,
FilterMinusIcon,
FilterUndoIcon,
MoreHorizontalIcon,
PageIcon,
} from '@blocksuite/icons';
@@ -32,25 +31,21 @@ 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 t = useAFFiNEI18N();
const { setTrashModal } = useTrashModalHelper(workspace);
const onClickDelete = useCallback(() => {
setTrashModal({
open: true,
pageId: page.id,
pageTitle: page.title,
pageIds: [page.id],
pageTitles: [page.title],
});
}, [page.id, page.title, setTrashModal]);
const actions = useMemo<
@@ -79,24 +74,13 @@ export const PageOperations = ({
name: t['Remove special filter'](),
click: () => removeFromAllowList(page.id),
},
]
: []),
...(!inExcludeList
? [
{
icon: (
<MenuIcon>
<FilterUndoIcon />
</MenuIcon>
element: (
<div key="divider" className={styles.menuDividerStyle}></div>
),
name: t['Exclude from filter'](),
click: () => addToExcludeList(page.id),
},
]
: []),
{
element: <div key="divider" className={styles.menuDividerStyle}></div>,
},
{
icon: (
<MenuIcon>
@@ -108,15 +92,7 @@ export const PageOperations = ({
type: 'danger',
},
],
[
inAllowList,
t,
inExcludeList,
onClickDelete,
removeFromAllowList,
page.id,
addToExcludeList,
]
[inAllowList, t, onClickDelete, removeFromAllowList, page.id]
);
return (
<>
@@ -144,15 +120,11 @@ export const Page = ({
workspace,
allPageMeta,
inAllowList,
inExcludeList,
removeFromAllowList,
addToExcludeList,
}: {
page: PageMeta;
inAllowList: boolean;
removeFromAllowList: (id: string) => void;
inExcludeList: boolean;
addToExcludeList: (id: string) => void;
workspace: Workspace;
allPageMeta: Record<string, PageMeta>;
}) => {
@@ -186,8 +158,6 @@ export const Page = ({
<PageOperations
inAllowList={inAllowList}
removeFromAllowList={removeFromAllowList}
inExcludeList={inExcludeList}
addToExcludeList={addToExcludeList}
page={page}
workspace={workspace}
/>
@@ -1,6 +1,9 @@
import { globalStyle, keyframes, style } from '@vanilla-extract/css';
export const wrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '4px',
userSelect: 'none',
// marginLeft:8,
});
@@ -1,3 +1,4 @@
import type { DeleteCollectionInfo } from '@affine/env/filter';
import type { Workspace } from '@blocksuite/store';
export type FavoriteListProps = {
@@ -6,4 +7,5 @@ export type FavoriteListProps = {
export type CollectionsListProps = {
workspace: Workspace;
info: DeleteCollectionInfo;
};
@@ -1,3 +1,4 @@
import { AnimatedDeleteIcon } from '@affine/component';
import {
AddPageButton,
AppSidebar,
@@ -10,13 +11,10 @@ import {
SidebarContainer,
SidebarScrollableContainer,
} from '@affine/component/app-sidebar';
import { MoveToTrash, useCollectionManager } from '@affine/component/page-list';
import { MoveToTrash } from '@affine/component/page-list';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteTemporarilyIcon,
FolderIcon,
SettingsIcon,
} from '@blocksuite/icons';
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core';
import { Menu } from '@toeverything/components/menu';
@@ -27,11 +25,12 @@ import { forwardRef, useCallback, useEffect, useMemo } from 'react';
import { openWorkspaceListModalAtom } from '../../atoms';
import { useHistoryAtom } from '../../atoms/history';
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { useDeleteCollectionInfo } from '../../hooks/affine/use-delete-collection-info';
import { useGeneralShortcuts } from '../../hooks/affine/use-shortcuts';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/use-shortcut-commands';
import type { AllWorkspace } from '../../shared';
import { currentCollectionsAtom } from '../../utils/user-setting';
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
@@ -101,7 +100,6 @@ export const RootAppSidebar = ({
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace.id;
const { appSettings } = useAppSettingHelper();
const { backToAll } = useCollectionManager(currentCollectionsAtom);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom(
@@ -117,7 +115,7 @@ export const RootAppSidebar = ({
const { trashModal, setTrashModal, handleOnConfirm } =
useTrashModalHelper(blockSuiteWorkspace);
const deletePageTitle = trashModal.pageTitle;
const deletePageTitles = trashModal.pageTitles;
const trashConfirmOpen = trashModal.open;
const onTrashConfirmOpenChange = useCallback(
(open: boolean) => {
@@ -129,6 +127,10 @@ export const RootAppSidebar = ({
[trashModal, setTrashModal]
);
const navigateHelper = useNavigateHelper();
const backToAll = useCallback(() => {
navigateHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
}, [currentWorkspace.id, navigateHelper]);
// Listen to the "New Page" action from the menu
useEffect(() => {
if (environment.isDesktop) {
@@ -166,6 +168,7 @@ export const RootAppSidebar = ({
setOpenUserWorkspaceList(false);
}, [setOpenUserWorkspaceList]);
useRegisterBlocksuiteEditorCommands(router.back, router.forward);
const userInfo = useDeleteCollectionInfo();
return (
<>
<AppSidebar
@@ -183,7 +186,7 @@ export const RootAppSidebar = ({
open={trashConfirmOpen}
onConfirm={handleOnConfirm}
onOpenChange={onTrashConfirmOpenChange}
title={deletePageTitle}
titles={deletePageTitles}
/>
<SidebarContainer>
<Menu
@@ -243,16 +246,16 @@ export const RootAppSidebar = ({
</CategoryDivider>
<FavoriteList workspace={blockSuiteWorkspace} />
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
<AddCollectionButton workspace={blockSuiteWorkspace} />
<AddCollectionButton />
</CategoryDivider>
<CollectionsList workspace={blockSuiteWorkspace} />
<CollectionsList workspace={blockSuiteWorkspace} info={userInfo} />
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
{/* fixme: remove the following spacer */}
<div style={{ height: '4px' }} />
<RouteMenuLinkItem
ref={trashDroppable.setNodeRef}
isDraggedOver={trashDroppable.isOver}
icon={<DeleteTemporarilyIcon />}
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
currentPath={currentPath}
path={paths.trash(currentWorkspaceId)}
>
@@ -0,0 +1,14 @@
import { style } from '@vanilla-extract/css';
export const trashTitle = style({
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '0 8px',
fontWeight: 600,
});
export const trashIcon = style({
color: 'var(--affine-icon-color)',
fontSize: 'var(--affine-font-h-5)',
});
@@ -1,43 +1,50 @@
import {
CollectionList,
FilterList,
SaveCollectionButton,
SaveAsCollectionButton,
useCollectionManager,
} from '@affine/component/page-list';
import { Unreachable } from '@affine/env/constant';
import type { Collection } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import type {
WorkspaceFlavour,
WorkspaceHeaderProps,
} from '@affine/env/workspace';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DeleteIcon } from '@blocksuite/icons';
import { useSetAtom } from 'jotai/react';
import { useCallback } from 'react';
import { collectionsCRUDAtom } from '../atoms/collections';
import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
import { useGetPageInfoById } from '../hooks/use-get-page-info';
import { useAllPageListConfig } from '../hooks/affine/use-all-page-list-config';
import { useDeleteCollectionInfo } from '../hooks/affine/use-delete-collection-info';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { useWorkspace } from '../hooks/use-workspace';
import { currentCollectionsAtom } from '../utils/user-setting';
import { SharePageModal } from './affine/share-page-modal';
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
import { filterContainerStyle } from './filter-container.css';
import { Header } from './pure/header';
import { PluginHeader } from './pure/plugin-header';
import { WorkspaceModeFilterTab } from './pure/workspace-mode-filter-tab';
import * as styles from './workspace-header.css';
const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
const currentWorkspace = useWorkspace(workspaceId);
const setting = useCollectionManager(currentCollectionsAtom);
const navigateHelper = useNavigateHelper();
const setting = useCollectionManager(collectionsCRUDAtom);
const saveToCollection = useCallback(
async (collection: Collection) => {
await setting.saveCollection(collection);
setting.selectCollection(collection.id);
console.log(setting.currentCollection.filterList);
await setting.createCollection({
...collection,
mode: 'rule',
filterList: setting.currentCollection.filterList,
});
navigateHelper.jumpToCollection(workspaceId, collection.id);
},
[setting]
);
const getPageInfoById = useGetPageInfoById(
currentWorkspace.blockSuiteWorkspace
[setting, navigateHelper, workspaceId]
);
if (!setting.isDefault || !setting.currentCollection.filterList.length) {
return null;
@@ -59,16 +66,9 @@ const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
</div>
<div>
{setting.currentCollection.filterList.length > 0 ? (
<SaveCollectionButton
propertiesMeta={
currentWorkspace.blockSuiteWorkspace.meta
.properties as PropertiesMeta
}
getPageInfo={getPageInfoById}
<SaveAsCollectionButton
onConfirm={saveToCollection}
filterList={setting.currentCollection.filterList}
workspaceId={workspaceId}
></SaveCollectionButton>
></SaveAsCollectionButton>
) : null}
</div>
</div>
@@ -78,14 +78,17 @@ const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
export function WorkspaceHeader({
currentWorkspaceId,
currentEntry,
rightSlot,
}: WorkspaceHeaderProps<WorkspaceFlavour>) {
const setAppHeader = useSetAtom(appHeaderAtom);
const currentWorkspace = useWorkspace(currentWorkspaceId);
const setting = useCollectionManager(currentCollectionsAtom);
const getPageInfoById = useGetPageInfoById(
currentWorkspace.blockSuiteWorkspace
);
const workspace = currentWorkspace.blockSuiteWorkspace;
const setting = useCollectionManager(collectionsCRUDAtom);
const config = useAllPageListConfig();
const userInfo = useDeleteCollectionInfo();
const t = useAFFiNEI18N();
// route in all page
if (
@@ -99,25 +102,24 @@ export function WorkspaceHeader({
ref={setAppHeader}
left={
<CollectionList
userInfo={userInfo}
allPageListConfig={config}
setting={setting}
getPageInfo={getPageInfoById}
propertiesMeta={
currentWorkspace.blockSuiteWorkspace.meta.properties
}
propertiesMeta={workspace.meta.properties}
/>
}
right={rightSlot}
center={<WorkspaceModeFilterTab />}
/>
{<FilterContainer workspaceId={currentWorkspaceId} />}
<FilterContainer workspaceId={currentWorkspaceId} />
</>
);
}
// route in shared or trash
// route in shared
if (
'subPath' in currentEntry &&
(currentEntry.subPath === WorkspaceSubPath.SHARED ||
currentEntry.subPath === WorkspaceSubPath.TRASH)
currentEntry.subPath === WorkspaceSubPath.SHARED
) {
return (
<Header
@@ -128,11 +130,28 @@ export function WorkspaceHeader({
);
}
// route in trash
if (
'subPath' in currentEntry &&
currentEntry.subPath === WorkspaceSubPath.TRASH
) {
return (
<Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
left={
<div className={styles.trashTitle}>
<DeleteIcon className={styles.trashIcon} />
{t['com.affine.workspaceSubPath.trash']()}
</div>
}
/>
);
}
// route in edit page
if ('pageId' in currentEntry) {
const currentPage = currentWorkspace.blockSuiteWorkspace.getPage(
currentEntry.pageId
);
const currentPage = workspace.getPage(currentEntry.pageId);
const sharePageModal = currentPage ? (
<SharePageModal workspace={currentWorkspace} page={currentPage} />
) : null;
@@ -152,6 +171,7 @@ export function WorkspaceHeader({
<PluginHeader />
</div>
}
bottomBorder
/>
);
}