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 { 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> {

View File

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

View File

@ -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[];
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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]
}

View File

@ -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

View File

@ -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
}
]

View File

@ -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
}
]

View File

@ -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"
/>

View File

@ -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

View File

@ -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)
}

View File

@ -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[];
}

View File

@ -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,

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

View File

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

View File

@ -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;
}