From 778d7d4acd1f9c53e71e86b97419aee4940ee8e1 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Mon, 15 Jul 2024 16:59:52 +0700 Subject: [PATCH 01/12] Add GQL mutation for bulk-adding org members Most of this code is identical to the bulk-add code in ProjectMutations, but the org version explicitly does *not* invite members if an email address is not found. Instead, any usernames or emails not found are returned to the frontend so that the user can take appropriate action. --- backend/LexBoxApi/GraphQL/OrgMutations.cs | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/backend/LexBoxApi/GraphQL/OrgMutations.cs b/backend/LexBoxApi/GraphQL/OrgMutations.cs index e7d6ae56e..e8e261ce2 100644 --- a/backend/LexBoxApi/GraphQL/OrgMutations.cs +++ b/backend/LexBoxApi/GraphQL/OrgMutations.cs @@ -196,6 +196,52 @@ private async Task UpdateOrgMemberRole(LexBoxDbContext dbContext, Organization o await dbContext.SaveChangesAsync(); } + public record BulkAddOrgMembersInput(Guid OrgId, string[] Usernames, OrgRole Role); + public record OrgMemberRole(string Username, OrgRole Role); + public record BulkAddOrgMembersResult(List AddedMembers, List NotFoundMembers, List ExistingMembers); + + [Error] + [Error] + [AdminRequired] + [UseMutationConvention] + public async Task BulkAddOrgMembers( + BulkAddOrgMembersInput input, + LexBoxDbContext dbContext) + { + var orgExists = await dbContext.Orgs.AnyAsync(p => p.Id == input.OrgId); + if (!orgExists) throw NotFoundException.ForType(); + List AddedMembers = []; + List ExistingMembers = []; + List NotFoundMembers = []; + var existingUsers = await dbContext.Users.Include(u => u.Organizations).Where(u => input.Usernames.Contains(u.Username) || input.Usernames.Contains(u.Email)).ToArrayAsync(); + var byUsername = existingUsers.Where(u => u.Username is not null).ToDictionary(u => u.Username!); + var byEmail = existingUsers.Where(u => u.Email is not null).ToDictionary(u => u.Email!); + foreach (var usernameOrEmail in input.Usernames) + { + var user = byUsername.GetValueOrDefault(usernameOrEmail) ?? byEmail.GetValueOrDefault(usernameOrEmail); + if (user is null) + { + NotFoundMembers.Add(new OrgMemberRole(usernameOrEmail, input.Role)); + } + else + { + var userOrg = user.Organizations.FirstOrDefault(p => p.OrgId == input.OrgId); + if (userOrg is not null) + { + ExistingMembers.Add(new OrgMemberRole(user.Username ?? user.Email!, userOrg.Role)); + } + else + { + AddedMembers.Add(new OrgMemberRole(user.Username ?? user.Email!, input.Role)); + // Not yet a member, so add a membership. We don't want to touch existing memberships, which might have other roles + user.Organizations.Add(new OrgMember { Role = input.Role, OrgId = input.OrgId, UserId = user.Id }); + } + } + } + await dbContext.SaveChangesAsync(); + return new BulkAddOrgMembersResult(AddedMembers, NotFoundMembers, ExistingMembers); + } + [Error] [Error] [Error] From 75a925e667285fdc6c1c37be7276f353dc09ab7c Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 16 Jul 2024 10:24:24 +0700 Subject: [PATCH 02/12] Add frontend UI to bulk-add members to orgs --- frontend/schema.graphql | 25 +++ frontend/src/lib/i18n/locales/en.json | 15 ++ .../(authenticated)/org/[org_id]/+page.svelte | 2 + .../(authenticated)/org/[org_id]/+page.ts | 33 ++++ .../org/[org_id]/BulkAddOrgMembers.svelte | 166 ++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 frontend/src/routes/(authenticated)/org/[org_id]/BulkAddOrgMembers.svelte diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 2f5792744..f730cd9a9 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -31,6 +31,17 @@ type AuthUserProject { projectId: UUID! } +type BulkAddOrgMembersPayload { + bulkAddOrgMembersResult: BulkAddOrgMembersResult + errors: [BulkAddOrgMembersError!] +} + +type BulkAddOrgMembersResult { + addedMembers: [OrgMemberRole!]! + notFoundMembers: [OrgMemberRole!]! + existingMembers: [OrgMemberRole!]! +} + type BulkAddProjectMembersPayload { bulkAddProjectMembersResult: BulkAddProjectMembersResult errors: [BulkAddProjectMembersError!] @@ -208,6 +219,7 @@ type Mutation { removeProjectFromOrg(input: RemoveProjectFromOrgInput!): RemoveProjectFromOrgPayload! setOrgMemberRole(input: SetOrgMemberRoleInput!): SetOrgMemberRolePayload! changeOrgMemberRole(input: ChangeOrgMemberRoleInput!): ChangeOrgMemberRolePayload! + bulkAddOrgMembers(input: BulkAddOrgMembersInput!): BulkAddOrgMembersPayload! @authorize(policy: "AdminRequiredPolicy") changeOrgName(input: ChangeOrgNameInput!): ChangeOrgNamePayload! createProject(input: CreateProjectInput!): CreateProjectPayload! @authorize(policy: "VerifiedEmailRequiredPolicy") addProjectMember(input: AddProjectMemberInput!): AddProjectMemberPayload! @@ -293,6 +305,11 @@ type OrgMemberDtoCreatedBy { name: String! } +type OrgMemberRole { + username: String! + role: OrgRole! +} + type OrgProjects { org: Organization! project: Project! @@ -462,6 +479,8 @@ union AddProjectMemberError = NotFoundError | DbError | ProjectMembersMustBeVeri union AddProjectToOrgError = DbError | NotFoundError +union BulkAddOrgMembersError = NotFoundError | DbError + union BulkAddProjectMembersError = NotFoundError | InvalidEmailError | DbError union ChangeOrgMemberRoleError = DbError | NotFoundError @@ -518,6 +537,12 @@ input BooleanOperationFilterInput { neq: Boolean } +input BulkAddOrgMembersInput { + orgId: UUID! + usernames: [String!]! + role: OrgRole! +} + input BulkAddProjectMembersInput { projectId: UUID usernames: [String!]! diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 24ed816d7..6ba8c8545 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -263,6 +263,21 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia "user_needs_to_relogin": "Added members will need to log out and back in again before they see the new organization.", "invalid_email_address": "Invalid email address: {email}", }, + "bulk_add_members": { + "add_button": "Bulk Add Members", + "explanation": "Adds all the entered logins and emails to this organization. Unlike the bulk-add feature for projects, new accounts will NOT be automatically created.", + "modal_title": "Bulk Add Members", + "submit_button": "Add Members", + "finish_button": "Close", + "usernames": "Logins or emails (one per line)", + "usernames_description": "This should be the **email or Send/Receive login** for existing accounts", + "invalid_username": "Invalid login/username: {username}. Only letters, numbers, and underscore (_) characters are allowed.", + "empty_user_field": "Please enter email addresses and/or logins", + "members_added": "{addedCount} new {addedCount, plural, one {member was} other {members were}} added to organization.", + "already_members": "{count, plural, one {# user was} other {# users were}} already in the organization.", + "accounts_not_found": "{notFoundCount} {notFoundCount, plural, one {user was} other {users were}} not found.", + "invalid_email_address": "Invalid email address: {email}.", + }, "change_role_modal": { "title": "Choose role for {name}", "button_label": "Change Role" diff --git a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte index 3de1d91f7..26624b8b7 100644 --- a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte +++ b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte @@ -21,6 +21,7 @@ import OrgMemberTable from './OrgMemberTable.svelte'; import ProjectTable from '$lib/components/Projects/ProjectTable.svelte'; import type { UUID } from 'crypto'; + import BulkAddOrgMembers from './BulkAddOrgMembers.svelte'; export let data: PageData; $: user = data.user; @@ -102,6 +103,7 @@