From d00f6974717ba62e69fdf1da1767682428cffaa2 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sat, 18 Jul 2026 14:29:59 -0700 Subject: [PATCH] fix: Dashboard Files shift-click selection after navigation and byte formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shift-click selection: - renderDirectory cleared the .selected class but left window.latest_selected_item pointing at a now-detached row from the previous directory. The shift handler set shift_clicked=true, found the anchor's index as -1, skipped the range select, and onclick then early-returned on shift_clicked — so the first shift-click in a new directory selected nothing. Reset the anchor when it detaches, only take the range path when the anchor is still in the list, and treat a shift-click with no valid anchor as a plain select that becomes the anchor (onclick otherwise ignores clicks while Shift is held). formatFileSize: - Returned "NaN undefined" for missing/invalid sizes and "1.5 undefined" for terabyte-plus files (sizes array stopped at GB). Guard non-finite and non-positive input, clamp the unit index, and add TB/PB. Verified live: first shift-click selects the item, a second shift-click extends the range, and formatFileSize returns 0 B / 1.5 TB / 2 PB. --- src/gui/src/UI/Dashboard/TabFiles.js | 48 ++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js index 040ecbd8e..7aa1a2671 100644 --- a/src/gui/src/UI/Dashboard/TabFiles.js +++ b/src/gui/src/UI/Dashboard/TabFiles.js @@ -1834,6 +1834,14 @@ const TabFiles = { r.classList.remove('selected'); }); + // Drop the shift-click anchor — it points at a row from the directory + // we're leaving, and a stale detached anchor makes the first shift-click + // in the new directory select nothing. + if ( window.latest_selected_item && ! document.body.contains(window.latest_selected_item) ) { + window.latest_selected_item = null; + window.active_element = null; + } + // Determine whether target is a path or uid const isPath = typeof target === 'string' && target.startsWith('/'); const readdirArg = isPath @@ -2154,12 +2162,17 @@ const TabFiles = { return; } - // Handle Shift+Click for range selection - if ( e.shiftKey && window.latest_selected_item && window.latest_selected_item !== el_item ) { + // Handle Shift+Click for range selection. Require the anchor to + // still be in this row list — a detached anchor (indexOf === -1) + // would otherwise set shift_clicked and then select nothing, + // leaving the click a no-op. + const allShiftRows = $(el_item).parent().find('.row').toArray(); + if ( e.shiftKey && window.latest_selected_item && window.latest_selected_item !== el_item + && allShiftRows.indexOf(window.latest_selected_item) !== -1 ) { e.preventDefault(); shift_clicked = true; - const allRows = $(el_item).parent().find('.row').toArray(); + const allRows = allShiftRows; const clickedIndex = allRows.indexOf(el_item); const lastSelectedIndex = allRows.indexOf(window.latest_selected_item); @@ -2186,6 +2199,22 @@ const TabFiles = { _this.updateFooterStats(); return; } + } else if ( e.shiftKey && e.pointerType !== 'touch' ) { + // Shift-click with no valid anchor (e.g. the first click after + // navigating to a new directory): select just this item and make + // it the anchor. onclick skips selection while Shift is held, so + // without this the click would select nothing. + e.preventDefault(); + shift_clicked = true; + el_item.parentElement.querySelectorAll('.row.selected').forEach(r => { + r.classList.remove('selected'); + }); + el_item.classList.add('selected'); + window.latest_selected_item = el_item; + window.active_element = el_item; + window.active_item_container = el_item.closest('.files'); + _this.updateFooterStats(); + return; } // In select mode on mobile, treat taps like Ctrl+click (toggle selection) @@ -2846,11 +2875,16 @@ const TabFiles = { * @returns {string} Formatted size string (e.g., "1.5 MB") */ formatFileSize (bytes) { - if ( bytes === 0 ) return '0 B'; + const num = Number(bytes); + // Missing/invalid sizes (undefined, null, NaN) and non-positive values + // shouldn't render as "NaN undefined". + if ( ! Number.isFinite(num) || num <= 0 ) return '0 B'; const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100 } ${ sizes[i]}`; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + // Clamp the index so sizes beyond PB don't index past the array (which + // produced "1.5 undefined" for terabyte-plus files). + const i = Math.min(Math.floor(Math.log(num) / Math.log(k)), sizes.length - 1); + return `${Math.round((num / Math.pow(k, i)) * 100) / 100 } ${ sizes[i]}`; }, /**