refactor: centralize API error notifications with a new useNotifyApiError hook
This commit is contained in:
@@ -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<string | null>(null);
|
||||
const notifyApiError = useNotifyApiError();
|
||||
|
||||
const resetPreviewState = useCallback(() => {
|
||||
previewReturnPathRef.current = null;
|
||||
|
||||
@@ -10,6 +10,8 @@ type ApiClient = {
|
||||
get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => 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<Identifier[] | null>(null);
|
||||
const [searchLoading, setSearchLoading] = useState<boolean>(false);
|
||||
const [searchTrigger, setSearchTrigger] = useState<number>(0);
|
||||
const notifyApiError = useNotifyApiError();
|
||||
|
||||
const toggleTagFilter = useCallback((tagId: Identifier) => {
|
||||
if (!tagId) return;
|
||||
|
||||
@@ -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<string | null>;
|
||||
mapDocumentCaches?: (mapper: (doc: any) => any) => void;
|
||||
}
|
||||
|
||||
const useCorrespondents = ({
|
||||
notifyApiError,
|
||||
tenantIdRef,
|
||||
mapDocumentCaches,
|
||||
}: UseCorrespondentsOptions) => {
|
||||
const [correspondents, setCorrespondents] = useState<Correspondent[]>([]);
|
||||
const { showToast } = useStatusToast();
|
||||
const notifyApiError = useNotifyApiError();
|
||||
|
||||
const refreshCorrespondents = useCallback(async () => {
|
||||
const requestTenantId = tenantIdRef.current;
|
||||
|
||||
@@ -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<DocumentId, Document>;
|
||||
@@ -90,7 +90,6 @@ interface UseDocumentMutationsArgs {
|
||||
focusedDocumentId: DocumentId | null;
|
||||
setFocusedEntryKey: Dispatch<SetStateAction<string | null>>;
|
||||
focusedEntryKey: string | null;
|
||||
notifyApiError: NotifyApiError;
|
||||
mapDocumentCaches: MapDocumentCaches;
|
||||
folderNodes: Map<FolderId, FolderNode>;
|
||||
setFolderNodes: Dispatch<SetStateAction<Map<FolderId, FolderNode>>>;
|
||||
@@ -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<DocumentId | Document>, targetFolderId?: NullableFolderId) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<TenantId | null>;
|
||||
setActiveTagFilters: (updater: (prev: Array<TagId>) => Array<TagId>) => 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<Tag[]>([]);
|
||||
const { showToast } = useStatusToast();
|
||||
const notifyApiError = useNotifyApiError();
|
||||
|
||||
const refreshTags = useCallback(async () => {
|
||||
const requestTenantId = tenantIdRef.current;
|
||||
|
||||
@@ -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<void>;
|
||||
refreshCorrespondents: () => Promise<void>;
|
||||
loadFolder: (folderId: FolderId, options?: { preserveSearch?: boolean }) => Promise<void>;
|
||||
@@ -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;
|
||||
|
||||
@@ -11,10 +11,11 @@ interface CorrespondentOption {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
||||
|
||||
interface UseDocumentCorrespondentActionsArgs {
|
||||
correspondents: CorrespondentOption[];
|
||||
handleCorrespondentCreate: (payload: { name: string }) => Promise<CorrespondentOption | null>;
|
||||
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<string, CorrespondentOption>();
|
||||
|
||||
@@ -29,13 +29,14 @@ interface FolderClickHandlers {
|
||||
onDragLeave: (event: DragEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
||||
|
||||
interface UseFolderTreeActionsOptions {
|
||||
token?: string | null;
|
||||
folderNodes: Map<FolderKey, FolderNode>;
|
||||
setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => 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> | void;
|
||||
moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -15,12 +15,13 @@ interface TagManager {
|
||||
buildPayload: (input: { label: string }) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
||||
|
||||
interface UseDocumentTaggingArgs {
|
||||
tags: TagRecord[];
|
||||
tagManager: TagManager;
|
||||
refreshTags: () => Promise<void> | 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<BulkTagOperationResult> => {
|
||||
|
||||
@@ -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<DropOverlayState>({
|
||||
active: false,
|
||||
@@ -136,6 +137,7 @@ const useDocumentUploads = ({
|
||||
const queueIdRef = useRef(0);
|
||||
const [uploadQueue, setUploadQueue] = useState<UploadQueueItem[]>([]);
|
||||
const { showToast } = useStatusToast();
|
||||
const notifyApiError = useNotifyApiError();
|
||||
|
||||
const uploadFile = useCallback(
|
||||
async (file: File, targetFolderId: FolderId) => {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
@@ -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<RevokePasskeyResult>;
|
||||
}
|
||||
|
||||
const usePasskeys = ({ notifyApiError, token }: UsePasskeysArgs): UsePasskeysResult => {
|
||||
const usePasskeys = ({ token }: UsePasskeysArgs): UsePasskeysResult => {
|
||||
const [passkeys, setPasskeys] = useState<PasskeyRecord[]>([]);
|
||||
const [passkeysSupported, setPasskeysSupported] = useState<boolean | null>(null);
|
||||
const [passkeysLoading, setPasskeysLoading] = useState(false);
|
||||
const [registeringPasskey, setRegisteringPasskey] = useState(false);
|
||||
const [revokingPasskeyId, setRevokingPasskeyId] = useState<PasskeyId | null>(null);
|
||||
const { showToast } = useStatusToast();
|
||||
const notifyApiError = useNotifyApiError();
|
||||
|
||||
const refreshPasskeys = useCallback(async (): Promise<void> => {
|
||||
if (!token) {
|
||||
|
||||
Reference in New Issue
Block a user