From fb4b9c9595f43969c1221fbae910e43500e977b9 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 May 2026 11:43:29 +0200 Subject: [PATCH] feat(documents): useDocumentFolders hook + mutations Wraps the folder tree fetch in TanStack with a 30s staleTime, and provides create / rename / move / delete / move-document mutations that invalidate the relevant query keys. buildFolderPaths flattens the tree into ' / '-separated path strings for picker dropdowns. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/hooks/use-document-folders.ts | 84 +++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/hooks/use-document-folders.ts diff --git a/src/hooks/use-document-folders.ts b/src/hooks/use-document-folders.ts new file mode 100644 index 00000000..97d216ef --- /dev/null +++ b/src/hooks/use-document-folders.ts @@ -0,0 +1,84 @@ +'use client'; + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { apiFetch } from '@/lib/api/client'; +import type { DocumentFolder } from '@/lib/db/schema/documents'; + +export interface FolderNode extends DocumentFolder { + children: FolderNode[]; +} + +const FOLDERS_KEY = ['document-folders'] as const; + +export function useDocumentFolders() { + return useQuery({ + queryKey: FOLDERS_KEY, + queryFn: () => apiFetch<{ data: FolderNode[] }>('/api/v1/document-folders').then((r) => r.data), + staleTime: 30_000, + }); +} + +export function useCreateFolder() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (input: { name: string; parentId: string | null }) => + apiFetch('/api/v1/document-folders', { method: 'POST', body: input }), + onSuccess: () => qc.invalidateQueries({ queryKey: FOLDERS_KEY }), + }); +} + +export function useRenameFolder() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, name }: { id: string; name: string }) => + apiFetch(`/api/v1/document-folders/${id}`, { method: 'PATCH', body: { name } }), + onSuccess: () => qc.invalidateQueries({ queryKey: FOLDERS_KEY }), + }); +} + +export function useMoveFolder() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, parentId }: { id: string; parentId: string | null }) => + apiFetch(`/api/v1/document-folders/${id}`, { method: 'PATCH', body: { parentId } }), + onSuccess: () => qc.invalidateQueries({ queryKey: FOLDERS_KEY }), + }); +} + +export function useDeleteFolder() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => apiFetch(`/api/v1/document-folders/${id}`, { method: 'DELETE' }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: FOLDERS_KEY }); + qc.invalidateQueries({ queryKey: ['documents'] }); + }, + }); +} + +export function useMoveDocument() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ docId, folderId }: { docId: string; folderId: string | null }) => + apiFetch(`/api/v1/documents/${docId}/folder`, { + method: 'PATCH', + body: { folderId }, + }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['documents'] }), + }); +} + +/** Walk the tree → produce flat path strings like "Deals 2026 / Q1". */ +export function buildFolderPaths(tree: FolderNode[]): Array<{ id: string; path: string }> { + const out: Array<{ id: string; path: string }> = []; + function walk(nodes: FolderNode[], prefix: string) { + for (const n of nodes) { + const path = prefix ? `${prefix} / ${n.name}` : n.name; + out.push({ id: n.id, path }); + walk(n.children, path); + } + } + walk(tree, ''); + return out; +}