diff --git a/lib/global-search/__tests__/providers.test.ts b/lib/global-search/__tests__/providers.test.ts index c24e7dcfa..111bf736c 100644 --- a/lib/global-search/__tests__/providers.test.ts +++ b/lib/global-search/__tests__/providers.test.ts @@ -445,4 +445,21 @@ describe('files provider', () => { expect(hits.map((h) => h.id)).toEqual(['d']); expect(hits[0]).toMatchObject({ isFolder: true }); }); + + // The Files trash is a folder the app keeps (.Trash, one folder per + // delete): neither it nor what was deleted is a hit. + it('leaves the Files trash and everything in it out of the hits', async () => { + const listing = [ + { id: 'zebra-live', parentId: null, name: 'zebra.txt', type: 'text/plain', blobId: 'b1', size: 1, created: '', modified: '' }, + { id: 'trash', parentId: null, name: '.Trash', type: 'd', blobId: null, size: 0, created: '', modified: '' }, + { id: 'batch', parentId: 'trash', name: '1730112000000~zebra', type: 'd', blobId: null, size: 0, created: '', modified: '' }, + { id: 'zebra-gone', parentId: 'batch', name: 'zebra old.txt', type: 'text/plain', blobId: 'b2', size: 1, created: '', modified: '' }, + ]; + invalidateFileSearchCache(); + const acc = account('login-t', { listAllFileNodesAcrossAccounts: vi.fn(async () => listing) }); + const { hits } = await filesProvider.remote(parseSearchQuery('zebra'), acc, { limit: 10, signal }); + expect(hits.map((h) => h.id)).toEqual(['zebra-live']); + const trash = await filesProvider.remote(parseSearchQuery('trash'), acc, { limit: 10, signal }); + expect(trash.hits).toEqual([]); + }); }); diff --git a/lib/global-search/providers/files.ts b/lib/global-search/providers/files.ts index 480345346..7388e666c 100644 --- a/lib/global-search/providers/files.ts +++ b/lib/global-search/providers/files.ts @@ -2,6 +2,7 @@ import { matchesTerms, type ParsedQuery } from '@/lib/global-search/query-parser import type { FileHit, SearchAccount, SearchProvider } from '@/lib/global-search/types'; import type { FileNode } from '@/lib/jmap/types'; import { fileNodeKind } from '@/lib/jmap/filenode-kind'; +import { findTrashFolder, trashSubtreeIds } from '@/lib/files-trash'; import { useFileStore } from '@/stores/file-store'; /** @@ -93,9 +94,11 @@ export function rawFileNodeId(node: FileNode): string { function filterNodes(parsed: ParsedQuery, nodes: FileNode[], account: SearchAccount, limit: number, source: 'local' | 'remote'): FileHit[] { const hits: FileHit[] = []; + // The Files trash (.Trash, one folder per delete, what was deleted) is no hit. + const trashed = trashSubtreeIds(nodes, findTrashFolder(nodes)?.id); for (const node of nodes) { // The owner's folders above a shared node only feed the path below. - if (node.discoverableOnly) continue; + if (node.discoverableOnly || trashed.has(node.id)) continue; const kind = fileNodeKind(node); // A symlink has nothing a hit could open. if (kind === 'symlink') continue;