refactor: Decouple frontend API calls from a shared client instance
This commit is contained in:
@@ -250,8 +250,34 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
||||
const fetchPromise = (async () => {
|
||||
try {
|
||||
const data = await getFolderTree();
|
||||
setRemoteFolderTree(data);
|
||||
return data;
|
||||
|
||||
// Convert flat list to tree
|
||||
const nodeMap = new Map<string, FolderTreeNode>();
|
||||
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([]);
|
||||
|
||||
@@ -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<string | null>;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<CorrespondentOption | null>;
|
||||
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 = (
|
||||
|
||||
@@ -8,16 +8,13 @@ interface TagRecord {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ApiClient {
|
||||
post: <T = { data: unknown }>(path: string, payload: unknown) => Promise<{ data: T } | T>;
|
||||
}
|
||||
import { createTag, bulkTagDocuments, bulkReanalyzeDocuments } from '../../lib/apiClient';
|
||||
|
||||
interface TagManager {
|
||||
buildPayload: (input: { label: string }) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface UseDocumentTaggingArgs {
|
||||
apiClient: ApiClient;
|
||||
tags: TagRecord[];
|
||||
tagManager: TagManager;
|
||||
refreshTags: () => Promise<void> | 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 {
|
||||
|
||||
@@ -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<T = UploadResponse>(url: string, payload: unknown): Promise<{ data: T; status?: number }>;
|
||||
get<T = { document?: unknown }>(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<File>((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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<T = FolderContentsEntry>(path: string, config?: { params?: Record<string, unknown> }): Promise<{ data: T }>;
|
||||
}
|
||||
|
||||
interface SelectionHelpers {
|
||||
focusedDocumentId: Identifier | null;
|
||||
setFocusedDocumentId: Dispatch<SetStateAction<Identifier | null>>;
|
||||
@@ -58,7 +55,6 @@ interface SelectionHelpers {
|
||||
|
||||
interface UseFolderTreeOptions {
|
||||
initialSelectedFolder?: FolderId;
|
||||
apiClient: ApiClient;
|
||||
tenantIdRef: MutableRefObject<Identifier | null>;
|
||||
documentsSortFieldRef: MutableRefObject<string>;
|
||||
documentsSortDirectionRef: MutableRefObject<string>;
|
||||
@@ -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<FolderId>(initialSelectedFolder || 'root');
|
||||
const [currentFolder, setCurrentFolder] = useState<FolderSummary | null>(null);
|
||||
const [currentSubfolders, setCurrentSubfolders] = useState<FolderSummary[]>([]);
|
||||
@@ -245,8 +286,7 @@ const useFolderTree = ({
|
||||
params.sort = sortField;
|
||||
params.dir = sortDirection;
|
||||
}
|
||||
const requestConfig = Object.keys(params).length ? { params } : {};
|
||||
const { data } = await apiClient.get<FolderContentsEntry>(`/folders/${path}/contents`, requestConfig);
|
||||
const data = await listFolderContents<FolderContentsEntry>(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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, unknown> } };
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<DocumentResponse> => {
|
||||
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<string, unknown> = {}): Promi
|
||||
return Array.isArray(data) ? data : [];
|
||||
};
|
||||
|
||||
export const getFolderTree = async (): Promise<FolderTreeNode[]> => {
|
||||
const { data } = await api.get<FolderTreeNode[]>('/folders/tree');
|
||||
export const getFolderTree = async (): Promise<FolderTreeResponseItem[]> => {
|
||||
const { data } = await api.get<FolderTreeResponseItem[]>('/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<void> => {
|
||||
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<TagResponse[]> => {
|
||||
const { data } = await api.get<TagResponse[]>('/tags');
|
||||
return Array.isArray(data) ? data : [];
|
||||
};
|
||||
|
||||
export const updateTag = async (
|
||||
tagId: Identifier,
|
||||
payload: { label?: string; color?: string | null },
|
||||
): Promise<void> => {
|
||||
await api.patch(`/tags/${tagId}`, payload);
|
||||
};
|
||||
|
||||
export const deleteTag = async (tagId: Identifier): Promise<void> => {
|
||||
await api.delete(`/tags/${tagId}`);
|
||||
};
|
||||
|
||||
export const listCorrespondents = async (): Promise<CorrespondentResponse[]> => {
|
||||
const { data } = await api.get<CorrespondentResponse[]>('/correspondents');
|
||||
return Array.isArray(data) ? data : [];
|
||||
};
|
||||
|
||||
export const createCorrespondent = async (payload: { name: string }): Promise<CorrespondentResponse> => {
|
||||
const { data } = await api.post<CorrespondentResponse>('/correspondents', payload);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateCorrespondent = async (
|
||||
correspondentId: Identifier,
|
||||
payload: { name?: string },
|
||||
): Promise<void> => {
|
||||
await api.patch(`/correspondents/${correspondentId}`, payload);
|
||||
};
|
||||
|
||||
export const deleteCorrespondent = async (correspondentId: Identifier): Promise<void> => {
|
||||
await api.delete(`/correspondents/${correspondentId}`);
|
||||
};
|
||||
|
||||
export const addDocumentCorrespondent = async (
|
||||
documentId: Identifier,
|
||||
correspondentId: Identifier,
|
||||
): Promise<void> => {
|
||||
await api.post(`/documents/${documentId}/correspondents`, {
|
||||
assignments: [{ correspondent_id: correspondentId }],
|
||||
replace: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeDocumentCorrespondent = async (
|
||||
documentId: Identifier,
|
||||
correspondentId: Identifier,
|
||||
): Promise<void> => {
|
||||
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 <T = any>(
|
||||
path: string,
|
||||
params?: Record<string, unknown>,
|
||||
): Promise<T> => {
|
||||
const { data } = await api.get<T>(`/folders/${path}/contents`, { params });
|
||||
return data;
|
||||
};
|
||||
|
||||
export type { ApiTokenRecord } from './apiTypes';
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface TagResponse {
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
interface CorrespondentResponse {
|
||||
export interface CorrespondentResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
metadata: Record<string, unknown>;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.status-toast-container {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
top: 3rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 5000000;
|
||||
|
||||
Reference in New Issue
Block a user