use of resourceList

This commit is contained in:
Edouard Vanbelle
2026-05-26 23:21:58 +02:00
parent 6ac4e6177c
commit 720bf11168
6 changed files with 371 additions and 144 deletions
+32 -6
View File
@@ -22,6 +22,7 @@ import { getAuthHeaders } from './fileOperations.js';
/**
* @import {ItemTypeEnum, LightItem} from '../../core/types.js'
* @import {BatchResult} from './fileOperations.js'
* @import {ResourceListComponent} from '../../components/resourceList.js'
*/
const batchToolbar = {
@@ -35,6 +36,23 @@ const batchToolbar = {
/** Whether the selection bar is currently visible */
_barVisible: false,
/**
* The `ResourceListComponent` currently managing the active view.
* When set, keyboard shortcuts (Ctrl+A, Escape) delegate to the component
* so its internal selection state stays consistent.
* @type {ResourceListComponent | null}
*/
_activeComponent: null,
/**
* Register (or unregister) the component that owns the current view's
* selection state. Pass `null` when leaving a component-managed view.
* @param {ResourceListComponent | null} component
*/
setActiveComponent(component) {
this._activeComponent = component;
},
// ── Public API ──────────────────────────────────────────
get count() {
@@ -514,15 +532,23 @@ const batchToolbar = {
if (target.closest('input, textarea, [contenteditable], .rename-dialog, .share-dialog, .confirm-dialog')) return;
const selectAllCheckbox = /** @type {HTMLInputElement} */ (document.getElementById('select-all-checkbox'));
// ctrl+a cmd+a
// ctrl+a / cmd+a — delegate to active component when present
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
if (selectAllCheckbox) selectAllCheckbox.checked = true;
this.selectAll();
if (this._activeComponent) {
this._activeComponent.selectAll();
} else {
if (selectAllCheckbox) selectAllCheckbox.checked = true;
this.selectAll();
}
e.preventDefault();
}
if (e.key === 'Escape' && this.hasSelection) {
this.clear();
if (selectAllCheckbox) selectAllCheckbox.checked = false;
if (e.key === 'Escape') {
if (this._activeComponent && this._activeComponent._selected.size > 0) {
this._activeComponent.clearSelection();
} else if (this.hasSelection) {
this.clear();
if (selectAllCheckbox) selectAllCheckbox.checked = false;
}
}
if (e.key === 'Delete' && this.hasSelection) this.batchDelete();
});
+59 -15
View File
@@ -9,6 +9,7 @@
import { ui } from '../../app/ui.js';
import { getCsrfHeaders } from '../../core/csrf.js';
import { i18n } from '../../core/i18n.js';
import { ResourceListComponent } from '../../components/resourceList.js';
import { batchToolbar } from '../files/batchToolbar.js';
import * as pathTooltip from '../pathTooltip.js';
@@ -21,6 +22,9 @@ const favorites = {
/** Whether the initial fetch from the server has completed */
_ready: false,
/** @type {ResourceListComponent|null} */
_component: null,
// ───────────────────── helpers ─────────────────────
_authHeaders() {
@@ -105,7 +109,7 @@ const favorites = {
* @param {string} id
* @param {string} name
* @param {string} type
* @param {string} _parentId
* @param {string | null} _parentId
*/
async addToFavorites(id, name, type, _parentId) {
try {
@@ -178,11 +182,8 @@ const favorites = {
try {
await this._fetchFromServer();
ui.resetFilesList(); // ensure also list visible & error hidden
// wire buttons & select-all-checkbox as list header has changed in ui.resetFilesList()
// FIXME: this case is not easy to understand, should apply better implementation
ui.resetFilesList();
batchToolbar.init();
ui.updateBreadcrumb();
if (this._cache.size === 0) {
@@ -201,11 +202,10 @@ const favorites = {
const files = [];
for (const item of this._cache.values()) {
// owner_id comes from the backend JOIN (actual file/folder owner, not the favoriter)
// owner_id comes from the backend JOIN (actual file/folder owner)
if (item.item_type === 'folder') {
folders.push(
// FIXME: better to grab the real values
/** @type {FolderItem} */ {
/** @type {FolderItem} */ ({
id: item.item_id,
name: item.item_name || item.item_id,
parent_id: item.parent_id || '',
@@ -217,12 +217,11 @@ const favorites = {
icon_special_class: item.icon_special_class,
owner_id: item.owner_id ?? '',
is_root: false
}
})
);
} else {
files.push(
// FIXME: better to grab the real values
/** @type {FileItem} */ {
/** @type {FileItem} */ ({
id: item.item_id,
name: item.item_name || item.item_id,
folder_id: item.parent_id || '',
@@ -237,15 +236,60 @@ const favorites = {
owner_id: item.owner_id ?? '',
created_at: item.created_at,
sort_date: item.created_at
}
})
);
}
}
if (folders.length) ui.renderFolders(folders);
if (files.length) ui.renderFiles(files);
const filesList = document.getElementById('files-list');
if (filesList) pathTooltip.init(filesList);
if (filesList) {
if (!this._component) {
this._component = new ResourceListComponent(
/** @type {HTMLElement} */ (filesList),
{
selectable: true,
showFavorite: true,
showOwner: true,
showShareBadge: true,
draggable: false,
showContextMenu: true,
itemModifierClass: 'favorite-item',
isFavorite: (id, type) => this.isFavorite(id, type),
onOpen: (item) => ui.openItem(item),
onFavoriteToggle: async (item) => {
const isFile = 'mime_type' in item;
const type = isFile ? 'file' : 'folder';
if (this.isFavorite(item.id, type)) {
await this.removeFromFavorites(item.id, type);
this._component?.setFavoriteVisualState(item.id, type, false);
} else {
await this.addToFavorites(item.id, item.name, type, null);
this._component?.setFavoriteVisualState(item.id, type, true);
}
},
onContextMenu: (item, e) => ui.showContextMenuForItem(item, e),
onSelectionChange: (selectedItems) => {
batchToolbar._selected.clear();
for (const sel of selectedItems) {
const isFile = 'mime_type' in sel;
batchToolbar._selected.set(sel.id, {
id: sel.id,
name: sel.name,
type: isFile ? 'file' : 'folder',
parentId: isFile
? (/** @type {FileItem} */ (sel)).folder_id || ''
: (/** @type {FolderItem} */ (sel)).parent_id || ''
});
}
batchToolbar._syncUI();
}
}
);
}
batchToolbar.setActiveComponent(this._component);
this._component.render(folders, files);
pathTooltip.init(filesList);
}
await ui.resolveOwnerCells();
} catch (error) {
+64 -22
View File
@@ -9,6 +9,7 @@
import { ui } from '../../app/ui.js';
import { getCsrfHeaders } from '../../core/csrf.js';
import { i18n } from '../../core/i18n.js';
import { ResourceListComponent } from '../../components/resourceList.js';
import { batchToolbar } from '../files/batchToolbar.js';
import * as pathTooltip from '../pathTooltip.js';
@@ -18,6 +19,9 @@ const recent = {
/** Maximum items to request from the server */
MAX_RECENT_FILES: 20,
/** @type {ResourceListComponent|null} */
_component: null,
// ───────────────────── helpers ─────────────────────
_authHeaders() {
@@ -97,24 +101,27 @@ const recent = {
const recentItems = /** @type {RecentItem[]} */ (await response.json());
ui.resetFilesList(); // ensure also list visible & error hidden
// resetFilesList injects the standard list-header with the
// Modified column label; we swap the last header cell to "Accessed".
ui.resetFilesList();
const filesList = document.getElementById('files-list');
filesList.innerHTML = `
<div class="list-header">
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
<div data-i18n="files.name">Name</div>
<div data-i18n="files.type">Type</div>
<div data-i18n="files.size">Size</div>
<div data-i18n="recent.accessed">Accessed</div>
<div></div>
</div>
`;
if (batchToolbar) {
batchToolbar.clear();
batchToolbar.init(); // this will wire buttons & select-all-checkbox
if (filesList) {
// Relabel the date column header from "Modified" → "Accessed"
const dateHeader = /** @type {HTMLElement|null} */ (
[...filesList.querySelectorAll('.list-header > div')].find(
(el) => el.getAttribute('data-i18n') === 'files.modified'
)
);
if (dateHeader) {
dateHeader.removeAttribute('data-i18n');
dateHeader.setAttribute('data-i18n', 'recent.accessed');
dateHeader.textContent = i18n.t('recent.accessed', 'Accessed');
}
}
batchToolbar.clear();
batchToolbar.init();
ui.updateBreadcrumb();
if (recentItems.length === 0) {
@@ -123,6 +130,7 @@ const recent = {
<p>${i18n.t('recent.empty_state')}</p>
<p>${i18n.t('recent.empty_hint')}</p>
`);
return;
}
/** @type {FolderItem[]} */
@@ -141,7 +149,7 @@ const recent = {
modified_at: item.accessed_at,
path: item.item_path || '',
category: 'folder',
created_at: item.accessed_at, //Wrong information
created_at: item.accessed_at, // Wrong information — server only stores accessed_at
icon_class: item.icon_class,
icon_special_class: item.icon_special_class,
owner_id: '',
@@ -151,7 +159,6 @@ const recent = {
if (item.item_mime_type === undefined || item.item_mime_type === null) {
// FIXME: this case should not be possible, is it an information badly cleaned up on server ?
console.warn('Broken information for RecentItem: ', item);
//continue;
}
files.push({
id: item.item_id,
@@ -166,14 +173,49 @@ const recent = {
modified_at: item.accessed_at,
path: item.item_path || '',
owner_id: '',
created_at: item.accessed_at, //wrong information
created_at: item.accessed_at, // Wrong information — server only stores accessed_at
sort_date: item.accessed_at
});
}
}
if (folders.length) ui.renderFolders(folders);
if (files.length) ui.renderFiles(files);
if (filesList) pathTooltip.init(filesList);
if (filesList) {
if (!this._component) {
this._component = new ResourceListComponent(
/** @type {HTMLElement} */ (filesList),
{
selectable: true,
showFavorite: true,
showOwner: false,
showShareBadge: false,
draggable: false,
showContextMenu: true,
itemModifierClass: 'recent-item',
dateField: 'modified_at', // mapped from accessed_at above
onOpen: (item) => ui.openItem(item),
onContextMenu: (item, e) => ui.showContextMenuForItem(item, e),
onSelectionChange: (selectedItems) => {
batchToolbar._selected.clear();
for (const sel of selectedItems) {
const isFile = 'mime_type' in sel;
batchToolbar._selected.set(sel.id, {
id: sel.id,
name: sel.name,
type: isFile ? 'file' : 'folder',
parentId: isFile
? (/** @type {FileItem} */ (sel)).folder_id || ''
: (/** @type {FolderItem} */ (sel)).parent_id || ''
});
}
batchToolbar._syncUI();
}
}
);
}
batchToolbar.setActiveComponent(this._component);
this._component.render(folders, files);
pathTooltip.init(filesList);
}
} catch (error) {
console.error('Error displaying recent files:', error);
if (ui?.showNotification) {