apiclient
This commit is contained in:
@@ -57,7 +57,7 @@ const SettingsRoute: React.FC<SettingsRouteProps> = ({ open = true, onClose }) =
|
||||
capabilities,
|
||||
capabilitiesLoading,
|
||||
refreshCapabilities,
|
||||
} = useCapabilities({ api, notifyApiError, token });
|
||||
} = useCapabilities({ notifyApiError, token });
|
||||
|
||||
useEffect(() => {
|
||||
refreshTokens();
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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 : [];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user