Project member management

This commit is contained in:
Takeshi Kimata
2024-05-30 22:29:46 +09:00
parent abf2d48bee
commit db121940d8
13 changed files with 362 additions and 14 deletions

View File

@@ -267,6 +267,10 @@
"manager": "Manager",
"developer": "Developer",
"reporter": "Reporter",
"no_members_found": "No members found"
"no_members_found": "No members found",
"add_member": "Add member",
"user_name_or_email": "User name or email",
"close": "Close",
"add": "Add"
}
}

View File

@@ -266,6 +266,10 @@
"manager": "管理者",
"developer": "開発者",
"reporter": "報告者",
"no_members_found": "メンバーがいません"
"no_members_found": "メンバーがいません",
"add_member": "メンバーの追加",
"user_name_or_email": "ユーザー名またはメールアドレス",
"close": "閉じる",
"add": "追加"
}
}

View File

@@ -0,0 +1,103 @@
'use client';
import React from 'react';
import { useState, useEffect, useContext } from 'react';
import { Button, Input, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
import { SettingsMessages } from '@/types/settings';
import { TokenContext } from '@/utils/TokenProvider';
import Config from '@/config/config';
import { MemberType, UserType } from '@/types/user';
import CandidatesTable from './CandidatesTable';
const apiServer = Config.apiServer;
type Props = {
isOpen: boolean;
members: MemberType[];
onCancel: () => void;
onAddMember: (memberAdded: UserType) => void;
messages: SettingsMessages;
};
// User Search by username
async function searchUsers(jwt: string, text: string, excludeIdsParam: string) {
const fetchOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
};
const url = `${apiServer}/users/find?search=${text}&excludeIds=${excludeIdsParam}`;
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 AddMemberDialog({ isOpen, members, onCancel, onAddMember, messages }: Props) {
const context = useContext(TokenContext);
const [searchText, setSearchText] = useState('');
const [candidates, setCandidates] = useState<UserType[]>([]);
useEffect(() => {
async function fetchDataEffect() {
if (!searchText) {
return;
}
if (!context.isSignedIn()) {
return;
}
try {
const excludeIds = members.map((member) => {
return member.id;
});
const excludeIdsParam = excludeIds.join(',');
const data = await searchUsers(context.token.access_token, searchText, excludeIdsParam);
setCandidates(data);
} catch (error: any) {
console.error('Error in effect:', error.message);
}
}
fetchDataEffect();
}, [searchText]);
return (
<Modal
isOpen={isOpen}
onOpenChange={() => {
onCancel();
}}
size="2xl"
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">{messages.addMember}</ModalHeader>
<ModalBody>
<Input
type="text"
label={messages.userNameOrEmail}
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
}}
/>
<CandidatesTable candidates={candidates} onAddPress={onAddMember} messages={messages} />
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onCancel}>
{messages.close}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@@ -0,0 +1,90 @@
import { useMemo, useCallback } from 'react';
import { Button, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@nextui-org/react';
import { UserType } from '@/types/user';
import { SettingsMessages } from '@/types/settings';
import Avatar from 'boring-avatars';
type Props = {
candidates: UserType[];
onAddPress: (memberAdded: UserType) => void;
messages: SettingsMessages;
};
export default function MembersTable({ candidates, onAddPress, messages }: Props) {
const headerColumns = [
{ name: messages.avatar, uid: 'avatar' },
{ name: messages.email, uid: 'email' },
{ name: messages.username, uid: 'username' },
{ name: messages.add, uid: 'add' },
];
const renderCell = useCallback((candidate: UserType, columnKey: Key) => {
const cellValue = candidate[columnKey as keyof UserType];
switch (columnKey) {
case 'avatar':
return (
<Avatar
size={16}
name={candidate.username}
variant="beam"
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
/>
);
case 'email':
return cellValue;
case 'username':
return cellValue;
case 'add':
return (
<Button color="primary" variant="faded" size="sm" onPress={() => onAddPress(candidate)}>
{messages.add}
</Button>
);
default:
return cellValue;
}
}, []);
const classNames = useMemo(
() => ({
wrapper: ['max-w-3xl'],
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
td: [
// changing the rows border radius
// first
'group-data-[first=true]:first:before:rounded-none',
'group-data-[first=true]:last:before:rounded-none',
// middle
'group-data-[middle=true]:before:rounded-none',
// last
'group-data-[last=true]:first:before:rounded-none',
'group-data-[last=true]:last:before:rounded-none',
],
}),
[]
);
return (
<>
<Table isCompact aria-label="Users table" classNames={classNames}>
<TableHeader columns={headerColumns}>
{(column) => (
<TableColumn
key={column.uid}
align={column.uid === 'actions' ? 'center' : 'start'}
allowsSorting={column.sortable}
>
{column.name}
</TableColumn>
)}
</TableHeader>
<TableBody emptyContent={messages.noMembersFound} items={candidates}>
{(item) => (
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
)}
</TableBody>
</Table>
</>
);
}

View File

@@ -1,17 +1,16 @@
import { useState, useMemo, useCallback } from 'react';
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, SortDescriptor } from '@nextui-org/react';
import { UserType } from '@/types/user';
import { MemberType, UserType } from '@/types/user';
import { SettingsMessages } from '@/types/settings';
import { memberRoles } from '@/config/selection';
import Avatar from 'boring-avatars';
type Props = {
members: UserType[];
members: MemberType[];
messages: SettingsMessages;
locale: string;
};
export default function MembersTable({ members, messages, locale }: Props) {
export default function MembersTable({ members, messages }: Props) {
const headerColumns = [
{ name: messages.avatar, uid: 'avatar', sortable: false },
{ name: messages.email, uid: 'email', sortable: true },

View File

@@ -1,11 +1,14 @@
'use client';
import React from 'react';
import { useState, useEffect, useContext } from 'react';
import { UserType } from '@/types/user';
import { Button } from '@nextui-org/react';
import { Plus } from 'lucide-react';
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;
type Props = {
@@ -32,24 +35,22 @@ async function fetchProjectMembers(jwt: string, projectId: string) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
return data;
} catch (error: any) {
console.error('Error fetching data:', error.message);
}
}
// User Search by username
async function fetchUsers(jwt: string, text: string) {
async function addMember(jwt: string, userId: string, projectId: string) {
const fetchOptions = {
method: 'GET',
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
};
const url = `${apiServer}/users?text=${text}`;
const url = `${apiServer}/members?userId=${userId}&projectId=${projectId}`;
try {
const response = await fetch(url, fetchOptions);
@@ -65,7 +66,8 @@ async function fetchUsers(jwt: string, text: string) {
export default function SettingsPage({ projectId, messages, locale }: Props) {
const context = useContext(TokenContext);
const [members, setMembers] = useState<UserType[]>([]);
const [members, setMembers] = useState<MemberType[]>([]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
useEffect(() => {
async function fetchDataEffect() {
@@ -84,13 +86,34 @@ 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 updateMembers = [...members];
updateMembers.push(newMember);
setMembers(updateMembers);
setIsDialogOpen(false);
};
return (
<div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow">
<div className="w-full p-3 flex items-center justify-between">
<h3 className="font-bold">{messages.memberManagement}</h3>
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={() => setIsDialogOpen(true)}>
{messages.addMember}
</Button>
</div>
<MembersTable members={members} messages={messages} locale={locale} />
<MembersTable members={members} messages={messages} />
<AddMemberDialog
isOpen={isDialogOpen}
members={members}
onCancel={() => setIsDialogOpen(false)}
onAddMember={handleAddMember}
messages={messages}
/>
</div>
);
}

View File

@@ -13,6 +13,10 @@ export default function Page({ params }: { params: { projectId: string; locale:
developer: t('developer'),
reporter: t('reporter'),
noMembersFound: t('no_members_found'),
addMember: t('add_member'),
userNameOrEmail: t('user_name_or_email'),
close: t('close'),
add: t('add'),
};
return (

View File

@@ -5,4 +5,8 @@ export type SettingsMessages = {
username: string;
role: string;
noMembersFound: string;
addMember: string;
userNameOrEmail: string;
close: string;
add: string;
};

View File

@@ -68,3 +68,11 @@ export type AccountDropDownMessages = {
signIn: string;
signOut: string;
};
export type MemberType = {
id: number | null;
userId: number;
projectId: number;
role: number;
User: UserType;
};