Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7fcd7bc
Add interactive map (partial impl)
VadimKovalenkoSNF Mar 4, 2026
174533e
Connect Map component
VadimKovalenkoSNF Mar 5, 2026
2cef5c0
Test different Map layout
VadimKovalenkoSNF Mar 5, 2026
130ad33
Connect satellite layout
VadimKovalenkoSNF Mar 5, 2026
374374d
Connect back to location button
VadimKovalenkoSNF Mar 5, 2026
b037689
Adjust map control buttons
VadimKovalenkoSNF Mar 5, 2026
b400e2f
Adjust map to show multiple facilities
VadimKovalenkoSNF Mar 5, 2026
24b0197
Refactor map grid layout to render green circles
VadimKovalenkoSNF Mar 6, 2026
c9038fc
Fix infinite production location re-render on marker click
VadimKovalenkoSNF Mar 6, 2026
7007ba6
Refactor styles for Open in Google Maps button
VadimKovalenkoSNF Mar 6, 2026
b55cbec
Add Drag to Pan info element
VadimKovalenkoSNF Mar 6, 2026
46cca6e
Add legend for the map grid view
VadimKovalenkoSNF Mar 6, 2026
adadd8b
Add styles to subsection title
VadimKovalenkoSNF Mar 6, 2026
078d444
Set adaptive height to the Map component
VadimKovalenkoSNF Mar 6, 2026
21ddb2a
Merge branch 'main' into OSDEV-2373-geographic-information-section
VadimKovalenkoSNF Mar 6, 2026
972b109
Connect data points to Geographic Information section
VadimKovalenkoSNF Mar 9, 2026
0a874a5
Show date for coordinates contribution
VadimKovalenkoSNF Mar 9, 2026
8b464c7
Add tooltip
VadimKovalenkoSNF Mar 9, 2026
e80bb61
Refactor field items count for address field
VadimKovalenkoSNF Mar 9, 2026
99fde42
Update release notes
VadimKovalenkoSNF Mar 9, 2026
b69467a
Remove redundant code
VadimKovalenkoSNF Mar 9, 2026
d6d7488
Add unit tests
VadimKovalenkoSNF Mar 9, 2026
386c6b1
Prevent mismatched date provenance for coordinates contributor
VadimKovalenkoSNF Mar 10, 2026
9bd7ce8
Preserve all canonical entries in address and coordinates contributions
VadimKovalenkoSNF Mar 10, 2026
4a30015
Hide Google Maps button and align zoom when facility has no coordinates
VadimKovalenkoSNF Mar 10, 2026
e5d8f9e
Remove unused leaflet settings
VadimKovalenkoSNF Mar 10, 2026
1548eb6
Fix lint issues for setupTests
VadimKovalenkoSNF Mar 10, 2026
13f778c
Fix unit test
VadimKovalenkoSNF Mar 10, 2026
e16b266
Require coordinate match for canonical other_location selection
VadimKovalenkoSNF Mar 10, 2026
a775e6f
Show no attribution when no address field matches the canonical address
VadimKovalenkoSNF Mar 10, 2026
2ce3762
Minor refactoring of the formatClaimDate
VadimKovalenkoSNF Mar 10, 2026
8106e6a
fix: use created_at || updated_at fallback for address date in utils.js
VadimKovalenkoSNF Mar 10, 2026
ecd9809
Remove utc() from the claim date
VadimKovalenkoSNF Mar 10, 2026
3aad9cf
fix: use epsilon tolerance for coordinate matching to handle float pr…
VadimKovalenkoSNF Mar 10, 2026
0f93d62
fix: remove contributors from fetchFacility useEffect dependency array
VadimKovalenkoSNF Mar 10, 2026
d392060
Remove coordinates date based on contributor info
VadimKovalenkoSNF Mar 10, 2026
b26ef9e
Remove redundant comments, minor fixes
VadimKovalenkoSNF Mar 11, 2026
24bae24
Only show the spinner for stale data if the fetch hasnt failed
VadimKovalenkoSNF Mar 11, 2026
a93fc97
Merge branch 'main' into OSDEV-2373-geographic-information-section
VadimKovalenkoSNF Mar 11, 2026
7fbc45f
Fix geomarker overlap
VadimKovalenkoSNF Mar 11, 2026
a8be8bb
Fix unit test
VadimKovalenkoSNF Mar 11, 2026
0ea1b36
Add resetAllFilters
VadimKovalenkoSNF Mar 11, 2026
7025124
Do not count null promotedContribution as an anonymous contributor
VadimKovalenkoSNF Mar 11, 2026
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
1 change: 1 addition & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* [OSDEV-2369](https://opensupplyhub.atlassian.net/browse/OSDEV-2369) - As part of the Production Location page redesign, implemented the "Contribute to this profile" section in the sidebar. The section includes: Suggest Correction (link to the contribute flow), Report Duplicate and Dispute Claim (mailto links; Dispute Claim is shown only when the facility is claimed by someone else), and Report Closed / Report Reopened. Report Closed/Reopened opens a dialog where logged-in users can submit a reason; anonymous users see a prompt to log in.
* [OSDEV-2375](https://opensupplyhub.atlassian.net/browse/OSDEV-2375) - Created UI for the location name, OS ID, and "Understanding Data Sources" sections. Introduced `doc/frontend.md` with UI development considerations.
* [OSDEV-2366](https://opensupplyhub.atlassian.net/browse/OSDEV-2366) - Added "Jump to" section to the sidebar with links to the different sections of the Production Location page.
* [OSDEV-2373](https://opensupplyhub.atlassian.net/browse/OSDEV-2373) - Implemented the Geographical Information section on the Production Location page, displaying an interactive satellite map with zoom controls, location centering, and "Open in Google Maps" link. Added Address and Coordinates data points below the map, each with contributor metadata and a drawer showing all contributions for that field.
* [OSDEV-2368](https://opensupplyhub.atlassian.net/browse/OSDEV-2368) - Integrated the Partner Data section into the Production Location page:
* Added `PartnerDataContainer` that fetches partner field groups from the API and renders them when partner data is available for a production location.
* Each partner group is displayed as a collapsible `PartnerSectionItem` with a toggle switch, partner icon, helper text tooltip, and a two-column layout of partner fields.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jest.mock(
jest.mock('../../actions/facilities', () => ({
fetchSingleFacility: () => ({ type: 'noop' }),
resetSingleFacility: () => ({ type: 'RESET_SINGLE_FACILITY' }),
fetchFacilities: () => () => {},
}));

jest.mock('../../actions/partnerFieldGroups', () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import React from 'react';
import { MemoryRouter, Route } from 'react-router-dom';
import { render, screen, fireEvent } from '@testing-library/react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';

import ProductionLocationDetailsMap from '../../components/ProductionLocation/ProductionLocationDetailsMap/ProductionLocationDetailsMap';

jest.mock('leaflet', () => ({ icon: jest.fn(() => ({})) }));
jest.mock('leaflet/dist/leaflet.css', () => {});

jest.mock('react-leaflet', () => ({
Map: ({ children }) => <div data-testid="leaflet-map">{children}</div>,

Check warning on line 13 in src/react/src/__tests__/components/ProductionLocationDetailsMap.test.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'children' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=opensupplyhub_open-supply-hub&issues=AZzTjxy0kz7ky6K7qFrb&open=AZzTjxy0kz7ky6K7qFrb&pullRequest=904
TileLayer: () => null,
Marker: () => null,
}));

jest.mock('react-leaflet-control', () => ({ children }) => <>{children}</>);

Check warning on line 18 in src/react/src/__tests__/components/ProductionLocationDetailsMap.test.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'children' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=opensupplyhub_open-supply-hub&issues=AZzTjxy0kz7ky6K7qFrc&open=AZzTjxy0kz7ky6K7qFrc&pullRequest=904

jest.mock('../../components/VectorTileFacilitiesLayer', () => ({
__esModule: true,
default: () => null,
createMarkerIcon: () => ({}),
}));
jest.mock('../../components/VectorTileFacilityGridLayer', () => () => null);
jest.mock('../../components/VectorTileGridLegend', () => () => null);

jest.mock(
'../../components/ProductionLocation/DataPoint/DataPoint',
() => ({
__esModule: true,
default: ({ label, value, drawerData, onOpenDrawer, renderDrawer }) => {
const hasContributions =
Array.isArray(drawerData?.contributions) &&
drawerData.contributions.length > 0 &&
!!onOpenDrawer;
return (
<div data-testid="data-point">
<span data-testid="data-point-label">{label}</span>
<span data-testid="data-point-value">{value}</span>
{hasContributions && (
<button
type="button"
data-testid="data-point-sources-button"
onClick={onOpenDrawer}
>
{`+${drawerData.contributions.length} data sources`}
</button>
)}
{typeof renderDrawer === 'function' && renderDrawer()}
</div>
);
},
}),
);

jest.mock(
'../../components/ProductionLocation/ContributionsDrawer/ContributionsDrawer',
() => ({
__esModule: true,
default: ({ open }) =>
open ? <div data-testid="contributions-drawer" /> : null,
}),
);

const FACILITY_ADDRESS = '123 Main St, New York';
const FACILITY_LNG = -73.8314318;
const FACILITY_LAT = 40.762569;
const EXPECTED_COORD_DISPLAY = `${FACILITY_LAT}, ${FACILITY_LNG}`;

const makeFacility = (overrides = {}) => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [FACILITY_LNG, FACILITY_LAT],
},
properties: {
address: FACILITY_ADDRESS,
other_locations: [],
created_from: {
contributor: 'Test Org',
created_at: '2023-01-01T00:00:00Z',
},
extended_fields: {
address: [
{
value: FACILITY_ADDRESS,
contributor_name: 'Test Org',
contributor_id: 1,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
is_from_claim: false,
},
],
},
},
...overrides,
});

const makeStore = facilityData =>
createStore(() => ({
facilities: { singleFacility: { data: facilityData } },
vectorTileLayer: { gridColorRamp: [] },
}));

const renderMap = (facilityData = null) =>
render(
<Provider store={makeStore(facilityData)}>
<MemoryRouter initialEntries={['/production-locations/OS12345']}>
<Route
path="/production-locations/:osID"
component={ProductionLocationDetailsMap}
/>
</MemoryRouter>
</Provider>,
);

describe('ProductionLocationDetailsMap', () => {
describe('section header', () => {
it('renders the "Geographic information" section title', () => {
renderMap(makeFacility());

expect(
screen.getByText('Geographic information'),
).toBeInTheDocument();
});
});

describe('info grid', () => {
it('renders the info grid container', () => {
renderMap(makeFacility());

expect(
screen.getByTestId('production-location-info-grid'),
).toBeInTheDocument();
});

it('renders an address row and a coordinates row', () => {
renderMap(makeFacility());

expect(
screen.getByTestId('production-location-address-row'),
).toBeInTheDocument();
expect(
screen.getByTestId('production-location-coordinates-row'),
).toBeInTheDocument();
});
});

describe('address DataPoint', () => {
it('renders the "Address" label', () => {
renderMap(makeFacility());

const addressRow = screen.getByTestId(
'production-location-address-row',
);
expect(addressRow).toHaveTextContent('Address');
});

it('displays the facility address', () => {
renderMap(makeFacility());

const addressRow = screen.getByTestId(
'production-location-address-row',
);
expect(addressRow).toHaveTextContent(FACILITY_ADDRESS);
});

it('shows "—" when properties.address is empty', () => {
const facility = makeFacility();
facility.properties.address = '';
facility.properties.extended_fields.address = [];
renderMap(facility);

const addressRow = screen.getByTestId(
'production-location-address-row',
);
expect(addressRow).toHaveTextContent('—');
});
});

describe('coordinates DataPoint', () => {
it('renders the "Coordinates" label', () => {
renderMap(makeFacility());

const coordRow = screen.getByTestId(
'production-location-coordinates-row',
);
expect(coordRow).toHaveTextContent('Coordinates');
});

it('displays coordinates formatted as "lat, lng"', () => {
renderMap(makeFacility());

const coordRow = screen.getByTestId(
'production-location-coordinates-row',
);
expect(coordRow).toHaveTextContent(EXPECTED_COORD_DISPLAY);
});

it('shows "—" when geometry.coordinates is absent', () => {
const facility = makeFacility({ geometry: null });
renderMap(facility);

const coordRow = screen.getByTestId(
'production-location-coordinates-row',
);
expect(coordRow).toHaveTextContent('—');
});
});

describe('sources button and drawer', () => {
const facilityWithMultipleAddresses = makeFacility({
properties: {
address: FACILITY_ADDRESS,
other_locations: [],
created_from: {
contributor: 'Test Org',
created_at: '2023-01-01T00:00:00Z',
},
extended_fields: {
address: [
{
value: FACILITY_ADDRESS,
contributor_name: 'Test Org',
contributor_id: 1,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
is_from_claim: false,
},
{
value: '456 Second Ave',
contributor_name: 'Second Org',
contributor_id: 2,
created_at: '2023-03-01T00:00:00Z',
updated_at: '2023-03-01T00:00:00Z',
is_from_claim: false,
},
],
},
},
});

it('shows the sources button when there are address contributions', () => {
renderMap(facilityWithMultipleAddresses);

expect(
screen.getByTestId('data-point-sources-button'),
).toBeInTheDocument();
});

it('does not show the sources button when there is only one address entry', () => {
renderMap(makeFacility());

expect(
screen.queryByTestId('data-point-sources-button'),
).not.toBeInTheDocument();
});

it('opens the ContributionsDrawer when the sources button is clicked', () => {
renderMap(facilityWithMultipleAddresses);

expect(
screen.queryByTestId('contributions-drawer'),
).not.toBeInTheDocument();

fireEvent.click(screen.getByTestId('data-point-sources-button'));

expect(
screen.getByTestId('contributions-drawer'),
).toBeInTheDocument();
});
});
});
Loading
Loading