-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
task/WG-237-Delete-Project-Modal-React #273
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { | ||
Project, | ||
DesignSafeProject, | ||
DesignSafeProjectCollection, | ||
ProjectRequest, | ||
} from '../types'; | ||
|
||
export const projectMock: Project = { | ||
id: 1, | ||
uuid: 'abc123', | ||
name: 'Sample Project', | ||
description: 'A sample project for testing purposes.', | ||
public: true, | ||
system_file: 'sample-file', | ||
system_id: 'sample-id', | ||
system_path: '/path/to/sample', | ||
deletable: true, | ||
streetview_instances: null, | ||
ds_project: { | ||
uuid: 'proj-uuid', | ||
projectId: 'proj-id', | ||
title: 'Sample DesignSafe Project', | ||
value: { | ||
dois: [], | ||
coPis: [], | ||
title: 'Hazmapper V3 PROD Map Test 2024.08.07', | ||
users: [ | ||
{ | ||
inst: 'University of Texas at Austin (utexas.edu)', | ||
role: 'pi', | ||
email: '[email protected]', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make these generic/fake users. |
||
fname: 'John', | ||
lname: 'Gentle', | ||
username: 'jgentle', | ||
}, | ||
{ | ||
inst: 'University of Texas at Austin (utexas.edu)', | ||
role: 'co_pi', | ||
email: '[email protected]', | ||
fname: 'Sophia', | ||
lname: 'Massie-Perez', | ||
username: 'smassie', | ||
}, | ||
], | ||
authors: [], | ||
frTypes: [], | ||
nhEvent: '', | ||
nhTypes: [], | ||
fileObjs: [], | ||
fileTags: [], | ||
keywords: [], | ||
nhEvents: [], | ||
dataTypes: [], | ||
projectId: 'PRJ-5566', | ||
tombstone: false, | ||
facilities: [], | ||
nhLatitude: '', | ||
nhLocation: '', | ||
description: | ||
'Hazmapper V3 PROD Map Test 2024.08.07 description required.', | ||
nhLongitude: '', | ||
projectType: 'None', | ||
teamMembers: [], | ||
awardNumbers: [], | ||
guestMembers: [], | ||
hazmapperMaps: [ | ||
{ | ||
name: 'v3_PROD_Hazmapper_2024-08-07_TestProject', | ||
path: '/', | ||
uuid: '620aeaf4-f813-4b90-ba52-bc87cfa7b07b', | ||
deployment: 'production', | ||
}, | ||
], | ||
referencedData: [], | ||
associatedProjects: [], | ||
}, | ||
}, | ||
}; | ||
|
||
export const designSafeProjectMock: DesignSafeProject = { | ||
uuid: 'proj-uuid', | ||
projectId: 'proj-id', | ||
title: 'Sample DesignSafe Project', | ||
value: {}, | ||
}; | ||
|
||
export const designSafeProjectCollectionMock: DesignSafeProjectCollection = { | ||
result: [designSafeProjectMock], | ||
}; | ||
|
||
export const projectRequestMock: ProjectRequest = { | ||
name: 'New Project Request', | ||
description: 'A description for the new project request.', | ||
public: true, | ||
system_file: 'new-project-file', | ||
system_id: 'new-system-id', | ||
system_path: '/path/to/new-project', | ||
watch_content: true, | ||
watch_users: false, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.root { | ||
display: flex; | ||
flex-direction: column; | ||
width: 100%; | ||
height: 200px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import React from 'react'; | ||
import { | ||
render, | ||
cleanup, | ||
fireEvent, | ||
screen, | ||
waitFor, | ||
} from '@testing-library/react'; | ||
import { BrowserRouter as Router } from 'react-router-dom'; | ||
import { act } from 'react-dom/test-utils'; | ||
import { QueryClient, QueryClientProvider } from 'react-query'; | ||
import DeleteMapModal from './DeleteMapModal'; | ||
import { Provider } from 'react-redux'; | ||
import store from '../../redux/store'; | ||
import { projectMock } from '../../__fixtures__/projectFixtures'; | ||
|
||
jest.mock('../../hooks/projects/useProjects', () => ({ | ||
__esModule: true, | ||
useDeleteProject: (projectId) => ({ | ||
mutate: jest.fn((data, { onSuccess, onError }) => { | ||
if (projectId === 404) { | ||
onError({ response: { status: 404 } }); | ||
} else { | ||
onSuccess(); | ||
} | ||
}), | ||
isLoading: false, | ||
}), | ||
})); | ||
const mockNavigate = jest.fn(); | ||
jest.mock('react-router-dom', () => ({ | ||
...jest.requireActual('react-router-dom'), | ||
useNavigate: () => mockNavigate, | ||
})); | ||
|
||
const toggleMock = jest.fn(); | ||
const queryClient = new QueryClient(); | ||
|
||
const renderComponent = async ( | ||
projectId = 123, | ||
projectName = 'Sample Project' | ||
) => { | ||
await act(async () => { | ||
render( | ||
<Provider store={store}> | ||
<QueryClientProvider client={queryClient}> | ||
<Router> | ||
<DeleteMapModal | ||
isOpen={true} | ||
toggle={toggleMock} | ||
projectId={projectMock.id} | ||
project={projectMock} | ||
/> | ||
</Router> | ||
</QueryClientProvider> | ||
</Provider> | ||
); | ||
}); | ||
}; | ||
|
||
describe('DeleteMapModal', () => { | ||
afterEach(() => { | ||
cleanup(); | ||
}); | ||
|
||
test('renders the modal when open', async () => { | ||
await renderComponent(); | ||
await waitFor(() => { | ||
expect(screen.getByText('Delete Map: Sample Project')).toBeTruthy(); | ||
}); | ||
}); | ||
|
||
test('successfully deletes a project', async () => { | ||
await renderComponent(123, 'Sample Project'); | ||
|
||
await act(async () => { | ||
fireEvent.click(screen.getByRole('button', { name: /Delete/ })); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(toggleMock).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,77 @@ | ||||||
import React, { useState } from 'react'; | ||||||
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; | ||||||
import { Button, SectionMessage } from '../../core-components'; | ||||||
import styles from './DeleteMapModal.module.css'; | ||||||
import { Project } from '../../types'; | ||||||
import { useDeleteProject } from '../../hooks/projects/'; | ||||||
|
||||||
type DeleteMapModalProps = { | ||||||
isOpen: boolean; | ||||||
toggle: () => void; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
projectId?: number; | ||||||
project?: Project; | ||||||
}; | ||||||
|
||||||
const DeleteMapModal = ({ | ||||||
isOpen, | ||||||
toggle: parentToggle, | ||||||
projectId, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could |
||||||
project, | ||||||
}: DeleteMapModalProps) => { | ||||||
const [errorMessage, setErrorMessage] = useState(''); | ||||||
const { mutate: deleteProject, isLoading: isDeletingProject } = | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
suggestions: could |
||||||
useDeleteProject(projectId); | ||||||
const handleClose = () => { | ||||||
setErrorMessage(''); // Clear the error message | ||||||
parentToggle(); // Call the original toggle function passed as a prop | ||||||
}; | ||||||
|
||||||
const handleDeleteProject = () => { | ||||||
deleteProject(undefined, { | ||||||
onSuccess: () => { | ||||||
handleClose(); | ||||||
}, | ||||||
onError: () => { | ||||||
setErrorMessage('There was an error deleting your project.'); | ||||||
}, | ||||||
}); | ||||||
}; | ||||||
|
||||||
return ( | ||||||
<Modal isOpen={isOpen} toggle={handleClose} className={styles.root}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: what about making modal bigger. |
||||||
<ModalHeader toggle={handleClose}> | ||||||
Delete Map: {project?.name}{' '} | ||||||
</ModalHeader> | ||||||
<ModalBody> | ||||||
{project?.deletable | ||||||
? 'Are you sure you want to delete this map? All associated features, metadata, and saved files will be deleted. THIS CANNOT BE UNDONE.' | ||||||
: "This map is not able to be deleted either because the map is public or because you don't have permission."} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to angular version, can we avoid opening the modal if the map isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do want the text |
||||||
</ModalBody> | ||||||
<ModalFooter className="justify-content-start"> | ||||||
<Button size="short" type="secondary" onClick={handleClose}> | ||||||
Cancel | ||||||
</Button> | ||||||
{project?.deletable ? ( | ||||||
<Button | ||||||
size="short" | ||||||
type="primary" | ||||||
attr="submit" | ||||||
isLoading={isDeletingProject} | ||||||
onClick={handleDeleteProject} | ||||||
> | ||||||
Delete | ||||||
</Button> | ||||||
) : ( | ||||||
<Button size="short" type="primary" attr="submit" disabled> | ||||||
Delete | ||||||
</Button> | ||||||
)} | ||||||
{errorMessage && ( | ||||||
<SectionMessage type="error">{errorMessage}</SectionMessage> | ||||||
)} | ||||||
</ModalFooter> | ||||||
</Modal> | ||||||
); | ||||||
}; | ||||||
|
||||||
export default DeleteMapModal; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,10 +2,14 @@ import React, { useState } from 'react'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||
import { useProjectsWithDesignSafeInformation } from '../../hooks'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Button, LoadingSpinner, Icon } from '../../core-components'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import CreateMapModal from '../CreateMapModal/CreateMapModal'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import DeleteMapModal from '../DeleteMapModal/DeleteMapModal'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Project } from '../../types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { useNavigate } from 'react-router-dom'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const ProjectListing: React.FC = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const [isModalOpen, setIsModalOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const [selectedProjectForDeletion, setSelectedProjectForDeletion] = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
useState<Project | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
const navigate = useNavigate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const navigateToProject = (projectId) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -16,6 +20,10 @@ export const ProjectListing: React.FC = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
setIsModalOpen(!isModalOpen); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const toggleDeleteModal = (project: Project | null) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
setSelectedProjectForDeletion(project); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { data, isLoading, isError } = useProjectsWithDesignSafeInformation(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (isLoading) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -43,24 +51,33 @@ export const ProjectListing: React.FC = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
</thead> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<tbody> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{data?.map((proj) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<tr key={proj.id} onClick={() => navigateToProject(proj.uuid)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<td>{proj.name}</td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<tr key={proj.id}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<td onClick={() => navigateToProject(proj.uuid)}>{proj.name}</td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<td onClick={() => navigateToProject(proj.uuid)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{proj.ds_project?.value.projectId}{' '} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
{proj.ds_project?.value.title} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Icon name="edit-document"></Icon> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Button onClick={() => toggleDeleteModal(proj)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Button's |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Icon name="trash"></Icon> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</tr> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</tbody> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</table> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
{selectedProjectForDeletion && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
<DeleteMapModal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
isOpen={!!selectedProjectForDeletion} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
toggle={() => toggleDeleteModal(null)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+64
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
projectId={selectedProjectForDeletion.id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
project={selectedProjectForDeletion} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
</> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export { | ||
useDeleteProject, | ||
useProjectsWithDesignSafeInformation, | ||
useProjects, | ||
useDsProjects, | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,7 +1,7 @@ | ||||||
import { UseQueryResult } from 'react-query'; | ||||||
import { UseQueryResult, useQueryClient } from 'react-query'; | ||||||
import { useMemo } from 'react'; | ||||||
import { Project, DesignSafeProjectCollection, ApiService } from '../../types'; | ||||||
import { useGet } from '../../requests'; | ||||||
import { useGet, useDelete } from '../../requests'; | ||||||
|
||||||
export const useProjects = (): UseQueryResult<Project[]> => { | ||||||
const query = useGet<Project[]>({ | ||||||
|
@@ -75,3 +75,17 @@ export function useProjectsWithDesignSafeInformation(): UseQueryResult< | |||||
error: dsProjectQuery.error || projectQuery.error, | ||||||
} as UseQueryResult<Project[]>; | ||||||
} | ||||||
|
||||||
export const useDeleteProject = (projectId: number | undefined) => { | ||||||
const queryClient = useQueryClient(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
this could be changed if we fix the issue with optional members in #273 (comment) (and then some other changes in its usage). |
||||||
const endpoint = `/projects/${projectId}/`; | ||||||
return useDelete<void>({ | ||||||
endpoint, | ||||||
apiService: ApiService.Geoapi, | ||||||
options: { | ||||||
onSuccess: () => { | ||||||
queryClient.invalidateQueries('projects'); | ||||||
}, | ||||||
}, | ||||||
}); | ||||||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯