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:
parent
0894d8da3c
commit
c61df21a00
@ -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"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -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;
|
@ -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
|
||||||
<span className="bg-blue-100">{variable.key}</span>?
|
<span className="bg-blue-100">{variable.key}</span>?
|
||||||
</Typography>
|
</Typography>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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[];
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user