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) => {
setEditingId(correspondent.id);
setDraftName(correspondent.name || '');
setDraftName(correspondent.name);
}, []);
const cancelEdit = useCallback(() => {
+47 -46
View File
@@ -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 = ({
>
<img
src={entry.url}
alt={entry.alt || ''}
alt={entry.alt}
className="preview-stack__image"
onClick={(event) => {
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(() => {
@@ -17,7 +17,7 @@ function DocumentTypesPanel({
const startEdit = useCallback((entry) => {
setEditingId(entry.id);
setDraftName(entry.name || '');
setDraftName(entry.name);
}, []);
const cancelEdit = useCallback(() => {
+6 -14
View File
@@ -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 ? (
<img
src={url}
alt={alt || ''}
alt={alt}
className="document-thumbnail"
loading="lazy"
decoding="async"
@@ -883,7 +875,7 @@ const DocumentsTable = ({
aria-label={`Rename folder ${folder.name}`}
onClick={(event) => {
event.stopPropagation();
const nextName = window.prompt('Rename folder', folder.name || '');
const nextName = window.prompt('Rename folder', folder.name);
if (!nextName) {
return;
}
+4 -16
View File
@@ -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);
+23 -23
View File
@@ -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;
+19 -15
View File
@@ -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],
+2 -2
View File
@@ -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(() => {