From 191d8c0a72cee68c4e8a14b56ad5c8ec049dd515 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Fri, 31 May 2024 23:15:03 +0900 Subject: [PATCH] Project member management --- backend/routes/members/edit.js | 2 + frontend/messages/en.json | 2 + frontend/messages/ja.json | 2 + .../[projectId]/settings/AddMemberDialog.tsx | 17 +++- .../[projectId]/settings/CandidatesTable.tsx | 2 +- .../[projectId]/settings/MembersTable.tsx | 46 ++++++++- .../[projectId]/settings/SettingsPage.tsx | 80 ++++++--------- .../[projectId]/settings/membersControl.ts | 97 +++++++++++++++++++ .../projects/[projectId]/settings/page.tsx | 2 + frontend/types/settings.ts | 5 + 10 files changed, 194 insertions(+), 61 deletions(-) create mode 100644 frontend/src/app/[locale]/projects/[projectId]/settings/membersControl.ts diff --git a/backend/routes/members/edit.js b/backend/routes/members/edit.js index 91838ab..23a4c13 100644 --- a/backend/routes/members/edit.js +++ b/backend/routes/members/edit.js @@ -10,6 +10,7 @@ module.exports = function (sequelize) { router.put('/', verifySignedIn, verifyProjectManager, async (req, res) => { const userId = req.query.userId; const projectId = req.query.projectId; + const role = req.query.role; try { const member = await Member.findOne({ @@ -26,6 +27,7 @@ module.exports = function (sequelize) { await member.update({ userId, projectId, + role, }); res.json(member); } catch (error) { diff --git a/frontend/messages/en.json b/frontend/messages/en.json index baf6780..b48e6ec 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -267,6 +267,8 @@ "manager": "Manager", "developer": "Developer", "reporter": "Reporter", + "delete": "Delete", + "deleteMember": "Delete member", "no_members_found": "No members found", "add_member": "Add member", "user_name_or_email": "User name or email", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index 6dfef32..8af2270 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -266,6 +266,8 @@ "manager": "管理者", "developer": "開発者", "reporter": "報告者", + "delete": "削除", + "deleteMember": "メンバーの削除", "no_members_found": "メンバーがいません", "add_member": "メンバーの追加", "user_name_or_email": "ユーザー名またはメールアドレス", diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/AddMemberDialog.tsx b/frontend/src/app/[locale]/projects/[projectId]/settings/AddMemberDialog.tsx index 569f8c9..d698378 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/settings/AddMemberDialog.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/settings/AddMemberDialog.tsx @@ -71,11 +71,22 @@ export default function AddMemberDialog({ isOpen, members, onCancel, onAddMember fetchDataEffect(); }, [searchText]); + const handleExit = () => { + setSearchText(''); + setCandidates([]); + onCancel(); + }; + + const handleAdd = (userAdded: UserType) => { + onAddMember(userAdded); + handleExit(); + }; + return ( { - onCancel(); + handleExit(); }} size="2xl" > @@ -90,10 +101,10 @@ export default function AddMemberDialog({ isOpen, members, onCancel, onAddMember setSearchText(e.target.value); }} /> - + - diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/CandidatesTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/settings/CandidatesTable.tsx index 0be9dc8..5479e8f 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/settings/CandidatesTable.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/settings/CandidatesTable.tsx @@ -6,7 +6,7 @@ import Avatar from 'boring-avatars'; type Props = { candidates: UserType[]; - onAddPress: (memberAdded: UserType) => void; + onAddPress: (userAdded: UserType) => void; messages: SettingsMessages; }; diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/MembersTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/settings/MembersTable.tsx index 8f82f4b..0f38266 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/settings/MembersTable.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/settings/MembersTable.tsx @@ -1,5 +1,19 @@ import { useState, useMemo, useCallback } from 'react'; -import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, SortDescriptor } from '@nextui-org/react'; +import { + Button, + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + SortDescriptor, + DropdownTrigger, + Dropdown, + DropdownMenu, + DropdownItem, +} from '@nextui-org/react'; +import { ChevronDown } from 'lucide-react'; import { MemberType, UserType } from '@/types/user'; import { SettingsMessages } from '@/types/settings'; import { memberRoles } from '@/config/selection'; @@ -7,15 +21,18 @@ import Avatar from 'boring-avatars'; type Props = { members: MemberType[]; + onChangeRole: (userEdit: UserType, role: number) => void; + onDeleteMember: (userDeleted: UserType) => void; messages: SettingsMessages; }; -export default function MembersTable({ members, messages }: Props) { +export default function MembersTable({ members, onChangeRole, onDeleteMember, messages }: Props) { const headerColumns = [ - { name: messages.avatar, uid: 'avatar', sortable: false }, + { name: messages.avatar, uid: 'avatar' }, { name: messages.email, uid: 'email', sortable: true }, { name: messages.username, uid: 'username', sortable: true }, { name: messages.role, uid: 'role', sortable: true }, + { name: messages.delete, uid: 'delete' }, ]; const [sortDescriptor, setSortDescriptor] = useState({ @@ -51,7 +68,28 @@ export default function MembersTable({ members, messages }: Props) { case 'username': return member.User.username; case 'role': - return {messages[memberRoles[cellValue].uid]}; + return ( + + + + + + {memberRoles.map((role, index) => ( + onChangeRole(member.User, index)}> + {messages[role.uid]} + + ))} + + + ); + case 'delete': + return ( + + ); default: return cellValue; } diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx b/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx index c1a64db..9b91faf 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx @@ -7,9 +7,8 @@ import { MemberType, UserType } from '@/types/user'; import { SettingsMessages } from '@/types/settings'; import { TokenContext } from '@/utils/TokenProvider'; import MembersTable from './MembersTable'; -import Config from '@/config/config'; import AddMemberDialog from './AddMemberDialog'; -const apiServer = Config.apiServer; +import { fetchProjectMembers, addMember, deleteMember, updateMember } from './membersControl'; type Props = { projectId: string; @@ -17,53 +16,6 @@ type Props = { locale: string; }; -// Member Search -async function fetchProjectMembers(jwt: string, projectId: string) { - const fetchOptions = { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwt}`, - }, - }; - - const url = `${apiServer}/members?projectId=${projectId}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error: any) { - console.error('Error fetching data:', error.message); - } -} - -async function addMember(jwt: string, userId: string, projectId: string) { - const fetchOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwt}`, - }, - }; - - const url = `${apiServer}/members?userId=${userId}&projectId=${projectId}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error: any) { - console.error('Error fetching data:', error.message); - } -} - export default function SettingsPage({ projectId, messages, locale }: Props) { const context = useContext(TokenContext); const [members, setMembers] = useState([]); @@ -86,9 +38,9 @@ export default function SettingsPage({ projectId, messages, locale }: Props) { fetchDataEffect(); }, [context]); - const handleAddMember = async (memberAdded: UserType) => { - const newMember = await addMember(context.token.access_token, memberAdded.id, projectId); - newMember.User = memberAdded; + const handleAddMember = async (userAdded: UserType) => { + const newMember = await addMember(context.token.access_token, userAdded.id, projectId); + newMember.User = userAdded; const updateMembers = [...members]; updateMembers.push(newMember); setMembers(updateMembers); @@ -96,6 +48,23 @@ export default function SettingsPage({ projectId, messages, locale }: Props) { setIsDialogOpen(false); }; + const handleDeleteMember = async (userDeleted: UserType) => { + await deleteMember(context.token.access_token, userDeleted.id, projectId); + setMembers(members.filter((member) => member.User.id !== userDeleted.id)); + }; + + const handleChangeRole = async (userEdit: UserType, role: number) => { + await updateMember(context.token.access_token, userEdit.id, projectId, role); + setMembers((prevMembers) => { + return prevMembers.map((member) => { + if (member.User.id === userEdit.id) { + return { ...member, role: role }; + } + return member; + }); + }); + }; + return (
@@ -105,7 +74,12 @@ export default function SettingsPage({ projectId, messages, locale }: Props) {
- +