diff --git a/frontend/src/documents/SelectionFloatingActions.tsx b/frontend/src/documents/SelectionFloatingActions.tsx index d42d0c3..5707980 100644 --- a/frontend/src/documents/SelectionFloatingActions.tsx +++ b/frontend/src/documents/SelectionFloatingActions.tsx @@ -250,8 +250,34 @@ const SelectionFloatingActions: React.FC = ({ const fetchPromise = (async () => { try { const data = await getFolderTree(); - setRemoteFolderTree(data); - return data; + + // Convert flat list to tree + const nodeMap = new Map(); + data.forEach((item) => { + nodeMap.set(item.id, { + ...item, + children: [], + } as FolderTreeNode); + }); + + const roots: FolderTreeNode[] = []; + data.forEach((item) => { + const node = nodeMap.get(item.id); + if (!node) return; + + if (item.children && item.children.length > 0) { + node.children = item.children + .map((id) => nodeMap.get(id)) + .filter((n): n is FolderTreeNode => Boolean(n)); + } + + if (!item.parent_id) { + roots.push(node); + } + }); + + setRemoteFolderTree(roots); + return roots; } catch (error) { console.warn('[selection] Failed to load folder tree', error); setRemoteFolderTree([]); diff --git a/frontend/src/hooks/documents/useCorrespondents.ts b/frontend/src/hooks/documents/useCorrespondents.ts index 12435b1..c7fd447 100644 --- a/frontend/src/hooks/documents/useCorrespondents.ts +++ b/frontend/src/hooks/documents/useCorrespondents.ts @@ -1,15 +1,9 @@ import { MutableRefObject, useCallback, useState } from 'react'; import type { Correspondent } from '../../types/documents'; -type ApiClient = { - get: (path: string) => Promise<{ data: unknown }>; - post: (path: string, body: unknown) => Promise<{ data: unknown }>; - patch: (path: string, body: unknown) => Promise<{ data: unknown }>; - delete: (path: string) => Promise<{ data: unknown }>; -}; +import { listCorrespondents, createCorrespondent, updateCorrespondent, deleteCorrespondent } from '../../lib/apiClient'; interface UseCorrespondentsOptions { - apiClient: ApiClient; notifyApiError: (error: unknown, fallback: string) => void; setStatusMessage: (message: string, variant?: string) => void; tenantIdRef: MutableRefObject; @@ -17,7 +11,6 @@ interface UseCorrespondentsOptions { } const useCorrespondents = ({ - apiClient, notifyApiError, setStatusMessage, tenantIdRef, @@ -28,7 +21,7 @@ const useCorrespondents = ({ const refreshCorrespondents = useCallback(async () => { const requestTenantId = tenantIdRef.current; try { - const { data } = await apiClient.get('/correspondents'); + const data = await listCorrespondents(); if (tenantIdRef.current !== requestTenantId) { return; } @@ -39,7 +32,7 @@ const useCorrespondents = ({ } notifyApiError(error, 'Unable to load correspondents.'); } - }, [apiClient, notifyApiError, tenantIdRef]); + }, [notifyApiError, tenantIdRef]); const handleCorrespondentUpdate = useCallback( async (correspondentId: string, changes: { name?: string }) => { @@ -61,7 +54,7 @@ const useCorrespondents = ({ } try { - await apiClient.patch(`/correspondents/${correspondentId}`, payload); + await updateCorrespondent(correspondentId, payload); await refreshCorrespondents(); setStatusMessage('Correspondent updated.', 'success'); return true; @@ -71,7 +64,7 @@ const useCorrespondents = ({ throw new Error(message); } }, - [apiClient, notifyApiError, refreshCorrespondents, setStatusMessage], + [notifyApiError, refreshCorrespondents, setStatusMessage], ); const handleCorrespondentCreate = useCallback( @@ -81,7 +74,7 @@ const useCorrespondents = ({ throw new Error('Correspondent name is required.'); } try { - const { data } = await apiClient.post('/correspondents', { name: trimmed }); + const data = await createCorrespondent({ name: trimmed }); await refreshCorrespondents(); setStatusMessage('Correspondent created.', 'success'); return data; @@ -91,7 +84,7 @@ const useCorrespondents = ({ throw new Error(message); } }, - [apiClient, notifyApiError, refreshCorrespondents, setStatusMessage], + [notifyApiError, refreshCorrespondents, setStatusMessage], ); const handleCorrespondentDelete = useCallback( @@ -112,7 +105,7 @@ const useCorrespondents = ({ }; try { - await apiClient.delete(`/correspondents/${correspondentId}`); + await deleteCorrespondent(correspondentId); await refreshCorrespondents(); mapDocumentCaches?.(stripFromDoc); @@ -125,7 +118,7 @@ const useCorrespondents = ({ throw new Error(message); } }, - [apiClient, mapDocumentCaches, notifyApiError, refreshCorrespondents, setStatusMessage], + [mapDocumentCaches, notifyApiError, refreshCorrespondents, setStatusMessage], ); return { diff --git a/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts b/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts index 726476f..76d3a06 100644 --- a/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts +++ b/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts @@ -2,10 +2,7 @@ import { useCallback, useMemo } from 'react'; import type { Identifier } from '../../types/identifiers'; -type ApiClient = { - post: (path: string, body?: unknown) => Promise<{ data: unknown }>; - delete: (path: string) => Promise<{ data: unknown }>; -}; +import { addDocumentCorrespondent, removeDocumentCorrespondent } from '../../lib/apiClient'; interface CorrespondentOption { id?: string; @@ -14,7 +11,6 @@ interface CorrespondentOption { } interface UseDocumentCorrespondentActionsArgs { - apiClient: ApiClient; correspondents: CorrespondentOption[]; handleCorrespondentCreate: (payload: { name: string }) => Promise; notifyApiError: (error: unknown, fallback: string) => void; @@ -26,7 +22,6 @@ interface UseDocumentCorrespondentActionsArgs { } const useDocumentCorrespondentActions = ({ - apiClient, correspondents, handleCorrespondentCreate, notifyApiError, @@ -56,10 +51,7 @@ const useDocumentCorrespondentActions = ({ throw new Error('Missing document or correspondent.'); } try { - await apiClient.post(`/documents/${documentId}/correspondents`, { - assignments: [{ correspondent_id: correspondentId }], - replace: false, - }); + await addDocumentCorrespondent(documentId, correspondentId); if (updateDocumentCaches) { const resolved = correspondent || correspondents.find((entry) => entry?.id === correspondentId) @@ -88,7 +80,7 @@ const useDocumentCorrespondentActions = ({ throw new Error(message); } }, - [apiClient, correspondents, notifyApiError, setStatusMessage, updateDocumentCaches], + [correspondents, notifyApiError, setStatusMessage, updateDocumentCaches], ); const handleCorrespondentRemove = useCallback( @@ -100,7 +92,7 @@ const useDocumentCorrespondentActions = ({ throw new Error('Missing document or correspondent.'); } try { - await apiClient.delete(`/documents/${documentId}/correspondents/${correspondentId}`); + await removeDocumentCorrespondent(documentId, correspondentId); if (updateDocumentCaches) { updateDocumentCaches(documentId, (doc) => { if (!doc || !Array.isArray(doc.correspondents)) { @@ -120,7 +112,7 @@ const useDocumentCorrespondentActions = ({ throw new Error(message); } }, - [apiClient, notifyApiError, setStatusMessage, updateDocumentCaches], + [notifyApiError, setStatusMessage, updateDocumentCaches], ); const normalizeOption = ( diff --git a/frontend/src/hooks/documents/useDocumentTagging.ts b/frontend/src/hooks/documents/useDocumentTagging.ts index 7dedd6f..fb64b65 100644 --- a/frontend/src/hooks/documents/useDocumentTagging.ts +++ b/frontend/src/hooks/documents/useDocumentTagging.ts @@ -8,16 +8,13 @@ interface TagRecord { [key: string]: unknown; } -interface ApiClient { - post: (path: string, payload: unknown) => Promise<{ data: T } | T>; -} +import { createTag, bulkTagDocuments, bulkReanalyzeDocuments } from '../../lib/apiClient'; interface TagManager { buildPayload: (input: { label: string }) => Record; } interface UseDocumentTaggingArgs { - apiClient: ApiClient; tags: TagRecord[]; tagManager: TagManager; refreshTags: () => Promise | void; @@ -42,7 +39,6 @@ interface BulkTagOperationResult { } const useDocumentTagging = ({ - apiClient, tags, tagManager, refreshTags, @@ -85,9 +81,9 @@ const useDocumentTagging = ({ for (const label of normalized) { let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null; if (!tag) { - const payload = tagManager.buildPayload({ label }); - const response = await apiClient.post<{ id?: Identifier; label: string }>('/tags', payload); - tag = 'data' in response ? response.data : response; + const payload = tagManager.buildPayload({ label }) as { label: string; color?: string | null }; + const response = await createTag(payload); + tag = response as TagRecord; await refreshTags(); } createdIds.push(tag.id); @@ -137,7 +133,7 @@ const useDocumentTagging = ({ return { ok: false, reason: 'no-tags' }; } - await apiClient.post('/documents/bulk/tags', { + await bulkTagDocuments({ document_ids: targetDocumentIds, tag_ids: tagIds, action, @@ -180,7 +176,6 @@ const useDocumentTagging = ({ refreshTags, notifyApiError, tagManager, - apiClient, updateDocumentCaches, ], ); @@ -259,14 +254,11 @@ const useDocumentTagging = ({ } try { - const response = await apiClient.post<{ queued?: number }>( - '/documents/bulk/reanalyze', - { - document_ids: targetIds, - force: true, - }, - ); - const payload = 'data' in response ? response.data : response; + const response = await bulkReanalyzeDocuments({ + document_ids: targetIds, + force: true, + }); + const payload = response; const queued = payload?.queued != null ? Number(payload.queued) : targetIds.length; @@ -280,7 +272,7 @@ const useDocumentTagging = ({ notifyApiError(error, message); } }, - [resolveTargetDocumentIds, notifyApiError, setStatusMessage, apiClient], + [resolveTargetDocumentIds, notifyApiError, setStatusMessage], ); return { diff --git a/frontend/src/hooks/documents/useDocumentUploads.ts b/frontend/src/hooks/documents/useDocumentUploads.ts index f239c74..617db5a 100644 --- a/frontend/src/hooks/documents/useDocumentUploads.ts +++ b/frontend/src/hooks/documents/useDocumentUploads.ts @@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import useFileDrop from './useFileDrop'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils'; -import { fetchDocument } from '../../lib/apiClient'; +import { fetchDocument, uploadDocument, resolveFolderPath } from '../../lib/apiClient'; import type { Identifier } from '../../types/identifiers'; type FolderId = Identifier | 'root' | null; @@ -26,17 +26,6 @@ type UploadQueueItem = { conflictDocumentId: Identifier | null; }; -interface UploadResponse { - reused?: boolean; - document?: unknown; - folder?: { id?: FolderId }; -} - -interface ApiClient { - post(url: string, payload: unknown): Promise<{ data: T; status?: number }>; - get(url: string): Promise<{ data: T }>; -} - type StatusLevel = 'success' | 'info' | 'warning' | 'error' | string; type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; type SetStatusMessage = (message: string, level?: StatusLevel) => void; @@ -59,7 +48,7 @@ interface FileSystemDirectoryReaderLike { ) => void; } -interface FileSystemFileEntryLike { +interface FileSystemFileEntryLike extends FileSystemEntry { isFile: true; isDirectory: false; name: string; @@ -69,12 +58,19 @@ interface FileSystemFileEntryLike { ) => void; } -interface FileSystemDirectoryEntryLike { +interface FileSystemDirectoryEntryLike extends FileSystemEntry { isFile: false; isDirectory: true; name: string; createReader: () => FileSystemDirectoryReaderLike; } +const isFileEntry = (entry: FileSystemEntryLike): entry is FileSystemFileEntryLike => { + return entry.isFile && !entry.isDirectory; +}; + +const isDirectoryEntry = (entry: FileSystemEntryLike): entry is FileSystemDirectoryEntryLike => { + return entry.isDirectory && !entry.isFile && 'createReader' in entry; +}; const mapFilesToEntries = (filesInput?: FileList | File[] | null): FileEntry[] => { if (!filesInput) { @@ -96,7 +92,6 @@ const mapFilesToEntries = (filesInput?: FileList | File[] | null): FileEntry[] = }; interface UseDocumentUploadsArgs { - apiClient: ApiClient; token?: string | null; selectedFolder?: FolderId; currentFolderName?: string | null; @@ -126,7 +121,6 @@ interface UseDocumentUploadsResult { } const useDocumentUploads = ({ - apiClient, token, selectedFolder, currentFolderName, @@ -158,11 +152,10 @@ const useDocumentUploads = ({ } try { - const { data, status } = await apiClient.post('/documents', formData); - const duplicate = data?.reused || status === 200; - const document = data?.document ?? data ?? null; + const { reused, document, status } = await uploadDocument(formData); + const duplicate = reused || status === 200; return { - document, + document: document ?? null, duplicate, statusCode: status ?? (duplicate ? 200 : 201), conflictDocumentId: null, @@ -192,7 +185,7 @@ const useDocumentUploads = ({ throw wrapped; } }, - [apiClient, notifyApiError, setStatusMessage], + [notifyApiError, setStatusMessage], ); const appendQueueItems = useCallback((entries: FileEntry[], targetFolderId?: FolderId) => { @@ -244,15 +237,12 @@ const useDocumentUploads = ({ segments: trimmedSegments, }; - const { data } = await apiClient.post<{ folder?: { id?: FolderId | null } }>( - '/folders/path', - payload, - ); - const resolvedId = (data?.folder?.id ?? null) as FolderId; + const { folder } = await resolveFolderPath(payload); + const resolvedId = (folder?.id ?? null) as FolderId; cache.set(cacheKey, resolvedId); return resolvedId; }, - [apiClient], + [], ); const extractFilesFromDataTransfer = useCallback(async (dataTransfer: DataTransfer) => { @@ -294,10 +284,10 @@ const useDocumentUploads = ({ const walkEntry = async (entry: FileSystemEntryLike | null, ancestors: string[] = []) => { if (!entry) return; - if (entry.isFile) { + if (isFileEntry(entry)) { const file = await new Promise((resolve, reject) => { try { - (entry as unknown as FileSystemFileEntryLike).file(resolve, reject); + entry.file(resolve, reject); } catch (error) { console.warn('[Uploads] entry.file failed', error); reject(error as Error); @@ -306,9 +296,9 @@ const useDocumentUploads = ({ pushFile(file, ancestors); return; } - if (entry.isDirectory) { + if (isDirectoryEntry(entry)) { const nextAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors]; - const reader = (entry as unknown as FileSystemDirectoryEntryLike).createReader(); + const reader = entry.createReader(); const entries = await readAllEntries(reader); for (const child of entries) { await walkEntry(child, nextAncestors); diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.ts b/frontend/src/hooks/documents/useDocumentsWorkspace.ts index ae983cd..b881326 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.ts +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.ts @@ -338,7 +338,6 @@ const useDocumentsWorkspace = ({ isInvalidFolderDrop, } = useFolderTree({ initialSelectedFolder: routeFolderId || 'root', - apiClient, tenantIdRef, documentsSortFieldRef: activeSortFieldRef, documentsSortDirectionRef: activeSortDirectionRef, @@ -506,7 +505,6 @@ const useDocumentsWorkspace = ({ registerPasskey, revokePasskey, } = useWorkspaceTaxonomies({ - apiClient, notifyApiError, setStatusMessage, tagManager, @@ -544,7 +542,6 @@ const useDocumentsWorkspace = ({ handleBulkTagRemoveFromDetail, handleBulkSelectionReanalyze, } = useDocumentTagging({ - apiClient, tags, tagManager, refreshTags, @@ -562,7 +559,6 @@ const useDocumentsWorkspace = ({ clearUploadQueue, resetUploadsState, } = useDocumentUploads({ - apiClient, token, selectedFolder, currentFolderName, @@ -1124,7 +1120,6 @@ const useDocumentsWorkspace = ({ }); const { handleTenantSelect } = useTenantManager({ - apiClient, appDispatch, currentTenantId, resetWorkspaceState, diff --git a/frontend/src/hooks/documents/useFolderTree.ts b/frontend/src/hooks/documents/useFolderTree.ts index c0a8917..76b300c 100644 --- a/frontend/src/hooks/documents/useFolderTree.ts +++ b/frontend/src/hooks/documents/useFolderTree.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { getFolderTree, listFolderContents } from '../../lib/apiClient'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import { createRootNode, DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils'; import { @@ -43,10 +44,6 @@ interface FolderTreeNode extends FolderSummary { hasChildren?: boolean; } -interface ApiClient { - get(path: string, config?: { params?: Record }): Promise<{ data: T }>; -} - interface SelectionHelpers { focusedDocumentId: Identifier | null; setFocusedDocumentId: Dispatch>; @@ -58,7 +55,6 @@ interface SelectionHelpers { interface UseFolderTreeOptions { initialSelectedFolder?: FolderId; - apiClient: ApiClient; tenantIdRef: MutableRefObject; documentsSortFieldRef: MutableRefObject; documentsSortDirectionRef: MutableRefObject; @@ -75,7 +71,6 @@ interface FolderOption { const useFolderTree = ({ initialSelectedFolder = 'root', - apiClient, tenantIdRef, documentsSortFieldRef, documentsSortDirectionRef, @@ -89,6 +84,52 @@ const useFolderTree = ({ return new Map([[rootNode.id, rootNode]]); }); + useEffect(() => { + const fetchTree = async () => { + try { + const data = await getFolderTree(); + setFolderNodes((prev) => { + const next = new Map(prev); + const rootChildren: FolderId[] = []; + + data.forEach((item) => { + const id = item.id as FolderId; + const parentId = (item.parent_id || 'root') as FolderId; + const children = (item.children || []).map((c) => c as FolderId); + + next.set(id, { + id, + name: item.name, + parentId, + children, + expanded: false, + loaded: true, + hasChildren: children.length > 0, + }); + + if (parentId === 'root') { + rootChildren.push(id); + } + }); + + const root = next.get('root'); + if (root) { + next.set('root', { + ...(root as FolderTreeNode), + children: rootChildren, + hasChildren: rootChildren.length > 0, + loaded: true, + }); + } + return next; + }); + } catch (error) { + console.error('Failed to fetch folder tree', error); + } + }; + fetchTree(); + }, []); + const [selectedFolder, setSelectedFolder] = useState(initialSelectedFolder || 'root'); const [currentFolder, setCurrentFolder] = useState(null); const [currentSubfolders, setCurrentSubfolders] = useState([]); @@ -245,8 +286,7 @@ const useFolderTree = ({ params.sort = sortField; params.dir = sortDirection; } - const requestConfig = Object.keys(params).length ? { params } : {}; - const { data } = await apiClient.get(`/folders/${path}/contents`, requestConfig); + const data = await listFolderContents(path, params); const childFolders = Array.isArray(data.subfolders) ? data.subfolders : []; const childIds = childFolders .map((child) => (child?.id ?? null) as FolderId | null) @@ -359,7 +399,6 @@ const useFolderTree = ({ return enriched; }, [ - apiClient, documentsSortDirectionRef, documentsSortFieldRef, tenantIdRef, diff --git a/frontend/src/hooks/documents/useTags.ts b/frontend/src/hooks/documents/useTags.ts index baa4e5b..c14c145 100644 --- a/frontend/src/hooks/documents/useTags.ts +++ b/frontend/src/hooks/documents/useTags.ts @@ -2,19 +2,14 @@ import { MutableRefObject, useCallback, useState } from 'react'; import type { TagId, TenantId } from '../../types/identifiers'; import type { Tag } from '../../types/documents'; -type ApiClient = { - get: (path: string) => Promise<{ data: unknown }> - post: (path: string, body: unknown) => Promise<{ data: unknown }> - patch: (path: string, body: unknown) => Promise<{ data: unknown }> - delete: (path: string) => Promise<{ data: unknown }> -}; +import { listTags, updateTag, createTag, deleteTag } from '../../lib/apiClient'; interface TagManagerInterface { buildPayload: (input: { label?: string; color?: string | null }) => { label: string; color: string | null }; } interface UseTagsOptions { - apiClient: ApiClient; + // apiClient removed notifyApiError: (error: unknown, fallback: string) => void; setStatusMessage: (message: string, variant?: string) => void; tagManager: TagManagerInterface; @@ -24,7 +19,7 @@ interface UseTagsOptions { } const useTags = ({ - apiClient, + // apiClient removed notifyApiError, setStatusMessage, tagManager, @@ -37,7 +32,7 @@ const useTags = ({ const refreshTags = useCallback(async () => { const requestTenantId = tenantIdRef.current; try { - const { data } = await apiClient.get('/tags'); + const data = await listTags(); if (tenantIdRef.current !== requestTenantId) { return; } @@ -48,7 +43,7 @@ const useTags = ({ } notifyApiError(error, 'Unable to load tags.'); } - }, [apiClient, notifyApiError, tenantIdRef]); + }, [notifyApiError, tenantIdRef]); const handleTagUpdate = useCallback( async (tagId: TagId, changes: { label?: string; color?: string | null }) => { @@ -69,7 +64,7 @@ const useTags = ({ } try { - await apiClient.patch(`/tags/${tagId}`, payload); + await updateTag(tagId, payload); await refreshTags(); setStatusMessage('Tag updated.', 'success'); return true; @@ -79,14 +74,14 @@ const useTags = ({ throw new Error(message); } }, - [apiClient, notifyApiError, refreshTags, setStatusMessage], + [notifyApiError, refreshTags, setStatusMessage], ); const handleTagCreate = useCallback( async ({ label, color }: { label?: string; color?: string | null } = {}) => { const payload = tagManager.buildPayload({ label, color }); try { - await apiClient.post('/tags', payload); + await createTag(payload); await refreshTags(); setStatusMessage('Tag created.', 'success'); } catch (error) { @@ -95,7 +90,7 @@ const useTags = ({ throw new Error(message); } }, - [apiClient, notifyApiError, refreshTags, setStatusMessage, tagManager], + [notifyApiError, refreshTags, setStatusMessage, tagManager], ); const handleTagDelete = useCallback( @@ -105,7 +100,7 @@ const useTags = ({ } try { - await apiClient.delete(`/tags/${tagId}`); + await deleteTag(tagId); setActiveTagFilters((prev) => prev.filter((id) => id !== tagId)); const stripTagFromDoc = (doc: any) => { @@ -130,7 +125,7 @@ const useTags = ({ throw new Error(message); } }, - [apiClient, mapDocumentCaches, notifyApiError, refreshTags, setActiveTagFilters, setStatusMessage], + [mapDocumentCaches, notifyApiError, refreshTags, setActiveTagFilters, setStatusMessage], ); return { diff --git a/frontend/src/hooks/documents/useTenantManager.ts b/frontend/src/hooks/documents/useTenantManager.ts index d08b57a..d73135c 100644 --- a/frontend/src/hooks/documents/useTenantManager.ts +++ b/frontend/src/hooks/documents/useTenantManager.ts @@ -2,11 +2,7 @@ import { MutableRefObject, useCallback } from 'react'; import type { NavigateFunction } from 'react-router-dom'; import type { FolderId, TenantId } from '../../types/identifiers'; -interface ApiClient { - get: (path: string) => Promise<{ data: unknown }>; - post: (path: string, body?: unknown) => Promise<{ data: any }>; - defaults: { headers: { common: Record } }; -} +import { api, listTenants, switchTenant } from '../../lib/apiClient'; interface TenantOption { id?: TenantId; @@ -14,7 +10,6 @@ interface TenantOption { } interface UseTenantManagerOptions { - apiClient: ApiClient; appDispatch: (action: any) => void; currentTenantId: TenantId | null; resetWorkspaceState: () => void; @@ -30,7 +25,6 @@ interface UseTenantManagerOptions { } const useTenantManager = ({ - apiClient, appDispatch, currentTenantId, resetWorkspaceState, @@ -53,17 +47,15 @@ const useTenantManager = ({ try { if (refreshOnly) { - const { data } = await apiClient.get('/tenants'); + const data = await listTenants(); appDispatch({ type: 'SET_TENANTS', - tenants: Array.isArray(data) ? data : [], + tenants: data, }); return; } - const { data } = await apiClient.post('/auth/select-tenant', { - tenant_id: requestedTenantId, - }); + const data = await switchTenant(requestedTenantId); if (!data?.access_token) { throw new Error('Missing access token in tenant switch response.'); } @@ -77,7 +69,7 @@ const useTenantManager = ({ tenant: data.tenant || null, }); - apiClient.defaults.headers.common.Authorization = `Bearer ${data.access_token}`; + api.defaults.headers.common.Authorization = `Bearer ${data.access_token}`; if (tokenRef) { tokenRef.current = data.access_token; } @@ -102,7 +94,6 @@ const useTenantManager = ({ } }, [ - apiClient, appDispatch, currentTenantId, handleDocumentsViewModeChange, diff --git a/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts b/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts index 261aac1..c2c9e7f 100644 --- a/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts +++ b/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts @@ -8,7 +8,6 @@ import useTags from './useTags'; import type { Identifier } from '../../types/identifiers'; interface UseWorkspaceTaxonomiesArgs { - apiClient: any; notifyApiError: (error: unknown, fallbackMessage?: string, variant?: string) => void; setStatusMessage: (message: string, variant?: string) => void; tagManager: TagManager; @@ -21,7 +20,6 @@ interface UseWorkspaceTaxonomiesArgs { } const useWorkspaceTaxonomies = ({ - apiClient, notifyApiError, setStatusMessage, tagManager, @@ -40,7 +38,6 @@ const useWorkspaceTaxonomies = ({ handleTagDelete, setTags, } = useTags({ - apiClient, notifyApiError, setStatusMessage, tagManager, @@ -71,7 +68,6 @@ const useWorkspaceTaxonomies = ({ handleCorrespondentDelete, setCorrespondents, } = useCorrespondents({ - apiClient, notifyApiError, setStatusMessage, tenantIdRef, @@ -84,7 +80,6 @@ const useWorkspaceTaxonomies = ({ handleCorrespondentRemove, handleCorrespondentAdd, } = useDocumentCorrespondentActions({ - apiClient, correspondents, handleCorrespondentCreate, notifyApiError, diff --git a/frontend/src/lib/apiClient.ts b/frontend/src/lib/apiClient.ts index 85d66e5..9cc76d4 100644 --- a/frontend/src/lib/apiClient.ts +++ b/frontend/src/lib/apiClient.ts @@ -1,4 +1,5 @@ import api from './api'; +export { api }; import type { ApiTokenRecord, AssetResponse, @@ -6,11 +7,12 @@ import type { CapabilitySetResponse, DownloadLink, DocumentResponse, - FolderTreeNode, Identifier, PasskeySummary, TenantSnippet, TagResponse, + CorrespondentResponse, + FolderTreeResponseItem, } from './apiTypes'; import type { AxiosError, AxiosInstance, InternalAxiosRequestConfig, AxiosRequestConfig } from 'axios'; @@ -56,7 +58,7 @@ const normalizeDownload = (input?: DownloadLink | null): DownloadLink | null => export const fetchDocument = async (id: Identifier): Promise => { const { data } = await api.get<{ document?: DocumentResponse }>(`/documents/${id}`); - const doc = data?.document || (data as unknown as DocumentResponse); + const doc = data?.document || (data as DocumentResponse); if (doc?.current_version?.download) { doc.current_version.download = normalizeDownload(doc.current_version.download); } @@ -77,8 +79,8 @@ export const listDocuments = async (params: Record = {}): Promi return Array.isArray(data) ? data : []; }; -export const getFolderTree = async (): Promise => { - const { data } = await api.get('/folders/tree'); +export const getFolderTree = async (): Promise => { + const { data } = await api.get('/folders/tree'); return Array.isArray(data) ? data : []; }; @@ -342,4 +344,102 @@ api.interceptors.response.use( }, ); +export const uploadDocument = async ( + formData: FormData, +): Promise<{ reused?: boolean; document?: unknown; status?: number }> => { + const { data, status } = await api.post<{ reused?: boolean; document?: unknown }>('/documents', formData); + return { ...data, status }; +}; + +export const resolveFolderPath = async ( + payload: { parent_id?: Identifier | null; segments: string[] }, +): Promise<{ folder?: { id?: Identifier | null } }> => { + const { data } = await api.post<{ folder?: { id?: Identifier | null } }>('/folders/path', payload); + return data; +}; + +export const bulkTagDocuments = async ( + payload: { document_ids: Identifier[]; tag_ids: Identifier[]; action: 'add' | 'remove' }, +): Promise => { + await api.post('/documents/bulk/tags', payload); +}; + +export const bulkReanalyzeDocuments = async ( + payload: { document_ids: Identifier[]; force?: boolean }, +): Promise<{ queued?: number }> => { + const { data } = await api.post<{ queued?: number }>('/documents/bulk/reanalyze', payload); + return data; +}; + +export const listTags = async (): Promise => { + const { data } = await api.get('/tags'); + return Array.isArray(data) ? data : []; +}; + +export const updateTag = async ( + tagId: Identifier, + payload: { label?: string; color?: string | null }, +): Promise => { + await api.patch(`/tags/${tagId}`, payload); +}; + +export const deleteTag = async (tagId: Identifier): Promise => { + await api.delete(`/tags/${tagId}`); +}; + +export const listCorrespondents = async (): Promise => { + const { data } = await api.get('/correspondents'); + return Array.isArray(data) ? data : []; +}; + +export const createCorrespondent = async (payload: { name: string }): Promise => { + const { data } = await api.post('/correspondents', payload); + return data; +}; + +export const updateCorrespondent = async ( + correspondentId: Identifier, + payload: { name?: string }, +): Promise => { + await api.patch(`/correspondents/${correspondentId}`, payload); +}; + +export const deleteCorrespondent = async (correspondentId: Identifier): Promise => { + await api.delete(`/correspondents/${correspondentId}`); +}; + +export const addDocumentCorrespondent = async ( + documentId: Identifier, + correspondentId: Identifier, +): Promise => { + await api.post(`/documents/${documentId}/correspondents`, { + assignments: [{ correspondent_id: correspondentId }], + replace: false, + }); +}; + +export const removeDocumentCorrespondent = async ( + documentId: Identifier, + correspondentId: Identifier, +): Promise => { + await api.delete(`/documents/${documentId}/correspondents/${correspondentId}`); +}; + + + +export const switchTenant = async (tenantId: Identifier): Promise<{ access_token: string; tenant: any; tenants?: any[] }> => { + const { data } = await api.post<{ access_token: string; tenant: any; tenants?: any[] }>('/auth/select-tenant', { + tenant_id: tenantId, + }); + return data; +}; + +export const listFolderContents = async ( + path: string, + params?: Record, +): Promise => { + const { data } = await api.get(`/folders/${path}/contents`, { params }); + return data; +}; + export type { ApiTokenRecord } from './apiTypes'; diff --git a/frontend/src/lib/apiTypes.ts b/frontend/src/lib/apiTypes.ts index 0c6d621..7fd1acf 100644 --- a/frontend/src/lib/apiTypes.ts +++ b/frontend/src/lib/apiTypes.ts @@ -14,7 +14,7 @@ export interface TagResponse { color?: string | null; } -interface CorrespondentResponse { +export interface CorrespondentResponse { id: string; name: string; metadata: Record; @@ -69,6 +69,10 @@ export interface FolderTreeNode extends FolderInfo { children?: FolderTreeNode[]; } +export interface FolderTreeResponseItem extends FolderInfo { + children?: string[]; +} + export interface CapabilitySetResponse { id: string; slug: string; diff --git a/frontend/src/styles/status-toast.css b/frontend/src/styles/status-toast.css index c299c43..f248ce1 100644 --- a/frontend/src/styles/status-toast.css +++ b/frontend/src/styles/status-toast.css @@ -1,6 +1,6 @@ .status-toast-container { position: fixed; - top: 2rem; + top: 3rem; left: 50%; transform: translateX(-50%); z-index: 5000000;