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:
Nabarun Gogoi 2024-02-01 15:34:17 +05:30 committed by GitHub
parent 7e2a313012
commit 0aa35d05f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 252 additions and 113 deletions

View File

@ -9,7 +9,7 @@ import { Organization } from './entity/Organization';
import { UserOrganization } from './entity/UserOrganization'; import { UserOrganization } from './entity/UserOrganization';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
import { Deployment, Environment } from './entity/Deployment'; import { Deployment, Environment } from './entity/Deployment';
import { ProjectMember } from './entity/ProjectMember'; import { Permission, ProjectMember } from './entity/ProjectMember';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain'; 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: { async addEnvironmentVariablesByProjectId (projectId: string, environmentVariables: {
environments: string[]; environments: string[];
key: string; key: string;
@ -338,13 +370,18 @@ export class Database {
async deleteProjectById (projectId: string): Promise<boolean> { async deleteProjectById (projectId: string): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const deleteResult = await projectRepository.softDelete({ id: projectId }); const project = await projectRepository.findOneOrFail({
where: {
if (deleteResult.affected) { id: projectId
return deleteResult.affected > 0; },
} else { relations: {
return false; projectMembers: true
} }
});
const deleteResult = await projectRepository.softRemove(project);
return Boolean(deleteResult);
} }
async deleteDomainById (domainId: string): Promise<boolean> { async deleteDomainById (domainId: string): Promise<boolean> {

View File

@ -35,7 +35,7 @@ export class Domain {
name!: string; name!: string;
@Column('int', { nullable: true }) @Column('int', { nullable: true })
redirectToId!: number; redirectToId!: number | null;
@ManyToOne(() => Domain) @ManyToOne(() => Domain)
@JoinColumn({ name: 'redirectToId' }) @JoinColumn({ name: 'redirectToId' })

View File

@ -3,8 +3,10 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Column, Column,
CreateDateColumn, CreateDateColumn,
UpdateDateColumn UpdateDateColumn,
OneToMany
} from 'typeorm'; } from 'typeorm';
import { UserOrganization } from './UserOrganization';
@Entity() @Entity()
export class Organization { export class Organization {
@ -19,4 +21,9 @@ export class Organization {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
cascade: ['soft-remove']
})
userOrganizations!: UserOrganization[];
} }

View File

@ -63,9 +63,11 @@ export class Project {
@DeleteDateColumn() @DeleteDateColumn()
deletedAt?: Date; deletedAt?: Date;
@OneToMany(() => ProjectMember, projectMember => projectMember.project)
projectMembers!: ProjectMember[];
@OneToMany(() => Deployment, (deployment) => deployment.project) @OneToMany(() => Deployment, (deployment) => deployment.project)
deployments!: Deployment[]; deployments!: Deployment[];
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
cascade: ['soft-remove']
})
projectMembers!: ProjectMember[];
} }

View File

@ -5,7 +5,9 @@ import {
UpdateDateColumn, UpdateDateColumn,
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
JoinColumn JoinColumn,
Unique,
DeleteDateColumn
} from 'typeorm'; } from 'typeorm';
import { Project } from './Project'; import { Project } from './Project';
@ -17,15 +19,16 @@ export enum Permission {
} }
@Entity() @Entity()
@Unique(['project', 'member'])
export class ProjectMember { export class ProjectMember {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number; id!: number;
@ManyToOne(() => User, (user) => user.projectMembers, { onDelete: 'CASCADE' }) @ManyToOne(() => User, (user) => user.projectMembers)
@JoinColumn({ name: 'userId' }) @JoinColumn({ name: 'userId' })
member!: User; member!: User;
@ManyToOne(() => Project, (project) => project.projectMembers, { onDelete: 'CASCADE' }) @ManyToOne(() => Project, (project) => project.projectMembers)
@JoinColumn({ name: 'projectId' }) @JoinColumn({ name: 'projectId' })
project!: Project; project!: Project;
@ -34,9 +37,15 @@ export class ProjectMember {
}) })
permissions!: Permission[]; permissions!: Permission[];
@Column('boolean', { default: false })
isPending!: boolean;
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
} }

View File

@ -3,17 +3,21 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Column, Column,
CreateDateColumn, CreateDateColumn,
OneToMany OneToMany,
Unique
} from 'typeorm'; } from 'typeorm';
import { ProjectMember } from './ProjectMember'; import { ProjectMember } from './ProjectMember';
import { UserOrganization } from './UserOrganization';
@Entity() @Entity()
@Unique(['email'])
export class User { export class User {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number; id!: number;
@Column('varchar', { length: 255 }) @Column('varchar', { length: 255, nullable: true })
name!: string; name!: string | null;
@Column() @Column()
email!: string; email!: string;
@ -21,12 +25,22 @@ export class User {
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
gitHubToken!: string | null; gitHubToken!: string | null;
@Column('boolean', { default: false })
isVerified!: boolean;
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;
@CreateDateColumn() @CreateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => ProjectMember, projectMember => projectMember.project) @OneToMany(() => ProjectMember, projectMember => projectMember.project, {
cascade: ['soft-remove']
})
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
cascade: ['soft-remove']
})
userOrganizations!: UserOrganization[];
} }

View File

@ -5,7 +5,8 @@ import {
UpdateDateColumn, UpdateDateColumn,
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
JoinColumn JoinColumn,
DeleteDateColumn
} from 'typeorm'; } from 'typeorm';
import { User } from './User'; import { User } from './User';
@ -22,11 +23,11 @@ export class UserOrganization {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number; id!: number;
@ManyToOne(() => User, { onDelete: 'CASCADE' }) @ManyToOne(() => User)
@JoinColumn({ name: 'userId' }) @JoinColumn({ name: 'userId' })
member!: User; member!: User;
@ManyToOne(() => Organization, { onDelete: 'CASCADE' }) @ManyToOne(() => Organization)
@JoinColumn({ name: 'organizationId' }) @JoinColumn({ name: 'organizationId' })
organization!: Organization; organization!: Organization;
@ -40,4 +41,7 @@ export class UserOrganization {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
} }

View File

@ -111,6 +111,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
} }
}, },
// TODO: Return error in GQL response
Mutation: { Mutation: {
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => { removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
try { try {
@ -124,7 +125,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
assert(memberProject); assert(memberProject);
if (isUserOwner(String(context.userId), String(memberProject.owner.id))) { if (isUserOwner(String(context.userId), String(memberProject.owner.id))) {
return db.removeProjectMemberById(projectMemberId); return await db.removeProjectMemberById(projectMemberId);
} else { } else {
throw new Error('Invalid operation: not authorized'); throw new Error('Invalid operation: not authorized');
} }
@ -141,7 +142,23 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
} }
}) => { }) => {
try { 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) { } catch (err) {
log(err); log(err);
return false; 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}[] }) => { addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => {
try { try {
return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables); return await db.addEnvironmentVariablesByProjectId(projectId, environmentVariables);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -162,7 +179,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
value: string value: string
}}) => { }}) => {
try { try {
return db.updateEnvironmentVariable(environmentVariableId, environmentVariable); return await db.updateEnvironmentVariable(environmentVariableId, environmentVariable);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -171,7 +188,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => { removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
try { try {
return db.deleteEnvironmentVariable(environmentVariableId); return await db.deleteEnvironmentVariable(environmentVariableId);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -180,7 +197,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => { updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => {
try { try {
return db.updateDeploymentById(deploymentId, { return await db.updateDeploymentById(deploymentId, {
environment: Environment.Production environment: Environment.Production
}); });
} catch (err) { } catch (err) {
@ -200,8 +217,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
try { try {
await db.redeployToProdById(context.userId, deploymentId); return await db.redeployToProdById(context.userId, deploymentId);
return true;
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -210,7 +226,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
deleteProject: async (_: any, { projectId }: { projectId: string }) => { deleteProject: async (_: any, { projectId }: { projectId: string }) => {
try { try {
return db.deleteProjectById(projectId); return await db.deleteProjectById(projectId);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -219,8 +235,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
deleteDomain: async (_: any, { domainId }: { domainId: string }) => { deleteDomain: async (_: any, { domainId }: { domainId: string }) => {
try { try {
await db.deleteDomainById(domainId); return await db.deleteDomainById(domainId);
return true;
} catch (err) { } catch (err) {
log(err); log(err);
return false; 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 }) => { rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
try { try {
return db.rollbackDeploymentById(projectId, deploymentId); return await db.rollbackDeploymentById(projectId, deploymentId);
} catch (err) { } catch (err) {
log(err); log(err);
return false; 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 } }) => { addDomain: async (_: any, { projectId, domainDetails }: { projectId: string, domainDetails: { name: string } }) => {
try { try {
await db.addDomainByProjectId(projectId, domainDetails); return await db.addDomainByProjectId(projectId, domainDetails);
return true;
} catch (err) { } catch (err) {
log(err); log(err);
return false; 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>}) => { updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial<Domain>}) => {
try { try {
await db.updateDomainById(domainId, domainDetails); return await db.updateDomainById(domainId, domainDetails);
return true;
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;

View File

@ -28,10 +28,11 @@ enum DomainStatus {
type User { type User {
id: String! id: String!
name: String! name: String
email: String! email: String!
organizations: [Organization!] organizations: [Organization!]
projects: [Project!] projects: [Project!]
isVerified: Boolean!
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
gitHubToken: String gitHubToken: String
@ -77,6 +78,7 @@ type ProjectMember {
id: String! id: String!
member: User! member: User!
permissions: [Permission!]! permissions: [Permission!]!
isPending: Boolean!
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
} }
@ -134,6 +136,7 @@ type AuthResult {
type Mutation { type Mutation {
removeProjectMember(projectMemberId: String!): Boolean! removeProjectMember(projectMemberId: String!): Boolean!
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean! updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean! addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean! updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean!
@ -177,6 +180,11 @@ input UpdateEnvironmentVariableInput {
value: String value: String
} }
input AddProjectMemberInput {
email: String!
permissions: [Permission!]
}
input UpdateProjectMemberInput { input UpdateProjectMemberInput {
permissions: [Permission] permissions: [Permission]
} }

View File

@ -65,6 +65,7 @@ export const projectMemberToGqlType = (dbProjectMember: ProjectMember): any => {
return { return {
id: dbProjectMember.id, id: dbProjectMember.id,
member: dbProjectMember.member, member: dbProjectMember.member,
isPending: dbProjectMember.isPending,
permissions: dbProjectMember.permissions, permissions: dbProjectMember.permissions,
createdAt: dbProjectMember.createdAt, createdAt: dbProjectMember.createdAt,
updatedAt: dbProjectMember.updatedAt updatedAt: dbProjectMember.updatedAt

View File

@ -2,46 +2,77 @@
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 0, "projectIndex": 0,
"permissions": ["View"] "permissions": [
"View"
],
"isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 0, "projectIndex": 0,
"permissions": ["View", "Edit"] "permissions": [
"View",
"Edit"
],
"isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 1, "projectIndex": 1,
"permissions": ["View"] "permissions": [
"View"
],
"isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 2, "projectIndex": 2,
"permissions": ["View"] "permissions": [
"View"
],
"isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 2, "projectIndex": 2,
"permissions": ["View", "Edit"] "permissions": [
"View",
"Edit"
],
"isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 3, "projectIndex": 3,
"permissions": ["View"] "permissions": [
"View"
],
"isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 3, "projectIndex": 3,
"permissions": ["View", "Edit"] "permissions": [
"View",
"Edit"
],
"isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 4, "projectIndex": 4,
"permissions": ["View"] "permissions": [
"View"
],
"isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 4, "projectIndex": 4,
"permissions": ["View", "Edit"] "permissions": [
"View",
"Edit"
],
"isPending": false
} }
] ]

View File

@ -1,14 +1,17 @@
[ [
{ {
"name": "Saugat Yadav", "name": "Saugat Yadav",
"email": "saugaty@airfoil.studio" "email": "saugaty@airfoil.studio",
"isVerified": true
}, },
{ {
"name": "Gideon Low", "name": "Gideon Low",
"email": "gideonl@airfoil.studio" "email": "gideonl@airfoil.studio",
"isVerified": true
}, },
{ {
"name": "Sushan Yadav", "name": "Sushan Yadav",
"email": "sushany@airfoil.studio" "email": "sushany@airfoil.studio",
"isVerified": true
} }
] ]

View File

@ -1,5 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { AddProjectMemberInput, Permission } from 'gql-client';
import { import {
Button, Button,
@ -12,12 +13,10 @@ import {
Checkbox, Checkbox,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import { ProjectMember, Permission } from '../../../../types/project';
interface AddMemberDialogProp { interface AddMemberDialogProp {
open: boolean; open: boolean;
handleOpen: () => void; handleOpen: () => void;
handleAddMember: (projectMember: ProjectMember) => void; handleAddMember: (data: AddProjectMemberInput) => Promise<void>;
} }
interface formData { interface formData {
@ -48,21 +47,17 @@ const AddMemberDialog = ({
}, },
}); });
const submitHandler = useCallback((data: formData) => { const submitHandler = useCallback(async (data: formData) => {
reset(); reset();
handleOpen(); handleOpen();
const projectMember: ProjectMember = { const permissions = Object.entries(data.permissions)
id: Math.random().toString(), .filter(([, value]) => value)
permissions: [], .map(
member: { ([key]) => key.charAt(0).toUpperCase() + key.slice(1),
name: '', ) as Permission[];
email: data.emailAddress,
id: Math.random().toString(),
},
};
handleAddMember(projectMember); await handleAddMember({ email: data.emailAddress, permissions });
}, []); }, []);
return ( return (
@ -96,13 +91,13 @@ const AddMemberDialog = ({
</Typography> </Typography>
<Checkbox <Checkbox
crossOrigin={undefined} crossOrigin={undefined}
label={Permission.VIEW} label={Permission.View}
{...register(`permissions.view`)} {...register(`permissions.view`)}
color="blue" color="blue"
/> />
<Checkbox <Checkbox
crossOrigin={undefined} crossOrigin={undefined}
label={Permission.EDIT} label={Permission.Edit}
{...register(`permissions.edit`)} {...register(`permissions.edit`)}
color="blue" color="blue"
/> />

View File

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Permission } from 'gql-client'; import { Permission, User } from 'gql-client';
import { import {
Select, Select,
@ -10,7 +10,6 @@ import {
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
import { Member } from '../../../../types/project';
const PERMISSION_OPTIONS = [ const PERMISSION_OPTIONS = [
{ {
@ -29,12 +28,11 @@ const DROPDOWN_OPTIONS = [
]; ];
interface MemberCardProps { interface MemberCardProps {
member: Member; member: User;
isFirstCard: boolean; isFirstCard: boolean;
isOwner: boolean; isOwner: boolean;
isPending: boolean; isPending: boolean;
permissions: string[]; permissions: string[];
handleDeletePendingMember?: (id: string) => void;
onRemoveProjectMember?: () => Promise<void>; onRemoveProjectMember?: () => Promise<void>;
onUpdateProjectMember?: (data: { onUpdateProjectMember?: (data: {
permissions: Permission[]; permissions: Permission[];
@ -47,7 +45,6 @@ const MemberCard = ({
isOwner, isOwner,
isPending, isPending,
permissions, permissions,
handleDeletePendingMember,
onRemoveProjectMember, onRemoveProjectMember,
onUpdateProjectMember, onUpdateProjectMember,
}: MemberCardProps) => { }: MemberCardProps) => {
@ -82,7 +79,7 @@ const MemberCard = ({
> >
<div>^</div> <div>^</div>
<div className="basis-1/2"> <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> <Typography variant="small">{member.email}</Typography>
</div> </div>
<div className="basis-1/2"> <div className="basis-1/2">
@ -122,9 +119,7 @@ const MemberCard = ({
size="sm" size="sm"
className="rounded-full" className="rounded-full"
onClick={() => { onClick={() => {
if (handleDeletePendingMember) { setRemoveMemberDialogOpen((prevVal) => !prevVal);
handleDeletePendingMember(member.id);
}
}} }}
> >
D D

View File

@ -1,11 +1,15 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import toast, { Toaster } from 'react-hot-toast'; 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 { Chip, Button, Typography } from '@material-tailwind/react';
import MemberCard from './MemberCard'; import MemberCard from './MemberCard';
import { ProjectMember } from '../../../../types/project';
import AddMemberDialog from './AddMemberDialog'; import AddMemberDialog from './AddMemberDialog';
import { useGQLClient } from '../../../../context/GQLClientContext'; import { useGQLClient } from '../../../../context/GQLClientContext';
@ -18,17 +22,26 @@ const MembersTabPanel = ({ project }: { project: Project }) => {
const [projectMembers, setProjectMembers] = useState<ProjectMember[]>([]); const [projectMembers, setProjectMembers] = useState<ProjectMember[]>([]);
const addMemberHandler = useCallback((projectMember: ProjectMember) => {
setProjectMembers((val) => [...val, projectMember]);
toast.success('Invitation sent');
}, []);
const fetchProjectMembers = useCallback(async () => { const fetchProjectMembers = useCallback(async () => {
const { projectMembers } = await client.getProjectMembers(project.id); const { projectMembers } = await client.getProjectMembers(project.id);
setProjectMembers(projectMembers); setProjectMembers(projectMembers);
}, [project.id]); }, [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 removeMemberHandler = async (projectMemberId: string) => {
const { removeProjectMember: isMemberRemoved } = const { removeProjectMember: isMemberRemoved } =
await client.removeProjectMember(projectMemberId); await client.removeProjectMember(projectMemberId);
@ -96,15 +109,8 @@ const MembersTabPanel = ({ project }: { project: Project }) => {
key={projectMember.id} key={projectMember.id}
isFirstCard={index === FIRST_MEMBER_CARD} isFirstCard={index === FIRST_MEMBER_CARD}
isOwner={projectMember.member.id === project.owner.id} isOwner={projectMember.member.id === project.owner.id}
isPending={projectMember.member.name === ''} isPending={projectMember.isPending}
permissions={projectMember.permissions} permissions={projectMember.permissions}
handleDeletePendingMember={(id: string) => {
setProjectMembers(
projectMembers.filter(
(projectMember) => projectMember.member.id !== id,
),
);
}}
onRemoveProjectMember={async () => onRemoveProjectMember={async () =>
await removeMemberHandler(projectMember.id) await removeMemberHandler(projectMember.id)
} }

View File

@ -8,12 +8,6 @@ export interface ProjectDetails extends Project {
repositoryId?: number; repositoryId?: number;
} }
export interface ProjectMember {
id: string;
member: Member;
permissions: string[];
}
export interface DeploymentDetails extends Deployment { export interface DeploymentDetails extends Deployment {
commit: Commit; commit: Commit;
author: string; 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 { export interface ProjectSearchOutletContext {
projects: ProjectDetails[]; projects: ProjectDetails[];
} }

View File

@ -1,8 +1,8 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries'; 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 { 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 } from './mutations'; import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain, addProjectMember } from './mutations';
export interface GraphQLConfig { export interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@ -102,6 +102,18 @@ export class GQLClient {
return result.data; 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> { async updateProjectMember (projectMemberId: string, data: UpdateProjectMemberInput): Promise<UpdateProjectMemberResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: updateProjectMember, mutation: updateProjectMember,

View File

@ -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` export const addEnvironmentVariables = gql`
mutation ($projectId: String!, $environmentVariables: [AddEnvironmentVariableInput!]) { mutation ($projectId: String!, $environmentVariables: [AddEnvironmentVariableInput!]) {
addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables) addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables)

View File

@ -178,7 +178,9 @@ query ($projectId: String!) {
id id
name name
email email
isVerified
} }
isPending
createdAt createdAt
updatedAt updatedAt
permissions permissions

View File

@ -42,15 +42,16 @@ export type Domain = {
branch: string branch: string
name: string name: string
status: DomainStatus status: DomainStatus
redirectTo?: Domain redirectTo: Domain | null
createdAt: string createdAt: string
updatedAt: string updatedAt: string
} }
export type User = { export type User = {
id: string id: string
name: string name: string | null
email: string email: string
isVerified: boolean
createdAt: string createdAt: string
updatedAt: string updatedAt: string
gitHubToken: string | null gitHubToken: string | null
@ -82,6 +83,7 @@ export type ProjectMember = {
id: string id: string
member: User member: User
permissions: Permission[] permissions: Permission[]
isPending: boolean
createdAt: string createdAt: string
updatedAt: string updatedAt: string
} }
@ -135,6 +137,10 @@ export type GetProjectMembersResponse = {
projectMembers: ProjectMember[] projectMembers: ProjectMember[]
} }
export type AddProjectMemberResponse = {
addProjectMember: boolean
}
export type RemoveProjectMemberResponse = { export type RemoveProjectMemberResponse = {
removeProjectMember: boolean; removeProjectMember: boolean;
} }
@ -194,6 +200,11 @@ export type UpdateProjectMemberInput = {
permissions: Permission[]; permissions: Permission[];
} }
export type AddProjectMemberInput = {
email: string;
permissions: Permission[]
}
export type UpdateEnvironmentVariableResponse = { export type UpdateEnvironmentVariableResponse = {
updateEnvironmentVariable: boolean; updateEnvironmentVariable: boolean;
} }