This commit is contained in:
2025-11-01 22:49:50 +01:00
parent 326a6042ec
commit 16ac16cd30
6 changed files with 962 additions and 750 deletions
+31 -187
View File
@@ -257,12 +257,6 @@ const AppLayout = () => {
const previewReturnPathRef = useRef(null);
const [tags, setTags] = useState([]);
const [correspondents, setCorrespondents] = useState([]);
const [webdavTokens, setWebdavTokens] = useState([]);
const [webdavTokensLoading, setWebdavTokensLoading] = useState(false);
const [creatingWebdavToken, setCreatingWebdavToken] = useState(false);
const [deletingWebdavTokenId, setDeletingWebdavTokenId] = useState(null);
const [regeneratingWebdavTokenId, setRegeneratingWebdavTokenId] = useState(null);
const [webdavTokenSecret, setWebdavTokenSecret] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [activeTagFilters, setActiveTagFilters] = useState([]);
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]);
@@ -425,11 +419,6 @@ const AppLayout = () => {
setSearchResults(null);
setTags([]);
setCorrespondents([]);
setWebdavTokens([]);
setWebdavTokensLoading(false);
setCreatingWebdavToken(false);
setDeletingWebdavTokenId(null);
setWebdavTokenSecret(null);
setSearchQuery('');
setActiveTagFilters([]);
setActiveCorrespondentFilters([]);
@@ -927,32 +916,10 @@ const AppLayout = () => {
],
);
const handleDocumentRowClick = useCallback(
(documentId, event) => {
const rowKey = resolveDocumentRowKey(documentId);
if (!rowKey) return;
const hadSelection = selectionCount > 0;
handleRowSelection(rowKey, event);
if (!hadSelection) {
detailPanelControlRef.current.open();
}
},
[handleRowSelection, selectionCount],
);
const clearDocumentSelection = useCallback(() => {
clearSelectionInternal();
}, [clearSelectionInternal]);
const handleFolderRowClick = useCallback(
(folderId, event) => {
const rowKey = resolveFolderRowKey(folderId);
if (!rowKey) return;
handleRowSelection(rowKey, event);
},
[handleRowSelection],
);
const prevFocusedDocIdRef = useRef(focusedDocumentId);
useEffect(() => {
const previous = prevFocusedDocIdRef.current;
@@ -968,23 +935,39 @@ const AppLayout = () => {
}, [focusedDocumentId, setFocusedRowKey]);
useEffect(() => {
if (!focusedRowKey) {
if (!navigableRowKeys.length) {
if (focusedRowKey) {
setFocusedRowKey(null);
}
return;
}
if (navigableRowKeys.includes(focusedRowKey)) {
if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) {
return;
}
const docKey = focusedDocumentId ? resolveDocumentRowKey(focusedDocumentId) : null;
if (docKey && navigableRowKeys.includes(docKey)) {
setFocusedRowKey(docKey);
return;
}
if (navigableRowKeys.length) {
setFocusedRowKey(navigableRowKeys[0]);
} else {
const selectedKey = selectedRowKeys.find((key) => navigableRowKeys.includes(key));
if (selectedKey) {
setFocusedRowKey(selectedKey);
return;
}
if (focusedRowKey) {
setFocusedRowKey(null);
}
}, [focusedRowKey, navigableRowKeys, focusedDocumentId, setFocusedRowKey]);
}, [
focusedRowKey,
focusedDocumentId,
navigableRowKeys,
selectedRowKeys,
setFocusedRowKey,
]);
const ensureFolderData = useCallback(
@@ -1303,125 +1286,6 @@ const AppLayout = () => {
}
}, [notifyApiError]);
const refreshWebdavTokens = useCallback(async () => {
if (!token) {
return;
}
setWebdavTokensLoading(true);
try {
const { data } = await api.get('/profile/webdav-tokens');
setWebdavTokens(Array.isArray(data) ? data : []);
} catch (error) {
notifyApiError(error, 'Failed to load WebDAV tokens.');
} finally {
setWebdavTokensLoading(false);
}
}, [notifyApiError, token]);
const createWebdavToken = useCallback(
async ({ label, expires_at } = {}) => {
if (creatingWebdavToken) {
return false;
}
setCreatingWebdavToken(true);
try {
const payload = {};
if (label) {
payload.label = label;
}
if (expires_at) {
payload.expires_at = expires_at;
}
const { data } = await api.post('/profile/webdav-tokens', payload);
if (data?.token_info) {
setWebdavTokens((previous) => {
const filtered = previous.filter((entry) => entry.id !== data.token_info.id);
return [data.token_info, ...filtered];
});
} else {
await refreshWebdavTokens();
}
if (data?.token) {
setWebdavTokenSecret(data.token);
}
setStatusMessage('WebDAV token created.', 'success');
return data;
} catch (error) {
notifyApiError(error, 'Failed to create WebDAV token.');
return false;
} finally {
setCreatingWebdavToken(false);
}
},
[creatingWebdavToken, notifyApiError, refreshWebdavTokens, setStatusMessage],
);
const deleteWebdavToken = useCallback(
async (tokenId) => {
if (!tokenId) {
return false;
}
setDeletingWebdavTokenId(tokenId);
try {
await api.delete(`/profile/webdav-tokens/${tokenId}`);
await refreshWebdavTokens();
setStatusMessage('WebDAV token revoked.', 'success');
return true;
} catch (error) {
notifyApiError(error, 'Failed to revoke WebDAV token.');
return false;
} finally {
setDeletingWebdavTokenId(null);
}
},
[refreshWebdavTokens, notifyApiError, setStatusMessage],
);
const regenerateWebdavToken = useCallback(
async (tokenId) => {
if (!tokenId) {
return false;
}
setRegeneratingWebdavTokenId(tokenId);
try {
const { data } = await api.post(`/profile/webdav-tokens/${tokenId}/regenerate`);
if (data?.token_info) {
setWebdavTokens((previous) => {
let found = false;
const next = previous.map((entry) => {
if (entry.id === data.token_info.id) {
found = true;
return data.token_info;
}
return entry;
});
if (!found) {
return [data.token_info, ...previous];
}
return next;
});
} else {
await refreshWebdavTokens();
}
if (data?.token) {
setWebdavTokenSecret(data.token);
}
setStatusMessage('WebDAV token regenerated.', 'success');
return true;
} catch (error) {
notifyApiError(error, 'Failed to regenerate WebDAV token.');
return false;
} finally {
setRegeneratingWebdavTokenId(null);
}
},
[notifyApiError, refreshWebdavTokens, setStatusMessage],
);
const dismissCreatedWebdavToken = useCallback(() => {
setWebdavTokenSecret(null);
}, []);
const {
passkeys,
passkeysSupported,
@@ -3208,9 +3072,13 @@ const AppLayout = () => {
return;
}
if (selectedRowKeys.length === 0) {
return;
}
setFocusedRowKey(resolvedKey);
if (!selectedRowKeys.includes(resolvedKey)) {
if (!selectedRowKeys.includes(resolvedKey) && selectedRowKeys.length > 0) {
applySelection([resolvedKey], { anchor: resolvedKey, interactedKeys: [resolvedKey] });
}
}, [
@@ -4699,15 +4567,12 @@ const AppLayout = () => {
onFolderDragEnd: handleFolderDragEnd,
draggedFolderId,
onFolderDelete: handleFolderDelete,
onFolderRowClick: handleFolderRowClick,
onFolderRename: handleFolderRename,
onDocumentRowClick: handleDocumentRowClick,
onDocumentOpen: openDocumentPreview,
onDocumentDelete: handleDocumentDelete,
onDocumentRename: handleDocumentTitleUpdate,
selectedDocumentIds,
selectedFolderIds,
focusedDocumentId,
focusedRowKey,
draggingDocumentIds: draggedDocumentIds,
onDocumentDragStart: handleDocumentDragStart,
@@ -4730,6 +4595,8 @@ const AppLayout = () => {
viewMode: documentsViewMode,
onViewModeChange: handleDocumentsViewModeChange,
onClearSelection: clearDocumentSelection,
onRowSelection: handleRowSelection,
onOpenDetailPanel: openDetailPanel,
}),
[
activeCorrespondentFilters,
@@ -4741,7 +4608,6 @@ const AppLayout = () => {
documentsViewMode,
draggedDocumentIds,
draggedFolderId,
focusedDocumentId,
focusedRowKey,
folderClickHandlers,
handleDocumentDelete,
@@ -4749,7 +4615,6 @@ const AppLayout = () => {
handleDocumentDragStart,
handleDocumentListFocus,
handleDocumentListKeyDown,
handleDocumentRowClick,
handleDocumentTagDrop,
handleDocumentTitleUpdate,
handleDocumentsViewModeChange,
@@ -4757,8 +4622,9 @@ const AppLayout = () => {
handleFolderDragEnd,
handleFolderDragStart,
handleFolderRename,
handleFolderRowClick,
handleRowSelection,
isFilterActive,
openDetailPanel,
openDocumentPreview,
refreshCurrentFolder,
searchLoading,
@@ -4975,17 +4841,6 @@ const AppLayout = () => {
handleDocumentCorrespondentAttach,
handleCorrespondentRemove,
handleCorrespondentAdd,
webdavTokens,
webdavTokensLoading,
creatingWebdavToken,
deletingWebdavTokenId,
regeneratingWebdavTokenId,
refreshWebdavTokens,
createWebdavToken,
deleteWebdavToken,
regenerateWebdavToken,
webdavTokenSecret,
dismissCreatedWebdavToken,
passkeys,
passkeysSupported,
passkeysLoading,
@@ -5036,17 +4891,6 @@ const AppLayout = () => {
handleDocumentCorrespondentAttach,
handleCorrespondentRemove,
handleCorrespondentAdd,
webdavTokens,
webdavTokensLoading,
creatingWebdavToken,
deletingWebdavTokenId,
regeneratingWebdavTokenId,
refreshWebdavTokens,
createWebdavToken,
deleteWebdavToken,
regenerateWebdavToken,
webdavTokenSecret,
dismissCreatedWebdavToken,
passkeys,
passkeysSupported,
passkeysLoading,
+40 -28
View File
@@ -2,21 +2,15 @@ import React, { useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import SettingsModal from '../settings/SettingsModal';
import { useAppShell } from '../appShellContext';
import useApiTokens from '../settings/useApiTokens';
import { api } from './appState';
const SettingsRoute = () => {
const navigate = useNavigate();
const {
webdavTokens,
webdavTokensLoading,
creatingWebdavToken,
deletingWebdavTokenId,
regeneratingWebdavTokenId,
refreshWebdavTokens,
createWebdavToken,
deleteWebdavToken,
regenerateWebdavToken,
webdavTokenSecret,
dismissCreatedWebdavToken,
token,
notifyApiError,
setStatusMessage,
passkeys,
passkeysSupported,
passkeysLoading,
@@ -27,15 +21,31 @@ const SettingsRoute = () => {
revokePasskey,
} = useAppShell();
const {
tokens,
loading,
creating,
deletingId,
regeneratingId,
updatingId,
createdSecret,
refresh,
create,
revoke,
regenerate,
updateCapabilities,
dismissSecret,
} = useApiTokens({ api, token, notifyApiError, setStatusMessage });
useEffect(() => {
refreshWebdavTokens();
refresh();
refreshPasskeys();
}, [refreshWebdavTokens, refreshPasskeys]);
}, [refresh, refreshPasskeys]);
const handleClose = useCallback(() => {
dismissCreatedWebdavToken();
dismissSecret();
navigate(-1);
}, [dismissCreatedWebdavToken, navigate]);
}, [dismissSecret, navigate]);
useEffect(() => {
const handleKeyDown = (event) => {
@@ -49,24 +59,26 @@ const SettingsRoute = () => {
}, [handleClose]);
useEffect(() => () => {
dismissCreatedWebdavToken();
}, [dismissCreatedWebdavToken]);
dismissSecret();
}, [dismissSecret]);
return (
<SettingsModal
open
onClose={handleClose}
tokens={webdavTokens}
loading={webdavTokensLoading}
creating={creatingWebdavToken}
deletingId={deletingWebdavTokenId}
regeneratingId={regeneratingWebdavTokenId}
onRefresh={refreshWebdavTokens}
onCreate={createWebdavToken}
onDelete={deleteWebdavToken}
onRegenerate={regenerateWebdavToken}
createdToken={webdavTokenSecret}
onDismissCreatedToken={dismissCreatedWebdavToken}
tokens={tokens}
loading={loading}
creating={creating}
deletingId={deletingId}
regeneratingId={regeneratingId}
updatingId={updatingId}
onRefresh={refresh}
onCreate={create}
onDelete={revoke}
onRegenerate={regenerate}
onUpdateCapabilities={updateCapabilities}
createdToken={createdSecret}
onDismissCreatedToken={dismissSecret}
passkeys={passkeys}
passkeysSupported={passkeysSupported}
passkeysLoading={passkeysLoading}
+60 -26
View File
@@ -12,24 +12,32 @@ export const useDetailPanel = ({
const [detailPanelDocIds, setDetailPanelDocIds] = useState([]);
const [detailPanelDocs, setDetailPanelDocs] = useState([]);
const lastScrolledDetailDocRef = useRef(null);
const latestOrderedDocsRef = useRef([]);
useEffect(() => {
if (!orderedSelectedDocuments.length) {
setDetailPanelDocIds((prev) => (prev.length ? [] : prev));
return;
}
const nextIds = orderedSelectedDocuments
.map((doc) => doc?.id)
.filter((id) => typeof id === 'string' || typeof id === 'number');
setDetailPanelDocIds((prev) => {
if (prev.length === nextIds.length && prev.every((id, index) => id === nextIds[index])) {
return prev;
latestOrderedDocsRef.current = orderedSelectedDocuments;
if (detailPanelOpen && orderedSelectedDocuments.length) {
const snapshotIds = orderedSelectedDocuments
.map((doc) => doc?.id)
.filter((id) => typeof id === 'string' || typeof id === 'number');
if (snapshotIds.length) {
setDetailPanelDocIds(snapshotIds);
}
return nextIds;
});
}, [orderedSelectedDocuments]);
}
}, [orderedSelectedDocuments, detailPanelOpen]);
const resolveDocsForIds = useCallback(
(ids, fallbackDocs = []) => {
if (!ids?.length) {
return [];
}
const fallbackMap = new Map((fallbackDocs || []).map((doc) => [doc?.id, doc]));
return ids
.map((id) => documentLookup.get(id) || fallbackMap.get(id) || null)
.filter(Boolean);
},
[documentLookup],
);
useEffect(() => {
if (!detailPanelDocIds.length) {
@@ -38,17 +46,13 @@ export const useDetailPanel = ({
}
setDetailPanelDocs((prevDocs) => {
const prevMap = new Map((prevDocs || []).map((doc) => [doc.id, doc]));
const next = detailPanelDocIds
.map((id) => documentLookup.get(id) || prevMap.get(id) || null)
.filter(Boolean);
if (next.length === prevDocs.length && next.every((doc, index) => doc === prevDocs[index])) {
const resolved = resolveDocsForIds(detailPanelDocIds, prevDocs);
if (resolved.length === prevDocs.length && resolved.every((doc, index) => doc === prevDocs[index])) {
return prevDocs;
}
return next;
return resolved;
});
}, [detailPanelDocIds, documentLookup]);
}, [detailPanelDocIds, resolveDocsForIds]);
useEffect(() => {
if (!detailPanelOpen) {
@@ -108,9 +112,39 @@ export const useDetailPanel = ({
const detailPanelSelectedDocuments = detailPanelDocs;
const openDetailPanel = useCallback(() => {
setDetailPanelOpen(true);
}, []);
const openDetailPanel = useCallback(
({ documentIds: explicitIds, documents: explicitDocs } = {}) => {
let sourceDocs = Array.isArray(explicitDocs) ? explicitDocs : null;
let snapshotIds = Array.isArray(explicitIds)
? explicitIds.filter((id) => typeof id === 'string' || typeof id === 'number')
: null;
if (!snapshotIds?.length) {
if (!sourceDocs || !sourceDocs.length) {
sourceDocs = latestOrderedDocsRef.current;
}
snapshotIds = Array.isArray(sourceDocs)
? sourceDocs
.map((doc) => doc?.id)
.filter((id) => typeof id === 'string' || typeof id === 'number')
: [];
}
const uniqueIds = [];
snapshotIds.forEach((id) => {
if (!uniqueIds.includes(id)) {
uniqueIds.push(id);
}
});
const resolvedDocs = resolveDocsForIds(uniqueIds, sourceDocs || latestOrderedDocsRef.current);
setDetailPanelDocIds(uniqueIds);
setDetailPanelDocs(resolvedDocs);
setDetailPanelOpen(true);
},
[resolveDocsForIds],
);
const closeDetailPanel = useCallback(() => {
setDetailPanelOpen(false);