diff --git a/web/satellite/src/api/projectMembers.ts b/web/satellite/src/api/projectMembers.ts index 35e4759398cf..f4f0ea47b985 100644 --- a/web/satellite/src/api/projectMembers.ts +++ b/web/satellite/src/api/projectMembers.ts @@ -3,31 +3,11 @@ import { BaseGql } from '@/api/baseGql'; import { ProjectMember, ProjectMemberCursor, ProjectMembersApi, ProjectMembersPage } from '@/types/projectMembers'; +import { HttpClient } from '@/utils/httpClient'; export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi { - - /** - * Used for adding team members to project. - * - * @param projectId - * @param emails - */ - public async add(projectId: string, emails: string[]): Promise { - const query = - `mutation($projectId: String!, $emails:[String!]!) { - addProjectMembers( - publicId: $projectId, - email: $emails - ) {publicId} - }`; - - const variables = { - projectId, - emails, - }; - - await this.mutate(query, variables); - } + private readonly http: HttpClient = new HttpClient(); + private readonly ROOT_PATH: string = '/api/v0/projects'; /** * Used for deleting team members from project. @@ -106,6 +86,22 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi { return this.getProjectMembersList(response.data.project.members); } + /** + * Handles inviting users to a project. + * + * @throws Error + */ + public async invite(projectID: string, emails: string[]): Promise { + const path = `${this.ROOT_PATH}/${projectID}/invite`; + const body = { emails }; + const httpResponse = await this.http.post(path, JSON.stringify(body)); + + if (httpResponse.ok) return; + + const result = await httpResponse.json(); + throw new Error(result.error || 'Failed to send project invitations'); + } + /** * Method for mapping project members page from json to ProjectMembersPage type. * diff --git a/web/satellite/src/components/common/VInput.vue b/web/satellite/src/components/common/VInput.vue index 3b8c31501ffd..4a0c17a42a54 100644 --- a/web/satellite/src/components/common/VInput.vue +++ b/web/satellite/src/components/common/VInput.vue @@ -66,9 +66,9 @@ - @@ -245,12 +231,14 @@ export default defineComponent({ width: 100%; display: flex; justify-content: space-between; + align-items: center; font-size: 16px; line-height: 21px; color: #354049; & .add-label { - font-size: 'font_medium' sans-serif; + font-size: 12px; + line-height: 18px; color: var(--c-grey-5) !important; } } diff --git a/web/satellite/src/components/modals/AddTeamMemberModal.vue b/web/satellite/src/components/modals/AddTeamMemberModal.vue index 51f61ed5f47b..ed0be7977530 100644 --- a/web/satellite/src/components/modals/AddTeamMemberModal.vue +++ b/web/satellite/src/components/modals/AddTeamMemberModal.vue @@ -24,6 +24,7 @@ placeholder="email@email.com" role-description="email" :error="formError" + :max-symbols="72" @setData="(str) => setInput(index, str)" /> @@ -192,7 +193,7 @@ async function onAddUsersClick(): Promise { } try { - await pmStore.addProjectMembers(emailArray, projectsStore.state.selectedProject.id); + await pmStore.inviteMembers(emailArray, projectsStore.state.selectedProject.id); } catch (_) { await notify.error(`Error during adding project members.`, AnalyticsErrorEventSource.ADD_PROJECT_MEMBER_MODAL); isLoading.value = false; diff --git a/web/satellite/src/components/modals/JoinProjectModal.vue b/web/satellite/src/components/modals/JoinProjectModal.vue index 2cf60c6c37b4..d6f07f4ce877 100644 --- a/web/satellite/src/components/modals/JoinProjectModal.vue +++ b/web/satellite/src/components/modals/JoinProjectModal.vue @@ -109,6 +109,7 @@ async function respondToInvitation(response: ProjectInvitationResponse): Promise projectsStore.selectProject(invite.value.projectID); LocalData.setSelectedProjectId(invite.value.projectID); + notify.success('Invite accepted!'); analytics.pageVisit(RouteConfig.ProjectDashboard.path); router.push(RouteConfig.ProjectDashboard.path); } diff --git a/web/satellite/src/components/team/HeaderArea.vue b/web/satellite/src/components/team/HeaderArea.vue index 5d36d0ccbb59..b79125958116 100644 --- a/web/satellite/src/components/team/HeaderArea.vue +++ b/web/satellite/src/components/team/HeaderArea.vue @@ -148,7 +148,11 @@ async function processSearchQuery(search: string): Promise { pmStore.setSearchQuery(search); } try { - await pmStore.getProjectMembers(FIRST_PAGE, projectsStore.state.selectedProject.id); + const id = projectsStore.state.selectedProject.id; + if (!id) { + return; + } + await pmStore.getProjectMembers(FIRST_PAGE, id); } catch (error) { notify.error(`Unable to fetch project members. ${error.message}`, AnalyticsErrorEventSource.PROJECT_MEMBERS_HEADER); } diff --git a/web/satellite/src/store/modules/projectMembersStore.ts b/web/satellite/src/store/modules/projectMembersStore.ts index eb43c77a37cb..7ccca4c98ed6 100644 --- a/web/satellite/src/store/modules/projectMembersStore.ts +++ b/web/satellite/src/store/modules/projectMembersStore.ts @@ -26,8 +26,8 @@ export const useProjectMembersStore = defineStore('projectMembers', () => { const api: ProjectMembersApi = new ProjectMembersApiGql(); - async function addProjectMembers(emails: string[], projectID: string): Promise { - await api.add(projectID, emails); + async function inviteMembers(emails: string[], projectID: string): Promise { + await api.invite(projectID, emails); } async function deleteProjectMembers(projectID: string): Promise { @@ -109,7 +109,7 @@ export const useProjectMembersStore = defineStore('projectMembers', () => { return { state, - addProjectMembers, + inviteMembers, deleteProjectMembers, getProjectMembers, setSearchQuery, diff --git a/web/satellite/src/types/projectMembers.ts b/web/satellite/src/types/projectMembers.ts index 614a2a65ec03..b72d054e3eee 100644 --- a/web/satellite/src/types/projectMembers.ts +++ b/web/satellite/src/types/projectMembers.ts @@ -33,15 +33,16 @@ export enum ProjectMemberHeaderState { * Exposes all ProjectMembers-related functionality */ export interface ProjectMembersApi { + /** - * Add members to project by user emails. + * Invite members to project by user emails. * * @param projectId * @param emails list of project members email to add * * @throws Error */ - add(projectId: string, emails: string[]): Promise; + invite(projectId: string, emails: string[]): Promise; /** * Deletes ProjectMembers from project by project member emails diff --git a/web/satellite/src/views/LoginArea.vue b/web/satellite/src/views/LoginArea.vue index 4835a9bdb0cf..8201b532b385 100644 --- a/web/satellite/src/views/LoginArea.vue +++ b/web/satellite/src/views/LoginArea.vue @@ -65,6 +65,8 @@ (null); + const returnURL = ref(RouteConfig.ProjectDashboard.path); const hcaptcha = ref(null); -const mfaInput = ref(null); +const mfaInput = ref(null); const forgotPasswordPath: string = RouteConfig.ForgotPassword.path; const registerPath: string = RouteConfig.Register.path; @@ -238,6 +242,11 @@ const captchaConfig = computed((): MultiCaptchaConfig => { * Makes activated banner visible on successful account activation. */ onMounted(() => { + pathEmail.value = route.query.email as string ?? null; + if (pathEmail.value) { + setEmail(pathEmail.value); + } + isActivatedBannerShown.value = !!route.query.activated; isActivatedError.value = route.query.activated === 'false'; @@ -320,6 +329,10 @@ function clickSatellite(address): void { * Toggles satellite selection dropdown visibility (Tardigrade). */ function toggleDropdown(): void { + if (pathEmail.value) { + // this page was opened from an email link, so don't allow satellite selection. + return; + } isDropdownShown.value = !isDropdownShown.value; } diff --git a/web/satellite/tests/unit/store/projectMembers.spec.ts b/web/satellite/tests/unit/store/projectMembers.spec.ts index 7e93cfcce6f2..bd903de30cb7 100644 --- a/web/satellite/tests/unit/store/projectMembers.spec.ts +++ b/web/satellite/tests/unit/store/projectMembers.spec.ts @@ -128,40 +128,6 @@ describe('actions', () => { expect(store.state.selectedProjectMembersEmails.length).toBe(0); }); - it('add project members', async function () { - const store = useProjectMembersStore(); - - vi.spyOn(ProjectMembersApiGql.prototype, 'add').mockReturnValue(Promise.resolve()); - - try { - await store.addProjectMembers([projectMember1.user.email], selectedProject.id); - throw TEST_ERROR; - } catch (err) { - expect(err).toBe(TEST_ERROR); - } - }); - - it('add project member throws error when api call fails', async function () { - const store = useProjectMembersStore(); - - vi.spyOn(ProjectMembersApiGql.prototype, 'add').mockImplementation(() => { - throw TEST_ERROR; - }); - - const stateDump = store.state; - - try { - await store.addProjectMembers([projectMember1.user.email], selectedProject.id); - } catch (err) { - expect(err).toBe(TEST_ERROR); - expect(store.state).toBe(stateDump); - - return; - } - - fail(UNREACHABLE_ERROR); - }); - it('delete project members', async function () { const store = useProjectMembersStore();