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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
100 changes: 100 additions & 0 deletions react/src/__fixtures__/projectFixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

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]',
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
};
6 changes: 6 additions & 0 deletions react/src/components/DeleteMapModal/DeleteMapModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.root {
display: flex;
flex-direction: column;
width: 100%;
height: 200px;
}
84 changes: 84 additions & 0 deletions react/src/components/DeleteMapModal/DeleteMapModal.test.tsx
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,

Check failure on line 40 in react/src/components/DeleteMapModal/DeleteMapModal.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'projectId' is assigned a value but never used
projectName = 'Sample Project'

Check failure on line 41 in react/src/components/DeleteMapModal/DeleteMapModal.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'projectName' is assigned a value but never used
) => {
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();
});
});
});
77 changes: 77 additions & 0 deletions react/src/components/DeleteMapModal/DeleteMapModal.tsx
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toogle renamed to close?

projectId?: number;
project?: Project;
};

const DeleteMapModal = ({
isOpen,
toggle: parentToggle,
projectId,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could projectId be dropped if we have project?

project,
}: DeleteMapModalProps) => {
const [errorMessage, setErrorMessage] = useState('');
const { mutate: deleteProject, isLoading: isDeletingProject } =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { mutate: deleteProject, isLoading: isDeletingProject } =
const { mutate: deleteProject, isLoading: isDeletingProject, isError} =

suggestions: could isError be used. Then you could simplify things and remove const [errorMessage, setErrorMessage] = useState('');

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}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: what about making modal bigger. <Modal size="lg"

<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."}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 deletable. we can add disabled={!proj.deletable} to the delete button in ProjectListing and drop this extra message and button-conditions here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do want the text Note that this is a public map. shown if public; its additional info that they should be aware of before clicking delete. https://github.com/TACC-Cloud/hazmapper/blob/main/angular/src/app/components/main-welcome/main-welcome.component.ts#L81-L84

</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;
25 changes: 21 additions & 4 deletions react/src/components/Projects/ProjectListing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Button's iconNameBefore could be used instead of .

<Icon name="trash"></Icon>
</Button>
</td>
</tr>
))}
</tbody>
</table>

{selectedProjectForDeletion && (
<DeleteMapModal
isOpen={!!selectedProjectForDeletion}
toggle={() => toggleDeleteModal(null)}
Comment on lines +64 to +76
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleDeleteModal can be removed and just setSelectedProjectForDeletion used.

Suggested change
<Button onClick={() => toggleDeleteModal(proj)}>
<Icon name="trash"></Icon>
</Button>
</td>
</tr>
))}
</tbody>
</table>
{selectedProjectForDeletion && (
<DeleteMapModal
isOpen={!!selectedProjectForDeletion}
toggle={() => toggleDeleteModal(null)}
<Button onClick={() => setSelectedProjectForDeletion(proj)}>
<Icon name="trash"></Icon>
</Button>
</td>
</tr>
))}
</tbody>
</table>
{selectedProjectForDeletion && (
<DeleteMapModal
isOpen={!!selectedProjectForDeletion}
toggle={() => setSelectedProjectForDeletion(null)}

projectId={selectedProjectForDeletion.id}
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
18 changes: 16 additions & 2 deletions react/src/hooks/projects/useProjects.ts
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[]>({
Expand Down Expand Up @@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const useDeleteProject = (projectId: number | undefined) => {
export const useDeleteProject = (projectId: number) => {

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');
},
},
});
};
Loading
Loading