diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index c24067b..cc8cdec 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -165,13 +165,24 @@ export class Database { return environmentVariables; } - async removeProjectMemberById (memberId: string): Promise { + async removeProjectMemberById (projectMemberId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); - const deleted = await projectMemberRepository.delete(memberId); + const deleteResult = await projectMemberRepository.delete({ id: Number(projectMemberId) }); - if (deleted.affected) { - return deleted.affected > 0; + if (deleteResult.affected) { + return deleteResult.affected > 0; + } else { + return false; + } + } + + async updateProjectMemberById (projectMemberId: string, data: DeepPartial): Promise { + const projectMemberRepository = this.dataSource.getRepository(ProjectMember); + const updateResult = await projectMemberRepository.update({ id: Number(projectMemberId) }, data); + + if (updateResult.affected) { + return updateResult.affected > 0; } else { return false; } @@ -223,7 +234,7 @@ export class Database { } } - async getProjectMemberByMemberId (memberId: string): Promise { + async getProjectMemberById (projectMemberId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberWithProject = await projectMemberRepository.find({ @@ -234,7 +245,7 @@ export class Database { member: true }, where: { - id: Number(memberId) + id: Number(projectMemberId) } } ); diff --git a/packages/backend/src/entity/ProjectMember.ts b/packages/backend/src/entity/ProjectMember.ts index 27e9e20..806acb0 100644 --- a/packages/backend/src/entity/ProjectMember.ts +++ b/packages/backend/src/entity/ProjectMember.ts @@ -11,7 +11,7 @@ import { import { Project } from './Project'; import { User } from './User'; -enum Permissions { +export enum Permission { View = 'View', Edit = 'Edit' } @@ -32,7 +32,7 @@ export class ProjectMember { @Column({ type: 'simple-array' }) - permissions!: Permissions[]; + permissions!: Permission[]; @CreateDateColumn() createdAt!: Date; diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 2c1a252..143975e 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -6,6 +6,7 @@ import { OAuthApp } from '@octokit/oauth-app'; import { Database } from './database'; import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils'; import { Environment } from './entity/Deployment'; +import { Permission } from './entity/ProjectMember'; const log = debug('snowball:database'); @@ -108,9 +109,9 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise }, Mutation: { - removeMember: async (_: any, { memberId }: { memberId: string }, context: any) => { + removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => { try { - const member = await db.getProjectMemberByMemberId(memberId); + const member = await db.getProjectMemberById(projectMemberId); if (member.member.id === context.userId) { throw new Error('Invalid operation: cannot remove self'); @@ -120,7 +121,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise assert(memberProject); if (isUserOwner(String(context.userId), String(memberProject.owner.id))) { - return db.removeProjectMemberById(memberId); + return db.removeProjectMemberById(projectMemberId); } else { throw new Error('Invalid operation: not authorized'); } @@ -130,6 +131,20 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise } }, + updateProjectMember: async (_: any, { projectMemberId, data }: { + projectMemberId: string, + data: { + permissions: Permission[] + } + }) => { + try { + return db.updateProjectMemberById(projectMemberId, data); + } catch (err) { + log(err); + return false; + } + }, + addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => { try { return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables); diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 25381ca..a7d8ac6 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -131,7 +131,8 @@ type AuthResult { } type Mutation { - removeMember(memberId: String!): Boolean! + removeProjectMember(projectMemberId: String!): Boolean! + updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean! addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean! updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean! @@ -170,3 +171,7 @@ input UpdateEnvironmentVariableInput { key: String value: String } + +input UpdateProjectMemberInput { + permissions: [Permission] +} diff --git a/packages/frontend/src/components/projects/project/settings/MemberCard.tsx b/packages/frontend/src/components/projects/project/settings/MemberCard.tsx index fe6a2fe..a763463 100644 --- a/packages/frontend/src/components/projects/project/settings/MemberCard.tsx +++ b/packages/frontend/src/components/projects/project/settings/MemberCard.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useState } from 'react'; +import { Permission } from 'gql-client'; import { Select, @@ -33,8 +34,11 @@ interface MemberCardProps { isOwner: boolean; isPending: boolean; permissions: string[]; - handleDeletePendingMember: (id: string) => void; - removeMemberHandler: () => Promise; + handleDeletePendingMember?: (id: string) => void; + onRemoveProjectMember?: () => Promise; + onUpdateProjectMember?: (data: { + permissions: Permission[]; + }) => Promise; } const MemberCard = ({ @@ -44,7 +48,8 @@ const MemberCard = ({ isPending, permissions, handleDeletePendingMember, - removeMemberHandler, + onRemoveProjectMember, + onUpdateProjectMember, }: MemberCardProps) => { const [selectedPermission, setSelectedPermission] = useState( permissions.join('+'), @@ -52,7 +57,7 @@ const MemberCard = ({ const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false); const handlePermissionChange = useCallback( - (value: string) => { + async (value: string) => { setSelectedPermission(value); if (value === 'remove') { @@ -61,6 +66,11 @@ const MemberCard = ({ setTimeout(() => { setSelectedPermission(selectedPermission); }); + } else { + if (onUpdateProjectMember) { + const permissions = value.split('+') as Permission[]; + await onUpdateProjectMember({ permissions }); + } } }, [removeMemberDialogOpen, selectedPermission], @@ -112,7 +122,9 @@ const MemberCard = ({ size="sm" className="rounded-full" onClick={() => { - handleDeletePendingMember(member.id); + if (handleDeletePendingMember) { + handleDeletePendingMember(member.id); + } }} > D @@ -128,7 +140,9 @@ const MemberCard = ({ confirmButtonTitle="Yes, Remove member" handleConfirm={() => { setRemoveMemberDialogOpen((preVal) => !preVal); - removeMemberHandler(); + if (onRemoveProjectMember) { + onRemoveProjectMember(); + } }} color="red" > diff --git a/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx b/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx index c6d7996..899fc59 100644 --- a/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/settings/MembersTabPanel.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import toast, { Toaster } from 'react-hot-toast'; -import { Project } from 'gql-client'; +import { Permission, Project } from 'gql-client'; import { Chip, Button, Typography } from '@material-tailwind/react'; @@ -30,17 +30,32 @@ const MembersTabPanel = ({ project }: { project: Project }) => { }, [project.id]); const removeMemberHandler = async (projectMemberId: string) => { - const { removeMember: isMemberRemoved } = - await client.removeMember(projectMemberId); + const { removeProjectMember: isMemberRemoved } = + await client.removeProjectMember(projectMemberId); if (isMemberRemoved) { - toast.success('Member removed from project'); await fetchProjectMembers(); + toast.success('Member removed from project'); } else { toast.error('Not able to remove member'); } }; + const updateProjectMemberHandler = useCallback( + async (projectMemberId: string, data: { permissions: Permission[] }) => { + const { updateProjectMember: isProjectMemberUpdated } = + await client.updateProjectMember(projectMemberId, data); + + if (isProjectMemberUpdated) { + await fetchProjectMembers(); + toast.success('Project member permission updated'); + } else { + toast.error('Project member permission not updated'); + } + }, + [], + ); + useEffect(() => { fetchProjectMembers(); }, [project.id, fetchProjectMembers]); @@ -73,8 +88,6 @@ const MembersTabPanel = ({ project }: { project: Project }) => { isOwner={true} isPending={false} permissions={[]} - handleDeletePendingMember={() => {}} - removeMemberHandler={async () => {}} /> {projectMembers.map((projectMember, index) => { return ( @@ -92,7 +105,12 @@ const MembersTabPanel = ({ project }: { project: Project }) => { ), ); }} - removeMemberHandler={() => removeMemberHandler(projectMember.id)} + onRemoveProjectMember={async () => + await removeMemberHandler(projectMember.id) + } + onUpdateProjectMember={async (data) => { + await updateProjectMemberHandler(projectMember.id, data); + }} /> ); })} diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index b66c105..4c12c41 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -1,8 +1,8 @@ import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries'; -import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGithubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse } from './types'; -import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGithub, updateEnvironmentVariable, removeEnvironmentVariable } 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, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse } from './types'; +import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGithub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember } from './mutations'; export interface GraphQLConfig { gqlEndpoint: string; @@ -91,26 +91,38 @@ export class GQLClient { return data; } - async removeMember (memberId: string): Promise { - const { data } = await this.client.mutate({ - mutation: removeMember, - variables: { - memberId - } - }); - - return data; - } - async getProjectMembers (projectId: string) : Promise { - const { data } = await this.client.query({ + const result = await this.client.query({ query: getProjectMembers, variables: { projectId } }); - return data; + return result.data; + } + + async updateProjectMember (projectMemberId: string, data: UpdateProjectMemberInput): Promise { + const result = await this.client.mutate({ + mutation: updateProjectMember, + variables: { + projectMemberId, + data + } + }); + + return result.data; + } + + async removeProjectMember (projectMemberId: string): Promise { + const result = await this.client.mutate({ + mutation: removeProjectMember, + variables: { + projectMemberId + } + }); + + return result.data; } async searchProjects (searchText: string) : Promise { diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index 5551e52..3840380 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -1,8 +1,14 @@ import { gql } from '@apollo/client'; -export const removeMember = gql` -mutation ($memberId: String!) { - removeMember(memberId: $memberId) +export const removeProjectMember = gql` +mutation ($projectMemberId: String!) { + removeProjectMember(projectMemberId: $projectMemberId) +} +`; + +export const updateProjectMember = gql` +mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) { + updateProjectMember(projectMemberId: $projectMemberId, data: $data) } `; diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index e8cb30d..eeb8b40 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -134,8 +134,12 @@ export type GetProjectMembersResponse = { projectMembers: ProjectMember[] } -export type RemoveMemberResponse = { - removeMember: boolean; +export type RemoveProjectMemberResponse = { + removeProjectMember: boolean; +} + +export type UpdateProjectMemberResponse = { + updateProjectMember: boolean; } export type GetDeploymentsResponse = { @@ -185,6 +189,10 @@ export type UpdateEnvironmentVariableInput = { value: string; } +export type UpdateProjectMemberInput = { + permissions: Permission[]; +} + export type UpdateEnvironmentVariableResponse = { updateEnvironmentVariable: boolean; }