refactor(file-browser): Overhaul navigation state, directory caching, async rendering, and parent navigation - #2500
Conversation
Greptile SummaryThis PR refactors the file browser module across six commits, replacing scattered inline state and blocking modal loaders with a decoupled
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant handleClick
participant navigate
participant NavStack
participant renderCurrentDir
participant getDirList
participant DOM
User->>handleClick: click tile / prevDir
handleClick->>navigate: navigate(url, name)
navigate->>NavStack: has(url)?
alt URL already in stack
NavStack-->>navigate: true
navigate->>NavStack: popUntil(url)
NavStack-->>navigate: queues microtask "update"
else New URL
NavStack-->>navigate: false
navigate->>NavStack: push(url, name)
NavStack-->>navigate: queues microtask "update"
end
navigate->>renderCurrentDir: renderCurrentDir() [no await]
renderCurrentDir->>renderCurrentDir: abort previous _rndrAbortCtrl
renderCurrentDir->>DOM: "remove old #list, show placeholder spinner"
renderCurrentDir->>getDirList: await getDirList(url)
Note over getDirList: Promise.race(lsDir, 15s timeout)
getDirList-->>renderCurrentDir: list[]
Note over renderCurrentDir: microtask fires here → navbar updated
renderCurrentDir->>DOM: placeholder.replaceWith($list)
renderCurrentDir->>NavStack(cache): cachedDir.set(url, dir)
Reviews (26): Last reviewed commit: "feat(file-browser): Add parent directory..." | Re-trigger Greptile |
| case "oneDirUp": { | ||
| const dir = navStack.get(-2); | ||
| if (!dir) break; | ||
| const { url, name } = dir; | ||
| navigate(url, name); | ||
| } |
There was a problem hiding this comment.
.. resolves to navigation-history parent, not the filesystem parent
navStack.get(-2) returns the previously-visited directory, not the actual URL-parent of the current directory. These are the same in linear navigation, but diverge in edge cases — e.g. if a future feature adds bookmarks or deep-links that push multiple levels to navStack at once (like loadStates already does). In that scenario pressing .. could land on a directory that is not an ancestor of the current one at all. The traditional expected behaviour of .. is Url.dirname(currentDir.url). Consider adding a clarifying comment or computing the real parent as a fallback.
| case "oneDirUp": { | ||
| const dir = navStack.get(-2); | ||
| if (!dir) break; | ||
| const { url, name } = dir; | ||
| navigate(url, name); | ||
| } |
There was a problem hiding this comment.
Missing
break at end of oneDirUp case
The oneDirUp block has no trailing break. While this is currently safe because it is the last case, future additions to the switch will silently fall through into the new case without any visible indication that the omission is intentional. Adding break makes the intent explicit and future-proof.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
682762f to
b7687ee
Compare
This comment was marked as outdated.
This comment was marked as outdated.
08ccd4c to
105dc80
Compare
003df66 to
b513169
Compare
This comment was marked as outdated.
This comment was marked as outdated.
b513169 to
8bb4dc0
Compare
This comment has been minimized.
This comment has been minimized.
NavStack and implement parent directory navigation8bb4dc0 to
d8e4881
Compare
| if (checkbox) { | ||
| checkbox.checked = checked; | ||
| const url = item.querySelector("data-url").textContent; | ||
| if (checked) { | ||
| selectedItems.add(url); | ||
| } else { | ||
| selectedItems.delete(url); | ||
| } | ||
| selectedItems[checked ? "add" : "delete"](url); | ||
| } | ||
| }); | ||
| updateSelectionCount($count); |
There was a problem hiding this comment.
"Select All" does not clear phantom URLs on deselect
When checked = false (deselect all), selectedItems.delete(url) is called only for items that have a rendered checkbox. Any URL already in selectedItems before "Select All" was clicked is not removed, leaving phantom entries that pollute the next delete/copy/compress batch operation. A selectedItems.clear() before the deselect loop would make the deselect path symmetric with the select path.
96bbb2e to
e640f65
Compare
1cfe31e to
80872d4
Compare
|
@greptile there's already |
|
You're right that That's exactly the gap: the JS side sets the flag, but the template doesn't output it, so any selection-mode guard that reads The fix is a one-liner in With that added, |
5a913dd to
b561ec5
Compare
788df88 to
7717fb2
Compare
7717fb2 to
e3be768
Compare
|
@greptile review this, and the updated PR description, especially the dependence on yet another PR. |
|
Thanks for the heads up! I'll consider that concern resolved via #2725 then. No further action needed on this PR for that issue. |
|
Why there is need of skeleton loading on file browser ? |
Extract the SVG generator logic for the `tailSpin` loader into a dedicated, reusable component. Create `tailSpin` component (`src/components/tailSpin.js`): - Export `createTailSpinSvg` helper function - Generate unique gradient IDs dynamically to prevent SVG gradient collisions across multiple loader instances - Explicitly set function `name` property on exported SVG creator Refactor loader dialog (`src/dialogs/loader.js`): - Import `createTailSpinSvg` from component path - Remove redundant local `createTailSpinSvg` implementation and SVG gradient tracking logic (AI generated commit message)
e3be768 to
7972ceb
Compare
… management Abstract navigation tracking and event dispatching out of inline file browser code into a dedicated `EventTarget` class. Create `NavStack` class (`src/pages/fileBrowser/NavStack.js`): - Implement `NavStack` class extending `EventTarget` with custom `Symbol.toStringTag` - Add `push`, `pop`, `popUntil`, `get`, `has`, and `toJSON` methods with parameter validation - Queue microtasks for update event dispatching with added and removed location tracking Integrate `NavStack` into file browser (`src/pages/fileBrowser/fileBrowser.js`): - Replace manual state arrays and direct `localStorage` updates with `NavStack` instance - Listen to `update` events on `NavStack` to manage `actionStack` entries and navbar UI elements - Refactor `navigate` and `loadStates` functions to use `NavStack` methods (AI generated commit message)
Replace the plain object container used for cached directories with an ES6 `Map` to improve key lookup operations and key management semantics. Update cached directory data structure (`src/pages/fileBrowser/fileBrowser.js`): - Re-initialize `cachedDir` variable as a `Map` - Replace object property lookups with `Map.prototype.has()` and `Map.prototype.get()` - Update cache writes to use `Map.prototype.set()` - Update directory deletion calls to use `Map.prototype.delete()` (AI generated commit message)
…nline spinner Transition directory loading from blocking modal dialogs to inline loading state indicators with explicit timeout handling. Add async directory list fetching and rendering (`src/pages/fileBrowser/fileBrowser.js`): - Extract list retrieval into `getDirList` using `Promise.withResolvers` and `Promise.race` with a 15-second timeout - Replace render function with `renderCurrentDir` to handle asynchronous state changes - Render inline SVG spinner placeholder in place of missing list content - Maintain scroll position across directory re-renders Style inline spinner (`src/pages/fileBrowser/fileBrowser.scss`): - Add flexbox alignment styles for `#spinner` inside directory list container (AI generated commit message)
…ange or page hide Introduce `AbortController` tracking to cancel pending directory listing render tasks when navigating quickly or hiding the page. Add render abort controller logic (`src/pages/fileBrowser/fileBrowser.js`): - Instantiate `AbortController` instance inside `renderCurrentDir` - Abort existing controller prior to running new render sequence - Validate `abortSignal` before committing directory list DOM updates - Call `abort` on active render controller when `$page.onhide` fires (AI generated commit message)
Add a dedicated '..' list tile at the top of directory listings to provide explicit one-level-up navigation.
Update list template (`src/pages/fileBrowser/list.hbs`):
- Render parent directory tile ('..') when `prevDir` flag is active
- Adjust handlebars conditionals for message displays and item list loop
Handle `prevDir` action in file browser (`src/pages/fileBrowser/fileBrowser.js`):
- Add `prevDir` action handler navigating to `navStack.get(-2)`
- Pass `prevDir` condition check (`navStack.length >= 2`) into list render template
- Ignore context menu actions triggered on the `prevDir` item
Adjust layout styling (`src/pages/fileBrowser/fileBrowser.scss`):
- Adjust height calculation for message and spinner containers when `prevDir` item is visible
(AI generated commit message)
7972ceb to
ee00d08
Compare
|
@greptile there's already a |
|
You're right — the |
@greptile somewhere in #2500 (comment) |
|
Now I have the full picture. The original concern from the summary was about the null-destructure crash in The refactored while (states.length) {
try {
navStack.push(states.shift());
} catch (err) {
console.error(err);
}
}The |
|
You're right, and I can confirm it directly from the file. The function loadStates(states) {
if (!Array.isArray(states) || !states.length) return;
while (states.length) {
try {
navStack.push(states.shift());
} catch (err) {
console.error(err);
}
}
const dir = navStack.get(-1);
if (dir) navigate(dir);
}The |
|
You're right — the The catch only does |
Executive Summary
This Pull Request delivers a comprehensive architectural overhaul, performance refactoring, state management modernization, and user experience enhancement for the application's central File Browser module (
src/pages/fileBrowser/) and SVG loader dialog components (src/components/,src/dialogs/). Across 6 strategic commits, the codebase transitions from legacy inline state mutation patterns, plain object caching, and blocking modal dialogs to a modern, decoupled, event-driven architecture built around an isolatedNavStackclass (EventTarget), ES6Mapdirectory caching, non-blocking asynchronous list rendering with inline SVG spinners, and race-safeAbortControllertask cancellation.External Dependencies
tailSpinSVG component and dynamic gradient logic #2776 (commitd5d85a7e7ae5ef18694d80d1c545a545fc2c42c6), which extracts thetailSpinloader SVG creator into a dedicated component (src/components/tailSpin.js) and may need to be merged first.Key Objectives Achieved
tailSpinloader intosrc/components/tailSpin.js, adding dynamic gradient ID generation (tail-spin-gradient-${id}) to prevent SVG gradient collisions across multiple concurrent loader instances.NavStack): Replaced manual inline state arrays and directlocalStorageupdates with an event-drivenNavStackclass extendingEventTarget. Navigation events queue microtasks to notify listeners withaddedandremovedpath tracking, seamlessly drivingactionStackentries and breadcrumb UI elements.cachedDir = {}) to an ES6Mapinstance (cachedDir = new Map()), leveraginghas(),get(),set(), anddelete()methods for cleaner key management semantics and improved lookup performance.renderCurrentDir(), utilizingPromise.withResolvers()andPromise.race()with a 15-second timeout guard to display an inline SVG spinner placeholder while keeping the interface responsive.AbortController: IntroducedAbortControllertracking (_rndrAbortCtrl) within directory rendering sequences to cancel obsolete in-flight directory reads upon rapid navigation path changes or when$page.onhidetriggers...parent directory tile (data-action="prevDir") at the top of directory listings whenever the navigation stack depth supports upward navigation (navStack.length >= 2).High-Level Architecture Comparison
src/dialogs/loader.js.src/components/tailSpin.jshelper module with dynamic gradient ID generation and explicit function naming.state = []) and directlocalStorage/actionStackcalls scattered across file browser methods.NavStackclass extending standardEventTargetemitting asynchronous"update"microtask events.cachedDir = {}) utilizinginlookups anddeleteoperations.Mapinstance (cachedDir = new Map()) using nativehas(),get(),set(), anddelete()methods.loader.create()) that frozen user interaction during long filesystem reads.renderCurrentDir) displaying an SVGtailSpinspinner inside#spinnerwith a 15s timeout.AbortControllerper render task; obsolete tasks are immediately cancelled via.abort()on navigation or page hide...parent directory tile (data-action="prevDir") prepended at the top of listings whennavStack.length >= 2.Subsystem Architectural Breakdown
1. SVG Loader Component Extraction (
src/components/tailSpin.js)To support inline spinner rendering across UI modules without duplicating code or creating DOM gradient ID conflicts, the SVG generation logic for
tailSpinwas moved into a dedicated component:tailSpinSvgIdand replaces standardtail-spin-gradientstrings with dynamic IDs (tail-spin-gradient-0,tail-spin-gradient-1) usingreplaceAll(), preventing gradient rendering collisions when multiple spinners exist in the DOM.nameproperty oncreateTailSpinSvgviaObject.definePropertyfor consistent stack trace inspection.2. Event-Driven Navigation Stack (
NavStack)The file browser's path history and state tracking are decoupled into
src/pages/fileBrowser/NavStack.js:EventTargetand setsSymbol.toStringTagto"NavStack".#urlSet(Set<string>) for#arr(Array<Location>) for ordered stack depth management.push(),pop(), andpopUntil()collect path changes in a private#updatedURLsstructure and schedule a singleCustomEvent("update")usingqueueMicrotask().fileBrowser.jssubscribes to the"update"event to automatically updatelocalStorage.fileBrowserState, syncactionStackpush/remove commands, and push breadcrumb items to$navigation.3. ES6 Map Directory Cache
Refactored directory list caching from plain objects to an ES6
Mapcontainer (cachedDir):if (url in cachedDir)checks withcachedDir.has(url).cachedDir.get(url)andcachedDir.set(url, dir).delete cachedDir[url]statements withcachedDir.delete(url)during cache invalidation and directory reloads.4. Non-Blocking Async Rendering & Inline Spinner Lifecycle
Replaced blocking modal loader dialogs with non-blocking asynchronous directory rendering:
getDirList(url)Pipeline: Wraps filesystem calls (lsDir()) withPromise.withResolvers()andPromise.race()to enforce a strict 15-second (15000ms) loading timeout.renderCurrentDir()appends a temporary.placeholderelement containing<span id="spinner">${createTailSpinSvg()}</span>into$content.scrollTopon$oldListbefore removal and restoresscrollToponce directory DOM elements are appended.5. Concurrency Control & Render Cancellation (
AbortController)To prevent race conditions during rapid directory switching:
renderCurrentDir()instantiates a freshAbortController(rndrAbortCtrl) and aborts any active prior controller_rndrAbortCtrl?.abort().abortSignal.aborted.$page.onhideexecutes (e.g., navigating away or closing the file browser),_rndrAbortCtrl?.abort()is invoked immediately to cancel pending async directory operations.6. Parent Directory Traversal Tile (
list.hbs)Restored explicit parent directory traversal in the main item list:
list.hbsconditionally renders a<li class="tile" data-action="prevDir" data-not-selectable>tile with standard..text whenprevDirevaluates totrue.renderCurrentDir()checksnavStack.length >= 2to passprevDir: true...tile triggersnavStack.get(-2)and navigates to the parent directory. Context menu events onprevDiritems are explicitly ignored.:has(> [data-action="prevDir"])to automatically adjust empty folder messages and inline spinner container heights (height: calc(100% - 45px)) when the parent tile is visible.Detailed Commit Breakdown
Commit 1:
d5d85a7e7ae5ef18694d80d1c545a545fc2c42c6src/components/tailSpin.jssrc/dialogs/loader.jssrc/components/tailSpin.jsexporting defaultcreateTailSpinSvghelper.split().join()string replacement withreplaceAll()to inject dynamic unique gradient IDs (tail-spin-gradient-${tailSpinSvgId++}).Object.defineProperty(createTailSpinSvg, "name", { value: "createTailSpinSvg" }).src/dialogs/loader.js.tailSpinSVG component and dynamic gradient logic #2776.Commit 2:
337d4e64216a1ac23336da86929843d9460533absrc/pages/fileBrowser/NavStack.jssrc/pages/fileBrowser/fileBrowser.jsEventTargetsubclass that standardizes navigation pushes, pops, and microtask update event emissions.NavStackclass inheriting fromEventTargetwith customSymbol.toStringTag.#urlSet(Set) and#arr(Array<Location>) fields with full parameter validation onpush,pop,popUntil,get,has, andtoJSON.queueMicrotask) to emit"update"events withaddedandremovedlocation tracking.fileBrowser.jsto replace direct array mutations andlocalStoragewrites withNavStackinstance subscriptions.Commit 3:
63ce3b6c584ce0cd753418becc0b8dc5d704b3dasrc/pages/fileBrowser/fileBrowser.jsMapinstance to improve cache operation semantics and lookup performance.cachedDir = new Map().url in cachedDir) tocachedDir.has(url)andcachedDir.get(url).cachedDir.set(url, dir).delete cachedDir[url]) withcachedDir.delete(url)calls across reload and deletion handlers.Commit 4:
d77b129acac643bb707bc9f1ce00de6997de9383src/pages/fileBrowser/fileBrowser.js,src/pages/fileBrowser/fileBrowser.scssgetDirList(url)usingPromise.withResolvers()andPromise.race()with a 15-second (15000ms) timeout guard.renderCurrentDir(force)to replace legacy synchronousrenderfunction.<span id="spinner">${createTailSpinSvg()}</span>during active directory fetches.scrollTop) across directory re-renders.#spinnerinfileBrowser.scss.Commit 5:
9621a0c69b0e63107528ac6c39797258741ba19fsrc/pages/fileBrowser/fileBrowser.js_rndrAbortCtrl(AbortController) insiderenderCurrentDir._rndrAbortCtrl?.abort()before beginning a new rendering operation.abortSignal.abortedstatus prior to committing DOM updates._rndrAbortCtrl?.abort()to the$page.onhideevent handler to cancel pending fetches when hiding the file browser.Commit 6:
7972cebee07635bc11448602f9a2b09c7c4e7169src/pages/fileBrowser/fileBrowser.js,src/pages/fileBrowser/fileBrowser.scss,src/pages/fileBrowser/list.hbs..) directly inside the list view.list.hbstemplate to render parent directory tile (data-action="prevDir") whenprevDircondition is active.navStack.length >= 2to passprevDirflag into list template rendering.prevDiraction handler to navigate directly tonavStack.get(-2).prevDirtiles.:has(> [data-action="prevDir"])to adjust container height calculations for empty messages and spinners.Mathematical Performance & Complexity Analysis
1. Asynchronous Directory Fetching Guard
Let$T_{\text{lsDir}}$ denote the total asynchronous I/O execution latency for reading a directory listing across local storage, SAF Content URIs, FTP, or SFTP protocols, and let $T_{\text{guard}} = 15,000\text{ms}$ .
The race condition pipeline bounds latency according to:
$$T_{\text{fetch}} = \min(T_{\text{lsDir}}, T_{\text{guard}})$$
In network-constrained or unresponsive server conditions, execution is guaranteed to reject and exit within$T_{\text{guard}}$ ($15\text{s}$ ), preventing UI hangs or unresolved modal loaders.
2. Time & Space Complexity Comparisons
NavStackURL Lookuphas(url)Set.prototype.has)NavStackMutationpush(url)Set+Arraypush)has(url)/get(url)Map.prototype.get)delete obj[key])Map.prototype.delete)AbortController.abort()* Note: Plain JavaScript object lookups incur prototype chain resolution overhead and key stringification costs that are eliminated by using standard ES6
Mapkeys.Testing Plan & Quality Assurance Matrix
1. Unit & Structural Verification
createTailSpinSvg()produces unique SVG gradient IDs (tail-spin-gradient-0,tail-spin-gradient-1) across consecutive invocations.NavStackClass: Verifiedpush(),pop(),popUntil(),get(), andhas()behavior, ensuring parameter type checking throws explicitTypeErrorinstances on invalid inputs.MapCache Store: Verified directory entries correctly set, hit, and delete fromcachedDirwithout retaining stale references.2. Integration & Edge Case Scenarios
AbortControllercancels pending fetches; active view renders correct final directory without state leakage...tile at the top of a nested folder listing.navStack.get(-2))...tile.$page.onhidetriggers_rndrAbortCtrl.abort(), canceling pending renders cleanly.tailSpinSVG spinner renders inside list view without blocking UI dialogs.Migration & Compatibility Considerations
Backwards Compatibility & Dependencies
tailSpinSVG component and dynamic gradient logic #2776 (d5d85a7e7ae5ef18694d80d1c545a545fc2c42c6) forcreateTailSpinSvghelper component availability.fileBrowser.jsremain fully compatible with existing router mounts.NavStackevent detail structures emit standardCustomEventobjects compatible with DOMEventTargetlisteners.Conclusion
This pull request significantly modernizes the
fileBrowsersubsystem by introducing event-driven navigation history tracking, race-safe rendering pipelines withAbortController, ES6Mapcaching, reusable loader SVG components, and explicit parent directory traversal controls.(PR name and description are AI generated (Gemini 3.6 Flash))