mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-13 12:52:13 -03:00
feat(settings): declutter download root rule rows
- Move match-type selection out of rule rows: two side-by-side add buttons (+ Base model rule / + Tag rule) at the group bottom; rows show a static BM/Tag chip instead of a select. Changing a rule's type is now delete-and-recreate. Removed setDownloadRootRuleMatchType. - Replace Edit/Collapse text buttons with a chevron-down icon that rotates when expanded (aria-expanded preserved, i18n tooltip kept). - Truncate root dropdown option labels in the middle via truncateMiddlePath(path, 24, 16); option.value, option.title and select.title keep the full path, and (default) suffix is appended after truncation. - Summary wording: empty drafts show "Select base models…" / "Enter tags…"; more than two items collapse to first two + "+N more"; tag summaries drop the "tags:" prefix since the chip already conveys it.
This commit is contained in:
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+10
-3
@@ -497,11 +497,12 @@
|
||||
},
|
||||
"downloadRootRules": {
|
||||
"title": "Download Root Rules",
|
||||
"help": "Route downloads to a different root folder based on the model's base model.",
|
||||
"help": "Route downloads to a different root folder based on the model's base model or tags.",
|
||||
"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",
|
||||
"addBaseModelRule": "Base model rule",
|
||||
"addTagRule": "Tag rule",
|
||||
"removeRule": "Remove rule",
|
||||
"moveUp": "Move up",
|
||||
"moveDown": "Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "Base model",
|
||||
"tag": "Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "BM",
|
||||
"tag": "Tag"
|
||||
},
|
||||
"tagPlaceholder": "character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "tags: {tags}"
|
||||
"selectBaseModels": "Select base models…",
|
||||
"enterTags": "Enter tags…",
|
||||
"more": "+{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
+9
-2
@@ -501,7 +501,8 @@
|
||||
"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",
|
||||
"addBaseModelRule": "[TODO: Translate] Base model rule",
|
||||
"addTagRule": "[TODO: Translate] Tag rule",
|
||||
"removeRule": "[TODO: Translate] Remove rule",
|
||||
"moveUp": "[TODO: Translate] Move up",
|
||||
"moveDown": "[TODO: Translate] Move down",
|
||||
@@ -510,9 +511,15 @@
|
||||
"baseModel": "[TODO: Translate] Base model",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"matchTypeChips": {
|
||||
"baseModel": "[TODO: Translate] BM",
|
||||
"tag": "[TODO: Translate] Tag"
|
||||
},
|
||||
"tagPlaceholder": "[TODO: Translate] character, style, realistic",
|
||||
"summary": {
|
||||
"tags": "[TODO: Translate] tags: {tags}"
|
||||
"selectBaseModels": "[TODO: Translate] Select base models…",
|
||||
"enterTags": "[TODO: Translate] Enter tags…",
|
||||
"more": "[TODO: Translate] +{count} more"
|
||||
},
|
||||
"types": {
|
||||
"loras": "[TODO: Translate] LoRA",
|
||||
|
||||
@@ -695,21 +695,44 @@
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.download-root-rule-match-type {
|
||||
padding: 6px 10px;
|
||||
.download-root-rule-match-chip {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
opacity: 0.8;
|
||||
align-self: center;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.download-root-rule-match-type:focus {
|
||||
.download-root-rule-match-chip.is-tag {
|
||||
border-color: var(--lora-accent);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1);
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.download-root-rule-chevron {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.75;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.download-root-rule-toggle[aria-expanded="true"] .download-root-rule-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.download-root-rule-add-row {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.download-root-rule-add-row .add-mapping-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.download-root-rule-tag-input {
|
||||
|
||||
@@ -4573,13 +4573,21 @@ export class SettingsManager {
|
||||
group.appendChild(this.buildDownloadRootRuleRow(typeKey, draft, rules.length, true, roots));
|
||||
}
|
||||
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.type = 'button';
|
||||
addBtn.className = 'add-mapping-btn';
|
||||
addBtn.disabled = !!draft;
|
||||
addBtn.innerHTML = `<i class="fas fa-plus"></i><span>${translate('settings.downloadRootRules.addRule', {}, 'Add rule')}</span>`;
|
||||
addBtn.addEventListener('click', () => this.addDownloadRootRule(typeKey));
|
||||
group.appendChild(addBtn);
|
||||
const addRow = document.createElement('div');
|
||||
addRow.className = 'download-root-rule-add-row';
|
||||
[
|
||||
['base_model', 'settings.downloadRootRules.addBaseModelRule', 'Base model rule'],
|
||||
['tag', 'settings.downloadRootRules.addTagRule', 'Tag rule'],
|
||||
].forEach(([matchType, i18nKey, fallback]) => {
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.type = 'button';
|
||||
addBtn.className = 'add-mapping-btn';
|
||||
addBtn.disabled = !!draft;
|
||||
addBtn.innerHTML = `<i class="fas fa-plus"></i><span>${translate(i18nKey, {}, fallback)}</span>`;
|
||||
addBtn.addEventListener('click', () => this.addDownloadRootRule(typeKey, matchType));
|
||||
addRow.appendChild(addBtn);
|
||||
});
|
||||
group.appendChild(addRow);
|
||||
|
||||
return group;
|
||||
}
|
||||
@@ -4596,37 +4604,34 @@ export class SettingsManager {
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'mapping-controls download-root-rule-controls';
|
||||
|
||||
const matchTypeSelect = document.createElement('select');
|
||||
matchTypeSelect.className = 'download-root-rule-match-type';
|
||||
[
|
||||
['base_model', 'settings.downloadRootRules.matchTypes.baseModel', 'Base model'],
|
||||
['tag', 'settings.downloadRootRules.matchTypes.tag', 'Tag'],
|
||||
].forEach(([value, i18nKey, fallback]) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = translate(i18nKey, {}, fallback);
|
||||
matchTypeSelect.appendChild(option);
|
||||
});
|
||||
matchTypeSelect.value = rule.match_type === 'tag' ? 'tag' : 'base_model';
|
||||
matchTypeSelect.addEventListener('change', () => {
|
||||
this.setDownloadRootRuleMatchType(typeKey, index, matchTypeSelect.value, isDraft);
|
||||
});
|
||||
controls.appendChild(matchTypeSelect);
|
||||
// Static match-type chip: the type is fixed at creation time (the
|
||||
// two per-group add buttons); changing type means recreating the rule.
|
||||
const isTagRule = rule.match_type === 'tag';
|
||||
const chip = document.createElement('span');
|
||||
chip.className = `download-root-rule-match-chip${isTagRule ? ' is-tag' : ''}`;
|
||||
chip.textContent = isTagRule
|
||||
? translate('settings.downloadRootRules.matchTypeChips.tag', {}, 'Tag')
|
||||
: translate('settings.downloadRootRules.matchTypeChips.baseModel', {}, 'BM');
|
||||
chip.title = isTagRule
|
||||
? translate('settings.downloadRootRules.matchTypes.tag', {}, 'Tag')
|
||||
: translate('settings.downloadRootRules.matchTypes.baseModel', {}, 'Base model');
|
||||
controls.appendChild(chip);
|
||||
|
||||
const toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
toggle.className = 'secondary-btn base-model-skip-toggle download-root-rule-toggle';
|
||||
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
toggle.title = isOpen
|
||||
? translate('settings.downloadSkipBaseModels.actions.collapse', {}, 'Collapse')
|
||||
: translate('settings.downloadSkipBaseModels.actions.edit', {}, 'Edit');
|
||||
const summary = document.createElement('span');
|
||||
summary.className = 'download-root-rule-summary';
|
||||
this.renderDownloadRootRuleSummary(summary, rule);
|
||||
const toggleLabel = document.createElement('span');
|
||||
toggleLabel.className = 'base-model-skip-toggle-label';
|
||||
toggleLabel.textContent = isOpen
|
||||
? translate('settings.downloadSkipBaseModels.actions.collapse', {}, 'Collapse')
|
||||
: translate('settings.downloadSkipBaseModels.actions.edit', {}, 'Edit');
|
||||
const chevron = document.createElement('i');
|
||||
chevron.className = 'fas fa-chevron-down download-root-rule-chevron';
|
||||
chevron.setAttribute('aria-hidden', 'true');
|
||||
toggle.appendChild(summary);
|
||||
toggle.appendChild(toggleLabel);
|
||||
toggle.appendChild(chevron);
|
||||
toggle.addEventListener('click', () => this.toggleDownloadRootRulePanel(typeKey, index));
|
||||
controls.appendChild(toggle);
|
||||
|
||||
@@ -4636,15 +4641,21 @@ export class SettingsManager {
|
||||
roots.forEach((root) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = root;
|
||||
// Middle-truncate long paths for display; the option value keeps
|
||||
// the full path and the select title shows the selected one.
|
||||
const displayRoot = this.truncateMiddlePath(root);
|
||||
option.textContent = this.isDownloadRootRuleDefaultRoot(root, defaultRoot)
|
||||
? `${root} (${translate('settings.downloadRootRules.defaultSuffix', {}, 'default')})`
|
||||
: root;
|
||||
? `${displayRoot} (${translate('settings.downloadRootRules.defaultSuffix', {}, 'default')})`
|
||||
: displayRoot;
|
||||
option.title = root;
|
||||
rootSelect.appendChild(option);
|
||||
});
|
||||
if (roots.includes(rule.root)) {
|
||||
rootSelect.value = rule.root;
|
||||
}
|
||||
rootSelect.title = rootSelect.value;
|
||||
rootSelect.addEventListener('change', () => {
|
||||
rootSelect.title = rootSelect.value;
|
||||
this.updateDownloadRootRuleRoot(typeKey, index, rootSelect.value, isDraft);
|
||||
});
|
||||
controls.appendChild(rootSelect);
|
||||
@@ -4747,34 +4758,6 @@ export class SettingsManager {
|
||||
});
|
||||
}
|
||||
|
||||
setDownloadRootRuleMatchType(typeKey, index, matchType, isDraft) {
|
||||
if (matchType !== 'base_model' && matchType !== 'tag') {
|
||||
return;
|
||||
}
|
||||
if (isDraft) {
|
||||
const draft = this.downloadRootRuleDrafts[typeKey];
|
||||
if (!draft || draft.match_type === matchType) {
|
||||
return;
|
||||
}
|
||||
draft.match_type = matchType;
|
||||
this.renderDownloadRootRules();
|
||||
return;
|
||||
}
|
||||
const rules = this.getDownloadRootRules();
|
||||
const rule = rules[typeKey][index];
|
||||
if (!rule || rule.match_type === matchType) {
|
||||
return;
|
||||
}
|
||||
// Keep the match entries when switching: tags are free text, and
|
||||
// non-whitelist leftovers in a base_model rule are filtered by the
|
||||
// backend normalization on save.
|
||||
rule.match_type = matchType;
|
||||
this.downloadRootRuleOpenPanels.clear();
|
||||
this.downloadRootRuleSearches = {};
|
||||
this.renderDownloadRootRules();
|
||||
this.persistDownloadRootRules();
|
||||
}
|
||||
|
||||
commitDownloadRootRuleTags(typeKey, index, rawValue, isDraft) {
|
||||
const tags = this.parseDownloadRootRuleTags(rawValue);
|
||||
if (isDraft) {
|
||||
@@ -4825,29 +4808,35 @@ export class SettingsManager {
|
||||
this.persistDownloadRootRules();
|
||||
}
|
||||
|
||||
// Middle-truncate a long path for compact display, preserving both ends
|
||||
// (the tail is what actually disambiguates sibling roots).
|
||||
truncateMiddlePath(path, head = 24, tail = 16) {
|
||||
const text = String(path || '');
|
||||
if (text.length <= head + tail + 1) {
|
||||
return text;
|
||||
}
|
||||
return `${text.slice(0, head)}…${text.slice(-tail)}`;
|
||||
}
|
||||
|
||||
renderDownloadRootRuleSummary(summaryEl, rule) {
|
||||
const match = Array.isArray(rule?.match) ? rule.match : [];
|
||||
if (rule?.match_type === 'tag' && match.length) {
|
||||
summaryEl.textContent = translate(
|
||||
'settings.downloadRootRules.summary.tags',
|
||||
{ tags: match.join(', ') },
|
||||
`tags: ${match.join(', ')}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!match.length) {
|
||||
summaryEl.textContent = translate('settings.downloadSkipBaseModels.summary.none', {}, 'None selected');
|
||||
// Only drafts can be empty; persisted rules always have matches.
|
||||
summaryEl.textContent = rule?.match_type === 'tag'
|
||||
? translate('settings.downloadRootRules.summary.enterTags', {}, 'Enter tags…')
|
||||
: translate('settings.downloadRootRules.summary.selectBaseModels', {}, 'Select base models…');
|
||||
return;
|
||||
}
|
||||
if (match.length <= 2) {
|
||||
summaryEl.textContent = match.join(', ');
|
||||
return;
|
||||
}
|
||||
summaryEl.textContent = translate(
|
||||
'settings.downloadSkipBaseModels.summary.count',
|
||||
{ count: match.length },
|
||||
`${match.length} selected`
|
||||
const more = translate(
|
||||
'settings.downloadRootRules.summary.more',
|
||||
{ count: match.length - 2 },
|
||||
`+${match.length - 2} more`
|
||||
);
|
||||
summaryEl.textContent = `${match.slice(0, 2).join(', ')}, ${more}`;
|
||||
}
|
||||
|
||||
renderDownloadRootRuleBaseModelList(listEl, typeKey, index, isDraft, summaryEl) {
|
||||
@@ -4899,13 +4888,13 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
addDownloadRootRule(typeKey) {
|
||||
addDownloadRootRule(typeKey, matchType = 'base_model') {
|
||||
if (this.downloadRootRuleDrafts[typeKey]) {
|
||||
return;
|
||||
}
|
||||
const roots = this.downloadRootRuleRoots?.[typeKey] || [];
|
||||
this.downloadRootRuleDrafts[typeKey] = {
|
||||
match_type: 'base_model',
|
||||
match_type: matchType === 'tag' ? 'tag' : 'base_model',
|
||||
match: [],
|
||||
root: roots[0] || '',
|
||||
};
|
||||
|
||||
@@ -457,7 +457,7 @@ describe('SettingsManager download root rules tag matching', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders a tag rule with the match-type select, summary and tag input', () => {
|
||||
it('renders a tag rule with a static type chip, summary and tag input', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
state.global.settings.download_root_rules = {
|
||||
loras: [{ match_type: 'tag', match: ['character', 'style'], root: '/models/lora-archive' }],
|
||||
@@ -470,16 +470,20 @@ describe('SettingsManager download root rules tag matching', () => {
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
const row = groupRows('loras')[0];
|
||||
expect(row.querySelector('.download-root-rule-match-type').value).toBe('tag');
|
||||
// The match type is fixed at creation: a chip, not a dropdown.
|
||||
const chip = row.querySelector('.download-root-rule-match-chip');
|
||||
expect(chip.textContent).toBe('Tag');
|
||||
expect(chip.title).toBe('Tag');
|
||||
expect(row.querySelector('.download-root-rule-match-type')).toBeNull();
|
||||
expect(row.querySelector('.download-root-rule-summary').textContent)
|
||||
.toBe('tags: character, style');
|
||||
.toBe('character, style');
|
||||
expect(row.querySelector('.download-root-rule-tag-input').value)
|
||||
.toBe('character, style');
|
||||
// No base model checkbox list for tag rules
|
||||
expect(row.querySelector('.base-model-skip-list')).toBeNull();
|
||||
});
|
||||
|
||||
it('switching a persisted rule to tag persists immediately and keeps the match list', async () => {
|
||||
it('renders a base model rule chip with the full name as tooltip', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
state.global.settings.download_root_rules = {
|
||||
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: '/models/lora-archive' }],
|
||||
@@ -488,33 +492,20 @@ describe('SettingsManager download root rules tag matching', () => {
|
||||
embeddings: [],
|
||||
};
|
||||
const manager = createManager();
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
const select = groupRows('loras')[0].querySelector('.download-root-rule-match-type');
|
||||
select.value = 'tag';
|
||||
select.dispatchEvent(new Event('change'));
|
||||
await vi.waitFor(() => {
|
||||
expect(manager.saveSetting).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(manager.saveSetting).toHaveBeenCalledWith('download_root_rules', {
|
||||
loras: [{ match_type: 'tag', match: ['Flux.1 D'], root: '/models/lora-archive' }],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
});
|
||||
expect(groupRows('loras')[0].querySelector('.download-root-rule-tag-input')).not.toBeNull();
|
||||
const chip = groupRows('loras')[0].querySelector('.download-root-rule-match-chip');
|
||||
expect(chip.textContent).toBe('BM');
|
||||
expect(chip.title).toBe('Base model');
|
||||
});
|
||||
|
||||
it('a draft switched to tag promotes on the first committed tag input', async () => {
|
||||
it('a tag draft promotes on the first committed tag input', async () => {
|
||||
appendDownloadRootRulesUi();
|
||||
const manager = createManager();
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
manager.addDownloadRootRule('loras');
|
||||
const select = groupRows('loras')[0].querySelector('.download-root-rule-match-type');
|
||||
select.value = 'tag';
|
||||
select.dispatchEvent(new Event('change'));
|
||||
manager.addDownloadRootRule('loras', 'tag');
|
||||
|
||||
// Still a draft: nothing persisted yet
|
||||
expect(manager.saveSetting).not.toHaveBeenCalled();
|
||||
@@ -588,3 +579,168 @@ describe('SettingsManager download root rules tag matching', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('SettingsManager download root rules row layout', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.clearAllMocks();
|
||||
state.global.settings = {
|
||||
download_root_rules: { loras: [], checkpoints: [], unet: [], embeddings: [] },
|
||||
};
|
||||
});
|
||||
|
||||
it('offers separate add buttons for base model and tag rules', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
const manager = createManager();
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
const buttons = document.querySelectorAll('.download-root-rule-group')[0]
|
||||
.querySelectorAll('.download-root-rule-add-row .add-mapping-btn');
|
||||
expect(buttons).toHaveLength(2);
|
||||
expect(buttons[0].textContent).toContain('Base model rule');
|
||||
expect(buttons[1].textContent).toContain('Tag rule');
|
||||
|
||||
buttons[1].click();
|
||||
expect(manager.downloadRootRuleDrafts.loras.match_type).toBe('tag');
|
||||
|
||||
// While a draft exists both buttons are disabled
|
||||
const disabledButtons = document.querySelectorAll('.download-root-rule-group')[0]
|
||||
.querySelectorAll('.download-root-rule-add-row .add-mapping-btn');
|
||||
expect([...disabledButtons].every((btn) => btn.disabled)).toBe(true);
|
||||
});
|
||||
|
||||
it('base model add button creates a base_model draft', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
const manager = createManager();
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
document.querySelectorAll('.download-root-rule-group')[0]
|
||||
.querySelectorAll('.download-root-rule-add-row .add-mapping-btn')[0]
|
||||
.click();
|
||||
|
||||
expect(manager.downloadRootRuleDrafts.loras.match_type).toBe('base_model');
|
||||
expect(groupRows('loras')[0].querySelector('.download-root-rule-match-chip').textContent)
|
||||
.toBe('BM');
|
||||
});
|
||||
|
||||
it('the summary toggle uses a chevron icon with accessible title and aria state', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
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 toggle = groupRows('loras')[0].querySelector('.download-root-rule-toggle');
|
||||
expect(toggle.querySelector('.download-root-rule-chevron')).not.toBeNull();
|
||||
expect(toggle.querySelector('.base-model-skip-toggle-label')).toBeNull();
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('false');
|
||||
expect(toggle.title).toBe('Edit');
|
||||
|
||||
toggle.click();
|
||||
|
||||
const reopened = groupRows('loras')[0].querySelector('.download-root-rule-toggle');
|
||||
expect(reopened.getAttribute('aria-expanded')).toBe('true');
|
||||
expect(reopened.title).toBe('Collapse');
|
||||
});
|
||||
|
||||
it('truncateMiddlePath keeps short paths intact and ellipsizes long ones', () => {
|
||||
const manager = createManager();
|
||||
|
||||
expect(manager.truncateMiddlePath('/models/loras')).toBe('/models/loras');
|
||||
|
||||
const long = '/models/library/very/deeply/nested/folder/structure/loras';
|
||||
const truncated = manager.truncateMiddlePath(long);
|
||||
expect(truncated.startsWith(long.slice(0, 24))).toBe(true);
|
||||
expect(truncated.endsWith(long.slice(-16))).toBe(true);
|
||||
expect(truncated).toContain('…');
|
||||
expect(truncated.length).toBe(24 + 1 + 16);
|
||||
});
|
||||
|
||||
it('truncates long root paths in the dropdown but keeps full values and titles', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
const longRoot = '/models/library/very/deeply/nested/folder/structure/loras';
|
||||
const manager = createManager();
|
||||
manager.downloadRootRuleRoots = {
|
||||
loras: [longRoot],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
};
|
||||
state.global.settings.download_root_rules = {
|
||||
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: longRoot }],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
};
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
const rootSelect = groupRows('loras')[0].querySelector('select.download-root-rule-root');
|
||||
expect(rootSelect.options[0].value).toBe(longRoot);
|
||||
expect(rootSelect.options[0].title).toBe(longRoot);
|
||||
expect(rootSelect.options[0].textContent).toBe(manager.truncateMiddlePath(longRoot));
|
||||
expect(rootSelect.title).toBe(longRoot);
|
||||
});
|
||||
|
||||
it('keeps the (default) suffix after the truncated path', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
const longRoot = '/models/library/very/deeply/nested/folder/structure/loras';
|
||||
const manager = createManager();
|
||||
manager.downloadRootRuleRoots = {
|
||||
loras: [longRoot],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
};
|
||||
state.global.settings.default_lora_root = longRoot;
|
||||
state.global.settings.download_root_rules = {
|
||||
loras: [{ match_type: 'base_model', match: ['Flux.1 D'], root: longRoot }],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
};
|
||||
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
const option = groupRows('loras')[0]
|
||||
.querySelector('select.download-root-rule-root').options[0];
|
||||
expect(option.textContent).toBe(`${manager.truncateMiddlePath(longRoot)} (default)`);
|
||||
});
|
||||
|
||||
it('shows actionable placeholder summaries for empty drafts', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
const manager = createManager();
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
manager.addDownloadRootRule('loras', 'base_model');
|
||||
expect(groupRows('loras')[0].querySelector('.download-root-rule-summary').textContent)
|
||||
.toBe('Select base models…');
|
||||
|
||||
manager.removeDownloadRootRule('loras', 0, true);
|
||||
manager.addDownloadRootRule('loras', 'tag');
|
||||
expect(groupRows('loras')[0].querySelector('.download-root-rule-summary').textContent)
|
||||
.toBe('Enter tags…');
|
||||
});
|
||||
|
||||
it('summarizes more than two matches as first two plus a count', () => {
|
||||
appendDownloadRootRulesUi();
|
||||
state.global.settings.download_root_rules = {
|
||||
loras: [
|
||||
{ match_type: 'base_model', match: ['Flux.1 D', 'Wan Video', 'Pony', 'SDXL 1.0'], root: '/models/loras-main' },
|
||||
],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
};
|
||||
const manager = createManager();
|
||||
manager.renderDownloadRootRules();
|
||||
|
||||
expect(groupRows('loras')[0].querySelector('.download-root-rule-summary').textContent)
|
||||
.toBe('Flux.1 D, Wan Video, +2 more');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user