From 595c170c0047c1273c9399efd85b4b96e849a499 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 7 Dec 2025 23:46:10 +0100 Subject: [PATCH] refactor: centralize frontend status message handling with `useStatusToast` --- frontend/src/app/useManagementModals.tsx | 12 ++-- frontend/src/documents/data/useAuthManager.ts | 14 ++--- .../documents/data/useBulkDocumentActions.ts | 35 ++++++------ .../src/documents/data/useCorrespondents.ts | 16 +++--- .../documents/data/useDocumentMutations.ts | 55 +++++++++---------- .../documents/data/useDocumentsWorkspace.ts | 47 ++++------------ frontend/src/documents/data/useTags.ts | 16 +++--- .../src/documents/data/useTenantManager.ts | 8 +-- .../useDocumentCorrespondentActions.ts | 21 +++---- .../features/folders/useFolderTreeActions.ts | 43 ++++++++------- .../features/tagging/useDocumentTagActions.ts | 29 +++++----- .../features/upload/useDocumentUploads.ts | 10 ++-- frontend/src/settings/usePasskeys.ts | 18 +++--- 13 files changed, 149 insertions(+), 175 deletions(-) diff --git a/frontend/src/app/useManagementModals.tsx b/frontend/src/app/useManagementModals.tsx index 78d85d2..2ab3476 100644 --- a/frontend/src/app/useManagementModals.tsx +++ b/frontend/src/app/useManagementModals.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ReactNode } from 'react'; +import { useStatusToast } from '../lib/context/StatusToastContext'; import TagsPanel from '../tags/TagsPanel'; import CorrespondentsPanel, { CorrespondentsPanelProps } from '../correspondents/CorrespondentsPanel'; import PanelHeader from '../components/PanelHeader'; @@ -30,7 +31,6 @@ interface UseManagementModalsArgs { onCorrespondentCreate?: (...args: any[]) => void | Promise; onCorrespondentUpdate?: (...args: any[]) => void | Promise; onCorrespondentDelete?: (...args: any[]) => void | Promise; - setStatusMessage?: (message: string, variant?: string) => void; } interface UseManagementModalsResult { @@ -53,8 +53,8 @@ export const useManagementModals = ({ onCorrespondentCreate, onCorrespondentUpdate, onCorrespondentDelete, - setStatusMessage, }: UseManagementModalsArgs): UseManagementModalsResult => { + const { showToast } = useStatusToast(); const [activeModal, setActiveModal] = useState(null); const openTagsModal = useCallback(() => setActiveModal(TAGS_MODAL), []); @@ -117,7 +117,7 @@ export const useManagementModals = ({ onCreateTag={onTagCreate} onUpdateTag={onTagUpdate} onDeleteTag={onTagDelete} - onNotify={setStatusMessage} + onNotify={showToast} /> @@ -130,7 +130,7 @@ export const useManagementModals = ({ onTagDelete, onTagUpdate, refreshTags, - setStatusMessage, + showToast, tags, ]); @@ -199,7 +199,7 @@ export const useManagementModals = ({ onCreate={handleCorrespondentCreateSafe} onUpdate={handleCorrespondentUpdateSafe} onDelete={handleCorrespondentDeleteSafe} - onNotify={setStatusMessage} + onNotify={showToast} /> @@ -213,7 +213,7 @@ export const useManagementModals = ({ handleCorrespondentDeleteSafe, handleCorrespondentUpdateSafe, refreshCorrespondents, - setStatusMessage, + showToast, ]); const managementModals = ( diff --git a/frontend/src/documents/data/useAuthManager.ts b/frontend/src/documents/data/useAuthManager.ts index 1ee2a00..5be9fba 100644 --- a/frontend/src/documents/data/useAuthManager.ts +++ b/frontend/src/documents/data/useAuthManager.ts @@ -1,18 +1,16 @@ import { useCallback, useEffect, useRef } from 'react'; import type { MutableRefObject } from 'react'; import { clearAuthToken, logoutSession, refreshSession, setAuthToken } from '../../lib/api/apiClient'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; type AppStatus = string; -type AppDispatch = (action: { type: string; [key: string]: unknown }) => void; - -type SetStatusMessage = (message: string, variant?: string) => void; +type AppDispatch = (action: { type: string;[key: string]: unknown }) => void; interface UseAuthManagerArgs { token?: string | null; appStatus: AppStatus; appDispatch: AppDispatch; - setStatusMessage: SetStatusMessage; } interface UseAuthManagerResult { @@ -25,10 +23,10 @@ const useAuthManager = ({ token, appStatus, appDispatch, - setStatusMessage, }: UseAuthManagerArgs): UseAuthManagerResult => { const tokenRef = useRef(token); const initialRefreshAttemptedRef = useRef(Boolean(token)); + const { showToast } = useStatusToast(); const refreshAccessToken = useCallback(async (): Promise => { console.log('[Auth] Attempting to refresh access token…'); @@ -61,7 +59,7 @@ const useAuthManager = ({ if (!token && !initialRefreshAttemptedRef.current && appStatus === 'logged-out') { initialRefreshAttemptedRef.current = true; console.log('[Auth] Attempting refresh at startup'); - refreshAccessToken().catch(() => {}); + refreshAccessToken().catch(() => { }); } }, [token, appStatus, refreshAccessToken]); @@ -73,9 +71,9 @@ const useAuthManager = ({ } finally { clearAuthToken(); appDispatch({ type: 'LOGOUT' }); - setStatusMessage('Logged out.', 'info'); + showToast('Logged out.', 'info'); } - }, [appDispatch, setStatusMessage]); + }, [appDispatch, showToast]); return { tokenRef, refreshAccessToken, handleLogout }; }; diff --git a/frontend/src/documents/data/useBulkDocumentActions.ts b/frontend/src/documents/data/useBulkDocumentActions.ts index 0715741..b66cc84 100644 --- a/frontend/src/documents/data/useBulkDocumentActions.ts +++ b/frontend/src/documents/data/useBulkDocumentActions.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; import { assignCorrespondentsBulk } from '../../lib/api/apiClient'; import type { Identifier } from '../../types/identifiers'; import type { MessageOptions } from '../../types/documents'; @@ -16,7 +17,6 @@ interface UseBulkDocumentActionsArgs { resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[]; correspondentLookupByName: Map; handleCorrespondentCreate: (payload: { name: string }) => Promise<{ id?: Identifier } | null>; - setStatusMessage: (message: string, variant?: string) => void; selectedDocumentIds?: Identifier[]; selectedFolderIds?: Identifier[]; handleDocumentsDelete: (ids: Identifier[], options?: MessageOptions) => Promise; @@ -29,7 +29,6 @@ const useBulkDocumentActions = ({ resolveTargetDocumentIds, correspondentLookupByName, handleCorrespondentCreate, - setStatusMessage, selectedDocumentIds, selectedFolderIds, handleDocumentsDelete, @@ -37,16 +36,18 @@ const useBulkDocumentActions = ({ clearDocumentSelection, updateDocumentCaches, }: UseBulkDocumentActionsArgs) => { + const { showToast } = useStatusToast(); + const handleBulkCorrespondentAdd = useCallback( async ({ name, input, documentIds }: { name?: string; input?: HTMLInputElement | null; documentIds?: Identifier[] }) => { const trimmed = name?.trim?.() || ''; if (!trimmed) { - setStatusMessage('Correspondent name is required.', 'error'); + showToast('Correspondent name is required.', 'error'); return; } const targets = resolveTargetDocumentIds(documentIds); if (!targets.length) { - setStatusMessage('Select documents before assigning correspondents.', 'error'); + showToast('Select documents before assigning correspondents.', 'error'); return; } @@ -60,7 +61,7 @@ const useBulkDocumentActions = ({ } if (!target?.id) { - setStatusMessage('Unable to resolve correspondent.', 'error'); + showToast('Unable to resolve correspondent.', 'error'); return; } @@ -94,12 +95,12 @@ const useBulkDocumentActions = ({ const assignedSuffix = assigned === 1 ? '' : 's'; if (removed > 0) { const removedSuffix = removed === 1 ? '' : 's'; - setStatusMessage( + showToast( `Correspondent assigned (${assigned}) and replaced ${removed} link${removedSuffix}.`, 'success', ); } else { - setStatusMessage( + showToast( `Correspondent assigned to ${assigned} document${assignedSuffix}.`, 'success', ); @@ -113,7 +114,7 @@ const useBulkDocumentActions = ({ correspondentLookupByName, handleCorrespondentCreate, resolveTargetDocumentIds, - setStatusMessage, + showToast, updateDocumentCaches, ], ); @@ -121,14 +122,14 @@ const useBulkDocumentActions = ({ const handleBulkCorrespondentRemove = useCallback( async ({ assignments = [], documentIds }: { assignments?: CorrespondentAssignment[]; documentIds?: Identifier[] }) => { if (!assignments.length) { - setStatusMessage('Select a correspondent to remove.', 'error'); + showToast('Select a correspondent to remove.', 'error'); return; } const targets = resolveTargetDocumentIds(documentIds); if (!targets.length) { - setStatusMessage('Select documents before removing correspondents.', 'error'); + showToast('Select documents before removing correspondents.', 'error'); return; } @@ -162,18 +163,18 @@ const useBulkDocumentActions = ({ if (removed > 0) { const removedSuffix = removed === 1 ? '' : 's'; - setStatusMessage( + showToast( `Correspondent removed from ${removed} link${removedSuffix}.`, 'success', ); } else if (assigned > 0) { const assignedSuffix = assigned === 1 ? '' : 's'; - setStatusMessage(`Correspondent updated ${assigned} link${assignedSuffix}.`, 'info'); + showToast(`Correspondent updated ${assigned} link${assignedSuffix}.`, 'info'); } else { - setStatusMessage('No correspondents changed.', 'info'); + showToast('No correspondents changed.', 'info'); } }, - [resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches], + [resolveTargetDocumentIds, showToast, updateDocumentCaches], ); const handleDeleteSelection = useCallback(async () => { @@ -217,7 +218,7 @@ const useBulkDocumentActions = ({ } if (!docsOk || !foldersOk) { - setStatusMessage('Some items could not be deleted. Ensure folders are empty before deletion.', 'error'); + showToast('Some items could not be deleted. Ensure folders are empty before deletion.', 'error'); return; } @@ -231,14 +232,14 @@ const useBulkDocumentActions = ({ successParts.push(folderIds.length === 1 ? 'Folder deleted.' : 'Folders deleted.'); } - setStatusMessage(successParts.join(' '), 'success'); + showToast(successParts.join(' '), 'success'); }, [ clearDocumentSelection, handleDocumentsDelete, handleFolderDelete, selectedDocumentIds, selectedFolderIds, - setStatusMessage, + showToast, ]); return { diff --git a/frontend/src/documents/data/useCorrespondents.ts b/frontend/src/documents/data/useCorrespondents.ts index b4246d1..1388c60 100644 --- a/frontend/src/documents/data/useCorrespondents.ts +++ b/frontend/src/documents/data/useCorrespondents.ts @@ -1,22 +1,22 @@ import { MutableRefObject, useCallback, useState } from 'react'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; import type { Correspondent } from '../../types/documents'; import { listCorrespondents, createCorrespondent, updateCorrespondent, deleteCorrespondent } from '../../lib/api/apiClient'; interface UseCorrespondentsOptions { notifyApiError: (error: unknown, fallback: string) => void; - setStatusMessage: (message: string, variant?: string) => void; tenantIdRef: MutableRefObject; mapDocumentCaches?: (mapper: (doc: any) => any) => void; } const useCorrespondents = ({ notifyApiError, - setStatusMessage, tenantIdRef, mapDocumentCaches, }: UseCorrespondentsOptions) => { const [correspondents, setCorrespondents] = useState([]); + const { showToast } = useStatusToast(); const refreshCorrespondents = useCallback(async () => { const requestTenantId = tenantIdRef.current; @@ -56,7 +56,7 @@ const useCorrespondents = ({ try { await updateCorrespondent(correspondentId, payload); await refreshCorrespondents(); - setStatusMessage('Correspondent updated.', 'success'); + showToast('Correspondent updated.', 'success'); return true; } catch (error) { const message = error.response?.data?.error || 'Failed to update correspondent.'; @@ -64,7 +64,7 @@ const useCorrespondents = ({ throw new Error(message); } }, - [notifyApiError, refreshCorrespondents, setStatusMessage], + [notifyApiError, refreshCorrespondents, showToast], ); const handleCorrespondentCreate = useCallback( @@ -76,7 +76,7 @@ const useCorrespondents = ({ try { const data = await createCorrespondent({ name: trimmed }); await refreshCorrespondents(); - setStatusMessage('Correspondent created.', 'success'); + showToast('Correspondent created.', 'success'); return data; } catch (error) { const message = error.response?.data?.error || 'Failed to create correspondent.'; @@ -84,7 +84,7 @@ const useCorrespondents = ({ throw new Error(message); } }, - [notifyApiError, refreshCorrespondents, setStatusMessage], + [notifyApiError, refreshCorrespondents, showToast], ); const handleCorrespondentDelete = useCallback( @@ -110,7 +110,7 @@ const useCorrespondents = ({ mapDocumentCaches?.(stripFromDoc); - setStatusMessage('Correspondent deleted.', 'success'); + showToast('Correspondent deleted.', 'success'); return true; } catch (error) { const message = error.response?.data?.error || 'Failed to delete correspondent.'; @@ -118,7 +118,7 @@ const useCorrespondents = ({ throw new Error(message); } }, - [mapDocumentCaches, notifyApiError, refreshCorrespondents, setStatusMessage], + [mapDocumentCaches, notifyApiError, refreshCorrespondents, showToast], ); return { diff --git a/frontend/src/documents/data/useDocumentMutations.ts b/frontend/src/documents/data/useDocumentMutations.ts index 6ccbe5a..d68fbd9 100644 --- a/frontend/src/documents/data/useDocumentMutations.ts +++ b/frontend/src/documents/data/useDocumentMutations.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils'; import { getEntryId, isDocumentEntry } from '../../app/entryKey'; import { @@ -19,8 +20,6 @@ import type { Document, MessageOptions } from '../../types/documents'; type FolderId = FolderIdentifier | 'root'; type NullableFolderId = FolderId | null; -type StatusLevel = 'success' | 'error' | 'info' | string; - type DocumentCacheMapper = ( doc: Document | null, ) => Document | null; @@ -43,8 +42,6 @@ type CloseDocumentPreview = () => void; type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; -type SetStatusMessage = (message: string, level?: StatusLevel) => void; - interface Tag { id: DocumentId; label: string; @@ -94,7 +91,6 @@ interface UseDocumentMutationsArgs { setFocusedEntryKey: Dispatch>; focusedEntryKey: string | null; notifyApiError: NotifyApiError; - setStatusMessage: SetStatusMessage; mapDocumentCaches: MapDocumentCaches; folderNodes: Map; setFolderNodes: Dispatch>>; @@ -165,7 +161,6 @@ const useDocumentMutations = ({ setFocusedEntryKey, focusedEntryKey, notifyApiError, - setStatusMessage, mapDocumentCaches, folderNodes, setFolderNodes, @@ -181,6 +176,8 @@ const useDocumentMutations = ({ extractDocumentFromResponse, ingestDocuments, }: UseDocumentMutationsArgs): UseDocumentMutationsResult => { + const { showToast } = useStatusToast(); + const moveDocumentsToFolder = useCallback( async (documentIds: Array, targetFolderId?: NullableFolderId) => { const uniqueIds = Array.from( @@ -254,7 +251,7 @@ const useDocumentMutations = ({ const count = uniqueIds.length; const suffix = count === 1 ? '' : 's'; - setStatusMessage(`Moved ${count} document${suffix} to ${targetLabel}.`, 'success'); + showToast(`Moved ${count} document${suffix} to ${targetLabel}.`, 'success'); if (updatedDocsMap.size) { mapDocumentCaches((doc) => { @@ -335,7 +332,7 @@ const useDocumentMutations = ({ setFocusedEntryKey, focusedEntryKey, notifyApiError, - setStatusMessage, + showToast, mapDocumentCaches, ], ); @@ -343,19 +340,19 @@ const useDocumentMutations = ({ const handleThumbnailRegeneration = useCallback( async (documentId: DocumentId) => { if (!token) { - setStatusMessage('Log in to manage assets.', 'error'); + showToast('Log in to manage assets.', 'error'); return; } try { await queueDocumentReanalysis(documentId, { force: true }); - setStatusMessage('Document re-analysis queued.', 'info'); + showToast('Document re-analysis queued.', 'info'); await refreshCurrentFolder(); } catch (error) { const message = (error as Record)?.response?.data?.error || 'Failed to request thumbnail generation.'; notifyApiError(error, message); } }, - [token, refreshCurrentFolder, notifyApiError, setStatusMessage], + [token, refreshCurrentFolder, notifyApiError, showToast], ); const handleDocumentsDelete = useCallback( @@ -365,7 +362,7 @@ const useDocumentMutations = ({ } if (!token) { - setStatusMessage('Log in to manage documents.', 'error'); + showToast('Log in to manage documents.', 'error'); return false; } @@ -380,7 +377,7 @@ const useDocumentMutations = ({ if (showMessage) { const message = documentIds.length === 1 ? 'Document deleted.' : 'Documents deleted.'; - setStatusMessage(message, 'success'); + showToast(message, 'success'); } return true; } catch (error) { @@ -396,7 +393,7 @@ const useDocumentMutations = ({ previewDocumentId, closeDocumentPreview, notifyApiError, - setStatusMessage, + showToast, ], ); @@ -404,7 +401,7 @@ const useDocumentMutations = ({ async (documentId: DocumentId, nextTitle: string) => { const trimmed = nextTitle?.trim?.() || ''; if (!trimmed) { - setStatusMessage('Document title cannot be empty.', 'error'); + showToast('Document title cannot be empty.', 'error'); return false; } try { @@ -422,7 +419,7 @@ const useDocumentMutations = ({ }); } - setStatusMessage('Document title updated.', 'success'); + showToast('Document title updated.', 'success'); return true; } catch (error) { const message = (error as Record)?.response?.data?.error || 'Failed to update document title.'; @@ -434,7 +431,7 @@ const useDocumentMutations = ({ extractDocumentFromResponse, ingestDocuments, notifyApiError, - setStatusMessage, + showToast, updateDocumentCaches, ], ); @@ -458,7 +455,7 @@ const useDocumentMutations = ({ } const message = payload.issued_at ? 'Issued date updated.' : 'Issued date cleared.'; - setStatusMessage(message, 'success'); + showToast(message, 'success'); return true; } catch (error) { const message = (error as Record)?.response?.data?.error || 'Failed to update issued date.'; @@ -470,7 +467,7 @@ const useDocumentMutations = ({ extractDocumentFromResponse, ingestDocuments, notifyApiError, - setStatusMessage, + showToast, updateDocumentCaches, ], ); @@ -505,7 +502,7 @@ const useDocumentMutations = ({ } return { ...doc, tags: [...currentTags, cachedTag] }; }); - setStatusMessage('Tag assigned.', 'success'); + showToast('Tag assigned.', 'success'); return true; } catch (error) { const message = (error as Record)?.response?.data?.error || 'Failed to assign tag.'; @@ -513,7 +510,7 @@ const useDocumentMutations = ({ return false; } }, - [notifyApiError, setStatusMessage, updateDocumentCaches], + [notifyApiError, showToast, updateDocumentCaches], ); const handleDocumentTagAdd = useCallback( @@ -616,7 +613,7 @@ const useDocumentMutations = ({ try { await deleteDocumentTag(documentId, tagId); applyTagRemovalToCaches(documentId, tagId); - setStatusMessage('Tag removed.', 'success'); + showToast('Tag removed.', 'success'); return true; } catch (error) { const message = (error as Record)?.response?.data?.error || 'Failed to remove tag.'; @@ -624,20 +621,20 @@ const useDocumentMutations = ({ return false; } }, - [applyTagRemovalToCaches, notifyApiError, setStatusMessage], + [applyTagRemovalToCaches, notifyApiError, showToast], ); const handleFolderDelete = useCallback( async (folderId?: FolderId, { showMessage = true }: MessageOptions = {}) => { if (!token) { if (showMessage) { - setStatusMessage('Log in to manage folders.', 'error'); + showToast('Log in to manage folders.', 'error'); } return false; } if (!folderId || folderId === 'root') { if (showMessage) { - setStatusMessage('The root folder cannot be removed.', 'error'); + showToast('The root folder cannot be removed.', 'error'); } return false; } @@ -648,7 +645,7 @@ const useDocumentMutations = ({ const hasDocs = (contents.documents || []).length > 0; if (hasChildren || hasDocs) { if (showMessage) { - setStatusMessage('Folder must be empty before it can be deleted.', 'error'); + showToast('Folder must be empty before it can be deleted.', 'error'); } return false; } @@ -686,14 +683,14 @@ const useDocumentMutations = ({ } if (showMessage) { - setStatusMessage('Folder deleted.', 'success'); + showToast('Folder deleted.', 'success'); } return true; } catch (error) { const message = (error as Record)?.response?.data?.error || 'Failed to delete folder.'; notifyApiError(error, message); if (showMessage) { - setStatusMessage(message, 'error'); + showToast(message, 'error'); } return false; } @@ -706,7 +703,7 @@ const useDocumentMutations = ({ setSelectedFolder, setFolderNodes, notifyApiError, - setStatusMessage, + showToast, ], ); diff --git a/frontend/src/documents/data/useDocumentsWorkspace.ts b/frontend/src/documents/data/useDocumentsWorkspace.ts index 8c820c3..3e526ef 100644 --- a/frontend/src/documents/data/useDocumentsWorkspace.ts +++ b/frontend/src/documents/data/useDocumentsWorkspace.ts @@ -38,7 +38,7 @@ import { isDocumentEntry } from '../../app/entryKey'; import useDocumentsSearch from '../../app/useDocumentsSearch'; -import { useStatusToast, type ToastVariant } from '../../lib/context/StatusToastContext'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; import useAuthManager from './useAuthManager'; import useTenantManager from './useTenantManager'; import useDocuments from './useDocuments'; @@ -144,33 +144,26 @@ const useDocumentsWorkspace = ({ ? (tenantOptionsRaw as TenantOption[]) : []; const { showToast } = useStatusToast(); - const setStatusMessage = useCallback( - (message?: string | null, variant: ToastVariant = 'info') => { - if (message) { - showToast(message, variant); - } - }, - [showToast], - ); - const handleApiReport = useCallback( - ({ message, variant }) => setStatusMessage(message, variant), - [setStatusMessage], - ); const reportApiError = useApiError({ - onReport: handleApiReport, + onReport: useCallback( + ({ message, variant }) => showToast(message, variant), + [showToast], + ), }); + const notifyApiError = useCallback( (error, fallbackMessage, variant = 'error') => reportApiError(error, { message: fallbackMessage, variant }), [reportApiError], ); + const [creatingFolder, setCreatingFolder] = useState(false); + const { tokenRef, handleLogout } = useAuthManager({ token, appStatus, appDispatch, - setStatusMessage, }); @@ -514,9 +507,8 @@ const useDocumentsWorkspace = ({ setTags, } = useTags({ notifyApiError, - setStatusMessage, - tagManager, tenantIdRef, + tagManager, setActiveTagFilters, mapDocumentCaches, }); @@ -544,7 +536,6 @@ const useDocumentsWorkspace = ({ setCorrespondents, } = useCorrespondents({ notifyApiError, - setStatusMessage, tenantIdRef, mapDocumentCaches, }); @@ -558,7 +549,6 @@ const useDocumentsWorkspace = ({ correspondents, handleCorrespondentCreate, notifyApiError, - setStatusMessage, updateDocumentCaches, }); @@ -573,7 +563,6 @@ const useDocumentsWorkspace = ({ revokePasskey, } = usePasskeys({ notifyApiError, - setStatusMessage, token, }); @@ -606,7 +595,6 @@ const useDocumentsWorkspace = ({ refreshTags, resolveTargetDocumentIds, notifyApiError, - setStatusMessage, updateDocumentCaches, }); @@ -625,7 +613,6 @@ const useDocumentsWorkspace = ({ refreshCurrentFolder, shellRef, notifyApiError, - setStatusMessage, }); const { @@ -761,7 +748,6 @@ const useDocumentsWorkspace = ({ setFocusedEntryKey, focusedEntryKey, notifyApiError, - setStatusMessage, mapDocumentCaches, folderNodes, setFolderNodes, @@ -792,7 +778,6 @@ const useDocumentsWorkspace = ({ selectedFolder, setSelectedFolder, notifyApiError, - setStatusMessage, navigate, handleFileDrop, moveDocumentsToFolder, @@ -905,7 +890,6 @@ const useDocumentsWorkspace = ({ resolveTargetDocumentIds, correspondentLookupByName, handleCorrespondentCreate, - setStatusMessage, selectedDocumentIds, selectedFolderIds, handleDocumentsDelete, @@ -914,8 +898,6 @@ const useDocumentsWorkspace = ({ updateDocumentCaches, }); - - const ensureAssetUrl = useCallback( async (documentId, asset, { force = false } = {}) => { if (!documentId || !asset?.id) { @@ -942,8 +924,6 @@ const useDocumentsWorkspace = ({ [assetManager, updateDocumentCaches, notifyApiError], ); - - const handlePromptCreateFolder = useCallback(async (parentId?: Identifier | null) => { if (creatingFolder) { return; @@ -954,19 +934,19 @@ const useDocumentsWorkspace = ({ } const trimmed = input.trim(); if (!trimmed) { - setStatusMessage('Folder name cannot be empty.', 'error'); + showToast('Folder name cannot be empty.', 'error'); return; } setCreatingFolder(true); try { const success = await handleFolderCreate(trimmed, parentId); if (!success) { - setStatusMessage('Unable to create folder. Check the status message for details.', 'error'); + showToast('Unable to create folder. Check the status message for details.', 'error'); } } finally { setCreatingFolder(false); } - }, [creatingFolder, handleFolderCreate, setStatusMessage]); + }, [creatingFolder, handleFolderCreate, showToast]); const { managementModals, openTagsModal, openCorrespondentsModal } = useManagementModals({ locationPathname: location.pathname, @@ -980,7 +960,6 @@ const useDocumentsWorkspace = ({ onCorrespondentCreate: handleCorrespondentCreate, onCorrespondentUpdate: handleCorrespondentUpdate, onCorrespondentDelete: handleCorrespondentDelete, - setStatusMessage, }); const [settingsOpen, setSettingsOpen] = useState(false); @@ -1126,7 +1105,6 @@ const useDocumentsWorkspace = ({ appDispatch, currentTenantId, resetWorkspaceState, - setStatusMessage, notifyApiError, refreshTags, refreshCorrespondents, @@ -1148,7 +1126,6 @@ const useDocumentsWorkspace = ({ }; const uiContext = { - setStatusMessage, notifyApiError, settingsOpen, openSettings, diff --git a/frontend/src/documents/data/useTags.ts b/frontend/src/documents/data/useTags.ts index 5c6cd88..9016b1c 100644 --- a/frontend/src/documents/data/useTags.ts +++ b/frontend/src/documents/data/useTags.ts @@ -1,4 +1,5 @@ import { MutableRefObject, useCallback, useState } from 'react'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; import type { TagId, TenantId } from '../../types/identifiers'; import type { Tag } from '../../types/documents'; @@ -11,7 +12,6 @@ interface TagManagerInterface { interface UseTagsOptions { // apiClient removed notifyApiError: (error: unknown, fallback: string) => void; - setStatusMessage: (message: string, variant?: string) => void; tagManager: TagManagerInterface; tenantIdRef: MutableRefObject; setActiveTagFilters: (updater: (prev: Array) => Array) => void; @@ -21,13 +21,13 @@ interface UseTagsOptions { const useTags = ({ // apiClient removed notifyApiError, - setStatusMessage, tagManager, tenantIdRef, setActiveTagFilters, mapDocumentCaches, }: UseTagsOptions) => { const [tags, setTags] = useState([]); + const { showToast } = useStatusToast(); const refreshTags = useCallback(async () => { const requestTenantId = tenantIdRef.current; @@ -66,7 +66,7 @@ const useTags = ({ try { await updateTag(tagId, payload); await refreshTags(); - setStatusMessage('Tag updated.', 'success'); + showToast('Tag updated.', 'success'); return true; } catch (error) { const message = error.response?.data?.error || 'Failed to update tag.'; @@ -74,7 +74,7 @@ const useTags = ({ throw new Error(message); } }, - [notifyApiError, refreshTags, setStatusMessage], + [notifyApiError, refreshTags, showToast], ); const handleTagCreate = useCallback( @@ -83,14 +83,14 @@ const useTags = ({ try { await createTag(payload); await refreshTags(); - setStatusMessage('Tag created.', 'success'); + showToast('Tag created.', 'success'); } catch (error) { const message = error.response?.data?.error || 'Failed to create tag.'; notifyApiError(error, message); throw new Error(message); } }, - [notifyApiError, refreshTags, setStatusMessage, tagManager], + [notifyApiError, refreshTags, showToast, tagManager], ); const handleTagDelete = useCallback( @@ -117,7 +117,7 @@ const useTags = ({ mapDocumentCaches?.(stripTagFromDoc); await refreshTags(); - setStatusMessage('Tag deleted.', 'success'); + showToast('Tag deleted.', 'success'); return true; } catch (error) { const message = error.response?.data?.error || 'Failed to delete tag.'; @@ -125,7 +125,7 @@ const useTags = ({ throw new Error(message); } }, - [mapDocumentCaches, notifyApiError, refreshTags, setActiveTagFilters, setStatusMessage], + [mapDocumentCaches, notifyApiError, refreshTags, setActiveTagFilters, showToast], ); return { diff --git a/frontend/src/documents/data/useTenantManager.ts b/frontend/src/documents/data/useTenantManager.ts index 0a6b3c9..28ba4d2 100644 --- a/frontend/src/documents/data/useTenantManager.ts +++ b/frontend/src/documents/data/useTenantManager.ts @@ -1,6 +1,7 @@ import { MutableRefObject, useCallback } from 'react'; import type { NavigateFunction } from 'react-router-dom'; import type { FolderId, TenantId } from '../../types/identifiers'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; import { api, listTenants, switchTenant } from '../../lib/api/apiClient'; @@ -13,7 +14,6 @@ interface UseTenantManagerOptions { appDispatch: (action: any) => void; currentTenantId: TenantId | null; resetWorkspaceState: () => void; - setStatusMessage: (message: string, variant?: string) => void; notifyApiError: (error: unknown, message: string) => void; refreshTags: () => Promise; refreshCorrespondents: () => Promise; @@ -28,7 +28,6 @@ const useTenantManager = ({ appDispatch, currentTenantId, resetWorkspaceState, - setStatusMessage, notifyApiError, refreshTags, refreshCorrespondents, @@ -38,6 +37,7 @@ const useTenantManager = ({ tokenRef, tenantIdRef, }: UseTenantManagerOptions) => { + const { showToast } = useStatusToast(); const handleTenantSelect = useCallback( async (tenantOption: TenantOption | null, { refreshOnly = false }: { refreshOnly?: boolean } = {}) => { const requestedTenantId = tenantOption?.id ?? null; @@ -88,7 +88,7 @@ const useTenantManager = ({ await loadFolder('root', { preserveSearch: false }); const tenantLabel = data?.tenant?.name || data?.tenant?.id || 'tenant'; - setStatusMessage(`Switched to ${tenantLabel}.`, 'info'); + showToast(`Switched to ${tenantLabel}.`, 'info'); } catch (error) { notifyApiError(error, 'Failed to switch tenant.'); } @@ -103,7 +103,7 @@ const useTenantManager = ({ refreshCorrespondents, refreshTags, resetWorkspaceState, - setStatusMessage, + showToast, tenantIdRef, tokenRef, ], diff --git a/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts b/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts index e372d41..884151a 100644 --- a/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts +++ b/frontend/src/documents/features/correspondents/useDocumentCorrespondentActions.ts @@ -1,4 +1,5 @@ import { useCallback, useMemo } from 'react'; +import { useStatusToast } from '../../../lib/context/StatusToastContext'; import type { Identifier } from '../../../types/identifiers'; @@ -14,7 +15,6 @@ interface UseDocumentCorrespondentActionsArgs { correspondents: CorrespondentOption[]; handleCorrespondentCreate: (payload: { name: string }) => Promise; notifyApiError: (error: unknown, fallback: string) => void; - setStatusMessage: (message: string, variant?: string) => void; updateDocumentCaches?: ( id: Identifier, updater: (doc: { correspondents?: CorrespondentOption[] } | null) => { correspondents?: CorrespondentOption[] } | null, @@ -25,9 +25,10 @@ const useDocumentCorrespondentActions = ({ correspondents, handleCorrespondentCreate, notifyApiError, - setStatusMessage, updateDocumentCaches, }: UseDocumentCorrespondentActionsArgs) => { + const { showToast } = useStatusToast(); + const correspondentLookupByName = useMemo(() => { const map = new Map(); correspondents.forEach((correspondent) => { @@ -71,7 +72,7 @@ const useDocumentCorrespondentActions = ({ }); } if (notify) { - setStatusMessage('Correspondent assigned.', 'success'); + showToast('Correspondent assigned.', 'success'); } return true; } catch (error) { @@ -80,7 +81,7 @@ const useDocumentCorrespondentActions = ({ throw new Error(message); } }, - [correspondents, notifyApiError, setStatusMessage, updateDocumentCaches], + [correspondents, notifyApiError, showToast, updateDocumentCaches], ); const handleCorrespondentRemove = useCallback( @@ -103,7 +104,7 @@ const useDocumentCorrespondentActions = ({ }); } if (notify) { - setStatusMessage('Correspondent removed.', 'success'); + showToast('Correspondent removed.', 'success'); } return true; } catch (error) { @@ -112,7 +113,7 @@ const useDocumentCorrespondentActions = ({ throw new Error(message); } }, - [notifyApiError, setStatusMessage, updateDocumentCaches], + [notifyApiError, showToast, updateDocumentCaches], ); const normalizeOption = ( @@ -138,7 +139,7 @@ const useDocumentCorrespondentActions = ({ } const trimmed = name?.trim?.() || ''; if (!trimmed) { - setStatusMessage('Correspondent name is required.', 'error'); + showToast('Correspondent name is required.', 'error'); return; } @@ -152,7 +153,7 @@ const useDocumentCorrespondentActions = ({ } if (!target?.id) { - setStatusMessage('Unable to resolve correspondent.', 'error'); + showToast('Unable to resolve correspondent.', 'error'); return; } @@ -166,7 +167,7 @@ const useDocumentCorrespondentActions = ({ input.value = ''; } } catch (error) { - setStatusMessage('Failed to assign correspondent.', 'error'); + showToast('Failed to assign correspondent.', 'error'); console.error('[documents] assign correspondent failed', error); } }, @@ -174,7 +175,7 @@ const useDocumentCorrespondentActions = ({ correspondentLookupByName, handleCorrespondentCreate, handleDocumentCorrespondentAttach, - setStatusMessage, + showToast, ], ); diff --git a/frontend/src/documents/features/folders/useFolderTreeActions.ts b/frontend/src/documents/features/folders/useFolderTreeActions.ts index 2022fde..7edb772 100644 --- a/frontend/src/documents/features/folders/useFolderTreeActions.ts +++ b/frontend/src/documents/features/folders/useFolderTreeActions.ts @@ -1,5 +1,6 @@ import { useCallback, useMemo } from 'react'; import type { DragEvent } from 'react'; +import { useStatusToast } from '../../../lib/context/StatusToastContext'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../../app/workspaceUtils'; import { createFolder, @@ -35,7 +36,6 @@ interface UseFolderTreeActionsOptions { selectedFolder: FolderKey; setSelectedFolder: (folderId: FolderKey) => void; notifyApiError: (error: unknown, message?: string) => void; - setStatusMessage: (message: string, level?: string) => void; navigate?: (path: string, options?: { replace?: boolean }) => void; handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise | void; moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise; @@ -54,7 +54,6 @@ const useFolderTreeActions = ({ selectedFolder, setSelectedFolder, notifyApiError, - setStatusMessage, navigate, handleFileDrop, moveDocumentsToFolder, @@ -65,11 +64,13 @@ const useFolderTreeActions = ({ isInvalidFolderDrop, setCreatingFolder, }: UseFolderTreeActionsOptions) => { + const { showToast } = useStatusToast(); + const moveFolder = useCallback( async (folderId: FolderKey, targetFolderId: FolderKey | null) => { const node = folderNodes.get(folderId); if (!node) { - setStatusMessage('Folder metadata unavailable. Try refreshing.', 'error'); + showToast('Folder metadata unavailable. Try refreshing.', 'error'); return; } @@ -134,7 +135,7 @@ const useFolderTreeActions = ({ setSelectedFolder(folderId); } - setStatusMessage('Folder moved.', 'success'); + showToast('Folder moved.', 'success'); } catch (error) { const message = error.response?.data?.error || 'Failed to move folder.'; notifyApiError(error, message); @@ -146,7 +147,7 @@ const useFolderTreeActions = ({ selectedFolder, setFolderNodes, setSelectedFolder, - setStatusMessage, + showToast, ], ); @@ -180,12 +181,12 @@ const useFolderTreeActions = ({ const handleFolderRename = useCallback( async (folderId: FolderKey, nextName: string) => { if (!token) { - setStatusMessage('Log in to rename folders.', 'error'); + showToast('Log in to rename folders.', 'error'); return false; } const trimmed = nextName?.trim?.() || ''; if (!trimmed) { - setStatusMessage('Folder name cannot be empty.', 'error'); + showToast('Folder name cannot be empty.', 'error'); return false; } try { @@ -200,7 +201,7 @@ const useFolderTreeActions = ({ return next; }); - setStatusMessage('Folder renamed.', 'success'); + showToast('Folder renamed.', 'success'); return true; } catch (error) { const message = error.response?.data?.error || 'Failed to rename folder.'; @@ -211,7 +212,7 @@ const useFolderTreeActions = ({ [ notifyApiError, setFolderNodes, - setStatusMessage, + showToast, token, ], ); @@ -219,11 +220,11 @@ const useFolderTreeActions = ({ const handleFolderCreate = useCallback( async (name: string, parentId?: FolderKey | null) => { if (!token) { - setStatusMessage('Log in to create folders.', 'error'); + showToast('Log in to create folders.', 'error'); return false; } if (!name.trim()) { - setStatusMessage('Folder name cannot be empty.', 'error'); + showToast('Folder name cannot be empty.', 'error'); return false; } @@ -243,7 +244,7 @@ const useFolderTreeActions = ({ if (!folderData?.id) { throw new Error('Folder creation failed.'); } - setStatusMessage('Folder created.', 'success'); + showToast('Folder created.', 'success'); setFolderNodes((prev) => { const next = new Map(prev); const parentId = folderData.parent_id ?? payload.parent_id ?? 'root'; @@ -278,7 +279,7 @@ const useFolderTreeActions = ({ } finally { setCreatingFolder(false); if (!succeeded) { - setStatusMessage('Folder creation failed.', 'error'); + showToast('Folder creation failed.', 'error'); } } }, @@ -288,7 +289,7 @@ const useFolderTreeActions = ({ selectedFolder, setCreatingFolder, setFolderNodes, - setStatusMessage, + showToast, token, ], ); @@ -297,13 +298,13 @@ const useFolderTreeActions = ({ async (folderId: FolderKey, { showMessage = true }: MessageOptions = {}) => { if (!token) { if (showMessage) { - setStatusMessage('Log in to manage folders.', 'error'); + showToast('Log in to manage folders.', 'error'); } return false; } if (!folderId || folderId === 'root') { if (showMessage) { - setStatusMessage('The root folder cannot be removed.', 'error'); + showToast('The root folder cannot be removed.', 'error'); } return false; } @@ -337,14 +338,14 @@ const useFolderTreeActions = ({ } if (showMessage) { - setStatusMessage('Folder deleted.', 'success'); + showToast('Folder deleted.', 'success'); } return true; } catch (error) { const message = error.response?.data?.error || 'Failed to delete folder.'; notifyApiError(error, message); if (showMessage) { - setStatusMessage(message, 'error'); + showToast(message, 'error'); } return false; } @@ -356,7 +357,7 @@ const useFolderTreeActions = ({ selectedFolder, setFolderNodes, setSelectedFolder, - setStatusMessage, + showToast, ], ); @@ -404,7 +405,7 @@ const useFolderTreeActions = ({ setDraggedFolderId(null); const invalidMove = folderIds.some((sourceId) => isInvalidFolderDrop(sourceId, folderId)); if (invalidMove) { - setStatusMessage( + showToast( 'Cannot move a folder into itself or one of its descendants.', 'error', ); @@ -492,7 +493,7 @@ const useFolderTreeActions = ({ selectedFolder, setDraggedDocumentIds, setDraggedFolderId, - setStatusMessage, + showToast, ], ); diff --git a/frontend/src/documents/features/tagging/useDocumentTagActions.ts b/frontend/src/documents/features/tagging/useDocumentTagActions.ts index 467f853..ceb5712 100644 --- a/frontend/src/documents/features/tagging/useDocumentTagActions.ts +++ b/frontend/src/documents/features/tagging/useDocumentTagActions.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { useStatusToast } from '../../../lib/context/StatusToastContext'; import type { Identifier } from '../../../types/identifiers'; @@ -20,7 +21,6 @@ interface UseDocumentTaggingArgs { refreshTags: () => Promise | void; resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[]; notifyApiError: (error: unknown, message: string) => void; - setStatusMessage: (message: string, variant?: string) => void; updateDocumentCaches?: (id: Identifier, updater: (doc: TagRecord | null) => TagRecord | null) => void; } @@ -44,9 +44,10 @@ const useDocumentTagActions = ({ refreshTags, resolveTargetDocumentIds, notifyApiError, - setStatusMessage, updateDocumentCaches, }: UseDocumentTaggingArgs) => { + const { showToast } = useStatusToast(); + const bulkTagOperation = useCallback( async ({ labels, action, documentIds }: BulkTagOperationArgs): Promise => { const normalized = labels.map((label) => label.trim()).filter((label) => label.length > 0); @@ -184,12 +185,12 @@ const useDocumentTagActions = ({ async ({ label, input, documentIds }: { label?: string; input?: HTMLInputElement | null; documentIds?: Identifier[] }) => { const trimmed = label?.trim?.() || ''; if (!trimmed) { - setStatusMessage('Enter a tag label.', 'error'); + showToast('Enter a tag label.', 'error'); return; } const targetIds = resolveTargetDocumentIds(documentIds); if (!targetIds.length) { - setStatusMessage('Select documents before assigning tags.', 'error'); + showToast('Select documents before assigning tags.', 'error'); return; } const result = await bulkTagOperation({ @@ -199,7 +200,7 @@ const useDocumentTagActions = ({ }); if (result?.ok) { const { tagCount, docsCount } = result; - setStatusMessage( + showToast( `Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${docsCount === 1 ? '' : 's' }.`, 'success', @@ -209,19 +210,19 @@ const useDocumentTagActions = ({ } } }, - [bulkTagOperation, resolveTargetDocumentIds, setStatusMessage], + [bulkTagOperation, resolveTargetDocumentIds, showToast], ); const handleBulkTagRemoveFromDetail = useCallback( async ({ label, input, documentIds }: { label?: string; input?: HTMLInputElement | null; documentIds?: Identifier[] }) => { const trimmed = label?.trim?.() || ''; if (!trimmed) { - setStatusMessage('Enter a tag label to remove.', 'error'); + showToast('Enter a tag label to remove.', 'error'); return; } const targetIds = resolveTargetDocumentIds(documentIds); if (!targetIds.length) { - setStatusMessage('Select documents before removing tags.', 'error'); + showToast('Select documents before removing tags.', 'error'); return; } const result = await bulkTagOperation({ @@ -231,7 +232,7 @@ const useDocumentTagActions = ({ }); if (result?.ok) { const { docsCount } = result; - setStatusMessage( + showToast( `Removed tags from ${docsCount} document${docsCount === 1 ? '' : 's'}.`, 'success', ); @@ -239,17 +240,17 @@ const useDocumentTagActions = ({ input.value = ''; } } else if (result?.reason === 'tag-missing') { - setStatusMessage(`Tag “${result.label}” not found.`, 'error'); + showToast(`Tag “${result.label}” not found.`, 'error'); } }, - [bulkTagOperation, resolveTargetDocumentIds, setStatusMessage], + [bulkTagOperation, resolveTargetDocumentIds, showToast], ); const handleBulkSelectionReanalyze = useCallback( async (documentIdsOverride: Identifier[] | null = null) => { const targetIds = resolveTargetDocumentIds(documentIdsOverride); if (!targetIds.length) { - setStatusMessage('Select documents before requesting re-analysis.', 'error'); + showToast('Select documents before requesting re-analysis.', 'error'); return; } @@ -262,7 +263,7 @@ const useDocumentTagActions = ({ const queued = payload?.queued != null ? Number(payload.queued) : targetIds.length; - setStatusMessage( + showToast( `Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`, 'success', ); @@ -272,7 +273,7 @@ const useDocumentTagActions = ({ notifyApiError(error, message); } }, - [resolveTargetDocumentIds, notifyApiError, setStatusMessage], + [resolveTargetDocumentIds, notifyApiError, showToast], ); return { diff --git a/frontend/src/documents/features/upload/useDocumentUploads.ts b/frontend/src/documents/features/upload/useDocumentUploads.ts index ddaa59b..e321b9d 100644 --- a/frontend/src/documents/features/upload/useDocumentUploads.ts +++ b/frontend/src/documents/features/upload/useDocumentUploads.ts @@ -1,6 +1,7 @@ import { useCallback, useRef, useState } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import useFileDrop from './useFileDrop'; +import { useStatusToast } from '../../../lib/context/StatusToastContext'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../../app/workspaceUtils'; import { fetchDocument, uploadDocument, resolveFolderPath } from '../../../lib/api/apiClient'; import type { Identifier } from '../../../types/identifiers'; @@ -26,9 +27,7 @@ type UploadQueueItem = { conflictDocumentId: Identifier | null; }; -type StatusLevel = 'success' | 'info' | 'warning' | 'error' | string; type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; -type SetStatusMessage = (message: string, level?: StatusLevel) => void; type DropOverlayState = { active: boolean; @@ -99,7 +98,6 @@ interface UseDocumentUploadsArgs { refreshCurrentFolder: () => Promise; shellRef: MutableRefObject; notifyApiError?: NotifyApiError; - setStatusMessage?: SetStatusMessage; } interface UseDocumentUploadsResult { @@ -128,7 +126,6 @@ const useDocumentUploads = ({ refreshCurrentFolder, shellRef, notifyApiError, - setStatusMessage, }: UseDocumentUploadsArgs): UseDocumentUploadsResult => { const [dropOverlayState, setDropOverlayState] = useState({ active: false, @@ -138,6 +135,7 @@ const useDocumentUploads = ({ const folderPathCacheRef = useRef>(new Map()); const queueIdRef = useRef(0); const [uploadQueue, setUploadQueue] = useState([]); + const { showToast } = useStatusToast(); const uploadFile = useCallback( async (file: File, targetFolderId: FolderId) => { @@ -180,12 +178,12 @@ const useDocumentUploads = ({ } const message = error.response?.data?.error || `Failed to upload ${file.name}.`; notifyApiError?.(error, message); - setStatusMessage?.(message, 'error'); + showToast(message, 'error'); const wrapped = Object.assign(new Error(message), { response: error.response }); throw wrapped; } }, - [notifyApiError, setStatusMessage], + [notifyApiError, showToast], ); const appendQueueItems = useCallback((entries: FileEntry[], targetFolderId?: FolderId) => { diff --git a/frontend/src/settings/usePasskeys.ts b/frontend/src/settings/usePasskeys.ts index 10c674c..8c6e28e 100644 --- a/frontend/src/settings/usePasskeys.ts +++ b/frontend/src/settings/usePasskeys.ts @@ -1,4 +1,5 @@ import { useState, useCallback } from 'react'; +import { useStatusToast } from '../lib/context/StatusToastContext'; /* global PublicKeyCredentialCreationOptions, CredentialCreationOptions */ import { @@ -14,7 +15,6 @@ import { } from '../lib/api/apiClient'; import type { PasskeyId } from '../types/identifiers'; -type StatusMessageFn = (message: string, variant?: string) => void; type NotifyApiErrorFn = (error: unknown, message: string) => void; type ApiError = { @@ -71,7 +71,6 @@ type RevokePasskeyResult = interface UsePasskeysArgs { notifyApiError: NotifyApiErrorFn; - setStatusMessage: StatusMessageFn; token?: string | null; } @@ -89,12 +88,13 @@ interface UsePasskeysResult { ) => Promise; } -const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArgs): UsePasskeysResult => { +const usePasskeys = ({ notifyApiError, 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 refreshPasskeys = useCallback(async (): Promise => { if (!token) { @@ -122,7 +122,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg async ({ nickname }: { nickname?: string } = {}): Promise => { if (!isWebAuthnAvailable()) { setPasskeysSupported(false); - setStatusMessage('Passkeys are not supported in this browser.', 'error'); + showToast('Passkeys are not supported in this browser.', 'error'); return { ok: false, reason: 'unsupported' }; } if (registeringPasskey) { @@ -166,12 +166,12 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg await finishPasskeyRegistration(payload); await refreshPasskeys(); setPasskeysSupported(true); - setStatusMessage('Passkey registered.', 'success'); + showToast('Passkey registered.', 'success'); return { ok: true }; } catch (error) { const typedError = error as ApiError; if (typedError?.name === 'NotAllowedError') { - setStatusMessage('Passkey registration cancelled.', 'info'); + showToast('Passkey registration cancelled.', 'info'); return { ok: false, reason: 'cancelled' }; } @@ -187,7 +187,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg setRegisteringPasskey(false); } }, - [notifyApiError, refreshPasskeys, registeringPasskey, setStatusMessage], + [notifyApiError, refreshPasskeys, registeringPasskey, showToast], ); const revokePasskey = useCallback( @@ -202,7 +202,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg try { await deletePasskey(passkeyId, { reason }); await refreshPasskeys(); - setStatusMessage('Passkey revoked.', 'success'); + showToast('Passkey revoked.', 'success'); return { ok: true }; } catch (error) { const message = (error as ApiError)?.response?.data?.error || 'Failed to revoke passkey.'; @@ -212,7 +212,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg setRevokingPasskeyId(null); } }, - [notifyApiError, refreshPasskeys, setStatusMessage], + [notifyApiError, refreshPasskeys, showToast], ); return {