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 = ({
>
{
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 ? (
{
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(() => {