From 0aa35d05f43276024e8253d15a7f60a0da76091b Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Thu, 1 Feb 2024 15:34:17 +0530 Subject: [PATCH] Implement functionality to add project members (#48) * Add mutation for adding project member * Add GQL client method for add project member * Handle review comments * Integrate add project member GQL client method * Handle cascades for project members and user organizations * Add null type to user.name and domain.redirectTo --------- Co-authored-by: neeraj --- packages/backend/src/database.ts | 51 ++++++++++++++++--- packages/backend/src/entity/Domain.ts | 2 +- packages/backend/src/entity/Organization.ts | 9 +++- packages/backend/src/entity/Project.ts | 8 +-- packages/backend/src/entity/ProjectMember.ts | 15 ++++-- packages/backend/src/entity/User.ts | 22 ++++++-- .../backend/src/entity/UserOrganization.ts | 10 ++-- packages/backend/src/resolvers.ts | 45 ++++++++++------ packages/backend/src/schema.gql | 10 +++- packages/backend/src/utils.ts | 1 + .../test/fixtures/project-members.json | 49 ++++++++++++++---- packages/backend/test/fixtures/users.json | 9 ++-- .../project/settings/AddMemberDialog.tsx | 27 ++++------ .../projects/project/settings/MemberCard.tsx | 13 ++--- .../project/settings/MembersTabPanel.tsx | 38 ++++++++------ packages/frontend/src/types/project.ts | 17 ------- packages/gql-client/src/client.ts | 16 +++++- packages/gql-client/src/mutations.ts | 6 +++ packages/gql-client/src/queries.ts | 2 + packages/gql-client/src/types.ts | 15 +++++- 20 files changed, 252 insertions(+), 113 deletions(-) diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index d9fec249..74028f8e 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -9,7 +9,7 @@ import { Organization } from './entity/Organization'; import { UserOrganization } from './entity/UserOrganization'; import { Project } from './entity/Project'; import { Deployment, Environment } from './entity/Deployment'; -import { ProjectMember } from './entity/ProjectMember'; +import { Permission, ProjectMember } from './entity/ProjectMember'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { Domain } from './entity/Domain'; @@ -198,6 +198,38 @@ export class Database { } } + async addProjectMember (projectId: string, data: { + email: string, + permissions: Permission[] + }): Promise { + const projectMemberRepository = this.dataSource.getRepository(ProjectMember); + const userRepository = this.dataSource.getRepository(User); + + let user = await userRepository.findOneBy({ + email: data.email + }); + + if (!user) { + user = await userRepository.save({ + email: data.email, + isVerified: false + }); + } + + const newProjectMember = await projectMemberRepository.save({ + project: { + id: projectId + }, + permissions: data.permissions, + isPending: true, + member: { + id: user.id + } + }); + + return Boolean(newProjectMember); + } + async addEnvironmentVariablesByProjectId (projectId: string, environmentVariables: { environments: string[]; key: string; @@ -338,13 +370,18 @@ export class Database { async deleteProjectById (projectId: string): Promise { const projectRepository = this.dataSource.getRepository(Project); - const deleteResult = await projectRepository.softDelete({ id: projectId }); + const project = await projectRepository.findOneOrFail({ + where: { + id: projectId + }, + relations: { + projectMembers: true + } + }); - if (deleteResult.affected) { - return deleteResult.affected > 0; - } else { - return false; - } + const deleteResult = await projectRepository.softRemove(project); + + return Boolean(deleteResult); } async deleteDomainById (domainId: string): Promise { diff --git a/packages/backend/src/entity/Domain.ts b/packages/backend/src/entity/Domain.ts index d59dd782..b1f44515 100644 --- a/packages/backend/src/entity/Domain.ts +++ b/packages/backend/src/entity/Domain.ts @@ -35,7 +35,7 @@ export class Domain { name!: string; @Column('int', { nullable: true }) - redirectToId!: number; + redirectToId!: number | null; @ManyToOne(() => Domain) @JoinColumn({ name: 'redirectToId' }) diff --git a/packages/backend/src/entity/Organization.ts b/packages/backend/src/entity/Organization.ts index 71f55bbc..7aa92d21 100644 --- a/packages/backend/src/entity/Organization.ts +++ b/packages/backend/src/entity/Organization.ts @@ -3,8 +3,10 @@ import { PrimaryGeneratedColumn, Column, CreateDateColumn, - UpdateDateColumn + UpdateDateColumn, + OneToMany } from 'typeorm'; +import { UserOrganization } from './UserOrganization'; @Entity() export class Organization { @@ -19,4 +21,9 @@ export class Organization { @UpdateDateColumn() updatedAt!: Date; + + @OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, { + cascade: ['soft-remove'] + }) + userOrganizations!: UserOrganization[]; } diff --git a/packages/backend/src/entity/Project.ts b/packages/backend/src/entity/Project.ts index 8a9736d8..5736cd0f 100644 --- a/packages/backend/src/entity/Project.ts +++ b/packages/backend/src/entity/Project.ts @@ -63,9 +63,11 @@ export class Project { @DeleteDateColumn() deletedAt?: Date; - @OneToMany(() => ProjectMember, projectMember => projectMember.project) - projectMembers!: ProjectMember[]; - @OneToMany(() => Deployment, (deployment) => deployment.project) deployments!: Deployment[]; + + @OneToMany(() => ProjectMember, projectMember => projectMember.project, { + cascade: ['soft-remove'] + }) + projectMembers!: ProjectMember[]; } diff --git a/packages/backend/src/entity/ProjectMember.ts b/packages/backend/src/entity/ProjectMember.ts index 806acb0a..b93eed70 100644 --- a/packages/backend/src/entity/ProjectMember.ts +++ b/packages/backend/src/entity/ProjectMember.ts @@ -5,7 +5,9 @@ import { UpdateDateColumn, ManyToOne, PrimaryGeneratedColumn, - JoinColumn + JoinColumn, + Unique, + DeleteDateColumn } from 'typeorm'; import { Project } from './Project'; @@ -17,15 +19,16 @@ export enum Permission { } @Entity() +@Unique(['project', 'member']) export class ProjectMember { @PrimaryGeneratedColumn() id!: number; - @ManyToOne(() => User, (user) => user.projectMembers, { onDelete: 'CASCADE' }) + @ManyToOne(() => User, (user) => user.projectMembers) @JoinColumn({ name: 'userId' }) member!: User; - @ManyToOne(() => Project, (project) => project.projectMembers, { onDelete: 'CASCADE' }) + @ManyToOne(() => Project, (project) => project.projectMembers) @JoinColumn({ name: 'projectId' }) project!: Project; @@ -34,9 +37,15 @@ export class ProjectMember { }) permissions!: Permission[]; + @Column('boolean', { default: false }) + isPending!: boolean; + @CreateDateColumn() createdAt!: Date; @UpdateDateColumn() updatedAt!: Date; + + @DeleteDateColumn() + deletedAt?: Date; } diff --git a/packages/backend/src/entity/User.ts b/packages/backend/src/entity/User.ts index 35fb5f72..14bae32c 100644 --- a/packages/backend/src/entity/User.ts +++ b/packages/backend/src/entity/User.ts @@ -3,17 +3,21 @@ import { PrimaryGeneratedColumn, Column, CreateDateColumn, - OneToMany + OneToMany, + Unique } from 'typeorm'; + import { ProjectMember } from './ProjectMember'; +import { UserOrganization } from './UserOrganization'; @Entity() +@Unique(['email']) export class User { @PrimaryGeneratedColumn() id!: number; - @Column('varchar', { length: 255 }) - name!: string; + @Column('varchar', { length: 255, nullable: true }) + name!: string | null; @Column() email!: string; @@ -21,12 +25,22 @@ export class User { @Column('varchar', { nullable: true }) gitHubToken!: string | null; + @Column('boolean', { default: false }) + isVerified!: boolean; + @CreateDateColumn() createdAt!: Date; @CreateDateColumn() updatedAt!: Date; - @OneToMany(() => ProjectMember, projectMember => projectMember.project) + @OneToMany(() => ProjectMember, projectMember => projectMember.project, { + cascade: ['soft-remove'] + }) projectMembers!: ProjectMember[]; + + @OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, { + cascade: ['soft-remove'] + }) + userOrganizations!: UserOrganization[]; } diff --git a/packages/backend/src/entity/UserOrganization.ts b/packages/backend/src/entity/UserOrganization.ts index 3e7c7dc8..ceeac4fb 100644 --- a/packages/backend/src/entity/UserOrganization.ts +++ b/packages/backend/src/entity/UserOrganization.ts @@ -5,7 +5,8 @@ import { UpdateDateColumn, ManyToOne, PrimaryGeneratedColumn, - JoinColumn + JoinColumn, + DeleteDateColumn } from 'typeorm'; import { User } from './User'; @@ -22,11 +23,11 @@ export class UserOrganization { @PrimaryGeneratedColumn() id!: number; - @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @ManyToOne(() => User) @JoinColumn({ name: 'userId' }) member!: User; - @ManyToOne(() => Organization, { onDelete: 'CASCADE' }) + @ManyToOne(() => Organization) @JoinColumn({ name: 'organizationId' }) organization!: Organization; @@ -40,4 +41,7 @@ export class UserOrganization { @UpdateDateColumn() updatedAt!: Date; + + @DeleteDateColumn() + deletedAt?: Date; } diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index f4437361..ae17833c 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -111,6 +111,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise } }, + // TODO: Return error in GQL response Mutation: { removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => { try { @@ -124,7 +125,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise assert(memberProject); if (isUserOwner(String(context.userId), String(memberProject.owner.id))) { - return db.removeProjectMemberById(projectMemberId); + return await db.removeProjectMemberById(projectMemberId); } else { throw new Error('Invalid operation: not authorized'); } @@ -141,7 +142,23 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise } }) => { try { - return db.updateProjectMemberById(projectMemberId, data); + return await db.updateProjectMemberById(projectMemberId, data); + } catch (err) { + log(err); + return false; + } + }, + + addProjectMember: async (_: any, { projectId, data }: { + projectId: string, + data: { + email: string, + permissions: Permission[] + } + }) => { + try { + // TODO: Send invitation + return await db.addProjectMember(projectId, data); } catch (err) { log(err); return false; @@ -150,7 +167,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => { try { - return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables); + return await db.addEnvironmentVariablesByProjectId(projectId, environmentVariables); } catch (err) { log(err); return false; @@ -162,7 +179,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise value: string }}) => { try { - return db.updateEnvironmentVariable(environmentVariableId, environmentVariable); + return await db.updateEnvironmentVariable(environmentVariableId, environmentVariable); } catch (err) { log(err); return false; @@ -171,7 +188,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => { try { - return db.deleteEnvironmentVariable(environmentVariableId); + return await db.deleteEnvironmentVariable(environmentVariableId); } catch (err) { log(err); return false; @@ -180,7 +197,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => { try { - return db.updateDeploymentById(deploymentId, { + return await db.updateDeploymentById(deploymentId, { environment: Environment.Production }); } catch (err) { @@ -200,8 +217,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { try { - await db.redeployToProdById(context.userId, deploymentId); - return true; + return await db.redeployToProdById(context.userId, deploymentId); } catch (err) { log(err); return false; @@ -210,7 +226,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise deleteProject: async (_: any, { projectId }: { projectId: string }) => { try { - return db.deleteProjectById(projectId); + return await db.deleteProjectById(projectId); } catch (err) { log(err); return false; @@ -219,8 +235,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise deleteDomain: async (_: any, { domainId }: { domainId: string }) => { try { - await db.deleteDomainById(domainId); - return true; + return await db.deleteDomainById(domainId); } catch (err) { log(err); return false; @@ -229,7 +244,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => { try { - return db.rollbackDeploymentById(projectId, deploymentId); + return await db.rollbackDeploymentById(projectId, deploymentId); } catch (err) { log(err); return false; @@ -238,8 +253,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise addDomain: async (_: any, { projectId, domainDetails }: { projectId: string, domainDetails: { name: string } }) => { try { - await db.addDomainByProjectId(projectId, domainDetails); - return true; + return await db.addDomainByProjectId(projectId, domainDetails); } catch (err) { log(err); return false; @@ -248,8 +262,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial}) => { try { - await db.updateDomainById(domainId, domainDetails); - return true; + return await db.updateDomainById(domainId, domainDetails); } catch (err) { log(err); return false; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 4458f80a..589874a4 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -28,10 +28,11 @@ enum DomainStatus { type User { id: String! - name: String! + name: String email: String! organizations: [Organization!] projects: [Project!] + isVerified: Boolean! createdAt: String! updatedAt: String! gitHubToken: String @@ -77,6 +78,7 @@ type ProjectMember { id: String! member: User! permissions: [Permission!]! + isPending: Boolean! createdAt: String! updatedAt: String! } @@ -134,6 +136,7 @@ type AuthResult { type Mutation { removeProjectMember(projectMemberId: String!): Boolean! updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean! + addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean! addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean! updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean! @@ -177,6 +180,11 @@ input UpdateEnvironmentVariableInput { value: String } +input AddProjectMemberInput { + email: String! + permissions: [Permission!] +} + input UpdateProjectMemberInput { permissions: [Permission] } diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 676cbfbd..d262cc82 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -65,6 +65,7 @@ export const projectMemberToGqlType = (dbProjectMember: ProjectMember): any => { return { id: dbProjectMember.id, member: dbProjectMember.member, + isPending: dbProjectMember.isPending, permissions: dbProjectMember.permissions, createdAt: dbProjectMember.createdAt, updatedAt: dbProjectMember.updatedAt diff --git a/packages/backend/test/fixtures/project-members.json b/packages/backend/test/fixtures/project-members.json index 0a488052..7b2661ea 100644 --- a/packages/backend/test/fixtures/project-members.json +++ b/packages/backend/test/fixtures/project-members.json @@ -2,46 +2,77 @@ { "memberIndex": 1, "projectIndex": 0, - "permissions": ["View"] + "permissions": [ + "View" + ], + "isPending": false }, { "memberIndex": 2, "projectIndex": 0, - "permissions": ["View", "Edit"] + "permissions": [ + "View", + "Edit" + ], + "isPending": false }, { "memberIndex": 2, "projectIndex": 1, - "permissions": ["View"] + "permissions": [ + "View" + ], + "isPending": false }, { "memberIndex": 0, "projectIndex": 2, - "permissions": ["View"] + "permissions": [ + "View" + ], + "isPending": false }, { "memberIndex": 1, "projectIndex": 2, - "permissions": ["View", "Edit"] + "permissions": [ + "View", + "Edit" + ], + "isPending": false }, { "memberIndex": 0, "projectIndex": 3, - "permissions": ["View"] + "permissions": [ + "View" + ], + "isPending": false }, { "memberIndex": 2, "projectIndex": 3, - "permissions": ["View", "Edit"] + "permissions": [ + "View", + "Edit" + ], + "isPending": false }, { "memberIndex": 1, "projectIndex": 4, - "permissions": ["View"] + "permissions": [ + "View" + ], + "isPending": false }, { "memberIndex": 2, "projectIndex": 4, - "permissions": ["View", "Edit"] + "permissions": [ + "View", + "Edit" + ], + "isPending": false } ] diff --git a/packages/backend/test/fixtures/users.json b/packages/backend/test/fixtures/users.json index d2e4fb44..5a8a5317 100644 --- a/packages/backend/test/fixtures/users.json +++ b/packages/backend/test/fixtures/users.json @@ -1,14 +1,17 @@ [ { "name": "Saugat Yadav", - "email": "saugaty@airfoil.studio" + "email": "saugaty@airfoil.studio", + "isVerified": true }, { "name": "Gideon Low", - "email": "gideonl@airfoil.studio" + "email": "gideonl@airfoil.studio", + "isVerified": true }, { "name": "Sushan Yadav", - "email": "sushany@airfoil.studio" + "email": "sushany@airfoil.studio", + "isVerified": true } ] diff --git a/packages/frontend/src/components/projects/project/settings/AddMemberDialog.tsx b/packages/frontend/src/components/projects/project/settings/AddMemberDialog.tsx index df40e025..f631e4ce 100644 --- a/packages/frontend/src/components/projects/project/settings/AddMemberDialog.tsx +++ b/packages/frontend/src/components/projects/project/settings/AddMemberDialog.tsx @@ -1,5 +1,6 @@ import React, { useCallback } from 'react'; import { useForm } from 'react-hook-form'; +import { AddProjectMemberInput, Permission } from 'gql-client'; import { Button, @@ -12,12 +13,10 @@ import { Checkbox, } from '@material-tailwind/react'; -import { ProjectMember, Permission } from '../../../../types/project'; - interface AddMemberDialogProp { open: boolean; handleOpen: () => void; - handleAddMember: (projectMember: ProjectMember) => void; + handleAddMember: (data: AddProjectMemberInput) => Promise; } interface formData { @@ -48,21 +47,17 @@ const AddMemberDialog = ({ }, }); - const submitHandler = useCallback((data: formData) => { + const submitHandler = useCallback(async (data: formData) => { reset(); handleOpen(); - const projectMember: ProjectMember = { - id: Math.random().toString(), - permissions: [], - member: { - name: '', - email: data.emailAddress, - id: Math.random().toString(), - }, - }; + const permissions = Object.entries(data.permissions) + .filter(([, value]) => value) + .map( + ([key]) => key.charAt(0).toUpperCase() + key.slice(1), + ) as Permission[]; - handleAddMember(projectMember); + await handleAddMember({ email: data.emailAddress, permissions }); }, []); return ( @@ -96,13 +91,13 @@ const AddMemberDialog = ({ diff --git a/packages/frontend/src/components/projects/project/settings/MemberCard.tsx b/packages/frontend/src/components/projects/project/settings/MemberCard.tsx index a7634635..45b5c72c 100644 --- a/packages/frontend/src/components/projects/project/settings/MemberCard.tsx +++ b/packages/frontend/src/components/projects/project/settings/MemberCard.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { Permission } from 'gql-client'; +import { Permission, User } from 'gql-client'; import { Select, @@ -10,7 +10,6 @@ import { } from '@material-tailwind/react'; import ConfirmDialog from '../../../shared/ConfirmDialog'; -import { Member } from '../../../../types/project'; const PERMISSION_OPTIONS = [ { @@ -29,12 +28,11 @@ const DROPDOWN_OPTIONS = [ ]; interface MemberCardProps { - member: Member; + member: User; isFirstCard: boolean; isOwner: boolean; isPending: boolean; permissions: string[]; - handleDeletePendingMember?: (id: string) => void; onRemoveProjectMember?: () => Promise; onUpdateProjectMember?: (data: { permissions: Permission[]; @@ -47,7 +45,6 @@ const MemberCard = ({ isOwner, isPending, permissions, - handleDeletePendingMember, onRemoveProjectMember, onUpdateProjectMember, }: MemberCardProps) => { @@ -82,7 +79,7 @@ const MemberCard = ({ >
^
- {member.name} + {member.name && {member.name}} {member.email}
@@ -122,9 +119,7 @@ const MemberCard = ({ size="sm" className="rounded-full" onClick={() => { - if (handleDeletePendingMember) { - handleDeletePendingMember(member.id); - } + setRemoveMemberDialogOpen((prevVal) => !prevVal); }} > D diff --git a/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx b/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx index 899fc597..9ddf9e7a 100644 --- a/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx @@ -1,11 +1,15 @@ import React, { useCallback, useEffect, useState } from 'react'; import toast, { Toaster } from 'react-hot-toast'; -import { Permission, Project } from 'gql-client'; +import { + Permission, + Project, + AddProjectMemberInput, + ProjectMember, +} from 'gql-client'; import { Chip, Button, Typography } from '@material-tailwind/react'; import MemberCard from './MemberCard'; -import { ProjectMember } from '../../../../types/project'; import AddMemberDialog from './AddMemberDialog'; import { useGQLClient } from '../../../../context/GQLClientContext'; @@ -18,17 +22,26 @@ const MembersTabPanel = ({ project }: { project: Project }) => { const [projectMembers, setProjectMembers] = useState([]); - const addMemberHandler = useCallback((projectMember: ProjectMember) => { - setProjectMembers((val) => [...val, projectMember]); - toast.success('Invitation sent'); - }, []); - const fetchProjectMembers = useCallback(async () => { const { projectMembers } = await client.getProjectMembers(project.id); - setProjectMembers(projectMembers); }, [project.id]); + const addMemberHandler = useCallback( + async (data: AddProjectMemberInput) => { + const { addProjectMember: isProjectMemberAdded } = + await client.addProjectMember(project.id, data); + + if (isProjectMemberAdded) { + await fetchProjectMembers(); + toast.success('Invitation sent'); + } else { + toast.error('Invitation not sent'); + } + }, + [project], + ); + const removeMemberHandler = async (projectMemberId: string) => { const { removeProjectMember: isMemberRemoved } = await client.removeProjectMember(projectMemberId); @@ -96,15 +109,8 @@ const MembersTabPanel = ({ project }: { project: Project }) => { key={projectMember.id} isFirstCard={index === FIRST_MEMBER_CARD} isOwner={projectMember.member.id === project.owner.id} - isPending={projectMember.member.name === ''} + isPending={projectMember.isPending} permissions={projectMember.permissions} - handleDeletePendingMember={(id: string) => { - setProjectMembers( - projectMembers.filter( - (projectMember) => projectMember.member.id !== id, - ), - ); - }} onRemoveProjectMember={async () => await removeMemberHandler(projectMember.id) } diff --git a/packages/frontend/src/types/project.ts b/packages/frontend/src/types/project.ts index 8409854d..7232eb5b 100644 --- a/packages/frontend/src/types/project.ts +++ b/packages/frontend/src/types/project.ts @@ -8,12 +8,6 @@ export interface ProjectDetails extends Project { repositoryId?: number; } -export interface ProjectMember { - id: string; - member: Member; - permissions: string[]; -} - export interface DeploymentDetails extends Deployment { commit: Commit; author: string; @@ -72,17 +66,6 @@ export interface DomainDetails { }; } -export enum Permission { - VIEW = 'view', - EDIT = 'edit', -} - -export interface Member { - name: string; - email: string; - id: string; -} - export interface ProjectSearchOutletContext { projects: ProjectDetails[]; } diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 55e4a6f2..34250bd0 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -1,8 +1,8 @@ import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries'; -import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGitHubResponse, UnauthenticateGitHubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse, DeleteDomainResponse } from './types'; -import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain } from './mutations'; +import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGitHubResponse, UnauthenticateGitHubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse, DeleteDomainResponse, AddProjectMemberInput, AddProjectMemberResponse } from './types'; +import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain, addProjectMember } from './mutations'; export interface GraphQLConfig { gqlEndpoint: string; @@ -102,6 +102,18 @@ export class GQLClient { return result.data; } + async addProjectMember (projectId: string, data: AddProjectMemberInput) : Promise { + const result = await this.client.mutate({ + mutation: addProjectMember, + variables: { + projectId, + data + } + }); + + return result.data; + } + async updateProjectMember (projectMemberId: string, data: UpdateProjectMemberInput): Promise { const result = await this.client.mutate({ mutation: updateProjectMember, diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index 17949959..f3ff6afe 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -12,6 +12,12 @@ mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) { } `; +export const addProjectMember = gql` +mutation ($projectId: String!, $data: AddProjectMemberInput) { + addProjectMember(projectId: $projectId, data: $data) +} +`; + export const addEnvironmentVariables = gql` mutation ($projectId: String!, $environmentVariables: [AddEnvironmentVariableInput!]) { addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables) diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index f002ebae..9d846f75 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -178,7 +178,9 @@ query ($projectId: String!) { id name email + isVerified } + isPending createdAt updatedAt permissions diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 22ebf441..019cb52f 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -42,15 +42,16 @@ export type Domain = { branch: string name: string status: DomainStatus - redirectTo?: Domain + redirectTo: Domain | null createdAt: string updatedAt: string } export type User = { id: string - name: string + name: string | null email: string + isVerified: boolean createdAt: string updatedAt: string gitHubToken: string | null @@ -82,6 +83,7 @@ export type ProjectMember = { id: string member: User permissions: Permission[] + isPending: boolean createdAt: string updatedAt: string } @@ -135,6 +137,10 @@ export type GetProjectMembersResponse = { projectMembers: ProjectMember[] } +export type AddProjectMemberResponse = { + addProjectMember: boolean +} + export type RemoveProjectMemberResponse = { removeProjectMember: boolean; } @@ -194,6 +200,11 @@ export type UpdateProjectMemberInput = { permissions: Permission[]; } +export type AddProjectMemberInput = { + email: string; + permissions: Permission[] +} + export type UpdateEnvironmentVariableResponse = { updateEnvironmentVariable: boolean; }