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-238: follow on #281

Merged
merged 6 commits into from
Nov 15, 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
5 changes: 3 additions & 2 deletions react/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ export function shouldIgnoreError(args: any[]): boolean {

beforeAll(() => {
// Establish mocking of APIs before all tests
server.listen({ onUnhandledRequest: 'error' });

server.listen({
onUnhandledRequest: 'error',
});
const originalError = console.error;

jest.spyOn(console, 'error').mockImplementation((...args: any[]) => {
Expand Down
5 changes: 4 additions & 1 deletion react/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ function AppRouter() {
</ProtectedRoute>
}
/>
<Route path={ROUTES.PUBLIC_PROJECT} element={<MapProject isPublic />} />
<Route
path={ROUTES.PUBLIC_PROJECT}
element={<MapProject isPublicView />}
/>
<Route path={ROUTES.CALLBACK} element={<Callback />} />
<Route
path={ROUTES.STREETVIEW_CALLBACK}
Expand Down
5 changes: 3 additions & 2 deletions react/src/components/AssetsPanel/AssetsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { act } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import AssetsPanel from './AssetsPanel';
import { featureCollection } from '@hazmapper/__fixtures__/featuresFixture';
import { projectMock } from '@hazmapper/__fixtures__/projectFixtures';
import { useFeatures } from '@hazmapper/hooks';

jest.mock('@hazmapper/hooks', () => ({
Expand All @@ -18,8 +19,8 @@ jest.mock('@hazmapper/components/FeatureFileTree', () => {
describe('AssetsPanel', () => {
const defaultProps = {
featureCollection,
projectId: 1,
isPublic: false,
project: projectMock,
isPublicView: false,
};

beforeEach(() => {
Expand Down
38 changes: 21 additions & 17 deletions react/src/components/AssetsPanel/AssetsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import React from 'react';
import styles from './AssetsPanel.module.css';
import FeatureFileTree from '@hazmapper/components/FeatureFileTree';
import { FeatureCollection } from '@hazmapper/types';
import { FeatureCollection, Project } from '@hazmapper/types';
import { Button } from '@tacc/core-components';
import { useFeatures } from '@hazmapper/hooks';

const getFilename = (projectName: string) => {
// Convert to lowercase filename based on projectName
const sanitizedString = projectName.toLowerCase().replace(/[^a-z0-9]/g, '_');
return `${sanitizedString}.json`;
};

interface DownloadFeaturesButtonProps {
projectId: number;
isPublic: boolean;
project: Project;
}

const DownloadFeaturesButton: React.FC<DownloadFeaturesButtonProps> = ({
projectId,
isPublic,
project,
}) => {
const { isLoading: isDownloading, refetch: triggerDownload } = useFeatures({
projectId,
isPublic,
projectId: project.id,
isPublicView: project.public,
assetTypes: [], // Empty array to get all features
options: {
enabled: false, // Only fetch when triggered by user clicking button
Expand All @@ -31,7 +35,7 @@ const DownloadFeaturesButton: React.FC<DownloadFeaturesButtonProps> = ({
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `hazmapper.json`;
link.download = getFilename(project.name);

document.body.appendChild(link);
link.click();
Expand All @@ -56,23 +60,23 @@ interface Props {
featureCollection: FeatureCollection;

/**
* Whether or not the map project is public.
* Whether or not the map project is a public view.
*/
isPublic: boolean;
isPublicView: boolean;

/**
* active project id
* active project
*/
projectId: number;
project: Project;
}

/**
* A panel component that displays info on feature assets
*/
const AssetsPanel: React.FC<Props> = ({
isPublic,
isPublicView,
featureCollection,
projectId,
project,
}) => {
return (
<div className={styles.root}>
Expand All @@ -81,13 +85,13 @@ const AssetsPanel: React.FC<Props> = ({
</div>
<div className={styles.middleSection}>
<FeatureFileTree
projectId={projectId}
isPublic={isPublic}
projectId={project.id}
isPublicView={isPublicView}
featureCollection={featureCollection}
/>
</div>
<div className={styles.bottomSection}>
<DownloadFeaturesButton projectId={projectId} isPublic={isPublic} />
<DownloadFeaturesButton project={project} />
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions react/src/components/DeleteMapModal/DeleteMapModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ const DeleteMapModal = ({
isLoading: isDeletingProject,
isError,
isSuccess,
} = useDeleteProject(project.id);
} = useDeleteProject();
const handleClose = () => {
parentToggle();
};

const handleDeleteProject = () => {
deleteProject(undefined, {});
deleteProject({ projectId: project.id });
};

return (
Expand Down
71 changes: 31 additions & 40 deletions react/src/components/FeatureFileTree/FeatureFileTree.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import FeatureFileTree from './FeatureFileTree';
import { server, renderInTest } from '@hazmapper/test/testUtil';
import { featureCollection } from '@hazmapper/__fixtures__/featuresFixture';
import { useDeleteFeature } from '@hazmapper/hooks';

// Mock the hooks
jest.mock('@hazmapper/hooks', () => ({
useDeleteFeature: jest.fn(),
}));
import { testDevConfiguration } from '@hazmapper/__fixtures__/appConfigurationFixture';

jest.mock('react-resize-detector', () => ({
useResizeDetector: () => ({
Expand All @@ -17,64 +13,59 @@ jest.mock('react-resize-detector', () => ({
}),
}));

const renderWithRouter = (ui: React.ReactElement, { route = '/' } = {}) => {
return {
...render(<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>),
};
};

describe('FeatureFileTree', () => {
const defaultProps = {
const defaultTreeProps = {
featureCollection: featureCollection,
isPublic: false,
isPublicView: false,
projectId: 1,
};

beforeEach(() => {
jest.clearAllMocks();

(useDeleteFeature as jest.Mock).mockImplementation(() => ({
mutate: jest.fn(),
isLoading: false,
}));
});

it('renders feature list correctly', () => {
const { getByText } = renderWithRouter(
<FeatureFileTree {...defaultProps} />
const { getByText } = renderInTest(
<FeatureFileTree {...defaultTreeProps} />
);

expect(getByText('foo')).toBeDefined();
expect(getByText('image1.JPG')).toBeDefined();
expect(getByText('image2.JPG')).toBeDefined();
});

it('handles feature deletion for non-public projects', () => {
const deleteFeatureMock = jest.fn();
(useDeleteFeature as jest.Mock).mockImplementation(() => ({
mutate: deleteFeatureMock,
isLoading: false,
}));
it('handles feature deletion for non-public projects', async () => {
const featureId = 1;
let wasDeleted = false;

server.use(
http.delete(
`${testDevConfiguration.geoapiUrl}/projects/${defaultTreeProps.projectId}/features/${featureId}/`,
() => {
wasDeleted = true;
return HttpResponse.json({}, { status: 200 });
}
)
);

const { getByTestId } = renderWithRouter(
<FeatureFileTree {...defaultProps} />,
{ route: '/?selectedFeature=1' }
const { getByTestId } = renderInTest(
<FeatureFileTree {...defaultTreeProps} />,
`/?selectedFeature=${featureId}`
);

// Find and click delete button (as featured is selected)
const deleteButton = getByTestId('delete-feature-button');
fireEvent.click(deleteButton);

expect(deleteFeatureMock).toHaveBeenCalledWith({
projectId: 1,
featureId: 1,
await waitFor(() => {
expect(wasDeleted).toBeTruthy();
});
});

it('does not show delete button for public projects', () => {
const { queryByTestId } = renderWithRouter(
<FeatureFileTree {...defaultProps} isPublic={true} />,
{ route: '/?selectedFeature=1' }
const { queryByTestId } = renderInTest(
<FeatureFileTree {...defaultTreeProps} isPublicView={true} />,
'/?selectedFeature=1'
);

// Verify delete button is not present
Expand All @@ -83,8 +74,8 @@ describe('FeatureFileTree', () => {
});

it('does not show delete button when no feature is selected', () => {
const { queryByTestId } = renderWithRouter(
<FeatureFileTree {...defaultProps} isPublic={true} />
const { queryByTestId } = renderInTest(
<FeatureFileTree {...defaultTreeProps} isPublicView={false} />
);

// Verify delete button is not present
Expand Down
6 changes: 3 additions & 3 deletions react/src/components/FeatureFileTree/FeatureFileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface FeatureFileTreeProps {
/**
* Whether or not the map project is public.
*/
isPublic: boolean;
isPublicView: boolean;

/**
* active project id
Expand All @@ -44,7 +44,7 @@ interface FeatureFileTreeProps {
*/
const FeatureFileTree: React.FC<FeatureFileTreeProps> = ({
featureCollection,
isPublic,
isPublicView,
projectId,
}) => {
const { mutate: deleteFeature, isLoading } = useDeleteFeature();
Expand Down Expand Up @@ -169,7 +169,7 @@ const FeatureFileTree: React.FC<FeatureFileTreeProps> = ({
<FeatureIcon featureType={featureNode.featureType} />
)}
<span className={styles.fileName}>{featureNode.name}</span>
{!isPublic && isSelected && (
{!isPublicView && isSelected && (
<Button
size="small"
type="primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import styles from './ManageMapProjectModal.module.css';
import { Modal, ModalHeader, ModalBody } from 'reactstrap';

interface ManageMapProjectModalProps {
isPublic: boolean;
isPublicView: boolean;
}

const ManageMapProjectModal: React.FC<ManageMapProjectModalProps> = ({
isPublic,
isPublicView,
}) => {
const navigate = useNavigate();
const location = useLocation();
Expand All @@ -24,7 +24,7 @@ const ManageMapProjectModal: React.FC<ManageMapProjectModalProps> = ({
<ModalHeader toggle={closeModal}>TODO</ModalHeader>
<ModalBody>
<div className={styles.root}>
Manage Map Project TODO, isPublic: {isPublic}
Manage Map Project TODO, isPublicView: {isPublicView}
</div>
</ModalBody>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('MapProjectNavBar', () => {
it('renders the public nav items for public maps', () => {
const { getByText, queryByText } = render(
<BrowserRouter>
<MapProjectNavBar isPublic={true} />
<MapProjectNavBar isPublicView={true} />
</BrowserRouter>
);
expect(getByText('Assets')).toBeDefined();
Expand All @@ -22,7 +22,7 @@ describe('MapProjectNavBar', () => {
it('renders all nav items for non-public maps', () => {
const { getByText } = render(
<BrowserRouter>
<MapProjectNavBar isPublic={false} />
<MapProjectNavBar isPublicView={false} />
</BrowserRouter>
);

Expand Down
8 changes: 5 additions & 3 deletions react/src/components/MapProjectNavBar/MapProjectNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,20 @@ const navItems: NavItem[] = [
];

interface NavBarPanelProps {
isPublic?: boolean;
isPublicView?: boolean;
}

const MapProjectNavBar: React.FC<NavBarPanelProps> = ({ isPublic = false }) => {
const MapProjectNavBar: React.FC<NavBarPanelProps> = ({
isPublicView = false,
}) => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const activePanel = queryParams.get(queryPanelKey);

return (
<div className={styles.root}>
{navItems
.filter((item) => (isPublic ? item.showWhenPublic : true))
.filter((item) => (isPublicView ? item.showWhenPublic : true))
.map((item) => {
const updatedQueryParams = new URLSearchParams(location.search);

Expand Down
4 changes: 2 additions & 2 deletions react/src/hooks/features/useDeleteFeature.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQueryClient } from 'react-query';
import { useDeleteWithParams } from '@hazmapper/requests';
import { useDelete } from '@hazmapper/requests';

type DeleteFeatureParams = {
projectId: number;
Expand All @@ -9,7 +9,7 @@ type DeleteFeatureParams = {
export function useDeleteFeature() {
const queryClient = useQueryClient();

return useDeleteWithParams<void, DeleteFeatureParams>({
return useDelete<void, DeleteFeatureParams>({
endpoint: ({ projectId, featureId }) =>
`/projects/${projectId}/features/${featureId}/`,
options: {
Expand Down
Loading
Loading