This commit is contained in:
2025-11-06 01:46:33 +01:00
parent a16f1501d3
commit 82ae18b4b9
5 changed files with 489 additions and 28 deletions
+196 -21
View File
@@ -27,6 +27,10 @@ import { isTagTransferEvent } from '../documents/tagTransfer';
const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early
const DEFAULT_FOLDER_NAME = 'Documents';
const DEFAULT_SORT_FIELD = 'title';
const DEFAULT_SORT_DIRECTION = 'asc';
const SORT_FIELD_VALUES = ['title', 'issued_at', 'created_at', 'updated_at'];
const TAG_FILTER_UNTAGGED = '__UNTAGGED__';
const ROW_KEY_SEPARATOR = ':';
const DOCUMENT_ROW_PREFIX = 'document';
@@ -204,6 +208,46 @@ const AppLayout = () => {
return 'list';
}
});
const [documentsSortField, setDocumentsSortField] = useState(() => {
if (typeof window === 'undefined') {
return DEFAULT_SORT_FIELD;
}
try {
const stored = window.sessionStorage.getItem('papercrate_sort_field');
return SORT_FIELD_VALUES.includes(stored) ? stored : DEFAULT_SORT_FIELD;
} catch (error) {
console.warn('[sort] failed to read stored sort field', error);
return DEFAULT_SORT_FIELD;
}
});
const [documentsSortDirection, setDocumentsSortDirection] = useState(() => {
if (typeof window === 'undefined') {
return DEFAULT_SORT_DIRECTION;
}
try {
const stored = window.sessionStorage.getItem('papercrate_sort_direction');
return stored === 'desc' || stored === 'asc' ? stored : DEFAULT_SORT_DIRECTION;
} catch (error) {
console.warn('[sort] failed to read stored sort direction', error);
return DEFAULT_SORT_DIRECTION;
}
});
const documentsSortFieldRef = useRef(DEFAULT_SORT_FIELD);
const documentsSortDirectionRef = useRef(DEFAULT_SORT_DIRECTION);
const [searchIncludeDescendants, setSearchIncludeDescendants] = useState(() => {
if (typeof window === 'undefined') {
return true;
}
try {
const stored = window.sessionStorage.getItem('papercrate_include_descendants');
if (stored === 'true') return true;
if (stored === 'false') return false;
} catch (error) {
console.warn('[search] failed to read include_descendants preference', error);
}
return true;
});
const sortRefreshReadyRef = useRef(false);
const [deskHelpOpen, setDeskHelpOpen] = useState(false);
const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode);
@@ -218,6 +262,42 @@ const AppLayout = () => {
setDeskHelpOpen(false);
}
}, [documentsViewMode, deskHelpOpen]);
useEffect(() => {
documentsSortFieldRef.current = documentsSortField;
if (typeof window !== 'undefined') {
try {
window.sessionStorage.setItem('papercrate_sort_field', documentsSortField);
} catch (error) {
console.warn('[sort] failed to persist sort field', error);
}
}
}, [documentsSortField, documentsSortFieldRef]);
useEffect(() => {
documentsSortDirectionRef.current = documentsSortDirection;
if (typeof window !== 'undefined') {
try {
window.sessionStorage.setItem('papercrate_sort_direction', documentsSortDirection);
} catch (error) {
console.warn('[sort] failed to persist sort direction', error);
}
}
}, [documentsSortDirection, documentsSortDirectionRef]);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
window.sessionStorage.setItem(
'papercrate_include_descendants',
searchIncludeDescendants ? 'true' : 'false',
);
} catch (error) {
console.warn('[search] failed to persist include_descendants preference', error);
}
}, [searchIncludeDescendants]);
const tokenRef = useRef(token);
const refreshPromiseRef = useRef(null);
const breadcrumbFetchRef = useRef(new Set());
@@ -281,11 +361,16 @@ const AppLayout = () => {
);
const toggleTagFilter = useCallback((tagId) => {
if (!tagId) return;
setActiveTagFilters((previous) =>
previous.includes(tagId)
? previous.filter((id) => id !== tagId)
: previous.concat([tagId]),
);
setActiveTagFilters((previous) => {
if (tagId === TAG_FILTER_UNTAGGED) {
return previous.includes(TAG_FILTER_UNTAGGED) ? [] : [TAG_FILTER_UNTAGGED];
}
const sanitized = previous.filter((id) => id !== TAG_FILTER_UNTAGGED);
if (sanitized.includes(tagId)) {
return sanitized.filter((id) => id !== tagId);
}
return sanitized.concat([tagId]);
});
}, []);
const toggleCorrespondentFilter = useCallback((correspondentId) => {
@@ -297,6 +382,19 @@ const AppLayout = () => {
});
}, []);
const handleDocumentsSortFieldChange = useCallback((field) => {
const nextField = SORT_FIELD_VALUES.includes(field) ? field : DEFAULT_SORT_FIELD;
setDocumentsSortField((previous) => (previous === nextField ? previous : nextField));
}, []);
const handleDocumentsSortDirectionToggle = useCallback(() => {
setDocumentsSortDirection((previous) => (previous === 'asc' ? 'desc' : 'asc'));
}, []);
const toggleSearchIncludeDescendants = useCallback(() => {
setSearchIncludeDescendants((previous) => !previous);
}, []);
const initialRefreshAttemptedRef = useRef(Boolean(token));
useEffect(() => {
@@ -312,6 +410,7 @@ const AppLayout = () => {
setActiveTagFilters([]);
setActiveCorrespondentFilters([]);
setSearchLoading(false);
setSearchIncludeDescendants(true);
}, []);
const handleSearchChange = useCallback((value) => {
@@ -982,13 +1081,32 @@ const AppLayout = () => {
const ensureFolderData = useCallback(
async (
folderId,
{ force = false, includeDocuments = true, prefetchDepth = 0 } = {},
{
force = false,
includeDocuments = true,
prefetchDepth = 0,
sortField: overrideSortField,
sortDirection: overrideSortDirection,
} = {},
) => {
const requestTenantId = tenantIdRef.current;
const cached = folderContents.get(folderId);
const cachedSortField = cached?.__sortField ?? DEFAULT_SORT_FIELD;
const cachedSortDirection = cached?.__sortDirection ?? DEFAULT_SORT_DIRECTION;
const sortField = includeDocuments
? overrideSortField || documentsSortFieldRef.current || DEFAULT_SORT_FIELD
: null;
const sortDirection = includeDocuments
? overrideSortDirection || documentsSortDirectionRef.current || DEFAULT_SORT_DIRECTION
: null;
const cachedSortMatches =
!includeDocuments
|| !cached
|| (cachedSortField === sortField && cachedSortDirection === sortDirection);
if (!force && cached) {
const includesDocuments = Boolean(cached.__includesDocuments);
if (!includeDocuments || includesDocuments) {
if (!includeDocuments || (includesDocuments && cachedSortMatches)) {
if (prefetchDepth > 0) {
const subfolders = Array.isArray(cached.subfolders) ? cached.subfolders : [];
await Promise.allSettled(
@@ -1006,18 +1124,28 @@ const AppLayout = () => {
}
const path = folderId === 'root' ? 'root' : folderId;
const params = includeDocuments
? undefined
: { include_documents: false };
const { data } = await api.get(`/folders/${path}/contents`, {
params,
});
const params = {};
if (!includeDocuments) {
params.include_documents = false;
} else {
params.sort = sortField;
params.dir = sortDirection;
}
const requestConfig = Object.keys(params).length ? { params } : {};
const { data } = await api.get(`/folders/${path}/contents`, requestConfig);
const hydrated = assetManager.hydrateFolderContents(data);
const childFolders = Array.isArray(data.subfolders) ? data.subfolders : [];
const childIds = childFolders.map((child) => child.id);
const enriched = {
...hydrated,
__includesDocuments: includeDocuments,
__sortField: includeDocuments ? sortField : cachedSortField,
__sortDirection: includeDocuments ? sortDirection : cachedSortDirection,
};
if (tenantIdRef.current !== requestTenantId) {
return { ...hydrated, __includesDocuments: includeDocuments };
return enriched;
}
setFolderNodes((prev) => {
@@ -1089,11 +1217,6 @@ const AppLayout = () => {
);
}
const enriched = {
...hydrated,
__includesDocuments: includeDocuments,
};
if (includeDocuments) {
setFolderContents((prev) => {
if (tenantIdRef.current !== requestTenantId) {
@@ -1118,6 +1241,8 @@ const AppLayout = () => {
? existingEntry.documents
: hydrated.documents,
__includesDocuments: existingEntry.__includesDocuments || false,
__sortField: existingEntry.__sortField ?? enriched.__sortField,
__sortDirection: existingEntry.__sortDirection ?? enriched.__sortDirection,
});
} else {
next.set(folderId, enriched);
@@ -1128,7 +1253,7 @@ const AppLayout = () => {
return enriched;
},
[assetManager, folderContents],
[assetManager, documentsSortDirectionRef, documentsSortFieldRef, folderContents],
);
const isInvalidFolderDrop = useCallback(
@@ -1461,6 +1586,22 @@ const AppLayout = () => {
}
}, [selectedFolder, ensureFolderData, applySelectedFolder, notifyApiError]);
useEffect(() => {
if (!sortRefreshReadyRef.current) {
sortRefreshReadyRef.current = true;
return;
}
if (!isFilterActive && token) {
refreshCurrentFolder();
}
}, [
documentsSortField,
documentsSortDirection,
isFilterActive,
refreshCurrentFolder,
token,
]);
const handleDocumentCorrespondentAttach = useCallback(
async ({ documentId, correspondentId }, { notify = true, refresh = true } = {}) => {
if (!documentId || !correspondentId) {
@@ -3719,7 +3860,16 @@ const AppLayout = () => {
params.query = trimmedQuery;
}
if (activeTagFilters.length) {
params.tags = activeTagFilters.join(',');
const onlyUntagged = activeTagFilters.length === 1
&& activeTagFilters[0] === TAG_FILTER_UNTAGGED;
if (onlyUntagged) {
params.tags = 'none';
} else {
const tagIds = activeTagFilters.filter((id) => id !== TAG_FILTER_UNTAGGED);
if (tagIds.length) {
params.tags = tagIds.join(',');
}
}
}
if (activeCorrespondentFilters.length) {
params.correspondents = activeCorrespondentFilters.join(',');
@@ -3728,6 +3878,15 @@ const AppLayout = () => {
if (folderIdentifier) {
params.folder_id = folderIdentifier;
}
if (!searchIncludeDescendants) {
params.include_descendants = false;
}
if (documentsSortField) {
params.sort = documentsSortField;
}
if (documentsSortDirection) {
params.dir = documentsSortDirection;
}
const { data } = await api.get('/documents', { params });
if (cancelled) return;
@@ -3803,6 +3962,9 @@ const AppLayout = () => {
searchQuery,
activeTagFilters,
activeCorrespondentFilters,
searchIncludeDescendants,
documentsSortField,
documentsSortDirection,
selectedFolder,
notifyApiError,
assetManager,
@@ -4561,6 +4723,12 @@ const AppLayout = () => {
onCorrespondentClick: toggleCorrespondentFilter,
onDocumentTagDrop: handleDocumentTagDrop,
viewMode: documentsViewMode,
sortField: documentsSortField,
sortDirection: documentsSortDirection,
onSortFieldChange: handleDocumentsSortFieldChange,
onSortDirectionToggle: handleDocumentsSortDirectionToggle,
searchIncludeDescendants,
onToggleSearchIncludeDescendants: toggleSearchIncludeDescendants,
onViewModeChange: handleDocumentsViewModeChange,
onClearSelection: clearDocumentSelection,
onDeleteSelection: handleDeleteSelection,
@@ -4586,6 +4754,8 @@ const AppLayout = () => {
currentSubfolders,
documents,
documentsViewMode,
documentsSortField,
documentsSortDirection,
draggedDocumentIds,
draggedFolderId,
focusedRowKey,
@@ -4595,6 +4765,8 @@ const AppLayout = () => {
handleDocumentTagDrop,
handleDocumentTitleUpdate,
handleDocumentsViewModeChange,
handleDocumentsSortFieldChange,
handleDocumentsSortDirectionToggle,
handleFolderDragEnd,
handleFolderDragStart,
handleFolderRename,
@@ -4607,6 +4779,7 @@ const AppLayout = () => {
refreshCurrentFolder,
searchLoading,
searchResults,
searchIncludeDescendants,
selectFolder,
selectedDocumentIds,
selectedEntries,
@@ -4627,6 +4800,7 @@ const AppLayout = () => {
handleBulkSelectionReanalyze,
folderOptions,
moveDocumentsToFolder,
toggleSearchIncludeDescendants,
],
);
@@ -4647,6 +4821,7 @@ const AppLayout = () => {
onCreateFolder: handlePromptCreateFolder,
creatingFolder,
tags,
untaggedFilterId: TAG_FILTER_UNTAGGED,
activeTagIds: activeTagFilters,
onToggleTagFilter: toggleTagFilter,
onCreateTag: (label) => handleTagCreate({ label }),