diff --git a/frontend/components/DeleteConfirmDialog.tsx b/frontend/components/DeleteConfirmDialog.tsx index ce2fdb1..5669494 100644 --- a/frontend/components/DeleteConfirmDialog.tsx +++ b/frontend/components/DeleteConfirmDialog.tsx @@ -9,7 +9,14 @@ type Props = { deleteText: string; }; -export default function ProjectDialog({ isOpen, onCancel, onConfirm, closeText, confirmText, deleteText }: Props) { +export default function DeleteConfirmDialog({ + isOpen, + onCancel, + onConfirm, + closeText, + confirmText, + deleteText, +}: Props) { return ( , }, + { + key: 'members', + text: messages.members, + startContent: , + }, { key: 'settings', text: messages.settings, diff --git a/frontend/src/app/[locale]/projects/[projectId]/home/home.tsx b/frontend/src/app/[locale]/projects/[projectId]/home/ProjectHome.tsx similarity index 98% rename from frontend/src/app/[locale]/projects/[projectId]/home/home.tsx rename to frontend/src/app/[locale]/projects/[projectId]/home/ProjectHome.tsx index 130d2bd..06f00b5 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/home/home.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/home/ProjectHome.tsx @@ -43,7 +43,7 @@ type Props = { messages: HomeMessages; }; -export function Home({ projectId, messages }: Props) { +export function ProjectHome({ projectId, messages }: Props) { const context = useContext(TokenContext); const { theme, setTheme } = useTheme(); const [project, setProject] = useState({ diff --git a/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx index 087a63d..73b4c57 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx @@ -1,4 +1,4 @@ -import { Home } from './home'; +import { ProjectHome } from './ProjectHome'; import { useTranslations } from 'next-intl'; export type HomeMessages = { @@ -69,7 +69,7 @@ export default function Page({ params }: { params: { projectId: string } }) { }; return ( <> - + ); } diff --git a/frontend/src/app/[locale]/projects/[projectId]/layout.tsx b/frontend/src/app/[locale]/projects/[projectId]/layout.tsx index 8066f4e..6c4a6a2 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/layout.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/layout.tsx @@ -13,6 +13,7 @@ export default function SidebarLayout({ home: t('home'), testCases: t('test_cases'), testRuns: t('test_runs'), + members: t('members'), settings: t('settings'), }; diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/AddMemberDialog.tsx b/frontend/src/app/[locale]/projects/[projectId]/members/AddMemberDialog.tsx similarity index 100% rename from frontend/src/app/[locale]/projects/[projectId]/settings/AddMemberDialog.tsx rename to frontend/src/app/[locale]/projects/[projectId]/members/AddMemberDialog.tsx diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/CandidatesTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/members/CandidatesTable.tsx similarity index 100% rename from frontend/src/app/[locale]/projects/[projectId]/settings/CandidatesTable.tsx rename to frontend/src/app/[locale]/projects/[projectId]/members/CandidatesTable.tsx diff --git a/frontend/src/app/[locale]/projects/[projectId]/members/MembersPage.tsx b/frontend/src/app/[locale]/projects/[projectId]/members/MembersPage.tsx new file mode 100644 index 0000000..8b27df0 --- /dev/null +++ b/frontend/src/app/[locale]/projects/[projectId]/members/MembersPage.tsx @@ -0,0 +1,100 @@ +'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 { MembersMessages } from '@/types/members'; +import { TokenContext } from '@/utils/TokenProvider'; +import MembersTable from './MembersTable'; +import AddMemberDialog from './AddMemberDialog'; +import { fetchProjectMembers, addMember, deleteMember, updateMember } from './membersControl'; + +type Props = { + projectId: string; + messages: MembersMessages; + locale: string; +}; + +export default function MembersPage({ projectId, messages, locale }: Props) { + const context = useContext(TokenContext); + const [members, setMembers] = useState([]); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + useEffect(() => { + async function fetchDataEffect() { + if (!context.isSignedIn()) { + return; + } + + try { + const data = await fetchProjectMembers(context.token.access_token, projectId); + setMembers(data); + } catch (error: any) { + console.error('Error in effect:', error.message); + } + } + + 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); + }; + + 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 ( +
+
+

{messages.memberManagement}

+ +
+ + + + setIsDialogOpen(false)} + onAddMember={handleAddMember} + messages={messages} + /> +
+ ); +} diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/MembersTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/members/MembersTable.tsx similarity index 100% rename from frontend/src/app/[locale]/projects/[projectId]/settings/MembersTable.tsx rename to frontend/src/app/[locale]/projects/[projectId]/members/MembersTable.tsx diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/membersControl.ts b/frontend/src/app/[locale]/projects/[projectId]/members/membersControl.ts similarity index 100% rename from frontend/src/app/[locale]/projects/[projectId]/settings/membersControl.ts rename to frontend/src/app/[locale]/projects/[projectId]/members/membersControl.ts diff --git a/frontend/src/app/[locale]/projects/[projectId]/members/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/members/page.tsx new file mode 100644 index 0000000..6933085 --- /dev/null +++ b/frontend/src/app/[locale]/projects/[projectId]/members/page.tsx @@ -0,0 +1,29 @@ +import MembersPage from './MembersPage'; +import { useTranslations } from 'next-intl'; + +export default function Page({ params }: { params: { projectId: string; locale: string } }) { + const t = useTranslations('Members'); + 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'), + 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'), + }; + + return ( + <> + + + ); +} diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx b/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx index 4be3a56..0ac0465 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/settings/SettingsPage.tsx @@ -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([]); - const [isDialogOpen, setIsDialogOpen] = useState(false); + const router = useRouter(); + const [project, setProject] = useState({ + 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 ( -
+
-

{messages.memberManagement}

- +

{messages.projectManagement}

+
+ + +
- + + + dummy + dummy + + + + {messages.projectName} + {project.name} + + + {messages.projectDetail} + {project.detail} + + + {messages.publicity} + {project.isPublic ? messages.public : messages.private} + + +
+
+ + setIsProjectDialogOpen(false)} + onSubmit={onSubmit} messages={messages} /> - setIsDialogOpen(false)} - onAddMember={handleAddMember} - messages={messages} + setIsDeleteConfirmDialogOpen(false)} + onConfirm={onConfirm} + closeText={messages.close} + confirmText={messages.areYouSure} + deleteText={messages.delete} />
); diff --git a/frontend/src/app/[locale]/projects/[projectId]/settings/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/settings/page.tsx index b8580ae..84ee812 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/settings/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/settings/page.tsx @@ -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 ( diff --git a/frontend/types/member.ts b/frontend/types/member.ts new file mode 100644 index 0000000..fd90c2e --- /dev/null +++ b/frontend/types/member.ts @@ -0,0 +1,17 @@ +export type MembersMessages = { + memberManagement: string; + avatar: string; + email: string; + username: string; + role: string; + manager: string; + developer: string; + reporter: string; + delete: string; + deleteMember: string; + noMembersFound: string; + addMember: string; + userNameOrEmail: string; + close: string; + add: string; +}; diff --git a/frontend/types/project.ts b/frontend/types/project.ts index f62178e..64d5e16 100644 --- a/frontend/types/project.ts +++ b/frontend/types/project.ts @@ -5,7 +5,7 @@ export type ProjectType = { id: number; name: string; detail: string; - isPublic: string; + isPublic: boolean; userId: number; createdAt: string; updatedAt: string; diff --git a/frontend/types/settings.ts b/frontend/types/settings.ts index 6e41dae..93cb6cf 100644 --- a/frontend/types/settings.ts +++ b/frontend/types/settings.ts @@ -1,17 +1,16 @@ export type SettingsMessages = { - memberManagement: string; - avatar: string; - email: string; - username: string; - role: string; - manager: string; - developer: string; - reporter: string; + projectManagement: string; + projectName: string; + projectDetail: string; + editProject: string; + project: string; + publicity: string; + public: string; + private: string; + ifYouMakePublic: string; + update: string; + deleteProject: string; delete: string; - deleteMember: string; - noMembersFound: string; - addMember: string; - userNameOrEmail: string; close: string; - add: string; + areYouSure: string; }; diff --git a/frontend/utils/projectsControl.ts b/frontend/utils/projectsControl.ts new file mode 100644 index 0000000..585ac12 --- /dev/null +++ b/frontend/utils/projectsControl.ts @@ -0,0 +1,149 @@ +import Config from '@/config/config'; +const apiServer = Config.apiServer; + +/** + * fetch project + */ +async function fetchProject(jwt: string, projectId: number) { + const url = `${apiServer}/projects/${projectId}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + }); + + 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); + } +} + +/** + * fetch project records + */ +async function fetchProjects(jwt: string) { + const url = `${apiServer}/projects`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + }); + + 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); + } +} + +/** + * Create project + */ +async function createProject(jwt: string, name: string, detail: string, isPublic: boolean) { + const newProjectData = { + name, + detail, + isPublic, + }; + + const fetchOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(newProjectData), + }; + + const url = `${apiServer}/projects`; + + 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 creating new project:', error); + throw error; + } +} + +/** + * Update project + */ +async function updateProject(jwt: string, projectId: number, name: string, detail: string, isPublic: boolean) { + const updatedProjectData = { + name, + detail, + isPublic, + }; + + const fetchOptions = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(updatedProjectData), + }; + + const url = `${apiServer}/projects/${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 updating project:', error); + throw error; + } +} + +/** + * Delete project + */ +async function deleteProject(jwt: string, projectId: number) { + const fetchOptions = { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + }; + + const url = `${apiServer}/projects/${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 deleting project:', error); + throw error; + } +} + +export { fetchProject, fetchProjects, createProject, updateProject, deleteProject };