Add new member dialog in project settings tab (#42)

* Implement add member dialog

* Fix typo

* Display the added pending member

* Seperate form submit handler for add member dialog

* Enable production branch input on repo connected

* Refactor project to include members permissions

* Refactor add member handler

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-09 12:25:37 +05:30 committed by GitHub
parent 0894d8da3c
commit c61df21a00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 325 additions and 50 deletions

View File

@ -2,19 +2,16 @@
{ {
"name": "Saugat Yadav", "name": "Saugat Yadav",
"email": "saugaty@airfoil.studio", "email": "saugaty@airfoil.studio",
"id": 1, "id": 1
"permissions": []
}, },
{ {
"name": "Gideon Low", "name": "Gideon Low",
"email": "gideonl@airfoil.studio", "email": "gideonl@airfoil.studio",
"id": 2, "id": 2
"permissions": ["view", "edit"]
}, },
{ {
"name": "Sushan Yadav", "name": "Sushan Yadav",
"email": "sushany@airfoil.studio", "email": "sushany@airfoil.studio",
"id": 3, "id": 3
"permissions": ["view"]
} }
] ]

View File

@ -17,7 +17,20 @@
"branch": "main" "branch": "main"
}, },
"repositoryId": 1, "repositoryId": 1,
"members": [1, 2, 3], "members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1 "ownerId": 1
}, },
{ {
@ -38,7 +51,16 @@
"branch": "staging" "branch": "staging"
}, },
"repositoryId": 1, "repositoryId": 1,
"members": [2, 3], "members": [
{
"id": 2,
"permissions": []
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 2 "ownerId": 2
}, },
{ {
@ -59,7 +81,20 @@
"branch": "main" "branch": "main"
}, },
"repositoryId": 1, "repositoryId": 1,
"members": [1], "members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1 "ownerId": 1
}, },
{ {
@ -80,7 +115,20 @@
"branch": "main" "branch": "main"
}, },
"repositoryId": 1, "repositoryId": 1,
"members": [1], "members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1 "ownerId": 1
}, },
{ {
@ -101,8 +149,13 @@
"branch": "main" "branch": "main"
}, },
"repositoryId": 1, "repositoryId": 1,
"members": [1], "members": [
"ownerId": 1 {
"id": 3,
"permissions": []
}
],
"ownerId": 3
}, },
{ {
"id": 6, "id": 6,
@ -122,7 +175,16 @@
"branch": "prod" "branch": "prod"
}, },
"repositoryId": 1, "repositoryId": 1,
"members": [1], "members": [
"ownerId": 1 {
"id": 2,
"permissions": []
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 2
} }
] ]

View File

@ -0,0 +1,124 @@
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import {
Button,
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
Input,
Typography,
Checkbox,
} from '@material-tailwind/react';
import { Member, Permission } from '../../../../types/project';
interface AddMemberDialogProp {
open: boolean;
handleOpen: () => void;
handleAddMember: (member: Member) => void;
}
interface formData {
emailAddress: string;
permissions: {
view: boolean;
edit: boolean;
};
}
const AddMemberDialog = ({
open,
handleOpen,
handleAddMember,
}: AddMemberDialogProp) => {
const {
handleSubmit,
register,
reset,
formState: { isValid },
} = useForm({
defaultValues: {
emailAddress: '',
permissions: {
view: true,
edit: false,
},
},
});
const submitHandler = useCallback((data: formData) => {
reset();
handleOpen();
const member: Member = {
email: data.emailAddress,
id: Math.random(),
name: '',
};
handleAddMember(member);
}, []);
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader className="flex justify-between">
<div>Add member</div>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1 rounded-3xl"
>
X
</Button>
</DialogHeader>
<form onSubmit={handleSubmit(submitHandler)}>
<DialogBody className="flex flex-col gap-2 p-4">
<Typography variant="small">
We will send an invitation link to this email address.
</Typography>
<Typography variant="small">Email address</Typography>
<Input
type="email"
crossOrigin={undefined}
{...register('emailAddress', {
required: 'email field cannot be empty',
})}
/>
<Typography variant="small">Permissions</Typography>
<Typography variant="small">
You can change this later if required.
</Typography>
<Checkbox
crossOrigin={undefined}
label={Permission.VIEW}
{...register(`permissions.view`)}
color="blue"
/>
<Checkbox
crossOrigin={undefined}
label={Permission.EDIT}
{...register(`permissions.edit`)}
color="blue"
/>
</DialogBody>
<DialogFooter className="flex justify-start">
<Button variant="outlined" onClick={handleOpen} className="mr-1">
Cancel
</Button>
<Button
variant="gradient"
color="blue"
type="submit"
disabled={!isValid}
>
Send invite
</Button>
</DialogFooter>
</form>
</Dialog>
);
};
export default AddMemberDialog;

View File

@ -131,7 +131,7 @@ const EditEnvironmentVariableRow = ({
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small">
Are you sure you want to delete the variable Are you sure you want to delete the variable&nbsp;
<span className="bg-blue-100">{variable.key}</span>? <span className="bg-blue-100">{variable.key}</span>?
</Typography> </Typography>
</ConfirmDialog> </ConfirmDialog>

View File

@ -100,7 +100,11 @@ const GitTabPanel = () => {
</div> </div>
)} )}
<Typography variant="small">Branch name</Typography> <Typography variant="small">Branch name</Typography>
<Input crossOrigin={undefined} disabled value="main" /> <Input
crossOrigin={undefined}
disabled={Boolean(!linkedRepo)}
value="main"
/>
<Button size="sm" disabled className="mt-1"> <Button size="sm" disabled className="mt-1">
Save Save
</Button> </Button>

View File

@ -1,6 +1,13 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import toast from 'react-hot-toast';
import { Select, Typography, Option } from '@material-tailwind/react'; import {
Select,
Typography,
Option,
Chip,
IconButton,
} from '@material-tailwind/react';
import { Member } from '../../../../types/project'; import { Member } from '../../../../types/project';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
@ -25,11 +32,21 @@ interface MemberCardProps {
member: Member; member: Member;
isFirstCard: boolean; isFirstCard: boolean;
isOwner: boolean; isOwner: boolean;
isPending: boolean;
permissions: string[];
handleDeletePendingMember: (id: number) => void;
} }
const MemberCard = ({ member, isFirstCard, isOwner }: MemberCardProps) => { const MemberCard = ({
member,
isFirstCard,
isOwner,
isPending,
permissions,
handleDeletePendingMember,
}: MemberCardProps) => {
const [selectedPermission, setSelectedPermission] = useState( const [selectedPermission, setSelectedPermission] = useState(
member.permissions.join('+'), permissions.join('+'),
); );
const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false); const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false);
@ -53,11 +70,12 @@ const MemberCard = ({ member, isFirstCard, isOwner }: MemberCardProps) => {
className={`flex p-1 ${!isFirstCard && 'mt-1 border-t border-gray-300'}`} className={`flex p-1 ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
> >
<div>^</div> <div>^</div>
<div className="grow"> <div className="basis-1/2">
<Typography variant="small">{member.name}</Typography> <Typography variant="small">{member.name}</Typography>
<Typography variant="small">{member.email}</Typography> <Typography variant="small">{member.email}</Typography>
</div> </div>
<div className="grow"> <div className="basis-1/2">
{!isPending ? (
<Select <Select
size="lg" size="lg"
label={isOwner ? 'Owner' : ''} label={isOwner ? 'Owner' : ''}
@ -77,6 +95,30 @@ const MemberCard = ({ member, isFirstCard, isOwner }: MemberCardProps) => {
</Option> </Option>
))} ))}
</Select> </Select>
) : (
<div className="flex justify-end gap-2">
<div>
<Chip
value="Pending"
variant="outlined"
color="orange"
size="sm"
icon={'^'}
/>
</div>
<div>
<IconButton
size="sm"
className="rounded-full"
onClick={() => {
handleDeletePendingMember(member.id);
}}
>
D
</IconButton>
</div>
</div>
)}
</div> </div>
<ConfirmDialog <ConfirmDialog
dialogTitle="Remove member?" dialogTitle="Remove member?"
@ -85,6 +127,7 @@ const MemberCard = ({ member, isFirstCard, isOwner }: MemberCardProps) => {
confirmButtonTitle="Yes, Remove member" confirmButtonTitle="Yes, Remove member"
handleConfirm={() => { handleConfirm={() => {
setRemoveMemberDialogOpen((preVal) => !preVal); setRemoveMemberDialogOpen((preVal) => !preVal);
toast.success('Member removed from project');
}} }}
color="red" color="red"
> >

View File

@ -1,5 +1,6 @@
import React, { useMemo } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import toast, { Toaster } from 'react-hot-toast';
import { Chip, Button, Typography } from '@material-tailwind/react'; import { Chip, Button, Typography } from '@material-tailwind/react';
@ -8,24 +9,44 @@ import membersData from '../../../../assets/members.json';
import projectData from '../../../../assets/projects.json'; import projectData from '../../../../assets/projects.json';
import { Member } from '../../../../types/project'; import { Member } from '../../../../types/project';
import AddMemberDialog from './AddMemberDialog';
const FIRST_MEMBER_CARD = 0; const FIRST_MEMBER_CARD = 0;
const MembersTabPanel = () => { const MembersTabPanel = () => {
const { id } = useParams(); const { id } = useParams();
const [addmemberDialogOpen, setAddMemberDialogOpen] = useState(false);
const currProject = useMemo(() => { const currProject = useMemo(() => {
return projectData.find((data) => data.id === Number(id)); return projectData.find((data) => data.id === Number(id));
}, [id]); }, [id]);
const members = useMemo(() => { const members = useMemo(() => {
return ( return membersData.filter(
currProject?.members.map((memberId) => { (member) =>
return membersData.find((member) => member.id === memberId); currProject?.members.some(
}) || [] (projectMember) => projectMember.id === member.id,
),
); );
}, [currProject]); }, [currProject]);
const [updatedMembers, setUpdatedMembers] = useState([...members]);
const getMemberPermissions = useCallback(
(id: number) => {
return (
currProject?.members.find((projectMember) => projectMember.id === id)
?.permissions || []
);
},
[updatedMembers],
);
const addMemberHandler = useCallback((member: Member) => {
setUpdatedMembers((val) => [...val, member]);
toast.success('Invitation sent');
}, []);
return ( return (
<div className="p-2 mb-20"> <div className="p-2 mb-20">
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
@ -35,24 +56,44 @@ const MembersTabPanel = () => {
<Chip <Chip
className="normal-case ml-3 font-normal" className="normal-case ml-3 font-normal"
size="sm" size="sm"
value={members.length} value={updatedMembers.length}
/> />
</div> </div>
</div> </div>
<div> <div>
<Button size="sm">+ Add member</Button> <Button
size="sm"
onClick={() => setAddMemberDialogOpen((preVal) => !preVal)}
>
+ Add member
</Button>
</div> </div>
</div> </div>
{(members as Member[]).map((member, index) => { {(updatedMembers as Member[]).map((member, index) => {
return ( return (
<MemberCard <MemberCard
member={member} member={member}
key={member.id} key={member.id}
isFirstCard={index === FIRST_MEMBER_CARD} isFirstCard={index === FIRST_MEMBER_CARD}
isOwner={member.id === currProject?.ownerId} isOwner={member.id === currProject?.ownerId}
isPending={member.name === ''}
permissions={getMemberPermissions(member.id)}
handleDeletePendingMember={(id: number) => {
setUpdatedMembers(
updatedMembers.filter((member) => member.id !== id),
);
}}
/> />
); );
})} })}
<AddMemberDialog
handleOpen={() => {
setAddMemberDialogOpen((preVal) => !preVal);
}}
open={addmemberDialogOpen}
handleAddMember={addMemberHandler}
/>
<Toaster />
</div> </div>
); );
}; };

View File

@ -16,10 +16,15 @@ export interface ProjectDetails {
branch: string; branch: string;
}; };
repositoryId: number; repositoryId: number;
members: number[]; members: MemberPermission[];
ownerId: number; ownerId: number;
} }
export interface MemberPermission {
id: number;
permissions: string[];
}
export interface DeploymentDetails { export interface DeploymentDetails {
title: string; title: string;
isProduction: boolean; isProduction: boolean;
@ -93,5 +98,4 @@ export interface Member {
name: string; name: string;
email: string; email: string;
id: number; id: number;
permissions: Permission[];
} }