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-432 rework hooks, maps and feature tree to minimize renders #339

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8627bd8
Use just spinner on map for features
nathanfranklin Feb 13, 2025
8c41928
Merge branch 'main' into task/WG-432-rework-state-regarding-feature-c…
nathanfranklin Feb 14, 2025
0c9ab22
Merge branch 'main' into task/WG-432-rework-state-regarding-feature-c…
nathanfranklin Feb 17, 2025
8c388fa
Merge branch 'main' into task/WG-432-rework-state-regarding-feature-c…
nathanfranklin Feb 18, 2025
c98b242
Merge branch 'main' into task/WG-432-rework-state-regarding-feature-c…
nathanfranklin Feb 26, 2025
886b82b
Optimize a bit
nathanfranklin Feb 26, 2025
a14bf68
Optimize FeatureFileTree performance with component memoization
nathanfranklin Feb 26, 2025
f520932
Improve useCurrentFeatures to have a stable reference
nathanfranklin Feb 27, 2025
aad1d97
Optimize for re-renders
nathanfranklin Feb 27, 2025
55fa6d5
Optimize again for re-render
nathanfranklin Feb 27, 2025
c74c700
Refactor panel components into MapProjectPanelContent
nathanfranklin Feb 27, 2025
e44a353
Add height workaround when asset-panel is reselected
nathanfranklin Feb 27, 2025
0d9884a
Fix tests
nathanfranklin Feb 28, 2025
5f06c85
Change path for file lisings test handler
nathanfranklin Feb 28, 2025
33ae293
Exclude questionnaireBuilder.ts from test coverage calculation
nathanfranklin Feb 28, 2025
540b342
Improve height calculation
nathanfranklin Feb 28, 2025
1449f6e
Minor changes
nathanfranklin Feb 28, 2025
38be83a
Fix prettier issue
nathanfranklin Feb 28, 2025
938fcce
Fix linting
nathanfranklin Feb 28, 2025
096c9e2
Remove MapContent
nathanfranklin Feb 28, 2025
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
2 changes: 2 additions & 0 deletions react/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ module.exports = {
// Exclude secrets and other files
'!src/secret_local*ts',
'!src/types/react*.ts',
// Exclude questionaiire files as mostly 3rd party
'!src/components/QuestionnaireModal/questionnaireBuilder.ts',
// Exclude build/dist directories
'!dist/**/*',
'!build/**/*',
Expand Down
23 changes: 19 additions & 4 deletions react/src/components/AssetsPanel/AssetsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import {
createSpyHandler,
server,
renderInTest,
WithUseFeaturesComponent,
renderInTestWaitForQueries,
} from '@hazmapper/test/testUtil';
import { geoapi_project_features } from '@hazmapper/test/handlers';
import '@testing-library/jest-dom';

// Mock FeatureFileTree component since it's a complex component and tested elswhere
jest.mock('@hazmapper/components/FeatureFileTree', () => {
Expand All @@ -19,7 +22,6 @@ jest.mock('@hazmapper/components/FeatureFileTree', () => {

describe('AssetsPanel', () => {
const defaultProps = {
featureCollection,
project: projectMock,
isPublicView: false,
};
Expand Down Expand Up @@ -48,16 +50,29 @@ describe('AssetsPanel', () => {
const { handler, spy } = createSpyHandler(geoapi_project_features);
server.use(handler);

renderInTest(<AssetsPanel {...defaultProps} />);
await renderInTestWaitForQueries(
<WithUseFeaturesComponent>
<AssetsPanel {...defaultProps} />
</WithUseFeaturesComponent>
);

const exportButton = screen
.getByText('Export to GeoJSON')
.closest('button') as HTMLButtonElement;

// Wait for Export button to be enabled before clicking
await waitFor(() => {
expect(exportButton).not.toBeDisabled();
});

// Click export button
await act(async () => {
fireEvent.click(screen.getByText('Export to GeoJSON'));
});

// Wait for the API call to get features
// Wait for the API call to get features for download (so second request)
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(2);
});

// Verify the geojson blob was created with correct content
Expand Down
29 changes: 8 additions & 21 deletions react/src/components/AssetsPanel/AssetsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState } from 'react';
import styles from './AssetsPanel.module.css';
import FeatureFileTree from '@hazmapper/components/FeatureFileTree';
import { FeatureCollection, Project, TapisFilePath } from '@hazmapper/types';
import { Project, TapisFilePath } from '@hazmapper/types';
import { Flex, Layout, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import {
useFeatures,
useCurrentFeatures,
useImportFeature,
useNotification,
} from '@hazmapper/hooks';
Expand All @@ -21,14 +22,14 @@ const getFilename = (projectName: string) => {
interface DownloadFeaturesButtonProps {
project: Project;
isPublicView: boolean;
disabled: boolean;
}

const DownloadFeaturesButton: React.FC<DownloadFeaturesButtonProps> = ({
project,
isPublicView,
disabled,
}) => {
const { data: featureCollection } = useCurrentFeatures();

const { isLoading: isDownloading, refetch } = useFeatures({
projectId: project.id,
isPublicView: isPublicView,
Expand Down Expand Up @@ -61,25 +62,20 @@ const DownloadFeaturesButton: React.FC<DownloadFeaturesButtonProps> = ({
}
});
};

return (
<Button
loading={isDownloading}
data-testid="exportGeoJson"
onClick={() => triggerDownload()}
type="primary"
disabled={disabled}
disabled={featureCollection === undefined || !featureCollection.features}
>
Export to GeoJSON
</Button>
);
};

interface Props {
/**
* Features of map
*/
featureCollection: FeatureCollection;

/**
* Whether or not the map project is a public view.
*/
Expand All @@ -94,11 +90,7 @@ interface Props {
/**
* A panel component that displays info on feature assets
*/
const AssetsPanel: React.FC<Props> = ({
isPublicView,
featureCollection,
project,
}) => {
const AssetsPanel: React.FC<Props> = ({ isPublicView, project }) => {
const notification = useNotification();
const [isModalOpen, setIsModalOpen] = useState(false);

Expand Down Expand Up @@ -139,17 +131,12 @@ const AssetsPanel: React.FC<Props> = ({
</Header>
)}
<Content className={styles.middleSection}>
<FeatureFileTree
projectId={project.id}
isPublicView={isPublicView}
featureCollection={featureCollection}
/>
<FeatureFileTree projectId={project.id} isPublicView={isPublicView} />
</Content>
<Footer className={styles.bottomSection}>
<DownloadFeaturesButton
project={project}
isPublicView={isPublicView}
disabled={featureCollection?.features.length === 0}
/>
</Footer>
</Flex>
Expand Down
69 changes: 32 additions & 37 deletions react/src/components/FeatureFileTree/FeatureFileTree.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React from 'react';
import { fireEvent, waitFor, act } from '@testing-library/react';
import { fireEvent, waitFor, act, screen } 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 {
server,
renderInTestWaitForQueries,
WithUseFeaturesComponent,
} from '@hazmapper/test/testUtil';
import { testDevConfiguration } from '@hazmapper/__fixtures__/appConfigurationFixture';

jest.mock('react-resize-detector', () => ({
Expand All @@ -15,7 +18,6 @@ jest.mock('react-resize-detector', () => ({

describe('FeatureFileTree', () => {
const defaultTreeProps = {
featureCollection: featureCollection,
isPublicView: false,
projectId: 1,
};
Expand All @@ -25,15 +27,15 @@ describe('FeatureFileTree', () => {
});

it('renders feature list correctly', async () => {
let rendered;
await act(async () => {
rendered = renderInTest(<FeatureFileTree {...defaultTreeProps} />);
});
await renderInTestWaitForQueries(
<WithUseFeaturesComponent>
<FeatureFileTree {...defaultTreeProps} />
</WithUseFeaturesComponent>
);

const { getByText } = rendered;
expect(getByText('foo')).toBeDefined();
expect(getByText('image1.JPG')).toBeDefined();
expect(getByText('image2.JPG')).toBeDefined();
expect(screen.getByText('foo')).toBeDefined();
expect(screen.getByText('image1.JPG')).toBeDefined();
expect(screen.getByText('image2.JPG')).toBeDefined();
});

it('handles feature deletion for non-public projects', async () => {
Expand All @@ -50,17 +52,15 @@ describe('FeatureFileTree', () => {
)
);

let rendered;
await act(async () => {
rendered = renderInTest(
<FeatureFileTree {...defaultTreeProps} />,
`/?selectedFeature=${featureId}`
);
});
await renderInTestWaitForQueries(
<WithUseFeaturesComponent>
<FeatureFileTree {...defaultTreeProps} />
</WithUseFeaturesComponent>,
`/?selectedFeature=${featureId}`
);

// Find and click delete button (as featured is selected)
const { getByTestId } = rendered;
const deleteButton = getByTestId('delete-feature-button');
const deleteButton = screen.getByTestId('delete-feature-button');
await act(async () => {
fireEvent.click(deleteButton);
});
Expand All @@ -71,31 +71,26 @@ describe('FeatureFileTree', () => {
});

it('does not show delete button for public projects', async () => {
let rendered;
await act(async () => {
rendered = renderInTest(
<FeatureFileTree {...defaultTreeProps} isPublicView={true} />,
'/?selectedFeature=1'
);
});
await renderInTestWaitForQueries(
<WithUseFeaturesComponent>
<FeatureFileTree {...defaultTreeProps} isPublicView={true} />
</WithUseFeaturesComponent>
);

// Verify delete button is not present
const { queryByTestId } = rendered;
const deleteButton = queryByTestId('delete-feature-button');
const deleteButton = screen.queryByTestId('delete-feature-button');
expect(deleteButton).toBeNull();
});

it('does not show delete button when no feature is selected', async () => {
let rendered;
await act(async () => {
rendered = renderInTest(
await renderInTestWaitForQueries(
<WithUseFeaturesComponent>
<FeatureFileTree {...defaultTreeProps} isPublicView={false} />
);
});
</WithUseFeaturesComponent>
);

// Verify delete button is not present
const { queryByTestId } = rendered;
const deleteButton = queryByTestId('delete-feature-button');
const deleteButton = screen.queryByTestId('delete-feature-button');
expect(deleteButton).toBeNull();
});
});
Loading
Loading