Workspace: private folder per account, owner-managed shared folders, Save asks where, Files tab
This commit is contained in:
@@ -8,6 +8,7 @@ import { RoomSync } from "./sync.js";
|
||||
import { Presence } from "./presence.js";
|
||||
import { RigSharePanel } from "./panel.js";
|
||||
import { STYLES } from "./styles.js";
|
||||
import { installSaveDialog } from "./save-dialog.js";
|
||||
|
||||
const TAB_ID = "rigshare";
|
||||
const ICON_CLASS = "rigshare-tab-icon";
|
||||
@@ -241,6 +242,7 @@ app.registerExtension({
|
||||
installQueueGuard(client);
|
||||
installManagerGuard(client);
|
||||
installComfyAccountGuard(client);
|
||||
installSaveDialog(app, api, client);
|
||||
presence.install();
|
||||
|
||||
app.extensionManager.registerSidebarTab({
|
||||
|
||||
+148
-25
@@ -7,7 +7,8 @@ import { ServerWidget } from "./server-widget.js";
|
||||
const ICON_URL = new URL("./assets/icon.svg", import.meta.url).href;
|
||||
|
||||
const VIEWS = [
|
||||
{ id: "workflows", icon: "pi-share-alt", label: "Workflows" },
|
||||
{ id: "workflows", icon: "pi-share-alt", label: "Live" },
|
||||
{ id: "files", icon: "pi-folder", label: "Files" },
|
||||
{ id: "people", icon: "pi-users", label: "People" },
|
||||
{ id: "chat", icon: "pi-comments", label: "Chat" },
|
||||
{ id: "server", icon: "pi-server", label: "Server" },
|
||||
@@ -182,6 +183,7 @@ export class RigSharePanel {
|
||||
}
|
||||
const views = {
|
||||
workflows: () => this.workflowsView(),
|
||||
files: () => this.filesView(),
|
||||
people: () => this.peopleView(),
|
||||
chat: () => [this.chatView],
|
||||
server: () => this.serverView(),
|
||||
@@ -269,49 +271,57 @@ export class RigSharePanel {
|
||||
}
|
||||
|
||||
accessEditor() {
|
||||
const box = h("div", { class: "rs-access-editor" }, h("p", { class: "rs-muted" }, "Loading…"));
|
||||
const key = this.sync.targetKey;
|
||||
Promise.all([
|
||||
this.run(() => this.client.request("GET", `/rigshare/api/room/acl?key=${encodeURIComponent(key)}`)),
|
||||
this.run(() => this.client.request("GET", "/rigshare/api/people")),
|
||||
]).then(([acl, people]) => {
|
||||
return this.accessEditorFor({
|
||||
load: () => this.client.request("GET", `/rigshare/api/room/acl?key=${encodeURIComponent(key)}`),
|
||||
save: (restricted, members) => this.sync.setAccess(restricted, members),
|
||||
everyoneHint: "Anyone with an account can open it, with their usual permissions",
|
||||
close: () => { this.accessOpen = false; this.accessDraft = null; this.renderBody(); },
|
||||
draftKey: "accessDraft",
|
||||
});
|
||||
}
|
||||
|
||||
/** Access editor shared by live workflows and shared folders. */
|
||||
accessEditorFor({ load, save, everyoneHint, close, draftKey }) {
|
||||
const box = h("div", { class: "rs-access-editor" }, h("p", { class: "rs-muted" }, "Loading…"));
|
||||
Promise.all([this.run(load), this.run(() => this.client.request("GET", "/rigshare/api/people"))]).then(([acl, people]) => {
|
||||
if (!acl || !people) return;
|
||||
const draft = this.accessDraft ??= { restricted: acl.restricted, members: { ...acl.members } };
|
||||
const draft = this[draftKey] ??= { restricted: acl.restricted, members: { ...acl.members } };
|
||||
const render = () => {
|
||||
const fixed = people.filter((p) => p.username === acl.owner || p.admin);
|
||||
const choosable = people.filter((p) => p.username !== acl.owner && !p.admin);
|
||||
const fixedRows = fixed.map((p) => h("div", { class: "rs-row-item" },
|
||||
h("div", { class: "rs-grow rs-min0 rs-ellipsis" }, p.display_name, h("span", { class: "rs-muted" }, ` @${p.username}`)),
|
||||
h("span", { class: "rs-chip" }, p.username === acl.owner ? "Owner · always" : "Admin · always")));
|
||||
const rows = choosable
|
||||
.map((p) => {
|
||||
const select = h("select", {
|
||||
class: "rs-input rs-select", disabled: !draft.restricted,
|
||||
onchange: (e) => { if (e.target.value) draft.members[p.username] = e.target.value; else delete draft.members[p.username]; },
|
||||
}, ...[["", "No access"], ["view", "Can view"], ["edit", "Can edit"]].map(([v, label]) =>
|
||||
h("option", { value: v, selected: (draft.members[p.username] || "") === v }, label)));
|
||||
return h("div", { class: "rs-row-item" },
|
||||
h("div", { class: "rs-grow rs-min0 rs-ellipsis" }, p.display_name, h("span", { class: "rs-muted" }, ` @${p.username}`),
|
||||
p.edit ? null : h("span", { class: "rs-muted rs-small" }, " · viewer account")),
|
||||
select);
|
||||
});
|
||||
const rows = choosable.map((p) => {
|
||||
const select = h("select", {
|
||||
class: "rs-input rs-select", disabled: !draft.restricted,
|
||||
onchange: (e) => { if (e.target.value) draft.members[p.username] = e.target.value; else delete draft.members[p.username]; },
|
||||
}, ...[["", "No access"], ["view", "Can view"], ["edit", "Can edit"]].map(([v, label]) =>
|
||||
h("option", { value: v, selected: (draft.members[p.username] || "") === v }, label)));
|
||||
return h("div", { class: "rs-row-item" },
|
||||
h("div", { class: "rs-grow rs-min0 rs-ellipsis" }, p.display_name, h("span", { class: "rs-muted" }, ` @${p.username}`),
|
||||
p.edit ? null : h("span", { class: "rs-muted rs-small" }, " · viewer account")),
|
||||
select);
|
||||
});
|
||||
const name = `rs-access-${draftKey}`;
|
||||
const radio = (value, label, hint) => h("label", { class: "rs-check rs-block" },
|
||||
h("input", { type: "radio", name: "rs-access", checked: draft.restricted === value, onchange: () => { draft.restricted = value; render(); } }),
|
||||
h("input", { type: "radio", name, checked: draft.restricted === value, onchange: () => { draft.restricted = value; render(); } }),
|
||||
h("span", {}, label, h("div", { class: "rs-muted rs-small" }, hint)));
|
||||
box.replaceChildren(...[
|
||||
radio(false, "Everyone", "Anyone with an account can open it, with their usual permissions"),
|
||||
radio(false, "Everyone", everyoneHint),
|
||||
radio(true, "Only people I choose", "The owner and admins always have access"),
|
||||
draft.restricted ? h("div", { class: "rs-access-list" }, ...fixedRows, ...rows,
|
||||
rows.length ? null : h("p", { class: "rs-muted rs-small" },
|
||||
"Everyone else is an admin, and admins always have access. Add accounts without Admin under Admin → Accounts to choose who can open this workflow.")) : null,
|
||||
"Everyone else is an admin, and admins always have access. Add accounts without Admin under Admin → Accounts to choose who can open it.")) : null,
|
||||
h("div", { class: "rs-actions" },
|
||||
h("button", {
|
||||
class: "rs-btn rs-btn-primary", onclick: async () => {
|
||||
const ok = await this.run(() => this.sync.setAccess(draft.restricted, draft.restricted ? draft.members : {}), "Access updated");
|
||||
if (ok !== undefined) { this.accessOpen = false; this.accessDraft = null; this.renderBody(); }
|
||||
const ok = await this.run(() => save(draft.restricted, draft.restricted ? draft.members : {}), "Access updated");
|
||||
if (ok !== undefined) { this[draftKey] = null; close(); }
|
||||
},
|
||||
}, "Save"),
|
||||
h("button", { class: "rs-btn rs-btn-ghost", onclick: () => { this.accessOpen = false; this.accessDraft = null; this.renderBody(); } }, "Cancel")),
|
||||
h("button", { class: "rs-btn rs-btn-ghost", onclick: () => { this[draftKey] = null; close(); } }, "Cancel")),
|
||||
].filter(Boolean));
|
||||
};
|
||||
render();
|
||||
@@ -381,6 +391,119 @@ export class RigSharePanel {
|
||||
...(rows.length ? rows : [h("p", { class: "rs-muted" }, "Nothing shared yet. Open a saved workflow or edit a tab.")]));
|
||||
}
|
||||
|
||||
// ----- files ----------------------------------------------------------
|
||||
|
||||
filesView() {
|
||||
this.filesEl ??= h("div", {});
|
||||
this.loadFiles();
|
||||
return [this.filesEl];
|
||||
}
|
||||
|
||||
async loadFiles() {
|
||||
const tree = await this.run(() => this.client.request("GET", "/rigshare/api/workspace"));
|
||||
if (!tree) return;
|
||||
this.tree = tree;
|
||||
const destinations = [
|
||||
tree.private ? { folder: tree.private.path, label: "My files" } : null,
|
||||
...tree.shared.filter((f) => f.role === "edit").map((f) => ({ folder: f.path, label: f.name })),
|
||||
tree.common.role === "edit" ? { folder: "", label: "Common" } : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const fileRow = (file, folder, canMoveOut) => h("div", { class: "rs-row-item rs-file" },
|
||||
h("i", { class: "pi pi-file rs-row-icon" }),
|
||||
h("div", { class: "rs-grow rs-min0" },
|
||||
h("div", { class: "rs-name rs-ellipsis", title: file.path.slice("workflows/".length) }, file.name.replace(/\.json$/, "")),
|
||||
h("div", { class: "rs-muted rs-small" }, timeAgo(file.modified * 1000))),
|
||||
canMoveOut && destinations.length > 1 ? h("select", {
|
||||
class: "rs-input rs-select", title: "Move to another folder",
|
||||
onchange: async (e) => {
|
||||
const dest = e.target.value;
|
||||
e.target.value = "__";
|
||||
if (dest === "__") return;
|
||||
const base = file.path.split("/").pop();
|
||||
const target = dest ? `workflows/${dest}/${base}` : `workflows/${base}`;
|
||||
await this.run(() => this.sync.movePath(file.path, target), "Moved");
|
||||
this.loadFiles();
|
||||
},
|
||||
}, h("option", { value: "__" }, "Move to…"), ...destinations.filter((d) => d.folder !== folder).map((d) =>
|
||||
h("option", { value: d.folder }, d.label))) : null,
|
||||
h("button", { class: "rs-btn rs-btn-icon", title: "Open", onclick: () => this.run(() => this.sync.openPath(file.path)) },
|
||||
h("i", { class: "pi pi-arrow-right" })));
|
||||
|
||||
const empty = (text) => h("p", { class: "rs-muted rs-small" }, text);
|
||||
const sections = [];
|
||||
if (tree.private) {
|
||||
sections.push(this.section("pi-user", "My files",
|
||||
h("p", { class: "rs-muted rs-small" }, "Only you (and admins) can see these."),
|
||||
...(tree.private.files.length ? tree.private.files.map((f) => fileRow(f, tree.private.path, true)) : [empty("Nothing here yet. Save a workflow and choose My files.")])));
|
||||
}
|
||||
|
||||
const folderBlocks = tree.shared.map((f) => {
|
||||
const open = this.openFolders?.has(f.path);
|
||||
const owner = f.owner ? this.client.users.find((u) => u.key === f.owner)?.name ?? `@${f.owner}` : null;
|
||||
const head = h("div", { class: "rs-row-item rs-folder" },
|
||||
h("button", { class: "rs-btn rs-btn-icon rs-btn-ghost", title: open ? "Collapse" : "Expand", onclick: () => {
|
||||
this.openFolders ??= new Set();
|
||||
open ? this.openFolders.delete(f.path) : this.openFolders.add(f.path);
|
||||
this.loadFiles();
|
||||
} }, h("i", { class: `pi ${open ? "pi-chevron-down" : "pi-chevron-right"}` })),
|
||||
h("i", { class: `pi ${f.restricted ? "pi-lock" : "pi-folder"} rs-row-icon` }),
|
||||
h("div", { class: "rs-grow rs-min0" },
|
||||
h("div", { class: "rs-name rs-ellipsis" }, f.name),
|
||||
h("div", { class: "rs-muted rs-small" }, `${f.files.length} workflow${f.files.length === 1 ? "" : "s"}${owner ? ` · owner ${owner}` : ""}${f.role === "view" ? " · view only" : ""}`)),
|
||||
f.manage ? h("button", { class: `rs-btn rs-btn-sm ${this.folderAccess === f.path ? "active" : ""}`, onclick: () => {
|
||||
this.folderAccess = this.folderAccess === f.path ? null : f.path;
|
||||
this.folderDraft = null;
|
||||
this.loadFiles();
|
||||
} }, "Access") : null,
|
||||
f.manage ? h("button", { class: "rs-btn rs-btn-icon", title: "Rename folder", onclick: async () => {
|
||||
const name = await this.prompt("Rename folder", `New name for "${f.name}"`, f.name);
|
||||
if (!name || name === f.name) return;
|
||||
await this.run(() => this.client.request("PATCH", "/rigshare/api/workspace/folders", { path: f.path, name }), "Folder renamed");
|
||||
this.loadFiles();
|
||||
} }, h("i", { class: "pi pi-pencil" })) : null,
|
||||
f.manage ? h("button", { class: "rs-btn rs-btn-icon rs-danger", title: "Delete folder (must be empty)", onclick: async () => {
|
||||
if (!(await this.confirm("Delete folder", `Delete the empty folder "${f.name}"?`))) return;
|
||||
await this.run(() => this.client.request("DELETE", `/rigshare/api/workspace/folders?path=${encodeURIComponent(f.path)}`), "Folder deleted");
|
||||
this.loadFiles();
|
||||
} }, h("i", { class: "pi pi-trash" })) : null);
|
||||
const parts = [head];
|
||||
if (this.folderAccess === f.path) {
|
||||
parts.push(this.accessEditorFor({
|
||||
load: async () => {
|
||||
const t = await this.client.request("GET", "/rigshare/api/workspace");
|
||||
const folder = t.shared.find((x) => x.path === f.path);
|
||||
const members = await this.client.request("GET", `/rigshare/api/workspace/folder?path=${encodeURIComponent(f.path)}`);
|
||||
return { owner: folder?.owner, restricted: !!folder?.restricted, members: members.members || {} };
|
||||
},
|
||||
save: (restricted, members) => this.client.request("PATCH", "/rigshare/api/workspace/folders", { path: f.path, restricted, members }),
|
||||
everyoneHint: "Anyone with an account can open its workflows, with their usual permissions",
|
||||
close: () => { this.folderAccess = null; this.loadFiles(); },
|
||||
draftKey: "folderDraft",
|
||||
}));
|
||||
}
|
||||
if (open) {
|
||||
parts.push(h("div", { class: "rs-folder-files" },
|
||||
...(f.files.length ? f.files.map((file) => fileRow(file, f.path, f.manage)) : [empty("Empty folder.")])));
|
||||
}
|
||||
return h("div", { class: "rs-folder-block" }, ...parts);
|
||||
});
|
||||
const newFolderBtn = tree.can_create ? h("button", { class: "rs-btn", onclick: async () => {
|
||||
const name = await this.prompt("New shared folder", "Folder name");
|
||||
if (!name) return;
|
||||
await this.run(() => this.client.request("POST", "/rigshare/api/workspace/folders", { name }), "Folder created");
|
||||
this.loadFiles();
|
||||
} }, h("i", { class: "pi pi-folder-plus" }), "New shared folder") : null;
|
||||
sections.push(this.section("pi-folder", "Shared folders",
|
||||
...(folderBlocks.length ? folderBlocks : [empty("No shared folders yet.")]),
|
||||
newFolderBtn ? h("div", { class: "rs-actions" }, newFolderBtn) : null));
|
||||
|
||||
sections.push(this.section("pi-globe", "Common",
|
||||
h("p", { class: "rs-muted rs-small" }, "Top-level workflows, visible to everyone."),
|
||||
...(tree.common.files.length ? tree.common.files.map((f) => fileRow(f, "", this.client.perms.admin)) : [empty("No workflows at the top level.")])));
|
||||
this.filesEl.replaceChildren(...sections);
|
||||
}
|
||||
|
||||
// ----- people ---------------------------------------------------------
|
||||
|
||||
peopleView() {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// "Save" asks where: My files, a shared folder, or Common.
|
||||
//
|
||||
// ComfyUI's save flow asks the workflow for a file name (promptSave) and saves
|
||||
// to "<workflow directory>/<name>.json". Returning "<folder>/<name>" makes it
|
||||
// save straight into the chosen folder, with ComfyUI's own overwrite checks.
|
||||
|
||||
import { h, storage } from "./ui.js";
|
||||
|
||||
const LAST_FOLDER = "rigshare.lastSaveFolder";
|
||||
|
||||
function cleanName(name) {
|
||||
return (name || "").trim().replace(/\.json$/i, "").replace(/[\\/]+/g, "-").slice(0, 120);
|
||||
}
|
||||
|
||||
export function installSaveDialog(app, api, client) {
|
||||
const patch = () => {
|
||||
const wf = app.extensionManager?.workflow?.activeWorkflow;
|
||||
if (!wf) return false;
|
||||
let proto = Object.getPrototypeOf(wf);
|
||||
while (proto && !Object.prototype.hasOwnProperty.call(proto, "promptSave")) proto = Object.getPrototypeOf(proto);
|
||||
if (!proto) return false;
|
||||
if (proto.__rigshare) return true;
|
||||
const original = proto.promptSave;
|
||||
proto.promptSave = async function (...args) {
|
||||
if (!client.online || !client.self || !this.path?.startsWith("workflows/")) return original.apply(this, args);
|
||||
try {
|
||||
return await chooseLocation(app, api, client, this);
|
||||
} catch (e) {
|
||||
console.error("[RigShare] save dialog", e);
|
||||
return original.apply(this, args);
|
||||
}
|
||||
};
|
||||
proto.__rigshare = true;
|
||||
return true;
|
||||
};
|
||||
const timer = setInterval(() => { if (patch()) clearInterval(timer); }, 1000);
|
||||
}
|
||||
|
||||
export async function chooseLocation(app, api, client, wf) {
|
||||
let tree = await client.request("GET", "/rigshare/api/workspace");
|
||||
const directory = wf.directory || "workflows";
|
||||
|
||||
const options = () => {
|
||||
const out = [];
|
||||
if (tree.private) out.push({ folder: tree.private.path, label: "My files", hint: "Only you", icon: "pi-user" });
|
||||
for (const f of tree.shared) {
|
||||
if (f.role === "edit") out.push({ folder: f.path, label: f.name, hint: f.restricted ? "Shared with chosen people" : "Shared with everyone", icon: f.restricted ? "pi-lock" : "pi-folder" });
|
||||
}
|
||||
if (tree.common?.role === "edit") out.push({ folder: "", label: "Common", hint: "Top level, visible to everyone", icon: "pi-globe" });
|
||||
return out;
|
||||
};
|
||||
|
||||
const current = directory === "workflows" ? null : directory.slice("workflows/".length);
|
||||
const pick = (opts) => {
|
||||
const remembered = storage.get(LAST_FOLDER);
|
||||
return opts.find((o) => o.folder === current)?.folder
|
||||
?? opts.find((o) => o.folder === remembered)?.folder
|
||||
?? opts[0]?.folder;
|
||||
};
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
let opts = options();
|
||||
let selected = pick(opts);
|
||||
const name = h("input", { class: "rs-input", value: cleanName(wf.filename) || "Untitled", maxLength: 120 });
|
||||
const list = h("div", { class: "rs-save-list" });
|
||||
const newFolder = h("input", { class: "rs-input", placeholder: "New shared folder name", maxLength: 64 });
|
||||
const error = h("div", { class: "rs-error" });
|
||||
|
||||
const renderList = () => {
|
||||
list.replaceChildren(...opts.map((o) => h("label", { class: `rs-save-option ${o.folder === selected ? "on" : ""}` },
|
||||
h("input", { type: "radio", name: "rs-save-folder", checked: o.folder === selected, onchange: () => { selected = o.folder; renderList(); } }),
|
||||
h("i", { class: `pi ${o.icon}` }),
|
||||
h("span", { class: "rs-grow rs-min0" },
|
||||
h("div", { class: "rs-name rs-ellipsis" }, o.label),
|
||||
h("div", { class: "rs-muted rs-small" }, o.hint)))));
|
||||
};
|
||||
|
||||
const close = (value) => {
|
||||
backdrop.remove();
|
||||
document.removeEventListener("keydown", onKey, true);
|
||||
resolve(value);
|
||||
};
|
||||
const submit = () => {
|
||||
const n = cleanName(name.value);
|
||||
if (!n) { error.textContent = "Enter a name"; return; }
|
||||
if (selected === undefined) { error.textContent = "Choose where to save it"; return; }
|
||||
storage.set(LAST_FOLDER, selected);
|
||||
close({ name: n, folder: selected });
|
||||
};
|
||||
const onKey = (e) => {
|
||||
if (!backdrop.isConnected) return;
|
||||
if (e.key === "Escape") { e.stopPropagation(); close(null); }
|
||||
if (e.key === "Enter" && document.activeElement !== newFolder) { e.preventDefault(); e.stopPropagation(); submit(); }
|
||||
};
|
||||
|
||||
const canCreate = tree.can_create;
|
||||
const createRow = canCreate ? h("div", { class: "rs-row" }, newFolder,
|
||||
h("button", {
|
||||
class: "rs-btn", onclick: async () => {
|
||||
error.textContent = "";
|
||||
try {
|
||||
const res = await client.request("POST", "/rigshare/api/workspace/folders", { name: newFolder.value });
|
||||
tree = await client.request("GET", "/rigshare/api/workspace");
|
||||
opts = options();
|
||||
selected = res.path;
|
||||
newFolder.value = "";
|
||||
renderList();
|
||||
} catch (e) { error.textContent = e.message; }
|
||||
},
|
||||
}, h("i", { class: "pi pi-folder-plus" }), "Create")) : null;
|
||||
|
||||
const backdrop = h("div", { class: "rs-modal-backdrop rs-panel-vars", onmousedown: (e) => { if (e.target === backdrop) close(null); } },
|
||||
h("div", { class: "rs-modal", role: "dialog", "aria-label": "Save workflow" },
|
||||
h("div", { class: "rs-modal-title" }, h("i", { class: "pi pi-save" }), "Save workflow"),
|
||||
h("label", { class: "rs-label" }, "Name"), name,
|
||||
h("label", { class: "rs-label" }, "Save in"), list,
|
||||
createRow, error,
|
||||
h("div", { class: "rs-actions rs-modal-actions" },
|
||||
h("button", { class: "rs-btn rs-btn-ghost", onclick: () => close(null) }, "Cancel"),
|
||||
h("button", { class: "rs-btn rs-btn-primary", onclick: submit }, "Save"))));
|
||||
backdrop.addEventListener("keydown", (e) => e.stopPropagation());
|
||||
renderList();
|
||||
document.body.append(backdrop);
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
setTimeout(() => { name.focus(); name.select(); }, 30);
|
||||
});
|
||||
|
||||
if (!result) return null;
|
||||
const targetDir = result.folder ? `workflows/${result.folder}` : "workflows";
|
||||
if (targetDir === directory) return result.name;
|
||||
if (directory === "workflows") return `${result.folder}/${result.name}`;
|
||||
// "Save As" from a file that lives in another folder: write the copy
|
||||
// ourselves, open it, and tell ComfyUI there is nothing left to do.
|
||||
return await saveCopyElsewhere(app, api, client, wf, `${targetDir}/${result.name}.json`);
|
||||
}
|
||||
|
||||
async function saveCopyElsewhere(app, api, client, wf, path) {
|
||||
const store = app.extensionManager.workflow;
|
||||
await store.syncWorkflows?.();
|
||||
if (store.getWorkflowByPath?.(path)) {
|
||||
const dialog = app.extensionManager?.dialog;
|
||||
const ok = dialog?.confirm
|
||||
? await dialog.confirm({ title: "Overwrite?", message: `${path.slice("workflows/".length)} already exists. Replace it?` })
|
||||
: window.confirm(`${path} already exists. Replace it?`);
|
||||
if (!ok) return null;
|
||||
}
|
||||
const state = wf.activeState ?? app.rootGraph?.serialize?.() ?? app.graph.serialize();
|
||||
await api.storeUserData(path, JSON.stringify(state), { overwrite: true, full_info: true, throwOnError: true });
|
||||
await store.syncWorkflows?.();
|
||||
const copy = store.getWorkflowByPath?.(path);
|
||||
if (copy) {
|
||||
if (!copy.isLoaded) await copy.load();
|
||||
await app.loadGraphData(copy.activeState, true, true, copy);
|
||||
}
|
||||
client.emit("toast", { severity: "success", summary: "RigShare", detail: `Saved as ${path.slice("workflows/".length)}` });
|
||||
return null;
|
||||
}
|
||||
@@ -170,6 +170,24 @@ body.rs-no-manager [aria-label="ComfyUI Manager"] { display: none !important; }
|
||||
.rs-gpu-name .rs-muted { font-weight: 400; font-size: 11px; }
|
||||
.rs-queue-line { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 6px 8px; border-radius: 6px; background: var(--rs-bg2); }
|
||||
|
||||
/* files */
|
||||
.rs-folder-block + .rs-folder-block { border-top: 1px solid var(--rs-soft); }
|
||||
.rs-folder-files { margin-left: 26px; }
|
||||
.rs-file .rs-select { max-width: 110px; }
|
||||
|
||||
/* save dialog */
|
||||
.rs-modal-backdrop { position: fixed; inset: 0; z-index: 3000; background: rgb(0 0 0 / .55); display: grid; place-items: center; padding: 16px; color: var(--rs-fg); font-size: 13px; }
|
||||
.rs-modal { width: 100%; max-width: 440px; max-height: 90vh; overflow-y: auto; background: var(--rs-bg); border: 1px solid var(--rs-border);
|
||||
border-radius: 12px; padding: 18px 18px 14px; box-shadow: 0 20px 60px rgb(0 0 0 / .45); display: flex; flex-direction: column; gap: 6px; }
|
||||
.rs-modal-title { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 650; margin-bottom: 6px; }
|
||||
.rs-modal-actions { justify-content: flex-end; margin-top: 10px; }
|
||||
.rs-save-list { display: flex; flex-direction: column; gap: 4px; max-height: 260px; overflow-y: auto; margin-bottom: 6px; }
|
||||
.rs-save-option { display: flex; align-items: center; gap: 10px; padding: 7px 9px; border: 1px solid var(--rs-border); border-radius: 8px; cursor: pointer; }
|
||||
.rs-save-option.on { border-color: var(--rs-accent); background: color-mix(in srgb, var(--rs-accent) 12%, transparent); }
|
||||
.rs-save-option input { margin: 0; }
|
||||
.rs-error { color: #f87171; font-size: 12px; }
|
||||
.rs-error:empty { display: none; }
|
||||
|
||||
/* floating server monitor */
|
||||
.rs-float { position: fixed; z-index: 1000; width: 240px; background: var(--rs-bg); border: 1px solid var(--rs-border);
|
||||
border-radius: 10px; box-shadow: 0 10px 30px rgb(0 0 0 / .35); font-size: 11px; user-select: none; }
|
||||
|
||||
@@ -541,6 +541,29 @@ export class RoomSync extends EventTarget {
|
||||
};
|
||||
}
|
||||
|
||||
/** Open a saved workflow file (like ComfyUI's workflow browser does). */
|
||||
async openPath(path) {
|
||||
const store = this.store;
|
||||
await store.syncWorkflows?.();
|
||||
const wf = store.getWorkflowByPath?.(path);
|
||||
if (!wf) throw new Error("That workflow no longer exists");
|
||||
if (wf === store.activeWorkflow) return;
|
||||
if (!wf.isLoaded) await wf.load();
|
||||
await this.app.loadGraphData(wf.activeState, true, true, wf);
|
||||
}
|
||||
|
||||
/** Move a saved workflow to another folder (ComfyUI's own rename). */
|
||||
async movePath(path, target) {
|
||||
const store = this.store;
|
||||
await store.syncWorkflows?.();
|
||||
const wf = store.getWorkflowByPath?.(path);
|
||||
if (!wf) throw new Error("That workflow no longer exists");
|
||||
if (store.getWorkflowByPath?.(target)) throw new Error("A workflow with that name already exists there");
|
||||
if (store.renameWorkflow) await store.renameWorkflow(wf, target);
|
||||
else await wf.rename(target);
|
||||
await store.syncWorkflows?.();
|
||||
}
|
||||
|
||||
/** Find an already-open tab for a room. */
|
||||
findTab(room) {
|
||||
for (const wf of this.store?.openWorkflows || []) {
|
||||
|
||||
Reference in New Issue
Block a user