diff --git a/frontend/src/documents/data/useDocumentTagMutations.ts b/frontend/src/documents/data/useDocumentTagMutations.ts index aae44bf..6dd5383 100644 --- a/frontend/src/documents/data/useDocumentTagMutations.ts +++ b/frontend/src/documents/data/useDocumentTagMutations.ts @@ -6,8 +6,6 @@ import { createTag, deleteDocumentTag, } from '../../lib/api/apiClient'; -import useNotifyApiError from '../../hooks/useNotifyApiError'; -import { useStatusToast } from '../../lib/context/StatusToastContext'; import type { TagsState, DocumentsState } from '../types/workspaceTypes'; interface DocumentTagExtras { @@ -24,51 +22,36 @@ export const useDocumentTagMutations = ({ tagsState, documentsState, }: UseDocumentTagMutationsArgs) => { - const { showToast } = useStatusToast(); - const notifyApiError = useNotifyApiError(); + // Note: Toasts are handled by the caller, e.g. useDetailWorkspace or ResultQueue. const attachTagToDocument = useCallback( async ({ documentId, tag, - silent = false, }: { documentId?: DocumentId; tag?: Tag | null; - silent?: boolean; }) => { if (!documentId || !tag?.id) { return false; } - try { - await addDocumentTags(documentId, [tag.id]); - documentsState.documentsManager.map((doc) => { - if (doc.id !== documentId) { - return undefined; - } - const currentTags = Array.isArray(doc.tags) ? doc.tags : []; - - if (currentTags.includes(tag.id)) { - return doc; - } - return { ...doc, tags: [...currentTags, tag.id] }; - }); - - if (!silent) { - showToast('Tag assigned.', 'success'); + await addDocumentTags(documentId, [tag.id]); + documentsState.documentsManager.map((doc) => { + if (doc.id !== documentId) { + return undefined; } - return true; - } catch (error) { - if (!silent) { - const message = (error as Record)?.response?.data?.error || 'Failed to assign tag.'; - notifyApiError(error, message); + const currentTags = Array.isArray(doc.tags) ? doc.tags : []; + + if (currentTags.includes(tag.id)) { + return doc; } - return false; - } + return { ...doc, tags: [...currentTags, tag.id] }; + }); + + return true; }, - - [notifyApiError, showToast, documentsState], + [documentsState], ); const handleDocumentTagAdd = useCallback( @@ -87,31 +70,29 @@ export const useDocumentTagMutations = ({ const knownTags = Array.from(tagsState.tagLookupById.values()); tag = knownTags.find((item) => item.label?.toLowerCase() === normalizedLabel.toLowerCase()) || null; } - try { - if (!tag) { - const payload = tagsState.tagManager.buildPayload({ label: normalizedLabel }) as { label: string; color?: string | null }; - const data = await createTag(payload); - tag = data as Tag; - // Ingest new tag into manager to ensure it's available - tagsState.tagManager.ingest([tag]); - await tagsState.refreshTags(); - } - await attachTagToDocument({ - documentId: document.id as DocumentId, - tag, - }); - if (input && typeof input === 'object' && 'value' in input) { - (input as { value?: string }).value = ''; - } - } catch (error) { - notifyApiError(error, 'Failed to assign tag.'); + + // Create tag if needed. Errors bubble up. + if (!tag) { + const payload = tagsState.tagManager.buildPayload({ label: normalizedLabel }) as { label: string; color?: string | null }; + const data = await createTag(payload); + tag = data as Tag; + // Ingest new tag into manager to ensure it's available + tagsState.tagManager.ingest([tag]); + await tagsState.refreshTags(); + } + await attachTagToDocument({ + documentId: document.id as DocumentId, + tag, + }); + if (input && typeof input === 'object' && 'value' in input) { + (input as { value?: string }).value = ''; } }, - [tagsState, attachTagToDocument, notifyApiError], + [tagsState, attachTagToDocument], ); const handleDocumentTagAttach = useCallback( - async (documentId: DocumentId, tagId: DocumentId, options: { silent?: boolean } = {}) => { + async (documentId: DocumentId, tagId: DocumentId) => { if (!documentId || !tagId) { return false; } @@ -129,7 +110,6 @@ export const useDocumentTagMutations = ({ return attachTagToDocument({ documentId, tag: resolvedTag, - silent: options.silent, }); }, [ @@ -139,41 +119,30 @@ export const useDocumentTagMutations = ({ ); const handleDocumentTagDetach = useCallback( - async (documentId?: DocumentId, tagId?: DocumentId, options: { silent?: boolean } = {}) => { + async (documentId?: DocumentId, tagId?: DocumentId) => { if (!documentId || !tagId) { return false; } - try { - await deleteDocumentTag(documentId, tagId); - // Inlined applyTagRemovalToCaches logic - documentsState.documentsManager.map((doc) => { - if (doc.id !== documentId) { - return undefined; - } - if (!doc || !Array.isArray(doc.tags)) { - return doc; - } - // Filter IDs - const nextTags = doc.tags.filter((id) => id !== tagId); - if (nextTags.length === doc.tags.length) { - return doc; - } - return { ...doc, tags: nextTags }; - }); - if (!options.silent) { - showToast('Tag removed.', 'success'); + await deleteDocumentTag(documentId, tagId); + // Inlined applyTagRemovalToCaches logic + documentsState.documentsManager.map((doc) => { + if (doc.id !== documentId) { + return undefined; } - return true; - } catch (error) { - if (!options.silent) { - const message = (error as Record)?.response?.data?.error || 'Failed to remove tag.'; - notifyApiError(error, message); + if (!doc || !Array.isArray(doc.tags)) { + return doc; } - return false; - } + // Filter IDs + const nextTags = doc.tags.filter((id) => id !== tagId); + if (nextTags.length === doc.tags.length) { + return doc; + } + return { ...doc, tags: nextTags }; + }); + return true; }, - [documentsState, notifyApiError, showToast], + [documentsState], ); return { diff --git a/frontend/src/documents/interactions/useTagInteractions.ts b/frontend/src/documents/interactions/useTagInteractions.ts index f66564a..5ae7402 100644 --- a/frontend/src/documents/interactions/useTagInteractions.ts +++ b/frontend/src/documents/interactions/useTagInteractions.ts @@ -47,8 +47,8 @@ const cleanupPreview = (previewNode: HTMLElement | null) => { }; interface UseTagInteractionsArgs { - onAssignTagToDocument?: (docId: Identifier, tagId: Identifier, options?: { silent?: boolean }) => Promise | void; - onRemoveTagFromDocument?: (docId: Identifier, tagId: Identifier, options?: { silent?: boolean }) => Promise | void; + onAssignTagToDocument?: (docId: Identifier, tagId: Identifier) => Promise | void; + onRemoveTagFromDocument?: (docId: Identifier, tagId: Identifier) => Promise | void; onTagClick?: (tagId: Identifier) => void; } @@ -164,7 +164,7 @@ export const useTagInteractions = ({ // Queue Result Logic beginAction(); try { - await onAssignTagToDocument(doc.id, payload.id, { silent: true }); + await onAssignTagToDocument(doc.id, payload.id); finishAction({ type: 'attach', success: true }); } catch { finishAction({ type: 'attach', success: false }); @@ -226,7 +226,7 @@ export const useTagInteractions = ({ if (onRemoveTagFromDocument && sourceDocId && tagId) { beginAction(); try { - await onRemoveTagFromDocument(sourceDocId, tagId, { silent: true }); + await onRemoveTagFromDocument(sourceDocId, tagId); finishAction({ type: 'detach', success: true }); } catch { finishAction({ type: 'detach', success: false }); diff --git a/frontend/src/documents/logic/useDocumentsPanelProps.ts b/frontend/src/documents/logic/useDocumentsPanelProps.ts index 75bfe24..5389fb6 100644 --- a/frontend/src/documents/logic/useDocumentsPanelProps.ts +++ b/frontend/src/documents/logic/useDocumentsPanelProps.ts @@ -40,8 +40,8 @@ interface UseDocumentsPanelPropsArgs { activeCorrespondentFilters?: Identifier[]; ensureAssetUrl?: (...args: unknown[]) => void; getDocumentAsset?: (...args: unknown[]) => unknown; - handleDocumentTagAttach?: (docId: Identifier, tagId: Identifier, options?: { silent?: boolean }) => void; - handleDocumentTagDetach?: (docId: Identifier, tagId: Identifier, options?: { silent?: boolean }) => void; + handleDocumentTagAttach?: (docId: Identifier, tagId: Identifier) => void; + handleDocumentTagDetach?: (docId: Identifier, tagId: Identifier) => void; documentsViewMode?: string; documentsSortField?: string; documentsSortDirection?: string; diff --git a/frontend/src/viewer/logic/useDetailWorkspace.ts b/frontend/src/viewer/logic/useDetailWorkspace.ts index 7a72c09..f050aa5 100644 --- a/frontend/src/viewer/logic/useDetailWorkspace.ts +++ b/frontend/src/viewer/logic/useDetailWorkspace.ts @@ -8,6 +8,9 @@ import type { Identifier } from '../../types/identifiers'; import type { Document } from '../../types/documents'; import { resolveBreadcrumbs } from '../../documents/logic/breadcrumbs'; import type { Tag, Correspondent } from '../../types/documents'; +import { listFolderContents } from '../../lib/api/apiClient'; +import { useStatusToast } from '../../lib/context/StatusToastContext'; +import useNotifyApiError from '../../hooks/useNotifyApiError'; interface FolderNode { id: Identifier | 'root'; @@ -52,8 +55,6 @@ interface UseDetailWorkspaceResult { resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>; } -import { listFolderContents } from '../../lib/api/apiClient'; - const useDetailWorkspace = ({ documents, documentLookup, @@ -77,6 +78,9 @@ const useDetailWorkspace = ({ tagLookupById, correspondentLookupById, }: UseDetailWorkspaceArgs): UseDetailWorkspaceResult => { + const { showToast } = useStatusToast(); + const notifyApiError = useNotifyApiError(); + const { detailPanelOpen, detailPanelDocument, @@ -181,13 +185,39 @@ const useDetailWorkspace = ({ closeDetailPanel(); }, [closeDetailPanel]); + const handleDetailTagAdd = useCallback( + async (doc: Document, value: string, context?: { option?: unknown }) => { + if (!handleDocumentTagAdd) return; + try { + await handleDocumentTagAdd(doc, value, context); + showToast('Tag assigned.', 'success'); + } catch (error) { + notifyApiError(error, 'Failed to assign tag.'); + } + }, + [handleDocumentTagAdd, showToast, notifyApiError], + ); + + const handleDetailTagRemove = useCallback( + async (docId: any, tagId: any) => { + if (!handleDocumentTagDetach) return; + try { + await handleDocumentTagDetach(docId, tagId); + showToast('Tag removed.', 'success'); + } catch (error) { + notifyApiError(error, 'Failed to remove tag.'); + } + }, + [handleDocumentTagDetach, showToast, notifyApiError], + ); + const detailPanelProps = { document: detailPanelDocument, tags, tagLookupById, correspondentLookupById, - onTagAdd: handleDocumentTagAdd, - onTagRemove: handleDocumentTagDetach, + onTagAdd: handleDetailTagAdd, + onTagRemove: handleDetailTagRemove, onOpenPreview: openDocumentPreview, activePreviewId, onUpdateTitle: handleDocumentTitleUpdate,