This commit is contained in:
2025-10-31 00:28:48 +01:00
parent 813ce24aeb
commit 39d1d80383
8 changed files with 103 additions and 118 deletions
@@ -17,7 +17,7 @@ function CorrespondentsPanel({
const startEdit = useCallback((correspondent) => { const startEdit = useCallback((correspondent) => {
setEditingId(correspondent.id); setEditingId(correspondent.id);
setDraftName(correspondent.name || ''); setDraftName(correspondent.name);
}, []); }, []);
const cancelEdit = useCallback(() => { const cancelEdit = useCallback(() => {
+47 -46
View File
@@ -30,12 +30,9 @@ const derivePreviewOrientation = (metadata) => {
const sortCorrespondents = (entries = []) => const sortCorrespondents = (entries = []) =>
entries entries
.map((entry) => ({ .filter((entry) => entry && entry.name)
id: entry.id, .map(({ id, name, count }) => ({ id, name, count }))
name: entry.name || '', .sort((a, b) => a.name.localeCompare(b.name));
count: entry.count,
}))
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
const resolveDocumentType = (doc) => { const resolveDocumentType = (doc) => {
if (!doc) { if (!doc) {
@@ -46,15 +43,8 @@ const resolveDocumentType = (doc) => {
if (!type) { if (!type) {
return null; return null;
} }
if (typeof type === 'string') { if (typeof type === 'object' && typeof type.name === 'string') {
const name = type.trim(); const name = type.name.trim();
if (!name) {
return null;
}
return { id: fallbackId, name };
}
if (typeof type === 'object') {
const name = (type.name || type.label || '').trim();
if (!name) { if (!name) {
return null; return null;
} }
@@ -265,7 +255,7 @@ const PreviewStack = ({
> >
<img <img
src={entry.url} src={entry.url}
alt={entry.alt || ''} alt={entry.alt}
className="preview-stack__image" className="preview-stack__image"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
@@ -405,7 +395,7 @@ const DetailPanel = ({
const startTitleEdit = useCallback(() => { const startTitleEdit = useCallback(() => {
if (!singleDoc) return; if (!singleDoc) return;
setTitleEditDocId(singleDoc.id); setTitleEditDocId(singleDoc.id);
setTitleDraft(singleDoc.title || singleDoc.original_name || ''); setTitleDraft(singleDoc.title || singleDoc.original_name);
setTitleError(null); setTitleError(null);
}, [singleDoc]); }, [singleDoc]);
@@ -576,14 +566,15 @@ const DetailPanel = ({
const tagMap = new Map(); const tagMap = new Map();
selectedDocuments.forEach((doc) => { selectedDocuments.forEach((doc) => {
(doc.tags || []).forEach((tag) => { (doc.tags || []).forEach((tag) => {
const label = (tag?.label || '').trim(); if (!tag?.label) return;
const label = tag.label.trim();
if (!label) return; if (!label) return;
if (!tagMap.has(label)) { if (!tagMap.has(label)) {
const fallback = tagLookupById.get(tag.id) || {}; const fallback = tagLookupById.get(tag.id);
tagMap.set(label, { tagMap.set(label, {
id: tag.id, id: tag.id,
label, label,
color: tag.color || fallback.color, color: tag.color ?? fallback?.color ?? null,
}); });
} }
}); });
@@ -608,32 +599,42 @@ const DetailPanel = ({
const correspondentOptions = useMemo(() => { const correspondentOptions = useMemo(() => {
const seen = new Set(); const seen = new Set();
return availableCorrespondents return availableCorrespondents.reduce((options, entry) => {
.map((entry) => (entry?.name || '').trim()) if (typeof entry?.name !== 'string') {
.filter((name) => { return options;
if (!name) return false; }
const lower = name.toLowerCase(); const name = entry.name.trim();
if (seen.has(lower)) { if (!name) {
return false; return options;
} }
seen.add(lower); const lower = name.toLowerCase();
return true; if (seen.has(lower)) {
}); return options;
}
seen.add(lower);
options.push(name);
return options;
}, []);
}, [availableCorrespondents]); }, [availableCorrespondents]);
const documentTypeOptions = useMemo(() => { const documentTypeOptions = useMemo(() => {
const seen = new Set(); const seen = new Set();
return (documentTypes || []) return (documentTypes || []).reduce((options, entry) => {
.map((entry) => (entry?.name || '').trim()) if (typeof entry?.name !== 'string') {
.filter((name) => { return options;
if (!name) return false; }
const lower = name.toLowerCase(); const name = entry.name.trim();
if (seen.has(lower)) { if (!name) {
return false; return options;
} }
seen.add(lower); const lower = name.toLowerCase();
return true; if (seen.has(lower)) {
}); return options;
}
seen.add(lower);
options.push(name);
return options;
}, []);
}, [documentTypes]); }, [documentTypes]);
const singleDocumentType = useMemo(() => resolveDocumentType(singleDoc), [singleDoc]); const singleDocumentType = useMemo(() => resolveDocumentType(singleDoc), [singleDoc]);
@@ -751,11 +752,11 @@ const DetailPanel = ({
selectedDocuments.forEach((doc) => { selectedDocuments.forEach((doc) => {
if (!doc?.id) return; if (!doc?.id) return;
(doc.correspondents || []).forEach((entry) => { (doc.correspondents || []).forEach((entry) => {
if (!entry?.id) return; if (!entry?.id || typeof entry.name !== 'string') return;
if (!map.has(entry.id)) { if (!map.has(entry.id)) {
map.set(entry.id, { map.set(entry.id, {
id: entry.id, id: entry.id,
name: entry.name || '', name: entry.name,
documentIds: new Set(), documentIds: new Set(),
}); });
} }
@@ -770,7 +771,7 @@ const DetailPanel = ({
documentIds: [...entry.documentIds], documentIds: [...entry.documentIds],
count: entry.documentIds.size, count: entry.documentIds.size,
})) }))
.sort((a, b) => (a.name || '').localeCompare(b.name || '')); .sort((a, b) => a.name.localeCompare(b.name));
}, [selectedDocuments]); }, [selectedDocuments]);
const bulkDocumentTypes = useMemo(() => { const bulkDocumentTypes = useMemo(() => {
@@ -789,7 +790,7 @@ const DetailPanel = ({
map.get(key).count += 1; 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]); }, [selectedDocuments]);
const bulkDocumentTypeSummary = useMemo(() => { const bulkDocumentTypeSummary = useMemo(() => {
@@ -17,7 +17,7 @@ function DocumentTypesPanel({
const startEdit = useCallback((entry) => { const startEdit = useCallback((entry) => {
setEditingId(entry.id); setEditingId(entry.id);
setDraftName(entry.name || ''); setDraftName(entry.name);
}, []); }, []);
const cancelEdit = useCallback(() => { const cancelEdit = useCallback(() => {
+6 -14
View File
@@ -32,7 +32,7 @@ const resolveCorrespondents = (doc) => {
const results = []; const results = [];
doc.correspondents.forEach((entry, index) => { doc.correspondents.forEach((entry, index) => {
if (!entry) return; if (!entry || typeof entry.name !== 'string') return;
const id = entry.id; const id = entry.id;
const name = entry.name.trim(); const name = entry.name.trim();
@@ -68,16 +68,8 @@ const resolveDocumentType = (doc) => {
return null; return null;
} }
if (typeof type === 'string') { if (typeof type === 'object' && typeof type.name === 'string') {
const name = type.trim(); const name = type.name.trim();
if (!name) {
return null;
}
return { id: fallbackId, name };
}
if (typeof type === 'object') {
const name = (type.name || type.label || '').trim();
if (!name) { if (!name) {
return null; return null;
} }
@@ -138,7 +130,7 @@ const DocumentThumbnailImage = ({
document, document,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
alt, alt = '',
maxSize = LIST_ICON_SIZE, maxSize = LIST_ICON_SIZE,
scrollRootRef = null, scrollRootRef = null,
}) => { }) => {
@@ -210,7 +202,7 @@ const DocumentThumbnailImage = ({
{url ? ( {url ? (
<img <img
src={url} src={url}
alt={alt || ''} alt={alt}
className="document-thumbnail" className="document-thumbnail"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
@@ -883,7 +875,7 @@ const DocumentsTable = ({
aria-label={`Rename folder ${folder.name}`} aria-label={`Rename folder ${folder.name}`}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
const nextName = window.prompt('Rename folder', folder.name || ''); const nextName = window.prompt('Rename folder', folder.name);
if (!nextName) { if (!nextName) {
return; return;
} }
+4 -16
View File
@@ -29,26 +29,14 @@ const sanitizeTags = (tags) => {
if (!Array.isArray(tags)) { if (!Array.isArray(tags)) {
return []; return [];
} }
return tags return tags.filter((tag) => tag && (tag.label || tag.id));
.filter((tag) => tag && (tag.label || tag.id))
.map((tag) => ({
id: tag.id,
label: tag.label || '',
color: tag.color || null,
}));
}; };
const sanitizeCorrespondents = (entries) => { const sanitizeCorrespondents = (entries) => {
if (!Array.isArray(entries)) { if (!Array.isArray(entries)) {
return []; return [];
} }
return entries return entries.filter((entry) => entry && (entry.name || entry.id));
.filter((entry) => entry && (entry.name || entry.id))
.map((entry) => ({
id: entry.id,
name: entry.name || '',
count: entry.count,
}));
}; };
export const describeDocumentSummary = (document, options = {}) => { export const describeDocumentSummary = (document, options = {}) => {
@@ -76,8 +64,8 @@ export const describeDocumentSummary = (document, options = {}) => {
formatDateTime = defaultFormatDateTime, formatDateTime = defaultFormatDateTime,
} = options; } = options;
const title = document.title || ''; const title = document.title;
const originalName = document.original_name || ''; const originalName = document.original_name;
const mimeTypeLabel = document.content_type || 'Unknown'; const mimeTypeLabel = document.content_type || 'Unknown';
const sizeBytes = Number(document.current_version?.size_bytes); const sizeBytes = Number(document.current_version?.size_bytes);
+23 -23
View File
@@ -55,7 +55,7 @@ const api = axios.create({
}); });
const storage = typeof window !== 'undefined' ? window.sessionStorage : undefined; 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; let STORED_TENANT = null;
if (storage) { if (storage) {
try { try {
@@ -192,7 +192,7 @@ const AppStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(appStateReducer, initialAppState); const [state, dispatch] = useReducer(appStateReducer, initialAppState);
useEffect(() => { useEffect(() => {
const token = state.token || ''; const token = state.token ?? '';
if (token) { if (token) {
api.defaults.headers.common.Authorization = `Bearer ${token}`; api.defaults.headers.common.Authorization = `Bearer ${token}`;
storage?.setItem('papercrate_token', token); storage?.setItem('papercrate_token', token);
@@ -1952,7 +1952,7 @@ const AppLayout = () => {
const handleCorrespondentCreate = useCallback( const handleCorrespondentCreate = useCallback(
async ({ name }) => { async ({ name }) => {
const trimmed = (name || '').trim(); const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) { if (!trimmed) {
throw new Error('Correspondent name is required.'); throw new Error('Correspondent name is required.');
} }
@@ -2073,7 +2073,7 @@ const AppLayout = () => {
if (!document?.id) { if (!document?.id) {
throw new Error('Missing document for correspondent assignment.'); throw new Error('Missing document for correspondent assignment.');
} }
const trimmed = (name || '').trim(); const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) { if (!trimmed) {
setStatusMessage('Correspondent name is required.', 'error'); setStatusMessage('Correspondent name is required.', 'error');
return; return;
@@ -2149,7 +2149,7 @@ const AppLayout = () => {
const handleDocumentTypeCreate = useCallback( const handleDocumentTypeCreate = useCallback(
async ({ name }) => { async ({ name }) => {
const trimmed = (name || '').trim(); const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) { if (!trimmed) {
throw new Error('Document type name is required.'); throw new Error('Document type name is required.');
} }
@@ -2184,12 +2184,8 @@ const AppLayout = () => {
} }
const existingType = doc.document_type; const existingType = doc.document_type;
const existingTypeId = const existingTypeId = doc.document_type_id ?? existingType?.id ?? null;
doc.document_type_id ?? (typeof existingType === 'object' ? existingType?.id : null); const existingTypeName = typeof existingType?.name === 'string' ? existingType.name : undefined;
const existingTypeName =
typeof existingType === 'string'
? existingType
: existingType?.name || existingType?.label || '';
const shouldClear = const shouldClear =
existingTypeId === documentTypeId || existingTypeId === documentTypeId ||
@@ -2223,7 +2219,7 @@ const AppLayout = () => {
); );
const handleDocumentTypeAssign = useCallback( const handleDocumentTypeAssign = useCallback(
async ({ documentId, documentTypeId }, { notify = true } = {}) => { async ({ documentId, documentTypeId, documentType }, { notify = true } = {}) => {
if (!documentId) { if (!documentId) {
throw new Error('Missing document identifier.'); throw new Error('Missing document identifier.');
} }
@@ -2240,8 +2236,12 @@ const AppLayout = () => {
const next = { ...doc }; const next = { ...doc };
if (documentTypeId) { if (documentTypeId) {
const entry = documentTypeLookupById.get(documentTypeId); const entry =
next.document_type = entry || { id: documentTypeId, name: entry?.name || '' }; documentTypeLookupById.get(documentTypeId) ||
(documentType?.name ? documentType : null);
next.document_type = entry
? { id: entry.id ?? documentTypeId, name: entry.name }
: { id: documentTypeId, name: '' };
} else { } else {
next.document_type = null; next.document_type = null;
} }
@@ -2282,7 +2282,7 @@ const AppLayout = () => {
if (!document?.id) { if (!document?.id) {
throw new Error('Missing document for document type assignment.'); throw new Error('Missing document for document type assignment.');
} }
const trimmed = (name || '').trim(); const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) { if (!trimmed) {
setStatusMessage('Document type name is required.', 'error'); setStatusMessage('Document type name is required.', 'error');
return; return;
@@ -2303,7 +2303,7 @@ const AppLayout = () => {
} }
const success = await handleDocumentTypeAssign( const success = await handleDocumentTypeAssign(
{ documentId: document.id, documentTypeId: target.id }, { documentId: document.id, documentTypeId: target.id, documentType: target },
{ notify: true }, { notify: true },
); );
@@ -2334,7 +2334,7 @@ const AppLayout = () => {
const handleBulkDocumentTypeSet = useCallback( const handleBulkDocumentTypeSet = useCallback(
async ({ name, input, documentIds }) => { async ({ name, input, documentIds }) => {
const trimmed = (name || '').trim(); const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) { if (!trimmed) {
setStatusMessage('Document type name is required.', 'error'); setStatusMessage('Document type name is required.', 'error');
return; return;
@@ -2658,7 +2658,7 @@ const AppLayout = () => {
const handleBulkCorrespondentAdd = useCallback( const handleBulkCorrespondentAdd = useCallback(
async ({ name, input, documentIds }) => { async ({ name, input, documentIds }) => {
const trimmed = (name || '').trim(); const trimmed = typeof name === 'string' ? name.trim() : '';
if (!trimmed) { if (!trimmed) {
setStatusMessage('Correspondent name is required.', 'error'); setStatusMessage('Correspondent name is required.', 'error');
return; return;
@@ -2887,7 +2887,7 @@ const AppLayout = () => {
const handleBulkTagAddFromDetail = useCallback( const handleBulkTagAddFromDetail = useCallback(
async ({ label, input, documentIds }) => { async ({ label, input, documentIds }) => {
const trimmed = (label || '').trim(); const trimmed = typeof label === 'string' ? label.trim() : '';
if (!trimmed) { if (!trimmed) {
setStatusMessage('Enter a tag label.', 'error'); setStatusMessage('Enter a tag label.', 'error');
return; return;
@@ -2920,7 +2920,7 @@ const AppLayout = () => {
const handleBulkTagRemoveFromDetail = useCallback( const handleBulkTagRemoveFromDetail = useCallback(
async ({ label, input, documentIds }) => { async ({ label, input, documentIds }) => {
const trimmed = (label || '').trim(); const trimmed = typeof label === 'string' ? label.trim() : '';
if (!trimmed) { if (!trimmed) {
setStatusMessage('Enter a tag label to remove.', 'error'); setStatusMessage('Enter a tag label to remove.', 'error');
return; return;
@@ -4190,7 +4190,7 @@ const AppLayout = () => {
setStatusMessage('The root folder cannot be renamed.', 'error'); setStatusMessage('The root folder cannot be renamed.', 'error');
return false; return false;
} }
const trimmed = (nextName || '').trim(); const trimmed = typeof nextName === 'string' ? nextName.trim() : '';
if (!trimmed) { if (!trimmed) {
setStatusMessage('Folder name cannot be empty.', 'error'); setStatusMessage('Folder name cannot be empty.', 'error');
return false; return false;
@@ -6157,7 +6157,7 @@ const LoginRoute = () => {
const handlePasskeyLogin = useCallback( const handlePasskeyLogin = useCallback(
async (rawUsername) => { async (rawUsername) => {
const username = (rawUsername || '').trim(); const username = typeof rawUsername === 'string' ? rawUsername.trim() : '';
if (!username) { if (!username) {
setStatusMessage('Enter your username before using a passkey.', 'error'); setStatusMessage('Enter your username before using a passkey.', 'error');
return; return;
@@ -6238,7 +6238,7 @@ const LoginRoute = () => {
const handleSignup = useCallback( const handleSignup = useCallback(
async (rawUsername) => { async (rawUsername) => {
const username = (rawUsername || '').trim(); const username = typeof rawUsername === 'string' ? rawUsername.trim() : '';
if (!username) { if (!username) {
setStatusMessage('Choose a username to create your account.', 'error'); setStatusMessage('Choose a username to create your account.', 'error');
return; return;
+19 -15
View File
@@ -89,7 +89,7 @@ const FolderNode = ({
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
if (!onRename) return; if (!onRename) return;
const nextName = window.prompt('Rename folder', node.name || ''); const nextName = window.prompt('Rename folder', node.name);
if (!nextName) { if (!nextName) {
return; return;
} }
@@ -174,24 +174,28 @@ const Sidebar = ({
onSelectTenant, onSelectTenant,
onOpenSettings, onOpenSettings,
}) => { }) => {
const sortedCorrespondents = useMemo( const sortedCorrespondents = useMemo(() => {
() => if (!Array.isArray(correspondents)) {
[...correspondents].sort((a, b) => return [];
(a?.name || '').localeCompare(b?.name || '', undefined, { sensitivity: 'base' }), }
), return correspondents
[correspondents], .filter((entry) => entry?.name)
); .slice()
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
}, [correspondents]);
const activeCorrespondentSet = useMemo( const activeCorrespondentSet = useMemo(
() => new Set(activeCorrespondentIds || []), () => new Set(activeCorrespondentIds || []),
[activeCorrespondentIds], [activeCorrespondentIds],
); );
const sortedDocumentTypes = useMemo( const sortedDocumentTypes = useMemo(() => {
() => if (!Array.isArray(documentTypes)) {
[...documentTypes].sort((a, b) => return [];
(a?.name || '').localeCompare(b?.name || '', undefined, { sensitivity: 'base' }), }
), return documentTypes
[documentTypes], .filter((entry) => entry?.name)
); .slice()
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
}, [documentTypes]);
const activeDocumentTypeSet = useMemo( const activeDocumentTypeSet = useMemo(
() => new Set(activeDocumentTypeIds || []), () => new Set(activeDocumentTypeIds || []),
[activeDocumentTypeIds], [activeDocumentTypeIds],
+2 -2
View File
@@ -24,8 +24,8 @@ function TagsPanel({
const startEdit = useCallback((tag) => { const startEdit = useCallback((tag) => {
setEditingId(tag.id); setEditingId(tag.id);
setDraftLabel(tag.label || ''); setDraftLabel(tag.label);
setDraftColor(tag.color || ''); setDraftColor(tag.color ?? '');
}, []); }, []);
const cancelEdit = useCallback(() => { const cancelEdit = useCallback(() => {