mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-08 02:22:12 -03:00
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.
47 lines
1.7 KiB
JavaScript
47 lines
1.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'fs';
|
|
import path from 'path';
|
|
|
|
// Regression guard for the sidebar folder context-menu layout: the two refresh
|
|
// actions (scan this folder, check for updates) sit on top, the folder
|
|
// operations form a single group, and the destructive entry stays last behind
|
|
// its own divider. SidebarManager gates those groups per page and collapses the
|
|
// dividers when a group is hidden, so a reorder here also changes what the
|
|
// recipes page shows.
|
|
describe('Sidebar folder context menu layout', () => {
|
|
const repoRoot = path.resolve(__dirname, '../../..');
|
|
const html = readFileSync(
|
|
path.join(repoRoot, 'templates/components/context_menu.html'),
|
|
'utf-8'
|
|
);
|
|
|
|
const menuHtml = html.slice(
|
|
html.indexOf('id="sidebarFolderContextMenu"'),
|
|
html.indexOf('<!-- Sidebar View Options Menu -->')
|
|
);
|
|
|
|
const sequence = [...menuHtml.matchAll(/<div class="([^"]+)"([^>]*)>/g)].map(([, classes, rest]) => {
|
|
if (classes.includes('context-menu-separator')) return 'separator';
|
|
return /data-action="([^"]+)"/.exec(rest)?.[1] || null;
|
|
});
|
|
|
|
it('keeps the refresh actions first and the folder operations grouped', () => {
|
|
expect(sequence).toEqual([
|
|
'scan-folder',
|
|
'check-folder-updates',
|
|
'separator',
|
|
'create-subfolder',
|
|
'rename-folder',
|
|
'separator',
|
|
'delete-folder',
|
|
]);
|
|
});
|
|
|
|
it('keeps the destructive entry last and visually marked', () => {
|
|
const deleteEntry = menuHtml.match(/<div class="([^"]*)"\s+data-action="delete-folder"/);
|
|
expect(deleteEntry).not.toBeNull();
|
|
expect(deleteEntry[1]).toContain('delete-item');
|
|
expect(sequence[sequence.length - 1]).toBe('delete-folder');
|
|
});
|
|
});
|