tags, folder rename, other stuff

This commit is contained in:
2025-10-15 21:32:17 +02:00
parent b5119a8e6f
commit 068f96880b
13 changed files with 525 additions and 130 deletions
+92 -21
View File
@@ -28,7 +28,7 @@ import TagsPanel from './tags/TagsPanel';
import CorrespondentsPanel from './correspondents/CorrespondentsPanel';
import { CORRESPONDENT_ROLES } from './constants/correspondents';
import { DownloadIcon } from './ui/icons';
import { generateRandomTagColor } from './utils/colors';
import TagManager from './tag_manager';
import { formatFileSize } from './utils/format';
import Sidebar from './sidebar/Sidebar';
import DocumentsTable, { FilterBar } from './documents/DocumentsTable';
@@ -385,6 +385,17 @@ const AppLayout = () => {
}
const assetManager = assetManagerRef.current;
const tagManagerRef = useRef(null);
if (!tagManagerRef.current) {
tagManagerRef.current = new TagManager();
}
const tagManager = tagManagerRef.current;
const buildTagPayload = useCallback(
({ label, color } = {}) => tagManager.buildPayload({ label, color }),
[tagManager],
);
const getDocumentAsset = useCallback((doc, type) => {
if (!doc || !type) return null;
return getAssetFromVersion(doc.current_version || null, type);
@@ -1238,12 +1249,8 @@ const AppLayout = () => {
);
const handleTagCreate = useCallback(
async ({ label, color }) => {
const trimmed = (label || '').trim();
if (!trimmed) {
throw new Error('Tag label is required.');
}
const payload = { label: trimmed, color: color || generateRandomTagColor() };
async ({ label, color } = {}) => {
const payload = tagManager.buildPayload({ label, color });
try {
await api.post('/tags', payload);
await refreshTags();
@@ -1254,7 +1261,7 @@ const AppLayout = () => {
throw new Error(message);
}
},
[api, refreshTags, notifyApiError],
[api, refreshTags, notifyApiError, setStatusMessage, tagManager],
);
const handleCorrespondentUpdate = useCallback(
@@ -1783,7 +1790,8 @@ const AppLayout = () => {
for (const label of normalized) {
let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null;
if (!tag) {
const { data } = await api.post('/tags', { label, color: null });
const payload = tagManager.buildPayload({ label });
const { data } = await api.post('/tags', payload);
tag = data;
await refreshTags();
}
@@ -1829,6 +1837,7 @@ const AppLayout = () => {
refreshCurrentFolder,
notifyApiError,
setLoading,
tagManager,
],
);
@@ -2938,13 +2947,13 @@ const AppLayout = () => {
const handleTagAdd = useCallback(
async (document, label, input) => {
let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null;
const normalizedLabel = tagManager.normalizeLabel(label);
let tag =
tags.find((item) => item.label.toLowerCase() === normalizedLabel.toLowerCase()) || null;
try {
if (!tag) {
const { data } = await api.post('/tags', {
label,
color: generateRandomTagColor(),
});
const payload = tagManager.buildPayload({ label: normalizedLabel });
const { data } = await api.post('/tags', payload);
tag = data;
await refreshTags();
}
@@ -2956,13 +2965,7 @@ const AppLayout = () => {
notifyApiError(error, 'Failed to assign tag.');
}
},
[
tags,
refreshTags,
refreshCurrentFolder,
notifyApiError,
setStatusMessage,
],
[tags, refreshTags, refreshCurrentFolder, notifyApiError, setStatusMessage, tagManager],
);
const handleDocumentTagAttach = useCallback(
@@ -3054,6 +3057,63 @@ const AppLayout = () => {
],
);
const handleFolderRename = useCallback(
async (folderId, nextName) => {
if (!token) {
setStatusMessage('Log in to rename folders.', 'error');
return false;
}
if (!folderId || folderId === 'root') {
setStatusMessage('The root folder cannot be renamed.', 'error');
return false;
}
const trimmed = (nextName || '').trim();
if (!trimmed) {
setStatusMessage('Folder name cannot be empty.', 'error');
return false;
}
setLoading(true);
try {
await api.patch(`/folders/${folderId}`, { name: trimmed });
setFolderNodes((prev) => {
const next = new Map(prev);
const node = next.get(folderId);
if (node) {
next.set(folderId, { ...node, name: trimmed });
}
return next;
});
setFolderContents((prev) => {
if (!prev.has(folderId)) {
return prev;
}
const next = new Map(prev);
const existing = next.get(folderId) || {};
const folderInfo = existing.folder
? { ...existing.folder, name: trimmed }
: { id: folderId, name: trimmed };
next.set(folderId, { ...existing, folder: folderInfo });
return next;
});
setCurrentFolder((prev) => (prev?.id === folderId ? { ...prev, name: trimmed } : prev));
setStatusMessage('Folder renamed.', 'success');
return true;
} catch (error) {
const message = error.response?.data?.error || 'Failed to rename folder.';
notifyApiError(error, message);
return false;
} finally {
setLoading(false);
}
},
[api, token, notifyApiError, setStatusMessage],
);
const handleFolderCreate = useCallback(
async (name, onSuccess) => {
if (!token) {
@@ -3769,6 +3829,7 @@ const AppLayout = () => {
onDragOver: folderClickHandlers.onDragOver,
onDragLeave: folderClickHandlers.onDragLeave,
onDeleteFolder: handleFolderDelete,
onRenameFolder: handleFolderRename,
selectedFolder,
onFolderDragStart: handleFolderDragStart,
onFolderDragEnd: handleFolderDragEnd,
@@ -3810,9 +3871,11 @@ const AppLayout = () => {
onFolderDragEnd: handleFolderDragEnd,
draggedFolderId,
onFolderDelete: handleFolderDelete,
onFolderRename: handleFolderRename,
onDocumentRowClick: handleDocumentRowClick,
onDocumentOpen: openDocumentPreview,
onDocumentDelete: handleDocumentDelete,
onDocumentRename: handleDocumentTitleUpdate,
selectedDocumentIds,
focusedDocumentId,
focusedRowKey,
@@ -3830,6 +3893,12 @@ const AppLayout = () => {
doc?.current_version?.download_path
? resolveApiPath(doc.current_version.download_path)
: null,
onTagClick: (tagId) => {
if (!tagId) return;
setActiveTagFilters((previous) =>
previous.includes(tagId) ? previous : previous.concat([tagId]),
);
},
};
const detailPanelProps = {
@@ -3875,6 +3944,7 @@ const AppLayout = () => {
onRemoveTagFromDocument: handleTagRemove,
ensureAssetUrl,
getDocumentAsset,
prepareTagPayload: buildTagPayload,
}),
[
documents,
@@ -3891,6 +3961,7 @@ const AppLayout = () => {
handleTagRemove,
ensureAssetUrl,
getDocumentAsset,
buildTagPayload,
],
);