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",
"email": "saugaty@airfoil.studio",
"id": 1,
"permissions": []
"id": 1
},
{
"name": "Gideon Low",
"email": "gideonl@airfoil.studio",
"id": 2,
"permissions": ["view", "edit"]
"id": 2
},
{
"name": "Sushan Yadav",
"email": "sushany@airfoil.studio",
"id": 3,
"permissions": ["view"]
"id": 3
}
]

View File

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

View File

@ -100,7 +100,11 @@ const GitTabPanel = () => {
</div>
)}
<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">
Save
</Button>

View File

@ -1,6 +1,13 @@
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 ConfirmDialog from '../../../shared/ConfirmDialog';
@ -25,11 +32,21 @@ interface MemberCardProps {
member: Member;
isFirstCard: 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(
member.permissions.join('+'),
permissions.join('+'),
);
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'}`}
>
<div>^</div>
<div className="grow">
<div className="basis-1/2">
<Typography variant="small">{member.name}</Typography>
<Typography variant="small">{member.email}</Typography>
</div>
<div className="grow">
<div className="basis-1/2">
{!isPending ? (
<Select
size="lg"
label={isOwner ? 'Owner' : ''}
@ -77,6 +95,30 @@ const MemberCard = ({ member, isFirstCard, isOwner }: MemberCardProps) => {
</Option>
))}
</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>
<ConfirmDialog
dialogTitle="Remove member?"
@ -85,6 +127,7 @@ const MemberCard = ({ member, isFirstCard, isOwner }: MemberCardProps) => {
confirmButtonTitle="Yes, Remove member"
handleConfirm={() => {
setRemoveMemberDialogOpen((preVal) => !preVal);
toast.success('Member removed from project');
}}
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 toast, { Toaster } from 'react-hot-toast';
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 { Member } from '../../../../types/project';
import AddMemberDialog from './AddMemberDialog';
const FIRST_MEMBER_CARD = 0;
const MembersTabPanel = () => {
const { id } = useParams();
const [addmemberDialogOpen, setAddMemberDialogOpen] = useState(false);
const currProject = useMemo(() => {
return projectData.find((data) => data.id === Number(id));
}, [id]);
const members = useMemo(() => {
return (
currProject?.members.map((memberId) => {
return membersData.find((member) => member.id === memberId);
}) || []
return membersData.filter(
(member) =>
currProject?.members.some(
(projectMember) => projectMember.id === member.id,
),
);
}, [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 (
<div className="p-2 mb-20">
<div className="flex justify-between mb-2">
@ -35,24 +56,44 @@ const MembersTabPanel = () => {
<Chip
className="normal-case ml-3 font-normal"
size="sm"
value={members.length}
value={updatedMembers.length}
/>
</div>
</div>
<div>
<Button size="sm">+ Add member</Button>
<Button
size="sm"
onClick={() => setAddMemberDialogOpen((preVal) => !preVal)}
>
+ Add member
</Button>
</div>
</div>
{(members as Member[]).map((member, index) => {
{(updatedMembers as Member[]).map((member, index) => {
return (
<MemberCard
member={member}
key={member.id}
isFirstCard={index === FIRST_MEMBER_CARD}
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>
);
};

View File

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