feat(ui): improve groupby / sortby user experience + fix confusion in trash section

This commit is contained in:
Edouard Vanbelle
2026-05-30 11:15:46 +02:00
parent ae82f4b664
commit 19e8af84b7
10 changed files with 108 additions and 33 deletions
+15 -2
View File
@@ -33,8 +33,8 @@ import { uiNotifications } from './uiNotifications.js';
/** @import {FileItem, FolderItem} from '../core/types.js' */
/**
* @typedef {{ key: string, label: string, orderBy: string,
* keyFn: (item: FileItem|FolderItem) => string|null,
* @typedef {{ key: string, label: string, icon?: string, orderBy: string,
* keyFn?: (item: FileItem|FolderItem) => string|null,
* labelFn?: (key: string) => string }} GroupByDef
*/
@@ -48,11 +48,21 @@ import { uiNotifications } from './uiNotifications.js';
* @type {GroupByDef[]}
*/
const GROUP_BY_DEFS = [
{
key: '',
get label() {
return i18n.t('files.name', 'Name');
},
icon: 'fas fa-arrow-up-a-z',
orderBy: 'name'
// no keyFn → flat list.
},
{
key: 'type',
get label() {
return i18n.t('groupby.type', 'Type');
},
icon: 'fas fa-layer-group',
orderBy: 'type',
// Folders → 'Folder'; files → their pre-computed category string.
keyFn: (item) => ('mime_type' in item ? /** @type {Record<string,string>} */ (/** @type {unknown} */ (item)).category || 'other' : 'Folder'),
@@ -82,6 +92,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.size', 'Size');
},
icon: 'fas fa-layer-group',
orderBy: 'size',
// sizeBucket(-1) → "Folders" sentinel; no labelFn needed.
keyFn: (item) => {
@@ -95,6 +106,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.modifiedAt', 'Modified date');
},
icon: 'fas fa-layer-group',
orderBy: 'modified_at',
// keyFn returns the human-readable bucket; the bucket IS the key.
keyFn: (item) => {
@@ -107,6 +119,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.createdAt', 'Created date');
},
icon: 'fas fa-layer-group',
orderBy: 'created_at',
keyFn: (item) => {
const r = /** @type {Record<string, number>} */ (/** @type {unknown} */ (item));
+15 -13
View File
@@ -86,7 +86,6 @@ const _toggleButtons = `
<div class="group-by-selector hidden" id="group-by-selector">
<button class="toggle-btn group-by-btn" id="group-by-btn"
title="Group by" data-i18n-title="groupby.title">
<i class="fas fa-layer-group"></i>
<span class="group-by-label"></span>
</button>
<button class="toggle-btn sort-dir-btn" id="sort-dir-btn"
@@ -168,7 +167,6 @@ const ACTIONS_BAR_TEMPLATES = {
<div class="group-by-selector hidden" id="group-by-selector">
<button class="toggle-btn group-by-btn" id="group-by-btn"
title="Group by" data-i18n-title="groupby.title">
<i class="fas fa-layer-group"></i>
<span class="group-by-label"></span>
</button>
<button class="toggle-btn sort-dir-btn" id="sort-dir-btn"
@@ -251,7 +249,7 @@ let _groupByDocumentClickHandler = null;
* Must be called AFTER `setActionsBarMode()` so the selector elements exist
* in the DOM.
*
* @param {Array<{key: string, label: string}>} [defs]
* @param {Array<{key: string, label: string, icon?: string}>} [defs]
*/
function syncGroupByMenu(defs = []) {
const selector = document.getElementById('group-by-selector');
@@ -274,15 +272,18 @@ function syncGroupByMenu(defs = []) {
return;
}
// Rebuild menu options — call i18n.t() directly so each label is resolved
// at call time (translations are loaded by the time any section switch runs).
// A def with key='' lets the section override the default "None" label.
const noneOverride = defs.find((d) => d.key === '');
const noneLabel = noneOverride ? noneOverride.label : i18n.t('groupby.none', 'None');
menu.innerHTML = `<button class="group-by-option active" data-group-by="">${escapeHtml(noneLabel)}</button>`;
// Rebuild menu options — call i18n.t() at call time so labels are
// localised. Each def supplies its own icon. A section opts into an
// ungrouped/sorted entry by including a def with key=''.
menu.innerHTML = '';
for (const def of defs) {
if (def.key === '') continue;
menu.insertAdjacentHTML('beforeend', `<button class="group-by-option" data-group-by="${escapeHtml(def.key)}">${escapeHtml(def.label)}</button>`);
const iconClass = def.icon ?? 'fas fa-layer-group';
menu.insertAdjacentHTML(
'beforeend',
`<button class="group-by-option" data-group-by="${escapeHtml(def.key)}">` +
`<i class="${escapeHtml(iconClass)}"></i> ${escapeHtml(def.label)}` +
`</button>`
);
}
// One stable document-level handler to close the menu on outside clicks.
@@ -314,9 +315,10 @@ function setupActionsBarDelegation() {
btn.classList.add('active');
document.getElementById('group-by-menu')?.classList.add('hidden');
const groupByBtn = document.getElementById('group-by-btn');
groupByBtn?.classList.toggle('active', key !== '');
// Always active — there's no neutral "None" state anymore.
groupByBtn?.classList.add('active');
const lbl = groupByBtn?.querySelector('.group-by-label');
if (lbl) lbl.textContent = key !== '' ? (btn.textContent ?? '') : '';
if (lbl) lbl.innerHTML = btn.innerHTML; // clone icon + label markup
// Changing order-by dimension resets direction to ascending
_groupByView?.setDirection(false);
document.getElementById('sort-dir-btn')?.classList.remove('active');
+2
View File
@@ -417,6 +417,8 @@ function switchToTrashSection() {
setActionsBarMode('trash');
setGroupByView(trashView);
syncGroupByMenu(trashView.groupByDefs);
const trashPrefs = viewPrefs.load('trash');
applyGroupByMenuState(trashPrefs.groupBy || 'remainingDays', trashPrefs.reversed);
//reset files view + remove any error
ui.resetFilesList();