Skip to content

feat: allow boards to be owned by a circle (team)#7897

Closed
jospoortvliet wants to merge 7 commits intonextcloud:mainfrom
jospoortvliet:feat/circle-owned-boards
Closed

feat: allow boards to be owned by a circle (team)#7897
jospoortvliet wants to merge 7 commits intonextcloud:mainfrom
jospoortvliet:feat/circle-owned-boards

Conversation

@jospoortvliet
Copy link
Copy Markdown
Member

Summary

This PR implements circle (team) ownership of Deck boards, analogous to how Collectives uses circles as owners. It allows boards to be transferred to a Nextcloud team, after which all circle members automatically receive full owner-level permissions.

Fixes #7747 (adds validation that the new owner exists before transferring).

What changes

  • Database: New owner_type column on oc_deck_boards (SMALLINT, default 0 = user). Migration Version11002Date20260429000000.
  • BoardMapper: Resolves circle owners via CirclesService::getCircle(), lists circle-owned boards alongside user-owned and shared boards in findAllForUser() and findBoardIds().
  • PermissionService: userIsBoardOwner() checks circle membership for circle-owned boards; findUsers() expands circle members instead of treating the owner ID as a user UID.
  • BoardService: transferBoardOwnership() validates the target (user must exist; circle must exist and Circles app must be enabled) before making any DB change. Skips the "add previous owner as ACL participant" step and content remap when transferring to a circle.
  • BoardController: Accepts newOwnerType (0 or 7) in the transferOwner REST endpoint; returns HTTP 400 for invalid values.
  • OCC command: New --to-circle flag on deck:transfer-ownership to treat <newOwner> as a circle ID.
  • Frontend:
    • Board list (BoardItem.vue): Shows a team icon instead of an avatar for circle-owned boards.
    • Sharing sidebar (SharingTabSidebar.vue): Shows "Team" label for circle-owned boards; adds a "Transfer ownership" action button on each ACL participant row (visible to board owner for user-owned boards, or to managers for circle-owned boards).
    • Vuex store: transferOwnership action forwards newOwnerType to the REST endpoint.
  • Tests: Unit tests added for BoardTest, PermissionServiceTest, and BoardServiceTest covering the new paths.

How to use

Via OCC (CLI):

# Transfer a user-owned board to a circle
php occ deck:transfer-ownership <currentOwnerUid> <circleId> <boardId> --to-circle

# Transfer all boards from a user to a circle
php occ deck:transfer-ownership <currentOwnerUid> <circleId> --to-circle

Via UI:
Open a board → Sharing tab → click the "⋯" menu next to any participant → "Transfer ownership".

Notes

  • Creation is always "create as user, then transfer" — no special creation UI is added.
  • Circle members receive full owner-level permissions (read/edit/manage/share) automatically via userIsBoardOwner().
  • Card content (assignedUsers, createdBy) is not remapped when transferring to a circle, since a circle is not a user account.
  • The Circles app must be enabled for circle ownership to function; the --to-circle flag returns an error otherwise.

🤖 Generated with Claude Code

Add an owner_type column to deck_boards (SMALLINT, default 0) that mirrors
the existing Acl::PERMISSION_TYPE_* constants. A value of 0 means the owner
is a user (preserving all existing behaviour); 7 means the owner is a
circle/team.

- DB migration Version11002Date20260429000000 adds the column idempotently
- Board entity gains $ownerType property, type registration, docblock
  accessors, and automatic serialisation into API responses as ownerType
- BoardMapper: add owner_type to every explicit SELECT column list so the
  field is populated when entities are hydrated from those queries
  (SELECT * queries already include it automatically)
- BoardTest: update all jsonSerialize assertions to expect ownerType: 0

No functional changes in this commit; subsequent steps will wire up
permission checks, transfer logic, and the UI.

AI-assistant: Claude Code 2.1.80 (Claude Sonnet 4.7)
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
mapOwner(): when owner_type = PERMISSION_TYPE_CIRCLE (7), resolve the
owner string as a circle ID via CirclesService and return a Circle object,
matching the behaviour already used in mapAcl() for circle ACL entries.
The federated-user and plain-user paths are unchanged.

findAllByCircleOwner(): new method that finds boards where owner_type = 7
and owner is a circle the requesting user belongs to. Follows the same
filter-parameter contract as the other findAllBy* methods; sets shared = 0
(user is effectively an owner, not just a collaborator).

findAllForUser(): includes findAllByCircleOwner() results in the merged
board list alongside the existing user, group, and circle-share sources.

findBoardIds(): adds a third query segment for circle-owned boards,
reusing the $circles list already fetched for the circle-share segment.

transferOwnership(): adds an optional $newOwnerType parameter (default
PERMISSION_TYPE_USER, placed after $boardId to preserve backward
compatibility) and stores it as owner_type in the UPDATE, so a future
transfer to a circle atomically sets both owner and owner_type.

No functional change for existing user-owned boards; all new paths either
return empty results (no circles app / user in no circles) or are blocked
by the as-yet-unchanged PermissionService (step 4).

AI-assistant: Claude Code 2.1.80 (Claude Sonnet 4.7)
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
… boards

userIsBoardOwner(): when the board's owner_type is PERMISSION_TYPE_CIRCLE,
delegate to CirclesService::isUserInCircle() instead of comparing the owner
string directly to the user ID.  Because getPermissions() and
matchPermissions() both gate every permission on userIsBoardOwner(), this
single change gives every circle member full read/edit/manage/share access
to a circle-owned board with no further changes to the permission stack.

findUsers(): for circle-owned boards the owner field holds a circle ID, not
a user ID, so the existing "add board owner as a User" path would create a
dangling entry.  It is replaced by an expansion of the owning circle's
inherited members, reusing the same Member::LEVEL_MEMBER + getUserType()===1
guard already present for circle ACL entries below.

Tests: add testUserIsBoardOwnerCircleMember covering the member→true and
non-member→false cases for a circle-owned board.

AI-assistant: Claude Code 2.1.80 (Claude Sonnet 4.7)
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
transferBoardOwnership() gains a newOwnerType parameter (default PERMISSION_TYPE_USER, backward-compatible). Validates new owner before any DB change: userExists() for user targets, CirclesService::getCircle() for circle targets, throwing BadRequestException on failure (also fixes the silent corruption bug when transferring to a non-existent user).

For circle transfers: correct ACL type used in deleteParticipantFromBoard, content remap is skipped (card owners cannot map to a circle), previous user owner receives a back-fill ACL entry unless changeContent=true.

transferOwnership() (bulk OCC path) gains the same newOwnerType parameter and switches to findAllByOwner so it works for both user-owned and circle-owned boards. CirclesService added to the constructor for circle validation.

Tests: transfer to circle, to missing user, to missing circle.

AI-assistant: Claude Code 2.1.80 (Claude Sonnet 4.7)
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
BoardController::transferOwner() now accepts an optional newOwnerType parameter (0=user, 7=circle). Unknown values return HTTP 400. The validated type is forwarded to BoardService::transferBoardOwnership().

AI-assistant: Claude Code 2.1.80 (Claude Sonnet 4.7)
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
New --to-circle option treats the newOwner argument as a circle ID. The command labels output accordingly, wraps the transfer in an error handler so invalid circle IDs print a clean message, and forwards PERMISSION_TYPE_CIRCLE to the service layer. Error messages are now surfaced for both single-board and bulk transfers.

AI-assistant: Claude Code 2.1.80 (Claude Sonnet 4.7)
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
…ip button

SharingTabSidebar: when board.ownerType === 7, render a team icon instead of NcAvatar for the owner row and label it Team. The hidden Owner NcActionCheckbox is replaced by a NcActionButton labelled Transfer ownership. For user-owned boards it appears only for user ACL entries when the current user is the owner (unchanged). For circle-owned boards it appears for any ACL entry when canManage is true. Confirmation dialog and success/error messages include the target label (team name or user ID). newOwnerType is forwarded through the Vuex transferOwnership action to the PUT payload.

BoardItem: guard NcAvatar with v-if board.ownerType !== 7 and show a team icon div for circle-owned boards, preventing a lookup of a circle ID as a user avatar.

AI-assistant: Claude Code 2.1.80 (Claude Sonnet 4.7)
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
@jospoortvliet jospoortvliet deleted the feat/circle-owned-boards branch April 30, 2026 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Board ownership transfer to non-existent user

1 participant