mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-13 04:42:13 -03:00
feat(settings): show default root cue in download root rules editor
Each rule group now shows which root is the type's default ('Default:
{root} — unmatched downloads go here'), and the default root's option in
the per-rule root dropdown carries a '(default)' suffix. Comparison is
deliberately loose (separators, trailing slashes, case) since this is
display-only. Changing a default root select in the Library section
re-renders the rules editor so the cue never goes stale.
This commit is contained in:
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "Download Root Rules",
|
||||
"help": "Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "default",
|
||||
"addRule": "Add rule",
|
||||
"removeRule": "Remove rule",
|
||||
"moveUp": "Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"title": "[TODO: Translate] Download Root Rules",
|
||||
"help": "[TODO: Translate] Route downloads to a different root folder based on the model's base model.",
|
||||
"description": "[TODO: Translate] Rules are checked in order. First match wins; unmatched downloads use the default root.",
|
||||
"defaultHint": "[TODO: Translate] Default: {root} — unmatched downloads go here",
|
||||
"defaultSuffix": "[TODO: Translate] default",
|
||||
"addRule": "[TODO: Translate] Add rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
|
||||
@@ -686,6 +686,11 @@
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.download-root-rule-default-hint {
|
||||
padding-top: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.download-root-rule-panel {
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,20 @@ const DOWNLOAD_ROOT_RULE_ROOT_ENDPOINTS = {
|
||||
embeddings: '/api/lm/embeddings/roots',
|
||||
};
|
||||
|
||||
// Rule type -> the settings key holding that type's default download root.
|
||||
const DOWNLOAD_ROOT_RULE_DEFAULT_ROOT_SETTINGS = {
|
||||
loras: 'default_lora_root',
|
||||
checkpoints: 'default_checkpoint_root',
|
||||
unet: 'default_unet_root',
|
||||
embeddings: 'default_embedding_root',
|
||||
};
|
||||
|
||||
// Setting keys whose change must refresh the download root rules editor
|
||||
// (it shows which root is the default for each type).
|
||||
const DOWNLOAD_ROOT_RULE_DEFAULT_SETTING_KEYS = new Set(
|
||||
Object.values(DOWNLOAD_ROOT_RULE_DEFAULT_ROOT_SETTINGS)
|
||||
);
|
||||
|
||||
export class SettingsManager {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
@@ -3257,6 +3271,12 @@ export class SettingsManager {
|
||||
// Apply frontend settings immediately
|
||||
this.applyFrontendSettings();
|
||||
|
||||
// The download root rules editor marks each type's default root;
|
||||
// refresh it when a default root select changes.
|
||||
if (DOWNLOAD_ROOT_RULE_DEFAULT_SETTING_KEYS.has(settingKey)) {
|
||||
this.renderDownloadRootRules();
|
||||
}
|
||||
|
||||
// Recalculate layout when display density changes
|
||||
if (settingKey === 'display_density' && state.virtualScroller) {
|
||||
state.virtualScroller.calculateLayout();
|
||||
@@ -4469,7 +4489,7 @@ export class SettingsManager {
|
||||
|
||||
renderDownloadRootRules() {
|
||||
const container = document.getElementById('downloadRootRulesContainer');
|
||||
if (!container) {
|
||||
if (!container || !this.downloadRootRuleRoots) {
|
||||
return;
|
||||
}
|
||||
container.innerHTML = '';
|
||||
@@ -4478,6 +4498,24 @@ export class SettingsManager {
|
||||
});
|
||||
}
|
||||
|
||||
getDownloadRootRuleDefaultRoot(typeKey) {
|
||||
const settingKey = DOWNLOAD_ROOT_RULE_DEFAULT_ROOT_SETTINGS[typeKey];
|
||||
const value = settingKey ? state.global.settings[settingKey] : '';
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
// Loose display-only comparison: separator, trailing slash and case
|
||||
// differences must not hide the "(default)" cue.
|
||||
isDownloadRootRuleDefaultRoot(root, defaultRoot) {
|
||||
const normalize = (value) => String(value || '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\/+$/, '')
|
||||
.toLowerCase();
|
||||
const normalizedRoot = normalize(root);
|
||||
const normalizedDefault = normalize(defaultRoot);
|
||||
return !!normalizedRoot && !!normalizedDefault && normalizedRoot === normalizedDefault;
|
||||
}
|
||||
|
||||
buildDownloadRootRuleGroup(typeKey) {
|
||||
const group = document.createElement('div');
|
||||
group.className = 'download-root-rule-group';
|
||||
@@ -4496,6 +4534,18 @@ export class SettingsManager {
|
||||
return group;
|
||||
}
|
||||
|
||||
const defaultRoot = this.getDownloadRootRuleDefaultRoot(typeKey);
|
||||
if (defaultRoot) {
|
||||
const defaultHint = document.createElement('div');
|
||||
defaultHint.className = 'download-root-rules-hint download-root-rule-default-hint';
|
||||
defaultHint.textContent = translate(
|
||||
'settings.downloadRootRules.defaultHint',
|
||||
{ root: defaultRoot },
|
||||
`Default: ${defaultRoot} — unmatched downloads go here`
|
||||
);
|
||||
group.appendChild(defaultHint);
|
||||
}
|
||||
|
||||
const rules = this.getDownloadRootRules()[typeKey];
|
||||
rules.forEach((rule, index) => {
|
||||
group.appendChild(this.buildDownloadRootRuleRow(typeKey, rule, index, false, roots));
|
||||
@@ -4548,10 +4598,13 @@ export class SettingsManager {
|
||||
|
||||
const rootSelect = document.createElement('select');
|
||||
rootSelect.className = 'base-model-select download-root-rule-root';
|
||||
const defaultRoot = this.getDownloadRootRuleDefaultRoot(typeKey);
|
||||
roots.forEach((root) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = root;
|
||||
option.textContent = root;
|
||||
option.textContent = this.isDownloadRootRuleDefaultRoot(root, defaultRoot)
|
||||
? `${root} (${translate('settings.downloadRootRules.defaultSuffix', {}, 'default')})`
|
||||
: root;
|
||||
rootSelect.appendChild(option);
|
||||
});
|
||||
if (roots.includes(rule.root)) {
|
||||
|
||||
@@ -325,3 +325,107 @@ describe('SettingsManager download root rules', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('SettingsManager download root rules default-root cue', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.clearAllMocks();
|
||||
state.global.settings = {
|
||||
download_root_rules: { loras: [], checkpoints: [], unet: [], embeddings: [] },
|
||||
};
|
||||
});
|
||||
|
||||
const defaultHintFor = (typeIndex) =>
|
||||
document.querySelectorAll('.download-root-rule-group')[typeIndex]
|
||||
.querySelector('.download-root-rule-default-hint');
|
||||
|
||||
it('renders a default hint under the group header when a default root is set', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
state.global.settings.default_lora_root = '/models/loras-main';
|
||||
const manager = createManager();
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
expect(defaultHintFor(0).textContent)
|
||||
.toBe('Default: /models/loras-main — unmatched downloads go here');
|
||||
});
|
||||
|
||||
it('omits the default hint when the type has no roots or no default root', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
state.global.settings.default_lora_root = '';
|
||||
state.global.settings.default_embedding_root = '/models/embeddings';
|
||||
const manager = createManager();
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
// loras: default root is empty -> no hint
|
||||
expect(defaultHintFor(0)).toBeNull();
|
||||
// embeddings: no roots configured -> noRoots hint only, no default hint
|
||||
expect(defaultHintFor(3)).toBeNull();
|
||||
});
|
||||
|
||||
it('marks the default root option with a (default) suffix but keeps the plain value', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
state.global.settings.default_lora_root = '/models/lora-archive';
|
||||
state.global.settings.download_root_rules = {
|
||||
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/loras-main' }],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
};
|
||||
const manager = createManager();
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
const options = groupRows('loras')[0].querySelectorAll('select.download-root-rule-root option');
|
||||
expect([...options].map((o) => o.value)).toEqual(['/models/loras-main', '/models/lora-archive']);
|
||||
expect([...options].map((o) => o.textContent)).toEqual([
|
||||
'/models/loras-main',
|
||||
'/models/lora-archive (default)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('compares roots loosely: separators, trailing slashes and case are ignored', () => {
|
||||
const manager = createManager();
|
||||
|
||||
expect(manager.isDownloadRootRuleDefaultRoot('C:\\Models\\Loras\\', 'c:/models/loras')).toBe(true);
|
||||
expect(manager.isDownloadRootRuleDefaultRoot('/a/b/', '/a/b')).toBe(true);
|
||||
expect(manager.isDownloadRootRuleDefaultRoot('/a/b', '/a/c')).toBe(false);
|
||||
expect(manager.isDownloadRootRuleDefaultRoot('/a/b', '')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render groups before the roots have been fetched', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
const manager = createManager();
|
||||
manager.downloadRootRuleRoots = null;
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
expect(document.querySelectorAll('.download-root-rule-group')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('changing a default root select re-renders the rules editor with the new default', async () => {
|
||||
appendDownloadRootRulesUi();
|
||||
document.body.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
'<select id="defaultLoraRoot"><option value="/models/loras-main">a</option><option value="/models/lora-archive" selected>b</option></select>'
|
||||
);
|
||||
state.global.settings.default_lora_root = '/models/loras-main';
|
||||
const manager = createManager();
|
||||
manager.applyFrontendSettings = vi.fn();
|
||||
// Mimic the real saveSetting, which writes the value into state.
|
||||
manager.saveSetting = vi.fn().mockImplementation(async (key, value) => {
|
||||
state.global.settings[key] = value;
|
||||
});
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
expect(defaultHintFor(0).textContent)
|
||||
.toBe('Default: /models/loras-main — unmatched downloads go here');
|
||||
|
||||
await manager.saveSelectSetting('defaultLoraRoot', 'default_lora_root');
|
||||
|
||||
expect(manager.saveSetting).toHaveBeenCalledWith('default_lora_root', '/models/lora-archive');
|
||||
expect(defaultHintFor(0).textContent)
|
||||
.toBe('Default: /models/lora-archive — unmatched downloads go here');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user