Skip to content

Conversation

@fm3
Copy link
Member

@fm3 fm3 commented Nov 10, 2025

Allow specifying user roles and teams already when creating invites.

URL of deployed dev instance (used for testing):

Steps to test:

  • Go to Users page
  • Open "Invite Users" modal and insert some email addresses
  • select permissions and roles.
  • To test the frontend code, have a look at the network tab and make sure the request includes all permissions and roles specified
  • Reopen modal again and check that the state has been reset
  • Also test that the older modal "Change teams and permissions", also available from the user list view, is still working fine
    • coderabbit found a bug there: before, when setting permissions of multiple users, admin rights could be revoked by changing nothing and "setting teams&permissions" -> this should not happen anymore, simply opening the modal for multiple users should not change anything
  • Check that users signing up or joining an orga using the generated invite actually get the specified teams and roles

TODOs:

  • Discuss
    • In the UI, you can create invites in bulk. Should the selected stuff be the same for all created invites?
    • Do we need to support editing created invites?
  • Backend
    • store isTeamManager and isAdmin from invite parameters
    • use them when new user uses invite
    • store team roles from invite parameters
    • use them when new user uses invite
  • Frontend
    • Allow specifying isTeamManager and isAdmin when creating invites
    • Also allow support specifying team roles
    • pass them to the backend invite create route

Issues:

Context


  • Added changelog entry (create a $PR_NUMBER.md file in unreleased_changes or use ./tools/create-changelog-entry.py)
  • Considered common edge cases

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 10, 2025

📝 Walkthrough

Walkthrough

Adds invite-time role and team assignment: invitations, DB schema, services, and controllers now carry isAdmin, isDatasetManager, and per-invite teamMemberships. User creation and join flows compute and persist initial team memberships from organization and optional invite. Frontend invite UI and API send the new fields.

Changes

Cohort / File(s) Summary
Backend Controllers
app/controllers/AuthenticationController.scala, app/controllers/OrganizationController.scala, app/controllers/UserController.scala
Fetch/propagate initial team memberships during registration, signup, and join flows; extend insert/joinOrganization calls to accept isDatasetManager, isAdmin, and teamMemberships; remove teamMembershipService ctor dependency and adjust team-admin authorization check.
User service & DAO
app/models/user/UserService.scala, app/models/user/User.scala, app/models/user/UserDAO.scala
Add initialTeamMemberships(...); extend insert and joinOrganization signatures to accept teamMemberships and isDatasetManager; use updateTeamMembershipsForUser with Seq[TeamMembership]; add inviteDAO dependency.
Invite model & DAO
app/models/user/Invite.scala, app/models/user/InviteDAO.scala (and related invite DAO files)
Add isAdmin and isDatasetManager to Invite; persist flags; add invite-team-roles persistence (insertTeamMemberships, findTeamMembershipsFor); propagate teamMemberships through invite flows and mail sending.
Team domain
app/models/team/Team.scala, app/models/team/TeamMembership.scala
Remove couldBeAdministratedBy and extends FoxImplicits from Team; add additional DAOs to TeamService ctor; add implicit Reads[TeamMembership] and remove publicReads helper.
Auth API models / Controllers
app/controllers/auth/InviteParameters.scala (and related auth files)
Change recipients to Seq[String]; add isAdmin, isDatasetManager, teamMemberships; switch JSON accessor to Reads instead of Format.
DB schema / Migrations
conf/evolutions/146-invite-roles.sql, conf/evolutions/reversions/146-invite-roles.sql, tools/postgres/schema.sql
Add isAdmin and isDatasetManager columns to invites; create invite_team_roles(_invite, _team, isTeamManager) table; bump schema to 146 and provide revert.
Frontend — Onboarding & API
frontend/javascripts/admin/onboarding.tsx, frontend/javascripts/admin/rest_api.ts, frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx
UI to select permissions and teams when inviting; add APITeamMembership type; extend sendInvitesForOrganization payload/signature with isAdmin, isDatasetManager, and teamMemberships; extract PermissionsAndTeamsComponent and expose role enums.
Frontend — UI helpers & small updates
frontend/javascripts/dashboard/dataset/helper_components.tsx, frontend/javascripts/viewer/view/action-bar/share_modal_view.tsx
Add DividerWithSubtitle component and replace specific Dividers in Share modal.
Changelog
unreleased_changes/9062.md
Document invite-time team & role assignment feature and reference migration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas needing extra attention:

  • DB migrations and revert: schema changes, constraints, default values, and transactionality.
  • Invite persistence: insertTeamMemberships, findTeamMembershipsFor and their transaction boundaries.
  • UserService: propagation of teamMemberships through insert, joinOrganization, and fillSuperUserIdentity.
  • Controller auth checks: changed team-administration check in UserController.
  • Frontend ↔ backend contract: sendInvitesForOrganization payload vs InviteParameters Reads.

Possibly related PRs

Suggested labels

new feature

Suggested reviewers

  • MichaelBuessemeyer
  • daniel-wer

Poem

🐇 I hop with teams and roles in hand,
I tuck them into invites so plans expand.
New accounts arrive with badges bright,
Assigned at once — no extra flight.
Hooray — fewer hops, more delight! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #7456: storing isAdmin and isDatasetManager in invites, storing team roles, applying them when users accept invites, and exposing UI controls for specifying them.
Out of Scope Changes check ✅ Passed All changes are within scope: backend team/role assignment via invites, frontend UI for permissions and team selection, database schema updates for new fields, and helper components for UI composition.
Title check ✅ Passed The title 'Set Teams and Roles for Invites' clearly summarizes the main change—enabling users to assign teams and roles when creating invites.
Description check ✅ Passed The pull request description accurately describes the changeset: allowing specification of user roles and teams when creating invites, with comprehensive testing steps and backend/frontend TODOs.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch invite-roles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@knollengewaechs knollengewaechs marked this pull request as ready for review November 20, 2025 13:49
@knollengewaechs
Copy link
Contributor

@copilot write a testing protocol including setting the permissions and team roles upon invite, which is also making sure that the old team and permissions modal is still working fine

Copy link

Copilot AI commented Nov 20, 2025

@knollengewaechs I've opened a new pull request, #9086, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/models/team/Team.scala (1)

23-37: Defaulting requestingUser to None currently filters out all teams

removeForeignOrganizationTeams uses:

teams.filter(team => requestingUser.map(_._organization).contains(team._organization))

With the default requestingUser: Option[User] = None, requestingUser.map(...).contains(...) is always false, so both allowedTeamsForFolder and allowedTeamsForDataset will return an empty list whenever callers omit the new argument (which is likely for existing call sites).

To preserve previous behavior and only apply the extra filter when a user is actually provided, consider:

-  private def removeForeignOrganizationTeams(teams: List[Team], requestingUser: Option[User]): List[Team] =
-    teams.filter(team => requestingUser.map(_._organization).contains(team._organization))
+  private def removeForeignOrganizationTeams(teams: List[Team], requestingUser: Option[User]): List[Team] =
+    requestingUser match {
+      case Some(user) => teams.filter(_. _organization == user._organization)
+      case None       => teams
+    }

(or equivalently teams.filter(team => requestingUser.forall(_._organization == team._organization))).

This keeps old behavior for existing callers while enforcing same‑organization filtering when requestingUser is provided.

Also applies to: 90-92

app/models/user/UserService.scala (1)

139-167: initialTeamMemberships can create duplicate memberships for the same team

initialTeamMemberships currently does:

organizationTeamMembership = Seq(TeamMembership(organizationTeamId, isTeamManager = false))
inviteTeamMemberships <- ...
uniqueTeamMemberships = inviteTeamMemberships ++ organizationTeamMembership

Despite the name, uniqueTeamMemberships is just a concatenation. If an invite already contains a membership for the organization’s default team, you’ll end up with two entries for the same teamId. When passed into updateTeamMembershipsForUser, this can:

  • Cause duplicate rows in user_team_roles (or hit a unique constraint, depending on schema).
  • Lead to confusing behavior in code that assumes one membership per team.

A safer approach is to deduplicate by teamId and reconcile isTeamManager (e.g., prefer true if any entry for that team is a manager):

-    inviteTeamMemberships <- inviteIdOpt match {
-      case Some(inviteId) => inviteDAO.findTeamMembershipsFor(inviteId) ?~> "failed to get invite team memberships"
-      case None           => Fox.successful(Seq.empty)
-    }
-    uniqueTeamMemberships = inviteTeamMemberships ++ organizationTeamMembership
-  } yield uniqueTeamMemberships
+    inviteTeamMemberships <- inviteIdOpt match {
+      case Some(inviteId) => inviteDAO.findTeamMembershipsFor(inviteId) ?~> "failed to get invite team memberships"
+      case None           => Fox.successful(Seq.empty)
+    }
+    combined = inviteTeamMemberships ++ organizationTeamMembership
+    uniqueTeamMemberships = combined
+      .groupBy(_.teamId)
+      .values
+      .map { membershipsForTeam =>
+        val teamId = membershipsForTeam.head.teamId
+        val isTeamManager = membershipsForTeam.exists(_.isTeamManager)
+        TeamMembership(teamId, isTeamManager)
+      }
+      .toSeq
+  } yield uniqueTeamMemberships

This keeps the implicit “everyone is in the organization team” behavior while avoiding duplicates and preserving manager roles from invites.

🧹 Nitpick comments (2)
frontend/javascripts/admin/onboarding.tsx (1)

238-238: Remove debug console.log statement.

This console.log statement should be removed before merging to production.

Apply this diff:

-    console.log("Setting default team in invite modal");
app/models/user/Invite.scala (1)

46-57: Consider wrapping invite and team-membership inserts in a single transaction

Right now, inviteDAO.insertOne(invite) and inviteDAO.insertTeamMemberships(invite._id, teamMemberships) are two separate DBIO runs. If the second one fails, you can end up with an invite without its intended team roles.

It would be more robust to compose both operations into a single transactional DBIO so that either both the invite and its invite_team_roles are written, or neither is.

Also applies to: 59-75, 139-156

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4e1b2c and 55c6a01.

📒 Files selected for processing (17)
  • app/controllers/AuthenticationController.scala (7 hunks)
  • app/controllers/OrganizationController.scala (2 hunks)
  • app/controllers/UserController.scala (2 hunks)
  • app/models/team/Team.scala (1 hunks)
  • app/models/team/TeamMembership.scala (1 hunks)
  • app/models/user/Invite.scala (6 hunks)
  • app/models/user/User.scala (1 hunks)
  • app/models/user/UserService.scala (4 hunks)
  • conf/evolutions/146-invite-roles.sql (1 hunks)
  • conf/evolutions/reversions/146-invite-roles.sql (1 hunks)
  • frontend/javascripts/admin/onboarding.tsx (7 hunks)
  • frontend/javascripts/admin/rest_api.ts (2 hunks)
  • frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (7 hunks)
  • frontend/javascripts/dashboard/dataset/helper_components.tsx (2 hunks)
  • frontend/javascripts/viewer/view/action-bar/share_modal_view.tsx (3 hunks)
  • tools/postgres/schema.sql (3 hunks)
  • unreleased_changes/9062.md (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-04-23T08:51:57.756Z
Learnt from: frcroth
Repo: scalableminds/webknossos PR: 8236
File: webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/MeshFileService.scala:170-173
Timestamp: 2025-04-23T08:51:57.756Z
Learning: In the webknossos codebase, classes extending `FoxImplicits` have access to an implicit conversion from `Option[A]` to `Fox[A]`, where `None` is converted to an empty Fox that fails gracefully in for-comprehensions.

Applied to files:

  • app/models/user/Invite.scala
📚 Learning: 2025-04-28T14:18:04.368Z
Learnt from: frcroth
Repo: scalableminds/webknossos PR: 8236
File: webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/NeuroglancerPrecomputedMeshFileService.scala:161-166
Timestamp: 2025-04-28T14:18:04.368Z
Learning: In Scala for-comprehensions with the Fox error handling monad, `Fox.fromBool()` expressions should use the `<-` binding operator instead of the `=` assignment operator to properly propagate error conditions. Using `=` will cause validation failures to be silently ignored.

Applied to files:

  • app/controllers/UserController.scala
📚 Learning: 2025-08-14T14:12:03.293Z
Learnt from: hotzenklotz
Repo: scalableminds/webknossos PR: 8849
File: frontend/javascripts/viewer/view/action-bar/ai_job_modals/components/collapsible_split_merger_evaluation_settings.tsx:39-51
Timestamp: 2025-08-14T14:12:03.293Z
Learning: Since Ant Design version 5.18, Form.Item components support the layout prop (layout="horizontal", layout="vertical", etc.), allowing individual form items to override the parent Form's layout setting. This is a newer API addition that provides more granular control over form item layouts.

Applied to files:

  • frontend/javascripts/dashboard/dataset/helper_components.tsx
📚 Learning: 2025-08-14T14:12:03.293Z
Learnt from: hotzenklotz
Repo: scalableminds/webknossos PR: 8849
File: frontend/javascripts/viewer/view/action-bar/ai_job_modals/components/collapsible_split_merger_evaluation_settings.tsx:39-51
Timestamp: 2025-08-14T14:12:03.293Z
Learning: Since Ant Design version 5.18, Form.Item components support the layout prop (layout="horizontal", layout="vertical", layout="inline"), allowing individual form items to override the parent Form's layout setting. This enables mixed layouts within a single form and was added as a new API feature in v5.18 according to the official changelog.

Applied to files:

  • frontend/javascripts/dashboard/dataset/helper_components.tsx
📚 Learning: 2025-11-03T17:50:57.587Z
Learnt from: knollengewaechs
Repo: scalableminds/webknossos PR: 9025
File: frontend/javascripts/dashboard/advanced_dataset/dataset_table.tsx:10-10
Timestamp: 2025-11-03T17:50:57.587Z
Learning: In the webknossos codebase, both `import { Component } from "antd";` and `import { Component } from "antd/lib";` are valid import paths for antd components. Do not flag imports from "antd/lib" as errors.

Applied to files:

  • frontend/javascripts/dashboard/dataset/helper_components.tsx
🧬 Code graph analysis (9)
frontend/javascripts/viewer/view/action-bar/share_modal_view.tsx (1)
frontend/javascripts/dashboard/dataset/helper_components.tsx (1)
  • DividerWithSubtitle (80-82)
frontend/javascripts/admin/rest_api.ts (1)
frontend/javascripts/types/api_types.ts (1)
  • APITeamMembership (308-312)
app/models/user/Invite.scala (2)
app/models/team/TeamMembership.scala (2)
  • TeamMembership (11-11)
  • TeamMembership (12-17)
app/models/user/User.scala (1)
  • insertTeamMembershipQuery (558-561)
app/controllers/OrganizationController.scala (2)
app/models/user/UserService.scala (2)
  • initialTeamMemberships (157-168)
  • joinOrganization (168-198)
app/controllers/AuthenticationController.scala (1)
  • joinOrganization (415-444)
app/models/user/UserService.scala (4)
app/models/user/Invite.scala (1)
  • findTeamMembershipsFor (149-157)
app/models/user/User.scala (1)
  • updateTeamMembershipsForUser (561-569)
app/models/organization/Organization.scala (2)
  • isEmpty (107-113)
  • findOrganizationTeamId (145-152)
app/controllers/AuthenticationController.scala (1)
  • joinOrganization (415-444)
frontend/javascripts/admin/onboarding.tsx (5)
frontend/javascripts/libs/react_helpers.tsx (1)
  • useFetch (35-52)
frontend/javascripts/admin/rest_api.ts (2)
  • getEditableTeams (333-339)
  • sendInvitesForOrganization (1723-1740)
frontend/javascripts/types/api_types.ts (1)
  • APITeamMembership (308-312)
frontend/javascripts/dashboard/dataset/helper_components.tsx (1)
  • DividerWithSubtitle (80-82)
frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (1)
  • PermissionsAndTeamsComponent (63-249)
app/models/team/TeamMembership.scala (1)
app/models/team/Team.scala (2)
  • publicWrites (39-51)
  • findOne (120-127)
frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (4)
frontend/javascripts/types/api_types.ts (2)
  • APITeamMembership (308-312)
  • APIUser (338-353)
frontend/javascripts/libs/react_helpers.tsx (1)
  • useFetch (35-52)
frontend/javascripts/admin/rest_api.ts (2)
  • getEditableTeams (333-339)
  • updateUser (194-199)
frontend/javascripts/dashboard/dataset/helper_components.tsx (1)
  • DividerWithSubtitle (80-82)
app/controllers/AuthenticationController.scala (3)
app/models/team/TeamMembership.scala (2)
  • TeamMembership (11-11)
  • TeamMembership (12-17)
app/models/user/UserService.scala (5)
  • initialTeamMemberships (157-168)
  • insert (93-137)
  • joinOrganization (168-198)
  • isTeamManagerOrAdminOf (321-327)
  • isTeamManagerOrAdminOf (327-333)
app/models/user/Invite.scala (1)
  • inviteOneRecipient (46-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: frontend-tests
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (22)
app/models/user/User.scala (1)

561-568: Switch to Seq[TeamMembership] looks fine

The method logic (clear then re‑insert in one transaction) is unchanged and Seq integrates cleanly with the existing List(clearQuery) ++ insertQueries pattern. No issues from this refactor.

app/models/team/TeamMembership.scala (1)

11-17: JSON Reads for TeamMembership aligns with existing writer

The new jsonReads matches the fields emitted by publicWrites (id, isTeamManager), and unknown fields like name are ignored by Play JSON, so round‑tripping is consistent. No issues here.

Also applies to: 20-28

app/models/team/Team.scala (1)

221-227: Generalizing allowedTeams to Seq is safe

The method still deletes then reinserts all allowed teams in a single transactional replacement; switching the parameter from List to Seq doesn’t change behavior and aligns with other APIs moving to Seq.

unreleased_changes/9062.md (1)

1-5: Changelog entry is clear and sufficient

The note accurately summarizes the new invite capability and correctly links the evolution script. Looks good.

frontend/javascripts/viewer/view/action-bar/share_modal_view.tsx (1)

27-27: Shared DividerWithSubtitle usage looks good

Switching the two section headers to DividerWithSubtitle centralizes styling without touching the sharing logic. Import path and usage match the exported helper component.

Also applies to: 374-377, 441-444

frontend/javascripts/dashboard/dataset/helper_components.tsx (2)

2-2: LGTM!

The Divider import is used correctly in the new DividerWithSubtitle component below.


80-82: LGTM!

The DividerWithSubtitle component is clean and reusable. It provides consistent styling for section dividers across the application.

tools/postgres/schema.sql (3)

24-24: LGTM!

The schema version bump to 146 correctly reflects the database changes for invite roles and team memberships.


442-447: LGTM!

The invite_team_roles table structure is well-designed with appropriate constraints and a composite primary key on (_invite, _team).


574-575: LGTM!

The new columns isAdmin and isDatasetManager on the invites table have appropriate types, non-null constraints, and default values.

frontend/javascripts/admin/onboarding.tsx (2)

355-364: Note: TextArea is now a controlled component.

By adding the value prop alongside defaultValue, the TextArea becomes a controlled component. The defaultValue prop will be ignored. This appears intentional given the handleInviteesStringChange handler, but ensure this behavior is expected.


269-299: LGTM!

The sendInvite function correctly:

  • Validates at least one email address is provided
  • Computes isAdmin and isDatasetManager flags from selected permissions
  • Extracts team memberships for the invite
  • Passes all required parameters to the backend
  • Resets fields after successful invite
conf/evolutions/146-invite-roles.sql (1)

1-21: LGTM!

The forward migration is well-structured with:

  • Proper transaction boundaries
  • Version check precondition
  • Correct SQL syntax for adding columns and creating tables
  • Appropriate schema version update
frontend/javascripts/admin/rest_api.ts (2)

48-48: LGTM!

The APITeamMembership import is correctly added and used in the updated function signature below.


1723-1740: LGTM!

The sendInvitesForOrganization function signature is correctly extended to include:

  • isAdmin: boolean flag for admin permission
  • isDatasetManager: boolean flag for dataset manager permission
  • teamMemberships: array of team membership assignments

All parameters are properly passed to the backend API endpoint.

app/controllers/OrganizationController.scala (2)

80-87: LGTM!

The organization creation flow correctly:

  • Retrieves initial team memberships (organization team by default)
  • Passes isDatasetManager = false to maintain existing behavior
  • Preserves isOrganizationOwner = true for the owner
  • Supplies team memberships to the join flow

185-191: LGTM!

The addUser method consistently follows the same pattern as the create flow:

  • Retrieves initial team memberships before joining
  • Sets appropriate default permissions (isAdmin = false, isDatasetManager = false)
  • Provides team memberships to the service layer
app/controllers/UserController.scala (2)

185-185: LGTM!

The change from teamMembershipService.publicReads() to a direct List[TeamMembership] reader indicates that TeamMembership now has its own Reads implementation, which simplifies the code.


189-200: Simplified authorization check is sufficient and safe.

The authorization logic is secure due to the upstream filter at lines 340-341, which restricts teamsWithUpdate to only teams where the issuing user is already a team manager or organization admin. Regular org members cannot pass this filter, so the organization membership check in ensureProperTeamAdministration (line 342) provides a secondary safety layer but is not the only barrier.

The privilege escalation concern is not valid: a regular user cannot reach ensureProperTeamAdministration with teams unless they're already authorized to manage those teams via isTeamManagerOrAdminOf.

Note: The method couldBeAdministratedBy does not exist in the current codebase, so the claim about simplification cannot be verified. If this method was recently removed as part of the changes, please clarify the rationale for that removal.

app/models/user/Invite.scala (1)

23-33: Invite model and DAO mapping look consistent

Invite now includes isAdmin and isDatasetManager, and the DAO correctly parses and inserts these fields. This aligns the in-memory model with the new DB columns; no issues spotted here.

Also applies to: 107-119, 128-136

app/models/user/UserService.scala (1)

93-137: User creation/join flows now consistently use teamMemberships

The extended insert and joinOrganization signatures (including isDatasetManager and teamMemberships) plus the calls to userDAO.updateTeamMembershipsForUser give a single, consistent place where memberships are applied. That’s a good consolidation and matches how the rest of the code (e.g., teamMembershipsFor) expects to read memberships.

Also applies to: 168-197

app/controllers/AuthenticationController.scala (1)

284-308: User creation and org-join now correctly derive memberships from invites/organization

The changes to createUser, joinOrganization, and createOrganizationWithAdmin that call userService.initialTeamMemberships and pass teamMemberships (along with invite-derived isAdmin/isDatasetManager) into userService.insert/joinOrganization look consistent and match the new membership model. New users now start with the organization’s default team plus any invite-specified team roles.

Also applies to: 415-444, 878-895

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
frontend/javascripts/admin/onboarding.tsx (3)

226-252: Make resetFields robust when no “Default” team exists

setDefaultTeam only updates selectedTeams when a team named "Default" is present. If an organization ever lacks such a team, resetFields will leave whatever teams were previously selected in place, so closing/reopening the modal won’t actually reset team selections even though the permissions and email list reset.

You can make this robust and still keep the “Default” behavior by clearing selectedTeams when no default is found:

-  const setDefaultTeam = useCallback(() => {
-    if (defaultTeam != null) {
-      setSelectedTeams({
-        [defaultTeam.name]: {
-          id: defaultTeam.id,
-          name: defaultTeam.name,
-          isTeamManager: false,
-        },
-      });
-    }
-  }, [defaultTeam]);
+  const setDefaultTeam = useCallback(() => {
+    if (defaultTeam != null) {
+      setSelectedTeams({
+        [defaultTeam.name]: {
+          id: defaultTeam.id,
+          name: defaultTeam.name,
+          isTeamManager: false,
+        },
+      });
+    } else {
+      setSelectedTeams({});
+    }
+  }, [defaultTeam, setSelectedTeams]);

That way resetFields genuinely resets both permissions and team selections regardless of team setup.

Also applies to: 262-267, 381-382


268-298: Invite sending flow matches API contract; consider minor UX hardening

The sendInvite handler correctly validates that at least one email is present, derives isAdmin/isDatasetManager from selectedPermission, and passes Object.values(selectedTeams) into sendInvitesForOrganization, so all created invites share the configured permissions/teams as per the PR description. Resetting fields after a successful send and on cancel keeps the modal tidy.

If you want to harden UX slightly, you could:

  • Guard against double-submits by disabling the “Send Invite Emails” button while the request is in-flight.
  • Wrap sendInvitesForOrganization in a try/catch and show a failure Toast so backend errors don’t silently fail.

These are optional polish items; the current flow is functionally sound.

Also applies to: 300-307


337-353: Avoid mixing defaultValue and value on the invitee textarea

The additional copy and DividerWithSubtitle around the email input improve clarity, and making the textarea controlled via value={inviteesString} is appropriate, especially since resetFields clears it.

However, you now pass both defaultValue={inviteesString} and value={inviteesString} to Input.TextArea. With value present, defaultValue is redundant and can be confusing. Consider simplifying to:

-        <Input.TextArea
+        <Input.TextArea
           spellCheck={false}
           autoSize={{ minRows: 6 }}
           onChange={handleInviteesStringChange}
           placeholder={"[email protected]\[email protected]"}
-          defaultValue={inviteesString}
-          value={inviteesString}
+          value={inviteesString}
         />

This keeps the field fully controlled without any ambiguity.

Also applies to: 355-363

frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (2)

63-72: PermissionsAndTeamsComponent extraction looks good; watch for minor duplication/Radio defaults

The extracted PermissionsAndTeamsComponent nicely centralizes permission radio selection and per-team manager/member roles, and the renderSubtitlesWithDivider flag lets onboarding reuse it with DividerWithSubtitle while preserving the original modal’s look.

Two small follow-ups you might consider:

  • You now call useFetch(getEditableTeams, [], []) inside this component, and InviteUsersModal in onboarding.tsx also calls useFetch(getEditableTeams, [], []) just to discover the default team. That results in two parallel /api/teams?isEditable=true calls whenever the invite modal is used. If this endpoint is at all heavy or rate-limited, consider lifting the fetch to the parent and optionally passing teams into the component, or providing a “pre-fetched teams” prop to avoid the second call.

  • Similar to the textarea in onboarding, Radio.Group is effectively controlled via value={selectedPermission}, so defaultValue={selectedPermission} is redundant. Dropping defaultValue would simplify the control and match React’s standard controlled-component pattern.

These are non-blocking refinements; the current implementation is functionally correct.

Also applies to: 79-141, 143-153, 155-223, 224-249


251-291: Permission update logic now preserves multi-user flags; consider tightening payload and state reset

The new permission update flow fixes the earlier multi-user bug: setPermissionsAndTeams now seeds permissions from each user ({ isAdmin: user.isAdmin, isDatasetManager: user.isDatasetManager }) and only overrides them when activeUser.isAdmin && selectedUserIds.length === 1, so bulk “teams only” edits no longer silently demote admins or dataset managers. This addresses the previous review concern.

Two refinements worth considering:

  1. Avoid sending permission fields for multi-user edits: For bulk updates, you could omit isAdmin/isDatasetManager entirely instead of re-sending unchanged values, which makes the intent (“only teams change”) explicit and resilient to future server-side interpretation:

  •    const newTeams = Utils.values(selectedTeams);
    
  •    let permissions = { isAdmin: user.isAdmin, isDatasetManager: user.isDatasetManager };
    
  •    if (activeUser.isAdmin && selectedUserIds.length === 1) {
    
  •      permissions = { isAdmin: false, isDatasetManager: false };
    
  •    const newTeams = Utils.values(selectedTeams);
    
  •    let permissions: Partial<Pick<APIUser, "isAdmin" | "isDatasetManager">> = {};
    
  •    if (activeUser.isAdmin && selectedUserIds.length === 1) {
    
  •      permissions = { isAdmin: false, isDatasetManager: false };
         // If the current user is admin and only one user is edited we also update the permissions.
         if (selectedPermission === PERMISSIONS.admin) {
           permissions["isAdmin"] = true;
           permissions["isDatasetManager"] = false;
         } else if (selectedPermission === PERMISSIONS.datasetManager) {
           permissions["isDatasetManager"] = true;
           permissions["isAdmin"] = false;
         }
       }
    
  •    const newUser = { ...user, ...permissions, teams: newTeams };
    
  •    const newUser = { ...user, ...permissions, teams: newTeams };
    
    
    
  1. Reset local state when the selection/modal changes: selectedTeams/selectedPermission are only reinitialized when exactly one user is selected; if the component stays mounted across openings, switching from editing a single user to multiple users may keep stale selectedTeams from the prior session. If that’s not desired, consider resetting state when selection mode changes or when the modal is (re)opened, e.g.:

    useEffect(() => {
      if (selectedUserIds.length === 1) {
        const singleUserMaybe = getSingleUserMaybe(selectedUserIds, users);
        if (singleUserMaybe) {
          setSelectedTeams(_.keyBy(singleUserMaybe.teams, "name"));
          setSelectedPermission(getPermissionGroupOfUser(singleUserMaybe));
        }
      } else {
        // For multi-user edits, start with no teams selected
        setSelectedTeams({});
      }
    }, [selectedUserIds, users]);

Both points are about making behavior more explicit and predictable; the current code already satisfies the “don’t revoke permissions on multi-user edits” requirement.

Also applies to: 293-329, 331-358

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55c6a01 and 77c4433.

📒 Files selected for processing (3)
  • frontend/javascripts/admin/onboarding.tsx (7 hunks)
  • frontend/javascripts/admin/rest_api.ts (2 hunks)
  • frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/admin/rest_api.ts
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/javascripts/admin/onboarding.tsx (5)
frontend/javascripts/libs/react_helpers.tsx (1)
  • useFetch (35-52)
frontend/javascripts/admin/rest_api.ts (2)
  • getEditableTeams (333-339)
  • sendInvitesForOrganization (1729-1746)
frontend/javascripts/types/api_types.ts (1)
  • APITeamMembership (308-312)
frontend/javascripts/dashboard/dataset/helper_components.tsx (1)
  • DividerWithSubtitle (80-82)
frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (1)
  • PermissionsAndTeamsComponent (63-249)
frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (4)
frontend/javascripts/types/api_types.ts (2)
  • APITeamMembership (308-312)
  • APIUser (338-353)
frontend/javascripts/libs/react_helpers.tsx (1)
  • useFetch (35-52)
frontend/javascripts/admin/rest_api.ts (2)
  • getEditableTeams (333-339)
  • updateUser (194-199)
frontend/javascripts/dashboard/dataset/helper_components.tsx (1)
  • DividerWithSubtitle (80-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (3)
frontend/javascripts/admin/onboarding.tsx (1)

19-36: New invite permissions/teams wiring looks consistent

The added imports (REST helpers, DividerWithSubtitle, useFetch, APITeamMembership, PERMISSIONS, PermissionsAndTeamsComponent) and their usage in InviteUsersModal line up cleanly: the modal now delegates all permission/team UI to PermissionsAndTeamsComponent and passes the resulting selectedTeams/selectedPermission into sendInvitesForOrganization as expected. No integration issues spotted here.

Also applies to: 364-372

frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx (2)

3-4: Enums and shared props/helpers are clear and reusable

Exporting ROLES/PERMISSIONS and introducing TeamRoleComponentProps/TeamRoleModalProps makes the permissions/teams model explicit and reusable across the admin UI (including onboarding). The getSingleUserMaybe helper cleanly encapsulates the “only when a single user is selected” logic. No issues here.

Also applies to: 14-22, 24-41, 55-61


360-377: Modal integration and confirmation flow look good; ensure App context is present

Hooking into App.useApp().modal.confirm for the permission-change confirmation keeps the dialog styling consistent with the rest of Ant Design, and wiring PermissionsAndTeamsComponent into the Modal with onOk={handleUpdatePermissionsAndTeams} is straightforward.

Just double-check that PermissionsAndTeamsModalView is always rendered under an <App> provider from antd; otherwise App.useApp() won’t supply a real modal instance and the confirmation dialog will fail at runtime.

@fm3 fm3 changed the title WIP: Set Teams and Roles for Invites Set Teams and Roles for Invites Nov 20, 2025
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.

Assign user roles, team assignment upon invite

3 participants