feat(editor): pin an Engine category, not just the section
Sections can name an inner view via pinTarget/ctx.sub; a pin's identity includes it, so two Engine categories don't evict each other.
This commit is contained in:
+2
-1
@@ -8,7 +8,8 @@
|
||||
deep: fine to walk once, tedious while dialling something in. `📌 Pin to a button` in the
|
||||
Settings header puts whatever is open into one of three slots, chosen from a dialog that
|
||||
shows what each slot currently holds and asks before replacing one. Pinning while a node
|
||||
is open pins the NODE, not the section it lives in. Slot 1 is always the leftmost button
|
||||
is open pins the NODE, and pinning inside Engine pins the CATEGORY — "Sampler algorithm",
|
||||
not "Engine" — since the section itself was never the slow part. Slot 1 is always the leftmost button
|
||||
and slot 3 the one nearest Assets; an empty slot closes up and the remaining buttons keep
|
||||
their own numbers, so a shortcut's position never moves. Hovering says where it leads.
|
||||
Pins ride with the project, like the other editor preferences. A node opened this way is a
|
||||
|
||||
@@ -1009,11 +1009,27 @@
|
||||
if (pane) pane.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
function mount(body) {
|
||||
/** Human name for a category, group included — "Sampler algorithm" alone appears under
|
||||
* more than one group, and a pin's tooltip has to be unambiguous. */
|
||||
function viewLabel(id) {
|
||||
// Guarded: this runs from mount(), and viewList reads project state that may not be
|
||||
// there yet. A throw here would blank the whole section over a tooltip.
|
||||
try {
|
||||
const found = (viewList(S.get()) || []).find((v) => v.id === id);
|
||||
if (!found) return null;
|
||||
return found.group ? `${found.group} ▸ ${found.title}` : found.title;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
function mount(body, ctx) {
|
||||
const content = el("div", "models-mount eng-mount");
|
||||
body.append(content);
|
||||
_mounted = { content };
|
||||
view = "overview";
|
||||
// A pinned button can name a category to open straight into. A category that is not in
|
||||
// the list for THIS pipeline (Studio panes with no Studio wired) falls back to the
|
||||
// overview — the sidebar visibly does not offer it, so the reason is on screen.
|
||||
const wanted = ctx && ctx.sub;
|
||||
view = (wanted && viewLabel(wanted)) ? wanted : "overview";
|
||||
|
||||
content.addEventListener("focusin", (e) => {
|
||||
const t = e.target;
|
||||
@@ -1045,6 +1061,14 @@
|
||||
iconBg: "linear-gradient(180deg,#ffb64d,#e07f1f)",
|
||||
icon: '<svg viewBox="0 0 16 16" width="13" height="13"><path d="M9.2 1.3 3 9h4.1l-1 5.7L12.9 7H8.5l.7-5.7z" fill="#fff"/></svg>',
|
||||
mount,
|
||||
// Pin the CATEGORY that is open, not the section — "Engine" is one click from anywhere
|
||||
// already; "Sampler algorithm" is the one that costs a hunt through the sidebar.
|
||||
pinTarget: () => {
|
||||
if (!view || view === "overview") return null;
|
||||
const label = viewLabel(view);
|
||||
if (!label) return null;
|
||||
return { kind: "section", id: "engine", sub: view, label: `Engine ▸ ${label}` };
|
||||
},
|
||||
});
|
||||
|
||||
window.EngineSettingsModal = {
|
||||
|
||||
@@ -30,19 +30,25 @@
|
||||
.filter((e) => e.target);
|
||||
}
|
||||
|
||||
/** Identity of a destination. `sub` is part of it: two Engine categories are the same
|
||||
* section but different places, and treating them as one would make the second pin
|
||||
* silently evict the first. */
|
||||
function pinKey(t) {
|
||||
return t ? `${t.kind}:${t.id}:${t.sub || ""}` : "";
|
||||
}
|
||||
|
||||
/** The same destination twice is a wasted slot. Placing it in `index` clears any other
|
||||
* slot pointing at the same thing. */
|
||||
function placePin(list, index, target) {
|
||||
const next = normalizePins(list);
|
||||
next.forEach((p, j) => {
|
||||
if (p && j !== index && p.kind === target.kind && p.id === target.id) next[j] = null;
|
||||
});
|
||||
const key = pinKey(target);
|
||||
next.forEach((p, j) => { if (p && j !== index && pinKey(p) === key) next[j] = null; });
|
||||
next[index] = target;
|
||||
return next;
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { SLOTS, normalizePins, visibleSlots, placePin };
|
||||
module.exports = { SLOTS, normalizePins, visibleSlots, placePin, pinKey };
|
||||
}
|
||||
if (typeof window === "undefined" || !window.dom || !window.Store) return;
|
||||
|
||||
@@ -90,7 +96,7 @@
|
||||
+ "Pin something else to that slot to replace it.");
|
||||
return;
|
||||
}
|
||||
window.SettingsWindow.open(t.id);
|
||||
window.SettingsWindow.open(t.id, t.sub || null);
|
||||
}
|
||||
|
||||
// ── the toolbar ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { SLOTS, normalizePins, visibleSlots, placePin } = require("./pinned_buttons.js");
|
||||
const { SLOTS, normalizePins, visibleSlots, placePin, pinKey } = require("./pinned_buttons.js");
|
||||
|
||||
const A = { kind: "section", id: "engine", label: "Engine" };
|
||||
const B = { kind: "node", id: "fp_lora", label: "FunPack LoRA Loader" };
|
||||
@@ -49,3 +49,34 @@ test("placing does not mutate the list it was given", () => {
|
||||
placePin(before, 1, B);
|
||||
assert.deepStrictEqual(before, [A, null, null]);
|
||||
});
|
||||
|
||||
|
||||
// ── sub-views: a place INSIDE a section is its own destination ─────────────────
|
||||
|
||||
const ENG = { kind: "section", id: "engine", label: "Engine" };
|
||||
const ENG_SAMPLER = { kind: "section", id: "engine", sub: "studio_sampler",
|
||||
label: "Engine ▸ FunPack Studio ▸ Sampler algorithm" };
|
||||
const ENG_GUIDANCE = { kind: "section", id: "engine", sub: "chain_guidance",
|
||||
label: "Engine ▸ Chain Sampler ▸ Guidance" };
|
||||
|
||||
test("two categories of one section are different destinations", () => {
|
||||
assert.notStrictEqual(pinKey(ENG_SAMPLER), pinKey(ENG_GUIDANCE));
|
||||
// Pinning the second must NOT evict the first — that was the failure the key prevents.
|
||||
const after = placePin([ENG_SAMPLER, null, null], 1, ENG_GUIDANCE);
|
||||
assert.deepStrictEqual(after, [ENG_SAMPLER, ENG_GUIDANCE, null]);
|
||||
});
|
||||
|
||||
test("a section and one of its categories are different destinations", () => {
|
||||
assert.notStrictEqual(pinKey(ENG), pinKey(ENG_SAMPLER));
|
||||
assert.deepStrictEqual(placePin([ENG, null, null], 2, ENG_SAMPLER), [ENG, null, ENG_SAMPLER]);
|
||||
});
|
||||
|
||||
test("the SAME category still never occupies two slots", () => {
|
||||
assert.deepStrictEqual(placePin([ENG_SAMPLER, null, null], 2, ENG_SAMPLER),
|
||||
[null, null, ENG_SAMPLER]);
|
||||
});
|
||||
|
||||
test("a missing sub is the same as an empty one", () => {
|
||||
assert.strictEqual(pinKey(ENG), pinKey({ kind: "section", id: "engine", sub: "" }));
|
||||
assert.strictEqual(pinKey(null), "");
|
||||
});
|
||||
|
||||
@@ -62,7 +62,9 @@
|
||||
cleanup = null;
|
||||
}
|
||||
|
||||
function show(id) {
|
||||
// `sub` names a place INSIDE a section (an Engine category, say). Sections that have
|
||||
// inner views read it from ctx on mount; the rest ignore it harmlessly.
|
||||
function show(id, subView) {
|
||||
const spec = registry.find((s) => s.id === id) || orderedSections()[0];
|
||||
if (!spec || !overlay) return;
|
||||
teardownSection();
|
||||
@@ -78,6 +80,7 @@
|
||||
const ctx = {
|
||||
setActions: (nodes) => { clear(actionsEl); (nodes || []).forEach((n) => actionsEl.append(n)); },
|
||||
openSection: (sid) => show(sid),
|
||||
sub: subView || null,
|
||||
};
|
||||
cleanup = spec.mount(bodyEl, ctx) || null;
|
||||
renderNav();
|
||||
@@ -93,11 +96,11 @@
|
||||
if (wasOpen) window.Store?.resumeSave?.();
|
||||
}
|
||||
|
||||
function open(id) {
|
||||
function open(id, subView) {
|
||||
// Recover if something removed our overlay from the DOM directly
|
||||
// (e.g. the tour's closeModalOverlay sweep) without calling close().
|
||||
if (overlay && !overlay.isConnected) { close(); }
|
||||
if (overlay) { show(id || activeId); return; }
|
||||
if (overlay) { show(id || activeId, subView); return; }
|
||||
query = "";
|
||||
// Held until close(). Paired with the overlay's lifetime, so only on this branch.
|
||||
window.Store?.suspendSave?.();
|
||||
@@ -134,7 +137,7 @@
|
||||
overlay.append(win);
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
||||
document.body.append(overlay);
|
||||
show(id);
|
||||
show(id, subView);
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
|
||||
Reference in New Issue
Block a user