feat: close LoRA sidebar when selection clears
This commit is contained in:
@@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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 {
|
||||
buildExternalLinks,
|
||||
closeActiveSidebarTab,
|
||||
extractLoraNames,
|
||||
getActiveSidebarTabId,
|
||||
getSelectedGraphNodes,
|
||||
isVideoMedia,
|
||||
matchModelItems,
|
||||
@@ -14,6 +16,64 @@ import {
|
||||
normalizeUsageTips,
|
||||
} 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", () => {
|
||||
assert.equal(
|
||||
normalizeLoraIdentifier("Styles\\Portrait.safetensors"),
|
||||
|
||||
@@ -4,7 +4,9 @@ import { api } from "../../scripts/api.js";
|
||||
import {
|
||||
buildExternalLinks,
|
||||
cleanLoraName,
|
||||
closeActiveSidebarTab,
|
||||
extractLoraNames,
|
||||
getActiveSidebarTabId,
|
||||
getSelectedGraphNodes,
|
||||
isVideoMedia,
|
||||
loraSearchTerm,
|
||||
@@ -27,6 +29,7 @@ const NODE_SELECTION_HOOK = Symbol.for("lmRemote.loraInfo.nodeSelectionHook");
|
||||
const CANVAS_SELECTION_HOOK = Symbol.for("lmRemote.loraInfo.canvasSelectionHook");
|
||||
const MAX_SHARED_MEDIA = 40;
|
||||
const MATURE_MEDIA_LEVEL = 4;
|
||||
const SIDEBAR_CLOSE_DELAY_MS = 80;
|
||||
|
||||
let sidebarRoot = null;
|
||||
let selectedNode = null;
|
||||
@@ -37,6 +40,7 @@ let lookupState = { status: "idle" };
|
||||
let lookupGeneration = 0;
|
||||
let lookupController = null;
|
||||
let monitorTimer = null;
|
||||
let sidebarCloseTimer = null;
|
||||
|
||||
function createElement(tag, className, text) {
|
||||
const element = document.createElement(tag);
|
||||
@@ -1030,27 +1034,15 @@ function autoOpenEnabled() {
|
||||
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() {
|
||||
const manager = app.extensionManager;
|
||||
if (!manager) return false;
|
||||
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") {
|
||||
manager.setActiveSidebarTab(TAB_ID);
|
||||
if (activeSidebarId(manager, sidebar) === TAB_ID) return true;
|
||||
if (getActiveSidebarTabId(manager) === TAB_ID) return true;
|
||||
}
|
||||
|
||||
if (sidebar && "activeSidebarTabId" in sidebar) {
|
||||
@@ -1064,7 +1056,7 @@ function openSidebarTab() {
|
||||
} catch {
|
||||
// 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") {
|
||||
@@ -1082,10 +1074,29 @@ function openSidebarTab() {
|
||||
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 } = {}) {
|
||||
const nodes = getSelectedGraphNodes(app.canvas);
|
||||
const node = nodes.length === 1 ? nodes[0] : null;
|
||||
const names = node ? extractLoraNames(node) : [];
|
||||
const shouldCloseSidebar = selectedNames.length > 0 && names.length === 0;
|
||||
if (names.length) cancelSidebarClose();
|
||||
const signature = `${node?.id ?? ""}|${names
|
||||
.map(normalizeLoraIdentifier)
|
||||
.join("|")}`;
|
||||
@@ -1112,6 +1123,8 @@ function updateSelection({ autoOpen = false, force = false } = {}) {
|
||||
if (activeName) {
|
||||
if (autoOpen && autoOpenEnabled()) openSidebarTab();
|
||||
lookupActiveName();
|
||||
} else if (shouldCloseSidebar) {
|
||||
scheduleSidebarClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1169,6 +1182,7 @@ function registerSidebarTab() {
|
||||
updateSelection({ force: true });
|
||||
},
|
||||
destroy() {
|
||||
cancelSidebarClose();
|
||||
lookupGeneration += 1;
|
||||
lookupController?.abort();
|
||||
sidebarRoot?.remove();
|
||||
|
||||
@@ -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) {
|
||||
if (typeof value !== "string") return [];
|
||||
const names = [];
|
||||
|
||||
Reference in New Issue
Block a user