Skip to content

Commit

Permalink
Task/wg 243 manage project (#313)
Browse files Browse the repository at this point in the history
* Use large panel for map project instead of modal

* task/WG-243-manage-project
Map Tab complete. Other tabs WIP

* - Adds taggit button to Map tab

* - Adds all tabs, notifications for success,
updates hook, manages hook state on the main panel

* - adds key to table rows
- adjusts paginator to fit in panel
- removes debug statement in members tab

* - Disables trash on project listing if not deletable
- Removes reset so that notifications on map don't bounce on state reset

* - Adds test for manage ManageMapProjectPanel

* - After merging main, MapProject had this
conditional excluding the manage panel sneak back in

* - Adds option to hook prevent showing members if
map is not associated with DS project

* - Fixes hook to include projectID
- Adds option to user hook to only get if map is associated with
DS project

* Update react/src/hooks/projects/useProjects.ts

Co-authored-by: Nathan Franklin <nathan.franklin@gmail.com>

* - Addresses requested changes
- Adds links to DesignSafe modeled after angular in the save tab of manage panel

* - Adds better state handling when deleting a project
- Replaces the way public tab gets url

* - Adds suggested patch

* - Moves notifications to bottom left like DS

* Change replacement approach (#319)

---------

Co-authored-by: Nathan Franklin <nathan.franklin@gmail.com>
Co-authored-by: Sal Tijerina <r.sal.tijerina@gmail.com>
  • Loading branch information
3 people authored Feb 12, 2025
1 parent c24280c commit f1b860c
Show file tree
Hide file tree
Showing 20 changed files with 733 additions and 62 deletions.
10 changes: 0 additions & 10 deletions react/src/components/DeleteMapModal/DeleteMapModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,5 @@ describe('DeleteMapModal', () => {
);
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();
});
});
});
31 changes: 25 additions & 6 deletions react/src/components/DeleteMapModal/DeleteMapModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { Button, SectionMessage } from '@tacc/core-components';
import { useNavigate, useLocation } from 'react-router-dom';
import { Project } from '../../types';
import { useDeleteProject } from '../../hooks/projects/';

Expand All @@ -15,18 +16,41 @@ const DeleteMapModal = ({
close: parentToggle,
project,
}: DeleteMapModalProps) => {
const navigate = useNavigate();
const location = useLocation();
const {
mutate: deleteProject,
isPending: isDeletingProject,
isError,
isSuccess,
} = useDeleteProject();

const handleClose = () => {
parentToggle();
};

const handleDeleteProject = () => {
deleteProject({ projectId: project.id });
deleteProject(
{ projectId: project.id },
{
onSuccess: () => {
parentToggle();
if (location.pathname.includes(`/project/${project.uuid}`)) {
// If on project page, navigate home with success state
navigate('/', {
replace: true,
state: { onSuccess: true },
});
} else {
// If not on project page, just navigate to current location with success state
navigate(location, {
replace: true,
state: { onSuccess: true },
});
}
},
}
);
};

return (
Expand Down Expand Up @@ -63,11 +87,6 @@ const DeleteMapModal = ({
>
Delete
</Button>
{isSuccess && (
<SectionMessage type="success">
{'Succesfully deleted the map.'}
</SectionMessage>
)}
{isError && (
<SectionMessage type="error">
{'There was an error deleting your map.'}
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion react/src/components/ManageMapProjectModal/index.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { screen, fireEvent } from '@testing-library/react';
import ManageMapProjectPanel from './ManageMapProjectPanel';
import { projectMock } from '@hazmapper/__fixtures__/projectFixtures';
import { renderInTest, testQueryClient } from '@hazmapper/test/testUtil';

// Mock the child components
jest.mock('./MapTabContent', () => {
return function MockMapTabContent() {
return <div data-testid="map-tab-content">Map Tab Content</div>;
};
});

jest.mock('./MembersTabContent', () => {
return function MockMembersTabContent() {
return <div data-testid="members-tab-content">Members Tab Content</div>;
};
});

jest.mock('./PublicTabContent', () => {
return function MockPublicTabContent() {
return <div data-testid="public-tab-content">Public Tab Content</div>;
};
});

jest.mock('./SaveTabContent', () => {
return function MockSaveTabContent() {
return <div data-testid="save-tab-content">Save Tab Content</div>;
};
});

describe('ManageMapProjectPanel', () => {
const defaultProps = {
project: projectMock,
onProjectUpdate: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
testQueryClient.clear();
});

it('renders all tab buttons', () => {
renderInTest(<ManageMapProjectPanel {...defaultProps} />);

expect(screen.getByRole('tab', { name: 'Map' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Members' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Public' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Save' })).toBeTruthy();
});

it('renders map tab content by default', () => {
renderInTest(<ManageMapProjectPanel {...defaultProps} />);

expect(screen.getByTestId('map-tab-content')).toBeTruthy();
});
it('switches between tabs correctly', () => {
renderInTest(<ManageMapProjectPanel {...defaultProps} />);

// Click Members tab
const membersTab = screen.getByRole('tab', { name: 'Members' });
fireEvent.click(membersTab);
expect(screen.getByTestId('members-tab-content')).toBeTruthy();

// Click Public tab
const publicTab = screen.getByRole('tab', { name: 'Public' });
fireEvent.click(publicTab);
expect(screen.getByTestId('public-tab-content')).toBeTruthy();

// Click Save tab
const saveTab = screen.getByRole('tab', { name: 'Save' });
fireEvent.click(saveTab);
expect(screen.getByTestId('save-tab-content')).toBeTruthy();
});
});
115 changes: 115 additions & 0 deletions react/src/components/ManageMapProjectPanel/ManageMapProjectPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useState } from 'react';
import styles from './ManageMapProjectPanel.module.css';
import type { TabsProps } from 'antd';
import { Flex, Tabs, notification, Card } from 'antd';
import { Project, ProjectRequest } from '@hazmapper/types';
import MapTabContent from './MapTabContent';
import MembersTabContent from './MembersTabContent';
import PublicTabContent from './PublicTabContent';
import { useUpdateProjectInfo } from '@hazmapper/hooks';
import SaveTabContent from './SaveTabContent';

interface ManageMapProjectModalProps {
project: Project;
}

const ManageMapProjectPanel: React.FC<ManageMapProjectModalProps> = ({
project: activeProject,
}) => {
const [activeKey, setActiveKey] = useState('1');
const [updateApi, contextHolder] = notification.useNotification();

const { mutate, isPending } = useUpdateProjectInfo();

const handleProjectUpdate = (updateData: Partial<ProjectRequest>) => {
const newData: ProjectRequest = {
name: updateData.name ?? activeProject.name,
description: updateData.description ?? activeProject.description,
public: updateData.public ?? activeProject.public,
};

mutate(newData, {
onSuccess: () => {
updateApi.open({
type: 'success',
message: 'Success!',
description: 'Your project was successfully updated.',
placement: 'bottomLeft',
closable: false,
});
},
onError: () => {
updateApi.open({
type: 'error',
message: 'Error!',
description: 'There was an error updating your project.',
placement: 'bottomLeft',
closable: false,
});
},
});
};

const items: TabsProps['items'] = [
{
label: 'Map',
key: '1',
children: (
<Card title="Map Details" type="inner">
<MapTabContent
project={activeProject}
onProjectUpdate={handleProjectUpdate}
isPending={isPending}
/>
</Card>
),
},
{
label: 'Members',
key: '2',
children: (
<Card title="Members" type="inner">
<MembersTabContent project={activeProject} />
</Card>
),
},
{
label: 'Public',
key: '3',
children: (
<Card type="inner" title="Public Access">
<PublicTabContent
project={activeProject}
onProjectUpdate={handleProjectUpdate}
isPending={isPending}
/>
</Card>
),
},
{
label: 'Save',
key: '4',
children: (
<Card type="inner" title="Save Location">
<SaveTabContent project={activeProject}></SaveTabContent>
</Card>
),
},
];

return (
<Flex vertical className={styles.root}>
{contextHolder}
<Tabs
type="card"
size="small"
activeKey={activeKey}
onChange={setActiveKey}
items={items}
style={{ marginTop: 20, marginLeft: 30, marginRight: 30 }}
/>
</Flex>
);
};

export default ManageMapProjectPanel;
Loading

0 comments on commit f1b860c

Please sign in to comment.