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
+16 -2
View File
@@ -31,8 +31,8 @@ import { systemUsers } from '../../model/systemUsers.js';
/** @import {FavoritesResourceItem, FileItem, FolderItem, ResourceTypeEnum} 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,
* headerNodeFn?: (key: string) => HTMLElement }} GroupByDef
*/
@@ -44,11 +44,21 @@ import { systemUsers } from '../../model/systemUsers.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: 'owner',
get label() {
return i18n.t('groupby.owner', 'Owner');
},
icon: 'fas fa-layer-group',
orderBy: 'owner',
// keyFn groups by UUID — stable, avoids collisions on identical display names.
keyFn: (item) => {
@@ -63,6 +73,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.type', 'Type');
},
icon: 'fas fa-layer-group',
orderBy: 'type',
// keyFn: folders → 'Folder' swimlane; files → their `category` field.
keyFn: (item) => ('mime_type' in item ? /** @type {Record<string,string>} */ (/** @type {unknown} */ (item)).category || 'other' : 'Folder'),
@@ -92,6 +103,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.size', 'Size');
},
icon: 'fas fa-layer-group',
orderBy: 'size',
// Folders have no size — sizeBucket(-1) returns the "Folders" label.
keyFn: (item) => {
@@ -105,6 +117,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.favoriteDate', 'Favorite date');
},
icon: 'fas fa-layer-group',
orderBy: 'favorited_at',
// sort_date is stored as unix seconds in _mapItems().
keyFn: (item) => {
@@ -117,6 +130,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.modifiedAt', 'Modified date');
},
icon: 'fas fa-layer-group',
orderBy: 'modified_at',
// modified_at is a unix seconds timestamp on FileItem/FolderItem.
keyFn: (item) => {
+3 -1
View File
@@ -22,7 +22,7 @@ import { grants } from '../../model/grants.js';
/** @import {FileItem, FolderItem} from '../../core/types.js' */
/**
* @typedef {{ key: string, label: string, orderBy: string }} GroupByDef
* @typedef {{ key: string, label: string, icon?: string, orderBy: string }} GroupByDef
* @typedef {'items'|'sharedWith'} ViewMode
*/
@@ -42,6 +42,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.byFiles', 'By files');
},
icon: 'fas fa-layer-group',
orderBy: 'type'
},
{
@@ -49,6 +50,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.sharedWith', 'Shared with');
},
icon: 'fas fa-layer-group',
orderBy: 'subject'
}
];
+16 -2
View File
@@ -32,8 +32,8 @@ import { systemUsers } from '../../model/systemUsers.js';
/** @import {FileItem, FolderItem, ResourceTypeEnum} 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,
* headerNodeFn?: (key: string) => HTMLElement }} GroupByDef
*/
@@ -54,11 +54,21 @@ import { systemUsers } from '../../model/systemUsers.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: 'owner',
get label() {
return i18n.t('groupby.owner', 'Owner');
},
icon: 'fas fa-layer-group',
orderBy: 'owner',
keyFn: (item) => {
const r = /** @type {Record<string,string>} */ (/** @type {unknown} */ (item));
@@ -72,6 +82,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.type', 'Type');
},
icon: 'fas fa-layer-group',
orderBy: 'type',
keyFn: (item) => ('mime_type' in item ? /** @type {Record<string,string>} */ (/** @type {unknown} */ (item)).category || 'other' : 'Folder'),
labelFn: (key) => {
@@ -100,6 +111,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.size', 'Size');
},
icon: 'fas fa-layer-group',
orderBy: 'size',
keyFn: (item) => {
if (!('mime_type' in item)) return sizeBucket(-1);
@@ -112,6 +124,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.accessedAt', 'Accessed date');
},
icon: 'fas fa-layer-group',
orderBy: 'accessed_at',
// sort_date is unix seconds set in _mapItems(); keyFn returns the bucket label.
keyFn: (item) => {
@@ -124,6 +137,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.modifiedAt', 'Modified date');
},
icon: 'fas fa-layer-group',
orderBy: 'modified_at',
keyFn: (item) => {
const r = /** @type {Record<string,number>} */ (/** @type {unknown} */ (item));
@@ -25,8 +25,8 @@ import { systemUsers } from '../../model/systemUsers.js';
/** @import {SharedWithMeItem, FileItem, FolderItem, ResourceTypeEnum} 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,
* headerNodeFn?: (key: string) => HTMLElement }} GroupByDef
*/
@@ -44,6 +44,15 @@ import { systemUsers } from '../../model/systemUsers.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: 'owner',
// label is accessed via syncGroupByMenu → read at section-switch time,
@@ -51,6 +60,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.owner', 'Owner');
},
icon: 'fas fa-layer-group',
orderBy: 'granted_by',
// keyFn groups by UUID — stable and unique, avoids collisions between
// users with the same display name.
@@ -67,6 +77,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.type', 'Type');
},
icon: 'fas fa-layer-group',
orderBy: 'type',
// keyFn: folders get their own swimlane; files use the pre-computed
// `category` field from the DTO (e.g. 'Image', 'Video', 'Audio' …).
@@ -99,6 +110,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.size', 'Size');
},
icon: 'fas fa-layer-group',
orderBy: 'size',
// keyFn: the key IS the bucket label returned by sizeBucket(), so no
// separate labelFn is needed (same pattern as shareDate).
@@ -115,6 +127,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.shareDate', 'Share date');
},
icon: 'fas fa-layer-group',
orderBy: 'granted_at',
// keyFn returns the human-readable bucket label; the label IS the key
// because consecutive items with the same bucket should be in one group.
+16 -2
View File
@@ -28,8 +28,8 @@ import { fetchTrashPage } from '../../model/trashModel.js';
/** @import {FileItem, FolderItem, ResourceTypeEnum, TrashResourceItem} from '../../core/types.js' */
/**
* @typedef {{ key: string, label: string, orderBy: string, reverseDefault?: boolean,
* keyFn: (item: FileItem|FolderItem) => string|null,
* @typedef {{ key: string, label: string, icon?: string, orderBy: string, reverseDefault?: boolean,
* keyFn?: (item: FileItem|FolderItem) => string|null,
* labelFn?: (key: string) => string,
* headerNodeFn?: (key: string) => HTMLElement }} GroupByDef
*/
@@ -49,11 +49,22 @@ import { fetchTrashPage } from '../../model/trashModel.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',
reverseDefault: false
// no keyFn → ResourceListComponent renders a flat list (server pins folders first).
},
{
key: 'remainingDays',
get label() {
return i18n.t('trash.groupby.remaining_days', 'Remaining days');
},
icon: 'fas fa-layer-group',
orderBy: 'deletion_date',
reverseDefault: false,
keyFn: (item) => {
@@ -66,6 +77,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.type', 'Type');
},
icon: 'fas fa-layer-group',
orderBy: 'type',
reverseDefault: false,
keyFn: (item) => ('mime_type' in item ? /** @type {Record<string,string>} */ (/** @type {unknown} */ (item)).category || 'other' : 'Folder'),
@@ -95,6 +107,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('groupby.size', 'Size');
},
icon: 'fas fa-layer-group',
orderBy: 'size',
reverseDefault: false,
keyFn: (item) => {
@@ -108,6 +121,7 @@ const GROUP_BY_DEFS = [
get label() {
return i18n.t('trash.groupby.trashed_time', 'Trashed time');
},
icon: 'fas fa-layer-group',
orderBy: 'trashed_at',
reverseDefault: false,
keyFn: (item) => {