forked from cerc-io/snowballtools-base
		
	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 <neeraj.rtly@gmail.com>
This commit is contained in:
		
							parent
							
								
									7e2a313012
								
							
						
					
					
						commit
						0aa35d05f4
					
				| @ -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<boolean> { | ||||
|     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<boolean> { | ||||
|     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<boolean> { | ||||
|  | ||||
| @ -35,7 +35,7 @@ export class Domain { | ||||
|     name!: string; | ||||
| 
 | ||||
|   @Column('int', { nullable: true }) | ||||
|     redirectToId!: number; | ||||
|     redirectToId!: number | null; | ||||
| 
 | ||||
|   @ManyToOne(() => Domain) | ||||
|   @JoinColumn({ name: 'redirectToId' }) | ||||
|  | ||||
| @ -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[]; | ||||
| } | ||||
|  | ||||
| @ -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[]; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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[]; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -111,6 +111,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any> | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     // 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<any> | ||||
|           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<any> | ||||
|         } | ||||
|       }) => { | ||||
|         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<any> | ||||
| 
 | ||||
|       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<any> | ||||
|         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<any> | ||||
| 
 | ||||
|       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<any> | ||||
| 
 | ||||
|       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<any> | ||||
| 
 | ||||
|       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<any> | ||||
| 
 | ||||
|       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<any> | ||||
| 
 | ||||
|       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<any> | ||||
| 
 | ||||
|       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<any> | ||||
| 
 | ||||
|       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<any> | ||||
| 
 | ||||
|       updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial<Domain>}) => { | ||||
|         try { | ||||
|           await db.updateDomainById(domainId, domainDetails); | ||||
|           return true; | ||||
|           return await db.updateDomainById(domainId, domainDetails); | ||||
|         } catch (err) { | ||||
|           log(err); | ||||
|           return false; | ||||
|  | ||||
| @ -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] | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|   } | ||||
| ] | ||||
|  | ||||
							
								
								
									
										9
									
								
								packages/backend/test/fixtures/users.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								packages/backend/test/fixtures/users.json
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||
|   } | ||||
| ] | ||||
|  | ||||
| @ -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<void>; | ||||
| } | ||||
| 
 | ||||
| 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 = ({ | ||||
|           </Typography> | ||||
|           <Checkbox | ||||
|             crossOrigin={undefined} | ||||
|             label={Permission.VIEW} | ||||
|             label={Permission.View} | ||||
|             {...register(`permissions.view`)} | ||||
|             color="blue" | ||||
|           /> | ||||
|           <Checkbox | ||||
|             crossOrigin={undefined} | ||||
|             label={Permission.EDIT} | ||||
|             label={Permission.Edit} | ||||
|             {...register(`permissions.edit`)} | ||||
|             color="blue" | ||||
|           /> | ||||
|  | ||||
| @ -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<void>; | ||||
|   onUpdateProjectMember?: (data: { | ||||
|     permissions: Permission[]; | ||||
| @ -47,7 +45,6 @@ const MemberCard = ({ | ||||
|   isOwner, | ||||
|   isPending, | ||||
|   permissions, | ||||
|   handleDeletePendingMember, | ||||
|   onRemoveProjectMember, | ||||
|   onUpdateProjectMember, | ||||
| }: MemberCardProps) => { | ||||
| @ -82,7 +79,7 @@ const MemberCard = ({ | ||||
|     > | ||||
|       <div>^</div> | ||||
|       <div className="basis-1/2"> | ||||
|         <Typography variant="small">{member.name}</Typography> | ||||
|         {member.name && <Typography variant="small">{member.name}</Typography>} | ||||
|         <Typography variant="small">{member.email}</Typography> | ||||
|       </div> | ||||
|       <div className="basis-1/2"> | ||||
| @ -122,9 +119,7 @@ const MemberCard = ({ | ||||
|                 size="sm" | ||||
|                 className="rounded-full" | ||||
|                 onClick={() => { | ||||
|                   if (handleDeletePendingMember) { | ||||
|                     handleDeletePendingMember(member.id); | ||||
|                   } | ||||
|                   setRemoveMemberDialogOpen((prevVal) => !prevVal); | ||||
|                 }} | ||||
|               > | ||||
|                 D | ||||
|  | ||||
| @ -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<ProjectMember[]>([]); | ||||
| 
 | ||||
|   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) | ||||
|             } | ||||
|  | ||||
| @ -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[]; | ||||
| } | ||||
|  | ||||
| @ -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<AddProjectMemberResponse> { | ||||
|     const result = await this.client.mutate({ | ||||
|       mutation: addProjectMember, | ||||
|       variables: { | ||||
|         projectId, | ||||
|         data | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     return result.data; | ||||
|   } | ||||
| 
 | ||||
|   async updateProjectMember (projectMemberId: string, data: UpdateProjectMemberInput): Promise<UpdateProjectMemberResponse> { | ||||
|     const result = await this.client.mutate({ | ||||
|       mutation: updateProjectMember, | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -178,7 +178,9 @@ query ($projectId: String!) { | ||||
|       id | ||||
|       name | ||||
|       email | ||||
|       isVerified | ||||
|     } | ||||
|     isPending | ||||
|     createdAt | ||||
|     updatedAt | ||||
|     permissions | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user