diff --git a/frontend/src/components/Project/ProjectEditPage.tsx b/frontend/src/components/Project/ProjectEditPage.tsx deleted file mode 100644 index 1fd2136e9..000000000 --- a/frontend/src/components/Project/ProjectEditPage.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useEffect, useState } from 'react' -import { useNavigate, useParams } from 'react-router-dom' -import { Button, CircularProgress, Stack, Typography } from '@mui/material' - -import { PermissionDenied } from '@/components/PermissionDenied' -import { ProjectForm, type ProjectFormValues } from '@/components/Project/ProjectForm' -import { UnsavedChangesProvider } from '@/components/UnsavedChangesProvider' -import { useNotify } from '@/hooks/notification' -import { useProject } from '@/hooks/useProject' -import { useUser } from '@/hooks/user' -import { useUpdateProjectMutation } from '@/redux/projectReducer' -import { Role } from '@/shared/types' -import type { FetchBaseQueryError } from '@reduxjs/toolkit/query' - -export const ProjectEditPage = () => { - const { id } = useParams() - const projectId = id ? Number(id) : null - const user = useUser() - const navigate = useNavigate() - const { notify } = useNotify() - const { project, initialValues, users, isLoading, isError, refetch } = useProject(projectId) - const [updateProject, { isLoading: isUpdating }] = useUpdateProjectMutation() - const [submitError, setSubmitError] = useState(null) - - useEffect(() => { - if (project) { - document.title = `Edit project - ${project.proj_name ?? project.pid}` - } - }, [project]) - - if (!user.token) { - return ( - - ) - } - - if (user.role !== Role.Admin) { - return ( - - ) - } - - if (isLoading || !initialValues) { - return ( - - - Loading project... - - ) - } - - if (isError || !project || !initialValues) { - return ( - - - - - ) - } - - const handleSubmit = async (values: ProjectFormValues) => { - if (!projectId) return - setSubmitError(null) - - try { - await updateProject({ - pid: projectId, - projectCode: values.projectCode.trim(), - projectName: values.projectName.trim(), - coordinatorUserId: values.coordinatorUserId!, - projectStatus: values.projectStatus, - recordStatus: values.recordStatus as boolean, - memberUserIds: values.memberUserIds.length ? values.memberUserIds : undefined, - }).unwrap() - - notify('Project updated successfully.') - navigate(`/project/${projectId}`) - } catch (error) { - const fetchError = error as FetchBaseQueryError - const message = - fetchError && - typeof fetchError === 'object' && - 'data' in fetchError && - fetchError.data && - typeof fetchError.data === 'object' && - 'error' in fetchError.data && - typeof fetchError.data.error === 'string' - ? fetchError.data.error - : 'Failed to update project.' - setSubmitError(message) - } - } - - return ( - - - - Edit Project - - Update project information, coordinator, and members. - - - - - ) -} - -export default ProjectEditPage diff --git a/frontend/src/pages/projects/ProjectEditPage.tsx b/frontend/src/pages/projects/ProjectEditPage.tsx deleted file mode 100644 index 70763ef04..000000000 --- a/frontend/src/pages/projects/ProjectEditPage.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ProjectEditPage as ProjectEditPageComponent } from '@/components/Project/ProjectEditPage' - -export const ProjectEditPage = () => - -export default ProjectEditPage diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index df1241934..799fb1a20 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -99,13 +99,6 @@ const router = createBrowserRouter([ return { Component: ProjectNewPage } }, }, - { - path: 'project/:id/edit', - lazy: async () => { - const { ProjectEditPage } = await import('../pages/projects/ProjectEditPage') - return { Component: ProjectEditPage } - }, - }, { path: 'email', lazy: async () => { diff --git a/frontend/src/tests/pages/ProjectEditPage.test.tsx b/frontend/src/tests/pages/ProjectEditPage.test.tsx deleted file mode 100644 index 1f95da788..000000000 --- a/frontend/src/tests/pages/ProjectEditPage.test.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import React from 'react' -import { beforeEach, describe, expect, it, jest } from '@jest/globals' -import '@testing-library/jest-dom' -import { Link, MemoryRouter, Route, RouterProvider, Routes, createMemoryRouter, useLocation } from 'react-router-dom' -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { Provider } from 'react-redux' -import { Role } from '@/shared/types' -import { ProjectEditPage } from '@/pages/projects/ProjectEditPage' -import type { ProjectFormValues } from '@/components/Project/ProjectForm' -import { store } from '@/redux/store' - -const mockUseUser = jest.fn() -const mockUseProject = jest.fn() -const mockUseUpdateProjectMutation = jest.fn() -const mockNotify = jest.fn() -const mockNavigate = jest.fn() -type UpdateProjectTrigger = (payload: ProjectFormValues) => { - unwrap: () => Promise -} -let updateProjectMock: jest.MockedFunction - -jest.mock('@/hooks/user', () => ({ - useUser: () => mockUseUser(), -})) - -jest.mock('@/hooks/useProject', () => ({ - useProject: (arg?: unknown) => mockUseProject(arg), -})) - -jest.mock('@/redux/projectReducer', () => ({ - useUpdateProjectMutation: () => mockUseUpdateProjectMutation(), -})) - -jest.mock('@/hooks/notification', () => ({ - useNotify: () => ({ notify: mockNotify }), -})) - -jest.mock('react-router-dom', () => { - const actualRouterDom = jest.requireActual('react-router-dom') - return { - ...actualRouterDom, - useNavigate: () => mockNavigate, - } -}) - -type User = { userId: number; label: string; initials: string } - -const userFactory = (overrides: Partial = {}): User => ({ - userId: 1, - label: 'Doe, Jane', - initials: 'JD', - ...overrides, -}) - -const defaultInitialValues: ProjectFormValues = { - projectCode: 'PRJ-100', - projectName: 'Existing Project', - coordinatorUserId: 1, - projectStatus: 'current', - recordStatus: true, - memberUserIds: [2], -} - -const renderWithRouter = () => { - const Wrapper = () => { - const location = useLocation() - return ( - -
{location.pathname}
- -
- ) - } - - render( - - - - } /> - - - - ) -} - -const renderWithDataRouter = () => { - const router = createMemoryRouter( - [ - { - path: '/project/:id/edit', - element: ( - - - Go elsewhere - - ), - }, - { path: '/other', element:
Other page
}, - ], - { initialEntries: ['/project/7/edit'] } - ) - - render( - - - - ) - return router -} - -describe('ProjectEditPage', () => { - beforeEach(() => { - mockUseUser.mockReturnValue({ token: 'token-123', role: Role.Admin }) - mockUseProject.mockReturnValue({ - project: { pid: 7, proj_name: 'Existing Project' }, - initialValues: defaultInitialValues, - users: [userFactory(), userFactory({ userId: 2, label: 'Smith, Alex', initials: 'AS' })], - isLoading: false, - isError: false, - refetch: jest.fn(), - }) - - const updateProjectImpl: UpdateProjectTrigger = payload => ({ - unwrap: () => Promise.resolve({ pid: 7, ...payload }), - }) - updateProjectMock = jest.fn(updateProjectImpl) as jest.MockedFunction - - mockUseUpdateProjectMutation.mockReturnValue([ - updateProjectMock as unknown as UpdateProjectTrigger, - { isLoading: false }, - ]) - mockNotify.mockReset() - mockNavigate.mockReset() - }) - - it('submits form data and navigates to the project detail page', async () => { - const user = userEvent.setup() - renderWithRouter() - - await user.clear(screen.getByLabelText('Project Name')) - await user.type(screen.getByLabelText('Project Name'), 'Updated Project') - - const form = document.querySelector('form') - expect(form).toBeTruthy() - act(() => { - fireEvent.submit(form as HTMLFormElement) - }) - - await waitFor(() => { - expect(updateProjectMock).toHaveBeenCalledWith( - expect.objectContaining({ - projectCode: 'PRJ-100', - projectName: 'Updated Project', - coordinatorUserId: 1, - projectStatus: 'current', - recordStatus: true, - memberUserIds: [2], - }) - ) - }) - - await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith('/project/7') - }) - }) - - it('shows a prompt when attempting to leave a dirty form and stays on cancel', async () => { - const user = userEvent.setup() - const router = renderWithDataRouter() - - await user.type(screen.getByLabelText('Project Name'), ' Updated') - - await user.click(screen.getByRole('link', { name: /go elsewhere/i })) - - await screen.findByRole('dialog') - await user.click(screen.getByRole('button', { name: /stay on page/i })) - - expect(router.state.location.pathname).toBe('/project/7/edit') - }) - - it('allows navigation when the user confirms leaving a dirty form', async () => { - const user = userEvent.setup() - const router = renderWithDataRouter() - - await user.type(screen.getByLabelText('Project Code'), ' UPDATED') - await user.click(screen.getByRole('link', { name: /go elsewhere/i })) - - await user.click(await screen.findByRole('button', { name: /leave page/i })) - - await waitFor(() => { - expect(router.state.location.pathname).toBe('/other') - }) - }) - - it('navigates without prompting when the form is clean', async () => { - const user = userEvent.setup() - const router = renderWithDataRouter() - - await user.click(screen.getByRole('link', { name: /go elsewhere/i })) - - await waitFor(() => { - expect(router.state.location.pathname).toBe('/other') - }) - }) -})