Skip to content

Commit

Permalink
task/WG-237-Delete-Project-Modal-React (#273)
Browse files Browse the repository at this point in the history
* task/WG-237-Delete-Project-Modal-React

* - Linting

* - Fix frontend tests

* - Fixing unit tests

* - Addresses commented suggestions and requested
changes

* - Updated more requested changes

* - More updates and linting

* - Added key to table rows with onClick

* - Linting

* - Addresses accesibility erros with table row links

* - Updated changes

* - Fixes unit tests - only tests UI
  • Loading branch information
sophia-massie authored Nov 9, 2024
1 parent 25f88db commit f5767b2
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 25 deletions.
99 changes: 99 additions & 0 deletions react/src/__fixtures__/projectFixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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]',
fname: 'Fixture First Name',
lname: 'Fixture Last Name',
username: 'fixture1Username',
},
{
inst: 'University of Texas at Austin (utexas.edu)',
role: 'co_pi',
email: '[email protected]',
fname: 'Tester',
lname: 'Test',
username: 'fixture2Username',
},
],
authors: [],
frTypes: [],
nhEvent: '',
nhTypes: [],
fileObjs: [],
fileTags: [],
keywords: [],
nhEvents: [],
dataTypes: [],
projectId: 'PRJ-5566',
tombstone: false,
facilities: [],
nhLatitude: '',
nhLocation: '',
description: 'Map Test description required.',
nhLongitude: '',
projectType: 'None',
teamMembers: [],
awardNumbers: [],
guestMembers: [],
hazmapperMaps: [
{
name: 'Hazmapper_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,
};
133 changes: 133 additions & 0 deletions react/src/components/DeleteMapModal/DeleteMapModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react';
import { render, screen } 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';
import { Project } from '../../types';
import { useDeleteProject } from '../../hooks/projects';

jest.mock('../../hooks/projects', () => ({
useDeleteProject: jest.fn(),
}));

const mockHookReturn = {
mutate: jest.fn(),
isLoading: false,
isError: false,
isSuccess: false,
};

const nonDeletableProjectMock: Project = {
...projectMock,
deletable: false,
name: 'Non-Deletable Project',
};

const publicProjectMock: Project = {
...projectMock,
public: true,
name: 'Public Project',
};

const toggleMock = jest.fn();
const queryClient = new QueryClient();

const renderComponent = async (projectData: Project = projectMock) => {
await act(async () => {
render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<Router>
<DeleteMapModal
isOpen={true}
close={toggleMock}
project={projectData}
/>
</Router>
</QueryClientProvider>
</Provider>
);
});
};

describe('DeleteMapModal', () => {
beforeEach(() => {
jest.clearAllMocks();
(useDeleteProject as jest.Mock).mockReturnValue(mockHookReturn);
});

describe('DeleteMapModal', () => {
beforeEach(() => {
jest.clearAllMocks();
(useDeleteProject as jest.Mock).mockReturnValue(mockHookReturn);
});

it('should display the modal with correct project name', async () => {
await renderComponent();
const titleElement = screen.getByText(`Delete Map: ${projectMock.name}`);
expect(titleElement).toBeDefined();
});

it('should show delete confirmation message for deletable projects', async () => {
await renderComponent();
const confirmMessage = screen.getByText(
/Are you sure you want to delete this map?/
);
const warningMessage = screen.getByText(/This cannot be undone./);
expect(confirmMessage).toBeDefined();
expect(warningMessage).toBeDefined();
});

it('should show permission denied message for non-deletable projects', async () => {
await renderComponent(nonDeletableProjectMock);
const deniedMessage = screen.getByText('permission to delete this map', {
exact: false,
});
expect(deniedMessage).toBeDefined();
});

it('should disable delete button for non-deletable projects', async () => {
await renderComponent(nonDeletableProjectMock);
const deleteButton = screen.getByText('Delete') as HTMLButtonElement;
expect(deleteButton.disabled).toBe(true);
});

it('should enable delete button for deletable projects', async () => {
await renderComponent();
const deleteButton = screen.getByText('Delete') as HTMLButtonElement;
expect(deleteButton.disabled).toBe(false);
});

it('should show additional warning for public projects', async () => {
await renderComponent(publicProjectMock);
const publicWarning = screen.getByText(/Note that this is a public map./);
expect(publicWarning).toBeDefined();
});

it('should show error message when isError is true', async () => {
(useDeleteProject as jest.Mock).mockReturnValue({
...mockHookReturn,
isError: true,
});
await renderComponent();
const errorMessage = screen.getByText(
'There was an error deleting your map.'
);
expect(errorMessage).toBeDefined();
});

it('should show success message when isSuccess is true', async () => {
(useDeleteProject as jest.Mock).mockReturnValue({
...mockHookReturn,
isSuccess: true,
});
await renderComponent();
const successMessage = screen.getByText('Succesfully deleted the map.');
expect(successMessage).toBeDefined();
});
});
});
81 changes: 81 additions & 0 deletions react/src/components/DeleteMapModal/DeleteMapModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { Button, SectionMessage } from '@tacc/core-components';
import { Project } from '../../types';
import { useDeleteProject } from '../../hooks/projects/';

type DeleteMapModalProps = {
isOpen: boolean;
close: () => void;
project: Project;
};

const DeleteMapModal = ({
isOpen,
close: parentToggle,
project,
}: DeleteMapModalProps) => {
const {
mutate: deleteProject,
isLoading: isDeletingProject,
isError,
isSuccess,
} = useDeleteProject(project.id);
const handleClose = () => {
parentToggle();
};

const handleDeleteProject = () => {
deleteProject(undefined, {});
};

return (
<Modal size="lg" isOpen={isOpen} toggle={handleClose}>
<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.
{project?.public && <b> Note that this is a public map. </b>}
<br />
<b>
<u>This cannot be undone.</u>
</b>
</>
) : (
'You don’t have permission to delete this map.'
)}
</ModalBody>
<ModalFooter className="justify-content-start">
<Button size="short" type="secondary" onClick={handleClose}>
{isSuccess ? 'Close' : 'Cancel'}
</Button>
<Button
size="short"
type="primary"
attr="submit"
isLoading={isDeletingProject}
onClick={handleDeleteProject}
disabled={isSuccess || !project?.deletable}
>
Delete
</Button>
{isSuccess && (
<SectionMessage type="success">
{'Succesfully deleted the map.'}
</SectionMessage>
)}
{isError && (
<SectionMessage type="error">
{'There was an error deleting your map.'}
</SectionMessage>
)}
</ModalFooter>
</Modal>
);
};

export default DeleteMapModal;
47 changes: 37 additions & 10 deletions react/src/components/Projects/ProjectListing.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React, { useState } from 'react';
import { useProjectsWithDesignSafeInformation } from '@hazmapper/hooks';
import { Button, LoadingSpinner, Icon } from '@tacc/core-components';
import { Button, LoadingSpinner } from '@tacc/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) => {
Expand Down Expand Up @@ -43,24 +47,47 @@ export const ProjectListing: React.FC = () => {
</thead>
<tbody>
{data?.map((proj) => (
<tr key={proj.id} onClick={() => navigateToProject(proj.uuid)}>
<td>{proj.name}</td>
<tr key={proj.id}>
<td>
{proj.ds_project?.value.projectId}{' '}
{proj.ds_project?.value.title}
{' '}
<Button
type="link"
onClick={() => navigateToProject(proj.uuid)}
>
{proj.name}
</Button>
</td>
<td>
<Button>
<Icon name="edit-document"></Icon>
</Button>
<Button>
<Icon name="trash"></Icon>
{' '}
<Button
type="link"
onClick={() => navigateToProject(proj.uuid)}
>
{proj.ds_project
? `${proj.ds_project?.value.projectId} |
${proj.ds_project?.value.title}`
: '---------'}
</Button>
</td>
<td>
<Button iconNameBefore="edit-document"></Button>
<Button
iconNameBefore="trash"
onClick={() => setSelectedProjectForDeletion(proj)}
></Button>
</td>
</tr>
))}
</tbody>
</table>

{selectedProjectForDeletion && (
<DeleteMapModal
isOpen={!!selectedProjectForDeletion}
close={() => setSelectedProjectForDeletion(null)}
project={selectedProjectForDeletion}
/>
)}
</>
);
};
1 change: 1 addition & 0 deletions react/src/hooks/projects/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
useDeleteProject,
useProjectsWithDesignSafeInformation,
useProjects,
useDsProjects,
Expand Down
Loading

0 comments on commit f5767b2

Please sign in to comment.