mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-30 00:29:46 +08:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user