App icon for workflows with an App set up; smaller icons in folder lists

This commit is contained in:
Edouard Durand
2026-09-26 04:32:38 +00:00
parent d6ef325ee1
commit e533e64970
4 changed files with 59 additions and 4 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ from aiohttp import web, WSMsgType
from .chatlog import ChatLog
from .graphdoc import apply_patch, remap_conflicts
from .stats import collect_stats
from .workspace import Workspace
from .workspace import Workspace, has_app
log = logging.getLogger("ComfyUI-RigShare")
@@ -285,7 +285,7 @@ class Hub:
"updated": int(room.updated), "members": members,
"nodes": len((room.doc or {}).get("nodes") or []),
"restricted": self.folder_restricted(self.folder_of(room)),
"app": room.key.endswith(".app.json")})
"app": room.key.endswith(".app.json") or has_app(room.doc)})
out.sort(key=lambda r: (-len(r["members"]), -r["updated"]))
return out
+28 -1
View File
@@ -11,6 +11,7 @@ Paths handled here are relative to the *userdata* root, e.g.
every path is fully URL-decoded and normalised before it is checked.
"""
import json
import os
import posixpath
import re
@@ -40,6 +41,13 @@ def norm(path):
return posixpath.normpath("/" + path).lstrip("/") if path else ""
def has_app(doc):
"""A workflow with an App set up (ComfyUI App mode: chosen inputs or outputs)."""
data = (doc or {}).get("extra") if isinstance(doc, dict) else None
linear = (data or {}).get("linearData") if isinstance(data, dict) else None
return isinstance(linear, dict) and bool(linear.get("inputs") or linear.get("outputs"))
def min_role(a, b):
return a if ROLE_RANK[a] <= ROLE_RANK[b] else b
@@ -51,6 +59,7 @@ class Workspace:
self.folders = store._read("folders.json", {}) # "shared/<name>" -> {owner, acl, created}
os.makedirs(os.path.join(root, "users"), exist_ok=True)
os.makedirs(os.path.join(root, "shared"), exist_ok=True)
self._app_cache = {} # absolute path -> (mtime, size, has an App)
def save(self):
self.store._write("folders.json", self.folders)
@@ -191,6 +200,24 @@ class Workspace:
def _abs(self, rel):
return os.path.join(self.root, *[p for p in rel.split("/") if p])
def _file_has_app(self, full):
"""Read a workflow file only when it changed since the last check."""
try:
st = os.stat(full)
except OSError:
return False
cached = self._app_cache.get(full)
if cached and cached[0] == st.st_mtime and cached[1] == st.st_size:
return cached[2]
try:
with open(full, "r", encoding="utf-8") as f:
text = f.read()
found = '"linearData"' in text and has_app(json.loads(text))
except (OSError, ValueError):
found = False
self._app_cache[full] = (st.st_mtime, st.st_size, found)
return found
def _entry(self, rel, name, is_dir, kind, key, perms):
full = self._abs(rel)
is_app = not is_dir and name.endswith(".app.json") # saved as an App (ComfyUI App mode)
@@ -203,7 +230,7 @@ class Workspace:
meta = self.folders.get(folder) or {}
entry.update(owner=meta.get("owner"), restricted=bool(meta.get("acl")),
manage=self.can_manage(folder, kind, key, perms))
if is_app:
if is_app or (not is_dir and self._file_has_app(full)):
entry["app"] = True
entry["deletable"] = self.can_delete(rel, kind, key, perms)
return entry
+28
View File
@@ -390,3 +390,31 @@ def test_listing_inside_a_shared_folder(run):
assert (await here(bob, "shared/FaceSwap"))["manage"] is False
assert await here(alice, "users/alice") is None
run(scenario())
def test_workflows_with_an_app_set_up(run):
async def scenario():
async with rig_server() as rig:
await rig.setup_admin()
alice = await rig.add_user("alice", edit=True)
h = rig.http
with_app = json.dumps({"nodes": [], "extra": {"linearData": {"inputs": [[3, "seed"]], "outputs": [9]}}}).encode()
empty_app = json.dumps({"nodes": [], "extra": {"linearData": {"inputs": [], "outputs": []}}}).encode()
for name, body in (("faceswap.json", with_app), ("empty.json", empty_app), ("plain.json", b"{}")):
await h.post(rig.url(f"/api/userdata/{enc('workflows/users/alice/' + name)}"), data=body, headers=alice)
async def apps():
listing = await (await h.get(rig.url("/rigshare/api/workspace/list?path=users/alice"), headers=alice)).json()
return {e["name"] for e in listing["entries"] if e.get("app")}
assert await apps() == {"faceswap"}, "only a workflow whose App has inputs or outputs"
# Edited on disk: the cached answer follows the file.
await h.post(rig.url(f"/api/userdata/{enc('workflows/users/alice/faceswap.json')}"), data=b'{"nodes": []}', headers=alice)
assert await apps() == set()
sock = await rig.ws("alice")
await sock.send({"type": "join", "room": "file:workflows/shared/f.json", "name": "f", "doc": json.loads(with_app)})
await sock.until("room")
presence = [m for m in await sock.drain() if m["type"] == "presence"][-1]
assert presence["rooms"][0]["app"] is True
await sock.close()
run(scenario())
+1 -1
View File
@@ -204,7 +204,7 @@ body.rs-no-manager [aria-label="ComfyUI Manager"] { display: none !important; }
.rs-fb-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; padding: 7px 8px; border: 0; background: none;
color: inherit; font: inherit; text-align: left; cursor: pointer; }
.rs-fb-main:disabled { cursor: default; }
.rs-fb-icon { font-size: 16px; color: var(--rs-muted); width: 18px; text-align: center; flex: none; }
.rs-fb-icon { font-size: 13px; color: var(--rs-muted); width: 15px; text-align: center; flex: none; }
.rs-fb-icon.folder { color: #f5b942; }
.rs-fb-tools-row { display: none; gap: 3px; padding-right: 6px; }
.rs-fb-row:hover .rs-fb-tools-row, .rs-fb-row:focus-within .rs-fb-tools-row { display: flex; }