diff --git a/backend/LexBoxApi/GraphQL/ProjectMutations.cs b/backend/LexBoxApi/GraphQL/ProjectMutations.cs index 8d4070514..a48d7fa8c 100644 --- a/backend/LexBoxApi/GraphQL/ProjectMutations.cs +++ b/backend/LexBoxApi/GraphQL/ProjectMutations.cs @@ -230,6 +230,7 @@ await dbContext.ProjectUsers [Error] [Error] [Error] + [Error] [UseMutationConvention] [UseFirstOrDefault] [UseProjection] @@ -254,11 +255,14 @@ public async Task> AskToJoinProject( NotFoundException.ThrowIfNull(project); var managers = project.Users.Where(u => u.Role == ProjectRole.Manager); + var emailsSent = 0; foreach (var manager in managers) { if (manager.User is null) continue; await emailService.SendJoinProjectRequestEmail(manager.User, user, project); + emailsSent++; } + if (emailsSent == 0) throw new ProjectHasNoManagers(project.Code); return dbContext.Projects.Where(p => p.Id == projectId); } diff --git a/backend/LexCore/Exceptions/ProjectHasNoManagers.cs b/backend/LexCore/Exceptions/ProjectHasNoManagers.cs new file mode 100644 index 000000000..4df5f2cb6 --- /dev/null +++ b/backend/LexCore/Exceptions/ProjectHasNoManagers.cs @@ -0,0 +1,3 @@ +namespace LexCore.Exceptions; + +public class ProjectHasNoManagers(string projectCode) : Exception($"project {projectCode} has no managers"); diff --git a/frontend/schema.graphql b/frontend/schema.graphql index c090efba6..2476dcf57 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -404,6 +404,10 @@ type ProjectCreatorsMustHaveEmail implements Error { message: String! } +type ProjectHasNoManagers implements Error { + message: String! +} + type ProjectMemberInvitedByEmail implements Error { message: String! } @@ -579,7 +583,7 @@ union AddProjectToOrgError = DbError | NotFoundError union AddProjectsToOrgError = DbError | NotFoundError -union AskToJoinProjectError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMembersMustBeVerifiedForRole +union AskToJoinProjectError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMembersMustBeVerifiedForRole | ProjectHasNoManagers union BulkAddOrgMembersError = NotFoundError | DbError | UnauthorizedAccessError diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 552e726f2..be14d0e7d 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -212,6 +212,7 @@ Lexbox is free and [open source](https://github.com/sillsdev/languageforge-lexbo "no_thanks": "No thanks, create a new project", "click_to_view_related_projects": "Found {count, plural, one {# related project} other {# related projects}}, click here to see {count, plural, one {it} other {them}}", "join_request_sent": "Your request to join the {projectName} project has been sent to the project manager(s)", + "join_request_error_no_managers": "The {projectName} project has no project managers, so your request to join could not be sent", "description": "Description", "no_description": "This project does not have a description", "name_missing": "Project name required", @@ -531,6 +532,9 @@ Lexbox is free and [open source](https://github.com/sillsdev/languageforge-lexbo "promote_project": { "label": "Promote to real project", }, + "join_project": { + "label": "Ask to join project", + }, "open_with_viewer": "Browse", "open_with_flex": { "button": "Open with FLEx", diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte index f7e3ff574..f990c18ec 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte +++ b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte @@ -56,6 +56,8 @@ import { getSearchParamValues } from '$lib/util/query-params'; import FlexModelVersionText from '$lib/components/Projects/FlexModelVersionText.svelte'; import CrdtSyncButton from './CrdtSyncButton.svelte'; + import {_askToJoinProject} from '../create/+page'; // TODO: Should we duplicate this function in the project_code/+page.ts file, rather than importing it from elsewhere? + import {Duration} from '$lib/util/time'; export let data: PageData; $: user = data.user; @@ -143,6 +145,11 @@ || projectRole && !project.isConfidential // public by default for members (non-members shouldn't even be here) || orgRoles.some(role => role === OrgRole.Admin); + // Almost mirrors PermissionService.CanAskToJoinProject() in C#, but admins won't be shown the "ask to join" button + $: canAskToJoinProject = !user.isAdmin + && !projectRole + && orgRoles.some((_) => true); + let resetProjectModal: ResetProjectModal; async function resetProject(): Promise { await resetProjectModal.open(project.code, project.resetStatus); @@ -271,6 +278,19 @@ } } + let askLoading = false; + async function askToJoinProject(projectId: string, projectName: string): Promise { + askLoading = true; + const joinResult = await _askToJoinProject(projectId); + askLoading = false; + if (!joinResult.error) { + notifySuccess($t('project.create.join_request_sent', { projectName }), Duration.Persistent); + } + if (joinResult.error?.byType('ProjectHasNoManagers')) { + notifyWarning($t('project.create.join_request_error_no_managers', { projectName }), Duration.Persistent); + } + } + let projectConfidentialityModal: ProjectConfidentialityModal; let openInFlexModal: OpenInFlexModal; let leaveModal: ConfirmModal; @@ -316,6 +336,17 @@ + {:else if canAskToJoinProject} + {:else}