sorting
This commit is contained in:
+196
-21
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user