feat(resourceList): keep consistence in groupby options order accross sectons + add userVignette on owner's swimlane
This commit is contained in:
@@ -574,6 +574,20 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* When the header contains a rich DOM node (e.g. a user vignette for the
|
||||
"owner" group-by), reset the typographic overrides that only make sense
|
||||
for plain-text labels, and lay the node out inline. */
|
||||
.resource-list__swimlane-header--node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
font-size: inherit;
|
||||
font-weight: normal;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ── Swimlane group card (list view only) ────────────────── */
|
||||
|
||||
/* When swimlane groups are present, dissolve the outer container into the
|
||||
|
||||
+13
-13
@@ -76,6 +76,19 @@ const GROUP_BY_DEFS = [
|
||||
return labels[key] ?? key;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
get label() {
|
||||
return i18n.t('groupby.size', 'Size');
|
||||
},
|
||||
orderBy: 'size',
|
||||
// sizeBucket(-1) → "Folders" sentinel; no labelFn needed.
|
||||
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: 'modifiedAt',
|
||||
get label() {
|
||||
@@ -98,19 +111,6 @@ const GROUP_BY_DEFS = [
|
||||
const r = /** @type {Record<string, number>} */ (/** @type {unknown} */ (item));
|
||||
return r.created_at ? normalizeDateBucket(r.created_at) : null;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
get label() {
|
||||
return i18n.t('groupby.size', 'Size');
|
||||
},
|
||||
orderBy: 'size',
|
||||
// sizeBucket(-1) → "Folders" sentinel; no labelFn needed.
|
||||
keyFn: (item) => {
|
||||
if (!('mime_type' in item)) return sizeBucket(-1);
|
||||
const r = /** @type {Record<string, number>} */ (/** @type {unknown} */ (item));
|
||||
return sizeBucket(r.size ?? 0);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -115,6 +115,14 @@ export class ResourceListComponent {
|
||||
*/
|
||||
this._groupLabelFn = undefined;
|
||||
|
||||
/**
|
||||
* Optional node-builder stored between `render()` and `append()` calls.
|
||||
* When set, the swimlane header renders a DOM node instead of plain text
|
||||
* (e.g. a user vignette for the "owner" group-by dimension).
|
||||
* @type {((key: string) => HTMLElement) | undefined}
|
||||
*/
|
||||
this._headerNodeFn = undefined;
|
||||
|
||||
this._ownerVisible = this._cfg.showOwner;
|
||||
|
||||
this._initDelegation();
|
||||
@@ -138,8 +146,12 @@ export class ResourceListComponent {
|
||||
* @param {((key: string) => string)=} groupLabelFn
|
||||
* Optional: converts the raw grouping key to a human-readable header
|
||||
* label. When omitted the key itself is used.
|
||||
* @param {((key: string) => HTMLElement)=} headerNodeFn
|
||||
* Optional: builds a rich DOM node for the swimlane header (e.g. a user
|
||||
* vignette for the "owner" group-by). When provided, `groupLabelFn` is
|
||||
* ignored for the header and the returned node is appended instead.
|
||||
*/
|
||||
render(items, groupFn, groupLabelFn) {
|
||||
render(items, groupFn, groupLabelFn, headerNodeFn) {
|
||||
const header = this._container.querySelector('.list-header');
|
||||
this._container.innerHTML = '';
|
||||
if (header) this._container.appendChild(header);
|
||||
@@ -151,11 +163,12 @@ export class ResourceListComponent {
|
||||
this._lastGroupKey = undefined;
|
||||
this._lastGroupEl = null;
|
||||
this._groupLabelFn = groupLabelFn;
|
||||
this._headerNodeFn = headerNodeFn;
|
||||
|
||||
// Prevent ui.js global delegation from firing on this container
|
||||
this._container.dataset.managedBy = 'resource-list';
|
||||
|
||||
this._appendItems(items, groupFn, groupLabelFn);
|
||||
this._appendItems(items, groupFn, groupLabelFn, headerNodeFn);
|
||||
this._wireSelectAll();
|
||||
}
|
||||
|
||||
@@ -167,9 +180,10 @@ export class ResourceListComponent {
|
||||
* @param {Array<FileItem|FolderItem>} items
|
||||
* @param {((item: FileItem|FolderItem) => string|null)=} groupFn
|
||||
* @param {((key: string) => string)=} groupLabelFn
|
||||
* @param {((key: string) => HTMLElement)=} headerNodeFn
|
||||
*/
|
||||
append(items, groupFn, groupLabelFn) {
|
||||
this._appendItems(items, groupFn, groupLabelFn ?? this._groupLabelFn);
|
||||
append(items, groupFn, groupLabelFn, headerNodeFn) {
|
||||
this._appendItems(items, groupFn, groupLabelFn ?? this._groupLabelFn, headerNodeFn ?? this._headerNodeFn);
|
||||
}
|
||||
|
||||
/** Remove all items (but keep `.list-header` if present). */
|
||||
@@ -328,8 +342,9 @@ export class ResourceListComponent {
|
||||
* @param {Array<FileItem|FolderItem>} items
|
||||
* @param {((item: FileItem|FolderItem) => string|null)=} groupFn
|
||||
* @param {((key: string) => string)=} groupLabelFn
|
||||
* @param {((key: string) => HTMLElement)=} headerNodeFn
|
||||
*/
|
||||
_appendItems(items, groupFn, groupLabelFn) {
|
||||
_appendItems(items, groupFn, groupLabelFn, headerNodeFn) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Start from the persisted key so load-more pages continue seamlessly.
|
||||
@@ -354,7 +369,7 @@ export class ResourceListComponent {
|
||||
if (key !== null) {
|
||||
fragmentGroup = document.createElement('div');
|
||||
fragmentGroup.className = 'resource-list__swimlane-group';
|
||||
fragmentGroup.appendChild(this._createGroupHeader(key, groupLabelFn));
|
||||
fragmentGroup.appendChild(this._createGroupHeader(key, groupLabelFn, headerNodeFn));
|
||||
fragment.appendChild(fragmentGroup);
|
||||
}
|
||||
}
|
||||
@@ -382,14 +397,25 @@ export class ResourceListComponent {
|
||||
|
||||
/**
|
||||
* Create a swimlane divider element.
|
||||
* @param {string} key - Raw grouping key (e.g. UUID or bucket name).
|
||||
* @param {((key: string) => string)=} labelFn - Optional human-readable resolver.
|
||||
*
|
||||
* When `headerNodeFn` is supplied the header renders a rich DOM node
|
||||
* (e.g. a user vignette) instead of plain text; the `--node` CSS modifier
|
||||
* is added to suppress the small-caps / uppercase text styles.
|
||||
*
|
||||
* @param {string} key - Raw grouping key (e.g. UUID or bucket name).
|
||||
* @param {((key: string) => string)=} labelFn - Optional plain-text resolver.
|
||||
* @param {((key: string) => HTMLElement)=} headerNodeFn - Optional rich-node builder.
|
||||
*/
|
||||
_createGroupHeader(key, labelFn) {
|
||||
_createGroupHeader(key, labelFn, headerNodeFn) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'resource-list__swimlane-header';
|
||||
el.dataset.swimlaneHeader = 'true';
|
||||
el.textContent = labelFn ? labelFn(key) : key;
|
||||
if (headerNodeFn) {
|
||||
el.classList.add('resource-list__swimlane-header--node');
|
||||
el.appendChild(headerNodeFn(key));
|
||||
} else {
|
||||
el.textContent = labelFn ? labelFn(key) : key;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user