feat: close LoRA sidebar when selection clears

This commit is contained in:
ethanfel
2026-08-24 23:32:51 +02:00
parent 932272806c
commit 6778110c50
4 changed files with 153 additions and 15 deletions
+1
View File
@@ -105,6 +105,7 @@ Selecting a LoRA loader opens the **LoRA Info** sidebar and follows the node's c
- If the selected LoRA is indexed by the remote LoRA Manager, the sidebar shows its image or video preview, file details, base model, trigger words, tags, usage tips, and direct model links. - If the selected LoRA is indexed by the remote LoRA Manager, the sidebar shows its image or video preview, file details, base model, trigger words, tags, usage tips, and direct model links.
- Cached community creations and Civitai examples appear with video controls, their shared prompt and negative prompt, generation settings, attribution, navigation, and one-click prompt copying. - Cached community creations and Civitai examples appear with video controls, their shared prompt and negative prompt, generation settings, attribution, navigation, and one-click prompt copying.
- If a node contains multiple active LoRAs, use the selector at the top of the sidebar to switch cards. - If a node contains multiple active LoRAs, use the selector at the top of the sidebar to switch cards.
- LoRA Info closes automatically after the selected LoRA loader is cleared, but stays open while switching directly between loaders.
- If no Manager card exists, the sidebar offers name searches on LoRA Manager, Civitai, Civitai Red, and CivArchive. - If no Manager card exists, the sidebar offers name searches on LoRA Manager, Civitai, Civitai Red, and CivArchive.
- Duplicate filenames are not guessed: the sidebar asks you to choose the matching Manager path. - Duplicate filenames are not guessed: the sidebar asks you to choose the matching Manager path.
@@ -3,7 +3,9 @@ import assert from "node:assert/strict";
import { import {
buildExternalLinks, buildExternalLinks,
closeActiveSidebarTab,
extractLoraNames, extractLoraNames,
getActiveSidebarTabId,
getSelectedGraphNodes, getSelectedGraphNodes,
isVideoMedia, isVideoMedia,
matchModelItems, matchModelItems,
@@ -14,6 +16,64 @@ import {
normalizeUsageTips, normalizeUsageTips,
} from "../../web/comfyui/lora_manager_sidebar_utils.js"; } from "../../web/comfyui/lora_manager_sidebar_utils.js";
test("closes only the active LoRA sidebar across ComfyUI API shapes", () => {
const managed = {
activeSidebarTabId: "lm-remote-lora-info",
setActiveSidebarTab(id) {
this.activeSidebarTabId = id;
},
};
assert.equal(
closeActiveSidebarTab(managed, "lm-remote-lora-info"),
true
);
assert.equal(getActiveSidebarTabId(managed), null);
const activeRef = { value: "lm-remote-lora-info" };
const refManager = { sidebarTab: { activeSidebarTabId: activeRef } };
assert.equal(
closeActiveSidebarTab(refManager, "lm-remote-lora-info"),
true
);
assert.equal(activeRef.value, null);
let otherTabToggleCount = 0;
const otherTabManager = {
sidebarTab: {
activeSidebarTabId: "node-library",
toggleSidebarTab() {
otherTabToggleCount += 1;
},
},
};
assert.equal(
closeActiveSidebarTab(otherTabManager, "lm-remote-lora-info"),
false
);
assert.equal(otherTabToggleCount, 0);
});
test("falls back to a guarded toggle for readonly sidebar state", () => {
let activeId = "lm-remote-lora-info";
let toggleCount = 0;
const sidebarTab = {
toggleSidebarTab(tabId) {
toggleCount += 1;
activeId = activeId === tabId ? null : tabId;
},
};
Object.defineProperty(sidebarTab, "activeSidebarTabId", {
get: () => activeId,
});
assert.equal(
closeActiveSidebarTab({ sidebarTab }, "lm-remote-lora-info"),
true
);
assert.equal(activeId, null);
assert.equal(toggleCount, 1);
});
test("normalizes loader paths and weight extensions", () => { test("normalizes loader paths and weight extensions", () => {
assert.equal( assert.equal(
normalizeLoraIdentifier("Styles\\Portrait.safetensors"), normalizeLoraIdentifier("Styles\\Portrait.safetensors"),
+29 -15
View File
@@ -4,7 +4,9 @@ import { api } from "../../scripts/api.js";
import { import {
buildExternalLinks, buildExternalLinks,
cleanLoraName, cleanLoraName,
closeActiveSidebarTab,
extractLoraNames, extractLoraNames,
getActiveSidebarTabId,
getSelectedGraphNodes, getSelectedGraphNodes,
isVideoMedia, isVideoMedia,
loraSearchTerm, loraSearchTerm,
@@ -27,6 +29,7 @@ const NODE_SELECTION_HOOK = Symbol.for("lmRemote.loraInfo.nodeSelectionHook");
const CANVAS_SELECTION_HOOK = Symbol.for("lmRemote.loraInfo.canvasSelectionHook"); const CANVAS_SELECTION_HOOK = Symbol.for("lmRemote.loraInfo.canvasSelectionHook");
const MAX_SHARED_MEDIA = 40; const MAX_SHARED_MEDIA = 40;
const MATURE_MEDIA_LEVEL = 4; const MATURE_MEDIA_LEVEL = 4;
const SIDEBAR_CLOSE_DELAY_MS = 80;
let sidebarRoot = null; let sidebarRoot = null;
let selectedNode = null; let selectedNode = null;
@@ -37,6 +40,7 @@ let lookupState = { status: "idle" };
let lookupGeneration = 0; let lookupGeneration = 0;
let lookupController = null; let lookupController = null;
let monitorTimer = null; let monitorTimer = null;
let sidebarCloseTimer = null;
function createElement(tag, className, text) { function createElement(tag, className, text) {
const element = document.createElement(tag); const element = document.createElement(tag);
@@ -1030,27 +1034,15 @@ function autoOpenEnabled() {
return app.extensionManager?.setting?.get?.(AUTO_OPEN_SETTING) !== false; return app.extensionManager?.setting?.get?.(AUTO_OPEN_SETTING) !== false;
} }
function unwrapValue(value) {
return value && typeof value === "object" && "value" in value
? value.value
: value;
}
function activeSidebarId(manager, sidebar) {
return unwrapValue(
sidebar?.activeSidebarTabId ?? manager?.activeSidebarTabId
);
}
function openSidebarTab() { function openSidebarTab() {
const manager = app.extensionManager; const manager = app.extensionManager;
if (!manager) return false; if (!manager) return false;
const sidebar = manager.sidebarTab || manager; const sidebar = manager.sidebarTab || manager;
if (activeSidebarId(manager, sidebar) === TAB_ID) return true; if (getActiveSidebarTabId(manager) === TAB_ID) return true;
if (typeof manager.setActiveSidebarTab === "function") { if (typeof manager.setActiveSidebarTab === "function") {
manager.setActiveSidebarTab(TAB_ID); manager.setActiveSidebarTab(TAB_ID);
if (activeSidebarId(manager, sidebar) === TAB_ID) return true; if (getActiveSidebarTabId(manager) === TAB_ID) return true;
} }
if (sidebar && "activeSidebarTabId" in sidebar) { if (sidebar && "activeSidebarTabId" in sidebar) {
@@ -1064,7 +1056,7 @@ function openSidebarTab() {
} catch { } catch {
// Some frontend versions expose a readonly store property. // Some frontend versions expose a readonly store property.
} }
if (activeSidebarId(manager, sidebar) === TAB_ID) return true; if (getActiveSidebarTabId(manager) === TAB_ID) return true;
} }
if (typeof sidebar?.toggleSidebarTab === "function") { if (typeof sidebar?.toggleSidebarTab === "function") {
@@ -1082,10 +1074,29 @@ function openSidebarTab() {
return false; return false;
} }
function cancelSidebarClose() {
if (sidebarCloseTimer == null) return;
window.clearTimeout(sidebarCloseTimer);
sidebarCloseTimer = null;
}
function scheduleSidebarClose() {
cancelSidebarClose();
sidebarCloseTimer = window.setTimeout(() => {
sidebarCloseTimer = null;
const nodes = getSelectedGraphNodes(app.canvas);
const node = nodes.length === 1 ? nodes[0] : null;
if (node && extractLoraNames(node).length) return;
closeActiveSidebarTab(app.extensionManager, TAB_ID);
}, SIDEBAR_CLOSE_DELAY_MS);
}
function updateSelection({ autoOpen = false, force = false } = {}) { function updateSelection({ autoOpen = false, force = false } = {}) {
const nodes = getSelectedGraphNodes(app.canvas); const nodes = getSelectedGraphNodes(app.canvas);
const node = nodes.length === 1 ? nodes[0] : null; const node = nodes.length === 1 ? nodes[0] : null;
const names = node ? extractLoraNames(node) : []; const names = node ? extractLoraNames(node) : [];
const shouldCloseSidebar = selectedNames.length > 0 && names.length === 0;
if (names.length) cancelSidebarClose();
const signature = `${node?.id ?? ""}|${names const signature = `${node?.id ?? ""}|${names
.map(normalizeLoraIdentifier) .map(normalizeLoraIdentifier)
.join("|")}`; .join("|")}`;
@@ -1112,6 +1123,8 @@ function updateSelection({ autoOpen = false, force = false } = {}) {
if (activeName) { if (activeName) {
if (autoOpen && autoOpenEnabled()) openSidebarTab(); if (autoOpen && autoOpenEnabled()) openSidebarTab();
lookupActiveName(); lookupActiveName();
} else if (shouldCloseSidebar) {
scheduleSidebarClose();
} }
} }
@@ -1169,6 +1182,7 @@ function registerSidebarTab() {
updateSelection({ force: true }); updateSelection({ force: true });
}, },
destroy() { destroy() {
cancelSidebarClose();
lookupGeneration += 1; lookupGeneration += 1;
lookupController?.abort(); lookupController?.abort();
sidebarRoot?.remove(); sidebarRoot?.remove();
+63
View File
@@ -395,6 +395,69 @@ export function normalizeMediaSettings(value) {
}; };
} }
function unwrapSidebarValue(value) {
return value && typeof value === "object" && "value" in value
? value.value
: value;
}
export function getActiveSidebarTabId(manager) {
if (!manager) return null;
const sidebar = manager.sidebarTab || manager;
return unwrapSidebarValue(
sidebar?.activeSidebarTabId ?? manager.activeSidebarTabId
);
}
export function closeActiveSidebarTab(manager, tabId) {
if (!manager || getActiveSidebarTabId(manager) !== tabId) return false;
const sidebar = manager.sidebarTab || manager;
if (typeof manager.setActiveSidebarTab === "function") {
try {
manager.setActiveSidebarTab(null);
if (getActiveSidebarTabId(manager) !== tabId) return true;
} catch {
// Older frontend wrappers may reject null.
}
}
if (
sidebar &&
(typeof sidebar === "object" || typeof sidebar === "function") &&
"activeSidebarTabId" in sidebar
) {
try {
const current = sidebar.activeSidebarTabId;
if (current && typeof current === "object" && "value" in current) {
current.value = null;
} else {
sidebar.activeSidebarTabId = null;
}
if (getActiveSidebarTabId(manager) !== tabId) return true;
} catch {
// Some frontend versions expose a readonly store property.
}
}
const toggleTargets = sidebar === manager ? [sidebar] : [sidebar, manager];
for (const target of toggleTargets) {
if (typeof target?.toggleSidebarTab !== "function") continue;
try {
target.toggleSidebarTab(tabId);
if (getActiveSidebarTabId(manager) !== tabId) return true;
} catch {
// Try the remaining compatibility paths.
}
}
if (typeof manager.command?.execute === "function") {
manager.command.execute(`Workspace.ToggleSidebarTab.${tabId}`);
return true;
}
return false;
}
export function extractLoraSyntax(value) { export function extractLoraSyntax(value) {
if (typeof value !== "string") return []; if (typeof value !== "string") return [];
const names = []; const names = [];