feature: Deck boards can be owned by a circle#7899
Draft
jospoortvliet wants to merge 17 commits intomainfrom
Draft
feature: Deck boards can be owned by a circle#7899jospoortvliet wants to merge 17 commits intomainfrom
jospoortvliet wants to merge 17 commits intomainfrom
Conversation
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>
The circle member type check `getUserType() !== 1` uses a raw integer where a named constant is available and already imported. The existing ACL-circle expansion path already uses the named constant `Member::LEVEL_MEMBER` right next to the same condition, making the inconsistency obvious. Replace both occurrences (circle-owned board owner expansion added in this feature, and the pre-existing ACL-share expansion) with `Member::TYPE_USER`.
Now that owner_type distinguishes user owners (0) from circle owners (7), queries that look up boards by user ID should explicitly exclude circle-owned boards from the user-owner path. Without this guard, findBoardIds and findAllByUser would accidentally return a circle-owned board if the circle's single ID happened to match the user ID string - impossible today (circle IDs are UUIDs, user IDs are logins) but semantically wrong and a latent bug. Being explicit also makes the intent clear to future readers.
findAllForUser now calls both findAllByCircles() and findAllByCircleOwner(), each of which independently calls getUserCircles(). Without caching, every board-list request launches two Circles API sessions (getFederatedUser, startSession, getCircles) for the same user in the same PHP process. Add a $userCirclesCache keyed by userId, mirroring the existing $userCircleCache already used by isUserInCircle. The cache is per-object (per-request in a normal Nextcloud HTTP context), so stale data is not a concern.
…stants.js The feature introduced comparisons like `ownerType !== 7`, `ownerType === 7`, and `newOwnerType === 7` in three different files (SharingTabSidebar.vue, BoardItem.vue, main.js), spreading the magic number 7 (PERMISSION_TYPE_CIRCLE) through the frontend. The same file already defined SOURCE_TO_SHARE_TYPE with `circles: 7` locally, duplicating the constant yet again. Introduce src/helpers/constants.js that exports named constants mirroring the PHP Acl::PERMISSION_TYPE_* values, and move SOURCE_TO_SHARE_TYPE there as well. All three files now import and use the named constants; the local SOURCE_TO_SHARE_TYPE definition in SharingTabSidebar.vue is removed.
findAllByOwner() already queries WHERE owner = $owner, so every board in the returned collection is guaranteed to have getOwner() === $owner. The inner guard is always true and adds noise without benefit.
Doctrine DBAL defaults to unsigned=false for integer columns; spelling it out adds noise without conveying intent and may imply the choice was deliberate rather than incidental.
Signed-off-by: Jos Poortvliet <jospoortvliet@gmail.com>
canTransferTo was guarded with acl.type === PERMISSION_TYPE_USER, which silently excluded circle entries (type=7) from ever seeing the Transfer ownership button. The board backend already accepts PERMISSION_TYPE_CIRCLE as a valid newOwnerType, so the UI restriction had no purpose. Split the eligibility check into two separate concerns: 1. canBeOwnershipTarget: only user and circle participants are valid new owners (groups, remotes, etc. are not). 2. Permission to perform the transfer: current user must be the board owner (user-owned board) or have manage rights (circle-owned board). This means the Transfer ownership button now appears in the ... menu for both user and team/circle participants, as long as the current user has the right to initiate the transfer.
… not include the php-cs-fixer cache file
- allow transfer ownership to team targets from UI and OCC with team-first wording - rename OCC flag to --to-team (drop --to-circle) and auto-detect team IDs safely - validate transfer target up front in transferOwnership to fail fast on invalid users/teams - show team display names (not circle IDs) in transfer confirmations and OCC output - extend board share notifications to team members - extend card assignment notifications to team members and add team-specific notification text - mark team assignment notifications as processed on unassign - keep internal backend semantics based on circle IDs, with clarifying comments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
owner_typecolumn onoc_deck_boards(SMALLINT, default 0 = user). MigrationVersion11002Date20260429000000.CirclesService::getCircle(), lists circle-owned boards alongside user-owned and shared boards infindAllForUser()andfindBoardIds().userIsBoardOwner()checks circle membership for circle-owned boards;findUsers()expands circle members instead of treating the owner ID as a user UID.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.newOwnerType(0 or 7) in thetransferOwnerREST endpoint; returns HTTP 400 for invalid values.--to-circleflag ondeck:transfer-ownershipto treat<newOwner>as a circle ID.BoardItem.vue): Shows a team icon instead of an avatar for circle-owned boards.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).transferOwnershipaction forwardsnewOwnerTypeto the REST endpoint.BoardTest,PermissionServiceTest, andBoardServiceTestcovering the new paths.How to use
Via OCC (CLI):
Via UI:
Open a board → Sharing tab → click the "⋯" menu next to any participant → "Transfer ownership".
Notes
userIsBoardOwner().assignedUsers,createdBy) is not remapped when transferring to a circle, since a circle is not a user account. Cards can be assigned to circles, but remapping automatically seemed a bit too strong.--to-teamflag returns an error otherwise.🤖 Generated with AI - claude, codex, and quite some human review and testing back and forth.
issues