From d6ef325ee113e91f84abe2701ddddfc9239ad7ee Mon Sep 17 00:00:00 2001 From: Edouard Durand Date: Sat, 26 Sep 2026 04:09:18 +0000 Subject: [PATCH] Files: access button inside a shared folder The listing reports the shared folder you are in; if you manage it, a lock button beside New folder opens its access list above the content. --- CHANGELOG.md | 2 +- docs/files.md | 2 +- rigshare_core/workspace.py | 10 +++++++++- tests/python/test_server.py | 22 ++++++++++++++++++++++ web/rigshare/files.js | 15 +++++++++++++++ 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90b73c6..77484a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - App mode: live workflows work in ComfyUI's App view. Each person keeps their own graph or App view through live edits, and App inputs sync. Cursors are off in the App view. Workflows saved as Apps (`.app.json`) show with an app icon in Files and in the Live list, and Save keeps them Apps - In the App view, where ComfyUI shows only its own sidebar tabs, RigShare adds **RigShare** and **Files** buttons to that sidebar, with the unread chat badge. They open the panels in a drawer beside it - The Live tab and People show whether each person is in the node graph or the App view, with ComfyUI's own Graph and App icons -- Folder access is managed only in the Files tab. *This tab* in the Live tab still says whether the folder is restricted +- Folder access is managed only in the Files tab. *This tab* in the Live tab still says whether the folder is restricted. Inside a shared folder you own, its πŸ”’ sits next to the new-folder button - ComfyUI's own Workflows and Apps sidebar tabs are hidden, since Files replaces them, and their shortcuts open Files. Admin setting `hide_comfy_file_tabs`, on by default ## 1.1.0 (2026-09-26) diff --git a/docs/files.md b/docs/files.md index 7f12903..a80d055 100644 --- a/docs/files.md +++ b/docs/files.md @@ -35,7 +35,7 @@ Workflows saved as **Apps** (ComfyUI's App mode, `.app.json`) show with an app i Click a workflow to open it. The Files tab marks live workflows with a green dot. **From computer** opens a workflow file from your machine, like ComfyUI's own *Open*. -Hover a row for its actions: **Rename**, **Move to…**, **Delete**, and on a shared folder you own, πŸ”’ **Who can open it**. The folder button creates a folder. In the Shared tab it creates a shared folder that you own. +Hover a row for its actions: **Rename**, **Move to…**, **Delete**, and on a shared folder you own, πŸ”’ **Who can open it**. Inside a shared folder you own, the same πŸ”’ sits next to the new-folder button. The folder button creates a folder. In the Shared tab it creates a shared folder that you own. ## Saving diff --git a/rigshare_core/workspace.py b/rigshare_core/workspace.py index 2d4ef1d..9ec7945 100644 --- a/rigshare_core/workspace.py +++ b/rigshare_core/workspace.py @@ -252,7 +252,15 @@ class Workspace: entries.append(self._entry(child, name, os.path.isdir(full), kind, key, perms)) can_mkdir = role == "edit" and rel not in ("", "users", "shared") entries.sort(key=lambda e: (e["type"] != "folder", e["name"].lower())) - return {"path": rel, "role": role, "entries": entries, "can_mkdir": can_mkdir} + out = {"path": rel, "role": role, "entries": entries, "can_mkdir": can_mkdir} + # Inside a shared folder: which one, and whether this person manages its access. + area, folder = self.area(rel) if rel != "@shared" else (None, None) + if area == "shared": + meta = self.folders.get(folder) or {} + out["shared_folder"] = {"path": folder, "name": folder.split("/", 1)[1], "owner": meta.get("owner"), + "restricted": bool(meta.get("acl")), + "manage": self.can_manage(folder, kind, key, perms)} + return out def make_dir(self, parent, name, kind, key, perms): """New folder. In the Shared root it becomes a shared folder you own.""" diff --git a/tests/python/test_server.py b/tests/python/test_server.py index d6a76ab..cdc82dd 100644 --- a/tests/python/test_server.py +++ b/tests/python/test_server.py @@ -368,3 +368,25 @@ def test_view_mode_in_presence(run): assert next(c for c in rig.hub.clients.values() if c.key == "alice").view == "app", "unknown views are ignored" await alice.close(); await boss.close() run(scenario()) + + +def test_listing_inside_a_shared_folder(run): + async def scenario(): + async with rig_server() as rig: + await rig.setup_admin() + alice = await rig.add_user("alice", edit=True) + bob = await rig.add_user("bob", edit=True) + h = rig.http + await h.post(rig.url("/rigshare/api/workspace/mkdir"), json={"parent": "@shared", "name": "FaceSwap"}, headers=alice) + await h.post(rig.url("/rigshare/api/workspace/mkdir"), json={"parent": "shared/FaceSwap", "name": "old"}, headers=alice) + + async def here(user, path): + r = await h.get(rig.url(f"/rigshare/api/workspace/list?path={path}"), headers=user) + return (await r.json()).get("shared_folder") + + # The folder you are in, even deeper down, and whether you manage its access. + assert await here(alice, "shared/FaceSwap/old") == {"path": "shared/FaceSwap", "name": "FaceSwap", "owner": "alice", + "restricted": False, "manage": True} + assert (await here(bob, "shared/FaceSwap"))["manage"] is False + assert await here(alice, "users/alice") is None + run(scenario()) diff --git a/web/rigshare/files.js b/web/rigshare/files.js index 76fc290..b3eadeb 100644 --- a/web/rigshare/files.js +++ b/web/rigshare/files.js @@ -161,7 +161,14 @@ export class FilesBrowser { h("button", { class: `rs-fb-crumb ${i === trail.length - 1 ? "current" : ""}`, onclick: () => this.go(c.path) }, c.icon ? h("i", { class: `pi ${c.icon}` }) : null, i === 0 && c.icon ? null : c.label), ]).filter(Boolean)); + const shared = this.listing?.shared_folder; this.actions.replaceChildren(...[ + // Inside a shared folder: its access list, like the πŸ”’ on its row one level up. + shared?.manage && this.mode === "browse" && this.accessEditor ? h("button", { + class: `rs-btn rs-btn-icon ${this.accessFor === shared.path ? "active" : ""}`, + title: `Who can open "${shared.name}"${shared.restricted ? " (restricted)" : ""}`, + onclick: () => { this.accessFor = this.accessFor === shared.path ? null : shared.path; this.render(); }, + }, h("i", { class: `pi ${shared.restricted ? "pi-lock" : "pi-lock-open"}` })) : null, this.listing?.can_mkdir && this.mode !== "open" ? h("button", { class: "rs-btn rs-btn-icon", title: this.path === SHARED ? "New shared folder" : "New folder", onclick: () => this.newFolder(), @@ -173,6 +180,14 @@ export class FilesBrowser { renderList() { const entries = (this.listing?.entries || []).filter((e) => !this.filter || e.name.toLowerCase().includes(this.filter)); + // The access editor of the shared folder we are in shows above its content. + const here = this.listing?.shared_folder; + const top = here && this.accessFor === here.path && this.accessEditor + ? this.accessEditor(here.path, () => { this.accessFor = null; this.reload(); }) : null; + if (top) { + this.list.replaceChildren(top, ...entries.map((e) => this.row(e))); + return; + } if (!entries.length) { this.list.replaceChildren(h("div", { class: "rs-fb-empty" }, this.filter ? "Nothing matches." : this.path === `users/${this.me}` ? "Nothing here yet. Save a workflow and choose My files." : "This folder is empty."));