Implement functionality to update project member permissions in settings (#56)
* Add mutation to update permissions of project member * Use update project member permission client method in UI * Handle review changes --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
parent
b5e7554c26
commit
ace27c7eae
@ -165,13 +165,24 @@ export class Database {
|
|||||||
return environmentVariables;
|
return environmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMemberById (memberId: string): Promise<boolean> {
|
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const deleted = await projectMemberRepository.delete(memberId);
|
const deleteResult = await projectMemberRepository.delete({ id: Number(projectMemberId) });
|
||||||
|
|
||||||
if (deleted.affected) {
|
if (deleteResult.affected) {
|
||||||
return deleted.affected > 0;
|
return deleteResult.affected > 0;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
|
||||||
|
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||||
|
const updateResult = await projectMemberRepository.update({ id: Number(projectMemberId) }, data);
|
||||||
|
|
||||||
|
if (updateResult.affected) {
|
||||||
|
return updateResult.affected > 0;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -223,7 +234,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMemberByMemberId (memberId: string): Promise<ProjectMember> {
|
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const projectMemberWithProject = await projectMemberRepository.find({
|
const projectMemberWithProject = await projectMemberRepository.find({
|
||||||
@ -234,7 +245,7 @@ export class Database {
|
|||||||
member: true
|
member: true
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
id: Number(memberId)
|
id: Number(projectMemberId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
enum Permissions {
|
export enum Permission {
|
||||||
View = 'View',
|
View = 'View',
|
||||||
Edit = 'Edit'
|
Edit = 'Edit'
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ export class ProjectMember {
|
|||||||
@Column({
|
@Column({
|
||||||
type: 'simple-array'
|
type: 'simple-array'
|
||||||
})
|
})
|
||||||
permissions!: Permissions[];
|
permissions!: Permission[];
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
@ -6,6 +6,7 @@ import { OAuthApp } from '@octokit/oauth-app';
|
|||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils';
|
import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils';
|
||||||
import { Environment } from './entity/Deployment';
|
import { Environment } from './entity/Deployment';
|
||||||
|
import { Permission } from './entity/ProjectMember';
|
||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
@ -108,9 +109,9 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
|||||||
},
|
},
|
||||||
|
|
||||||
Mutation: {
|
Mutation: {
|
||||||
removeMember: async (_: any, { memberId }: { memberId: string }, context: any) => {
|
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
||||||
try {
|
try {
|
||||||
const member = await db.getProjectMemberByMemberId(memberId);
|
const member = await db.getProjectMemberById(projectMemberId);
|
||||||
|
|
||||||
if (member.member.id === context.userId) {
|
if (member.member.id === context.userId) {
|
||||||
throw new Error('Invalid operation: cannot remove self');
|
throw new Error('Invalid operation: cannot remove self');
|
||||||
@ -120,7 +121,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(memberId);
|
return db.removeProjectMemberById(projectMemberId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid operation: not authorized');
|
throw new Error('Invalid operation: not authorized');
|
||||||
}
|
}
|
||||||
@ -130,6 +131,20 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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}[] }) => {
|
addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => {
|
||||||
try {
|
try {
|
||||||
return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables);
|
return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables);
|
||||||
|
@ -131,7 +131,8 @@ type AuthResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
removeMember(memberId: String!): Boolean!
|
removeProjectMember(projectMemberId: String!): Boolean!
|
||||||
|
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): 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!
|
||||||
@ -170,3 +171,7 @@ input UpdateEnvironmentVariableInput {
|
|||||||
key: String
|
key: String
|
||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input UpdateProjectMemberInput {
|
||||||
|
permissions: [Permission]
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { Permission } from 'gql-client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@ -33,8 +34,11 @@ interface MemberCardProps {
|
|||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
handleDeletePendingMember: (id: string) => void;
|
handleDeletePendingMember?: (id: string) => void;
|
||||||
removeMemberHandler: () => Promise<void>;
|
onRemoveProjectMember?: () => Promise<void>;
|
||||||
|
onUpdateProjectMember?: (data: {
|
||||||
|
permissions: Permission[];
|
||||||
|
}) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemberCard = ({
|
const MemberCard = ({
|
||||||
@ -44,7 +48,8 @@ const MemberCard = ({
|
|||||||
isPending,
|
isPending,
|
||||||
permissions,
|
permissions,
|
||||||
handleDeletePendingMember,
|
handleDeletePendingMember,
|
||||||
removeMemberHandler,
|
onRemoveProjectMember,
|
||||||
|
onUpdateProjectMember,
|
||||||
}: MemberCardProps) => {
|
}: MemberCardProps) => {
|
||||||
const [selectedPermission, setSelectedPermission] = useState(
|
const [selectedPermission, setSelectedPermission] = useState(
|
||||||
permissions.join('+'),
|
permissions.join('+'),
|
||||||
@ -52,7 +57,7 @@ const MemberCard = ({
|
|||||||
const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false);
|
const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false);
|
||||||
|
|
||||||
const handlePermissionChange = useCallback(
|
const handlePermissionChange = useCallback(
|
||||||
(value: string) => {
|
async (value: string) => {
|
||||||
setSelectedPermission(value);
|
setSelectedPermission(value);
|
||||||
|
|
||||||
if (value === 'remove') {
|
if (value === 'remove') {
|
||||||
@ -61,6 +66,11 @@ const MemberCard = ({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSelectedPermission(selectedPermission);
|
setSelectedPermission(selectedPermission);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (onUpdateProjectMember) {
|
||||||
|
const permissions = value.split('+') as Permission[];
|
||||||
|
await onUpdateProjectMember({ permissions });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[removeMemberDialogOpen, selectedPermission],
|
[removeMemberDialogOpen, selectedPermission],
|
||||||
@ -112,7 +122,9 @@ const MemberCard = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeletePendingMember(member.id);
|
if (handleDeletePendingMember) {
|
||||||
|
handleDeletePendingMember(member.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
D
|
D
|
||||||
@ -128,7 +140,9 @@ const MemberCard = ({
|
|||||||
confirmButtonTitle="Yes, Remove member"
|
confirmButtonTitle="Yes, Remove member"
|
||||||
handleConfirm={() => {
|
handleConfirm={() => {
|
||||||
setRemoveMemberDialogOpen((preVal) => !preVal);
|
setRemoveMemberDialogOpen((preVal) => !preVal);
|
||||||
removeMemberHandler();
|
if (onRemoveProjectMember) {
|
||||||
|
onRemoveProjectMember();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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 { Project } from 'gql-client';
|
import { Permission, Project } from 'gql-client';
|
||||||
|
|
||||||
import { Chip, Button, Typography } from '@material-tailwind/react';
|
import { Chip, Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
@ -30,17 +30,32 @@ const MembersTabPanel = ({ project }: { project: Project }) => {
|
|||||||
}, [project.id]);
|
}, [project.id]);
|
||||||
|
|
||||||
const removeMemberHandler = async (projectMemberId: string) => {
|
const removeMemberHandler = async (projectMemberId: string) => {
|
||||||
const { removeMember: isMemberRemoved } =
|
const { removeProjectMember: isMemberRemoved } =
|
||||||
await client.removeMember(projectMemberId);
|
await client.removeProjectMember(projectMemberId);
|
||||||
|
|
||||||
if (isMemberRemoved) {
|
if (isMemberRemoved) {
|
||||||
toast.success('Member removed from project');
|
|
||||||
await fetchProjectMembers();
|
await fetchProjectMembers();
|
||||||
|
toast.success('Member removed from project');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Not able to remove member');
|
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(() => {
|
useEffect(() => {
|
||||||
fetchProjectMembers();
|
fetchProjectMembers();
|
||||||
}, [project.id, fetchProjectMembers]);
|
}, [project.id, fetchProjectMembers]);
|
||||||
@ -73,8 +88,6 @@ const MembersTabPanel = ({ project }: { project: Project }) => {
|
|||||||
isOwner={true}
|
isOwner={true}
|
||||||
isPending={false}
|
isPending={false}
|
||||||
permissions={[]}
|
permissions={[]}
|
||||||
handleDeletePendingMember={() => {}}
|
|
||||||
removeMemberHandler={async () => {}}
|
|
||||||
/>
|
/>
|
||||||
{projectMembers.map((projectMember, index) => {
|
{projectMembers.map((projectMember, index) => {
|
||||||
return (
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -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, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGithubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse } 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, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse } from './types';
|
||||||
import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGithub, updateEnvironmentVariable, removeEnvironmentVariable } from './mutations';
|
import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGithub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember } from './mutations';
|
||||||
|
|
||||||
export interface GraphQLConfig {
|
export interface GraphQLConfig {
|
||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
@ -91,26 +91,38 @@ export class GQLClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeMember (memberId: string): Promise<RemoveMemberResponse> {
|
|
||||||
const { data } = await this.client.mutate({
|
|
||||||
mutation: removeMember,
|
|
||||||
variables: {
|
|
||||||
memberId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectMembers (projectId: string) : Promise<GetProjectMembersResponse> {
|
async getProjectMembers (projectId: string) : Promise<GetProjectMembersResponse> {
|
||||||
const { data } = await this.client.query({
|
const result = await this.client.query({
|
||||||
query: getProjectMembers,
|
query: getProjectMembers,
|
||||||
variables: {
|
variables: {
|
||||||
projectId
|
projectId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectMember (projectMemberId: string, data: UpdateProjectMemberInput): Promise<UpdateProjectMemberResponse> {
|
||||||
|
const result = await this.client.mutate({
|
||||||
|
mutation: updateProjectMember,
|
||||||
|
variables: {
|
||||||
|
projectMemberId,
|
||||||
|
data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeProjectMember (projectMemberId: string): Promise<RemoveProjectMemberResponse> {
|
||||||
|
const result = await this.client.mutate({
|
||||||
|
mutation: removeProjectMember,
|
||||||
|
variables: {
|
||||||
|
projectMemberId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchProjects (searchText: string) : Promise<SearchProjectsResponse> {
|
async searchProjects (searchText: string) : Promise<SearchProjectsResponse> {
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const removeMember = gql`
|
export const removeProjectMember = gql`
|
||||||
mutation ($memberId: String!) {
|
mutation ($projectMemberId: String!) {
|
||||||
removeMember(memberId: $memberId)
|
removeProjectMember(projectMemberId: $projectMemberId)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const updateProjectMember = gql`
|
||||||
|
mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) {
|
||||||
|
updateProjectMember(projectMemberId: $projectMemberId, data: $data)
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -134,8 +134,12 @@ export type GetProjectMembersResponse = {
|
|||||||
projectMembers: ProjectMember[]
|
projectMembers: ProjectMember[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RemoveMemberResponse = {
|
export type RemoveProjectMemberResponse = {
|
||||||
removeMember: boolean;
|
removeProjectMember: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateProjectMemberResponse = {
|
||||||
|
updateProjectMember: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetDeploymentsResponse = {
|
export type GetDeploymentsResponse = {
|
||||||
@ -185,6 +189,10 @@ export type UpdateEnvironmentVariableInput = {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateProjectMemberInput = {
|
||||||
|
permissions: Permission[];
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateEnvironmentVariableResponse = {
|
export type UpdateEnvironmentVariableResponse = {
|
||||||
updateEnvironmentVariable: boolean;
|
updateEnvironmentVariable: boolean;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user