diff --git a/frontend/src/correspondents/CorrespondentsPanel.jsx b/frontend/src/correspondents/CorrespondentsPanel.jsx index 835705e..845090a 100644 --- a/frontend/src/correspondents/CorrespondentsPanel.jsx +++ b/frontend/src/correspondents/CorrespondentsPanel.jsx @@ -17,7 +17,7 @@ function CorrespondentsPanel({ const startEdit = useCallback((correspondent) => { setEditingId(correspondent.id); - setDraftName(correspondent.name || ''); + setDraftName(correspondent.name); }, []); const cancelEdit = useCallback(() => { diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index bdd71d2..88c7e9f 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -30,12 +30,9 @@ const derivePreviewOrientation = (metadata) => { const sortCorrespondents = (entries = []) => entries - .map((entry) => ({ - id: entry.id, - name: entry.name || '', - count: entry.count, - })) - .sort((a, b) => (a.name || '').localeCompare(b.name || '')); + .filter((entry) => entry && entry.name) + .map(({ id, name, count }) => ({ id, name, count })) + .sort((a, b) => a.name.localeCompare(b.name)); const resolveDocumentType = (doc) => { if (!doc) { @@ -46,15 +43,8 @@ const resolveDocumentType = (doc) => { if (!type) { return null; } - if (typeof type === 'string') { - const name = type.trim(); - if (!name) { - return null; - } - return { id: fallbackId, name }; - } - if (typeof type === 'object') { - const name = (type.name || type.label || '').trim(); + if (typeof type === 'object' && typeof type.name === 'string') { + const name = type.name.trim(); if (!name) { return null; } @@ -265,7 +255,7 @@ const PreviewStack = ({ > {entry.alt { event.stopPropagation(); @@ -405,7 +395,7 @@ const DetailPanel = ({ const startTitleEdit = useCallback(() => { if (!singleDoc) return; setTitleEditDocId(singleDoc.id); - setTitleDraft(singleDoc.title || singleDoc.original_name || ''); + setTitleDraft(singleDoc.title || singleDoc.original_name); setTitleError(null); }, [singleDoc]); @@ -576,14 +566,15 @@ const DetailPanel = ({ const tagMap = new Map(); selectedDocuments.forEach((doc) => { (doc.tags || []).forEach((tag) => { - const label = (tag?.label || '').trim(); + if (!tag?.label) return; + const label = tag.label.trim(); if (!label) return; if (!tagMap.has(label)) { - const fallback = tagLookupById.get(tag.id) || {}; + const fallback = tagLookupById.get(tag.id); tagMap.set(label, { id: tag.id, label, - color: tag.color || fallback.color, + color: tag.color ?? fallback?.color ?? null, }); } }); @@ -608,32 +599,42 @@ const DetailPanel = ({ const correspondentOptions = useMemo(() => { const seen = new Set(); - return availableCorrespondents - .map((entry) => (entry?.name || '').trim()) - .filter((name) => { - if (!name) return false; - const lower = name.toLowerCase(); - if (seen.has(lower)) { - return false; - } - seen.add(lower); - return true; - }); + return availableCorrespondents.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; + }, []); }, [availableCorrespondents]); const documentTypeOptions = useMemo(() => { const seen = new Set(); - return (documentTypes || []) - .map((entry) => (entry?.name || '').trim()) - .filter((name) => { - if (!name) return false; - const lower = name.toLowerCase(); - if (seen.has(lower)) { - return false; - } - seen.add(lower); - return true; - }); + 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]); @@ -751,11 +752,11 @@ const DetailPanel = ({ selectedDocuments.forEach((doc) => { if (!doc?.id) return; (doc.correspondents || []).forEach((entry) => { - if (!entry?.id) return; + if (!entry?.id || typeof entry.name !== 'string') return; if (!map.has(entry.id)) { map.set(entry.id, { id: entry.id, - name: entry.name || '', + name: entry.name, documentIds: new Set(), }); } @@ -770,7 +771,7 @@ const DetailPanel = ({ documentIds: [...entry.documentIds], count: entry.documentIds.size, })) - .sort((a, b) => (a.name || '').localeCompare(b.name || '')); + .sort((a, b) => a.name.localeCompare(b.name)); }, [selectedDocuments]); const bulkDocumentTypes = useMemo(() => { @@ -789,7 +790,7 @@ const DetailPanel = ({ map.get(key).count += 1; }); - return [...map.values()].sort((a, b) => (a.name || '').localeCompare(b.name || '')); + return [...map.values()].sort((a, b) => a.name.localeCompare(b.name)); }, [selectedDocuments]); const bulkDocumentTypeSummary = useMemo(() => { diff --git a/frontend/src/documentTypes/DocumentTypesPanel.jsx b/frontend/src/documentTypes/DocumentTypesPanel.jsx index 8dd0557..c6cf54f 100644 --- a/frontend/src/documentTypes/DocumentTypesPanel.jsx +++ b/frontend/src/documentTypes/DocumentTypesPanel.jsx @@ -17,7 +17,7 @@ function DocumentTypesPanel({ const startEdit = useCallback((entry) => { setEditingId(entry.id); - setDraftName(entry.name || ''); + setDraftName(entry.name); }, []); const cancelEdit = useCallback(() => { diff --git a/frontend/src/documents/DocumentsTable.jsx b/frontend/src/documents/DocumentsTable.jsx index 83e9b8f..6098180 100644 --- a/frontend/src/documents/DocumentsTable.jsx +++ b/frontend/src/documents/DocumentsTable.jsx @@ -32,7 +32,7 @@ const resolveCorrespondents = (doc) => { const results = []; doc.correspondents.forEach((entry, index) => { - if (!entry) return; + if (!entry || typeof entry.name !== 'string') return; const id = entry.id; const name = entry.name.trim(); @@ -68,16 +68,8 @@ const resolveDocumentType = (doc) => { return null; } - if (typeof type === 'string') { - const name = type.trim(); - if (!name) { - return null; - } - return { id: fallbackId, name }; - } - - if (typeof type === 'object') { - const name = (type.name || type.label || '').trim(); + if (typeof type === 'object' && typeof type.name === 'string') { + const name = type.name.trim(); if (!name) { return null; } @@ -138,7 +130,7 @@ const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, - alt, + alt = '', maxSize = LIST_ICON_SIZE, scrollRootRef = null, }) => { @@ -210,7 +202,7 @@ const DocumentThumbnailImage = ({ {url ? ( {alt { event.stopPropagation(); - const nextName = window.prompt('Rename folder', folder.name || ''); + const nextName = window.prompt('Rename folder', folder.name); if (!nextName) { return; } diff --git a/frontend/src/documents/documentSummary.js b/frontend/src/documents/documentSummary.js index 371cac1..66fb986 100644 --- a/frontend/src/documents/documentSummary.js +++ b/frontend/src/documents/documentSummary.js @@ -29,26 +29,14 @@ const sanitizeTags = (tags) => { if (!Array.isArray(tags)) { return []; } - return tags - .filter((tag) => tag && (tag.label || tag.id)) - .map((tag) => ({ - id: tag.id, - label: tag.label || '', - color: tag.color || null, - })); + return tags.filter((tag) => tag && (tag.label || tag.id)); }; const sanitizeCorrespondents = (entries) => { if (!Array.isArray(entries)) { return []; } - return entries - .filter((entry) => entry && (entry.name || entry.id)) - .map((entry) => ({ - id: entry.id, - name: entry.name || '', - count: entry.count, - })); + return entries.filter((entry) => entry && (entry.name || entry.id)); }; export const describeDocumentSummary = (document, options = {}) => { @@ -76,8 +64,8 @@ export const describeDocumentSummary = (document, options = {}) => { formatDateTime = defaultFormatDateTime, } = options; - const title = document.title || ''; - const originalName = document.original_name || ''; + const title = document.title; + const originalName = document.original_name; const mimeTypeLabel = document.content_type || 'Unknown'; const sizeBytes = Number(document.current_version?.size_bytes); diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index 8b0070d..6990eb6 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -55,7 +55,7 @@ const api = axios.create({ }); const storage = typeof window !== 'undefined' ? window.sessionStorage : undefined; -const STORED_TOKEN = storage?.getItem('papercrate_token') || ''; +const STORED_TOKEN = storage?.getItem('papercrate_token') ?? ''; let STORED_TENANT = null; if (storage) { try { @@ -192,7 +192,7 @@ const AppStateProvider = ({ children }) => { const [state, dispatch] = useReducer(appStateReducer, initialAppState); useEffect(() => { - const token = state.token || ''; + const token = state.token ?? ''; if (token) { api.defaults.headers.common.Authorization = `Bearer ${token}`; storage?.setItem('papercrate_token', token); @@ -1952,7 +1952,7 @@ const AppLayout = () => { const handleCorrespondentCreate = useCallback( async ({ name }) => { - const trimmed = (name || '').trim(); + const trimmed = typeof name === 'string' ? name.trim() : ''; if (!trimmed) { throw new Error('Correspondent name is required.'); } @@ -2073,7 +2073,7 @@ const AppLayout = () => { if (!document?.id) { throw new Error('Missing document for correspondent assignment.'); } - const trimmed = (name || '').trim(); + const trimmed = typeof name === 'string' ? name.trim() : ''; if (!trimmed) { setStatusMessage('Correspondent name is required.', 'error'); return; @@ -2149,7 +2149,7 @@ const AppLayout = () => { const handleDocumentTypeCreate = useCallback( async ({ name }) => { - const trimmed = (name || '').trim(); + const trimmed = typeof name === 'string' ? name.trim() : ''; if (!trimmed) { throw new Error('Document type name is required.'); } @@ -2184,12 +2184,8 @@ const AppLayout = () => { } const existingType = doc.document_type; - const existingTypeId = - doc.document_type_id ?? (typeof existingType === 'object' ? existingType?.id : null); - const existingTypeName = - typeof existingType === 'string' - ? existingType - : existingType?.name || existingType?.label || ''; + const existingTypeId = doc.document_type_id ?? existingType?.id ?? null; + const existingTypeName = typeof existingType?.name === 'string' ? existingType.name : undefined; const shouldClear = existingTypeId === documentTypeId || @@ -2223,7 +2219,7 @@ const AppLayout = () => { ); const handleDocumentTypeAssign = useCallback( - async ({ documentId, documentTypeId }, { notify = true } = {}) => { + async ({ documentId, documentTypeId, documentType }, { notify = true } = {}) => { if (!documentId) { throw new Error('Missing document identifier.'); } @@ -2240,8 +2236,12 @@ const AppLayout = () => { const next = { ...doc }; if (documentTypeId) { - const entry = documentTypeLookupById.get(documentTypeId); - next.document_type = entry || { id: documentTypeId, name: entry?.name || '' }; + 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; } @@ -2282,7 +2282,7 @@ const AppLayout = () => { if (!document?.id) { throw new Error('Missing document for document type assignment.'); } - const trimmed = (name || '').trim(); + const trimmed = typeof name === 'string' ? name.trim() : ''; if (!trimmed) { setStatusMessage('Document type name is required.', 'error'); return; @@ -2303,7 +2303,7 @@ const AppLayout = () => { } const success = await handleDocumentTypeAssign( - { documentId: document.id, documentTypeId: target.id }, + { documentId: document.id, documentTypeId: target.id, documentType: target }, { notify: true }, ); @@ -2334,7 +2334,7 @@ const AppLayout = () => { const handleBulkDocumentTypeSet = useCallback( async ({ name, input, documentIds }) => { - const trimmed = (name || '').trim(); + const trimmed = typeof name === 'string' ? name.trim() : ''; if (!trimmed) { setStatusMessage('Document type name is required.', 'error'); return; @@ -2658,7 +2658,7 @@ const AppLayout = () => { const handleBulkCorrespondentAdd = useCallback( async ({ name, input, documentIds }) => { - const trimmed = (name || '').trim(); + const trimmed = typeof name === 'string' ? name.trim() : ''; if (!trimmed) { setStatusMessage('Correspondent name is required.', 'error'); return; @@ -2887,7 +2887,7 @@ const AppLayout = () => { const handleBulkTagAddFromDetail = useCallback( async ({ label, input, documentIds }) => { - const trimmed = (label || '').trim(); + const trimmed = typeof label === 'string' ? label.trim() : ''; if (!trimmed) { setStatusMessage('Enter a tag label.', 'error'); return; @@ -2920,7 +2920,7 @@ const AppLayout = () => { const handleBulkTagRemoveFromDetail = useCallback( async ({ label, input, documentIds }) => { - const trimmed = (label || '').trim(); + const trimmed = typeof label === 'string' ? label.trim() : ''; if (!trimmed) { setStatusMessage('Enter a tag label to remove.', 'error'); return; @@ -4190,7 +4190,7 @@ const AppLayout = () => { setStatusMessage('The root folder cannot be renamed.', 'error'); return false; } - const trimmed = (nextName || '').trim(); + const trimmed = typeof nextName === 'string' ? nextName.trim() : ''; if (!trimmed) { setStatusMessage('Folder name cannot be empty.', 'error'); return false; @@ -6157,7 +6157,7 @@ const LoginRoute = () => { const handlePasskeyLogin = useCallback( async (rawUsername) => { - const username = (rawUsername || '').trim(); + const username = typeof rawUsername === 'string' ? rawUsername.trim() : ''; if (!username) { setStatusMessage('Enter your username before using a passkey.', 'error'); return; @@ -6238,7 +6238,7 @@ const LoginRoute = () => { const handleSignup = useCallback( async (rawUsername) => { - const username = (rawUsername || '').trim(); + const username = typeof rawUsername === 'string' ? rawUsername.trim() : ''; if (!username) { setStatusMessage('Choose a username to create your account.', 'error'); return; diff --git a/frontend/src/sidebar/Sidebar.jsx b/frontend/src/sidebar/Sidebar.jsx index 9c91ec8..d113802 100644 --- a/frontend/src/sidebar/Sidebar.jsx +++ b/frontend/src/sidebar/Sidebar.jsx @@ -89,7 +89,7 @@ const FolderNode = ({ onClick={(event) => { event.stopPropagation(); if (!onRename) return; - const nextName = window.prompt('Rename folder', node.name || ''); + const nextName = window.prompt('Rename folder', node.name); if (!nextName) { return; } @@ -174,24 +174,28 @@ const Sidebar = ({ onSelectTenant, onOpenSettings, }) => { - const sortedCorrespondents = useMemo( - () => - [...correspondents].sort((a, b) => - (a?.name || '').localeCompare(b?.name || '', undefined, { sensitivity: 'base' }), - ), - [correspondents], - ); + const sortedCorrespondents = useMemo(() => { + if (!Array.isArray(correspondents)) { + return []; + } + return correspondents + .filter((entry) => entry?.name) + .slice() + .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); + }, [correspondents]); const activeCorrespondentSet = useMemo( () => new Set(activeCorrespondentIds || []), [activeCorrespondentIds], ); - const sortedDocumentTypes = useMemo( - () => - [...documentTypes].sort((a, b) => - (a?.name || '').localeCompare(b?.name || '', undefined, { sensitivity: 'base' }), - ), - [documentTypes], - ); + 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], diff --git a/frontend/src/tags/TagsPanel.jsx b/frontend/src/tags/TagsPanel.jsx index 0839e5b..2505ea7 100644 --- a/frontend/src/tags/TagsPanel.jsx +++ b/frontend/src/tags/TagsPanel.jsx @@ -24,8 +24,8 @@ function TagsPanel({ const startEdit = useCallback((tag) => { setEditingId(tag.id); - setDraftLabel(tag.label || ''); - setDraftColor(tag.color || ''); + setDraftLabel(tag.label); + setDraftColor(tag.color ?? ''); }, []); const cancelEdit = useCallback(() => {