Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ dumps
# Cursor
.cursor-rules
.cursor-test-conventions.md
.cursor
2 changes: 2 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* [OSDEV-2355](https://opensupplyhub.atlassian.net/browse/OSDEV-2355) - The following changes have been made:
* Updated the GET `api/facilities/` and `api/facilities/{os_id}/` endpoints to include `contributor_type` (the raw type from the database) for both public and anonymous sources. Each contributor entry now also includes a `count` field (1 for public contributors and an aggregated count for anonymous entries of the same type), allowing the front end to display and sum counts by type (e.g., “18 Brands”, “9 Suppliers”).
* Additionally, updated GET `api/facilities/{os_id}/` to return the claim request creation date. All of this information is required for the redesigned Production Location page - specifically for the claim banner - as well as for the supply chain network.
* [OSDEV-2369](https://opensupplyhub.atlassian.net/browse/OSDEV-2369) - Moved single-facility data loading and redirect logic into the Production Location details container so the sidebar (including the "Contribute to this profile" section) and main content render with consistent facility data.

### Architecture/Environment changes
* Increased the CPU and memory allocation for the DedupeHub container to `8 CPU` and `40 GB` in the Terraform deployment configuration to address memory overload issues during production location reindexing for the `Test` environment.
Expand All @@ -31,6 +32,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* When the feature flag is disabled, accessing `/production-locations/:osID` routes will result in a "Not found" page with no automatic redirection to the legacy `/facilities/:osID` route.
* [OSDEV-2353](https://opensupplyhub.atlassian.net/browse/OSDEV-2353) - Created basic layout components for new Production Location page redesign.
* [OSDEV-2356](https://opensupplyhub.atlassian.net/browse/OSDEV-2356) - Added `GET api/partner-field-groups/` endpoint to retrieve partner field groups with pagination support and CDN caching for the endpoint (and additional endpoints for partner fields and contributors).
* [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.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
Expand Down
1 change: 1 addition & 0 deletions src/react/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function App({
},
background: {
grey: COLOURS.LIGHT_GREY,
white: COLOURS.WHITE,
},
},
}),
Expand Down
267 changes: 267 additions & 0 deletions src/react/src/__tests__/components/ContributeFields.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { screen, within, fireEvent } from '@testing-library/react';
import renderWithProviders from '../../util/testUtils/renderWithProviders';
import { setupStore } from '../../configureStore';
import ContributeFields from '../../components/ProductionLocation/Sidebar/ContributeFields/ContributeFields';
import { USER_DEFAULT_STATE } from '../../util/constants';
import { completeFetchSingleFacility } from '../../actions/facilities';

const TEST_OS_ID = 'US2026055ETKBY0';

const minimalFacilityFeature = {
id: TEST_OS_ID,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-73.83, 40.76],
},
properties: {
name: 'Test Facility Name',
address: '123 Test St',
country_code: 'US',
country_name: 'United States',
is_closed: false,
},
};

const getDefaultPreloadedState = (overrides = {}) => {
const defaultState = {
facilities: {
singleFacility: {
data: minimalFacilityFeature,
fetching: false,
error: null,
},
},
auth: {
user: {
user: { ...USER_DEFAULT_STATE },
},
},
dashboardActivityReports: {
activityReports: {
data: [],
fetching: false,
error: null,
message: null,
},
},
};
return {
...defaultState,
...overrides,
facilities: {
...defaultState.facilities,
...(overrides.facilities || {}),
singleFacility: {
...defaultState.facilities.singleFacility,
...(overrides.facilities?.singleFacility || {}),
},
},
auth: {
...defaultState.auth,
...(overrides.auth || {}),
user: {
...defaultState.auth.user,
...(overrides.auth?.user || {}),
user: {
...defaultState.auth.user.user,
...(overrides.auth?.user?.user || {}),
},
},
},
};
}

const renderContributeFields = (preloadedStateOverrides = {}, props = {}) => {
const preloadedState = getDefaultPreloadedState(preloadedStateOverrides);

return renderWithProviders(
<MemoryRouter>
<ContributeFields osId={props.osId ?? TEST_OS_ID} />
</MemoryRouter>,
{ preloadedState },
);
}

describe('ContributeFields and ReportFacilityStatusDialog', () => {
describe('ContributeFields section', () => {
test('renders contribute section with test id', () => {
renderContributeFields();

expect(screen.getByTestId('contribute-fields')).toBeInTheDocument();
});

test('renders suggest correction link with correct path', () => {
renderContributeFields();

const link = screen.getByTestId('contribute-suggest-correction');
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
'href',
expect.stringContaining(`/contribute/single-location/${TEST_OS_ID}/info/`),
);
});

test('renders report duplicate mailto link', () => {
renderContributeFields();

const link = screen.getByTestId('contribute-report-duplicate');
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', expect.stringMatching(/^mailto:/));
});

test('renders report status action that opens dialog', () => {
renderContributeFields();

expect(screen.getByTestId('contribute-report-status')).toBeInTheDocument();
});

test('renders report status action when facility is closed', () => {
const closedFacility = {
...minimalFacilityFeature,
properties: {
...minimalFacilityFeature.properties,
is_closed: true,
},
};
renderContributeFields({
facilities: {
singleFacility: {
data: closedFacility,
fetching: false,
error: null,
},
},
});

expect(screen.getByTestId('contribute-report-status')).toBeInTheDocument();
});
});

describe('ReportFacilityStatusDialog integration', () => {
test('opening report status shows dialog with facility data', () => {
renderContributeFields();

expect(screen.queryByTestId('report-facility-status-dialog')).not.toBeInTheDocument();

fireEvent.click(screen.getByTestId('contribute-report-status'));

const dialog = screen.getByTestId('report-facility-status-dialog');
expect(dialog).toBeInTheDocument();
expect(
within(dialog).getByTestId('report-facility-status-dialog-facility-name'),
).toHaveTextContent(minimalFacilityFeature.properties.name);
});

test('when single facility data is null dialog does not render content when opened', () => {
renderContributeFields({
facilities: {
singleFacility: {
data: null,
fetching: false,
error: null,
},
},
});

fireEvent.click(screen.getByTestId('contribute-report-status'));

expect(screen.queryByTestId('report-facility-status-dialog')).not.toBeInTheDocument();
});

test('anonymous user sees login prompt in dialog', () => {
renderContributeFields();

fireEvent.click(screen.getByTestId('contribute-report-status'));

expect(
screen.getByTestId('report-facility-status-dialog-login'),
).toBeInTheDocument();
});

test('logged-in user sees reason field and report action', () => {
renderContributeFields({
auth: {
user: {
user: {
...USER_DEFAULT_STATE,
isAnon: false,
},
},
},
});

fireEvent.click(screen.getByTestId('contribute-report-status'));

expect(screen.getByTestId('report-facility-status-reason')).toBeInTheDocument();
expect(
screen.getByTestId('report-facility-status-dialog-cancel'),
).toBeInTheDocument();
expect(
screen.getByTestId('report-facility-status-dialog-report'),
).toBeInTheDocument();
});

test('cancel closes dialog', () => {
renderContributeFields({
auth: {
user: {
user: {
...USER_DEFAULT_STATE,
isAnon: false,
},
},
},
});

fireEvent.click(screen.getByTestId('contribute-report-status'));
expect(screen.getByTestId('report-facility-status-dialog')).toBeInTheDocument();

fireEvent.click(screen.getByTestId('report-facility-status-dialog-cancel'));

const dialogAfterClose = screen.getByTestId('report-facility-status-dialog');
expect(dialogAfterClose).not.toBeVisible();
});

test('report with empty reason keeps dialog open', () => {
renderContributeFields({
auth: {
user: {
user: {
...USER_DEFAULT_STATE,
isAnon: false,
},
},
},
});

fireEvent.click(screen.getByTestId('contribute-report-status'));
fireEvent.click(screen.getByTestId('report-facility-status-dialog-report'));

expect(screen.getByTestId('report-facility-status-dialog')).toBeInTheDocument();
});
});

describe('Redux state via actions', () => {
test('prefilled facility via completeFetchSingleFacility is used by dialog', () => {
const store = setupStore({});
store.dispatch(completeFetchSingleFacility(minimalFacilityFeature));

const { getByTestId } = renderWithProviders(
<MemoryRouter>
<ContributeFields osId={TEST_OS_ID} />
</MemoryRouter>,
{ reduxStore: store },
);

fireEvent.click(getByTestId('contribute-report-status'));

expect(getByTestId('report-facility-status-dialog')).toBeInTheDocument();
expect(
getByTestId('report-facility-status-dialog-facility-name'),
).toHaveTextContent(minimalFacilityFeature.properties.name);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';

import styles from './styles';
import claimDataContainerStyles from './styles';

const ClaimDataContainer = ({ classes, className }) => (
<div className={`${classes.container} ${className || ''}`}>
Expand All @@ -12,4 +12,4 @@ const ClaimDataContainer = ({ classes, className }) => (
</div>
);

export default withStyles(styles)(ClaimDataContainer);
export default withStyles(claimDataContainerStyles)(ClaimDataContainer);
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { ENABLE_V1_CLAIMS_FLOW } from '../../../../util/constants';

import { getBackgroundColorClass, getMainText } from './utils';
import styles from './styles';
import facilityDetailsClaimFlagStyles from './styles';

const FacilityDetailsClaimFlag = ({
classes,
Expand Down Expand Up @@ -68,5 +68,5 @@ const mapStateToProps = ({ featureFlags: { flags } }) => {
};

export default connect(mapStateToProps)(
withStyles(styles)(FacilityDetailsClaimFlag),
withStyles(facilityDetailsClaimFlagStyles)(FacilityDetailsClaimFlag),
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import FeatureFlag from '../../../FeatureFlag';
import { REPORT_A_FACILITY } from '../../../../util/constants';

import PrimaryText from './PrimaryText';
import styles from './styles';
import productionLocationDetailClosureStatusStyles from './styles';

const ProductionLocationDetailClosureStatus = ({
data,
Expand Down Expand Up @@ -60,4 +60,6 @@ const ProductionLocationDetailClosureStatus = ({
);
};

export default withStyles(styles)(ProductionLocationDetailClosureStatus);
export default withStyles(productionLocationDetailClosureStatusStyles)(
ProductionLocationDetailClosureStatus,
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';

import styles from './styles';
import productionLocationDetailsDataSourcesInfoStyles from './styles';

const ProductionLocationDetailsDataSourcesInfo = ({ classes, className }) => (
<div className={`${classes.container} ${className || ''}`}>
Expand All @@ -12,4 +12,6 @@ const ProductionLocationDetailsDataSourcesInfo = ({ classes, className }) => (
</div>
);

export default withStyles(styles)(ProductionLocationDetailsDataSourcesInfo);
export default withStyles(productionLocationDetailsDataSourcesInfoStyles)(
ProductionLocationDetailsDataSourcesInfo,
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';

import styles from './styles';
import productionLocationDetailsTitleStyles from './styles';

const ProductionLocationDetailsTitle = ({ classes }) => (
<div className={classes.container}>
Expand All @@ -13,4 +13,6 @@ const ProductionLocationDetailsTitle = ({ classes }) => (
</div>
);

export default withStyles(styles)(ProductionLocationDetailsTitle);
export default withStyles(productionLocationDetailsTitleStyles)(
ProductionLocationDetailsTitle,
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';

import styles from './styles';
import assessmentsAndAuditsStyles from './styles';

const AssessmentsAndAudits = ({ classes }) => (
<div className={classes.container}>
Expand All @@ -12,4 +12,4 @@ const AssessmentsAndAudits = ({ classes }) => (
</div>
);

export default withStyles(styles)(AssessmentsAndAudits);
export default withStyles(assessmentsAndAuditsStyles)(AssessmentsAndAudits);
Loading