diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx index 016d43c..cc0fd2a 100644 --- a/frontend/src/app/AppLayout.jsx +++ b/frontend/src/app/AppLayout.jsx @@ -1859,7 +1859,7 @@ const AppLayout = () => { ); const handleCorrespondentAdd = useCallback( - async ({ document, name, input }) => { + async ({ document, name, input = null, option = null }) => { if (!document?.id) { throw new Error('Missing document for correspondent assignment.'); } @@ -1869,7 +1869,12 @@ const AppLayout = () => { return; } - let target = correspondentLookupByName.get(trimmed.toLowerCase()) || null; + let target = null; + if (option && option.id) { + target = correspondentLookupByName.get(trimmed.toLowerCase()) || option; + } else { + target = correspondentLookupByName.get(trimmed.toLowerCase()) || null; + } if (!target) { try { target = await handleCorrespondentCreate({ name: trimmed }); @@ -3585,6 +3590,35 @@ const AppLayout = () => { [notifyApiError, setStatusMessage, updateDocumentCaches, extractDocumentFromResponse], ); + const handleDocumentIssuedUpdate = useCallback( + async (documentId, nextIssuedDate) => { + setLoading(true); + const payload = { issued_at: nextIssuedDate || null }; + try { + const { data } = await api.patch(`/documents/${documentId}`, payload); + const updatedDocument = extractDocumentFromResponse(data); + + updateDocumentCaches(documentId, (doc) => { + if (updatedDocument) { + return { ...doc, ...updatedDocument }; + } + return { ...doc, issued_at: payload.issued_at }; + }); + + const message = payload.issued_at ? 'Issued date updated.' : 'Issued date cleared.'; + setStatusMessage(message, 'success'); + return true; + } catch (error) { + const message = error.response?.data?.error || 'Failed to update issued date.'; + notifyApiError(error, message); + return false; + } finally { + setLoading(false); + } + }, + [extractDocumentFromResponse, notifyApiError, setStatusMessage, updateDocumentCaches], + ); + const applyTagRemovalToCaches = useCallback( (documentId, tagId) => { if (!documentId || !tagId) { @@ -3631,10 +3665,21 @@ const AppLayout = () => { ); const handleTagAdd = useCallback( - async (document, label, input) => { + async (document, label, extras = null) => { const normalizedLabel = tagManager.normalizeLabel(label); - let tag = - tags.find((item) => item.label.toLowerCase() === normalizedLabel.toLowerCase()) || null; + const optionCandidate = + extras && typeof extras === 'object' && 'option' in extras ? extras.option : null; + const input = + extras && typeof extras === 'object' && 'input' in extras ? extras.input : null; + + let tag = null; + if (optionCandidate && optionCandidate.id) { + tag = tags.find((item) => item.id === optionCandidate.id) || optionCandidate; + } + if (!tag) { + tag = + tags.find((item) => item.label.toLowerCase() === normalizedLabel.toLowerCase()) || null; + } try { if (!tag) { const payload = tagManager.buildPayload({ label: normalizedLabel }); @@ -3644,7 +3689,9 @@ const AppLayout = () => { } await api.post(`/documents/${document.id}/tags`, { tag_ids: [tag.id] }); setStatusMessage('Tag assigned.', 'success'); - input.value = ''; + if (input && typeof input === 'object') { + input.value = ''; + } await refreshCurrentFolder(); } catch (error) { notifyApiError(error, 'Failed to assign tag.'); @@ -5104,6 +5151,7 @@ const AppLayout = () => { onPromoteSelection: promoteSelectionOrder, activePreviewId, onUpdateTitle: handleDocumentTitleUpdate, + onUpdateIssued: handleDocumentIssuedUpdate, ensureAssetUrl, getDocumentAsset, ensurePreviewData, @@ -5131,6 +5179,7 @@ const AppLayout = () => { handleCorrespondentRemove, handleDetailPanelClose, handleDocumentTitleUpdate, + handleDocumentIssuedUpdate, handleTagAdd, handleTagRemove, handleThumbnailRegeneration, diff --git a/frontend/src/app/useWorkspaceSurface.js b/frontend/src/app/useWorkspaceSurface.js index 2099103..70a04b9 100644 --- a/frontend/src/app/useWorkspaceSurface.js +++ b/frontend/src/app/useWorkspaceSurface.js @@ -68,6 +68,20 @@ export const useWorkspaceSurface = ({ if (!showPreviewWorkspace || !previewWorkspaceDocument) { return null; } + const detailExtras = detailPanelProps || {}; + const { + tagLookupById, + tags: tagOptions, + onTagAdd, + onTagRemove, + correspondents, + onCorrespondentAdd, + onCorrespondentRemove, + onUpdateTitle, + onUpdateIssued, + resolveFolderPath, + onFolderNavigate, + } = detailExtras; return createPreviewSurface({ document: previewWorkspaceDocument, previewEntry: previewWorkspaceEntry, @@ -79,6 +93,17 @@ export const useWorkspaceSurface = ({ onRegenerate: handleThumbnailRegeneration, onClose: closeDocumentPreview, renderSidebarToggle, + tagLookupById, + tagOptions, + onTagAdd, + onTagRemove, + correspondents, + onCorrespondentAdd, + onCorrespondentRemove, + onUpdateTitle, + onUpdateIssued, + resolveFolderPath, + onFolderNavigate, }); }, [ showPreviewWorkspace, @@ -92,6 +117,7 @@ export const useWorkspaceSurface = ({ handleThumbnailRegeneration, closeDocumentPreview, renderSidebarToggle, + detailPanelProps, ]); const workspaceSurface = useMemo(() => { diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index fe2635f..8da5dbe 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { DownloadIcon, - EditIcon, ArrowLeftIcon, ArrowRightIcon, ChevronsRightIcon, @@ -10,13 +9,18 @@ import { TextScanIcon, } from '../ui/icons'; import PanelHeader from '../ui/PanelHeader'; -import { getTagColorStyle } from '../utils/colors'; import { formatFileSize } from '../utils/format'; import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager'; import { useAssetNavigator } from '../hooks/useAssetNavigator'; import { describeDocumentSummary } from '../documents/documentSummary'; import { createDocumentActionState } from '../documents/documentActions'; import PreviewZoomOverlay from './PreviewZoomOverlay'; +import DocumentSummarySection, { + TagSection, + CorrespondentSection, + sortCorrespondents, + buildCorrespondentOptions, +} from '../documents/DocumentSummarySection'; const MAX_PREVIEW_STACK_ITEMS = 15; @@ -29,156 +33,6 @@ const derivePreviewOrientation = (metadata) => { return 'landscape'; }; -const sortCorrespondents = (entries = []) => - entries - .filter((entry) => entry && entry.name) - .map(({ id, name, count }) => ({ id, name, count })) - .sort((a, b) => a.name.localeCompare(b.name)); - -const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => ( -
Select a document to view metadata, tags and actions.
; } - const displayName = singleDoc.title || singleDoc.original_name; - const isEditingTitle = titleEditDocId === singleDoc.id; - const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : []; - const metadata = - singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null; - const effectiveCardinality = singleEffectiveCardinality; + const effectiveCardinality = + singlePreviewNavigator.cardinality || (singlePreviewNavigator.currentUrl ? 1 : 0); const canGoPrev = singlePreviewNavigator.canGoPrev; const canGoNext = singlePreviewNavigator.canGoNext; - const hasPreviewImage = singleHasPreview; + const hasPreviewImage = Boolean(singlePreviewNavigator.currentUrl); const interceptNavPointer = (event) => { event.preventDefault(); event.stopPropagation(); @@ -997,142 +669,59 @@ const DetailPanel = ({ onZoomPreview={handleSingleZoom} /> {hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? ( -{JSON.stringify(metadata, null, 2)}
- - Full OCR text will appear here in a future update. Use the toolbar button to open the OCR view for now. -
-{JSON.stringify(metadata, null, 2)}
+ {JSON.stringify(metadataPayload, null, 2)}
Custom notes will be editable here once the feature lands.
-Change history will be displayed here in an upcoming release.
-Access control management is planned and will surface here.
-