feat: add project settings page
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
'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 { UserType } from '@/types/user';
|
||||
import { searchUsers } from './membersControl';
|
||||
import CandidatesTable from './CandidatesTable';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
projectId: string;
|
||||
onCancel: () => void;
|
||||
onAddMember: (memberAdded: UserType) => void;
|
||||
messages: SettingsMessages;
|
||||
};
|
||||
|
||||
export default function AddMemberDialog({ isOpen, projectId, 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 data = await searchUsers(context.token.access_token, projectId, searchText);
|
||||
setCandidates(data);
|
||||
} catch (error: any) {
|
||||
console.error('Error in effect:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
fetchDataEffect();
|
||||
}, [searchText]);
|
||||
|
||||
const handleExit = () => {
|
||||
setSearchText('');
|
||||
setCandidates([]);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const handleAdd = (userAdded: UserType) => {
|
||||
onAddMember(userAdded);
|
||||
handleExit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={() => {
|
||||
handleExit();
|
||||
}}
|
||||
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={handleAdd} messages={messages} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={handleExit}>
|
||||
{messages.close}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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: (userAdded: 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import { useState, useMemo } from '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';
|
||||
import Avatar from 'boring-avatars';
|
||||
|
||||
type Props = {
|
||||
members: MemberType[];
|
||||
isDisabled: boolean;
|
||||
onChangeRole: (userEdit: UserType, role: number) => void;
|
||||
onDeleteMember: (userDeleted: UserType) => void;
|
||||
messages: SettingsMessages;
|
||||
};
|
||||
|
||||
export default function MembersTable({ members, isDisabled, onChangeRole, onDeleteMember, messages }: Props) {
|
||||
const headerColumns = [
|
||||
{ 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<SortDescriptor>({
|
||||
column: 'role',
|
||||
direction: 'ascending',
|
||||
});
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return [...members].sort((a: UserType, b: UserType) => {
|
||||
const first = a[sortDescriptor.column as keyof UserType] as number;
|
||||
const second = b[sortDescriptor.column as keyof UserType] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, members]);
|
||||
|
||||
const renderCell = (member: UserType, columnKey: Key) => {
|
||||
const cellValue = member[columnKey as keyof UserType];
|
||||
|
||||
switch (columnKey) {
|
||||
case 'avatar':
|
||||
return (
|
||||
<Avatar
|
||||
size={24}
|
||||
name={member.User.username}
|
||||
variant="beam"
|
||||
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
|
||||
/>
|
||||
);
|
||||
case 'email':
|
||||
return member.User.email;
|
||||
case 'username':
|
||||
return member.User.username;
|
||||
case 'role':
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button size="sm" isDisabled={isDisabled} variant="light" endContent={<ChevronDown size={16} />}>
|
||||
<span className="w-12">{messages[memberRoles[cellValue].uid]}</span>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="test case actions">
|
||||
{memberRoles.map((role, index) => (
|
||||
<DropdownItem key={index} onPress={() => onChangeRole(member.User, index)}>
|
||||
{messages[role.uid]}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
case 'delete':
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
isDisabled={isDisabled}
|
||||
color="danger"
|
||||
variant="light"
|
||||
onClick={() => onDeleteMember(member.User)}
|
||||
>
|
||||
{messages.deleteMember}
|
||||
</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}
|
||||
sortDescriptor={sortDescriptor}
|
||||
onSortChange={setSortDescriptor}
|
||||
>
|
||||
<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={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { MemberType, UserType } from '@/types/user';
|
||||
import { Button, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@nextui-org/react';
|
||||
import { Pencil, Trash } from 'lucide-react';
|
||||
import { SettingsMessages } from '@/types/settings';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
import MembersTable from './MembersTable';
|
||||
import AddMemberDialog from './AddMemberDialog';
|
||||
import { fetchProjectMembers, addMember, deleteMember, updateMember } from './membersControl';
|
||||
import { deleteProject, fetchProject, updateProject } from '@/utils/projectsControl';
|
||||
import { ProjectType } from '@/types/project';
|
||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||
import { useRouter } from '@/src/navigation';
|
||||
import ProjectDialog from '@/components/ProjectDialog';
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -18,8 +19,18 @@ type Props = {
|
||||
|
||||
export default function SettingsPage({ projectId, messages, locale }: Props) {
|
||||
const context = useContext(TokenContext);
|
||||
const [members, setMembers] = useState<MemberType[]>([]);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const [project, setProject] = useState<ProjectType>({
|
||||
id: 0,
|
||||
name: '',
|
||||
detail: '',
|
||||
isPublic: false,
|
||||
userId: 0,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
Folders: [],
|
||||
Runs: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDataEffect() {
|
||||
@@ -28,8 +39,8 @@ export default function SettingsPage({ projectId, messages, locale }: Props) {
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchProjectMembers(context.token.access_token, projectId);
|
||||
setMembers(data);
|
||||
const data = await fetchProject(context.token.access_token, Number(projectId));
|
||||
setProject(data);
|
||||
} catch (error: any) {
|
||||
console.error('Error in effect:', error.message);
|
||||
}
|
||||
@@ -38,62 +49,87 @@ export default function SettingsPage({ projectId, messages, locale }: Props) {
|
||||
fetchDataEffect();
|
||||
}, [context]);
|
||||
|
||||
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);
|
||||
|
||||
setIsDialogOpen(false);
|
||||
// project dialog
|
||||
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
|
||||
const onSubmit = async (name: string, detail: string, isPublic: boolean) => {
|
||||
const updatedProject = await updateProject(context.token.access_token, project.id, name, detail, isPublic);
|
||||
setProject(updatedProject);
|
||||
setIsProjectDialogOpen(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;
|
||||
});
|
||||
});
|
||||
// delete confirm dialog
|
||||
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
|
||||
const onConfirm = async () => {
|
||||
await deleteProject(context.token.access_token, Number(projectId));
|
||||
setIsDeleteConfirmDialogOpen(false);
|
||||
router.push(`/projects/`, { locale: locale });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow">
|
||||
<div className="container mx-auto max-w-3xl pt-6 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"
|
||||
isDisabled={!context.isProjectManager(Number(projectId))}
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
>
|
||||
{messages.addMember}
|
||||
</Button>
|
||||
<h3 className="font-bold">{messages.projectManagement}</h3>
|
||||
<div>
|
||||
<Button
|
||||
startContent={<Trash size={16} />}
|
||||
size="sm"
|
||||
color="danger"
|
||||
isDisabled={!context.isProjectManager(Number(projectId))}
|
||||
onClick={() => setIsDeleteConfirmDialogOpen(true)}
|
||||
>
|
||||
{messages.deleteProject}
|
||||
</Button>
|
||||
<Button
|
||||
startContent={<Pencil size={16} />}
|
||||
size="sm"
|
||||
color="primary"
|
||||
isDisabled={!context.isProjectManager(Number(projectId))}
|
||||
onClick={() => setIsProjectDialogOpen(true)}
|
||||
className="ms-2"
|
||||
>
|
||||
{messages.editProject}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MembersTable
|
||||
members={members}
|
||||
isDisabled={!context.isProjectManager(Number(projectId))}
|
||||
onChangeRole={handleChangeRole}
|
||||
onDeleteMember={handleDeleteMember}
|
||||
<div className="w-full p-3">
|
||||
<Table hideHeader aria-label="Example static collection table">
|
||||
<TableHeader>
|
||||
<TableColumn>dummy</TableColumn>
|
||||
<TableColumn>dummy</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow key="1">
|
||||
<TableCell>{messages.projectName}</TableCell>
|
||||
<TableCell>{project.name}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow key="2">
|
||||
<TableCell>{messages.projectDetail}</TableCell>
|
||||
<TableCell>{project.detail}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow key="3">
|
||||
<TableCell>{messages.publicity}</TableCell>
|
||||
<TableCell>{project.isPublic ? messages.public : messages.private}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<ProjectDialog
|
||||
isOpen={isProjectDialogOpen}
|
||||
editingProject={project}
|
||||
onCancel={() => setIsProjectDialogOpen(false)}
|
||||
onSubmit={onSubmit}
|
||||
messages={messages}
|
||||
/>
|
||||
|
||||
<AddMemberDialog
|
||||
isOpen={isDialogOpen}
|
||||
projectId={projectId}
|
||||
onCancel={() => setIsDialogOpen(false)}
|
||||
onAddMember={handleAddMember}
|
||||
messages={messages}
|
||||
<DeleteConfirmDialog
|
||||
isOpen={isDeleteConfirmDialogOpen}
|
||||
onCancel={() => setIsDeleteConfirmDialogOpen(false)}
|
||||
onConfirm={onConfirm}
|
||||
closeText={messages.close}
|
||||
confirmText={messages.areYouSure}
|
||||
deleteText={messages.delete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import Config from '@/config/config';
|
||||
const apiServer = Config.apiServer;
|
||||
|
||||
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 searchUsers(jwt: string, projectId: string, searchText: string) {
|
||||
const fetchOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
};
|
||||
|
||||
const url = `${apiServer}/users/find?projectId=${projectId}&search=${searchText}`;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMember(jwt: string, userId: string, projectId: string) {
|
||||
const fetchOptions = {
|
||||
method: 'DELETE',
|
||||
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}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching data:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMember(jwt: string, userId: string, projectId: string, role: number) {
|
||||
const fetchOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
};
|
||||
|
||||
const url = `${apiServer}/members?userId=${userId}&projectId=${projectId}&role=${role}`;
|
||||
|
||||
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 { fetchProjectMembers, searchUsers, addMember, deleteMember, updateMember };
|
||||
@@ -4,21 +4,20 @@ import { useTranslations } from 'next-intl';
|
||||
export default function Page({ params }: { params: { projectId: string; locale: string } }) {
|
||||
const t = useTranslations('Settings');
|
||||
const messages = {
|
||||
memberManagement: t('member_management'),
|
||||
avatar: t('avatar'),
|
||||
email: t('email'),
|
||||
username: t('username'),
|
||||
role: t('role'),
|
||||
manager: t('manager'),
|
||||
developer: t('developer'),
|
||||
reporter: t('reporter'),
|
||||
projectManagement: t('project_management'),
|
||||
projectName: t('project_name'),
|
||||
projectDetail: t('project_detail'),
|
||||
editProject: t('edit_project'),
|
||||
project: t('project'),
|
||||
ifYouMakePublic: t('if_you_make_public'),
|
||||
public: t('public'),
|
||||
publicity: t('publicity'),
|
||||
private: t('private'),
|
||||
update: t('update'),
|
||||
deleteProject: t('delete_project'),
|
||||
delete: t('delete'),
|
||||
deleteMember: t('deleteMember'),
|
||||
noMembersFound: t('no_members_found'),
|
||||
addMember: t('add_member'),
|
||||
userNameOrEmail: t('user_name_or_email'),
|
||||
close: t('close'),
|
||||
add: t('add'),
|
||||
areYouSure: t('are_you_sure'),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user