mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-08 10:32:13 -03:00
feat(sidebar): scan a single folder from the folder context menu (#1108)
The Refresh menu can scope a scan to one model root, but the folder a user is
looking at lives in the sidebar's unified tree, which merges every root into one
relative-path namespace. "Scan this folder" therefore addresses the folder, not a
root: the backend walks that relative path under every root that holds it, which
is also what makes the action safe while another drive is switched off.
Backend:
* GET /scan accepts `folder=<rel>` (alone or with `roots=`), rejected together
with full_rebuild=true like the roots parameter. Validation reuses
normalize_relative_folder(), extracted to module scope from ModelMoveService so
the folder operations and the scan endpoint reject the same input (absolute
paths, drive letters, `..` climbing) instead of each carrying its own copy.
* The reconcile summary carries `scope_label` (the folder) for a folder scope, so
the result toast names the folder the user clicked instead of the roots it
happens to live under; the completed WS payload carries it too.
* `folder` is a scope prefix exactly like a root: only that subtree is re-read or
pruned, and an unreachable root keeps the entries that fall inside it.
Frontend:
* The sidebar folder context menu gains "Scan this folder" above "Check for
updates in this folder" (they share the refresh divider); the entry is gated by
the same supportsFolderManagement flag as the other folder operations.
* SidebarManager.scanFolder() resolves the node through the existing
_resolveFolderCandidates() before doing anything: a folder no root holds any
more explains itself ("no longer exists on disk") instead of scanning nothing,
and an unresolvable multi-root node is refused rather than guessed.
* PageControls.refreshModels() and BaseModelApi.refreshModels() forward the folder
scope, and _showRefreshSummary() prefers scope_label over the walked roots.
Verified live on the three-root sandbox with one drive switched off:
GET /scan?folder=pack000 walks drive-G and drive-Y, reports scope_label=pack000,
keeps drive-Z's 6 entries under that folder (kept_unreachable=6) and leaves all
420 models cached. 3704 passed, 7 skipped; frontend 1495 passed (150 files); vue
widgets 96 passed. The 2 new sidebar keys are [TODO: Translate] placeholders
pending the feature owner's go-ahead.
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
|
||||
**Issue:** [#1108](https://github.com/willmiao/ComfyUI-Lora-Manager/issues/1108) — scan a single
|
||||
folder/root instead of the whole library.
|
||||
**Status:** **P1 implemented** (2026-10-07, uncommitted working tree). P2 (folder scope, Wave 5)
|
||||
is not implemented. Supersedes the earlier draft's per-root *status panel* and persisted
|
||||
*unreachable-subtree* state (both dropped, see "Must NOT have").
|
||||
**Status:** **P1 and P2 implemented** (2026-10-07). P1 shipped in `470d85cc` (translations in
|
||||
`bd184559`); P2 (folder scope + the sidebar entry) is in the working tree. Supersedes the earlier
|
||||
draft's per-root *status panel* and persisted *unreachable-subtree* state (both dropped, see
|
||||
"Must NOT have").
|
||||
|
||||
Implementation notes / deviations from the draft below:
|
||||
|
||||
@@ -90,14 +91,20 @@ and in the exact pruning predicate; both get explicit tests.
|
||||
- i18n keys in `locales/en.json` + `python scripts/sync_translation_keys.py`.
|
||||
- pytest + vitest coverage for every item above.
|
||||
|
||||
### Must have (P2, optional follow-up)
|
||||
### Must have (P2, implemented)
|
||||
|
||||
- `folder=<rel>` parameter: walk `<root>/<rel>` for every reachable root that contains it (the
|
||||
sidebar's unified tree has no root identity, so "this folder" means "this relative path in all
|
||||
roots"), prune only inside that prefix.
|
||||
roots"), prune only inside that prefix. Validation reuses `normalize_relative_folder()` (extracted
|
||||
from `ModelMoveService` so the folder operations and the scan endpoint reject the same input:
|
||||
absolute paths, drive letters, `..` climbing). The summary carries `scope_label` = the folder, so
|
||||
the toast names the folder the user clicked rather than the roots it happens to live under.
|
||||
- Sidebar folder context menu entry "Scan this folder" (`templates/components/context_menu.html`
|
||||
`#sidebarFolderContextMenu`, above `check-folder-updates`; update
|
||||
`tests/frontend/regression/sidebarFolderContextMenu.test.js` expectations).
|
||||
`tests/frontend/regression/sidebarFolderContextMenu.test.js` expectations). The entry is gated
|
||||
like the other folder operations and resolves the folder through the existing
|
||||
`_resolveFolderCandidates()` before scanning, so a folder no root holds any more explains itself
|
||||
instead of scanning nothing. It shares `check-folder-updates`'s divider (both are refresh-ish).
|
||||
|
||||
### Must NOT have (guardrails)
|
||||
|
||||
@@ -250,11 +257,19 @@ and in the exact pruning predicate; both get explicit tests.
|
||||
(`LM_WALK_DELAY_S`-style `sitecustomize` hook) to eyeball the scoped progress line, the offline
|
||||
row and the toast — the user verifies by eye.
|
||||
|
||||
### Wave 5 — P2 (folder scope), only if approved
|
||||
### Wave 5 — P2 (folder scope) — done
|
||||
|
||||
14. `folder=<rel>` in the scan endpoint + `_ReconcileScope.folder`, the sidebar menu entry, the
|
||||
14. `folder=<rel>` in the scan endpoint + `ReconcileScope.folder`, the sidebar menu entry, the
|
||||
context-menu regression test update, and the "folder in several roots" semantics (scan every
|
||||
reachable root that has the relative path; report per root).
|
||||
* `normalize_relative_folder()` extracted to module scope in `model_file_service.py` (the
|
||||
private static method now delegates) and reused by the scan handler.
|
||||
* `scanFolder()` in `SidebarManager` reuses `_resolveFolderCandidatesSafe()` and delegates to the
|
||||
host page controls, which pass `{ folder }` through `registerAPI`'s argument-forwarding
|
||||
`refreshModels`.
|
||||
* Verified live: `folder=pack000` on the three-root sandbox walks drive-G and drive-Y, reports
|
||||
`scope_label=pack000`, keeps drive-Z's 6 entries under that folder (`kept_unreachable=6`) and
|
||||
leaves all 420 models cached.
|
||||
|
||||
## Known limitations (accepted)
|
||||
|
||||
@@ -275,9 +290,15 @@ and in the exact pruning predicate; both get explicit tests.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] `pytest -q` green, `npx vitest run` green, `npm run test:vue` green.
|
||||
- [ ] `python scripts/sync_translation_keys.py --dry-run` reports no pending changes.
|
||||
- [ ] Sandbox: 2 roots, one offline → scoped scan of the online root reports `added`, keeps the
|
||||
offline root's models, and the grid/sidebar still show them.
|
||||
- [ ] Sandbox: preview of an offline root's model returns 404 and the DB keeps `preview_url`.
|
||||
- [x] `pytest -q` green (3704 passed, 7 skipped), `npx vitest run` green (1495 passed),
|
||||
`npm run test:vue` green (96 passed).
|
||||
- [x] `python scripts/sync_translation_keys.py --dry-run` reports no pending changes.
|
||||
- [x] Sandbox: 3 roots, one offline → scoped scan of one root walks only it; a full refresh keeps
|
||||
the offline root's models (`kept_unreachable=60`) and the grid still shows all 420.
|
||||
- [x] Sandbox: folder scope (`folder=pack000`) walks the two reachable roots, labels the scan by the
|
||||
folder and keeps the offline root's 6 entries under it.
|
||||
- [ ] Sandbox: preview of an offline root's model returns 404 and the DB keeps `preview_url`
|
||||
(test-locked; not eyeballed in the sandbox because the demo models have no previews).
|
||||
- [ ] Release note wording agreed for the behaviour change (decision 1).
|
||||
- [ ] The 2 new sidebar keys (`sidebar.scanFolder`, `sidebar.scanFolderResult.missing`) are
|
||||
`[TODO: Translate]` placeholders pending the feature owner's go-ahead.
|
||||
|
||||
Reference in New Issue
Block a user