feat(resourceList): keep consistence in groupby options order accross sectons + add userVignette on owner's swimlane

This commit is contained in:
Edouard Vanbelle
2026-05-28 12:07:56 +02:00
parent 80c9ace108
commit 29d8aab9c0
6 changed files with 148 additions and 99 deletions
+32 -29
View File
@@ -18,6 +18,7 @@
import { ui } from '../../app/ui.js';
import { ResourceListComponent } from '../../components/resourceList.js';
import { createUserVignette } from '../../components/userVignette.js';
import { normalizeDateBucket, sizeBucket } from '../../core/formatters.js';
import { i18n } from '../../core/i18n.js';
import * as viewPrefs from '../../core/viewPrefs.js';
@@ -32,7 +33,8 @@ import { systemUsers } from '../../model/systemUsers.js';
/**
* @typedef {{ key: string, label: string, orderBy: string,
* keyFn: (item: FileItem|FolderItem) => string|null,
* labelFn?: (key: string) => string }} GroupByDef
* labelFn?: (key: string) => string,
* headerNodeFn?: (key: string) => HTMLElement }} GroupByDef
*/
/**
@@ -42,6 +44,20 @@ import { systemUsers } from '../../model/systemUsers.js';
* @type {GroupByDef[]}
*/
const GROUP_BY_DEFS = [
{
key: 'owner',
get label() {
return i18n.t('groupby.owner', 'Owner');
},
orderBy: 'owner',
// keyFn groups by UUID — stable, avoids collisions on identical display names.
keyFn: (item) => {
const r = /** @type {Record<string,string>} */ (/** @type {unknown} */ (item));
return r.owner_id || null;
},
labelFn: (id) => systemUsers.getDisplayNameSync(id),
headerNodeFn: (id) => createUserVignette(id, 'sm')
},
{
key: 'type',
get label() {
@@ -71,6 +87,19 @@ const GROUP_BY_DEFS = [
return labels[key] ?? key;
}
},
{
key: 'size',
get label() {
return i18n.t('groupby.size', 'Size');
},
orderBy: 'size',
// Folders have no size — sizeBucket(-1) returns the "Folders" label.
keyFn: (item) => {
if (!('mime_type' in item)) return sizeBucket(-1);
const r = /** @type {Record<string,number>} */ (/** @type {unknown} */ (item));
return sizeBucket(r.size ?? 0);
}
},
{
key: 'favoriteDate',
get label() {
@@ -94,32 +123,6 @@ const GROUP_BY_DEFS = [
const r = /** @type {Record<string,number>} */ (/** @type {unknown} */ (item));
return r.modified_at ? normalizeDateBucket(r.modified_at) : null;
}
},
{
key: 'size',
get label() {
return i18n.t('groupby.size', 'Size');
},
orderBy: 'size',
// Folders have no size — sizeBucket(-1) returns the "Folders" label.
keyFn: (item) => {
if (!('mime_type' in item)) return sizeBucket(-1);
const r = /** @type {Record<string,number>} */ (/** @type {unknown} */ (item));
return sizeBucket(r.size ?? 0);
}
},
{
key: 'owner',
get label() {
return i18n.t('groupby.owner', 'Owner');
},
orderBy: 'owner',
// keyFn groups by UUID — stable, avoids collisions on identical display names.
keyFn: (item) => {
const r = /** @type {Record<string,string>} */ (/** @type {unknown} */ (item));
return r.owner_id || null;
},
labelFn: (id) => systemUsers.getDisplayNameSync(id)
}
];
@@ -304,9 +307,9 @@ const favoritesView = {
const items = this._mapItems(data.items);
if (isFirstPage) {
this._component?.render(items, def?.keyFn, def?.labelFn);
this._component?.render(items, def?.keyFn, def?.labelFn, def?.headerNodeFn);
} else {
this._component?.append(items, def?.keyFn, def?.labelFn);
this._component?.append(items, def?.keyFn, def?.labelFn, def?.headerNodeFn);
}
// Wire unified item tooltip (owner + path) after items are in the DOM
+30 -27
View File
@@ -19,6 +19,7 @@
import { ui } from '../../app/ui.js';
import { ResourceListComponent } from '../../components/resourceList.js';
import { createUserVignette } from '../../components/userVignette.js';
import { normalizeDateBucket, sizeBucket } from '../../core/formatters.js';
import { i18n } from '../../core/i18n.js';
import * as viewPrefs from '../../core/viewPrefs.js';
@@ -33,7 +34,8 @@ import { systemUsers } from '../../model/systemUsers.js';
/**
* @typedef {{ key: string, label: string, orderBy: string,
* keyFn: (item: FileItem|FolderItem) => string|null,
* labelFn?: (key: string) => string }} GroupByDef
* labelFn?: (key: string) => string,
* headerNodeFn?: (key: string) => HTMLElement }} GroupByDef
*/
/**
@@ -52,6 +54,19 @@ import { systemUsers } from '../../model/systemUsers.js';
* @type {GroupByDef[]}
*/
const GROUP_BY_DEFS = [
{
key: 'owner',
get label() {
return i18n.t('groupby.owner', 'Owner');
},
orderBy: 'owner',
keyFn: (item) => {
const r = /** @type {Record<string,string>} */ (/** @type {unknown} */ (item));
return r.owner_id || null;
},
labelFn: (id) => systemUsers.getDisplayNameSync(id),
headerNodeFn: (id) => createUserVignette(id, 'sm')
},
{
key: 'type',
get label() {
@@ -80,6 +95,18 @@ const GROUP_BY_DEFS = [
return labels[key] ?? key;
}
},
{
key: 'size',
get label() {
return i18n.t('groupby.size', 'Size');
},
orderBy: 'size',
keyFn: (item) => {
if (!('mime_type' in item)) return sizeBucket(-1);
const r = /** @type {Record<string,number>} */ (/** @type {unknown} */ (item));
return sizeBucket(r.size ?? 0);
}
},
{
key: 'accessedAt',
get label() {
@@ -102,30 +129,6 @@ const GROUP_BY_DEFS = [
const r = /** @type {Record<string,number>} */ (/** @type {unknown} */ (item));
return r.modified_at ? normalizeDateBucket(r.modified_at) : null;
}
},
{
key: 'size',
get label() {
return i18n.t('groupby.size', 'Size');
},
orderBy: 'size',
keyFn: (item) => {
if (!('mime_type' in item)) return sizeBucket(-1);
const r = /** @type {Record<string,number>} */ (/** @type {unknown} */ (item));
return sizeBucket(r.size ?? 0);
}
},
{
key: 'owner',
get label() {
return i18n.t('groupby.owner', 'Owner');
},
orderBy: 'owner',
keyFn: (item) => {
const r = /** @type {Record<string,string>} */ (/** @type {unknown} */ (item));
return r.owner_id || null;
},
labelFn: (id) => systemUsers.getDisplayNameSync(id)
}
];
@@ -311,9 +314,9 @@ const recentView = {
const items = this._mapItems(data.items);
if (isFirstPage) {
this._component?.render(items, def?.keyFn, def?.labelFn);
this._component?.render(items, def?.keyFn, def?.labelFn, def?.headerNodeFn);
} else {
this._component?.append(items, def?.keyFn, def?.labelFn);
this._component?.append(items, def?.keyFn, def?.labelFn, def?.headerNodeFn);
}
// Wire unified item tooltip (owner + path) after items are in the DOM.
@@ -12,6 +12,7 @@
import { ui } from '../../app/ui.js';
import { ResourceListComponent } from '../../components/resourceList.js';
import { createUserVignette } from '../../components/userVignette.js';
import { normalizeDateBucket, sizeBucket } from '../../core/formatters.js';
import { i18n } from '../../core/i18n.js';
import * as viewPrefs from '../../core/viewPrefs.js';
@@ -26,7 +27,8 @@ import { systemUsers } from '../../model/systemUsers.js';
/**
* @typedef {{ key: string, label: string, orderBy: string,
* keyFn: (item: FileItem|FolderItem) => string|null,
* labelFn?: (key: string) => string }} GroupByDef
* labelFn?: (key: string) => string,
* headerNodeFn?: (key: string) => HTMLElement }} GroupByDef
*/
/**
@@ -42,6 +44,24 @@ import { systemUsers } from '../../model/systemUsers.js';
* @type {GroupByDef[]}
*/
const GROUP_BY_DEFS = [
{
key: 'owner',
// label is accessed via syncGroupByMenu → read at section-switch time,
// when translations are guaranteed to be loaded.
get label() {
return i18n.t('groupby.owner', 'Owner');
},
orderBy: 'granted_by',
// keyFn groups by UUID — stable and unique, avoids collisions between
// users with the same display name.
keyFn: (item) => {
const r = /** @type {Record<string,string>} */ (/** @type {unknown} */ (item));
return r.owner_id || null;
},
// labelFn resolves UUID → display name from the pre-fetched cache.
labelFn: (id) => systemUsers.getDisplayNameSync(id),
headerNodeFn: (id) => createUserVignette(id, 'sm')
},
{
key: 'type',
get label() {
@@ -74,23 +94,6 @@ const GROUP_BY_DEFS = [
return labels[key] ?? key;
}
},
{
key: 'owner',
// label is accessed via syncGroupByMenu → read at section-switch time,
// when translations are guaranteed to be loaded.
get label() {
return i18n.t('groupby.owner', 'Owner');
},
orderBy: 'granted_by',
// keyFn groups by UUID — stable and unique, avoids collisions between
// users with the same display name.
keyFn: (item) => {
const r = /** @type {Record<string,string>} */ (/** @type {unknown} */ (item));
return r.owner_id || null;
},
// labelFn resolves UUID → display name from the pre-fetched cache.
labelFn: (id) => systemUsers.getDisplayNameSync(id)
},
{
key: 'size',
get label() {
@@ -316,9 +319,9 @@ const sharedWithMeView = {
const items = this._mapItems(data.items);
if (isFirstPage) {
this._component?.render(items, def?.keyFn, def?.labelFn);
this._component?.render(items, def?.keyFn, def?.labelFn, def?.headerNodeFn);
} else {
this._component?.append(items, def?.keyFn, def?.labelFn);
this._component?.append(items, def?.keyFn, def?.labelFn, def?.headerNodeFn);
}
// Wire owner tooltips after items are in the DOM