feat(sidebar): picks priority — drag-link menu, spotlight on add, modal hardening (#76)
* fix(sidebar): merge preset HEAD route into GET handler PR #68 shipped koolook_routes.py with @routes.get + @routes.head on the same path. aiohttp's add_get auto-registers HEAD by default, so the explicit @routes.head collided with the auto-synthesized HEAD and crashed ComfyUI startup with "Added route will never be executed, method HEAD is already registered". Trying allow_head=False on the GET to opt out triggered a second crash: ComfyUI's server.py mirrors every PromptServer route into a parallel /api/... table via api_routes.route(method, path)(handler, **kwargs), and RouteTableDef.route's inner closure rejects unknown kwargs with TypeError. So no kwarg-based opt-out is viable. Drop the dedicated HEAD handler. Aiohttp's auto-HEAD-from-GET delivers HEAD probes to get_preset; we branch on request.method to skip the file read for HEAD — preserving the cheap-existence optimization the client's presetExists relies on for multi-MB JSON over Dropbox / iCloud / NFS-mounted libraries. CHANGELOG [Unreleased] updated to match. This bug never reached users — koolook_routes.py was added in #68 (post-v0.2.0) and was never published to Comfy Registry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(sidebar): Tools row + starter preset replaces curated_defaults Two coordinated UX changes for fresh-install distribution + admin ergonomics — designed together and shipped as one unit. 1. New "Tools" row sits between the Snapshot row and the search field. Moves the Export and "Install missing for picks" buttons up out of the Nodes row (admin/advanced operations don't belong in the everyday + Add toolbar). Adds a new "Drop placeholders onto canvas" button — the security_level=normal escape hatch for installing missing packs: instantiates one placeholder per missing pack on a fresh canvas tab so ComfyUI/Manager's standard "Install Missing Custom Nodes" detection takes over via Manager's UI flow (which uses different gates than our programmatic /customnode/install/ git_url call). Nodes row keeps only the everyday + button. 2. curated_defaults.json retired in favor of starter_preset.json. Fresh installs no longer get picks seeded directly into localStorage on first load. Instead the bundled web/starter_preset.json (full snapshot format — picks + workflows + tags + archive) is copied into the user's snapshot library directory as starter.json. The user opens Snapshot -> Load -> starter to apply in one click. Unifies fresh-install distribution with the per-user snapshot library (#68); maintainer flow becomes "build state in ComfyUI -> click the (down-arrow) Tools-row button -> paste over web/starter_preset.json -> commit." Existing users with non-empty picks are explicitly skipped by the new seeder, so their state is untouched. Removed: seedDefaultsIfNeeded, exportPicks, web/curated_defaults.json, SEEDED_KEY, DEFAULTS_URL. Added: seedStarterPresetIfNeeded, exportStarterPreset (snapshot.js), STARTER_SEEDED_KEY, STARTER_URL, STARTER_PRESET_FILENAME, discoverMissingPacks (installer.js), dropPlaceholdersForPacks (canvas_io.js), web/starter_preset.json. Docs: CLAUDE.md "Curated Nodes sidebar — distributed defaults" -> "Kforge Labs sidebar — starter preset distribution". docs/maintainers/curated-sidebar.md rewritten. CHANGELOG.md [Unreleased] updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(sidebar): picks priority — drag-link menu, spotlight on add, modal hardening Three coordinated UX features for curated picks plus two underlying bug fixes uncovered while wiring them. 1. Drag-link-release menu hoists curated picks. When a connection is dragged from a socket and released on empty canvas, ComfyUI's compat- filtered suggestion list now sorts items in the user's curated picks above everything else, with a "── from your picks ──" separator before the rest. Detection via the "Add Node" + "Add Reroute" sentinel pair in the menu's options (narrow enough that other ContextMenu uses are untouched). Purely additive — no compatible nodes are removed, only re-ordered. New `web/sidebar/connection_menu.js` exports a `patchConnectionMenu()` registered alongside `patchCanvasMenu()`. 2. Spotlight effect on add. Clicking the toolbar `+` (or the canvas right-click "Add to Kforge Labs") collapses every Nodes-section pack folder, then auto-expands the pack + subcategory the just-saved node lives in — pedagogical aid that helps users learn which pack each node belongs to. Multi-select adds light up every hit pack simultaneously. Duplicate adds get the spotlight too (the educational reminder still applies). New helper `findPackPathForType` mirrors the gather code's REPO-precedence-then-user-pick-fallback. New exported `spotlightAddedPicks(typeNames)` does the collapse + pin sequence. 3. Modal drag-out-of-input no longer dismisses the dialog. Shared modal shell `makeModalShell` gained a mousedown / click-intent check: a click on the overlay only fires dismiss when the gesture STARTED on the overlay too. Drag-selecting text inside an input that releases in the overlay's dark area used to dismiss the modal mid-edit. Affects every Koolook dialog. Snapshot Settings path field also gained a `title` tooltip so the full path is hover-readable. Bug fixes uncovered + carried in this PR: - `pinExpanded` paths now expand on the immediate render. buildFolder's resolution chain gained a `pinnedPaths` check between `forceExpanded` and `pathStates`. Phase 3 of `renderTree` writes pins into pathStates AFTER Phase 2 builds the DOM, so without this check the pin had no effect on the current render. Improves the existing workflow-save pinning (visibly correct now) and is load-bearing for the new spotlight feature. CHANGELOG [Unreleased] updated with all five entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a6a31c8086
commit
96b07a2c12
@@ -7,6 +7,56 @@ The format is inspired by Keep a Changelog and SemVer.
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Drag-link-release menu hoists curated picks to the top.** When the user
|
||||
drags a connection out of a node socket and releases on empty canvas,
|
||||
ComfyUI's compatibility-filtered suggestion list now sorts items in the
|
||||
user's curated picks above everything else, with a `── from your picks ──`
|
||||
separator between hoisted and unhoisted compatible nodes. Implementation
|
||||
monkey-patches `LiteGraph.ContextMenu` and detects the connection-release
|
||||
menu via the `Add Node` + `Add Reroute` operation-sentinel pair (narrow
|
||||
enough that other ContextMenu uses — right-click on a node, on the
|
||||
canvas — are untouched). Purely additive: no compatible nodes are
|
||||
removed, only re-ordered. Wired in `web/sidebar/connection_menu.js`,
|
||||
registered alongside the existing `patchCanvasMenu` in
|
||||
`web/koolook_sidebar.js`.
|
||||
- **Spotlight effect on add.** Clicking the toolbar `+` (or the canvas
|
||||
right-click "Add to Kforge Labs") now collapses every Nodes-section
|
||||
pack folder, then auto-expands just the pack + subcategory the just-saved
|
||||
node lives in — a pedagogical aid that helps new users internalize
|
||||
which pack each node belongs to. Multi-select adds light up every hit
|
||||
pack simultaneously. Duplicate adds (already in picks) trigger the
|
||||
same spotlight, since the educational reminder still applies. Helper
|
||||
`findPackPathForType` mirrors the gather code's REPO-precedence-then-
|
||||
user-pick-fallback to locate any node ID's sidebar path; new exported
|
||||
`spotlightAddedPicks(typeNames)` from `web/sidebar/tree.js` does the
|
||||
collapse + pin sequence and is called from both add paths.
|
||||
|
||||
### Fixed
|
||||
- **`pinExpanded` paths now expand on the immediate render** instead of
|
||||
being delayed by one. `buildFolder`'s expansion-resolution chain gained
|
||||
a `pinnedPaths` check between `forceExpanded` and `pathStates`. Phase 3
|
||||
of `renderTree` writes pins into `pathStates` AFTER Phase 2 has built
|
||||
the DOM, so without the new check the pin had no effect on the current
|
||||
render — only on the next one. Side-effect-free correctness improvement
|
||||
for the workflow-save pinning that already existed; load-bearing for
|
||||
the new spotlight feature above which depends on pinned paths
|
||||
expanding immediately.
|
||||
- **Modal drag-out-of-input no longer dismisses the dialog.** The shared
|
||||
modal shell (`makeModalShell`) used by every Koolook dialog gained a
|
||||
mousedown / click-intent check: a click on the overlay now only fires
|
||||
the dismiss when the gesture *started* on the overlay too. Drag-
|
||||
selecting text inside an input field that releases in the overlay's
|
||||
dark area used to trigger `click` with `target=overlay` and dismiss
|
||||
the modal mid-edit; now it stays open. Affects all dialogs including
|
||||
Snapshot Save / Load / Settings, Install Missing, Save Workflow, Tags,
|
||||
Confirm, Input — load-bearing especially for the Snapshot Settings
|
||||
path field where the long absolute path encourages drag-selection.
|
||||
- **Snapshot Settings path field shows full path on hover.** Added a
|
||||
`title` attribute synced to the saved library path on every refresh
|
||||
and save, so the user can read the full path without scrolling /
|
||||
selecting inside the narrow input.
|
||||
|
||||
### Added (continued)
|
||||
- **"Install missing for picks" toolbar button** in the Nodes section
|
||||
(`pi-cloud-download` icon, next to Add and Export). Walks the user's
|
||||
picks against ComfyUI-Manager's `/customnode/getmappings` mapping,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "./sidebar/workflows_store.js";
|
||||
import { renderPanel } from "./sidebar/tree.js";
|
||||
import { patchCanvasMenu } from "./sidebar/canvas_menu.js";
|
||||
import { patchConnectionMenu } from "./sidebar/connection_menu.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "koolook.curated_sidebar",
|
||||
@@ -48,5 +49,6 @@ app.registerExtension({
|
||||
render: (el) => renderPanel(el),
|
||||
});
|
||||
patchCanvasMenu();
|
||||
patchConnectionMenu();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// =============================================================================
|
||||
import { toast } from "./constants.js";
|
||||
import { addToMyPicks, notifyPicksChanged } from "./picks_store.js";
|
||||
import { spotlightAddedPicks } from "./tree.js";
|
||||
|
||||
export function patchCanvasMenu() {
|
||||
const C = (typeof LGraphCanvas !== "undefined") ? LGraphCanvas : null;
|
||||
@@ -32,9 +33,14 @@ export function patchCanvasMenu() {
|
||||
const status = addToMyPicks(node.type);
|
||||
if (status === "added") {
|
||||
toast(`Added "${node.title || node.type}" to favorites.`);
|
||||
spotlightAddedPicks([node.type]);
|
||||
notifyPicksChanged();
|
||||
} else if (status === "duplicate") {
|
||||
toast("Already in favorites.");
|
||||
// Spotlight on duplicate too — the user reached for this
|
||||
// exact node; remind them where it lives.
|
||||
spotlightAddedPicks([node.type]);
|
||||
notifyPicksChanged();
|
||||
} else {
|
||||
toast(`Failed to save "${node.title || node.type}". See console.`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// =============================================================================
|
||||
// Drag-link-release menu — curated picks priority.
|
||||
//
|
||||
// When the user drags a connection out of a node socket and releases on empty
|
||||
// canvas, LiteGraph shows a context menu of operations + compatible nodes
|
||||
// (filtered by socket type). Default order is whatever LiteGraph emits,
|
||||
// which is essentially "all installed nodes that match the type, in
|
||||
// registration order." On a busy ComfyUI install this menu can be 20+ items
|
||||
// of nodes the user has never used.
|
||||
//
|
||||
// We hoist the user's curated picks (those that ALSO match the dragged
|
||||
// socket's type) to the top of the suggestion list, immediately below
|
||||
// LiteGraph's operation entries (Add Node / Add Reroute / Search / Reroute).
|
||||
// Other compatible nodes still render below — purely additive ordering, no
|
||||
// information loss.
|
||||
//
|
||||
// Strategy: monkey-patch `LiteGraph.ContextMenu` globally and detect the
|
||||
// connection-release menu by sentinel items in its options array. Detection
|
||||
// requires BOTH "Add Node" AND "Add Reroute" to be present — narrow enough
|
||||
// that other ContextMenu uses (right-click on a node, on the canvas, etc.)
|
||||
// are untouched, while broad enough to survive minor LiteGraph variations.
|
||||
//
|
||||
// LiteGraph fork variants used by ComfyUI may shape the suggestion items
|
||||
// either with `content: <typeId>` or `content: <displayName>` and a parallel
|
||||
// `value: <typeId>`. We check both, so the picks lookup matches whichever
|
||||
// shape is in flight.
|
||||
// =============================================================================
|
||||
import { loadUserPicks } from "./picks_store.js";
|
||||
|
||||
const SEPARATOR_LABEL = "── from your picks ──";
|
||||
|
||||
let installed = false;
|
||||
|
||||
export function patchConnectionMenu() {
|
||||
const LG = (typeof LiteGraph !== "undefined") ? LiteGraph : null;
|
||||
if (!LG || typeof LG.ContextMenu !== "function") {
|
||||
console.warn("[Koolook] LiteGraph.ContextMenu not available; connection-menu priority skipped.");
|
||||
return;
|
||||
}
|
||||
// Idempotency: this is a global constructor swap. A re-load (HMR /
|
||||
// multiple registerExtension calls) would otherwise stack wrappers,
|
||||
// running the priority logic N times per ContextMenu construction —
|
||||
// performance + duplicate "── from your picks ──" separators.
|
||||
if (LG.__koolookContextMenuPatched) return;
|
||||
LG.__koolookContextMenuPatched = true;
|
||||
|
||||
const origContextMenu = LG.ContextMenu;
|
||||
|
||||
function PatchedContextMenu(options, parentMenuOptions) {
|
||||
let finalOptions = options;
|
||||
try {
|
||||
if (looksLikeConnectionMenu(options)) {
|
||||
finalOptions = reorderByPicks(options);
|
||||
}
|
||||
} catch (e) {
|
||||
// Never let a reorder bug break the menu — fall back to the
|
||||
// original options array so the user always gets SOME menu.
|
||||
console.warn("[Koolook] connection-menu reorder failed, falling back:", e);
|
||||
finalOptions = options;
|
||||
}
|
||||
return new origContextMenu(finalOptions, parentMenuOptions);
|
||||
}
|
||||
|
||||
// Preserve the prototype chain so `instanceof LiteGraph.ContextMenu`
|
||||
// checks elsewhere in LiteGraph / ComfyUI still pass, and any static
|
||||
// properties on the original constructor remain reachable through ours.
|
||||
PatchedContextMenu.prototype = origContextMenu.prototype;
|
||||
Object.setPrototypeOf(PatchedContextMenu, origContextMenu);
|
||||
LG.ContextMenu = PatchedContextMenu;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Detection + reorder
|
||||
// =============================================================================
|
||||
|
||||
// Detects the drag-link-release menu by looking for the operation sentinels
|
||||
// LiteGraph emits at the top: BOTH "Add Node" AND "Add Reroute" must be
|
||||
// present. The right-click canvas menu has "Add Node" but not "Add Reroute",
|
||||
// so this filter doesn't accidentally re-shape unrelated menus.
|
||||
function looksLikeConnectionMenu(options) {
|
||||
if (!Array.isArray(options) || options.length === 0) return false;
|
||||
let hasAddNode = false, hasAddReroute = false;
|
||||
for (const opt of options) {
|
||||
if (!opt || typeof opt.content !== "string") continue;
|
||||
if (opt.content === "Add Node") hasAddNode = true;
|
||||
else if (opt.content === "Add Reroute") hasAddReroute = true;
|
||||
if (hasAddNode && hasAddReroute) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// LiteGraph sets `content` to either the node type ID directly or to the
|
||||
// node's display title (which can include spaces, emoji, decorations).
|
||||
// Our picks list stores type IDs, so we check `value` first (when present
|
||||
// it's typically the type ID) and fall back to `content`.
|
||||
function pickIdOf(opt) {
|
||||
if (!opt) return null;
|
||||
if (typeof opt.value === "string") return opt.value;
|
||||
if (typeof opt.content === "string") return opt.content;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Operation entries that always sit at the top of LiteGraph's connection
|
||||
// menu — preserve their position regardless of pick membership.
|
||||
const OPERATIONS = new Set(["Add Node", "Add Reroute", "Search", "Reroute"]);
|
||||
|
||||
function reorderByPicks(options) {
|
||||
const picks = new Set(loadUserPicks());
|
||||
if (picks.size === 0) return options;
|
||||
|
||||
// Walk the options array linearly. The operation block is typically
|
||||
// contiguous at the top (Add Node, Add Reroute, Search, plus the two
|
||||
// Reroute insertion entries); everything after is a node suggestion.
|
||||
// Stop the operation slice at the first non-operation, non-null item —
|
||||
// null entries are LiteGraph separators which we want to leave wherever
|
||||
// LiteGraph put them.
|
||||
let operationsEnd = 0;
|
||||
while (operationsEnd < options.length) {
|
||||
const opt = options[operationsEnd];
|
||||
if (opt === null) {
|
||||
operationsEnd += 1;
|
||||
continue;
|
||||
}
|
||||
if (opt && typeof opt.content === "string" && OPERATIONS.has(opt.content)) {
|
||||
operationsEnd += 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const operations = options.slice(0, operationsEnd);
|
||||
const suggestions = options.slice(operationsEnd);
|
||||
|
||||
// Stable partition: picks first, others second. Skipping non-pick-able
|
||||
// entries (separators, weird shapes) puts them in `others` to preserve
|
||||
// their relative order with the rest of the suggestion block.
|
||||
const picksHits = [];
|
||||
const others = [];
|
||||
for (const opt of suggestions) {
|
||||
const id = pickIdOf(opt);
|
||||
if (id != null && picks.has(id)) picksHits.push(opt);
|
||||
else others.push(opt);
|
||||
}
|
||||
|
||||
// No pick hits → no reordering needed; return the original array reference
|
||||
// so we don't pay the array-allocation cost in the common no-match path.
|
||||
if (picksHits.length === 0) return options;
|
||||
|
||||
const reordered = [...operations, ...picksHits];
|
||||
if (others.length > 0) {
|
||||
// Inert separator label so the user sees where their picks end and
|
||||
// ComfyUI's default suggestions begin. `disabled: true` prevents
|
||||
// hover/click activation; LiteGraph still renders it as a row.
|
||||
reordered.push({ content: SEPARATOR_LABEL, disabled: true });
|
||||
reordered.push(...others);
|
||||
}
|
||||
return reordered;
|
||||
}
|
||||
+19
-1
@@ -57,8 +57,20 @@ function makeModalShell({ title, body, actions }) {
|
||||
};
|
||||
|
||||
overlay.appendChild(modal);
|
||||
// Click-to-dismiss with drag-out-of-input protection. A single click
|
||||
// fires `mousedown` then `mouseup` then `click` — and `click.target`
|
||||
// is the deepest common ancestor of mousedown's and mouseup's targets.
|
||||
// So if you drag-select text inside an input and release in the
|
||||
// overlay's dark area, click fires with target=overlay and the modal
|
||||
// would otherwise close mid-edit. Track whether the gesture STARTED
|
||||
// on the overlay too; only dismiss if both ends did.
|
||||
let mouseDownOnOverlay = false;
|
||||
overlay.addEventListener("mousedown", (e) => {
|
||||
mouseDownOnOverlay = (e.target === overlay);
|
||||
});
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (e.target === overlay) close();
|
||||
if (e.target === overlay && mouseDownOnOverlay) close();
|
||||
mouseDownOnOverlay = false;
|
||||
});
|
||||
escHandler = (e) => { if (e.key === "Escape") close(); };
|
||||
document.addEventListener("keydown", escHandler);
|
||||
@@ -1233,6 +1245,11 @@ export function showSnapshotSettingsDialog({
|
||||
return;
|
||||
}
|
||||
input.value = settings.savedLibraryPath || "";
|
||||
// Native tooltip surfaces the full path on hover — the input is only
|
||||
// ~440px wide so long paths get truncated by horizontal scroll.
|
||||
// The user can still drag-select to view, but the tooltip avoids
|
||||
// making them do so.
|
||||
input.title = settings.savedLibraryPath || "";
|
||||
input.disabled = false;
|
||||
let sourceLabel;
|
||||
if (settings.source === "settings") sourceLabel = "from this Settings panel";
|
||||
@@ -1252,6 +1269,7 @@ export function showSnapshotSettingsDialog({
|
||||
hint.textContent =
|
||||
`Currently in effect: ${result.resolvedPath}\n(source: ${sourceLabel})`;
|
||||
input.value = result.savedLibraryPath || "";
|
||||
input.title = result.savedLibraryPath || "";
|
||||
toast(rawValue
|
||||
? "Library path saved."
|
||||
: "Override cleared — using env var / default."
|
||||
|
||||
+100
-4
@@ -185,6 +185,51 @@ function pinExpanded(paths) {
|
||||
for (const p of paths) pinnedPaths.add(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spotlight effect for newly-saved picks: collapse every folder under the
|
||||
* Nodes section, then pin the just-added node's pack-folder + subcategory-
|
||||
* folder open. Designed as a pedagogical aid — when the user adds a node
|
||||
* via the toolbar `+` or the right-click "Add to Kforge Labs", the sidebar
|
||||
* snaps to focus on exactly where that node lives in the pack hierarchy,
|
||||
* helping newcomers internalize which pack/category each node belongs to.
|
||||
*
|
||||
* Caller is responsible for triggering a re-render afterwards (typically
|
||||
* via `notifyPicksChanged()`); this function only mutates `pathStates` +
|
||||
* `pinnedPaths`.
|
||||
*
|
||||
* Multi-add (caller passes several types): every pack/subcategory hit by
|
||||
* the additions is pinned open. So adding 5 nodes from 3 packs lights up
|
||||
* all 3 pack folders simultaneously rather than just the last.
|
||||
*
|
||||
* Idempotent / safe: types not in the LiteGraph registry are silently
|
||||
* skipped (no-op). Empty input → no-op (no collapse, no pin).
|
||||
*
|
||||
* @param {string[]} typeNames
|
||||
*/
|
||||
export function spotlightAddedPicks(typeNames) {
|
||||
if (!Array.isArray(typeNames) || typeNames.length === 0) return;
|
||||
const pathsToExpand = new Set();
|
||||
for (const t of typeNames) {
|
||||
const loc = findPackPathForType(t);
|
||||
if (!loc) continue;
|
||||
pathsToExpand.add(`${SECTION_ID_NODES}/${loc.packLabel}`);
|
||||
pathsToExpand.add(`${SECTION_ID_NODES}/${loc.packLabel}/${loc.sub}`);
|
||||
}
|
||||
if (pathsToExpand.size === 0) return;
|
||||
|
||||
// Collapse every existing Nodes-section entry (the section root + every
|
||||
// pack/subcategory). The render-time fallback (`startExpanded`) keeps
|
||||
// the section header itself open since `addSection` registers it with
|
||||
// `startExpanded: true`; pack folders default to closed, which is what
|
||||
// we want for everything except the spotlighted location.
|
||||
for (const key of [...pathStates.keys()]) {
|
||||
if (key === SECTION_ID_NODES || key.startsWith(SECTION_ID_NODES + "/")) {
|
||||
pathStates.delete(key);
|
||||
}
|
||||
}
|
||||
pinExpanded([...pathsToExpand]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Single render entry point. Idempotent — safe to call from search input,
|
||||
* picks-changed, workflows-changed, or storage events.
|
||||
@@ -443,6 +488,40 @@ function subcategoryFor(category, categoryRoot) {
|
||||
return parts.length > 1 ? parts.slice(1).join("/") : category;
|
||||
}
|
||||
|
||||
// Given a node ID, compute the (packLabel, sub) pair the gather code would
|
||||
// place it under in the sidebar. Mirrors the precedence in
|
||||
// `gatherNodesByRepo` + `gatherUserPickPacks`: registered REPOS first
|
||||
// (`select="all"` matches by category prefix; `select=Array` matches by
|
||||
// explicit ID), then the user-picks fallback derives the pack label from
|
||||
// the first segment of the node's category.
|
||||
//
|
||||
// Returns `{packLabel, sub}` on success, `null` if the node isn't in the
|
||||
// LiteGraph registry (caller should skip silently — the node won't render
|
||||
// in the sidebar either way). Used by `spotlightAddedPicks` to know which
|
||||
// folder to expand after a save.
|
||||
function findPackPathForType(typeName) {
|
||||
const registry = (typeof LiteGraph !== "undefined" && LiteGraph.registered_node_types) || {};
|
||||
const nc = registry[typeName];
|
||||
if (!nc) return null;
|
||||
const cat = (nc && nc.category) || "";
|
||||
|
||||
for (const repo of REPOS) {
|
||||
if (repo.select === "all") {
|
||||
const root = repo.categoryRoot || "";
|
||||
if (root && (cat === root || cat.startsWith(root + "/"))) {
|
||||
return { packLabel: repo.label, sub: subcategoryFor(cat, root) };
|
||||
}
|
||||
} else if (Array.isArray(repo.select)) {
|
||||
if (repo.select.includes(typeName)) {
|
||||
return { packLabel: repo.label, sub: subcategoryFor(cat, repo.categoryRoot || "") };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const packLabel = cat.split("/")[0] || "(uncategorized)";
|
||||
return { packLabel, sub: subcategoryFor(cat, packLabel) };
|
||||
}
|
||||
|
||||
function matchesQuery(display, type, q) {
|
||||
if (!q) return true;
|
||||
return display.toLowerCase().includes(q) || type.toLowerCase().includes(q);
|
||||
@@ -804,11 +883,19 @@ function buildFolder({ name, count, iconKind, childrenBuilder, onContextMenu, st
|
||||
|
||||
// Resolution order:
|
||||
// 1. forceExpanded (e.g. search active) — overrides everything
|
||||
// 2. pathStates (user has previously toggled this folder)
|
||||
// 3. startExpanded (the natural default for this folder)
|
||||
// 2. pinnedPaths (a save/spotlight flow has flagged this for force-open
|
||||
// on the current render) — Phase 3 writes pins into pathStates AFTER
|
||||
// Phase 2 builds DOM, so without a direct pinnedPaths read here the
|
||||
// pin would only take effect on the *next* render. The spotlight
|
||||
// effect for newly-added picks depends on pinned paths expanding
|
||||
// on the immediate render.
|
||||
// 3. pathStates (user has previously toggled this folder)
|
||||
// 4. startExpanded (the natural default for this folder)
|
||||
let initiallyExpanded;
|
||||
if (forceExpanded) {
|
||||
initiallyExpanded = true;
|
||||
} else if (pinnedPaths.has(path)) {
|
||||
initiallyExpanded = true;
|
||||
} else if (pathStates.has(path)) {
|
||||
initiallyExpanded = pathStates.get(path);
|
||||
} else {
|
||||
@@ -1517,21 +1604,30 @@ export function renderPanel(container) {
|
||||
let added = 0;
|
||||
let duplicates = 0;
|
||||
let failed = 0;
|
||||
const successfulTypes = [];
|
||||
for (const t of types) {
|
||||
const status = addToMyPicks(t);
|
||||
if (status === "added") added += 1;
|
||||
else if (status === "duplicate") duplicates += 1;
|
||||
else failed += 1;
|
||||
// Spotlight even on duplicates — the user clicked + with this node
|
||||
// selected; reminding them where it lives is the whole feature.
|
||||
if (status !== "failed") successfulTypes.push(t);
|
||||
}
|
||||
spotlightAddedPicks(successfulTypes);
|
||||
if (added > 0) {
|
||||
const noun = added === 1 ? "node" : "nodes";
|
||||
toast(`Added ${added} ${noun} to favorites.`);
|
||||
notifyPicksChanged();
|
||||
} else if (duplicates > 0 && failed === 0) {
|
||||
toast("Already in favorites.");
|
||||
// Re-render anyway so the spotlight effect lands even on pure-
|
||||
// duplicate clicks. notifyPicksChanged is the agreed-upon trigger
|
||||
// for any sidebar refresh, even when picks didn't actually change.
|
||||
notifyPicksChanged();
|
||||
}
|
||||
if (failed > 0) {
|
||||
toast(`Failed to save ${failed} pick${failed === 1 ? "" : "s"}. See console.`);
|
||||
} else if (added === 0 && duplicates > 0) {
|
||||
toast("Already in favorites.");
|
||||
}
|
||||
});
|
||||
nodesRow.appendChild(addBtn);
|
||||
|
||||
Reference in New Issue
Block a user