apiclient

This commit is contained in:
2025-11-22 04:22:18 +01:00
parent 41005390e8
commit 5498cf4342
13 changed files with 224 additions and 46 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ const SettingsRoute: React.FC<SettingsRouteProps> = ({ open = true, onClose }) =
capabilities,
capabilitiesLoading,
refreshCapabilities,
} = useCapabilities({ api, notifyApiError, token });
} = useCapabilities({ notifyApiError, token });
useEffect(() => {
refreshTokens();
+7 -14
View File
@@ -4,6 +4,7 @@ import type {
MutableRefObject,
SetStateAction,
} from 'react';
import { fetchDocument } from '../lib/apiClient';
type DocumentId = string | number;
type FolderId = DocumentId | 'root';
@@ -23,10 +24,6 @@ type DocumentLink = {
expiresAt?: number;
};
interface ApiClient {
get: <T = unknown>(path: string) => Promise<{ data: T }>;
}
type NavigateHandler = (path: string, options?: { replace?: boolean }) => void;
interface UseDocumentPreviewArgs {
@@ -39,7 +36,6 @@ interface UseDocumentPreviewArgs {
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean };
};
selectedFolder?: FolderId | null;
api: ApiClient;
notifyApiError: (error: unknown, message: string) => void;
navigate: NavigateHandler;
locationPathname: string;
@@ -65,7 +61,6 @@ const useDocumentPreview = ({
routeDocumentId,
documentsManager,
selectedFolder,
api,
notifyApiError,
navigate,
locationPathname,
@@ -120,8 +115,8 @@ const useDocumentPreview = ({
const request: Promise<DocumentLink | null> = (async () => {
try {
const docResponse = await api.get<{ document?: Record<string, any> }>(`/documents/${documentId}`);
const download = docResponse.data?.document?.current_version?.download || null;
const docResponse = await fetchDocument(documentId);
const download = docResponse?.current_version?.download || null;
const downloadUrl = download?.url;
if (!downloadUrl) {
throw new Error('Document missing download url');
@@ -129,8 +124,8 @@ const useDocumentPreview = ({
const entry: DocumentLink = {
url: downloadUrl,
contentType: docResponse.data?.document?.current_version?.version?.content_type || null,
filename: docResponse.data?.document?.filename,
contentType: docResponse?.content_type || null,
filename: docResponse?.filename,
expiresAt: download?.expires_at,
};
setDocumentLinks((prev) => {
@@ -150,7 +145,7 @@ const useDocumentPreview = ({
previewInflightRef.current.set(documentId, request);
return request;
},
[documentLinks, api, notifyApiError],
[documentLinks, notifyApiError],
);
const ensurePreviewData = useCallback(
@@ -166,8 +161,7 @@ const useDocumentPreview = ({
}
if (!doc) {
const { data } = await api.get(`/documents/${documentId}`);
const fetched = (data as { document?: DocumentLike })?.document || data;
const fetched = await fetchDocument(documentId);
const { canonical } = documentsManager.ingest([fetched as unknown]);
doc = (canonical[0] as DocumentLike | undefined) || null;
if (!doc) {
@@ -189,7 +183,6 @@ const useDocumentPreview = ({
documentsManager,
ensureDownloadUrl,
setActivePreviewId,
api,
],
);
+2 -1
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { TAG_FILTER_UNTAGGED } from './appLayoutUtils';
import { listDocuments } from '../lib/apiClient';
type Identifier = string | number;
@@ -227,7 +228,7 @@ const useDocumentsSearch = ({
if (documentsSortDirection) {
params.dir = documentsSortDirection;
}
const { data } = await api.get<unknown[]>('/documents', { params });
const data = await listDocuments(params);
if (cancelled) return;
const results = Array.isArray(data) ? data : [];
+2 -3
View File
@@ -566,7 +566,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
}
const docContentType = doc?.content_type ?? null;
const versionContentType = (doc?.current_version as { version?: { content_type?: string | null } } | null)?.version?.content_type ?? null;
const applyEntry = (entry?: DocumentLinkLike | null) => {
if (!entry?.url) {
@@ -575,8 +574,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
}
setOverlaySource({
url: entry.url,
alt: doc.title as string | undefined,
contentType: entry.contentType || docContentType || versionContentType || undefined,
alt: doc.title,
contentType: docContentType || undefined,
});
};
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getFolderTree } from '../lib/apiClient';
import {
TrashIcon,
AnalyzeIcon,
@@ -10,7 +11,7 @@ import {
} from '../ui/icons';
import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './SelectionAssignmentMenu';
import SelectionSummary from './SelectionSummary';
import { api, useAppState } from '../app/appState';
import { useAppState } from '../app/appState';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
const ROOT_FOLDER_LABEL = 'Documents';
@@ -312,7 +313,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const fetchPromise = (async () => {
setLoadingFolders(true);
try {
const { data } = await api.get('/folders/tree');
const data = await getFolderTree();
const options = buildFolderTreeOptions(data);
setRemoteFolderOptions(options);
return options;
@@ -291,7 +291,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
};
}
const docContentType = previewDoc.content_type;
const versionContentType = previewDoc.current_version?.version?.content_type;
const versionContentType = previewDoc.current_version?.content_type;
const contentFallback = docContentType || versionContentType || null;
const applyEntry = (entry?: DocumentLinkLike | null) => {
@@ -2,6 +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/appLayoutUtils';
import { fetchDocument } from '../../lib/apiClient';
type Identifier = string | number;
type FolderId = Identifier | 'root' | null;
@@ -174,8 +175,7 @@ const useDocumentUploads = ({
let conflictDocument = null;
if (conflictId) {
try {
const { data } = await apiClient.get(`/documents/${conflictId}`);
conflictDocument = (data as any)?.document ?? data ?? null;
conflictDocument = await fetchDocument(conflictId);
} catch (fetchError) {
console.warn('[Uploads] failed to fetch conflict document', fetchError);
}
@@ -48,6 +48,7 @@ import useTags from './useTags';
import useCorrespondents from './useCorrespondents';
import useTenantManager from './useTenantManager';
import useDocuments from './useDocuments';
import { fetchDocument } from '../../lib/apiClient';
import useFolderTree from './useFolderTree';
import useFolderTreeActions from './useFolderTreeActions';
import useDocumentTagging from './useDocumentTagging';
@@ -238,7 +239,7 @@ const useDocumentsWorkspace = ({
if (!documentId) {
return null;
}
const { data } = await api.get(`/documents/${documentId}`);
const data = await fetchDocument(documentId);
return extractDocumentFromResponse(data);
},
[extractDocumentFromResponse],
@@ -449,7 +450,6 @@ const useDocumentsWorkspace = ({
routeDocumentId: previewDocumentId,
documentsManager,
selectedFolder,
api,
notifyApiError,
navigate,
locationPathname: location.pathname,
+88
View File
@@ -0,0 +1,88 @@
import api from './api';
import type {
ApiTokenRecord,
AssetResponse,
CapabilityResponse,
CapabilitySetResponse,
DownloadLink,
DocumentResponse,
FolderTreeNode,
Identifier,
PasskeySummary,
} from './apiTypes';
const normalizeNumber = (value: unknown): number | undefined => {
const n = Number(value);
return Number.isFinite(n) ? n : undefined;
};
const normalizeDownload = (input?: DownloadLink | null): DownloadLink | null => {
if (!input?.url) {
return null;
}
const expires_at = normalizeNumber(input.expires_at);
if (!expires_at) {
return null;
}
return { url: input.url, expires_at };
};
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);
if (doc?.current_version?.download) {
doc.current_version.download = normalizeDownload(doc.current_version.download);
}
return doc;
};
export const fetchAsset = async (id: Identifier): Promise<AssetResponse> => {
const { data } = await api.get<AssetResponse>(`/assets/${id}`);
const download = normalizeDownload(data.download);
return {
...data,
download,
};
};
export const listDocuments = async (params: Record<string, unknown> = {}): Promise<DocumentResponse[]> => {
const { data } = await api.get<DocumentResponse[]>('/documents', { params });
return Array.isArray(data) ? data : [];
};
export const getFolderTree = async (): Promise<FolderTreeNode[]> => {
const { data } = await api.get<FolderTreeNode[]>('/folders/tree');
return Array.isArray(data) ? data : [];
};
export const listCapabilitySets = async (): Promise<CapabilitySetResponse[]> => {
const { data } = await api.get<CapabilitySetResponse[]>('/capability-sets');
return Array.isArray(data) ? data : [];
};
export const listCapabilities = async (): Promise<CapabilityResponse[]> => {
const { data } = await api.get<CapabilityResponse[]>('/capabilities');
return Array.isArray(data) ? data : [];
};
export const listApiTokens = async (): Promise<ApiTokenRecord[]> => {
const { data } = await api.get<ApiTokenRecord[]>('/profile/api-tokens');
return Array.isArray(data) ? data : [];
};
export const listPasskeys = async (): Promise<PasskeySummary[]> => {
const { data } = await api.get<PasskeySummary[]>('/profile/passkeys');
return Array.isArray(data) ? data : [];
};
export type {
DownloadLink,
DocumentResponse,
AssetResponse,
FolderTreeNode,
CapabilitySetResponse,
CapabilityResponse,
ApiTokenRecord,
PasskeySummary,
Identifier,
} from './apiTypes';
+105
View File
@@ -0,0 +1,105 @@
// Types aligned with OpenAPI schemas for common endpoints.
export type Identifier = string | number;
export interface DownloadLink {
url: string;
expires_at: number;
}
export interface TagResponse {
id: string;
label: string;
color?: string | null;
}
export interface CorrespondentResponse {
id: string;
name: string;
metadata: Record<string, unknown>;
}
export interface AssetResponse {
id: string;
asset_type: string;
mime_type: string;
metadata: Record<string, unknown>;
download?: DownloadLink | null;
}
export interface DocumentVersionResponse {
id: string;
version_number: number;
size_bytes: number;
checksum: string;
created_at: string;
content_type: string | null;
metadata: Record<string, unknown>;
download: DownloadLink;
assets?: AssetResponse[] | null;
}
export interface DocumentResponse {
id: string;
filename: string;
title: string;
original_name: string;
content_type?: string | null;
folder_id?: string | null;
created_at: string;
updated_at: string;
issued_at?: string | null;
metadata: Record<string, unknown>;
tags: TagResponse[];
correspondents?: CorrespondentResponse[];
current_version?: DocumentVersionResponse | null;
}
export interface FolderInfo {
id: string;
name: string;
parent_id?: string | null;
created_at?: string;
updated_at?: string;
}
export interface FolderTreeNode extends FolderInfo {
children?: FolderTreeNode[];
}
export interface CapabilitySetResponse {
id: string;
slug: string;
is_system: boolean;
cap_version: number;
capabilities: string[];
}
export interface CapabilityResponse {
id?: string;
name: string;
}
export interface TenantSnippet {
id: string;
name: string;
}
export interface ApiTokenRecord {
id: string;
label?: string | null;
capability_set_id: string;
created_at: string;
last_used_at?: string | null;
expires_at?: string | null;
}
export interface PasskeySummary {
id: string;
nickname?: string | null;
createdAt: string;
lastUsedAt?: string | null;
transports?: string[];
revokedAt?: string | null;
revokedReason?: string | null;
}
+3 -5
View File
@@ -268,7 +268,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
if (!href) {
return null;
}
const contentType = document.current_version?.version?.content_type || document.content_type || null;
const contentType = document.content_type || null;
const filename = document.current_version?.filename || document.filename || document.title || null;
return {
url: href,
@@ -363,15 +363,13 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
if (!resolvedDocumentLink?.url || !document) {
return null;
}
const docContentType = document.content_type;
const versionContentType = document.current_version?.version?.content_type;
const normalizedContentType = resolvedDocumentLink.contentType || docContentType || versionContentType || null;
const normalizedContentType = document.content_type || null;
return {
url: resolvedDocumentLink.url,
alt: document.title,
contentType: normalizedContentType || undefined,
};
}, [resolvedDocumentLink?.url, resolvedDocumentLink?.contentType, document]);
}, [document, resolvedDocumentLink?.url]);
const headerActions = createDocumentViewerHeaderActions({
document,
+5 -13
View File
@@ -1,16 +1,12 @@
import { useCallback, useEffect, useState } from 'react';
interface CapabilitiesApi {
get: (path: string) => Promise<{ data: unknown }>;
}
import { listCapabilities } from '../lib/apiClient';
interface UseCapabilitiesOptions {
api: CapabilitiesApi;
notifyApiError?: (error: unknown, fallbackMessage: string) => void;
token?: string | null;
}
const useCapabilities = ({ api, notifyApiError, token }: UseCapabilitiesOptions) => {
const useCapabilities = ({ notifyApiError, token }: UseCapabilitiesOptions) => {
const [capabilities, setCapabilities] = useState<string[]>([]);
const [capabilitiesLoading, setCapabilitiesLoading] = useState(false);
@@ -21,19 +17,15 @@ const useCapabilities = ({ api, notifyApiError, token }: UseCapabilitiesOptions)
}
setCapabilitiesLoading(true);
try {
const { data } = await api.get('/capabilities');
if (Array.isArray(data)) {
setCapabilities(data as string[]);
} else {
setCapabilities([]);
}
const data = await listCapabilities();
setCapabilities(Array.isArray(data) ? data.map((item) => item.name) : []);
} catch (error) {
notifyApiError?.(error, 'Failed to load capabilities.');
setCapabilities([]);
} finally {
setCapabilitiesLoading(false);
}
}, [api, notifyApiError, token]);
}, [notifyApiError, token]);
useEffect(() => {
if (token) {
+3 -2
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { listCapabilitySets } from '../lib/apiClient';
type Identifier = string | number;
@@ -51,14 +52,14 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
}
setCapabilitySetsLoading(true);
try {
const { data } = await api.get<CapabilitySet[]>('/capability-sets');
const data = await listCapabilitySets();
applyCapabilitySets(Array.isArray(data) ? data : []);
} catch (error) {
notifyApiError?.(error, 'Failed to load capability sets.');
} finally {
setCapabilitySetsLoading(false);
}
}, [api, applyCapabilitySets, notifyApiError, token]);
}, [applyCapabilitySets, notifyApiError, token]);
useEffect(() => {
if (token) {