feat(core): new doc list for trash page (#12429)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added "Delete permanently" and "Restore" quick actions for documents in the Trash, enabling users to restore or permanently delete trashed documents directly from the UI.
  - Introduced multi-restore support, allowing batch restoration of selected trashed documents.
- **Improvements**
  - Quick action tooltips are now localized for better international user experience.
  - Trash page now uses an updated explorer interface for a more consistent and reactive document management experience.
  - Enhanced quick actions with optional click handlers for better extensibility.
- **Documentation**
  - Added new translation keys for "Delete permanently" and "Restore" actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
CatsJuice
2025-05-24 12:45:40 +00:00
parent dfa62f7683
commit 9599494e87
11 changed files with 294 additions and 230 deletions

View File

@@ -1,14 +1,18 @@
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
import { VirtualizedTrashList } from '@affine/core/components/page-list';
import { toast } from '@affine/component';
import {
createDocExplorerContext,
DocExplorerContext,
} from '@affine/core/components/explorer/context';
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
import { Header } from '@affine/core/components/pure/header';
import { DocsService } from '@affine/core/modules/doc';
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { DeleteIcon } from '@blocksuite/icons/rc';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import { useEffect, useMemo } from 'react';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react';
import {
useIsActiveView,
@@ -35,23 +39,76 @@ const TrashHeader = () => {
};
export const TrashPage = () => {
const t = useI18n();
const collectionRulesService = useService(CollectionRulesService);
const globalContextService = useService(GlobalContextService);
const currentWorkspace = useService(WorkspaceService).workspace;
const permissionService = useService(WorkspacePermissionService);
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
const isOwner = useLiveData(permissionService.permission.isOwner$);
const docCollection = currentWorkspace.docCollection;
const docsService = useService(DocsService);
const allTrashPageIds = useLiveData(
LiveData.from(docsService.allTrashDocIds$(), [])
const { restoreFromTrash } = useBlockSuiteMetaHelper();
const isActiveView = useIsActiveView();
const [explorerContextValue] = useState(() =>
createDocExplorerContext({
displayProperties: [
'system:createdAt',
'system:updatedAt',
'system:tags',
],
showMoreOperation: false,
showDragHandle: true,
showDocPreview: false,
quickFavorite: false,
quickDeletePermanently: true,
quickRestore: true,
})
);
const pageMetas = useBlockSuiteDocMeta(docCollection);
const filteredPageMetas = useMemo(() => {
return pageMetas.filter(page => allTrashPageIds.includes(page.id));
}, [pageMetas, allTrashPageIds]);
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
const isOwner = useLiveData(permissionService.permission.isOwner$);
const groups = useLiveData(explorerContextValue.groups$);
const isEmpty =
groups.length === 0 ||
(groups.length > 0 && groups.every(group => !group.items?.length));
const isActiveView = useIsActiveView();
const handleMultiRestore = useCallback(
(ids: string[]) => {
ids.forEach(id => {
restoreFromTrash(id);
});
toast(
t['com.affine.toastMessage.restored']({
title: ids.length > 1 ? 'docs' : 'doc',
})
);
},
[restoreFromTrash, t]
);
useEffect(() => {
const subscription = collectionRulesService
.watch({
filters: [
{
type: 'system',
key: 'trash',
method: 'is',
value: 'true',
},
],
orderBy: {
type: 'system',
key: 'updatedAt',
desc: true,
},
})
.subscribe(result => {
explorerContextValue.groups$.next(result.groups);
});
return () => {
subscription.unsubscribe();
};
}, [collectionRulesService, explorerContextValue.groups$]);
useEffect(() => {
if (isActiveView) {
@@ -64,9 +121,8 @@ export const TrashPage = () => {
return;
}, [globalContextService.globalContext.isTrash, isActiveView]);
const t = useI18n();
return (
<>
<DocExplorerContext.Provider value={explorerContextValue}>
<ViewTitle title={t['Trash']()} />
<ViewIcon icon={'trash'} />
<ViewHeader>
@@ -74,17 +130,17 @@ export const TrashPage = () => {
</ViewHeader>
<ViewBody>
<div className={styles.body}>
{filteredPageMetas.length > 0 ? (
<VirtualizedTrashList
disableMultiDelete={!isAdmin && !isOwner}
disableMultiRestore={!isAdmin && !isOwner}
/>
) : (
{isEmpty ? (
<EmptyPageList type="trash" />
) : (
<DocsExplorer
disableMultiDelete={!isAdmin && !isOwner}
onRestore={isAdmin || isOwner ? handleMultiRestore : undefined}
/>
)}
</div>
</ViewBody>
</>
</DocExplorerContext.Provider>
);
};