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",
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
>
|
||||
<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>?
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
|
@ -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>
|
||||
|
@ -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,30 +70,55 @@ 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">
|
||||
<Select
|
||||
size="lg"
|
||||
label={isOwner ? 'Owner' : ''}
|
||||
disabled={isOwner}
|
||||
value={selectedPermission}
|
||||
onChange={(value) => handlePermissionChange(value!)}
|
||||
selected={(_, index) => (
|
||||
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
||||
)}
|
||||
>
|
||||
{DROPDOWN_OPTIONS.map((permission, key) => (
|
||||
<Option key={key} value={permission.value}>
|
||||
^ {permission.label}
|
||||
{permission.value === selectedPermission && (
|
||||
<p className="float-right">^</p>
|
||||
)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="basis-1/2">
|
||||
{!isPending ? (
|
||||
<Select
|
||||
size="lg"
|
||||
label={isOwner ? 'Owner' : ''}
|
||||
disabled={isOwner}
|
||||
value={selectedPermission}
|
||||
onChange={(value) => handlePermissionChange(value!)}
|
||||
selected={(_, index) => (
|
||||
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
||||
)}
|
||||
>
|
||||
{DROPDOWN_OPTIONS.map((permission, key) => (
|
||||
<Option key={key} value={permission.value}>
|
||||
^ {permission.label}
|
||||
{permission.value === selectedPermission && (
|
||||
<p className="float-right">^</p>
|
||||
)}
|
||||
</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"
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user