refactor: Decouple frontend API calls from a shared client instance

This commit is contained in:
2025-12-04 21:59:39 +01:00
parent d06fc133e7
commit 4308ff220b
13 changed files with 248 additions and 136 deletions
@@ -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,
+48 -9
View File
@@ -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,
+11 -16
View File
@@ -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,
+104 -4
View File
@@ -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';
+5 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
.status-toast-container {
position: fixed;
top: 2rem;
top: 3rem;
left: 50%;
transform: translateX(-50%);
z-index: 5000000;