feat(documents): folder filter on list + per-doc move endpoint
listDocuments accepts folderId (string | null | undefined) and
includeDescendants. folderId=null returns only docs at root;
includeDescendants=true expands the subtree via collectDescendantIds
(in-memory walk over the cached tree -- folder trees are small).
PATCH /api/v1/documents/[id]/folder moves a single document under
documents.manage_folders, with audit-log metadata { type: 'folder_move' }.
Bumping updatedAt is correct for per-doc moves because reps deliberately
acted on that document -- different semantics from the bulk soft-rescue
in Task 4.
createDocument accepts an optional folderId for the upcoming UI's
"create in current folder" affordance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -275,3 +275,25 @@ export async function deleteFolderSoftRescue(
|
||||
metadata: { rescuedTo: newParent },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a folder tree and return the IDs of every descendant of `rootId`
|
||||
* (NOT including rootId itself). Used by `listDocuments` when
|
||||
* `includeDescendants=true` so a port-wide tree fetch is reused
|
||||
* instead of a recursive CTE.
|
||||
*/
|
||||
export function collectDescendantIds(tree: FolderNode[], rootId: string): string[] {
|
||||
const out: string[] = [];
|
||||
function visit(nodes: FolderNode[], inside: boolean) {
|
||||
for (const n of nodes) {
|
||||
if (inside || n.id === rootId) {
|
||||
if (n.id !== rootId) out.push(n.id);
|
||||
visit(n.children, true);
|
||||
} else {
|
||||
visit(n.children, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(tree, false);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, count, eq, gte, inArray, lt, lte, ne, sql, exists } from 'drizzle-orm';
|
||||
import { and, count, eq, gte, inArray, isNull, lt, lte, ne, sql, exists } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
voidDocument as documensoVoid,
|
||||
} from '@/lib/services/documenso-client';
|
||||
import { getPortEoiSigners } from '@/lib/services/documenso-payload';
|
||||
import { listTree, collectDescendantIds } from '@/lib/services/document-folders.service';
|
||||
import type {
|
||||
CreateDocumentInput,
|
||||
UpdateDocumentInput,
|
||||
@@ -159,6 +160,17 @@ export async function listDocuments(
|
||||
if (clientId) filters.push(eq(documents.clientId, clientId));
|
||||
if (documentType) filters.push(eq(documents.documentType, documentType));
|
||||
if (status) filters.push(eq(documents.status, status));
|
||||
if (query.folderId !== undefined) {
|
||||
if (query.folderId === null) {
|
||||
filters.push(isNull(documents.folderId));
|
||||
} else if (query.includeDescendants) {
|
||||
const tree = await listTree(portId);
|
||||
const descendantIds = collectDescendantIds(tree, query.folderId);
|
||||
filters.push(inArray(documents.folderId, [query.folderId, ...descendantIds]));
|
||||
} else {
|
||||
filters.push(eq(documents.folderId, query.folderId));
|
||||
}
|
||||
}
|
||||
if (sentSince) filters.push(gte(documents.createdAt, new Date(sentSince)));
|
||||
if (sentUntil) filters.push(lte(documents.createdAt, new Date(sentUntil)));
|
||||
if (signatureOnly === true) {
|
||||
@@ -443,6 +455,7 @@ export async function createDocument(portId: string, data: CreateDocumentInput,
|
||||
portId,
|
||||
interestId: data.interestId ?? null,
|
||||
clientId: data.clientId ?? null,
|
||||
folderId: data.folderId ?? null,
|
||||
documentType: data.documentType,
|
||||
title: data.title,
|
||||
notes: data.notes ?? null,
|
||||
|
||||
Reference in New Issue
Block a user