From 1bd905366d1c24bedea3c9ada70c8c75dfd4ae53 Mon Sep 17 00:00:00 2001 From: saltbo Date: Sat, 6 Jun 2026 12:16:11 -0400 Subject: [PATCH] fix(trash): list nested trashed roots --- server/routes/objects.integration.test.ts | 25 +++++++++++++++++++ server/services/matter.ts | 19 +++++++++++++- .../files/hooks/use-file-mutations.ts | 1 + 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/server/routes/objects.integration.test.ts b/server/routes/objects.integration.test.ts index 9e4c1ea4..3633770c 100644 --- a/server/routes/objects.integration.test.ts +++ b/server/routes/objects.integration.test.ts @@ -482,6 +482,31 @@ describe('Objects API', () => { expect(childBody.status).toBe('active') }) + it('GET /api/objects?status=trashed returns trashed folder roots nested under active parents', async () => { + const { app, db } = await createTestApp() + const headers = await authedHeaders(app) + await insertStorage(db) + const orgId = await getOrgId(db) + await insertFolder(db, orgId, { id: 'media', name: 'Media' }) + await insertFolder(db, orgId, { id: 'music', name: 'Music', parent: 'Media' }) + await insertFolder(db, orgId, { id: 'album', name: 'Album', parent: 'Media/Music' }) + await insertFile(db, orgId, { id: 'track', name: 'track.flac', parent: 'Media/Music/Album' }) + + const trashRes = await app.request('/api/objects/batch', { + method: 'PATCH', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'trash', ids: ['album'] }), + }) + expect(trashRes.status).toBe(200) + + const res = await app.request('/api/objects?status=trashed', { headers }) + expect(res.status).toBe(200) + const body = (await res.json()) as { items: Array<{ id: string }>; total: number } + + expect(body.total).toBe(1) + expect(body.items.map((item) => item.id)).toEqual(['album']) + }) + it('DELETE /api/trash purges all trashed items', async () => { const { app, db } = await createTestApp() const headers = await authedHeaders(app) diff --git a/server/services/matter.ts b/server/services/matter.ts index 100ab3bb..225e645f 100644 --- a/server/services/matter.ts +++ b/server/services/matter.ts @@ -112,6 +112,16 @@ export async function listMatters( filters: ListFilters, ): Promise<{ items: Matter[]; total: number; page: number; pageSize: number }> { const offset = (filters.page - 1) * filters.pageSize + if (filters.status === 'trashed' && !filters.search && !filters.typeFilter) { + const roots = await listTrashedRoots(db, orgId) + return { + items: roots.slice(offset, offset + filters.pageSize), + total: roots.length, + page: filters.page, + pageSize: filters.pageSize, + } + } + const conditions = [eq(matters.orgId, orgId), eq(matters.status, filters.status)] const typeCond = filters.typeFilter ? typeFilterCondition(filters.typeFilter) : undefined if (filters.search) { @@ -689,5 +699,12 @@ export async function listTrashedRoots(db: Database, orgId: string): Promise buildPath(m.parent, m.name))) - return all.filter((m) => !trashedPaths.has(m.parent)) + return all + .filter((m) => !trashedPaths.has(m.parent)) + .sort((a, b) => { + const aTrashedAt = a.trashedAt ?? 0 + const bTrashedAt = b.trashedAt ?? 0 + if (aTrashedAt !== bTrashedAt) return bTrashedAt - aTrashedAt + return b.createdAt.getTime() - a.createdAt.getTime() + }) } diff --git a/src/components/files/hooks/use-file-mutations.ts b/src/components/files/hooks/use-file-mutations.ts index 28228e8e..aecf66ff 100644 --- a/src/components/files/hooks/use-file-mutations.ts +++ b/src/components/files/hooks/use-file-mutations.ts @@ -10,6 +10,7 @@ export function useFileMutations(currentPath: string) { const queryClient = useQueryClient() const invalidate = () => { queryClient.invalidateQueries({ queryKey: ['objects', 'active', 'path', currentPath] }) + queryClient.invalidateQueries({ queryKey: ['objects', 'trashed'] }) queryClient.invalidateQueries({ queryKey: ['user', 'quota'] }) }