Skip to content
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

Merged
merged 16 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
sophia-massie marked this conversation as resolved.
Show resolved Hide resolved
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
Loading