diff --git a/frontend/src/app/useDocumentPreview.ts b/frontend/src/app/useDocumentPreview.ts index 589814e..c1e1192 100644 --- a/frontend/src/app/useDocumentPreview.ts +++ b/frontend/src/app/useDocumentPreview.ts @@ -12,6 +12,8 @@ import type { Document } from '../types/documents'; type NavigateHandler = (path: string, options?: { replace?: boolean }) => void; +import useNotifyApiError from '../hooks/useNotifyApiError'; + interface UseDocumentPreviewArgs { routeDocumentId?: DocumentId | null; documentsManager: { @@ -22,7 +24,6 @@ interface UseDocumentPreviewArgs { ingest: (docs: unknown[]) => { canonical: Document[]; changed: boolean }; }; selectedFolder?: FolderId | null; - notifyApiError: (error: unknown, message: string) => void; navigate: NavigateHandler; locationPathname: string; locationSearch: string; @@ -44,7 +45,6 @@ const useDocumentPreview = ({ routeDocumentId, documentsManager, selectedFolder, - notifyApiError, navigate, locationPathname, locationSearch, @@ -52,6 +52,7 @@ const useDocumentPreview = ({ setActivePreviewId, }: UseDocumentPreviewArgs): UseDocumentPreviewResult => { const previewReturnPathRef = useRef(null); + const notifyApiError = useNotifyApiError(); const resetPreviewState = useCallback(() => { previewReturnPathRef.current = null; diff --git a/frontend/src/app/useDocumentsSearch.ts b/frontend/src/app/useDocumentsSearch.ts index f1c5794..78eb7b0 100644 --- a/frontend/src/app/useDocumentsSearch.ts +++ b/frontend/src/app/useDocumentsSearch.ts @@ -10,6 +10,8 @@ type ApiClient = { get: (url: string, config?: { params?: Record }) => Promise<{ data: T }>; }; +import useNotifyApiError from '../hooks/useNotifyApiError'; + interface UseDocumentsSearchArgs { api: ApiClient; token?: string | null; @@ -20,7 +22,6 @@ interface UseDocumentsSearchArgs { searchIncludeDescendants?: boolean; documentsSortField?: string; documentsSortDirection?: string; - notifyApiError: (error: unknown, message: string) => void; setSearchIncludeDescendants: (value: boolean) => void; documentsManager: { ingest: (docs: unknown[]) => { canonical: Document[]; changed: boolean }; @@ -72,7 +73,6 @@ const useDocumentsSearch = ({ searchIncludeDescendants, documentsSortField, documentsSortDirection, - notifyApiError, setSearchIncludeDescendants, documentsManager, }: UseDocumentsSearchArgs): UseDocumentsSearchResult => { @@ -82,6 +82,7 @@ const useDocumentsSearch = ({ const [searchResultIds, setSearchResultIds] = useState(null); const [searchLoading, setSearchLoading] = useState(false); const [searchTrigger, setSearchTrigger] = useState(0); + const notifyApiError = useNotifyApiError(); const toggleTagFilter = useCallback((tagId: Identifier) => { if (!tagId) return; diff --git a/frontend/src/documents/data/useCorrespondents.ts b/frontend/src/documents/data/useCorrespondents.ts index 1388c60..e5f2c10 100644 --- a/frontend/src/documents/data/useCorrespondents.ts +++ b/frontend/src/documents/data/useCorrespondents.ts @@ -4,19 +4,20 @@ import type { Correspondent } from '../../types/documents'; import { listCorrespondents, createCorrespondent, updateCorrespondent, deleteCorrespondent } from '../../lib/api/apiClient'; +import useNotifyApiError from '../../hooks/useNotifyApiError'; + interface UseCorrespondentsOptions { - notifyApiError: (error: unknown, fallback: string) => void; tenantIdRef: MutableRefObject; mapDocumentCaches?: (mapper: (doc: any) => any) => void; } const useCorrespondents = ({ - notifyApiError, tenantIdRef, mapDocumentCaches, }: UseCorrespondentsOptions) => { const [correspondents, setCorrespondents] = useState([]); const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const refreshCorrespondents = useCallback(async () => { const requestTenantId = tenantIdRef.current; diff --git a/frontend/src/documents/data/useDocumentMutations.ts b/frontend/src/documents/data/useDocumentMutations.ts index d68fbd9..94c97bd 100644 --- a/frontend/src/documents/data/useDocumentMutations.ts +++ b/frontend/src/documents/data/useDocumentMutations.ts @@ -40,8 +40,6 @@ type RemoveDocumentsFromCaches = (documentIds: DocumentId[]) => void; type CloseDocumentPreview = () => void; -type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; - interface Tag { id: DocumentId; label: string; @@ -73,6 +71,8 @@ interface DocumentTagExtras { input?: { value?: string } | null; } +import useNotifyApiError from '../../hooks/useNotifyApiError'; + interface UseDocumentMutationsArgs { token?: string | null; documentLookup: Map; @@ -90,7 +90,6 @@ interface UseDocumentMutationsArgs { focusedDocumentId: DocumentId | null; setFocusedEntryKey: Dispatch>; focusedEntryKey: string | null; - notifyApiError: NotifyApiError; mapDocumentCaches: MapDocumentCaches; folderNodes: Map; setFolderNodes: Dispatch>>; @@ -160,7 +159,6 @@ const useDocumentMutations = ({ focusedDocumentId, setFocusedEntryKey, focusedEntryKey, - notifyApiError, mapDocumentCaches, folderNodes, setFolderNodes, @@ -177,6 +175,7 @@ const useDocumentMutations = ({ ingestDocuments, }: UseDocumentMutationsArgs): UseDocumentMutationsResult => { const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const moveDocumentsToFolder = useCallback( async (documentIds: Array, targetFolderId?: NullableFolderId) => { diff --git a/frontend/src/documents/data/useDocumentsWorkspace.ts b/frontend/src/documents/data/useDocumentsWorkspace.ts index 3e526ef..3539b1d 100644 --- a/frontend/src/documents/data/useDocumentsWorkspace.ts +++ b/frontend/src/documents/data/useDocumentsWorkspace.ts @@ -13,7 +13,7 @@ import { useNavigate, } from 'react-router-dom'; import AssetManager, { getAssetFromVersion } from '../../lib/assets/AssetManager'; -import useApiError from '../../hooks/useApiError'; +import useNotifyApiError from '../../hooks/useNotifyApiError'; import TagManager from '../../lib/assets/TagManager'; import { useManagementModals } from '../../app/useManagementModals'; import { useAppDispatch, useAppState } from '../../lib/store/appState'; @@ -144,19 +144,7 @@ const useDocumentsWorkspace = ({ ? (tenantOptionsRaw as TenantOption[]) : []; const { showToast } = useStatusToast(); - - const reportApiError = useApiError({ - onReport: useCallback( - ({ message, variant }) => showToast(message, variant), - [showToast], - ), - }); - - const notifyApiError = useCallback( - (error, fallbackMessage, variant = 'error') => - reportApiError(error, { message: fallbackMessage, variant }), - [reportApiError], - ); + const notifyApiError = useNotifyApiError(); const [creatingFolder, setCreatingFolder] = useState(false); @@ -389,7 +377,6 @@ const useDocumentsWorkspace = ({ searchIncludeDescendants, documentsSortField, documentsSortDirection, - notifyApiError, setSearchIncludeDescendants, documentsManager, }); @@ -457,7 +444,6 @@ const useDocumentsWorkspace = ({ routeDocumentId: previewDocumentId, documentsManager, selectedFolder, - notifyApiError, navigate, locationPathname: location.pathname, locationSearch: location.search, @@ -506,7 +492,6 @@ const useDocumentsWorkspace = ({ handleTagDelete, setTags, } = useTags({ - notifyApiError, tenantIdRef, tagManager, setActiveTagFilters, @@ -535,7 +520,6 @@ const useDocumentsWorkspace = ({ handleCorrespondentDelete, setCorrespondents, } = useCorrespondents({ - notifyApiError, tenantIdRef, mapDocumentCaches, }); @@ -548,7 +532,6 @@ const useDocumentsWorkspace = ({ } = useDocumentCorrespondentActions({ correspondents, handleCorrespondentCreate, - notifyApiError, updateDocumentCaches, }); @@ -562,7 +545,6 @@ const useDocumentsWorkspace = ({ registerPasskey, revokePasskey, } = usePasskeys({ - notifyApiError, token, }); @@ -594,7 +576,6 @@ const useDocumentsWorkspace = ({ tagManager, refreshTags, resolveTargetDocumentIds, - notifyApiError, updateDocumentCaches, }); @@ -612,7 +593,6 @@ const useDocumentsWorkspace = ({ ensureFolderData, refreshCurrentFolder, shellRef, - notifyApiError, }); const { @@ -747,7 +727,6 @@ const useDocumentsWorkspace = ({ focusedDocumentId, setFocusedEntryKey, focusedEntryKey, - notifyApiError, mapDocumentCaches, folderNodes, setFolderNodes, @@ -777,7 +756,6 @@ const useDocumentsWorkspace = ({ setFolderNodes, selectedFolder, setSelectedFolder, - notifyApiError, navigate, handleFileDrop, moveDocumentsToFolder, @@ -1105,7 +1083,6 @@ const useDocumentsWorkspace = ({ appDispatch, currentTenantId, resetWorkspaceState, - notifyApiError, refreshTags, refreshCorrespondents, loadFolder, diff --git a/frontend/src/documents/data/useTags.ts b/frontend/src/documents/data/useTags.ts index 9016b1c..8b01ce3 100644 --- a/frontend/src/documents/data/useTags.ts +++ b/frontend/src/documents/data/useTags.ts @@ -9,9 +9,10 @@ interface TagManagerInterface { buildPayload: (input: { label?: string; color?: string | null }) => { label: string; color: string | null }; } +import useNotifyApiError from '../../hooks/useNotifyApiError'; + interface UseTagsOptions { // apiClient removed - notifyApiError: (error: unknown, fallback: string) => void; tagManager: TagManagerInterface; tenantIdRef: MutableRefObject; setActiveTagFilters: (updater: (prev: Array) => Array) => void; @@ -20,7 +21,6 @@ interface UseTagsOptions { const useTags = ({ // apiClient removed - notifyApiError, tagManager, tenantIdRef, setActiveTagFilters, @@ -28,6 +28,7 @@ const useTags = ({ }: UseTagsOptions) => { const [tags, setTags] = useState([]); const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const refreshTags = useCallback(async () => { const requestTenantId = tenantIdRef.current; diff --git a/frontend/src/documents/data/useTenantManager.ts b/frontend/src/documents/data/useTenantManager.ts index 28ba4d2..4360f9d 100644 --- a/frontend/src/documents/data/useTenantManager.ts +++ b/frontend/src/documents/data/useTenantManager.ts @@ -10,11 +10,12 @@ interface TenantOption { name?: string; } +import useNotifyApiError from '../../hooks/useNotifyApiError'; + interface UseTenantManagerOptions { appDispatch: (action: any) => void; currentTenantId: TenantId | null; resetWorkspaceState: () => void; - notifyApiError: (error: unknown, message: string) => void; refreshTags: () => Promise; refreshCorrespondents: () => Promise; loadFolder: (folderId: FolderId, options?: { preserveSearch?: boolean }) => Promise; @@ -28,7 +29,6 @@ const useTenantManager = ({ appDispatch, currentTenantId, resetWorkspaceState, - notifyApiError, refreshTags, refreshCorrespondents, loadFolder, @@ -38,6 +38,7 @@ const useTenantManager = ({ tenantIdRef, }: UseTenantManagerOptions) => { const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const handleTenantSelect = useCallback( async (tenantOption: TenantOption | null, { refreshOnly = false }: { refreshOnly?: boolean } = {}) => { const requestedTenantId = tenantOption?.id ?? null; diff --git a/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts b/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts index 884151a..b738059 100644 --- a/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts +++ b/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts @@ -11,10 +11,11 @@ interface CorrespondentOption { [key: string]: unknown; } +import useNotifyApiError from '../../../hooks/useNotifyApiError'; + interface UseDocumentCorrespondentActionsArgs { correspondents: CorrespondentOption[]; handleCorrespondentCreate: (payload: { name: string }) => Promise; - notifyApiError: (error: unknown, fallback: string) => void; updateDocumentCaches?: ( id: Identifier, updater: (doc: { correspondents?: CorrespondentOption[] } | null) => { correspondents?: CorrespondentOption[] } | null, @@ -24,10 +25,10 @@ interface UseDocumentCorrespondentActionsArgs { const useDocumentCorrespondentActions = ({ correspondents, handleCorrespondentCreate, - notifyApiError, updateDocumentCaches, }: UseDocumentCorrespondentActionsArgs) => { const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const correspondentLookupByName = useMemo(() => { const map = new Map(); diff --git a/frontend/src/documents/features/folders/useFolderTreeActions.ts b/frontend/src/documents/features/folders/useFolderTreeActions.ts index 7edb772..bfa4330 100644 --- a/frontend/src/documents/features/folders/useFolderTreeActions.ts +++ b/frontend/src/documents/features/folders/useFolderTreeActions.ts @@ -29,13 +29,14 @@ interface FolderClickHandlers { onDragLeave: (event: DragEvent) => void; } +import useNotifyApiError from '../../../hooks/useNotifyApiError'; + interface UseFolderTreeActionsOptions { token?: string | null; folderNodes: Map; setFolderNodes: (updater: (prev: Map) => Map) => void; selectedFolder: FolderKey; setSelectedFolder: (folderId: FolderKey) => void; - notifyApiError: (error: unknown, message?: string) => void; navigate?: (path: string, options?: { replace?: boolean }) => void; handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise | void; moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise; @@ -53,7 +54,6 @@ const useFolderTreeActions = ({ setFolderNodes, selectedFolder, setSelectedFolder, - notifyApiError, navigate, handleFileDrop, moveDocumentsToFolder, @@ -65,6 +65,7 @@ const useFolderTreeActions = ({ setCreatingFolder, }: UseFolderTreeActionsOptions) => { const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const moveFolder = useCallback( async (folderId: FolderKey, targetFolderId: FolderKey | null) => { diff --git a/frontend/src/documents/features/tagging/useDocumentTagActions.ts b/frontend/src/documents/features/tagging/useDocumentTagActions.ts index ceb5712..211cb1d 100644 --- a/frontend/src/documents/features/tagging/useDocumentTagActions.ts +++ b/frontend/src/documents/features/tagging/useDocumentTagActions.ts @@ -15,12 +15,13 @@ interface TagManager { buildPayload: (input: { label: string }) => Record; } +import useNotifyApiError from '../../../hooks/useNotifyApiError'; + interface UseDocumentTaggingArgs { tags: TagRecord[]; tagManager: TagManager; refreshTags: () => Promise | void; resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[]; - notifyApiError: (error: unknown, message: string) => void; updateDocumentCaches?: (id: Identifier, updater: (doc: TagRecord | null) => TagRecord | null) => void; } @@ -43,10 +44,10 @@ const useDocumentTagActions = ({ tagManager, refreshTags, resolveTargetDocumentIds, - notifyApiError, updateDocumentCaches, }: UseDocumentTaggingArgs) => { const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const bulkTagOperation = useCallback( async ({ labels, action, documentIds }: BulkTagOperationArgs): Promise => { diff --git a/frontend/src/documents/features/upload/useDocumentUploads.ts b/frontend/src/documents/features/upload/useDocumentUploads.ts index e321b9d..c19d096 100644 --- a/frontend/src/documents/features/upload/useDocumentUploads.ts +++ b/frontend/src/documents/features/upload/useDocumentUploads.ts @@ -118,6 +118,8 @@ interface UseDocumentUploadsResult { clearUploadQueue: () => void; } +import useNotifyApiError from '../../../hooks/useNotifyApiError'; + const useDocumentUploads = ({ token, selectedFolder, @@ -125,7 +127,6 @@ const useDocumentUploads = ({ ensureFolderData, refreshCurrentFolder, shellRef, - notifyApiError, }: UseDocumentUploadsArgs): UseDocumentUploadsResult => { const [dropOverlayState, setDropOverlayState] = useState({ active: false, @@ -136,6 +137,7 @@ const useDocumentUploads = ({ const queueIdRef = useRef(0); const [uploadQueue, setUploadQueue] = useState([]); const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const uploadFile = useCallback( async (file: File, targetFolderId: FolderId) => { diff --git a/frontend/src/hooks/useApiError.ts b/frontend/src/hooks/useApiError.ts index ae9ba47..1886924 100644 --- a/frontend/src/hooks/useApiError.ts +++ b/frontend/src/hooks/useApiError.ts @@ -16,9 +16,7 @@ interface UseApiErrorOptions { onReport?: (payload: ReportPayload) => void; } -const noop = () => {}; - -const normalizeMessage = (error: unknown): string => { +export const normalizeMessage = (error: unknown): string => { if (!error) return 'Something went wrong.'; if (typeof (error as { trim?: () => string })?.trim === 'function') { return (error as { trim: () => string }).trim(); @@ -31,7 +29,7 @@ const normalizeMessage = (error: unknown): string => { const useApiError = ({ logger = console, - onReport = noop, + onReport, }: UseApiErrorOptions = {}) => { return useCallback( ( @@ -40,7 +38,9 @@ const useApiError = ({ ) => { const normalizedMessage = message || normalizeMessage(error); logger.error('[API]', normalizedMessage, error); - onReport({ message: normalizedMessage, variant, retry, error }); + if (onReport) { + onReport({ message: normalizedMessage, variant, retry, error }); + } return normalizedMessage; }, [logger, onReport], diff --git a/frontend/src/hooks/useNotifyApiError.ts b/frontend/src/hooks/useNotifyApiError.ts new file mode 100644 index 0000000..40e4ce6 --- /dev/null +++ b/frontend/src/hooks/useNotifyApiError.ts @@ -0,0 +1,18 @@ +import { useCallback } from 'react'; +import { useStatusToast, ToastVariant } from '../lib/context/StatusToastContext'; +import { normalizeMessage } from './useApiError'; + +const useNotifyApiError = () => { + const { showToast } = useStatusToast(); + + return useCallback( + (error: unknown, fallbackMessage?: string, variant: ToastVariant = 'error') => { + const message = fallbackMessage || normalizeMessage(error); + console.error('[API]', message, error); + showToast(message, variant); + }, + [showToast], + ); +}; + +export default useNotifyApiError; diff --git a/frontend/src/settings/usePasskeys.ts b/frontend/src/settings/usePasskeys.ts index 8c6e28e..511c205 100644 --- a/frontend/src/settings/usePasskeys.ts +++ b/frontend/src/settings/usePasskeys.ts @@ -15,8 +15,6 @@ import { } from '../lib/api/apiClient'; import type { PasskeyId } from '../types/identifiers'; -type NotifyApiErrorFn = (error: unknown, message: string) => void; - type ApiError = { response?: { status?: number; @@ -69,8 +67,9 @@ type RevokePasskeyResult = | { ok: true } | { ok: false; reason: RevokePasskeyFailureReason; message?: string }; +import useNotifyApiError from '../hooks/useNotifyApiError'; + interface UsePasskeysArgs { - notifyApiError: NotifyApiErrorFn; token?: string | null; } @@ -88,13 +87,14 @@ interface UsePasskeysResult { ) => Promise; } -const usePasskeys = ({ notifyApiError, token }: UsePasskeysArgs): UsePasskeysResult => { +const usePasskeys = ({ token }: UsePasskeysArgs): UsePasskeysResult => { const [passkeys, setPasskeys] = useState([]); const [passkeysSupported, setPasskeysSupported] = useState(null); const [passkeysLoading, setPasskeysLoading] = useState(false); const [registeringPasskey, setRegisteringPasskey] = useState(false); const [revokingPasskeyId, setRevokingPasskeyId] = useState(null); const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); const refreshPasskeys = useCallback(async (): Promise => { if (!token) {