revert document types

This commit is contained in:
2025-10-31 12:56:09 +01:00
parent 0a87f33d89
commit faf8fec9c7
20 changed files with 12 additions and 1984 deletions
+6 -402
View File
@@ -267,7 +267,6 @@ const AppLayout = () => {
const previewInflightRef = useRef(new Map());
const [tags, setTags] = useState([]);
const [correspondents, setCorrespondents] = useState([]);
const [documentTypes, setDocumentTypes] = useState([]);
const [webdavTokens, setWebdavTokens] = useState([]);
const [webdavTokensLoading, setWebdavTokensLoading] = useState(false);
const [creatingWebdavToken, setCreatingWebdavToken] = useState(false);
@@ -276,7 +275,6 @@ const AppLayout = () => {
const [searchQuery, setSearchQuery] = useState('');
const [activeTagFilters, setActiveTagFilters] = useState([]);
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]);
const [activeDocumentTypeFilters, setActiveDocumentTypeFilters] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const documentsRouteMatch = useMatch('/documents');
const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId');
@@ -302,15 +300,6 @@ const AppLayout = () => {
});
}, []);
const toggleDocumentTypeFilter = useCallback((documentTypeId) => {
setActiveDocumentTypeFilters((previous) => {
if (!documentTypeId) {
return [];
}
return previous.includes(documentTypeId) ? [] : [documentTypeId];
});
}, []);
const initialRefreshAttemptedRef = useRef(Boolean(token));
useEffect(() => {
@@ -346,7 +335,6 @@ const AppLayout = () => {
setSearchQuery('');
setActiveTagFilters([]);
setActiveCorrespondentFilters([]);
setActiveDocumentTypeFilters([]);
setSearchLoading(false);
}, []);
@@ -451,7 +439,6 @@ const AppLayout = () => {
setSearchResults(null);
setTags([]);
setCorrespondents([]);
setDocumentTypes([]);
setWebdavTokens([]);
setWebdavTokensLoading(false);
setCreatingWebdavToken(false);
@@ -460,7 +447,6 @@ const AppLayout = () => {
setSearchQuery('');
setActiveTagFilters([]);
setActiveCorrespondentFilters([]);
setActiveDocumentTypeFilters([]);
setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME });
setActivePreviewId(null);
setDetailPanelOpen(false);
@@ -496,27 +482,6 @@ const AppLayout = () => {
});
return map;
}, [correspondents]);
const documentTypeLookupByName = useMemo(() => {
const map = new Map();
documentTypes.forEach((entry) => {
if (entry?.name) {
map.set(entry.name.toLowerCase(), entry);
}
});
return map;
}, [documentTypes]);
const documentTypeLookupById = useMemo(() => {
const map = new Map();
documentTypes.forEach((entry) => {
if (entry?.id) {
map.set(entry.id, entry);
}
});
return map;
}, [documentTypes]);
const updateSelectionOrder = useCallback((nextSelection, interactedKeys = []) => {
const nextSet = new Set(nextSelection);
const previousOrder = selectionOrderRef.current.filter((id) => nextSet.has(id));
@@ -652,9 +617,8 @@ const AppLayout = () => {
() =>
searchQuery.trim().length > 0 ||
activeTagFilters.length > 0 ||
activeCorrespondentFilters.length > 0 ||
activeDocumentTypeFilters.length > 0,
[searchQuery, activeTagFilters, activeCorrespondentFilters, activeDocumentTypeFilters],
activeCorrespondentFilters.length > 0,
[searchQuery, activeTagFilters, activeCorrespondentFilters],
);
const applySelectedFolder = useCallback(
@@ -1479,22 +1443,6 @@ const AppLayout = () => {
}
}, [notifyApiError]);
const refreshDocumentTypes = useCallback(async () => {
const requestTenantId = tenantIdRef.current;
try {
const { data } = await api.get('/document-types');
if (tenantIdRef.current !== requestTenantId) {
return;
}
setDocumentTypes(data || []);
} catch (error) {
if (tenantIdRef.current !== requestTenantId) {
return;
}
notifyApiError(error, 'Unable to load document types.');
}
}, [notifyApiError]);
const refreshWebdavTokens = useCallback(async () => {
if (!token) {
return;
@@ -1834,211 +1782,6 @@ const AppLayout = () => {
],
);
const handleDocumentTypeUpdate = useCallback(
async (documentTypeId, changes) => {
if (!documentTypeId) {
throw new Error('Missing document type identifier.');
}
const payload = {};
if (typeof changes.name === 'string') {
const trimmed = changes.name.trim();
if (!trimmed) {
throw new Error('Document type name cannot be empty.');
}
payload.name = trimmed;
}
if (Object.keys(payload).length === 0) {
return false;
}
try {
await api.patch(`/document-types/${documentTypeId}`, payload);
await refreshDocumentTypes();
setStatusMessage('Document type updated.', 'success');
return true;
} catch (error) {
const message = error.response?.data?.error || 'Failed to update document type.';
notifyApiError(error, message);
throw new Error(message);
}
},
[refreshDocumentTypes, notifyApiError, setStatusMessage],
);
const handleDocumentTypeCreate = useCallback(
async ({ name }) => {
const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) {
throw new Error('Document type name is required.');
}
try {
const { data } = await api.post('/document-types', { name: trimmed });
await refreshDocumentTypes();
setStatusMessage('Document type created.', 'success');
return data;
} catch (error) {
const message = error.response?.data?.error || 'Failed to create document type.';
notifyApiError(error, message);
throw new Error(message);
}
},
[refreshDocumentTypes, notifyApiError, setStatusMessage],
);
const handleDocumentTypeDelete = useCallback(
async (documentTypeId) => {
if (!documentTypeId) {
throw new Error('Missing document type identifier.');
}
const deletedEntry = documentTypeLookupById.get(documentTypeId);
try {
await api.delete(`/document-types/${documentTypeId}`);
mapDocumentCaches((doc) => {
if (!doc) {
return doc;
}
const existingType = doc.document_type;
const existingTypeId = doc.document_type_id ?? existingType?.id ?? null;
const existingTypeName = typeof existingType?.name === 'string' ? existingType.name : undefined;
const shouldClear =
existingTypeId === documentTypeId ||
(!!deletedEntry?.name &&
existingTypeName &&
existingTypeName.toLowerCase() === deletedEntry.name.toLowerCase());
if (!shouldClear) {
return doc;
}
return { ...doc, document_type: null, document_type_id: null };
});
await refreshDocumentTypes();
setStatusMessage('Document type deleted.', 'success');
return true;
} catch (error) {
const message = error.response?.data?.error || 'Failed to delete document type.';
notifyApiError(error, message);
throw new Error(message);
}
},
[
refreshDocumentTypes,
notifyApiError,
setStatusMessage,
mapDocumentCaches,
documentTypeLookupById,
],
);
const handleDocumentTypeAssign = useCallback(
async ({ documentId, documentTypeId, documentType }, { notify = true } = {}) => {
if (!documentId) {
throw new Error('Missing document identifier.');
}
try {
const payload = { document_type_id: documentTypeId ?? null };
const { data } = await api.patch(`/documents/${documentId}`, payload);
const updatedDocument = extractDocumentFromResponse(data);
updateDocumentCaches(documentId, (doc) => {
if (updatedDocument) {
return { ...doc, ...updatedDocument };
}
const next = { ...doc };
if (documentTypeId) {
const entry =
documentTypeLookupById.get(documentTypeId) ||
(documentType?.name ? documentType : null);
next.document_type = entry
? { id: entry.id ?? documentTypeId, name: entry.name }
: { id: documentTypeId, name: '' };
} else {
next.document_type = null;
}
next.document_type_id = documentTypeId ?? null;
return next;
});
if (notify) {
setStatusMessage(
documentTypeId ? 'Document type updated.' : 'Document type cleared.',
'success',
);
}
return true;
} catch (error) {
const message = error.response?.data?.error || 'Failed to update document type.';
notifyApiError(error, message);
return false;
}
},
[
extractDocumentFromResponse,
updateDocumentCaches,
documentTypeLookupById,
notifyApiError,
setStatusMessage,
],
);
const handleDocumentTypeClear = useCallback(
async ({ documentId }, options = {}) =>
handleDocumentTypeAssign({ documentId, documentTypeId: null }, options),
[handleDocumentTypeAssign],
);
const handleDocumentTypeSet = useCallback(
async ({ document, name, input }) => {
if (!document?.id) {
throw new Error('Missing document for document type assignment.');
}
const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) {
setStatusMessage('Document type name is required.', 'error');
return;
}
let target = documentTypeLookupByName.get(trimmed.toLowerCase()) || null;
if (!target) {
try {
target = await handleDocumentTypeCreate({ name: trimmed });
} catch (error) {
return;
}
}
if (!target?.id) {
setStatusMessage('Unable to resolve document type.', 'error');
return;
}
const success = await handleDocumentTypeAssign(
{ documentId: document.id, documentTypeId: target.id, documentType: target },
{ notify: true },
);
if (success && input) {
input.value = '';
}
},
[
handleDocumentTypeCreate,
handleDocumentTypeAssign,
documentTypeLookupByName,
setStatusMessage,
],
);
const resolveTargetDocumentIds = useCallback(
(candidateIds) => {
const normalized = Array.isArray(candidateIds)
@@ -2052,90 +1795,6 @@ const AppLayout = () => {
[selectedDocumentIds],
);
const handleBulkDocumentTypeSet = useCallback(
async ({ name, input, documentIds }) => {
const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) {
setStatusMessage('Document type name is required.', 'error');
return;
}
const targets = resolveTargetDocumentIds(documentIds);
if (!targets.length) {
setStatusMessage('Select documents before assigning a document type.', 'error');
return;
}
let target = documentTypeLookupByName.get(trimmed.toLowerCase()) || null;
if (!target) {
try {
target = await handleDocumentTypeCreate({ name: trimmed });
} catch (error) {
return;
}
}
if (!target?.id) {
setStatusMessage('Unable to resolve document type.', 'error');
return;
}
try {
await Promise.all(
targets.map((documentId) =>
api.patch(`/documents/${documentId}`, { document_type_id: target.id }),
),
);
await refreshCurrentFolder();
const suffix = targets.length === 1 ? '' : 's';
setStatusMessage(
`Document type assigned to ${targets.length} document${suffix}.`,
'success',
);
if (input) {
input.value = '';
}
} catch (error) {
const message = error.response?.data?.error || 'Failed to assign document type.';
notifyApiError(error, message);
}
},
[
documentTypeLookupByName,
handleDocumentTypeCreate,
refreshCurrentFolder,
resolveTargetDocumentIds,
setStatusMessage,
notifyApiError,
],
);
const handleBulkDocumentTypeClear = useCallback(
async ({ documentIds }) => {
const targets = resolveTargetDocumentIds(documentIds);
if (!targets.length) {
setStatusMessage('Select documents before clearing the document type.', 'error');
return;
}
try {
await Promise.all(
targets.map((documentId) => api.patch(`/documents/${documentId}`, { document_type_id: null })),
);
await refreshCurrentFolder();
const suffix = targets.length === 1 ? '' : 's';
setStatusMessage(
`Document type cleared from ${targets.length} document${suffix}.`,
'success',
);
} catch (error) {
const message = error.response?.data?.error || 'Failed to clear document type.';
notifyApiError(error, message);
}
},
[resolveTargetDocumentIds, refreshCurrentFolder, setStatusMessage, notifyApiError],
);
const handleTagDelete = useCallback(
async (tagId) => {
if (!tagId) {
@@ -2336,7 +1995,7 @@ const AppLayout = () => {
const initializeAfterLogin = useCallback(async () => {
setLoading(true);
try {
await Promise.all([refreshTags(), refreshCorrespondents(), refreshDocumentTypes()]);
await Promise.all([refreshTags(), refreshCorrespondents()]);
const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root';
await loadFolder(initialFolder, { showLoading: false });
} catch (error) {
@@ -2345,7 +2004,7 @@ const AppLayout = () => {
} finally {
setLoading(false);
}
}, [refreshTags, refreshCorrespondents, refreshDocumentTypes, routeFolderId, loadFolder, notifyApiError]);
}, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder, notifyApiError]);
useEffect(() => {
if (!token) {
@@ -4038,12 +3697,7 @@ const AppLayout = () => {
}
}, [creatingFolder, handleFolderCreate, setStatusMessage]);
const {
managementModals,
openTagsModal,
openCorrespondentsModal,
openDocumentTypesModal,
} = useManagementModals({
const { managementModals, openTagsModal, openCorrespondentsModal } = useManagementModals({
locationPathname: location.pathname,
tags,
refreshTags,
@@ -4055,11 +3709,6 @@ const AppLayout = () => {
onCorrespondentCreate: handleCorrespondentCreate,
onCorrespondentUpdate: handleCorrespondentUpdate,
onCorrespondentDelete: handleCorrespondentDelete,
documentTypes,
refreshDocumentTypes,
onDocumentTypeCreate: handleDocumentTypeCreate,
onDocumentTypeUpdate: handleDocumentTypeUpdate,
onDocumentTypeDelete: handleDocumentTypeDelete,
setStatusMessage,
});
@@ -4095,9 +3744,6 @@ const AppLayout = () => {
if (activeCorrespondentFilters.length) {
params.correspondents = activeCorrespondentFilters.join(',');
}
if (activeDocumentTypeFilters.length) {
params.document_types = activeDocumentTypeFilters.join(',');
}
const folderIdentifier = selectedFolder === 'root' ? null : selectedFolder;
if (folderIdentifier) {
params.folder_id = folderIdentifier;
@@ -4179,7 +3825,6 @@ const AppLayout = () => {
searchQuery,
activeTagFilters,
activeCorrespondentFilters,
activeDocumentTypeFilters,
selectedFolder,
notifyApiError,
assetManager,
@@ -4981,7 +4626,7 @@ const AppLayout = () => {
setWorkspaceMode('table');
navigate('/documents', { replace: true });
await Promise.all([refreshTags(), refreshCorrespondents(), refreshDocumentTypes()]);
await Promise.all([refreshTags(), refreshCorrespondents()]);
await loadFolder('root', { showLoading: false, preserveSearch: false });
const tenantLabel = data?.tenant?.name || data?.tenant?.id || 'tenant';
@@ -5002,7 +4647,6 @@ const AppLayout = () => {
navigate,
refreshTags,
refreshCorrespondents,
refreshDocumentTypes,
loadFolder,
],
);
@@ -5128,10 +4772,6 @@ const AppLayout = () => {
activeCorrespondentIds: activeCorrespondentFilters,
onToggleCorrespondentFilter: toggleCorrespondentFilter,
onCreateCorrespondent: (name) => handleCorrespondentCreate({ name }),
documentTypes,
activeDocumentTypeIds: activeDocumentTypeFilters,
onToggleDocumentTypeFilter: toggleDocumentTypeFilter,
onCreateDocumentType: (name) => handleDocumentTypeCreate({ name }),
appStatus,
loading,
previewActive,
@@ -5155,7 +4795,6 @@ const AppLayout = () => {
clearFilters,
correspondents,
currentTenantId,
documentTypes,
folderClickHandlers,
folderNodes,
handleFolderDelete,
@@ -5181,13 +4820,10 @@ const AppLayout = () => {
toggleTagFilter,
handleTagCreate,
handleCorrespondentCreate,
toggleDocumentTypeFilter,
handleDocumentTypeCreate,
handlePromptCreateFolder,
creatingFolder,
handleNeutralHueChange,
neutralHue,
activeDocumentTypeFilters,
],
);
@@ -5222,11 +4858,6 @@ const AppLayout = () => {
correspondents,
onCorrespondentAdd: handleCorrespondentAdd,
onCorrespondentRemove: handleCorrespondentRemove,
documentTypes,
onDocumentTypeSet: handleDocumentTypeSet,
onDocumentTypeClear: handleDocumentTypeClear,
onBulkDocumentTypeSet: handleBulkDocumentTypeSet,
onBulkDocumentTypeClear: handleBulkDocumentTypeClear,
resolveApiPath,
onFolderNavigate: selectFolder,
onClose: handleDetailPanelClose,
@@ -5241,15 +4872,11 @@ const AppLayout = () => {
getDocumentAsset,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkDocumentTypeSet,
handleBulkDocumentTypeClear,
handleBulkSelectionReanalyze,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleCorrespondentAdd,
handleCorrespondentRemove,
handleDocumentTypeSet,
handleDocumentTypeClear,
handleDetailPanelClose,
handleDocumentTitleUpdate,
handleTagAdd,
@@ -5262,7 +4889,6 @@ const AppLayout = () => {
selectedPreviewEntry,
tags,
tagLookupById,
documentTypes,
],
);
@@ -5321,16 +4947,6 @@ const AppLayout = () => {
handleDocumentCorrespondentAttach,
handleCorrespondentRemove,
handleCorrespondentAdd,
documentTypes,
refreshDocumentTypes,
handleDocumentTypeUpdate,
handleDocumentTypeCreate,
handleDocumentTypeDelete,
handleDocumentTypeAssign,
handleDocumentTypeClear,
handleDocumentTypeSet,
handleBulkDocumentTypeSet,
handleBulkDocumentTypeClear,
webdavTokens,
webdavTokensLoading,
creatingWebdavToken,
@@ -5366,7 +4982,6 @@ const AppLayout = () => {
notifyApiError,
openTagsModal,
openCorrespondentsModal,
openDocumentTypesModal,
openSettings,
detailPanelOpen,
setDetailPanelOpen,
@@ -5392,16 +5007,6 @@ const AppLayout = () => {
handleDocumentCorrespondentAttach,
handleCorrespondentRemove,
handleCorrespondentAdd,
documentTypes,
refreshDocumentTypes,
handleDocumentTypeUpdate,
handleDocumentTypeCreate,
handleDocumentTypeDelete,
handleDocumentTypeAssign,
handleDocumentTypeClear,
handleDocumentTypeSet,
handleBulkDocumentTypeSet,
handleBulkDocumentTypeClear,
webdavTokens,
webdavTokensLoading,
creatingWebdavToken,
@@ -5436,7 +5041,6 @@ const AppLayout = () => {
notifyApiError,
openTagsModal,
openCorrespondentsModal,
openDocumentTypesModal,
openSettings,
detailPanelOpen,
setDetailPanelOpen,
+1 -3
View File
@@ -14,7 +14,6 @@ const DocumentsRoute = () => {
skeuoWorkspaceProps,
openTagsModal,
openCorrespondentsModal,
openDocumentTypesModal,
previewWorkspaceDocument,
previewWorkspaceEntry,
closeDocumentPreview,
@@ -36,10 +35,9 @@ const DocumentsRoute = () => {
...sidebarProps,
onManageTags: openTagsModal,
onManageCorrespondents: openCorrespondentsModal,
onManageDocumentTypes: openDocumentTypesModal,
onCollapse: collapseSidebar,
}),
[sidebarProps, openTagsModal, openCorrespondentsModal, openDocumentTypesModal, collapseSidebar],
[sidebarProps, openTagsModal, openCorrespondentsModal, collapseSidebar],
);
const breadcrumbs = documentsTableProps?.breadcrumbs || null;
-60
View File
@@ -1,11 +1,9 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import TagsPanel from '../tags/TagsPanel';
import CorrespondentsPanel from '../correspondents/CorrespondentsPanel';
import DocumentTypesPanel from '../documentTypes/DocumentTypesPanel';
const TAGS_MODAL = 'tags';
const CORRESPONDENTS_MODAL = 'correspondents';
const DOCUMENT_TYPES_MODAL = 'document-types';
export const useManagementModals = ({
locationPathname,
@@ -19,11 +17,6 @@ export const useManagementModals = ({
onCorrespondentCreate,
onCorrespondentUpdate,
onCorrespondentDelete,
documentTypes,
refreshDocumentTypes,
onDocumentTypeCreate,
onDocumentTypeUpdate,
onDocumentTypeDelete,
setStatusMessage,
}) => {
const [activeModal, setActiveModal] = useState(null);
@@ -33,10 +26,6 @@ export const useManagementModals = ({
() => setActiveModal(CORRESPONDENTS_MODAL),
[],
);
const openDocumentTypesModal = useCallback(
() => setActiveModal(DOCUMENT_TYPES_MODAL),
[],
);
const closeActiveModal = useCallback(() => setActiveModal(null), []);
useEffect(() => {
@@ -151,58 +140,10 @@ export const useManagementModals = ({
setStatusMessage,
]);
const documentTypesModal = useMemo(() => {
if (activeModal !== DOCUMENT_TYPES_MODAL) {
return null;
}
return (
<div
className="modal-backdrop"
role="presentation"
onClick={closeActiveModal}
>
<div
className="modal modal--panel"
role="dialog"
aria-modal="true"
aria-labelledby="document-types-modal-title"
onClick={(event) => event.stopPropagation()}
>
<div className="panel-modal__header">
<h3 id="document-types-modal-title">Manage Document Types</h3>
<button type="button" className="secondary" onClick={closeActiveModal}>
Close
</button>
</div>
<div className="panel-modal__body">
<DocumentTypesPanel
documentTypes={documentTypes}
onRefresh={refreshDocumentTypes}
onCreate={onDocumentTypeCreate}
onUpdate={onDocumentTypeUpdate}
onDelete={onDocumentTypeDelete}
onNotify={setStatusMessage}
/>
</div>
</div>
</div>
);
}, [
activeModal,
closeActiveModal,
documentTypes,
onDocumentTypeCreate,
onDocumentTypeDelete,
onDocumentTypeUpdate,
refreshDocumentTypes,
setStatusMessage,
]);
const managementModals = (
<>
{tagModal}
{correspondentsModal}
{documentTypesModal}
</>
);
@@ -210,7 +151,6 @@ export const useManagementModals = ({
managementModals,
openTagsModal,
openCorrespondentsModal,
openDocumentTypesModal,
closeActiveModal,
activeModal,
};
-170
View File
@@ -34,25 +34,6 @@ const sortCorrespondents = (entries = []) =>
.map(({ id, name, count }) => ({ id, name, count }))
.sort((a, b) => a.name.localeCompare(b.name));
const resolveDocumentType = (doc) => {
if (!doc) {
return null;
}
const type = doc.document_type;
const fallbackId = doc.document_type_id ?? null;
if (!type) {
return null;
}
if (typeof type === 'object' && typeof type.name === 'string') {
const name = type.name.trim();
if (!name) {
return null;
}
return { id: type.id ?? fallbackId, name };
}
return null;
};
const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => (
<div className="correspondent-list">
{entries.length ? (
@@ -318,11 +299,6 @@ const DetailPanel = ({
correspondents = [],
onCorrespondentAdd,
onCorrespondentRemove,
documentTypes = [],
onDocumentTypeSet,
onDocumentTypeClear,
onBulkDocumentTypeSet,
onBulkDocumentTypeClear,
resolveApiPath,
onFolderNavigate = null,
resolveFolderPath = null,
@@ -617,28 +593,6 @@ const DetailPanel = ({
}, []);
}, [availableCorrespondents]);
const documentTypeOptions = useMemo(() => {
const seen = new Set();
return (documentTypes || []).reduce((options, entry) => {
if (typeof entry?.name !== 'string') {
return options;
}
const name = entry.name.trim();
if (!name) {
return options;
}
const lower = name.toLowerCase();
if (seen.has(lower)) {
return options;
}
seen.add(lower);
options.push(name);
return options;
}, []);
}, [documentTypes]);
const singleDocumentType = useMemo(() => resolveDocumentType(singleDoc), [singleDoc]);
const singleCorrespondents = useMemo(() => {
if (!singleDoc) return [];
return sortCorrespondents(singleDoc.correspondents || []);
@@ -774,42 +728,6 @@ const DetailPanel = ({
.sort((a, b) => a.name.localeCompare(b.name));
}, [selectedDocuments]);
const bulkDocumentTypes = useMemo(() => {
if (!selectedDocuments.length) {
return [];
}
const map = new Map();
selectedDocuments.forEach((doc) => {
const type = resolveDocumentType(doc);
if (!type) return;
const key = type.id ?? type.name.toLowerCase();
if (!map.has(key)) {
map.set(key, { id: type.id ?? null, name: type.name, count: 0 });
}
map.get(key).count += 1;
});
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
}, [selectedDocuments]);
const bulkDocumentTypeSummary = useMemo(() => {
if (!bulkDocumentTypes.length) {
return '';
}
return bulkDocumentTypes
.map((entry) => {
if (!entry?.name) {
return null;
}
const suffix = entry.count === selectedDocuments.length ? '' : ` (${entry.count})`;
return `${entry.name}${suffix}`;
})
.filter(Boolean)
.join(', ');
}, [bulkDocumentTypes, selectedDocuments.length]);
const handleBulkCorrespondentRemove = useCallback(
(entry) => {
if (!entry?.id) return;
@@ -1177,53 +1095,6 @@ const DetailPanel = ({
);
})}
</div>
<div className="detail-field">
<div className="detail-field__label">Document type</div>
<div className="detail-field__value">
{singleDocumentType?.name ? (
<span>{singleDocumentType.name}</span>
) : (
<span className="meta">None assigned.</span>
)}
{singleDocumentType && onDocumentTypeClear ? (
<button
type="button"
className="secondary"
onClick={() => onDocumentTypeClear?.({ documentId: singleDoc.id })}
>
Clear
</button>
) : null}
</div>
{onDocumentTypeSet ? (
<form
className="inline"
onSubmit={(event) => {
event.preventDefault();
const form = event.currentTarget;
const input = form.elements.documentType;
const value = input?.value?.trim();
if (!value) {
return;
}
onDocumentTypeSet?.({ document: singleDoc, name: value, input });
}}
>
<input
name="documentType"
placeholder="Assign or create type"
list="document-type-catalog-single"
defaultValue=""
/>
<button type="submit">Set</button>
<datalist id="document-type-catalog-single">
{documentTypeOptions.map((name) => (
<option key={name} value={name} />
))}
</datalist>
</form>
) : null}
</div>
<TagSection
title="Tags"
tags={tagsForDoc.map((tag) => ({
@@ -1348,47 +1219,6 @@ const DetailPanel = ({
datalistOptions={tags}
className="bulk-tags"
/>
<div className="detail-field">
<div className="detail-field__label">Document type</div>
<div className="detail-field__value">
<span>{bulkDocumentTypeSummary || 'None assigned.'}</span>
{onBulkDocumentTypeClear && bulkDocumentTypes.length > 0 ? (
<button
type="button"
className="secondary"
onClick={() => onBulkDocumentTypeClear?.({ documentIds })}
>
Clear
</button>
) : null}
</div>
{onBulkDocumentTypeSet ? (
<form
className="inline"
onSubmit={(event) => {
event.preventDefault();
const form = event.currentTarget;
const input = form.elements.documentType;
const value = input?.value?.trim();
if (!value) {
return;
}
onBulkDocumentTypeSet?.({ name: value, input, documentIds });
}}
>
<input
name="documentType"
placeholder="Assign or create type"
list="document-type-catalog-bulk"
defaultValue=""
/>
<button type="submit">Set</button>
<datalist id="document-type-catalog-bulk">
{documentTypeOptions.map((name) => (<option key={name} value={name} />))}
</datalist>
</form>
) : null}
</div>
<CorrespondentSection
title="Correspondents"
entries={bulkCorrespondents}
@@ -1,233 +0,0 @@
import React, { useCallback, useState } from 'react';
function DocumentTypesPanel({
documentTypes = [],
onRefresh,
onCreate,
onUpdate,
onDelete,
onNotify,
}) {
const [editingId, setEditingId] = useState(null);
const [draftName, setDraftName] = useState('');
const [createName, setCreateName] = useState('');
const [saving, setSaving] = useState(false);
const [creating, setCreating] = useState(false);
const [deletingId, setDeletingId] = useState(null);
const startEdit = useCallback((entry) => {
setEditingId(entry.id);
setDraftName(entry.name);
}, []);
const cancelEdit = useCallback(() => {
setEditingId(null);
setDraftName('');
setSaving(false);
}, []);
const handleSave = useCallback(async () => {
if (!editingId) return;
const trimmed = draftName.trim();
if (!trimmed) {
onNotify?.('Document type name cannot be empty.', 'error');
return;
}
setSaving(true);
try {
await onUpdate(editingId, { name: trimmed });
cancelEdit();
} catch (error) {
onNotify?.('Failed to update document type.', 'error');
console.error('[document-types] update failed', error);
setSaving(false);
}
}, [editingId, draftName, onUpdate, cancelEdit, onNotify]);
const handleDelete = useCallback(
async (entry) => {
if (!entry?.id) return;
setDeletingId(entry.id);
try {
await onDelete(entry.id);
if (editingId === entry.id) {
cancelEdit();
}
} catch (error) {
onNotify?.('Failed to delete document type.', 'error');
console.error('[document-types] delete failed', error);
} finally {
setDeletingId(null);
}
},
[onDelete, editingId, cancelEdit, onNotify],
);
const handleCreate = useCallback(
async (event) => {
event.preventDefault();
const trimmed = createName.trim();
if (!trimmed) {
onNotify?.('Document type name cannot be empty.', 'error');
return;
}
setCreating(true);
try {
await onCreate({ name: trimmed });
setCreateName('');
} catch (error) {
onNotify?.('Failed to create document type.', 'error');
console.error('[document-types] create failed', error);
} finally {
setCreating(false);
}
},
[createName, onCreate, onNotify],
);
const handleKeyDown = useCallback(
(event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleSave();
} else if (event.key === 'Escape') {
event.preventDefault();
cancelEdit();
}
},
[handleSave, cancelEdit],
);
const renderUsage = useCallback((usage) => {
if (!usage) {
return '0';
}
const total = typeof usage.total === 'number' ? usage.total : 0;
return total.toString();
}, []);
return (
<section className="correspondents-panel">
<div className="panel-section__header">
<div className="panel-section__titles">
<h2>Document Types</h2>
<div className="panel-section__subtitle">{documentTypes.length} total</div>
</div>
<div className="header-actions correspondents-actions">
<form className="correspondents-actions__form" onSubmit={handleCreate}>
<input
type="text"
placeholder="New document type name"
value={createName}
onChange={(event) => setCreateName(event.target.value)}
disabled={creating}
/>
<button type="submit" disabled={creating || !createName.trim()}>
{creating ? 'Creating…' : 'Create'}
</button>
</form>
<button
className="secondary"
type="button"
onClick={onRefresh}
disabled={saving || creating || Boolean(deletingId)}
>
Refresh
</button>
</div>
</div>
<div className="panel-section__body tags-panel__body">
{documentTypes.length === 0 ? (
<div className="empty-state">No document types created yet.</div>
) : (
<div className="tags-table">
<table>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col" className="numeric">
Usage
</th>
<th scope="col" className="actions">
Actions
</th>
</tr>
</thead>
<tbody>
{documentTypes.map((entry) => {
const isEditing = editingId === entry.id;
return (
<tr key={entry.id} className={isEditing ? 'editing' : ''}>
<td className="tags-table__label">
{isEditing ? (
<input
className="tags-table__label-input"
value={draftName}
onChange={(event) => setDraftName(event.target.value)}
onKeyDown={handleKeyDown}
disabled={saving}
autoFocus
/>
) : (
<span>{entry.name}</span>
)}
</td>
<td className="numeric">{renderUsage(entry.usage)}</td>
<td className="actions">
{isEditing ? (
<div className="tags-table__edit-controls">
<button
type="button"
className="secondary"
onClick={handleSave}
disabled={saving}
>
Save
</button>
<button
type="button"
className="secondary"
onClick={cancelEdit}
disabled={saving}
>
Cancel
</button>
</div>
) : (
<div className="tags-table__row-actions">
<button
type="button"
className="icon-button ghost"
onClick={() => startEdit(entry)}
title="Rename"
aria-label={`Rename document type ${entry.name}`}
>
Edit
</button>
<button
type="button"
className="icon-button danger"
onClick={() => handleDelete(entry)}
disabled={deletingId === entry.id}
title="Delete"
aria-label={`Delete document type ${entry.name}`}
>
Delete
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</section>
);
}
export default DocumentTypesPanel;
-105
View File
@@ -56,29 +56,6 @@ const resolveCorrespondents = (doc) => {
return results;
};
const resolveDocumentType = (doc) => {
if (!doc) {
return null;
}
const type = doc.document_type;
const fallbackId = doc.document_type_id ?? null;
if (!type) {
return null;
}
if (typeof type === 'object' && typeof type.name === 'string') {
const name = type.name.trim();
if (!name) {
return null;
}
return { id: type.id ?? fallbackId, name };
}
return null;
};
// Detects when an element becomes visible within a scroll container.
const useLazyVisibility = (rootRef, resetKey) => {
const targetRef = useRef(null);
@@ -254,8 +231,6 @@ const DocumentsTable = ({
getDownloadHref,
onTagClick,
onCorrespondentClick,
activeDocumentTypeIds = [],
onDocumentTypeClick,
isSearchLoading = false,
onDocumentTagDrop,
viewMode = 'list',
@@ -282,10 +257,6 @@ const DocumentsTable = ({
() => new Set(activeCorrespondentIds || []),
[activeCorrespondentIds],
);
const activeDocumentTypeIdSet = useMemo(
() => new Set(activeDocumentTypeIds || []),
[activeDocumentTypeIds],
);
const scrollRef = useRef(null);
const suppressDocumentClickRef = useRef(false);
const [, forceVisibilityTick] = useState(0);
@@ -657,43 +628,10 @@ const DocumentsTable = ({
const visibleTags = tagList.slice(0, 3);
const remainingTagCount = tagList.length > 3 ? tagList.length - 3 : 0;
const correspondents = resolveCorrespondents(doc);
const documentType = resolveDocumentType(doc);
const isDocumentTypeActive =
documentType?.id != null && activeDocumentTypeIdSet.has(documentType.id);
const canToggleDocumentType =
documentType?.id != null && typeof onDocumentTypeClick === 'function';
const cardClasses = ['document-card', 'document'];
if (isSelected) cardClasses.push('selected');
if (isDraggingDoc) cardClasses.push('is-dragging');
const titleText = doc.title || doc.original_name;
const documentTypeNode = documentType
? (
<span
className={`doc-type-inline${
isDocumentTypeActive ? ' active' : ''
}`}
role={canToggleDocumentType ? 'button' : undefined}
tabIndex={canToggleDocumentType ? 0 : undefined}
onClick={(event) => {
event.stopPropagation();
if (canToggleDocumentType) {
onDocumentTypeClick?.(documentType.id, documentType);
}
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
if (canToggleDocumentType) {
onDocumentTypeClick?.(documentType.id, documentType);
}
}
}}
>
({documentType.name})
</span>
)
: null;
return (
<div
@@ -733,12 +671,6 @@ const DocumentsTable = ({
</span>
) : null}
<span className="doc-name__primary">{titleText}</span>
{documentTypeNode ? (
<>
{' '}
{documentTypeNode}
</>
) : null}
</div>
{visibleTags.length > 0 && (
<div className="document-card__tags">
@@ -924,38 +856,7 @@ const DocumentsTable = ({
if (isDraggingDoc) rowClasses.push('is-dragging');
const downloadHref = getDownloadHref?.(doc) || null;
const correspondents = resolveCorrespondents(doc);
const documentType = resolveDocumentType(doc);
const isRowDocumentTypeActive = documentType?.id != null && activeDocumentTypeIdSet.has(documentType.id);
const canToggleRowDocumentType = documentType?.id != null && typeof onDocumentTypeClick === 'function';
const titleText = doc.title || doc.original_name;
const documentTypeNode = documentType
? (
<span
className={`doc-type-inline${
isRowDocumentTypeActive ? ' active' : ''
}`}
role={canToggleRowDocumentType ? 'button' : undefined}
tabIndex={canToggleRowDocumentType ? 0 : undefined}
onClick={(event) => {
event.stopPropagation();
if (canToggleRowDocumentType) {
onDocumentTypeClick?.(documentType.id, documentType);
}
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
if (canToggleRowDocumentType) {
onDocumentTypeClick?.(documentType.id, documentType);
}
}
}}
>
({documentType.name})
</span>
)
: null;
return (
<tr
@@ -991,12 +892,6 @@ const DocumentsTable = ({
</span>
) : null}
<span className="doc-name__primary">{titleText}</span>
{documentTypeNode ? (
<>
{' '}
{documentTypeNode}
</>
) : null}
</span>
</div>
{(doc.tags || []).length > 0 && (
@@ -3,8 +3,6 @@ import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons'
import { describeDocumentSummary } from '../documents/documentSummary';
import { createDocumentActionState } from '../documents/documentActions';
const resolveDocumentTypeName = (doc) => doc?.document_type?.name;
const PreviewWorkspace = ({
document,
previewEntry,
@@ -21,7 +19,6 @@ const PreviewWorkspace = ({
const tags = Array.isArray(document.tags)
? document.tags.map((tag) => tag?.label).filter(Boolean).join(', ')
: '';
const documentTypeName = resolveDocumentTypeName(document);
const formatDateTime = (value) => {
if (!value) {
@@ -36,7 +33,6 @@ const PreviewWorkspace = ({
{ label: 'Archive Reference', value: document.archive_serial || '—' },
{ label: 'Issued On', value: formatDateTime(document.issued_at) },
{ label: 'Correspondent', value: correspondents || '—' },
{ label: 'Document Type', value: documentTypeName || '—' },
{
label: 'Filename',
value: document.archive_path || document.filename || '—',
-87
View File
@@ -151,15 +151,10 @@ const Sidebar = ({
correspondents = [],
activeCorrespondentIds = [],
onToggleCorrespondentFilter,
documentTypes = [],
activeDocumentTypeIds = [],
onToggleDocumentTypeFilter,
onManageTags,
onManageCorrespondents,
onManageDocumentTypes,
onCreateTag,
onCreateCorrespondent,
onCreateDocumentType,
searchQuery = '',
onSearchChange,
onSearchSubmit,
@@ -187,24 +182,10 @@ const Sidebar = ({
() => new Set(activeCorrespondentIds || []),
[activeCorrespondentIds],
);
const sortedDocumentTypes = useMemo(() => {
if (!Array.isArray(documentTypes)) {
return [];
}
return documentTypes
.filter((entry) => entry?.name)
.slice()
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
}, [documentTypes]);
const activeDocumentTypeSet = useMemo(
() => new Set(activeDocumentTypeIds || []),
[activeDocumentTypeIds],
);
const handleToggleTag = onToggleTagFilter || (() => {});
const activeTagSet = new Set(activeTagIds);
const handleManageTags = onManageTags || (() => {});
const handleManageCorrespondents = onManageCorrespondents || (() => {});
const handleManageDocumentTypes = onManageDocumentTypes || (() => {});
const handleCreateTag = useCallback(async () => {
const input = window.prompt('New tag name');
if (!input) {
@@ -237,22 +218,6 @@ const Sidebar = ({
}
}, [onCreateCorrespondent]);
const handleCreateDocumentType = useCallback(async () => {
const input = window.prompt('New document type name');
if (!input) {
return;
}
const trimmed = input.trim();
if (!trimmed) {
return;
}
try {
await onCreateDocumentType?.(trimmed);
} catch (error) {
console.error('[sidebar] failed to create document type', error);
}
}, [onCreateDocumentType]);
const handleCreateFolder = useCallback(() => {
if (creatingFolder) {
return;
@@ -629,58 +594,6 @@ const Sidebar = ({
})}
</ul>
</div>
<div className="sidebar-section">
<div className="sidebar-section__header">
<h3>Document Types</h3>
<div className="sidebar-section__actions">
<button
type="button"
className="icon-button"
onClick={handleCreateDocumentType}
aria-label="Create document type"
>
<PlusIcon size={16} />
</button>
<button
type="button"
className="icon-button"
onClick={handleManageDocumentTypes}
aria-label="Manage document types"
>
<SettingsIcon size={16} />
</button>
<span className="meta">{documentTypes.length}</span>
</div>
</div>
<ul className="sidebar-correspondent-list">
{sortedDocumentTypes.map((entry) => {
const isActive = activeDocumentTypeSet.has(entry.id);
const className = `sidebar-correspondent-item${isActive ? ' active' : ''}`;
const label = entry.name || 'Untitled';
const handleSelect = () => {
const nextId = isActive ? null : entry.id;
onToggleDocumentTypeFilter?.(nextId);
};
return (
<li key={entry.id}>
<span
className={className}
role="button"
onClick={handleSelect}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleSelect();
}
}}
>
{label}
</span>
</li>
);
})}
</ul>
</div>
{typeof neutralHue === 'number' || typeof neutralHue === 'string' ? (
<div className="sidebar-section">
<div className="sidebar-section__header">
-26
View File
@@ -2016,32 +2016,6 @@ button.danger:hover:not([disabled]) {
font-weight: 600;
}
.doc-type-inline {
color: var(--muted);
font-size: 0.85em;
line-height: 1.2;
cursor: default;
}
.doc-type-inline.active {
color: var(--accent-strong, var(--accent));
font-weight: 600;
}
.doc-type-inline[role='button'] {
cursor: pointer;
}
.doc-type-inline[role='button']:hover,
.doc-type-inline[role='button']:focus-visible {
color: var(--accent-strong, var(--accent));
}
.doc-type-inline[role='button']:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.doc-correspondent-link {
background: none;
background-color: transparent;