feat(core): add collection and tag filters to all pages (#5567)

close TOV-69

Added the `filterMode` parameter to the `/all` route.
Abstracted the `PageList` and associated components into more universal ones.
Added the `useTagMetas` hook to get and update  the workspace tags.

https://github.com/toeverything/AFFiNE/assets/102217452/7595944d-a056-40c2-8d89-d8df9e901a4b
This commit is contained in:
JimmFly
2024-01-26 07:42:47 +00:00
parent b867dcbdeb
commit 18068f4ae2
67 changed files with 3303 additions and 998 deletions
@@ -0,0 +1,110 @@
import { IconButton } from '@affine/component';
import type { AllPageFilterOption } from '@affine/core/atoms';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import {
CollectionList,
PageListNewPageButton,
useCollectionManager,
} from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
import { PlusIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import clsx from 'clsx';
import { useMemo } from 'react';
import * as styles from './all-page.css';
import { FilterContainer } from './all-page-filter';
export const AllPageHeader = ({
workspace,
showCreateNew,
isDefaultFilter,
activeFilter,
onCreateCollection,
}: {
workspace: Workspace;
showCreateNew: boolean;
isDefaultFilter: boolean;
activeFilter: AllPageFilterOption;
onCreateCollection?: () => void;
}) => {
const setting = useCollectionManager(collectionsCRUDAtom);
const config = useAllPageListConfig();
const userInfo = useDeleteCollectionInfo();
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const disableFilterButton = useMemo(() => {
return activeFilter !== 'docs' && isDefaultFilter;
}, [activeFilter, isDefaultFilter]);
const renderRightItem = useMemo(() => {
if (activeFilter === 'tags') {
return null;
}
if (
activeFilter === 'collections' &&
isDefaultFilter &&
onCreateCollection
) {
return (
<IconButton
type="default"
icon={<PlusIcon fontSize={16} />}
onClick={onCreateCollection}
className={clsx(
styles.headerCreateNewButton,
styles.headerCreateNewCollectionIconButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
/>
);
}
return (
<PageListNewPageButton
size="small"
className={clsx(
styles.headerCreateNewButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
>
<PlusIcon />
</PageListNewPageButton>
);
}, [activeFilter, isDefaultFilter, onCreateCollection, showCreateNew]);
return (
<>
<Header
left={
<CollectionList
userInfo={userInfo}
allPageListConfig={config}
setting={setting}
propertiesMeta={workspace.meta.properties}
disable={disableFilterButton}
/>
}
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{renderRightItem}
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={
<WorkspaceModeFilterTab
workspaceId={workspace.id}
activeFilter={activeFilter}
/>
}
/>
<FilterContainer />
</>
);
};
@@ -14,53 +14,15 @@ export const scrollContainer = style({
paddingBottom: '32px',
});
export const allPagesHeader = style({
height: 100,
alignItems: 'center',
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-5)',
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 headerCreateNewCollectionIconButton = style({
padding: '4px 8px',
fontSize: '16px',
width: '32px',
height: '28px',
borderRadius: '8px',
});
export const headerCreateNewButtonHidden = style({
@@ -72,5 +34,9 @@ export const headerRightWindows = style({
display: 'flex',
alignItems: 'center',
gap: 8,
transform: 'translateX(16px)',
selectors: {
'&[data-is-windows-desktop="true"]': {
transform: 'translateX(16px)',
},
},
});
@@ -1,322 +1,209 @@
import { toast } from '@affine/component';
import type { AllPageFilterOption } from '@affine/core/atoms';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { HubIsland } from '@affine/core/components/affine/hub-island';
import {
CollectionList,
CollectionListHeader,
type CollectionMeta,
createEmptyCollection,
currentCollectionAtom,
FloatingToolbar,
NewPageButton as PureNewPageButton,
OperationCell,
type PageListHandle,
PageListHeader,
useCollectionManager,
useEditCollectionName,
useFilteredPageMetas,
useSavedCollections,
useTagMetas,
VirtualizedCollectionList,
VirtualizedPageList,
} from '@affine/core/components/page-list';
import {
TagListHeader,
VirtualizedTagList,
} from '@affine/core/components/page-list/tags';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { Trans } from '@affine/i18n';
import { performanceRenderLogger } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
CloseIcon,
DeleteIcon,
PlusIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import {
type PropsWithChildren,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { nanoid } from 'nanoid';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { NIL } from 'uuid';
import { collectionsCRUDAtom } from '../../../atoms/collections';
import { HubIsland } from '../../../components/affine/hub-island';
import { usePageHelper } from '../../../components/blocksuite/block-suite-page-list/utils';
import { Header } from '../../../components/pure/header';
import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '../../../components/pure/workspace-mode-filter-tab';
import { useAllPageListConfig } from '../../../hooks/affine/use-all-page-list-config';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useDeleteCollectionInfo } from '../../../hooks/affine/use-delete-collection-info';
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { performanceRenderLogger } from '../../../shared';
import { EmptyPageList } from '../page-list-empty';
import { useFilteredPageMetas } from '../pages';
import {
EmptyCollectionList,
EmptyPageList,
EmptyTagList,
} from '../page-list-empty';
import * as styles from './all-page.css';
import { FilterContainer } from './all-page-filter';
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 testId="new-page-button-trigger">
{t['New Page']()}
</NewPageButton>
</div>
);
};
const usePageOperationsRenderer = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
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,
open,
}: {
open: boolean;
selectedIds: string[];
onClose: () => void;
}) => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
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 (
<FloatingToolbar className={styles.floatingToolbar} open={open}>
<FloatingToolbar.Item>
<Trans
i18nKey="com.affine.page.toolbar.selected"
count={selectedIds.length}
>
<div className={styles.toolbarSelectedNumber}>
{{ count: selectedIds.length } as any}
</div>
selected
</Trans>
</FloatingToolbar.Item>
<FloatingToolbar.Button onClick={onClose} icon={<CloseIcon />} />
<FloatingToolbar.Separator />
<FloatingToolbar.Button
onClick={handleMultiDelete}
icon={<DeleteIcon />}
type="danger"
data-testid="page-list-toolbar-delete"
/>
</FloatingToolbar>
);
};
const NewPageButton = ({
className,
children,
size,
testId,
}: PropsWithChildren<{
className?: string;
size?: 'small' | 'default';
testId?: string;
}>) => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const { importFile, createEdgeless, createPage } = usePageHelper(
currentWorkspace.blockSuiteWorkspace
);
return (
<div className={className} data-testid={testId}>
<PureNewPageButton
size={size}
importFile={importFile}
createNewEdgeless={createEdgeless}
createNewPage={createPage}
>
<div className={styles.newPageButtonLabel}>{children}</div>
</PureNewPageButton>
</div>
);
};
const AllPageHeader = ({
workspace,
showCreateNew,
}: {
workspace: Workspace;
showCreateNew: boolean;
}) => {
const setting = useCollectionManager(collectionsCRUDAtom);
const config = useAllPageListConfig();
const userInfo = useDeleteCollectionInfo();
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
return (
<>
<Header
left={
<CollectionList
userInfo={userInfo}
allPageListConfig={config}
setting={setting}
propertiesMeta={workspace.meta.properties}
/>
}
right={
<div className={clsx(isWindowsDesktop && styles.headerRightWindows)}>
<NewPageButton
size="small"
className={clsx(
styles.headerCreateNewButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
>
<PlusIcon />
</NewPageButton>
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={<WorkspaceModeFilterTab />}
/>
<FilterContainer />
</>
);
};
import { AllPageHeader } from './all-page-header';
// even though it is called all page, it is also being used for collection route as well
export const AllPage = () => {
export const AllPage = ({
activeFilter,
}: {
activeFilter: AllPageFilterOption;
}) => {
const t = useAFFiNEI18N();
const params = useParams();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const { isPreferredEdgeless } = usePageHelper(
currentWorkspace.blockSuiteWorkspace
);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const pageOperationsRenderer = usePageOperationsRenderer();
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const setting = useCollectionManager(collectionsCRUDAtom);
const config = useAllPageListConfig();
const { collections } = useSavedCollections(collectionsCRUDAtom);
const { tags, tagMetas, filterPageMetaByTag, deleteTags } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,
pageMetas
);
const filteredPageMetas = useFilteredPageMetas(
'all',
pageMetas,
currentWorkspace.blockSuiteWorkspace
);
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
const pageListRef = useRef<PageListHandle>(null);
const tagPageMetas = useMemo(() => {
if (params.tagId) {
return filterPageMetaByTag(params.tagId);
}
return [];
}, [filterPageMetaByTag, params.tagId]);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
const collectionMetas = useMemo(() => {
const collectionsList: CollectionMeta[] = collections.map(collection => {
return {
...collection,
title: collection.name,
};
});
return collectionsList;
}, [collections]);
const hideFloatingToolbar = useCallback(() => {
pageListRef.current?.toggleSelectable();
}, []);
const navigateHelper = useNavigateHelper();
const { open, node } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,
});
// 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 handleCreateCollection = useCallback(() => {
open('')
.then(name => {
const id = nanoid();
setting.createCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(currentWorkspace.id, id);
})
.catch(err => {
console.error(err);
});
}, [currentWorkspace.id, navigateHelper, open, setting]);
const [hideHeaderCreateNewPage, setHideHeaderCreateNewPage] = useState(true);
const currentTag = useMemo(() => {
if (params.tagId) {
return tags.find(tag => tag.id === params.tagId);
}
return;
}, [params.tagId, tags]);
const content = useMemo(() => {
if (filteredPageMetas.length > 0 && activeFilter === 'docs') {
return (
<VirtualizedPageList
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
);
} else if (activeFilter === 'collections' && !setting.isDefault) {
return (
<VirtualizedPageList
collection={setting.currentCollection}
config={config}
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
);
} else if (activeFilter === 'collections' && setting.isDefault) {
return collectionMetas.length > 0 ? (
<VirtualizedCollectionList
collections={collections}
collectionMetas={collectionMetas}
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
node={node}
config={config}
handleCreateCollection={handleCreateCollection}
/>
) : (
<EmptyCollectionList
heading={
<CollectionListHeader
node={node}
onCreate={handleCreateCollection}
/>
}
/>
);
} else if (activeFilter === 'tags') {
if (params.tagId) {
return tagPageMetas.length > 0 ? (
<VirtualizedPageList
tag={currentTag}
listItem={tagPageMetas}
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
) : (
<EmptyPageList
type="all"
heading={<PageListHeader workspaceId={currentWorkspace.id} />}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
);
}
return tags.length > 0 ? (
<VirtualizedTagList
tags={tags}
tagMetas={tagMetas}
setHideHeaderCreateNewTag={setHideHeaderCreateNew}
onTagDelete={deleteTags}
/>
) : (
<EmptyTagList heading={<TagListHeader />} />
);
}
return (
<EmptyPageList
type="all"
heading={<PageListHeader workspaceId={currentWorkspace.id} />}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
);
}, [
activeFilter,
collectionMetas,
collections,
config,
currentTag,
currentWorkspace.blockSuiteWorkspace,
currentWorkspace.id,
deleteTags,
filteredPageMetas.length,
handleCreateCollection,
node,
params.tagId,
setting.currentCollection,
setting.isDefault,
tagMetas,
tagPageMetas,
tags,
]);
return (
<div className={styles.root}>
<AllPageHeader
workspace={currentWorkspace.blockSuiteWorkspace}
showCreateNew={!hideHeaderCreateNewPage}
showCreateNew={!hideHeaderCreateNew}
isDefaultFilter={setting.isDefault}
activeFilter={activeFilter}
onCreateCollection={handleCreateCollection}
/>
{filteredPageMetas.length > 0 ? (
<>
<VirtualizedPageList
ref={pageListRef}
selectable="toggle"
draggable
atTopThreshold={80}
atTopStateChange={setHideHeaderCreateNewPage}
onSelectionActiveChange={setShowFloatingToolbar}
heading={<PageListHeader />}
selectedPageIds={filteredSelectedPageIds}
onSelectedPageIdsChange={setSelectedPageIds}
pages={filteredPageMetas}
rowAsLink
isPreferredEdgeless={isPreferredEdgeless}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
pageOperationsRenderer={pageOperationsRenderer}
/>
<PageListFloatingToolbar
open={showFloatingToolbar && filteredSelectedPageIds.length > 0}
selectedIds={filteredSelectedPageIds}
onClose={hideFloatingToolbar}
/>
</>
) : (
<EmptyPageList
type="all"
heading={<PageListHeader />}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
)}
{content}
<HubIsland />
</div>
);
@@ -329,6 +216,18 @@ export const Component = () => {
const currentCollection = useSetAtom(currentCollectionAtom);
const navigateHelper = useNavigateHelper();
const location = useLocation();
const activeFilter = useMemo(() => {
const query = new URLSearchParams(location.search);
const filterMode = query.get('filterMode');
if (filterMode === 'collections') {
return 'collections';
} else if (filterMode === 'tags') {
return 'tags';
}
return 'docs';
}, [location.search]);
useEffect(() => {
function checkJumpOnce() {
for (const [pageId] of currentWorkspace.blockSuiteWorkspace.pages) {
@@ -355,5 +254,5 @@ export const Component = () => {
currentCollection(NIL);
}, [currentCollection]);
return <AllPage />;
return <AllPage activeFilter={activeFilter} />;
};
@@ -10,6 +10,7 @@ import {
useEditCollection,
} from '@affine/core/components/page-list';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import type { Collection } from '@affine/env/filter';
@@ -31,7 +32,6 @@ import {
collectionsCRUDAtom,
pageCollectionBaseAtom,
} from '../../atoms/collections';
import { useAllPageListConfig } from '../../hooks/affine/use-all-page-list-config';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../shared';
import { getWorkspaceSetting } from '../../utils/workspace-setting';
@@ -89,17 +89,24 @@ export const Component = function CollectionPage() {
return null;
}
return isEmpty(collection) ? (
<Placeholder collection={collection} />
<Placeholder collection={collection} workspaceId={workspace.id} />
) : (
<AllPage />
<AllPage activeFilter="collections" />
);
};
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const Placeholder = ({ collection }: { collection: Collection }) => {
const Placeholder = ({
collection,
workspaceId,
}: {
collection: Collection;
workspaceId: string;
}) => {
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
const { node, open } = useEditCollection(useAllPageListConfig());
const { jumpToCollections } = useNavigateHelper();
const openPageEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page');
updateCollection(ret);
@@ -118,6 +125,11 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
}, []);
const t = useAFFiNEI18N();
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]);
return (
<div
style={{
@@ -143,9 +155,11 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
display: 'flex',
alignItems: 'center',
gap: 4,
cursor: 'pointer',
color: 'var(--affine-text-secondary-color)',
['WebkitAppRegion' as string]: 'no-drag',
}}
onClick={handleJumpToCollections}
>
<ViewLayersIcon
style={{ color: 'var(--affine-icon-color)' }}
@@ -66,3 +66,22 @@ export const EmptyPageList = ({
</div>
);
};
export const EmptyCollectionList = ({ heading }: { heading: ReactNode }) => {
const t = useAFFiNEI18N();
return (
<div className={styles.pageListEmptyStyle}>
{heading && <div>{heading}</div>}
<Empty title={t['com.affine.emptyDesc.collection']()} />
</div>
);
};
export const EmptyTagList = ({ heading }: { heading: ReactNode }) => {
const t = useAFFiNEI18N();
return (
<div className={styles.pageListEmptyStyle}>
{heading && <div>{heading}</div>}
<Empty title={t['com.affine.emptyDesc.tag']()} />
</div>
);
};
@@ -1,70 +0,0 @@
import {
filterPage,
filterPageByRules,
useCollectionManager,
} from '@affine/core/components/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, isDefault } =
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 isDefault
? filterPageByRules(
currentCollection.filterList,
currentCollection.allowList,
pageMeta
)
: filterPage(currentCollection, pageMeta);
}),
[
currentCollection,
isDefault,
isPreferredEdgeless,
pageMetas,
pageMode,
route,
]
);
return filteredPageMetas;
};
@@ -0,0 +1,52 @@
import { TagListHeader, useTagMetas } from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
import { AllPage } from './all-page/all-page';
import { AllPageHeader } from './all-page/all-page-header';
import { EmptyPageList } from './page-list-empty';
export const loader: LoaderFunction = async args => {
if (!args.params.tagId) {
return redirect('/404');
}
return null;
};
export const Component = function TagPage() {
const params = useParams();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const { tagUsageCounts } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,
pageMetas
);
const isEmpty = useMemo(() => {
if (params.tagId) {
return tagUsageCounts[params.tagId] === 0;
}
return true;
}, [params.tagId, tagUsageCounts]);
return isEmpty ? (
<>
<AllPageHeader
workspace={currentWorkspace.blockSuiteWorkspace}
showCreateNew={false}
isDefaultFilter={true}
activeFilter={'tags'}
/>
<EmptyPageList
type="all"
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
heading={<TagListHeader />}
/>
</>
) : (
<AllPage activeFilter="tags" />
);
};
@@ -1,9 +1,18 @@
import { toast } from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import {
currentCollectionAtom,
type ListItem,
ListTableHeader,
PageListItemRenderer,
TrashOperationCell,
VirtualizedPageList,
useFilteredPageMetas,
VirtualizedList,
} from '@affine/core/components/page-list';
import { pageHeaderColsDef } from '@affine/core/components/page-list/header-col-def';
import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -16,12 +25,7 @@ import { useCallback } from 'react';
import { type LoaderFunction } from 'react-router-dom';
import { NIL } from 'uuid';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { Header } from '../../components/pure/header';
import { WindowsAppControls } from '../../components/pure/header/windows-app-controls';
import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper';
import { EmptyPageList } from './page-list-empty';
import { useFilteredPageMetas } from './pages';
import * as styles from './trash-page.css';
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
@@ -74,7 +78,8 @@ export const TrashPage = () => {
const t = useAFFiNEI18N();
const pageOperationsRenderer = useCallback(
(page: PageMeta) => {
(item: ListItem) => {
const page = item as PageMeta;
const onRestorePage = () => {
restoreFromTrash(page.id);
toast(
@@ -94,20 +99,28 @@ export const TrashPage = () => {
/>
);
},
[permanentlyDeletePage, restoreFromTrash, t]
);
const pageItemRenderer = useCallback((item: ListItem) => {
return <PageListItemRenderer {...item} />;
}, []);
const pageHeaderRenderer = useCallback(() => {
return <ListTableHeader headerCols={pageHeaderColsDef} />;
}, []);
return (
<div className={styles.root}>
<TrashHeader />
{filteredPageMetas.length > 0 ? (
<VirtualizedPageList
pages={filteredPageMetas}
<VirtualizedList
items={filteredPageMetas}
rowAsLink
groupBy={false}
isPreferredEdgeless={isPreferredEdgeless}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
pageOperationsRenderer={pageOperationsRenderer}
operationsRenderer={pageOperationsRenderer}
itemRenderer={pageItemRenderer}
headerRenderer={pageHeaderRenderer}
/>
) : (
<EmptyPageList