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 }),
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
RefreshIcon,
|
||||
MinusVerticalIcon,
|
||||
InfoIcon,
|
||||
FoldersIcon,
|
||||
FoldersOffIcon,
|
||||
SortAscendingLettersIcon,
|
||||
SortDescendingLettersIcon,
|
||||
} from '../ui/icons';
|
||||
import QuickAddMenu from '../ui/QuickAddMenu';
|
||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
||||
import createWorkspaceSurfaceConfig from './workspaceHeader';
|
||||
import DetailPanel from '../detail/DetailPanel';
|
||||
@@ -25,6 +30,19 @@ const EntryType = {
|
||||
document: 'document',
|
||||
};
|
||||
|
||||
const SORT_OPTIONS = [
|
||||
{ value: 'title', label: 'Title' },
|
||||
{ value: 'issued_at', label: 'Issued date' },
|
||||
{ value: 'created_at', label: 'Added' },
|
||||
{ value: 'updated_at', label: 'Updated date' },
|
||||
];
|
||||
|
||||
const SORT_LABEL_LOOKUP = SORT_OPTIONS.reduce((accumulator, option) => {
|
||||
const next = accumulator;
|
||||
next[option.value] = option.label;
|
||||
return next;
|
||||
}, {});
|
||||
|
||||
const DocumentsPanel = ({
|
||||
currentFolderName,
|
||||
breadcrumbs,
|
||||
@@ -545,7 +563,6 @@ const DocumentsPanel = ({
|
||||
const showDefaultEmptyState = !showingSearchResults && !isFilterActive && entries.length === 0;
|
||||
const showListSearchEmptyState = showingSearchResults && !hasDocumentEntries && !isGridView && !isSearchLoading;
|
||||
const showGridSearchEmptyState = isGridView && showingSearchResults && !hasDocumentEntries && !isSearchLoading;
|
||||
const showSearchHint = showingSearchResults && rows.length > 0;
|
||||
const breadcrumbEntries = useMemo(() => (Array.isArray(breadcrumbs) ? breadcrumbs.filter(Boolean) : []), [breadcrumbs]);
|
||||
const trailEntries = useMemo(() => {
|
||||
if (!breadcrumbEntries.length) {
|
||||
@@ -724,11 +741,6 @@ const DocumentsPanel = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showSearchHint && (
|
||||
<div className="search-hint">
|
||||
Showing {rows.length} document{rows.length === 1 ? '' : 's'} in {currentFolderName} and subfolders.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -743,16 +755,115 @@ const DocumentsPanel = ({
|
||||
|
||||
export default DocumentsPanel;
|
||||
|
||||
const SortFieldQuickMenu = ({ sortField, onChange }) => {
|
||||
const currentOption = useMemo(
|
||||
() => SORT_OPTIONS.find((option) => option.value === sortField) || SORT_OPTIONS[0],
|
||||
[sortField],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => SORT_OPTIONS.map((option) => ({ id: option.value, label: option.label })),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value, option) => {
|
||||
if (typeof onChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
const nextValue = option?.id || option?.original?.id || value;
|
||||
if (nextValue) {
|
||||
onChange(nextValue);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const label = currentOption?.label || SORT_LABEL_LOOKUP[currentOption?.value] || 'Title';
|
||||
|
||||
return (
|
||||
<QuickAddMenu
|
||||
className="documents-sort__quickmenu"
|
||||
options={options}
|
||||
onSelectOption={handleSelect}
|
||||
triggerClassName="view-toggle__button documents-sort__trigger quick-add__trigger"
|
||||
triggerContent={(
|
||||
<span className="documents-sort__trigger-content">
|
||||
<span className="documents-sort__label">{label}</span>
|
||||
</span>
|
||||
)}
|
||||
triggerAriaLabel={`Sort by ${label}`}
|
||||
triggerTitle={`Sort by ${label}`}
|
||||
placeholder="Select sort field"
|
||||
menuMinWidth={200}
|
||||
align="start"
|
||||
positionStrategy="absolute"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createDocumentsTableHeaderActions = ({
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
onRefresh,
|
||||
onShowDeskHelp = null,
|
||||
sortField = 'title',
|
||||
onSortFieldChange = null,
|
||||
sortDirection = 'asc',
|
||||
onSortDirectionToggle = null,
|
||||
isFilterActive = false,
|
||||
includeDescendants = true,
|
||||
onToggleIncludeDescendants = null,
|
||||
}) => {
|
||||
const isListView = viewMode === 'list';
|
||||
const isGridView = viewMode === 'grid';
|
||||
const isDeskView = viewMode === 'desk';
|
||||
|
||||
const sortDirectionIsDesc = sortDirection === 'desc';
|
||||
const sortDirectionTitle = sortDirectionIsDesc
|
||||
? 'Sorting Z → A. Click to switch to ascending.'
|
||||
: 'Sorting A → Z. Click to switch to descending.';
|
||||
const includeDescendantsToggle = isFilterActive && typeof onToggleIncludeDescendants === 'function'
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button documents-toolbar__toggle"
|
||||
onClick={onToggleIncludeDescendants}
|
||||
aria-pressed={!includeDescendants}
|
||||
aria-label={includeDescendants ? 'Include subfolders' : 'Limit to current folder'}
|
||||
title={includeDescendants
|
||||
? 'Including subfolders. Click to limit the search to the current folder.'
|
||||
: 'Limiting to the current folder. Click to include subfolders again.'}
|
||||
>
|
||||
{includeDescendants ? <FoldersIcon /> : <FoldersOffIcon />}
|
||||
</button>
|
||||
)
|
||||
: null;
|
||||
|
||||
const sortControls = typeof onSortFieldChange === 'function'
|
||||
? (
|
||||
<div className="documents-actions__sort-group">
|
||||
<SortFieldQuickMenu sortField={sortField} onChange={onSortFieldChange} />
|
||||
{typeof onSortDirectionToggle === 'function' ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button documents-toolbar__toggle documents-sort__direction"
|
||||
onClick={onSortDirectionToggle}
|
||||
aria-pressed={sortDirectionIsDesc}
|
||||
aria-label={sortDirectionIsDesc ? 'Sort descending' : 'Sort ascending'}
|
||||
title={sortDirectionTitle}
|
||||
>
|
||||
{sortDirectionIsDesc ? (
|
||||
<SortDescendingLettersIcon size={18} />
|
||||
) : (
|
||||
<SortAscendingLettersIcon size={18} />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDeskView && typeof onShowDeskHelp === 'function' ? (
|
||||
@@ -771,6 +882,22 @@ export const createDocumentsTableHeaderActions = ({
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
{includeDescendantsToggle ? (
|
||||
<>
|
||||
{includeDescendantsToggle}
|
||||
<span className="main-content__actions-divider" aria-hidden="true">
|
||||
<MinusVerticalIcon />
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
{sortControls ? (
|
||||
<>
|
||||
{sortControls}
|
||||
<span className="main-content__actions-divider" aria-hidden="true">
|
||||
<MinusVerticalIcon />
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
<div className="view-toggle" role="group" aria-label="Change view">
|
||||
<button
|
||||
type="button"
|
||||
@@ -828,9 +955,14 @@ export const createDocumentsSurface = ({
|
||||
currentFolderName,
|
||||
breadcrumbs,
|
||||
searchResults,
|
||||
isFilterActive,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
onRefresh,
|
||||
sortField,
|
||||
sortDirection,
|
||||
onSortFieldChange,
|
||||
onSortDirectionToggle,
|
||||
selectedDocumentIds,
|
||||
selectedFolderIds,
|
||||
onDeleteSelection,
|
||||
@@ -846,6 +978,8 @@ export const createDocumentsSurface = ({
|
||||
onBulkReanalyze,
|
||||
folderOptions,
|
||||
onMoveDocumentsToFolder,
|
||||
searchIncludeDescendants,
|
||||
onToggleSearchIncludeDescendants,
|
||||
} = tableProps;
|
||||
|
||||
const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName;
|
||||
@@ -862,6 +996,13 @@ export const createDocumentsSurface = ({
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
onRefresh,
|
||||
sortField,
|
||||
onSortFieldChange,
|
||||
sortDirection,
|
||||
onSortDirectionToggle,
|
||||
isFilterActive,
|
||||
includeDescendants: searchIncludeDescendants,
|
||||
onToggleIncludeDescendants: onToggleSearchIncludeDescendants,
|
||||
});
|
||||
|
||||
const floatingActions = selectionCount > 0
|
||||
|
||||
@@ -165,6 +165,7 @@ const Sidebar = ({
|
||||
onCreateFolder,
|
||||
creatingFolder = false,
|
||||
tags = [],
|
||||
untaggedFilterId = null,
|
||||
activeTagIds = [],
|
||||
onToggleTagFilter,
|
||||
correspondents = [],
|
||||
@@ -215,6 +216,7 @@ const Sidebar = ({
|
||||
);
|
||||
const handleToggleTag = onToggleTagFilter || (() => {});
|
||||
const activeTagSet = new Set(activeTagIds);
|
||||
const untaggedActive = untaggedFilterId ? activeTagSet.has(untaggedFilterId) : false;
|
||||
const handleManageTags = onManageTags || (() => {});
|
||||
const handleManageCorrespondents = onManageCorrespondents || (() => {});
|
||||
const handleCreateTag = useCallback(async () => {
|
||||
@@ -619,6 +621,20 @@ const Sidebar = ({
|
||||
}`}
|
||||
role="list"
|
||||
>
|
||||
{untaggedFilterId ? (
|
||||
<button
|
||||
type="button"
|
||||
role="listitem"
|
||||
className={`sidebar-tag-pill sidebar-tag-pill--untagged${
|
||||
untaggedActive ? ' active' : ''
|
||||
}`}
|
||||
onClick={() => handleToggleTag(untaggedFilterId)}
|
||||
aria-pressed={untaggedActive}
|
||||
draggable={false}
|
||||
>
|
||||
No tag assigned
|
||||
</button>
|
||||
) : null}
|
||||
{tags.map((tag) => {
|
||||
const isActive = activeTagSet.has(tag.id);
|
||||
const style = getTagColorStyle(tag.color);
|
||||
|
||||
+90
-1
@@ -396,7 +396,7 @@ button.danger:hover:not([disabled]) {
|
||||
.panel-header button,
|
||||
.panel-header a.icon-button {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -631,6 +631,80 @@ button.danger:hover:not([disabled]) {
|
||||
height: 1.4rem;
|
||||
}
|
||||
|
||||
.documents-actions__sort-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.documents-sort {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.documents-sort__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
padding: 0.25rem 0.5rem;
|
||||
min-height: 2.1rem;
|
||||
}
|
||||
|
||||
.documents-sort__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.documents-sort__trigger-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.documents-sort__quickmenu .menu__item,
|
||||
.documents-sort__quickmenu .menu__item.active {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.documents-toolbar__toggle {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.3rem;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.documents-toolbar__toggle:hover:not([disabled]) {
|
||||
color: var(--fg);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.documents-toolbar__toggle[aria-pressed='true'] {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
.documents-sort__direction {
|
||||
padding: 0.3rem 0.45rem;
|
||||
}
|
||||
|
||||
.documents-sort__direction[aria-pressed='true'] {
|
||||
border-color: var(--border);
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.documents-sort__direction svg {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -1949,6 +2023,16 @@ button.danger:hover:not([disabled]) {
|
||||
box-shadow: 0 0 0 1.5px var(--sidebar-active-pill-border);
|
||||
}
|
||||
|
||||
.sidebar-tag-pill--untagged {
|
||||
border: 1px dashed var(--border);
|
||||
background: var(--surface-subtle);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.sidebar-tag-pill--untagged.active {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.sidebar-tag-cloud--has-active .sidebar-tag-pill:not(.active) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
@@ -2818,6 +2902,11 @@ button.danger:hover:not([disabled]) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.documents-sort__trigger.quick-add__trigger {
|
||||
padding: 0.25rem 0.5rem;
|
||||
min-height: 2.1rem;
|
||||
}
|
||||
|
||||
.quick-add__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
IconTextScan2,
|
||||
IconFolderPlus,
|
||||
IconFolder,
|
||||
IconFolders,
|
||||
IconFoldersOff,
|
||||
IconRefresh,
|
||||
IconRestore,
|
||||
IconMinusVertical,
|
||||
@@ -34,6 +36,8 @@ import {
|
||||
IconCircleDashedCheck,
|
||||
IconFile,
|
||||
IconLoader,
|
||||
IconSortAscendingLetters,
|
||||
IconSortDescendingLetters,
|
||||
} from '@tabler/icons-react';
|
||||
import FolderSvg from '../assets/folder.svg';
|
||||
|
||||
@@ -190,6 +194,24 @@ export const FolderPlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest
|
||||
/>
|
||||
);
|
||||
|
||||
export const FoldersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFolders
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const FoldersOffIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFoldersOff
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconRefresh
|
||||
className={composeClassName('icon', className)}
|
||||
@@ -262,6 +284,24 @@ export const IconFileStack = ({ className, size = 24, stroke = 160, ...rest }) =
|
||||
);
|
||||
};
|
||||
|
||||
export const SortAscendingLettersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconSortAscendingLetters
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SortDescendingLettersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconSortDescendingLetters
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const IconX = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<TablerIconX
|
||||
className={composeClassName('icon', className)}
|
||||
|
||||
Reference in New Issue
Block a user