Skip to content

Add board visibility (private/shared/public) for multiuser mode#122

Open
Copilot wants to merge 24 commits intolstein/feature/workflow-isolation-in-multiuser-modefrom
copilot/enhancement-allow-shared-boards
Open

Add board visibility (private/shared/public) for multiuser mode#122
Copilot wants to merge 24 commits intolstein/feature/workflow-isolation-in-multiuser-modefrom
copilot/enhancement-allow-shared-boards

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 9, 2026

Adds three visibility levels for boards in multiuser mode: private (owner/admin only), shared (all users can view, owner/admin can modify), and public (all users can view and write; only owner/admin can rename or delete).

Summary

Backend

  • BoardVisibility enum (private | shared | public) added to board_records_common.py; board_visibility field added to BoardRecord and BoardChanges
  • Migration 29: adds board_visibility TEXT NOT NULL DEFAULT 'private' column to boards table; migrates existing is_public=1 rows to 'public'
  • board_records_sqlite.py: update() handles board_visibility; get_many()/get_all() queries use board_visibility IN ('shared', 'public') instead of is_public = 1
  • boards.py router: get_board and list_all_board_image_names allow non-owner access for shared/public boards; update_board and delete_board remain owner/admin-only

Frontend

  • schema.ts: BoardVisibility enum, board_visibility field on BoardDTO and BoardChanges
  • BoardContextMenu.tsx: "Set Private / Set Shared / Set Public" menu items (owner and admins only); visibility handlers extracted into named useCallback hooks to comply with react/jsx-no-bind; Delete Board, Archive, and Unarchive menu items are disabled (greyed out) for non-owners of shared and public boards
  • BoardEditableTitle.tsx: pencil icon and double-click rename hidden for non-owners of shared and public boards
  • GalleryBoard.tsx: blue share icon badge for shared boards, green globe badge for public boards; DnD drop target disabled for non-owners of shared boards
  • GalleryImage.tsx: drag-out disabled for non-owners viewing a shared board, preventing images from being moved out
  • GalleryItemDeleteIconButton.tsx: shift+hover trash icon hidden when viewing a shared board as a non-owner
  • ContextMenuItemDeleteImage.tsx: delete image menu item hidden when viewing a shared board as a non-owner
  • ContextMenuItemChangeBoard.tsx: "Change Board" menu item disabled when viewing a shared board as a non-owner
  • MultipleSelectionMenuItems.tsx: "Change Board" and "Delete Selection" disabled when viewing a shared board as a non-owner
  • InvokeQueueBackButton.tsx: main Invoke/generate button disabled when the auto-add board is a shared board the current user does not own
  • FloatingLeftPanelButtons.tsx: floating invoke icon button also disabled when the auto-add board is a shared board the current user does not own
  • ChangeBoardModal.tsx: destination board list filtered to exclude shared boards the current user does not own, preventing moves into read-only boards
  • New hooksuseBoardAccess(board) returns { canWriteImages, canRenameBoard, canDeleteBoard }; useSelectedBoard() and useAutoAddBoard() look up the relevant BoardDTO from the RTK Query cache
  • en.json: i18n strings for all new visibility UI

Tests

10 new tests in test_boards_multiuser.py covering default visibility, setting each level, cross-user access enforcement, reversion to private, non-owner restriction, and admin override. All 33 tests pass.

Related Issues / Discussions

QA Instructions

  1. Enable multiuser mode (multiuser: true in config)
  2. Create a board as User A — verify it defaults to private (User B cannot see it)
  3. Right-click board → Set Shared — verify User B can now view it but:
    • The pencil icon is hidden and double-clicking the name does nothing
    • The Delete Board, Archive, and Unarchive options in the context menu are greyed out
    • The trash icon on images and "Delete Image" context menu item are hidden
    • Both the main Invoke button and the floating invoke icon button are disabled if this shared board is the auto-add target
    • Dragging images onto the board is disabled
    • Dragging images out of the board is disabled
    • The "Change Board" destination list does not include this shared board
  4. Set to Public — verify User B can view and write images (including generating into it), but cannot rename, delete, or archive the board
  5. Set back to Private — verify User B loses access again
  6. Verify shared/public boards show the appropriate icon badge in the boards list
  7. Verify admins retain full access to all boards regardless of visibility

Merge Plan

Migration 29 adds a new column with a safe default ('private'), so existing databases upgrade non-destructively. No redux slice changes.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)
Original prompt

This section details on the original issue you should resolve

<issue_title>[enhancement]: Allow shared boards in multiuser mode</issue_title>
<issue_description>### Is there an existing issue for this?

  • I have searched the existing issues

Contact Details

No response

What should this feature add?

Currently when multiuser mode is active, all user-created boards are strictly private. They are entirely isolated from each other.

I would like it to be possible to designate a board as one of "Private," "Shared," or "Public".

  • Private board -- Only visible to the user who created it.
  • Shared board -- Visible to all users. Read/Write for the user who created it (they can add images to it and delete images from it). Read-only for other users.
  • Public board -- Visible and Read/Write to all users.

The Administrator should continue to be able to see all boards created by all users and have read/write access to them.

There is code in the lstein/feature/workflow-isolation-in-multiuser-mode that enables sharing of workflows. I have designated this branch as the base for this pull request.

In terms of user interface, try adding a discrete pulldown menu to the right of the board's name can be used to choose among the three board types. Something small like a caret (^) mark, and badges that appear when the board is shared or public.

Alternatives

No response

Additional Content

No response</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Add support for shared boards in multiuser mode Add board visibility (private/shared/public) for multiuser mode Mar 9, 2026
@lstein
Copy link
Copy Markdown
Owner

lstein commented Mar 9, 2026

@copilot I'm getting the following eslint errors:

/home/lstein/Projects/InvokeAI-lstein/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx
  130:17  error  JSX props should not use arrow functions  react/jsx-no-bind
  137:17  error  JSX props should not use arrow functions  react/jsx-no-bind
  144:17  error  JSX props should not use arrow functions  react/jsx-no-bind

Copy link
Copy Markdown
Author

Copilot AI commented Mar 9, 2026

@copilot I'm getting the following eslint errors:

/home/lstein/Projects/InvokeAI-lstein/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx

...

Fixed in 0d7f7ea. Replaced the three inline arrow functions with dedicated useCallback handlers (handleSetVisibilityPrivate, handleSetVisibilityShared, handleSetVisibilityPublic) that each delegate to the shared handleSetVisibility callback.

@lstein lstein marked this pull request as ready for review March 9, 2026 22:24
@lstein lstein self-requested a review as a code owner March 9, 2026 22:24
Copy link
Copy Markdown
Owner

@lstein lstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few glitches to correct. In these examples, "Board A Public" and "Board A Shared" are owned by user A and are public and shared respectively. User B is a non-administrative user.

  1. Shared boards are read-only for non-owners. So when B is logged in he should not be able to generate images and store them in Board A Shared, to move images in and out of Board A Shared, or delete images from Board A Shared. As much as possible, user interfaces that attempt to modify the board should be disabled, so that if Board A Shared is the auto save target for image generation, the generation buttons should be disabled.
  2. Disable the option to delete a public or shared board when a non-owner tries to do so. So when B is logged in and right clicks on Board A Shared or Board A Public, the Delete menu option is greyed out.
  3. Similarly, the trash-can icon to delete individual image files, and the delete image menu item should be deleted when B is logged in and working with Board A Shared.
  4. The pencil icon should be hidden from Board A Shared and Board A Public when B is logged in. Only the owner (or the administrator) should be able to edit the name of a shared or public board.

Copy link
Copy Markdown
Author

Copilot AI commented Mar 9, 2026

A few glitches to correct. In these examples, "Board A Public" and "Board A Shared" are owned by user A and are public and shared respectively. User B is a non-administrative user.

  1. Shared boards a...

Addressed in a0b90b1. Here's what was changed:

  1. Shared board read-only: GalleryItemDeleteIconButton (shift+hover trash icon) and ContextMenuItemDeleteImage hide when a non-owner views a shared board. ContextMenuItemChangeBoard and multi-select "Delete Selection"/"Change Board" are disabled too. The DnD drop target on shared boards is disabled for non-owners, preventing drag-in. The Invoke button is disabled when the auto-add board is shared and the user is not the owner.

  2. Delete board greyed out: BoardContextMenu now sets isDisabled={!canDeleteBoard} on the Delete Board menu item — greys it out for non-owners of both shared and public boards.

  3. Trash icon / delete image hidden for shared: Items 6–9 above all use the new useSelectedBoard()useBoardAccess() chain. When the selected board is shared and the current user is not the owner, canWriteImages is false and the delete controls are hidden/disabled.

  4. Pencil icon hidden for shared and public: BoardEditableTitle uses useBoardAccess(board).canRenameBoard; when false (non-owner of shared or public board), the pencil icon is not rendered and double-clicking the title has no effect.

New helpers: useBoardAccess(board) returns { canWriteImages, canRenameBoard, canDeleteBoard }. Logic: owner or admin → full access; public board non-owner → can write images but not rename/delete; shared board non-owner → read-only.

@lstein lstein self-requested a review March 10, 2026 02:19
Copy link
Copy Markdown
Owner

@lstein lstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still some issues:

  1. When a read-only shared board is selected, and I am not its owner, the "Add to Queue" button is active and lets me generate into the shared board. This button should be disabled when the target is a shared board and I am not the owner of the board or the administrator.
  2. I am able to drag an image out of a shared board and into a private board, thereby deleting it from the shared board, even when I am not the owner of the shared board. Dragging from a shared board when I am not the owner (or administrator) should be disabled.
  3. I can right click on an image in a private or public board and select "Change Board" to move it into a shared board, even when I am not the owner of the shared board. The drop target for a shared board should be disabled unless I am the owner (or administrator).
  4. I can archive a public or shared board, even when I am not the owner of that board. If I am not the owner, the archive menu selection should be disabled.

Copy link
Copy Markdown
Author

Copilot AI commented Mar 10, 2026

There are still some issues:

  1. When a read-only shared board is selected, and I am not its owner, the "Add to Queue" button is active and lets me generate into the shared board. This button should b...

Fixed in ae9cca7. Here's what was addressed:

  1. "Add to Queue" icon button still active — The floating InvokeIconButton in FloatingLeftPanelButtons.tsx was a second invoke entry point that wasn't covered. Now it also checks useBoardAccess(autoAddBoard).canWriteImages and is disabled for non-owners of shared boards.

  2. Dragging images out of shared boardGalleryImage.tsx now reads useSelectedBoard() + useBoardAccess() and conditionally registers the draggable() binding. When canDragFromBoard is false (non-owner viewing a shared board), the draggable is not attached, so the image cannot be picked up. The monitorForElements monitor is always registered so multi-image drag indicators still work.

  3. "Change Board" destination includes shared boardsChangeBoardModal.tsx now applies a canWriteToBoard filter to the options list. Boards where the current user is not the owner/admin and which are shared are excluded. Public boards and the user's own boards remain available.

  4. Archive/Unarchive accessible to non-owners — Both the Archive and Unarchive menu items in BoardContextMenu.tsx now carry isDisabled={!canDeleteBoard}, so non-owners of shared and public boards see them greyed out.

Copilot AI and others added 3 commits March 9, 2026 22:56
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
…-board filter, archive

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
@lstein lstein force-pushed the copilot/enhancement-allow-shared-boards branch from 1fd217f to 9f8f7a1 Compare March 10, 2026 02:56
…image, etc.)

Previously, images in shared boards owned by another user could not be
dragged at all — the draggable setup was completely skipped in
GalleryImage.tsx when canWriteImages was false. This blocked ALL drop
targets including the viewer, reference image pane, and canvas.

Now images are always draggable. The board-move restriction is enforced
in the dnd target isValid functions instead:
- addImageToBoardDndTarget: rejects moves from shared boards the user
  doesn't own (unless admin or board is public)
- removeImageFromBoardDndTarget: same check

Other drop targets (viewer, reference images, canvas, comparison, etc.)
remain fully functional for shared board images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lstein
Copy link
Copy Markdown
Owner

lstein commented Apr 4, 2026

Fix: Drag-and-drop from shared boards

Problem: Images in shared boards owned by another user couldn't be dragged at all. The GalleryImage component skipped the entire draggable() setup when canWriteImages was false, which disabled ALL drop targets — including the viewer, reference image pane, canvas, and comparison.

Fix: Moved the ownership check from the drag source to the drop targets:

  1. GalleryImage.tsx: Images are now always draggable regardless of board ownership. Removed the canDragFromBoard gate and the useBoardAccess dependency.

  2. dnd.ts: Added canMoveFromSourceBoard() helper that checks the boards RTK Query cache to determine if the current user owns the source board (or is admin, or the board is public).

  3. addImageToBoardDndTarget.isValid and removeImageFromBoardDndTarget.isValid: Now reject board-to-board moves when the source board is a shared board the user doesn't own. Other drop targets (viewer, reference images, canvas entities, comparison, upscale, node fields) are unaffected.

Result: Users can drag images from shared boards to the viewer, reference image pane, canvas, etc. Only board-move drop targets are disabled for non-owned shared boards.

Copy link
Copy Markdown
Owner

@lstein lstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works as advertised.

DustyShoe and others added 11 commits April 4, 2026 23:58
…8963)

* Feat(Canvas): Add button to hide preview thumbnails in staging area.

* Code clean up. Added tests.

* Fix: Removed redundant Icon aliases
* Upgrade spandrel to 0.4.2 in uv.lock

* Fixed typos
* Fix workflow copy hotkeys in info view

* Fix Makefile help target copy

* Fix workflow info view copy handling

* Fix workflow edge delete hotkeys
Co-authored-by: dunkeroni <dunkeroni@gmail.com>
* Run vitest during frontend build

* Add frontend-test Make target
…red (invoke-ai#9017)

* Initial plan

* Warn user when credentials have expired in multiuser mode

Agent-Logs-Url: https://github.com/lstein/InvokeAI/sessions/f0947cda-b15c-475d-b7f4-2d553bdf2cd6

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Address code review: avoid multiple localStorage reads in base query

Agent-Logs-Url: https://github.com/lstein/InvokeAI/sessions/f0947cda-b15c-475d-b7f4-2d553bdf2cd6

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* bugfix(multiuser): ask user to log back in when authentication token expires

* feat: sliding window session expiry with token refresh

Backend:
- SlidingWindowTokenMiddleware refreshes JWT on each mutating request
  (POST/PUT/PATCH/DELETE), returning a new token in X-Refreshed-Token
  response header. GET requests don't refresh (they're often background
  fetches that shouldn't reset the inactivity timer).
- CORS expose_headers updated to allow X-Refreshed-Token.

Frontend:
- dynamicBaseQuery picks up X-Refreshed-Token from responses and
  updates localStorage so subsequent requests use the fresh expiry.
- 401 handler only triggers sessionExpiredLogout when a token was
  actually sent (not for unauthenticated background requests).
- ProtectedRoute polls localStorage every 5s and listens for storage
  events to detect token removal (e.g. manual deletion, other tabs).

Result: session expires after TOKEN_EXPIRATION_NORMAL (1 day) of
inactivity, not a fixed time after login. Any user-initiated action
resets the clock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(backend): ruff

* fix: address review feedback on auth token handling

Bug fixes:
- ProtectedRoute: only treat 401 errors as session expiry, not
  transient 500/network errors that should not force logout
- Token refresh: use explicit remember_me claim in JWT instead of
  inferring from remaining lifetime, preventing silent downgrade of
  7-day tokens to 1-day when <24h remains
- TokenData: add remember_me field, set during login

Tests (6 new):
- Mutating requests (POST/PUT/DELETE) return X-Refreshed-Token
- GET requests do not return X-Refreshed-Token
- Unauthenticated requests do not return X-Refreshed-Token
- Remember-me token refreshes to 7-day duration even near expiry
- Normal token refreshes to 1-day duration
- remember_me claim preserved through refresh cycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(backend): ruff

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
…f cardinality matches (invoke-ai#8869)

* Added If node

* Added stricter type checking on inputs

* feat(nodes): make if-node type checks cardinality-aware without loosening global AnyField

* chore: typegen
weblate and others added 7 commits April 5, 2026 23:54
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2264 of 2309 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.7% (1419 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2290 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2319 of 2372 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2327 of 2380 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2328 of 2382 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2370 of 2429 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Finnish)

Currently translated at 1.5% (37 of 2429 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fi/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2373 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Japanese)

Currently translated at 87.1% (2120 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2374 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Japanese)

Currently translated at 92.2% (2244 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2374 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Spanish)

Currently translated at 29.4% (720 of 2444 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/es/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
Co-authored-by: Ilmari Laakkonen <ilmarille@gmail.com>
Co-authored-by: 嶋田豪介 <shimada_gosuke@cyberagent.co.jp>
Co-authored-by: Lucas Prone <sac2087@gmail.com>
)

OneTrainer exports Z-Image LoRAs with 'transformer.layers.' key prefix
instead of 'diffusion_model.layers.'. Add this prefix (and the
PEFT-wrapped 'base_model.model.transformer.layers.' variant) to the
Z-Image LoRA probe so these models are correctly identified and loaded.
* feat: add support for OneTrainer BFL Flux LoRA format

Newer versions of OneTrainer export Flux LoRAs using BFL internal key
names (double_blocks, single_blocks, img_attn, etc.) with a
'transformer.' prefix and split QKV projections (qkv.0/1/2, linear1.0/1/2/3).
This format was not recognized by any existing detector.

Add detection and conversion for this format, merging split QKV and
linear1 layers into MergedLayerPatch instances for the fused BFL model.

* chore ruff
…-allow-shared-boards' into copilot/enhancement-allow-shared-boards
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants