Skip to content

Commit 052391f

Browse files
authored
release(v3.1.7): fix table header select-all checkbox + Pro bundle install progress UI (closes #99)
- file list fix header select-all checkbox robust click handling + sync state - file list preserve file selections when table re-renders after folder strip loads - admin show transfer progress for Pro bundle upload/download install actions Closes #99
1 parent af53be4 commit 052391f

4 files changed

Lines changed: 179 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
11
# Changelog
22

3+
## Changes 01/24/2026 (v3.1.7)
4+
5+
`release(v3.1.7): fix table header select-all checkbox + Pro bundle install progress UI (closes #99)`
6+
7+
**Commit message**
8+
9+
```text
10+
release(v3.1.7): fix table header select-all checkbox + Pro bundle install progress UI (closes #99)
11+
12+
- file list fix header select-all checkbox robust click handling + sync state
13+
- file list preserve file selections when table re-renders after folder strip loads
14+
- admin show transfer progress for Pro bundle upload/download install actions
15+
16+
Closes #99
17+
```
18+
19+
**Fixed**
20+
21+
- **#99:** The checkbox left of the **Name** column now correctly toggles “select all” in table view.
22+
- Uses a stable selector (`.select-all` + `data-select-all`) and robust click handling (checkbox + header cell click).
23+
- Keeps the header checkbox state synced (checked/indeterminate) as individual rows change.
24+
- Excludes folder rows from file “select all” so only file rows are toggled.
25+
26+
**Changed**
27+
28+
- **Selection preservation on table refresh**
29+
- When subfolders are loaded and the table view re-renders (inline folders above files), existing file selections are preserved.
30+
- **Pro bundle install UX**
31+
- Admin “Upload Pro bundle” and “Download latest Pro bundle” actions now use the existing transfer progress UI (minimizable card) and surface success/failure cleanly.
32+
33+
---
34+
335
## Changes 01/24/2026 (v3.1.5 & v3.1.6)
436

537
`release(v3.1.6): CodeQL fix for error handling (strip HTML safely in fileActions)`

public/js/adminPanel.js

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
44
import { showToast, toggleVisibility, attachEnterKeyListener, escapeHTML } from './domUtils.js?v={{APP_QVER}}';
55
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
66
import { withBase } from './basePath.js?v={{APP_QVER}}';
7+
import { startTransferProgress, finishTransferProgress } from './transferProgress.js?v={{APP_QVER}}';
78
import { initAdminStorageSection } from './adminStorage.js?v={{APP_QVER}}';
89
import { initAdminSponsorSection } from './adminSponsor.js?v={{APP_QVER}}';
910
import { initOnlyOfficeUI, collectOnlyOfficeSettingsForSave } from './adminOnlyOffice.js?v={{APP_QVER}}';
@@ -63,6 +64,21 @@ function compareSemver(a, b) {
6364
return 0;
6465
}
6566

67+
function startProBundleProgress({ action = 'Updating Pro', title, subText } = {}) {
68+
return startTransferProgress({
69+
action,
70+
title: title || action,
71+
subText: subText || 'Please keep this tab open.',
72+
itemCount: 0,
73+
bytesKnown: false
74+
});
75+
}
76+
77+
function finishProBundleProgress(job, ok, error = '') {
78+
if (!job) return;
79+
finishTransferProgress(job, { ok, error });
80+
}
81+
6682
// Ensure OIDC config object always exists
6783
if (!window.currentOIDCConfig || typeof window.currentOIDCConfig !== 'object') {
6884
window.currentOIDCConfig = {};
@@ -3030,6 +3046,10 @@ export function initProBundleInstaller(options = {}) {
30303046

30313047
statusEl.textContent = 'Uploading and installing Pro bundle...';
30323048
statusEl.className = 'small text-muted';
3049+
const progress = startProBundleProgress({
3050+
action: 'Installing Pro',
3051+
title: 'Installing FileRise Pro bundle'
3052+
});
30333053

30343054
try {
30353055
const resp = await fetch('/api/admin/installProBundle.php', {
@@ -3053,6 +3073,7 @@ export function initProBundleInstaller(options = {}) {
30533073
: `HTTP ${resp.status}`;
30543074
statusEl.textContent = 'Install failed: ' + msg;
30553075
statusEl.className = 'small text-danger';
3076+
finishProBundleProgress(progress, false, msg);
30563077
return;
30573078
}
30583079

@@ -3081,6 +3102,7 @@ export function initProBundleInstaller(options = {}) {
30813102
const versionText = finalVersion ? ` (version ${finalVersion})` : '';
30823103
statusEl.textContent = 'Pro bundle installed' + versionText + '. Reload the page to apply changes.';
30833104
statusEl.className = 'small text-success';
3105+
finishProBundleProgress(progress, true);
30843106

30853107
// Clear file input so repeat installs feel "fresh"
30863108
try { fileInput.value = ''; } catch (_) { }
@@ -3093,8 +3115,10 @@ export function initProBundleInstaller(options = {}) {
30933115
window.location.reload();
30943116
}, 800);
30953117
} catch (e) {
3096-
statusEl.textContent = 'Install failed: ' + (e && e.message ? e.message : String(e));
3118+
const errMsg = e && e.message ? e.message : String(e);
3119+
statusEl.textContent = 'Install failed: ' + errMsg;
30973120
statusEl.className = 'small text-danger';
3121+
finishProBundleProgress(progress, false, errMsg);
30983122
}
30993123
});
31003124

@@ -3115,6 +3139,10 @@ export function initProBundleInstaller(options = {}) {
31153139
dlBtn.disabled = true;
31163140
statusEl.textContent = 'Downloading and installing latest Pro bundle...';
31173141
statusEl.className = 'small text-muted';
3142+
const progress = startProBundleProgress({
3143+
action: 'Updating Pro',
3144+
title: 'Downloading and installing FileRise Pro bundle'
3145+
});
31183146

31193147
try {
31203148
const resp = await fetch('/api/admin/downloadProBundle.php', {
@@ -3138,27 +3166,31 @@ export function initProBundleInstaller(options = {}) {
31383166
: `HTTP ${resp.status}`;
31393167
statusEl.textContent = 'Download/install failed: ' + msg;
31403168
statusEl.className = 'small text-danger';
3169+
finishProBundleProgress(progress, false, msg);
31413170
return;
31423171
}
31433172

31443173
const finalVersion = data.proVersion ? String(data.proVersion) : '';
31453174
const versionText = finalVersion ? ` (version ${finalVersion})` : '';
31463175
statusEl.textContent = 'Pro bundle installed' + versionText + '. Reloading...';
31473176
statusEl.className = 'small text-success';
3177+
finishProBundleProgress(progress, true);
31483178

31493179
if (typeof loadAdminConfigFunc === 'function') {
31503180
loadAdminConfigFunc();
31513181
}
31523182
setTimeout(() => {
3153-
window.location.reload();
3154-
}, 800);
3155-
} catch (e) {
3156-
statusEl.textContent = 'Download/install failed: ' + (e && e.message ? e.message : String(e));
3157-
statusEl.className = 'small text-danger';
3158-
} finally {
3159-
dlBtn.disabled = false;
3160-
}
3161-
});
3183+
window.location.reload();
3184+
}, 800);
3185+
} catch (e) {
3186+
const errMsg = e && e.message ? e.message : String(e);
3187+
statusEl.textContent = 'Download/install failed: ' + errMsg;
3188+
statusEl.className = 'small text-danger';
3189+
finishProBundleProgress(progress, false, errMsg);
3190+
} finally {
3191+
dlBtn.disabled = false;
3192+
}
3193+
});
31623194
}
31633195
} catch (e) {
31643196
console.warn('Failed to init Pro bundle installer', e);
@@ -5637,6 +5669,10 @@ ${t("shared_max_upload_size_bytes")}
56375669
);
56385670
if (ok) {
56395671
setStatus('Downloading and installing latest Pro bundle...', 'muted');
5672+
const progress = startProBundleProgress({
5673+
action: 'Updating Pro',
5674+
title: 'Downloading and installing FileRise Pro bundle'
5675+
});
56405676
try {
56415677
const resp = await fetch('/api/admin/downloadProBundle.php', {
56425678
method: 'POST',
@@ -5659,13 +5695,15 @@ ${t("shared_max_upload_size_bytes")}
56595695
: `HTTP ${resp.status}`;
56605696
setStatus('Download/install failed: ' + msg, 'danger');
56615697
showToast(t('admin_pro_install_failed_detail', { error: msg }), 'error');
5698+
finishProBundleProgress(progress, false, msg);
56625699
return;
56635700
}
56645701

56655702
const finalVersion = dlData.proVersion ? String(dlData.proVersion) : '';
56665703
const versionText = finalVersion ? ` (${finalVersion})` : '';
56675704
setStatus('Pro bundle installed' + versionText + '. Reloading...', 'success');
56685705
showToast(t('admin_pro_installed_reloading', { version: versionText }));
5706+
finishProBundleProgress(progress, true);
56695707
if (typeof loadAdminConfigFunc === 'function') {
56705708
loadAdminConfigFunc();
56715709
}
@@ -5674,8 +5712,10 @@ ${t("shared_max_upload_size_bytes")}
56745712
}, 800);
56755713
return;
56765714
} catch (e) {
5715+
const errMsg = e && e.message ? e.message : 'Download/install failed.';
56775716
setStatus('Download/install failed.', 'danger');
56785717
showToast(t('admin_pro_install_failed'), 'error');
5718+
finishProBundleProgress(progress, false, errMsg);
56795719
return;
56805720
}
56815721
}

public/js/domUtils.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,11 @@ export function updateFileActionButtons() {
121121
};
122122

123123
// — Select All checkbox sync (unchanged) —
124-
const master = document.getElementById("selectAll");
124+
const master = document.querySelector(
125+
'#fileList thead input[type="checkbox"][data-select-all], ' +
126+
'#fileList thead input[type="checkbox"].select-all, ' +
127+
'#fileList thead input#selectAll'
128+
);
125129
if (master) {
126130
if (anyFolderSelected) {
127131
master.disabled = false;
@@ -292,7 +296,7 @@ export function buildFileTableHeader(sortOrder) {
292296
<table class="table filr-table table-hover table-striped">
293297
<thead>
294298
<tr>
295-
<th class="checkbox-col"><input type="checkbox" id="selectAll"></th>
299+
<th class="checkbox-col"><input type="checkbox" class="select-all" data-select-all="1"></th>
296300
<th data-column="name" class="sortable-col">${t("name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
297301
<th data-column="modified" class="hide-small sortable-col">${t("modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
298302
<th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("created")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>

public/js/fileListView.js

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3146,26 +3146,32 @@ async function fetchFolderPeek(folder, sourceId = '') {
31463146
// Wire "select all" header checkbox for the current table render
31473147
function wireSelectAll(fileListContent) {
31483148
// Be flexible about how the header checkbox is identified
3149-
const selectAll = fileListContent.querySelector(
3149+
const getSelectAll = () => fileListContent.querySelector(
31503150
'thead input[type="checkbox"].select-all, ' +
31513151
'thead .select-all input[type="checkbox"], ' +
31523152
'thead input#selectAll, ' +
31533153
'thead input#selectAllCheckbox, ' +
31543154
'thead input[data-select-all]'
31553155
);
3156+
const selectAll = getSelectAll();
31563157
if (!selectAll) return;
3158+
if (selectAll.__wiredSelectAll) return;
3159+
selectAll.__wiredSelectAll = true;
31573160

31583161
const getRowCbs = () =>
31593162
Array.from(fileListContent.querySelectorAll('tbody .file-checkbox'))
3160-
.filter(cb => !cb.disabled);
3163+
.filter(cb => !cb.disabled && !cb.closest('tr.folder-row'));
31613164

3162-
// Toggle all rows when the header checkbox changes
3163-
selectAll.addEventListener('change', () => {
3164-
// Clear any folder selection when toggling all files
3165-
document.querySelectorAll('#fileList .folder-checkbox:checked').forEach(cb => {
3165+
const clearFolderSelections = () => {
3166+
fileListContent.querySelectorAll('tbody .folder-checkbox:checked').forEach(cb => {
31663167
cb.checked = false;
31673168
updateRowHighlight(cb);
31683169
});
3170+
};
3171+
3172+
const applySelectAll = () => {
3173+
// Clear any folder selection when toggling all files
3174+
clearFolderSelections();
31693175
const checked = selectAll.checked;
31703176
getRowCbs().forEach(cb => {
31713177
cb.checked = checked;
@@ -3174,28 +3180,69 @@ function wireSelectAll(fileListContent) {
31743180
updateFileActionButtons();
31753181
// No indeterminate state when explicitly toggled
31763182
selectAll.indeterminate = false;
3183+
selectAll.__lastSelectAllChecked = selectAll.checked;
3184+
};
3185+
3186+
const handleSelectAllChange = () => {
3187+
applySelectAll();
3188+
};
3189+
3190+
// Toggle all rows when the header checkbox changes
3191+
selectAll.addEventListener('change', handleSelectAllChange);
3192+
3193+
// Some UI layers can swallow left-click toggles. Track state and force toggle if needed.
3194+
selectAll.addEventListener('mousedown', (e) => {
3195+
if (e.button !== 0) return;
3196+
selectAll.__lastSelectAllChecked = selectAll.checked;
3197+
});
3198+
3199+
selectAll.addEventListener('click', (e) => {
3200+
if (e.button !== 0) return;
3201+
if (selectAll.disabled) return;
3202+
const last = selectAll.__lastSelectAllChecked;
3203+
if (last === selectAll.checked) {
3204+
selectAll.checked = !selectAll.checked;
3205+
handleSelectAllChange();
3206+
}
3207+
selectAll.__lastSelectAllChecked = selectAll.checked;
31773208
});
31783209

3210+
const headerCell = selectAll.closest('th');
3211+
if (headerCell && !headerCell.__wiredSelectAllCell) {
3212+
headerCell.__wiredSelectAllCell = true;
3213+
headerCell.addEventListener('click', (e) => {
3214+
if (e.target === selectAll) return;
3215+
if (selectAll.disabled) return;
3216+
selectAll.checked = !selectAll.checked;
3217+
handleSelectAllChange();
3218+
});
3219+
}
3220+
31793221
// Keep header checkbox state in sync with row selections
31803222
const syncHeader = () => {
3223+
const master = getSelectAll();
3224+
if (!master) return;
31813225
const cbs = getRowCbs();
31823226
const total = cbs.length;
31833227
const checked = cbs.filter(cb => cb.checked).length;
31843228
if (!total) {
3185-
selectAll.checked = false;
3186-
selectAll.indeterminate = false;
3229+
master.checked = false;
3230+
master.indeterminate = false;
31873231
return;
31883232
}
3189-
selectAll.checked = checked === total;
3190-
selectAll.indeterminate = checked > 0 && checked < total;
3233+
master.checked = checked === total;
3234+
master.indeterminate = checked > 0 && checked < total;
31913235
};
31923236

31933237
// Listen for any row checkbox changes to refresh header state
3194-
fileListContent.addEventListener('change', (e) => {
3195-
if (e.target && e.target.classList.contains('file-checkbox')) {
3196-
syncHeader();
3197-
}
3198-
});
3238+
if (!fileListContent.__wiredSelectAllContainer) {
3239+
fileListContent.__wiredSelectAllContainer = true;
3240+
fileListContent.addEventListener('change', (e) => {
3241+
if (e.target && e.target.classList.contains('file-checkbox')) {
3242+
syncHeader();
3243+
}
3244+
});
3245+
}
31993246

32003247
// Initial sync on mount
32013248
syncHeader();
@@ -6001,7 +6048,7 @@ export async function loadFileList(folderParam, options = {}) {
60016048

60026049
// Re-render table view once folders are known so they appear inline above files
60036050
if (window.viewMode === "table" && reqId === __fileListReqSeq[pane]) {
6004-
renderFileTable(folder);
6051+
renderFileTable(folder, fileListContainer, undefined, { preserveSelection: true });
60056052
}
60066053
}
60076054
} catch (e) {
@@ -6755,9 +6802,30 @@ async function openDefaultFileFromHover(file) {
67556802
*/
67566803

67576804

6758-
export async function renderFileTable(folder, container, subfolders) {
6805+
function getSelectedFileValuesForList(listEl) {
6806+
if (!listEl) return [];
6807+
return Array.from(listEl.querySelectorAll('tbody .file-checkbox:checked'))
6808+
.map(cb => cb.value);
6809+
}
6810+
6811+
function restoreSelectedFileValues(listEl, selectedValues) {
6812+
if (!listEl || !Array.isArray(selectedValues) || !selectedValues.length) return;
6813+
const selectedSet = new Set(selectedValues.map(v => String(v)));
6814+
listEl.querySelectorAll('tbody .file-checkbox').forEach(cb => {
6815+
const raw = cb.value;
6816+
const decoded = decodeHtmlEntities(raw);
6817+
if (selectedSet.has(raw) || (decoded && selectedSet.has(decoded))) {
6818+
cb.checked = true;
6819+
updateRowHighlight(cb);
6820+
}
6821+
});
6822+
}
6823+
6824+
export async function renderFileTable(folder, container, subfolders, options = {}) {
67596825
clearInlineRenameState({ restore: false });
67606826
const fileListContent = container || document.getElementById("fileList");
6827+
const preserveSelection = options && options.preserveSelection === true;
6828+
let preservedSelection = [];
67616829
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
67626830
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "50", 10);
67636831
let currentPage = window.currentPage || 1;
@@ -6785,6 +6853,11 @@ export async function renderFileTable(folder, container, subfolders) {
67856853
// NEW: sort folders according to current sort order (name / size)
67866854
const subfoldersSorted = await sortSubfoldersForCurrentOrder(allSubfolders);
67876855

6856+
if (preserveSelection) {
6857+
// Capture after async work so user selections during the wait aren't lost.
6858+
preservedSelection = getSelectedFileValuesForList(fileListContent);
6859+
}
6860+
67886861
const totalFiles = filteredFiles.length;
67896862
const totalFolders = subfoldersSorted.length;
67906863
const totalRows = totalFiles + totalFolders;
@@ -6894,6 +6967,7 @@ const subfoldersSorted = await sortSubfoldersForCurrentOrder(allSubfolders);
68946967

68956968
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
68966969
updatePaneWidthClasses();
6970+
restoreSelectedFileValues(fileListContent, preservedSelection);
68976971

68986972
(function rightAlignSizeColumn() {
68996973
const table = fileListContent.querySelector("table.filr-table");

0 commit comments

Comments
 (0)