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

View File

@@ -0,0 +1,66 @@
import { style } from '@vanilla-extract/css';
export const root = style({
height: '100%',
width: '100%',
display: 'flex',
flexFlow: 'column',
background: 'var(--affine-background-primary-color)',
});
export const scrollContainer = style({
flex: 1,
width: '100%',
paddingBottom: '32px',
});
export const allPagesHeader = style({
padding: '48px 16px 20px 24px',
overflow: 'hidden',
display: 'flex',
justifyContent: 'space-between',
background: 'var(--affine-background-primary-color)',
});
export const allPagesHeaderTitle = style({
fontSize: 'var(--affine-font-h-3)',
fontWeight: 500,
color: 'var(--affine-text-secondary-color)',
display: 'flex',
alignItems: 'center',
gap: 8,
});
export const titleIcon = style({
color: 'var(--affine-icon-color)',
display: 'inline-flex',
alignItems: 'center',
});
export const titleCollectionName = style({
color: 'var(--affine-text-primary-color)',
});
export const floatingToolbar = style({
position: 'absolute',
bottom: 26,
width: '100%',
zIndex: 1,
});
export const toolbarSelectedNumber = style({
color: 'var(--affine-text-secondary-color)',
});
export const headerCreateNewButton = style({
transition: 'opacity 0.1s ease-in-out',
});
export const newPageButtonLabel = style({
display: 'flex',
alignItems: 'center',
});
export const headerCreateNewButtonHidden = style({
opacity: 0,
});

View File

@@ -1,16 +1,50 @@
import { useCollectionManager } from '@affine/component/page-list';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { toast } from '@affine/component';
import {
currentCollectionAtom,
FloatingToolbar,
NewPageButton as PureNewPageButton,
OperationCell,
PageList,
type PageListHandle,
PageListScrollContainer,
useCollectionManager,
} from '@affine/component/page-list';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import {
CloseIcon,
DeleteIcon,
PlusIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import { useCallback } from 'react';
import clsx from 'clsx';
import {
type PropsWithChildren,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
import { NIL } from 'uuid';
import { getUIAdapter } from '../../adapters/workspace';
import { collectionsCRUDAtom } from '../../atoms/collections';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { WorkspaceHeader } from '../../components/workspace-header';
import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { currentCollectionsAtom } from '../../utils/user-setting';
import * as styles from './all-page.css';
import { EmptyPageList } from './page-list-empty';
import { useFilteredPageMetas } from './pages';
export const loader: LoaderFunction = async args => {
const rootStore = getCurrentStore();
@@ -27,39 +61,274 @@ export const loader: LoaderFunction = async args => {
return redirect(`/workspace/${workspace.id}/${page.id}`);
}
}
rootStore.set(currentCollectionAtom, NIL);
return null;
};
export const AllPage = () => {
const { jumpToPage } = useNavigateHelper();
const PageListHeader = () => {
const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionsCRUDAtom);
const title = useMemo(() => {
if (setting.isDefault) {
return t['com.affine.all-pages.header']();
}
return (
<>
{t['com.affine.collections.header']()} /
<div className={styles.titleIcon}>
<ViewLayersIcon />
</div>
<div className={styles.titleCollectionName}>
{setting.currentCollection.name}
</div>
</>
);
}, [setting.currentCollection.name, setting.isDefault, t]);
return (
<div className={styles.allPagesHeader}>
<div className={styles.allPagesHeaderTitle}>{title}</div>
<NewPageButton>{t['New Page']()}</NewPageButton>
</div>
);
};
const usePageOperationsRenderer = () => {
const [currentWorkspace] = useCurrentWorkspace();
const setting = useCollectionManager(currentCollectionsAtom);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
if (newTab) {
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
} else {
jumpToPage(currentWorkspace.id, pageId);
const { setTrashModal } = useTrashModalHelper(
currentWorkspace.blockSuiteWorkspace
);
const { toggleFavorite } = useBlockSuiteMetaHelper(
currentWorkspace.blockSuiteWorkspace
);
const t = useAFFiNEI18N();
const pageOperationsRenderer = useCallback(
(page: PageMeta) => {
const onDisablePublicSharing = () => {
toast('Successfully disabled', {
portal: document.body,
});
};
return (
<>
<OperationCell
favorite={!!page.favorite}
isPublic={!!page.isPublic}
onDisablePublicSharing={onDisablePublicSharing}
link={`/workspace/${currentWorkspace.id}/${page.id}`}
onRemoveToTrash={() =>
setTrashModal({
open: true,
pageIds: [page.id],
pageTitles: [page.title],
})
}
onToggleFavoritePage={() => {
const status = page.favorite;
toggleFavorite(page.id);
toast(
status
? t['com.affine.toastMessage.removedFavorites']()
: t['com.affine.toastMessage.addedFavorites']()
);
}}
/>
</>
);
},
[currentWorkspace.id, setTrashModal, t, toggleFavorite]
);
return pageOperationsRenderer;
};
const PageListFloatingToolbar = ({
selectedIds,
onClose,
}: {
selectedIds: string[];
onClose: () => void;
}) => {
const open = selectedIds.length > 0;
const handleOpenChange = useCallback(
(open: boolean) => {
if (!open) {
onClose();
}
},
[currentWorkspace, jumpToPage]
[onClose]
);
const { PageList, Header } = getUIAdapter(currentWorkspace.flavour);
const [currentWorkspace] = useCurrentWorkspace();
const { setTrashModal } = useTrashModalHelper(
currentWorkspace.blockSuiteWorkspace
);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const handleMultiDelete = useCallback(() => {
const pageNameMapping = Object.fromEntries(
pageMetas.map(meta => [meta.id, meta.title])
);
const pageNames = selectedIds.map(id => pageNameMapping[id] ?? '');
setTrashModal({
open: true,
pageIds: selectedIds,
pageTitles: pageNames,
});
}, [pageMetas, selectedIds, setTrashModal]);
return (
<>
<Header
currentWorkspaceId={currentWorkspace.id}
currentEntry={{
subPath: WorkspaceSubPath.ALL,
}}
<FloatingToolbar
className={styles.floatingToolbar}
open={open}
onOpenChange={handleOpenChange}
>
<FloatingToolbar.Item>
<Trans
i18nKey="com.affine.page.toolbar.selected"
count={selectedIds.length}
>
<div className={styles.toolbarSelectedNumber}>
{{ count: selectedIds.length } as any}
</div>
pages selected
</Trans>
</FloatingToolbar.Item>
<FloatingToolbar.Button onClick={onClose} icon={<CloseIcon />} />
<FloatingToolbar.Separator />
<FloatingToolbar.Button
onClick={handleMultiDelete}
icon={<DeleteIcon />}
type="danger"
/>
<PageList
collection={setting.currentCollection}
onOpenPage={onClickPage}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
</>
</FloatingToolbar>
);
};
const NewPageButton = ({
className,
children,
size,
}: PropsWithChildren<{
className?: string;
size?: 'small' | 'default';
}>) => {
const [currentWorkspace] = useCurrentWorkspace();
const { importFile, createEdgeless, createPage } = usePageHelper(
currentWorkspace.blockSuiteWorkspace
);
return (
<div className={className}>
<PureNewPageButton
size={size}
importFile={importFile}
createNewEdgeless={createEdgeless}
createNewPage={createPage}
>
<div className={styles.newPageButtonLabel}>{children}</div>
</PureNewPageButton>
</div>
);
};
// even though it is called all page, it is also being used for collection route as well
export const AllPage = () => {
const [currentWorkspace] = useCurrentWorkspace();
const { isPreferredEdgeless } = usePageHelper(
currentWorkspace.blockSuiteWorkspace
);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const pageOperationsRenderer = usePageOperationsRenderer();
const filteredPageMetas = useFilteredPageMetas(
'all',
pageMetas,
currentWorkspace.blockSuiteWorkspace
);
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
const pageListRef = useRef<PageListHandle>(null);
const containerRef = useRef<HTMLDivElement>(null);
const deselectAllAndToggleSelect = useCallback(() => {
setSelectedPageIds([]);
pageListRef.current?.toggleSelectable();
}, []);
// make sure selected id is in the filtered list
const filteredSelectedPageIds = useMemo(() => {
const ids = filteredPageMetas.map(page => page.id);
return selectedPageIds.filter(id => ids.includes(id));
}, [filteredPageMetas, selectedPageIds]);
const [showHeaderCreateNewPage, setShowHeaderCreateNewPage] = useState(false);
// when PageListScrollContainer scrolls above 40px, show the create new page button on header
useEffect(() => {
const container = containerRef.current;
if (container) {
const handleScroll = () => {
setTimeout(() => {
const scrollTop = container.scrollTop ?? 0;
setShowHeaderCreateNewPage(scrollTop > 40);
});
};
container.addEventListener('scroll', handleScroll);
return () => {
container.removeEventListener('scroll', handleScroll);
};
}
return;
}, []);
return (
<div className={styles.root}>
{currentWorkspace.flavour !== WorkspaceFlavour.AFFINE_PUBLIC ? (
<WorkspaceHeader
currentWorkspaceId={currentWorkspace.id}
currentEntry={{
subPath: WorkspaceSubPath.ALL,
}}
rightSlot={
<NewPageButton
size="small"
className={clsx(
styles.headerCreateNewButton,
!showHeaderCreateNewPage && styles.headerCreateNewButtonHidden
)}
>
<PlusIcon />
</NewPageButton>
}
/>
) : null}
<PageListScrollContainer
ref={containerRef}
className={styles.scrollContainer}
>
<PageListHeader />
{filteredPageMetas.length > 0 ? (
<>
<PageList
ref={pageListRef}
selectable="toggle"
draggable
selectedPageIds={filteredSelectedPageIds}
onSelectedPageIdsChange={setSelectedPageIds}
pages={filteredPageMetas}
clickMode="link"
isPreferredEdgeless={isPreferredEdgeless}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
pageOperationsRenderer={pageOperationsRenderer}
/>
<PageListFloatingToolbar
selectedIds={filteredSelectedPageIds}
onClose={deselectAllAndToggleSelect}
/>
</>
) : (
<EmptyPageList
type="all"
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
)}
</PageListScrollContainer>
</div>
);
};

View File

@@ -0,0 +1,25 @@
import { style } from '@vanilla-extract/css';
export const placeholderButton = style({
padding: '8px 18px',
border: '1px solid var(--affine-border-color)',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
gap: 4,
fontWeight: 600,
cursor: 'pointer',
fontSize: 15,
lineHeight: '24px',
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
});
export const button = style({
userSelect: 'none',
borderRadius: 4,
cursor: 'pointer',
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
});

View File

@@ -0,0 +1,275 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import {
AffineShapeIcon,
currentCollectionAtom,
useCollectionManager,
useEditCollection,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import {
CloseIcon,
FilterIcon,
PageIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import { getCurrentStore } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
import {
collectionsCRUDAtom,
pageCollectionBaseAtom,
} from '../../atoms/collections';
import { useAllPageListConfig } from '../../hooks/affine/use-all-page-list-config';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../shared';
import { getWorkspaceSetting } from '../../utils/workspace-setting';
import { AllPage } from './all-page';
import * as styles from './collection.css';
export const loader: LoaderFunction = async args => {
const rootStore = getCurrentStore();
if (!args.params.collectionId) {
return redirect('/404');
}
rootStore.set(currentCollectionAtom, args.params.collectionId);
return null;
};
export const Component = function CollectionPage() {
const { collections, loading } = useAtomValue(pageCollectionBaseAtom);
const navigate = useNavigateHelper();
const params = useParams();
const [workspace] = useCurrentWorkspace();
const collection = collections.find(v => v.id === params.collectionId);
const pushNotification = useSetAtom(pushNotificationAtom);
useEffect(() => {
if (!loading && !collection) {
navigate.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
const collection = getWorkspaceSetting(
workspace.blockSuiteWorkspace
).collectionsTrash.find(v => v.collection.id === params.collectionId);
let text = 'Collection is not exist';
if (collection) {
if (collection.userId) {
text = `${collection.collection.name} is deleted by ${collection.userName}`;
} else {
text = `${collection.collection.name} is deleted`;
}
}
pushNotification({
type: 'error',
title: text,
});
}
}, [
collection,
loading,
navigate,
params.collectionId,
pushNotification,
workspace.blockSuiteWorkspace,
workspace.id,
]);
if (loading) {
return null;
}
if (!collection) {
return null;
}
return isEmpty(collection) ? (
<Placeholder collection={collection} />
) : (
<AllPage />
);
};
const Placeholder = ({ collection }: { collection: Collection }) => {
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
const { node, open } = useEditCollection(useAllPageListConfig());
const openPageEdit = useCallback(() => {
open({ ...collection, mode: 'page' }).then(updateCollection);
}, [open, collection, updateCollection]);
const openRuleEdit = useCallback(() => {
open({ ...collection, mode: 'rule' }).then(updateCollection);
}, [collection, open, updateCollection]);
const [showTips, setShowTips] = useState(false);
useEffect(() => {
setShowTips(!localStorage.getItem('hide-empty-collection-help-info'));
}, []);
const hideTips = useCallback(() => {
setShowTips(false);
localStorage.setItem('hide-empty-collection-help-info', 'true');
}, []);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '12px 24px',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
color: 'var(--affine-text-secondary-color)',
}}
>
<ViewLayersIcon style={{ color: 'var(--affine-icon-color)' }} />
All Collections
<div>/</div>
</div>
<div
data-testid="collection-name"
style={{ fontWeight: 600, color: 'var(--affine-text-primary-color)' }}
>
{collection.name}
</div>
</div>
<div
style={{
display: 'flex',
flex: 1,
flexDirection: 'column',
alignItems: 'center',
gap: 64,
}}
>
<div
style={{
maxWidth: 432,
marginTop: 118,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 18,
margin: '118px 12px 0',
}}
>
<AffineShapeIcon />
<div
style={{
fontSize: 20,
lineHeight: '28px',
fontWeight: 600,
color: 'var(--affine-text-primary-color)',
}}
>
Empty Collection
</div>
<div
style={{
fontSize: 12,
lineHeight: '20px',
color: 'var(--affine-text-secondary-color)',
textAlign: 'center',
}}
>
Collection is a smart folder where you can manually add pages or
automatically add pages through rules.
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px 32px',
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
<div onClick={openPageEdit} className={styles.placeholderButton}>
<PageIcon
style={{
width: 20,
height: 20,
color: 'var(--affine-icon-color)',
}}
/>
<span style={{ padding: '0 4px' }}>Add Pages</span>
</div>
<div onClick={openRuleEdit} className={styles.placeholderButton}>
<FilterIcon
style={{
width: 20,
height: 20,
color: 'var(--affine-icon-color)',
}}
/>
<span style={{ padding: '0 4px' }}>Add Rules</span>
</div>
</div>
</div>
{showTips ? (
<div
style={{
maxWidth: 452,
borderRadius: 8,
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
padding: 10,
gap: 14,
margin: '0 12px',
}}
>
<div
style={{
fontWeight: 600,
fontSize: 12,
lineHeight: '20px',
color: 'var(--affine-text-secondary-color)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>HELP INFO</div>
<CloseIcon
className={styles.button}
style={{ width: 16, height: 16 }}
onClick={hideTips}
/>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
fontSize: 12,
lineHeight: '20px',
}}
>
<div>
<span style={{ fontWeight: 600 }}>Add pages:</span> You can
freely select pages and add them to the collection.
</div>
<div>
<span style={{ fontWeight: 600 }}>Add rules:</span> Rules are
based on filtering. After adding rules, pages that meet the
requirements will be automatically added to the current
collection.
</div>
</div>
</div>
) : null}
</div>
{node}
</div>
);
};
const isEmpty = (collection: Collection) => {
return (
(collection.mode === 'page' && collection.pages.length === 0) ||
(collection.mode === 'rule' &&
collection.allowList.length === 0 &&
collection.filterList.length === 0)
);
};

View File

@@ -23,11 +23,12 @@ import type { Map as YMap } from 'yjs';
import { getUIAdapter } from '../../adapters/workspace';
import { setPageModeAtom } from '../../atoms';
import { collectionsCRUDAtom } from '../../atoms/collections';
import { currentModeAtom } from '../../atoms/mode';
import { WorkspaceHeader } from '../../components/workspace-header';
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { currentCollectionsAtom } from '../../utils/user-setting';
const DetailPageImpl = (): ReactElement => {
const { openPage, jumpToSubPath } = useNavigateHelper();
@@ -36,7 +37,7 @@ const DetailPageImpl = (): ReactElement => {
assertExists(currentWorkspace);
assertExists(currentPageId);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const collectionManager = useCollectionManager(currentCollectionsAtom);
const collectionManager = useCollectionManager(collectionsCRUDAtom);
const mode = useAtomValue(currentModeAtom);
const setPageMode = useSetAtom(setPageModeAtom);
useRegisterBlocksuiteEditorCommands(blockSuiteWorkspace, currentPageId, mode);
@@ -66,7 +67,6 @@ const DetailPageImpl = (): ReactElement => {
});
const disposeTagClick = editor.slots.tagClicked.on(async ({ tagId }) => {
jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
collectionManager.backToAll();
collectionManager.setTemporaryFilter([createTagFilter(tagId)]);
});
return () => {
@@ -86,10 +86,10 @@ const DetailPageImpl = (): ReactElement => {
]
);
const { PageDetail, Header } = getUIAdapter(currentWorkspace.flavour);
const { PageDetail } = getUIAdapter(currentWorkspace.flavour);
return (
<>
<Header
<WorkspaceHeader
currentWorkspaceId={currentWorkspace.id}
currentEntry={{
pageId: currentPageId,

View File

@@ -0,0 +1,27 @@
import { style } from '@vanilla-extract/css';
export const pageListEmptyStyle = style({
height: 'calc(100% - 52px)',
});
export const emptyDescButton = style({
cursor: 'pointer',
color: 'var(--affine-text-secondary-color)',
background: 'var(--affine-background-code-block)',
border: '1px solid var(--affine-border-color)',
borderRadius: '4px',
padding: '0 6px',
boxSizing: 'border-box',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
},
},
});
export const emptyDescKbd = style([
emptyDescButton,
{
cursor: 'text',
},
]);

View File

@@ -0,0 +1,65 @@
import { Empty } from '@affine/component';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@blocksuite/store';
import { useCallback } from 'react';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import * as styles from './page-list-empty.css';
export const EmptyPageList = ({
type,
blockSuiteWorkspace,
}: {
type: 'all' | 'trash' | 'shared' | 'public';
blockSuiteWorkspace: Workspace;
}) => {
const { createPage } = usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const onCreatePage = useCallback(() => {
createPage?.();
}, [createPage]);
const getEmptyDescription = () => {
if (type === 'all') {
const createNewPageButton = (
<button className={styles.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={styles.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 (type === 'trash') {
return t['emptyTrash']();
}
if (type === 'shared') {
return t['emptySharedPages']();
}
return;
};
return (
<div className={styles.pageListEmptyStyle}>
<Empty
title={t['com.affine.emptyDesc']()}
description={getEmptyDescription()}
/>
</div>
);
};

View File

@@ -0,0 +1,52 @@
import { filterPage, useCollectionManager } from '@affine/component/page-list';
import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { allPageModeSelectAtom } from '../../atoms';
import { collectionsCRUDAtom } from '../../atoms/collections';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import type { BlockSuiteWorkspace } from '../../shared';
export const useFilteredPageMetas = (
route: 'all' | 'trash',
pageMetas: PageMeta[],
workspace: BlockSuiteWorkspace
) => {
const { isPreferredEdgeless } = usePageHelper(workspace);
const pageMode = useAtomValue(allPageModeSelectAtom);
const { currentCollection } = useCollectionManager(collectionsCRUDAtom);
const filteredPageMetas = useMemo(
() =>
pageMetas
.filter(pageMeta => {
if (pageMode === 'all') {
return true;
}
if (pageMode === 'edgeless') {
return isPreferredEdgeless(pageMeta.id);
}
if (pageMode === 'page') {
return !isPreferredEdgeless(pageMeta.id);
}
console.error('unknown filter mode', pageMeta, pageMode);
return true;
})
.filter(pageMeta => {
if (
(route === 'trash' && !pageMeta.trash) ||
(route === 'all' && pageMeta.trash)
) {
return false;
}
if (!currentCollection) {
return true;
}
return filterPage(currentCollection, pageMeta);
}),
[pageMetas, pageMode, isPreferredEdgeless, route, currentCollection]
);
return filteredPageMetas;
};

View File

@@ -1,43 +1,91 @@
import { toast } from '@affine/component';
import {
PageList,
PageListScrollContainer,
TrashOperationCell,
} from '@affine/component/page-list';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useCallback } from 'react';
import { getUIAdapter } from '../../adapters/workspace';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { WorkspaceHeader } from '../../components/workspace-header';
import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import * as styles from './all-page.css';
import { EmptyPageList } from './page-list-empty';
import { useFilteredPageMetas } from './pages';
export const TrashPage = () => {
const { jumpToPage } = useNavigateHelper();
const [currentWorkspace] = useCurrentWorkspace();
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
if (newTab) {
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
} else {
jumpToPage(currentWorkspace.id, pageId);
}
},
[currentWorkspace, jumpToPage]
);
// todo(himself65): refactor to plugin
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace);
const { Header } = getUIAdapter(currentWorkspace.flavour);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const filteredPageMetas = useFilteredPageMetas(
'trash',
pageMetas,
currentWorkspace.blockSuiteWorkspace
);
const { restoreFromTrash, permanentlyDeletePage } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { isPreferredEdgeless } = usePageHelper(
currentWorkspace.blockSuiteWorkspace
);
const t = useAFFiNEI18N();
const pageOperationsRenderer = useCallback(
(page: PageMeta) => {
const onRestorePage = () => {
restoreFromTrash(page.id);
toast(
t['com.affine.toastMessage.restored']({
title: page.title || 'Untitled',
})
);
};
const onPermanentlyDeletePage = () => {
permanentlyDeletePage(page.id);
toast(t['com.affine.toastMessage.permanentlyDeleted']());
};
return (
<TrashOperationCell
onPermanentlyDeletePage={onPermanentlyDeletePage}
onRestorePage={onRestorePage}
/>
);
},
[permanentlyDeletePage, restoreFromTrash, t]
);
return (
<>
<Header
<WorkspaceHeader
currentWorkspaceId={currentWorkspace.id}
currentEntry={{
subPath: WorkspaceSubPath.TRASH,
}}
/>
<BlockSuitePageList
blockSuiteWorkspace={blockSuiteWorkspace}
onOpenPage={onClickPage}
listType="trash"
/>
<div className={styles.root}>
<PageListScrollContainer className={styles.scrollContainer}>
{filteredPageMetas.length > 0 ? (
<PageList
pages={filteredPageMetas}
clickMode="link"
groupBy={false}
isPreferredEdgeless={isPreferredEdgeless}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
pageOperationsRenderer={pageOperationsRenderer}
/>
) : (
<EmptyPageList
type="trash"
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
)}
</PageListScrollContainer>
</div>
</>
);
};