From a2fe05ad1142ded94e767bcfc0275e42c3efc3b4 Mon Sep 17 00:00:00 2001 From: Javi Pacheco Date: Tue, 26 Sep 2023 08:09:14 +0200 Subject: [PATCH] Project Screen (#449) * Project Screen * Showing empty message when there is no data --- .../services/ProjectRepositoryService.kt | 2 +- .../Pages/Organizations/Organizations.tsx | 162 +++++---- .../components/Pages/Projects/Projects.tsx | 307 ++++++++++++++++++ .../src/components/Pages/Projects/index.ts | 1 + server/web/src/components/Sidebar/Sidebar.tsx | 5 + server/web/src/main.tsx | 9 + server/web/src/utils/api/config.ts | 1 + server/web/src/utils/api/organizations.ts | 6 +- server/web/src/utils/api/projects.ts | 147 +++++++++ server/web/src/utils/models.ts | 16 + 10 files changed, 585 insertions(+), 71 deletions(-) create mode 100644 server/web/src/components/Pages/Projects/Projects.tsx create mode 100644 server/web/src/components/Pages/Projects/index.ts create mode 100644 server/web/src/utils/api/projects.ts diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/services/ProjectRepositoryService.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/services/ProjectRepositoryService.kt index b3e4a3df6..d1bcac9b6 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/services/ProjectRepositoryService.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/services/ProjectRepositoryService.kt @@ -129,7 +129,7 @@ class ProjectRepositoryService( throw OrganizationsException("You can't delete the project. User is not the owner of the organization") } - organization.delete() + project.delete() } } diff --git a/server/web/src/components/Pages/Organizations/Organizations.tsx b/server/web/src/components/Pages/Organizations/Organizations.tsx index 7c9f08ad3..b4f3d17cf 100644 --- a/server/web/src/components/Pages/Organizations/Organizations.tsx +++ b/server/web/src/components/Pages/Organizations/Organizations.tsx @@ -1,7 +1,8 @@ import { useAuth } from "@/state/Auth"; import { LoadingContext } from "@/state/Loading"; -import { PostOrganizationProps, deleteOrganizations, getOrganizations, postOrganizations, putOrganizations } from "@/utils/api/organizations"; +import { deleteOrganizations, getOrganizations, postOrganizations, putOrganizations } from "@/utils/api/organizations"; import { + Alert, Box, Button, Dialog, @@ -11,6 +12,7 @@ import { DialogTitle, Grid, Paper, + Snackbar, Table, TableBody, TableCell, @@ -22,28 +24,35 @@ import { } from "@mui/material"; import { ChangeEvent, useContext, useEffect, useState } from "react"; +type UpdateOrganization = { + id?: number; + name: string; +} + +const emptyUpdateOrganization = { name: "" } + export function Organizations() { const auth = useAuth(); + // Alerts + + const [showAlert, setShowAlert] = useState(''); + // functions to open/close the add/edit organization dialog const [openAddEditOrganization, setOpenAddEditOrganization] = useState(false); - const [organizationforUpdating, setOrganizationForUpdating] = useState(undefined); // when adding is undefined and updating has the organization - - const [organizationDataInDialog, setOrganizationDataInDialog] = useState(undefined); + const [organizationforUpdating, setOrganizationForUpdating] = useState(emptyUpdateOrganization); const handleClickAddOrganization = () => { setOpenAddEditOrganization(true); - setOrganizationForUpdating(undefined); - setOrganizationDataInDialog(undefined); + setOrganizationForUpdating(emptyUpdateOrganization); }; const handleClickEditOrganization = (org: OrganizationResponse) => { setOpenAddEditOrganization(true); - setOrganizationForUpdating(org); - setOrganizationDataInDialog({ name: org.name }) + setOrganizationForUpdating({ ...org }); }; const handleCloseAddEditOrganization = () => { @@ -51,26 +60,32 @@ export function Organizations() { }; const handleSaveAddEditOrganization = async () => { - if (organizationDataInDialog != undefined) { - if (organizationforUpdating == undefined) - await postOrganizations(auth.authToken, organizationDataInDialog); - else - await putOrganizations(auth.authToken, organizationforUpdating.id, organizationDataInDialog); - - loadOrganizations(); + if (organizationforUpdating.name == "") { + setShowAlert("Name must not be empty"); + throw new Error("Name must not be emptyl"); + } + if (organizationforUpdating.id == null) { + await postOrganizations(auth.authToken, { name: organizationforUpdating.name }); + } else { + await putOrganizations(auth.authToken, organizationforUpdating.id, { name: organizationforUpdating.name }); } + loadOrganizations(); + setOpenAddEditOrganization(false); }; const nameEditingHandleChange = (event: ChangeEvent) => { - setOrganizationDataInDialog({ name: event.target.value }) + setOrganizationForUpdating({ + id: organizationforUpdating.id, + name: event.target.value, + }); }; // functions to open/close the delete organization dialog const [openDeleteOrganization, setOpenDeleteOrganization] = useState(false); - const [organizationforDeleting, setOrganizationForDeleting] = useState(undefined); + const [organizationforDeleting, setOrganizationForDeleting] = useState(undefined); const handleClickDeleteOrganization = (org: OrganizationResponse) => { setOpenDeleteOrganization(true); @@ -82,12 +97,12 @@ export function Organizations() { }; const handleDeleteOrganization = async () => { - if (organizationforDeleting != undefined) - await deleteOrganizations(auth.authToken, organizationforDeleting.id); + if (organizationforDeleting != undefined) + await deleteOrganizations(auth.authToken, organizationforDeleting.id); - loadOrganizations(); - - setOpenDeleteOrganization(false); + loadOrganizations(); + + setOpenDeleteOrganization(false); }; // function to load organizations @@ -129,56 +144,61 @@ export function Organizations() { - - - - - Users - Name - - - - - - {organizations.map((organization) => ( - - - {organization.users} - - - {organization.name} - - - - - - - + {organizations.length == 0 ? + + No organizations at this moment + : + +
+ + + Users + Name + + - ))} - -
-
+ + + {organizations.map((organization) => ( + + + {organization.users} + + + {organization.name} + + + + + + + + + ))} + + + + } } {/* Add/Edit Organization Dialog */} - {organizationforUpdating == undefined ? "New Organization" : "Update Organization"} + {organizationforUpdating.id == null ? "New Organization" : "Update Organization"} - + @@ -225,5 +245,13 @@ export function Organizations() { + + {/* Alert */} + reason !== 'clickaway' && setShowAlert('')} + autoHideDuration={5000}> + {showAlert} + ; } diff --git a/server/web/src/components/Pages/Projects/Projects.tsx b/server/web/src/components/Pages/Projects/Projects.tsx new file mode 100644 index 000000000..4bef3b445 --- /dev/null +++ b/server/web/src/components/Pages/Projects/Projects.tsx @@ -0,0 +1,307 @@ +import { useAuth } from "@/state/Auth"; +import { LoadingContext } from "@/state/Loading"; +import { getOrganizations } from "@/utils/api/organizations"; +import { deleteProjects, getProjects, postProjects, putProjects } from "@/utils/api/projects"; +import { + Alert, + Autocomplete, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Grid, + Paper, + Snackbar, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography +} from "@mui/material"; +import { ChangeEvent, useContext, useEffect, useState } from "react"; + +type UpdateProject = { + id?: number; + name: string; + orgId?: number; +} + +const emptyUpdateProject = { name: "" } + +export function Projects() { + + const auth = useAuth(); + + // Alerts + + const [showAlert, setShowAlert] = useState(''); + + // functions to open/close the add/edit project dialog + + const [openAddEditProject, setOpenAddEditProject] = useState(false); + + const [projectforUpdating, setProjectForUpdating] = useState(emptyUpdateProject); + + const handleClickAddProject = () => { + setOpenAddEditProject(true); + setProjectForUpdating(emptyUpdateProject); + }; + + const handleClickEditProject = (org: ProjectResponse) => { + setOpenAddEditProject(true); + setProjectForUpdating({ ...org, orgId: org.org.id, }); + }; + + const handleCloseAddEditProject = () => { + setOpenAddEditProject(false); + }; + + const handleSaveAddEditProject = async () => { + if (projectforUpdating.name == "") { + setShowAlert("Name must not be empty"); + throw new Error("Name must not be emptyl"); + } + if (projectforUpdating.id == null) { + if (projectforUpdating.orgId == null) { + setShowAlert("Select an organization"); + throw new Error("orgId is null"); + } + await postProjects(auth.authToken, { name: projectforUpdating.name, orgId: projectforUpdating.orgId }); + } else { + await putProjects(auth.authToken, projectforUpdating.id, { name: projectforUpdating.name, orgId: projectforUpdating.orgId }); + + } + loadProjects(); + setOpenAddEditProject(false); + }; + + const nameEditingHandleChange = (event: ChangeEvent) => { + setProjectForUpdating({ + id: projectforUpdating.id, + name: event.target.value, + orgId: projectforUpdating.orgId + }) + }; + + const orgEditingHandleChange = (name: String) => { + const org = organizations.find((org) => org.name == name) + if (org != undefined) { + setProjectForUpdating({ + id: projectforUpdating.id, + name: projectforUpdating.name, + orgId: org.id + }) + } + }; + + // functions to open/close the delete organization dialog + + const [openDeleteProject, setOpenDeleteProject] = useState(false); + + const [organizationforDeleting, setProjectForDeleting] = useState(undefined); + + const handleClickDeleteProject = (org: ProjectResponse) => { + setOpenDeleteProject(true); + setProjectForDeleting(org); + }; + + const handleCloseDeleteProject = () => { + setOpenDeleteProject(false); + }; + + const handleDeleteProject = async () => { + if (organizationforDeleting != undefined) + await deleteProjects(auth.authToken, organizationforDeleting.id); + + loadProjects(); + + setOpenDeleteProject(false); + }; + + // list of organizations + + const [organizations, setOrganizations] = useState([]); + + async function loadOrganizations() { + const response = await getOrganizations(auth.authToken); + setOrganizations(response); + } + + useEffect(() => { + loadOrganizations() + }, []); + + // function to load projects + + const [loading, setLoading] = useContext(LoadingContext); + + const [projects, setProjects] = useState([]); + + async function loadProjects() { + setLoading(true); + const response = await getProjects(auth.authToken); + setProjects(response); + setLoading(false); + } + + useEffect(() => { + loadProjects() + }, []); + + return <> + {/* List of projects */} + {loading ? + <> + + Loading... + + : + <> + + + + Projects + + + + + + {projects.length == 0 ? + + No projects at this moment + : + + + + + Organization + Name + + + + + + {projects.map((project) => ( + + + {project.org.name} + + + {project.name} + + + + + + + + + ))} + +
+
+ } + + } + + { /* Add/Edit Project Dialog */} + + + {projectforUpdating.id == null ? "New Project" : "Update Project"} + + + { + if (newValue != null) orgEditingHandleChange(newValue); + }} + inputValue={projectforUpdating.orgId == null ? undefined : organizations.find((org) => org.id == projectforUpdating.orgId)?.name ?? undefined} + id="org" + options={organizations.map((option) => option.name)} + sx={{ + width: { xs: '100%', sm: 550 }, + }} + renderInput={(params) => } + /> + + + + + + + + + + {/* Delete Project Dialog */} + +
+ + + {"Delete Project?"} + + + + Are you sure you want to delete this project? + + + + + + + +
+ + {/* Alert */} + reason !== 'clickaway' && setShowAlert('')} + autoHideDuration={5000}> + {showAlert} + + +}; + diff --git a/server/web/src/components/Pages/Projects/index.ts b/server/web/src/components/Pages/Projects/index.ts new file mode 100644 index 000000000..e87c0056a --- /dev/null +++ b/server/web/src/components/Pages/Projects/index.ts @@ -0,0 +1 @@ +export * from './Projects'; diff --git a/server/web/src/components/Sidebar/Sidebar.tsx b/server/web/src/components/Sidebar/Sidebar.tsx index 3260c07a5..0d8e15b51 100644 --- a/server/web/src/components/Sidebar/Sidebar.tsx +++ b/server/web/src/components/Sidebar/Sidebar.tsx @@ -43,6 +43,11 @@ export function Sidebar({ drawerWidth, open }: SidebarProps) { + + + + + diff --git a/server/web/src/main.tsx b/server/web/src/main.tsx index 321305af2..3427b940e 100644 --- a/server/web/src/main.tsx +++ b/server/web/src/main.tsx @@ -20,6 +20,7 @@ import { theme } from '@/styles/theme'; import './main.css'; import { AuthProvider } from './state/Auth'; import { Login, RequireAuth } from './components/Login'; +import { Projects } from './components/Pages/Projects'; const router = createBrowserRouter([ { @@ -53,6 +54,14 @@ const router = createBrowserRouter([ ), }, + { + path: 'projects', + element: ( + + + + ), + }, { path: '2', element: ( diff --git a/server/web/src/utils/api/config.ts b/server/web/src/utils/api/config.ts index 4c5716b25..6979a9e2a 100644 --- a/server/web/src/utils/api/config.ts +++ b/server/web/src/utils/api/config.ts @@ -26,6 +26,7 @@ export enum EndpointsEnum { login = 'login', register = 'register', organization = 'v1/settings/org', + Projects = 'v1/settings/projects', } export type EndpointsTypes = { diff --git a/server/web/src/utils/api/organizations.ts b/server/web/src/utils/api/organizations.ts index d788139b8..7598f281a 100644 --- a/server/web/src/utils/api/organizations.ts +++ b/server/web/src/utils/api/organizations.ts @@ -43,7 +43,7 @@ export async function postOrganizations(authToken: string, { const createOrganizationApiConfig = apiConfigConstructor( createOrganizationApiOptions, ); - const createOrganizationResponse = await apiFetch( + const createOrganizationResponse = await apiFetch( createOrganizationApiConfig, ); @@ -104,7 +104,7 @@ export async function putOrganizations(authToken: string, id: number, { const putOrganizationApiConfig = apiConfigConstructor( putOrganizationApiOptions, ); - const putOrganizationResponse = await apiFetch( + const putOrganizationResponse = await apiFetch( putOrganizationApiConfig, ); @@ -130,7 +130,7 @@ export async function deleteOrganizations(authToken: string, id: number): Promis const deleteOrganizationApiConfig = apiConfigConstructor( deleteOrganizationApiOptions, ); - const deleteOrganizationResponse = await apiFetch( + const deleteOrganizationResponse = await apiFetch( deleteOrganizationApiConfig, ); diff --git a/server/web/src/utils/api/projects.ts b/server/web/src/utils/api/projects.ts new file mode 100644 index 000000000..380dd7b3e --- /dev/null +++ b/server/web/src/utils/api/projects.ts @@ -0,0 +1,147 @@ +import { + ApiOptions, + EndpointsEnum, + apiConfigConstructor, + apiFetch, + baseHeaders, + defaultApiServer, +} from '@/utils/api'; + +// Create Project Endpoint + +const projectApiBaseOptions: ApiOptions = { + endpointServer: defaultApiServer, + endpointPath: EndpointsEnum.Projects, + endpointValue: '', + requestOptions: { + headers: baseHeaders, + }, +}; + +export type PostProjectProps = { + name: string; + orgId: number; +}; + +export async function postProjects(authToken: string, { + name, + orgId, +}: PostProjectProps): Promise { + const createProjectRequest: ProjectRequest = { + name: name, + orgId: orgId, + }; + const createProjectApiOptions: ApiOptions = { + ...projectApiBaseOptions, + body: JSON.stringify(createProjectRequest), + requestOptions: { + method: 'POST', + ...projectApiBaseOptions.requestOptions, + headers: { + ...projectApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const createProjectApiConfig = apiConfigConstructor( + createProjectApiOptions, + ); + const createProjectResponse = await apiFetch( + createProjectApiConfig, + ); + + return createProjectResponse.status; +} + +// Get Projects Endpoint + +export async function getProjects(authToken: string): Promise { + const getProjectApiOptions: ApiOptions = { + ...projectApiBaseOptions, + requestOptions: { + method: 'GET', + ...projectApiBaseOptions.requestOptions, + headers: { + ...projectApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const getProjectApiConfig = apiConfigConstructor( + getProjectApiOptions, + ); + + const projectsResponse = await apiFetch( + getProjectApiConfig, + ); + + if (projectsResponse.data == null) { + throw new Error('Project data is null'); + } + + return projectsResponse.data!; +} + +// put project endpoint + +export type PutProjectProps = { + name: string; + orgId?: number; +}; + +export async function putProjects(authToken: string, id: number, { + name, + orgId, +}: PutProjectProps): Promise { + const putProjectRequest: PutProjectRequest = { + name: name, + orgId: orgId, + }; + const putProjectApiOptions: ApiOptions = { + ...projectApiBaseOptions, + endpointValue: `/${id}`, + body: JSON.stringify(putProjectRequest), + requestOptions: { + method: 'PUT', + ...projectApiBaseOptions.requestOptions, + headers: { + ...projectApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const putProjectApiConfig = apiConfigConstructor( + putProjectApiOptions, + ); + const putProjectResponse = await apiFetch( + putProjectApiConfig, + ); + + return putProjectResponse.status; +} + + +// delete project endpoint + +export async function deleteProjects(authToken: string, id: number): Promise { + const deleteProjectApiOptions: ApiOptions = { + ...projectApiBaseOptions, + endpointValue: `/${id}`, + requestOptions: { + method: 'DELETE', + ...projectApiBaseOptions.requestOptions, + headers: { + ...projectApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const deleteProjectApiConfig = apiConfigConstructor( + deleteProjectApiOptions, + ); + const deleteProjectResponse = await apiFetch( + deleteProjectApiConfig, + ); + + return deleteProjectResponse.status; +} diff --git a/server/web/src/utils/models.ts b/server/web/src/utils/models.ts index c1d8f967c..49b73353a 100644 --- a/server/web/src/utils/models.ts +++ b/server/web/src/utils/models.ts @@ -22,3 +22,19 @@ type OrganizationResponse = { name: string; users: number; } + +type ProjectRequest = { + name: string; + orgId: number; +} + +type PutProjectRequest = { + name: string; + orgId?: number; +} + +type ProjectResponse = { + id: number; + name: string; + org: OrganizationResponse; +}