Skip to content

Commit 59eb54a

Browse files
janpospisclaude
andcommitted
feat(folder-access): lazy level-by-level folder loading with drill-in
The folder-access editor loaded the whole folder tree up front and rendered one row per folder, freezing on large libraries. Load one level at a time using the shallow listing getFolderList.php already supports (folder=<path>&counts=0), and add a breadcrumb plus per-row drill-in to reach subfolders. In-progress edits are collected into the fallback map before navigating, so the batched save still persists grants from every visited level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b97b669 commit 59eb54a

1 file changed

Lines changed: 62 additions & 9 deletions

File tree

public/js/adminFolderAccess.js

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,18 @@ function buildFullGrantsForAllFolders(folders) {
202202

203203
let __allFoldersCache = new Map();
204204

205-
async function getAllFolders(force = false, sourceId = "") {
206-
const key = sourceId || getFolderAccessSourceId() || '';
207-
if (!force && __allFoldersCache.has(key)) return __allFoldersCache.get(key).slice();
208-
209-
const url = '/api/folder/getFolderList.php?counts=0&ts=' + Date.now()
210-
+ (key ? `&sourceId=${encodeURIComponent(key)}` : '');
205+
async function getAllFolders(force = false, sourceId = "", parent = "root") {
206+
// Load a single level (shallow: folder=<parent>&counts=0) instead of the full recursive tree.
207+
// The full list walks every folder and renders one row per folder, which freezes the editor on
208+
// large libraries. The backend already supports shallow listing (getFolderListLocalShallow); it
209+
// just was never requested here. parent='root' = top level; drill into a subfolder by passing parent=<path>.
210+
const sid = sourceId || getFolderAccessSourceId() || '';
211+
const p = parent || 'root';
212+
const cacheKey = sid + '::' + p;
213+
if (!force && __allFoldersCache.has(cacheKey)) return __allFoldersCache.get(cacheKey).slice();
214+
215+
const url = '/api/folder/getFolderList.php?counts=0&folder=' + encodeURIComponent(p) + '&ts=' + Date.now()
216+
+ (sid ? `&sourceId=${encodeURIComponent(sid)}` : '');
211217
const res = await fetch(url, {
212218
credentials: 'include',
213219
cache: 'no-store',
@@ -223,7 +229,7 @@ async function getAllFolders(force = false, sourceId = "") {
223229
.filter(f => f && !hidden.has(f.toLowerCase()))
224230
.sort((a, b) => (a === 'root' ? -1 : b === 'root' ? 1 : a.localeCompare(b)));
225231

226-
__allFoldersCache.set(key, cleaned);
232+
__allFoldersCache.set(cacheKey, cleaned);
227233
return cleaned.slice();
228234
}
229235

@@ -239,7 +245,7 @@ async function getUserGrants(username, sourceId = "") {
239245
return (grants && typeof grants === 'object' && !Array.isArray(grants)) ? grants : {};
240246
}
241247

242-
function renderFolderGrantsUI(principal, container, folders, grants) {
248+
function renderFolderGrantsUI(principal, container, folders, grants, parent = 'root', sourceId = '') {
243249
if (!Array.isArray(folders) || !container) return;
244250

245251
const grantsMap = (grants && typeof grants === 'object' && !Array.isArray(grants)) ? grants : {};
@@ -301,6 +307,45 @@ function renderFolderGrantsUI(principal, container, folders, grants) {
301307

302308
container.innerHTML = '';
303309
container.appendChild(toolbar);
310+
311+
// Drill-in into subfolders: load one level at a time (shallow). getAllFolders takes a parent arg.
312+
const __sid = sourceId || (typeof getFolderAccessSourceId === 'function' ? getFolderAccessSourceId() : '') || '';
313+
const __parent = parent || 'root';
314+
async function navigateFolderAccess(newParent) {
315+
// Collect in-progress edits into the fallback map before navigating so they survive the
316+
// re-render (save is batched via collectGrantsFrom).
317+
const acc = collectGrantsFrom(container, container.__grantsFallback || {});
318+
let f = [];
319+
try { f = await getAllFolders(true, __sid, newParent || 'root'); } catch (e) { f = []; }
320+
renderFolderGrantsUI(principal, container, f, acc, newParent || 'root', __sid);
321+
}
322+
const crumb = document.createElement('div');
323+
crumb.className = 'folder-access-breadcrumb';
324+
crumb.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin:4px 0 8px;font-size:13px;';
325+
(() => {
326+
const mkLink = (label, target) => {
327+
const a = document.createElement('a');
328+
a.href = '#'; a.textContent = label;
329+
a.style.cssText = 'cursor:pointer;text-decoration:underline;';
330+
a.addEventListener('click', (e) => { e.preventDefault(); navigateFolderAccess(target); });
331+
return a;
332+
};
333+
crumb.appendChild(mkLink(tf('root_folder', 'root') + ' /', 'root'));
334+
const parts = (__parent === 'root') ? [] : __parent.split('/').filter(Boolean);
335+
let accPath = '';
336+
parts.forEach((seg, i) => {
337+
accPath = accPath ? (accPath + '/' + seg) : seg;
338+
const sep = document.createElement('span'); sep.textContent = '›'; sep.style.opacity = '0.5';
339+
crumb.appendChild(sep);
340+
if (i === parts.length - 1) {
341+
const cur = document.createElement('strong'); cur.textContent = seg;
342+
crumb.appendChild(cur);
343+
} else {
344+
crumb.appendChild(mkLink(seg, accPath));
345+
}
346+
});
347+
})();
348+
container.appendChild(crumb);
304349
container.appendChild(list);
305350

306351
const rowHtml = (folder, idx) => {
@@ -330,7 +375,7 @@ function renderFolderGrantsUI(principal, container, folders, grants) {
330375
g.shareFolder = g.shareFolder !== false;
331376
}
332377

333-
const name = folder === "root" ? `${t("root_folder")} /` : folder;
378+
const name = folder === "root" ? `${t("root_folder")} /` : folder.split('/').pop();
334379
const shareFolderDisabled = isAdmin ? true : undefined;
335380

336381
const toggle = (cap, label, checked, disabled, title = "") => `
@@ -356,6 +401,7 @@ function renderFolderGrantsUI(principal, container, folders, grants) {
356401
<div class="folder-badge">
357402
<i class="material-icons" style="font-size:18px;">folder</i>
358403
<span class="folder-name-text">${name}</span>
404+
${(folder !== 'root' && folder !== __parent) ? `<button type="button" class="fa-drill-btn" data-drill="${folder}" title="${tf('open_subfolders', 'Open subfolders')}" style="margin-left:6px;border:none;background:transparent;cursor:pointer;padding:0;opacity:0.7;display:inline-flex;align-items:center;"><i class="material-icons" style="font-size:18px;">chevron_right</i></button>` : ''}
359405
<span class="inherited-tag" style="display:none;"></span>
360406
<span class="inherit-flag-note pill-note" style="display:none;"></span>
361407
<span class="group-flag-note pill-note" style="display:none;"></span>
@@ -546,6 +592,13 @@ function renderFolderGrantsUI(principal, container, folders, grants) {
546592
}
547593

548594
function wireRow(row) {
595+
const drillBtn = row.querySelector('.fa-drill-btn');
596+
if (drillBtn) {
597+
drillBtn.addEventListener('click', (e) => {
598+
e.preventDefault(); e.stopPropagation();
599+
navigateFolderAccess(drillBtn.dataset.drill);
600+
});
601+
}
549602
const isAdminRow = row.dataset.admin === '1';
550603
const cbView = row.querySelector('input[data-cap="view"]');
551604
const cbViewOwn = row.querySelector('input[data-cap="viewOwn"]');

0 commit comments

Comments
 (0)