refactor: centralize frontend status message handling with useStatusToast
This commit is contained in:
@@ -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<void>;
|
||||
onCorrespondentUpdate?: (...args: any[]) => void | Promise<void>;
|
||||
onCorrespondentDelete?: (...args: any[]) => void | Promise<void>;
|
||||
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<string | null>(null);
|
||||
|
||||
const openTagsModal = useCallback(() => setActiveModal(TAGS_MODAL), []);
|
||||
@@ -117,7 +117,7 @@ export const useManagementModals = ({
|
||||
onCreateTag={onTagCreate}
|
||||
onUpdateTag={onTagUpdate}
|
||||
onDeleteTag={onTagDelete}
|
||||
onNotify={setStatusMessage}
|
||||
onNotify={showToast}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@ export const useManagementModals = ({
|
||||
handleCorrespondentDeleteSafe,
|
||||
handleCorrespondentUpdateSafe,
|
||||
refreshCorrespondents,
|
||||
setStatusMessage,
|
||||
showToast,
|
||||
]);
|
||||
|
||||
const managementModals = (
|
||||
|
||||
@@ -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<string | null>(token);
|
||||
const initialRefreshAttemptedRef = useRef(Boolean(token));
|
||||
const { showToast } = useStatusToast();
|
||||
|
||||
const refreshAccessToken = useCallback(async (): Promise<string> => {
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -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<string, { id?: Identifier }>;
|
||||
handleCorrespondentCreate: (payload: { name: string }) => Promise<{ id?: Identifier } | null>;
|
||||
setStatusMessage: (message: string, variant?: string) => void;
|
||||
selectedDocumentIds?: Identifier[];
|
||||
selectedFolderIds?: Identifier[];
|
||||
handleDocumentsDelete: (ids: Identifier[], options?: MessageOptions) => Promise<boolean>;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string | null>;
|
||||
mapDocumentCaches?: (mapper: (doc: any) => any) => void;
|
||||
}
|
||||
|
||||
const useCorrespondents = ({
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
tenantIdRef,
|
||||
mapDocumentCaches,
|
||||
}: UseCorrespondentsOptions) => {
|
||||
const [correspondents, setCorrespondents] = useState<Correspondent[]>([]);
|
||||
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 {
|
||||
|
||||
@@ -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<SetStateAction<string | null>>;
|
||||
focusedEntryKey: string | null;
|
||||
notifyApiError: NotifyApiError;
|
||||
setStatusMessage: SetStatusMessage;
|
||||
mapDocumentCaches: MapDocumentCaches;
|
||||
folderNodes: Map<FolderId, FolderNode>;
|
||||
setFolderNodes: Dispatch<SetStateAction<Map<FolderId, FolderNode>>>;
|
||||
@@ -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<DocumentId | Document>, 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<string, any>)?.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<string, any>)?.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<string, any>)?.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<string, any>)?.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<string, any>)?.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<string, any>)?.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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<TenantId | null>;
|
||||
setActiveTagFilters: (updater: (prev: Array<TagId>) => Array<TagId>) => void;
|
||||
@@ -21,13 +21,13 @@ interface UseTagsOptions {
|
||||
const useTags = ({
|
||||
// apiClient removed
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
tagManager,
|
||||
tenantIdRef,
|
||||
setActiveTagFilters,
|
||||
mapDocumentCaches,
|
||||
}: UseTagsOptions) => {
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
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 {
|
||||
|
||||
@@ -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<void>;
|
||||
refreshCorrespondents: () => Promise<void>;
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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<CorrespondentOption | null>;
|
||||
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<string, CorrespondentOption>();
|
||||
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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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> | void;
|
||||
moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>;
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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> | 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<BulkTagOperationResult> => {
|
||||
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 {
|
||||
|
||||
@@ -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<void>;
|
||||
shellRef: MutableRefObject<HTMLElement | null>;
|
||||
notifyApiError?: NotifyApiError;
|
||||
setStatusMessage?: SetStatusMessage;
|
||||
}
|
||||
|
||||
interface UseDocumentUploadsResult {
|
||||
@@ -128,7 +126,6 @@ const useDocumentUploads = ({
|
||||
refreshCurrentFolder,
|
||||
shellRef,
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
}: UseDocumentUploadsArgs): UseDocumentUploadsResult => {
|
||||
const [dropOverlayState, setDropOverlayState] = useState<DropOverlayState>({
|
||||
active: false,
|
||||
@@ -138,6 +135,7 @@ const useDocumentUploads = ({
|
||||
const folderPathCacheRef = useRef<Map<string, FolderId>>(new Map());
|
||||
const queueIdRef = useRef(0);
|
||||
const [uploadQueue, setUploadQueue] = useState<UploadQueueItem[]>([]);
|
||||
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) => {
|
||||
|
||||
@@ -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<RevokePasskeyResult>;
|
||||
}
|
||||
|
||||
const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArgs): UsePasskeysResult => {
|
||||
const usePasskeys = ({ notifyApiError, 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 refreshPasskeys = useCallback(async (): Promise<void> => {
|
||||
if (!token) {
|
||||
@@ -122,7 +122,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg
|
||||
async ({ nickname }: { nickname?: string } = {}): Promise<RegisterPasskeyResult> => {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user