Files
olivv-cs-ComfyUI-FunPack/app/shell/settings_window.js
T
digital-garbage e6e4dfa882 app: mount every Settings section in place instead of deep-linking out
Picking Node packs, Updates, the log or Temp files used to close the
Settings window and open that feature's own separate modal, with no way
back except reopening Settings from the menu bar -- not what v4 does,
and not what "unified" is supposed to mean. Each of those five files
(plus pipeline_window.js) now exports mount({ setFooter, close }), the
same content its standalone open() already built, taking the modal
chrome's footer and close from whoever hosts it instead of owning a
modal directly. open() is unchanged for anyone opening one alone --
pipeline_window.test.js and every Playwright spec for these five prove
it, unedited, still passing.

Also fixes two findings from the adversarial pass on the earlier
deep-link version: filterList's search only matched a section's visible
label/hint, so a section findable only by a keyword (e.g. "loaders" for
"Models and pipeline") silently vanished from the filtered list; and
createSettingsWindow's "one at a time" state lived at module scope, so
a second independent instance would have redrawn the first one's modal
instead of opening its own. Both now have regression tests.
2026-09-07 01:06:02 +03:00

122 lines
4.9 KiB
JavaScript

// One home for everything that is a PREFERENCE about the app or the pipeline,
// rather than an edit to the project on screen -- replacing five separate
// menu items (Models and pipeline, Updates, Node packs, Log, Temp files) that
// nothing tied together and nothing let you search.
//
// Every section is MOUNTED here, in place, beside the same always-visible
// nav list -- picking "Node packs" does not leave this window any more than
// clicking a different System Settings pane leaves System Settings. Each of
// pipeline_window.js/updates.js/packs.js/logwindow.js/tempfiles.js already
// had its content built separately from its own modal chrome (a `body`
// region.stack, footer buttons set on the modal handle); their `mount()`
// exports are that same content, taking `setFooter`/`close` from whoever is
// hosting them instead of owning a modal directly. Their `open()` exports
// are unchanged for anyone opening one standalone.
import { composer } from "../composer/composer.js";
/**
* createSettingsWindow({ sections, gitStatus }) -> { open, close }
*
* `sections`: [{ id, title, subtitle, keywords, icon,
* mount({ setFooter, close }) -> handle }].
* `mount()` returns a composer element handle (its own `destroy()` is the
* cleanup, called when another section is picked or the window closes).
* `setFooter`/`close` act on this window's own modal chrome -- a section
* does not know or care whether it is standing alone or hosted here.
*
* `gitStatus` is injected, not imported, for the same reason every other
* shell window takes its network calls as props: testable without a server,
* and unable to reach for one that was not handed to it.
*
* `current` lives HERE, per call, not at module scope: a module-level
* singleton would let a second independent settings window (there is only
* ever one in this app today, but nothing enforced that) silently redraw
* the FIRST one's modal instead of opening its own.
*/
export function createSettingsWindow({ sections = [], gitStatus } = {}) {
const all = [aboutSection(gitStatus), ...sections];
let current = null;
function open(id) {
if (current) { current.show(id); return current; }
let activeCleanup = null;
const contentStack = composer.region.stack({ gap: "sm", fill: true });
function showSection(pick) {
const spec = all.find((s) => s.id === pick) || all[0];
if (!spec) return;
if (activeCleanup) { activeCleanup.destroy?.(); activeCleanup = null; }
// Cleared before mounting: a section that sets no footer of its own
// (About) must not go on showing the PREVIOUS section's buttons.
modal.setFooter({});
const content = spec.mount({
setFooter: (f) => modal.setFooter(f),
close: () => modal.close("done"),
});
activeCleanup = content;
contentStack.set([
composer.header.md({ text: spec.title }),
spec.subtitle ? composer.hint.default({ text: spec.subtitle }) : null,
content,
]);
nav.setValue(spec.id);
}
const nav = composer.filterList.md({
items: all.map((s) => ({ id: s.id, label: s.title, hint: s.subtitle, icon: s.icon, keywords: s.keywords })),
value: id,
onChange: showSection,
placeholder: "Search settings",
});
const body = composer.splitPane.h({ panes: [nav, contentStack], size: 25, min: 15, label: "Settings" });
const modal = composer.modal.generic({
title: "Settings", size: "xl", body,
onClose: () => {
if (activeCleanup) { activeCleanup.destroy?.(); activeCleanup = null; }
if (current === handle) current = null;
},
});
const handle = { node: modal.node, close: () => modal.close(), show: showSection };
current = handle;
showSection(id || (all[0] && all[0].id));
return handle;
}
function close() {
if (current) current.close();
}
return { open, close };
}
/** Version and branch -- the one thing everyone with the window open at all
* might be there to check, so it is built in rather than one more click away. */
function aboutSection(gitStatus) {
return {
id: "about", title: "About FunPack",
subtitle: "The version and branch running right now.",
keywords: "about version commit branch",
icon: "◉",
mount() {
const stack = composer.region.stack({ gap: "sm" });
const rows = composer.region.stack({ gap: "none" });
const row = (label, value) => composer.settingsRow.default({
label, control: composer.text.sm({ text: value }) });
rows.set([row("Version", "…"), row("Branch", "…")]);
stack.set([rows]);
if (gitStatus) {
gitStatus().then((s) => {
rows.set([row("Version", s.version || "unknown"), row("Branch", s.branch || "?")]);
}).catch(() => {
rows.set([composer.hint.default({ text: "Could not read version information." })]);
});
}
return stack;
},
};
}