diff --git a/.github/workflows/deploy-branch.yaml b/.github/workflows/deploy-branch.yaml new file mode 100644 index 000000000..d316ac42c --- /dev/null +++ b/.github/workflows/deploy-branch.yaml @@ -0,0 +1,53 @@ +name: Deploy to Develop +run-name: Deploy ${{ github.ref_name }} to Develop +on: + workflow_dispatch: + + +jobs: + set-version: + name: Set Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.setVersion.outputs.VERSION }} + steps: + - name: Set Version + id: setVersion + # set version to date in vYYYY-MM-DD-commitSha format + run: | + shortSha=$(echo ${{ github.sha }} | cut -c1-8) + echo "VERSION=v$(date --rfc-3339=date)-$shortSha" >> ${GITHUB_OUTPUT} + build-api: + name: Build API + needs: [ set-version ] + uses: ./.github/workflows/lexbox-api.yaml + with: + version: ${{ needs.set-version.outputs.version }} + label-latest: true + + build-ui: + name: Build UI + needs: [ set-version ] + uses: ./.github/workflows/lexbox-ui.yaml + with: + version: ${{ needs.set-version.outputs.version }} + label-latest: true + + build-hgweb: + name: Build hgweb + needs: [ set-version ] + uses: ./.github/workflows/lexbox-hgweb.yaml + with: + version: ${{ needs.set-version.outputs.version }} + label-latest: true + + deploy: + name: Deploy Develop + uses: ./.github/workflows/deploy.yaml + needs: [ build-api, build-ui, build-hgweb, set-version ] + secrets: inherit + with: + version: ${{ needs.set-version.outputs.version }} + image: 'ghcr.io/sillsdev/lexbox-*' + k8s-environment: develop + deploy-domain: lexbox.dev.languagetechnology.org diff --git a/backend/LexBoxApi/GraphQL/ProjectMutations.cs b/backend/LexBoxApi/GraphQL/ProjectMutations.cs index 7ade79061..390b0e925 100644 --- a/backend/LexBoxApi/GraphQL/ProjectMutations.cs +++ b/backend/LexBoxApi/GraphQL/ProjectMutations.cs @@ -61,7 +61,9 @@ public record CreateProjectResponse(Guid? Id, CreateProjectResult Result); [Error] [Error] [Error] + [Error] [Error] + [Error] [UseMutationConvention] [UseFirstOrDefault] [UseProjection] @@ -74,14 +76,20 @@ public async Task> AddProjectMember(IPermissionService permi permissionService.AssertCanManageProject(input.ProjectId); var project = await dbContext.Projects.FindAsync(input.ProjectId); if (project is null) throw new NotFoundException("Project not found"); - var user = await dbContext.Users.FindByEmailOrUsername(input.UserEmail); - if (user is null) + var user = await dbContext.Users.Include(u => u.Projects).FindByEmailOrUsername(input.UsernameOrEmail); + if (user is null && input.UsernameOrEmail.Contains('@')) { var manager = loggedInContext.User; - await emailService.SendCreateAccountEmail(input.UserEmail, input.ProjectId, input.Role, manager.Name, project.Name); + await emailService.SendCreateAccountEmail(input.UsernameOrEmail, input.ProjectId, input.Role, manager.Name, project.Name); throw new ProjectMemberInvitedByEmail("Invitation email sent"); } - if (!user.HasVerifiedEmailForRole(input.Role)) throw new ProjectMembersMustBeVerified("Member must verify email first"); + if (user is null) throw new NotFoundException("User not found"); + if (user.Projects.Any(p => p.ProjectId == input.ProjectId)) + { + throw new AlreadyExistsException("User is already a member of this project"); + } + + user.AssertHasVerifiedEmailForRole(input.Role); user.UpdateCreateProjectsPermission(input.Role); dbContext.ProjectUsers.Add( new ProjectUsers { Role = input.Role, ProjectId = input.ProjectId, UserId = user.Id }); @@ -162,6 +170,7 @@ public async Task BulkAddProjectMembers( [Error] [Error] [Error] + [Error] [UseMutationConvention] [UseFirstOrDefault] [UseProjection] @@ -175,7 +184,7 @@ public async Task> ChangeProjectMemberRole( await dbContext.ProjectUsers.Include(r => r.Project).Include(r => r.User).FirstOrDefaultAsync(u => u.ProjectId == input.ProjectId && u.UserId == input.UserId); if (projectUser is null) throw new NotFoundException("Project member not found"); - if (!projectUser.User.HasVerifiedEmailForRole(input.Role)) throw new ProjectMembersMustBeVerified("Member must verify email first"); + projectUser.User.AssertHasVerifiedEmailForRole(input.Role); projectUser.Role = input.Role; projectUser.User.UpdateCreateProjectsPermission(input.Role); projectUser.User.UpdateUpdatedDate(); diff --git a/backend/LexBoxApi/Models/Project/ProjectMemberInputs.cs b/backend/LexBoxApi/Models/Project/ProjectMemberInputs.cs index 1e090e5a5..2bcdd5a19 100644 --- a/backend/LexBoxApi/Models/Project/ProjectMemberInputs.cs +++ b/backend/LexBoxApi/Models/Project/ProjectMemberInputs.cs @@ -3,7 +3,7 @@ namespace LexBoxApi.Models.Project; -public record AddProjectMemberInput(Guid ProjectId, [property: EmailAddress] string UserEmail, ProjectRole Role); +public record AddProjectMemberInput(Guid ProjectId, string UsernameOrEmail, ProjectRole Role); public record BulkAddProjectMembersInput(Guid ProjectId, string[] Usernames, ProjectRole Role, string PasswordHash); diff --git a/backend/LexCore/Exceptions/ProjectMembersMustBeVerifiedForRole.cs b/backend/LexCore/Exceptions/ProjectMembersMustBeVerifiedForRole.cs new file mode 100644 index 000000000..8f8b030ec --- /dev/null +++ b/backend/LexCore/Exceptions/ProjectMembersMustBeVerifiedForRole.cs @@ -0,0 +1,11 @@ +using LexCore.Entities; + +namespace LexCore.Exceptions; + +public class ProjectMembersMustBeVerifiedForRole : Exception +{ + public ProjectMembersMustBeVerifiedForRole(string message, ProjectRole role) : base(message) + { + Data["role"] = role; + } +} diff --git a/backend/LexData/Entities/UserEntityConfiguration.cs b/backend/LexData/Entities/UserEntityConfiguration.cs index 7a75c77b6..08bb25cfd 100644 --- a/backend/LexData/Entities/UserEntityConfiguration.cs +++ b/backend/LexData/Entities/UserEntityConfiguration.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using LexCore.Entities; +using LexCore.Exceptions; using LexData.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -45,12 +46,14 @@ public static IQueryable FilterByEmailOrUsername(this IQueryable use return await users.FilterByEmailOrUsername(emailOrUsername).FirstOrDefaultAsync(); } - public static bool HasVerifiedEmailForRole(this User user, ProjectRole forRole = ProjectRole.Unknown) + public static void AssertHasVerifiedEmailForRole(this User user, ProjectRole forRole = ProjectRole.Unknown) { - // Users bulk-created by admins might not have email addresses, and that's okay - // BUT if they are to be project managers, they must have verified email addresses - if (forRole == ProjectRole.Editor && user.CreatedById is not null) return true; - // Otherwise, we can simply use the EmailVerified property - return user.Email is not null && user.EmailVerified; + // Users with verified emails are the most common case, so check that first + if (user.Email is not null && user.EmailVerified) return; + // Users bulk-created by admins might not have email addresses + // Users who self-registered must verify email in all cases + if (user.CreatedById is null) throw new ProjectMembersMustBeVerified("Member must verify email first"); + // Only project editors (basic role) are allowed not to have verified email addresses + if (forRole != ProjectRole.Editor) throw new ProjectMembersMustBeVerifiedForRole("Member must verify email before taking on this role", forRole); } } diff --git a/deployment/base/hg-deployment.yaml b/deployment/base/hg-deployment.yaml index 8fb1fb0c1..1f3a42d2b 100644 --- a/deployment/base/hg-deployment.yaml +++ b/deployment/base/hg-deployment.yaml @@ -131,32 +131,3 @@ spec: items: - key: hgweb.hgrc path: hgweb.hgrc - - initContainers: - - name: init-repo-structure - securityContext: - runAsUser: 33 - runAsGroup: 33 # www-data - image: busybox:1.36.1 - command: - - 'sh' - - '-c' - - | - cd /repos - mkdir -p a b c d e f g h i j k l m n o p q r s t u v w x y z - mkdir -p 0 1 2 3 4 5 6 7 8 9 - find . -maxdepth 1 -type d -name '[a-z0-9]?*' | while IFS= read -r folder; do - project=$(basename "$folder") - if [[ -L "$project" ]] || [[ "$project" == "lost+found" ]]; then - echo "Skipping folder: $project" - continue - fi - first_letter=$(echo "$project" | head -c1) - - mv "$folder" ./"$first_letter"/ - # need to use relative path for symlink to work in multiple pods - ln -s "$first_letter"/"$project" "$project" - done - volumeMounts: - - name: repos - mountPath: /repos diff --git a/frontend/package.json b/frontend/package.json index 33391580b..a3c6eb4c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -67,7 +67,7 @@ "tslib": "^2.6.2", "type-fest": "^4.10.2", "typescript": "^5.3.3", - "vite": "^5.0.12", + "vite": "^5.0.13", "vite-plugin-graphql-codegen": "^3.3.6", "vitest": "^1.2.2", "viewer": "workspace:*", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5b5c5d11b..53cf12fa3 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: version: 2.4.7 '@vitejs/plugin-basic-ssl': specifier: ^1.1.0 - version: 1.1.0(vite@5.0.12) + version: 1.1.0(vite@5.0.13) css-tree: specifier: ^2.3.1 version: 2.3.1 @@ -110,10 +110,10 @@ importers: version: 4.0.1(@sveltejs/kit@2.5.0) '@sveltejs/kit': specifier: ^2.5.0 - version: 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.12) + version: 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.13) '@sveltejs/vite-plugin-svelte': specifier: ^3.0.2 - version: 3.0.2(svelte@4.2.9)(vite@5.0.12) + version: 3.0.2(svelte@4.2.9)(vite@5.0.13) '@tailwindcss/typography': specifier: ^0.5.10 version: 0.5.10(tailwindcss@3.4.1) @@ -202,11 +202,11 @@ importers: specifier: workspace:* version: link:viewer vite: - specifier: ^5.0.12 - version: 5.0.12(@types/node@20.11.16) + specifier: ^5.0.13 + version: 5.0.13(@types/node@20.11.16) vite-plugin-graphql-codegen: specifier: ^3.3.6 - version: 3.3.6(@graphql-codegen/cli@5.0.0)(graphql@16.8.1)(vite@5.0.12) + version: 3.3.6(@graphql-codegen/cli@5.0.0)(graphql@16.8.1)(vite@5.0.13) vitest: specifier: ^1.2.2 version: 1.2.2(@types/node@20.11.16) @@ -3647,11 +3647,11 @@ packages: '@rollup/plugin-commonjs': 25.0.7(rollup@4.9.6) '@rollup/plugin-json': 6.1.0(rollup@4.9.6) '@rollup/plugin-node-resolve': 15.2.3(rollup@4.9.6) - '@sveltejs/kit': 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.12) + '@sveltejs/kit': 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.13) rollup: 4.9.6 dev: true - /@sveltejs/kit@2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.12): + /@sveltejs/kit@2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.13): resolution: {integrity: sha512-1uyXvzC2Lu1FZa30T4y5jUAC21R309ZMRG0TPt+PPPbNUoDpy8zSmSNVWYaBWxYDqLGQ5oPNWvjvvF2IjJ1jmA==} engines: {node: '>=18.13'} hasBin: true @@ -3661,7 +3661,7 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.3 dependencies: - '@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.9)(vite@5.0.12) + '@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.9)(vite@5.0.13) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 4.3.2 @@ -3675,7 +3675,7 @@ packages: sirv: 2.0.4 svelte: 4.2.9 tiny-glob: 0.2.9 - vite: 5.0.12(@types/node@20.11.16) + vite: 5.0.13(@types/node@20.11.16) /@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.2): resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==} @@ -3693,7 +3693,7 @@ packages: - supports-color dev: true - /@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.12): + /@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.13): resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==} engines: {node: ^18.0.0 || >=20} peerDependencies: @@ -3701,10 +3701,10 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.0 dependencies: - '@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.9)(vite@5.0.12) + '@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.9)(vite@5.0.13) debug: 4.3.4 svelte: 4.2.9 - vite: 5.0.12(@types/node@20.11.16) + vite: 5.0.13(@types/node@20.11.16) transitivePeerDependencies: - supports-color @@ -3728,22 +3728,22 @@ packages: - supports-color dev: true - /@sveltejs/vite-plugin-svelte@3.0.2(svelte@4.2.9)(vite@5.0.12): + /@sveltejs/vite-plugin-svelte@3.0.2(svelte@4.2.9)(vite@5.0.13): resolution: {integrity: sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==} engines: {node: ^18.0.0 || >=20} peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.0 dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.12) + '@sveltejs/vite-plugin-svelte-inspector': 2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.13) debug: 4.3.4 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.6 svelte: 4.2.9 svelte-hmr: 0.15.3(svelte@4.2.9) - vite: 5.0.12(@types/node@20.11.16) - vitefu: 0.2.5(vite@5.0.12) + vite: 5.0.13(@types/node@20.11.16) + vitefu: 0.2.5(vite@5.0.13) transitivePeerDependencies: - supports-color @@ -4253,13 +4253,13 @@ packages: - graphql dev: true - /@vitejs/plugin-basic-ssl@1.1.0(vite@5.0.12): + /@vitejs/plugin-basic-ssl@1.1.0(vite@5.0.13): resolution: {integrity: sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==} engines: {node: '>=14.6.0'} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: - vite: 5.0.12(@types/node@20.11.16) + vite: 5.0.13(@types/node@20.11.16) dev: false /@vitest/expect@1.2.2: @@ -9204,7 +9204,7 @@ packages: '@sveltejs/kit': ^1.0.0 || ^2.0.0 svelte: ^3.55.0 || ^4.0.0 || ^5.0.0 dependencies: - '@sveltejs/kit': 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.12) + '@sveltejs/kit': 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.13) svelte: 4.2.9 dev: false @@ -9215,7 +9215,7 @@ packages: svelte: 3.x || 4.x zod: 3.x dependencies: - '@sveltejs/kit': 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.12) + '@sveltejs/kit': 2.5.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.9)(vite@5.0.13) devalue: 4.3.2 klona: 2.0.6 svelte: 4.2.9 @@ -9608,7 +9608,7 @@ packages: - terser dev: true - /vite-plugin-graphql-codegen@3.3.6(@graphql-codegen/cli@5.0.0)(graphql@16.8.1)(vite@5.0.12): + /vite-plugin-graphql-codegen@3.3.6(@graphql-codegen/cli@5.0.0)(graphql@16.8.1)(vite@5.0.13): resolution: {integrity: sha512-TXMaUpPCfqzSpujjzFjVeeCH9JOSBwFWxOJottZ+gouQtNhnNpgXcj4nZep3om5Wq0UlDwDYLqXWrAa8XaZW1w==} peerDependencies: '@graphql-codegen/cli': ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -9618,11 +9618,11 @@ packages: '@graphql-codegen/cli': 5.0.0(@types/node@20.11.16)(graphql@16.8.1)(typescript@5.3.3) '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) graphql: 16.8.1 - vite: 5.0.12(@types/node@20.11.16) + vite: 5.0.13(@types/node@20.11.16) dev: true - /vite@5.0.12(@types/node@20.11.16): - resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} + /vite@5.0.13(@types/node@20.11.16): + resolution: {integrity: sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -9692,7 +9692,7 @@ packages: fsevents: 2.3.3 dev: true - /vitefu@0.2.5(vite@5.0.12): + /vitefu@0.2.5(vite@5.0.13): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -9700,7 +9700,7 @@ packages: vite: optional: true dependencies: - vite: 5.0.12(@types/node@20.11.16) + vite: 5.0.13(@types/node@20.11.16) /vitefu@0.2.5(vite@5.2.2): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 7c1918125..047b61000 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -207,6 +207,10 @@ type ProjectMembersMustBeVerified implements Error { message: String! } +type ProjectMembersMustBeVerifiedForRole implements Error { + message: String! +} + type ProjectUsers { userId: UUID! projectId: UUID! @@ -288,13 +292,13 @@ type UsersCollectionSegment { totalCount: Int! } -union AddProjectMemberError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMemberInvitedByEmail +union AddProjectMemberError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMembersMustBeVerifiedForRole | ProjectMemberInvitedByEmail | AlreadyExistsError union BulkAddProjectMembersError = NotFoundError | DbError union ChangeProjectDescriptionError = NotFoundError | DbError -union ChangeProjectMemberRoleError = NotFoundError | DbError | ProjectMembersMustBeVerified +union ChangeProjectMemberRoleError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMembersMustBeVerifiedForRole union ChangeProjectNameError = NotFoundError | DbError | RequiredError @@ -314,7 +318,7 @@ union SoftDeleteProjectError = NotFoundError | DbError input AddProjectMemberInput { projectId: UUID! - userEmail: String! + usernameOrEmail: String! role: ProjectRole! } diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index f7e861eb9..25e5d57f0 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -120,7 +120,7 @@ "button_login": "Log in", "button_login_again": "Try to log in again?", "forgot_password": "Forgot your password?", - "label_email": "Email (or Send/Receive login)", + "label_email": "Email or Send/Receive login", "label_password": "Password", "missing_user_info": "User info missing", "sign_in_with_google": "Sign in with Google", @@ -177,12 +177,15 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia "project_page": { "project": "Project", "add_user": { - "add_button": "Add Member", - "modal_title": "Add a Member to this project", + "add_button": "Add/Invite Member", + "modal_title": "Add or invite a Member to this project", "submit_button": "Add Member", + "submit_button_email": "Add or invite Member", "project_not_found": "Project not found. Please refresh the page.", - "user_not_found": "No user was found with this email address", + "username_not_found": "No user was found with this login", "user_must_be_verified": "User needs a verified email address", + "manager_must_be_verified": "User needs a verified email address in order to be a manager", + "user_already_member": "User is already a member of this project", "user_needs_to_relogin": "Added members will need to log out and back in again before they see the new project.", }, "bulk_add_members": { diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index bd706a417..ca5fa868e 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -167,8 +167,8 @@ el [Linguistics Institute at Payap University](https://li.payap.ac.th/) en Chian "email_required": "Correo Electrónico requerido", "modal_title": "Agregar un Miembro a este proyecto", "submit_button": "Agregar Miembro", - "user_not_found": "No se encontró ningún usuario con esta dirección de correo electrónico", - "user_not_verified": "El usuario no ha verificado su dirección de correo electrónico", + "user_must_be_verified": "El usuario no ha verificado su dirección de correo electrónico", + "manager_must_be_verified": "El usuario debe verificar su dirección de correo electrónico antes de convertirse en gerente de proyecto.", "user_needs_to_relogin": "Los miembros agregados deberán cerrar sesión y volver a iniciar sesión antes de ver el nuevo proyecto." }, "change_role_modal": { diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index 3eeecd3ee..52fc8bb03 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -167,8 +167,8 @@ le [Linguistics Institute at Payap University](https://li.payap.ac.th/) à Chian "email_required": "E-mail requis", "modal_title": "Ajouter un membre à ce projet", "submit_button": "Ajouter un membre", - "user_not_found": "Aucun utilisateur n'a été trouvé avec cette adresse e-mail", - "user_not_verified": "L'utilisateur n'a pas vérifié son adresse e-mail", + "user_must_be_verified": "L'utilisateur n'a pas vérifié son adresse e-mail", + "manager_must_be_verified": "L'utilisateur doit vérifier son adresse e-mail avant de devenir un responsable du projet", "user_needs_to_relogin": "Les membres ajoutés devront se déconnecter et se reconnecter pour voir le nouveau projet." }, "change_role_modal": { diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts b/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts index 1d2c05f06..e29550d93 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts +++ b/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts @@ -133,6 +133,9 @@ export async function _addProjectMember(input: AddProjectMemberInput): $OpResult } errors { __typename + ... on Error { + message + } } } } diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte index 8b9aa560b..0491364fc 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte +++ b/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte @@ -10,7 +10,7 @@ export let projectId: string; const schema = z.object({ - email: z.string().email($t('form.invalid_email')), + usernameOrEmail: z.string(), role: z.enum([ProjectRole.Editor, ProjectRole.Manager]).default(ProjectRole.Editor), }); let formModal: FormModal; @@ -23,15 +23,25 @@ const { response, formState } = await formModal.open(async () => { const { error } = await _addProjectMember({ projectId, - userEmail: $form.email, + usernameOrEmail: $form.usernameOrEmail, role: $form.role, }); if (error?.byType('NotFoundError')) { - return { email: [$t('project_page.add_user.project_not_found')] }; + if (error.message === 'Project not found') { + return $t('project_page.add_user.project_not_found'); + } else { + return { usernameOrEmail: [$t('project_page.add_user.username_not_found')] }; + } } if (error?.byType('ProjectMembersMustBeVerified')) { - return { email: [$t('project_page.add_user.user_must_be_verified')] }; + return { usernameOrEmail: [$t('project_page.add_user.user_must_be_verified')] }; + } + if (error?.byType('ProjectMembersMustBeVerifiedForRole')) { + return { role: [$t('project_page.add_user.manager_must_be_verified')] }; + } + if (error?.byType('AlreadyExistsError')) { + return { usernameOrEmail: [$t('project_page.add_user.user_already_member')] }; } if (error?.byType('ProjectMemberInvitedByEmail')) { userInvited = true; @@ -42,7 +52,7 @@ }); if (response === DialogResponse.Submit) { const message = userInvited ? 'member_invited' : 'add_member'; - notifySuccess($t(`project_page.notifications.${message}`, { email: formState.email.currentValue })); + notifySuccess($t(`project_page.notifications.${message}`, { email: formState.usernameOrEmail.currentValue })); } } @@ -54,13 +64,19 @@ {$t('project_page.add_user.modal_title')} - {$t('project_page.add_user.submit_button')} + + {#if $form.usernameOrEmail.includes('@')} + {$t('project_page.add_user.submit_button_email')} + {:else} + {$t('project_page.add_user.submit_button')} + {/if} + diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/ChangeMemberRoleModal.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/ChangeMemberRoleModal.svelte index 662e82079..ed9d53923 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/ChangeMemberRoleModal.svelte +++ b/frontend/src/routes/(authenticated)/project/[project_code]/ChangeMemberRoleModal.svelte @@ -28,6 +28,9 @@ if (result.error?.byType('ProjectMembersMustBeVerified')) { return { role: [$t('project_page.add_user.user_must_be_verified')] }; } + if (result.error?.byType('ProjectMembersMustBeVerifiedForRole')) { + return { role: [$t('project_page.add_user.manager_must_be_verified')] }; + } return result.error?.message; }); }