diff --git a/.gitignore b/.gitignore
index abf0a7ede..61bc56f8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,3 +104,4 @@ dumps
# Cursor
.cursor-rules
.cursor-test-conventions.md
+.cursor
diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md
index 0ca9976b8..b7372efe5 100644
--- a/doc/release/RELEASE-NOTES.md
+++ b/doc/release/RELEASE-NOTES.md
@@ -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.
@@ -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:
diff --git a/src/react/src/App.jsx b/src/react/src/App.jsx
index 07cf06eef..3f52eaaf6 100644
--- a/src/react/src/App.jsx
+++ b/src/react/src/App.jsx
@@ -71,6 +71,7 @@ function App({
},
background: {
grey: COLOURS.LIGHT_GREY,
+ white: COLOURS.WHITE,
},
},
}),
diff --git a/src/react/src/__tests__/components/ContributeFields.test.js b/src/react/src/__tests__/components/ContributeFields.test.js
new file mode 100644
index 000000000..c4b566ae5
--- /dev/null
+++ b/src/react/src/__tests__/components/ContributeFields.test.js
@@ -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(
+
+
+ ,
+ { 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(
+
+
+ ,
+ { 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);
+ });
+ });
+});
diff --git a/src/react/src/components/ProductionLocation/ClaimSection/ClaimDataContainer/ClaimDataContainer.jsx b/src/react/src/components/ProductionLocation/ClaimSection/ClaimDataContainer/ClaimDataContainer.jsx
index 3addd2361..8b49c25ce 100644
--- a/src/react/src/components/ProductionLocation/ClaimSection/ClaimDataContainer/ClaimDataContainer.jsx
+++ b/src/react/src/components/ProductionLocation/ClaimSection/ClaimDataContainer/ClaimDataContainer.jsx
@@ -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 }) => (
@@ -12,4 +12,4 @@ const ClaimDataContainer = ({ classes, className }) => (
);
-export default withStyles(styles)(ClaimDataContainer);
+export default withStyles(claimDataContainerStyles)(ClaimDataContainer);
diff --git a/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimFlag.jsx b/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimFlag.jsx
index 77007633d..f2aaab6a9 100644
--- a/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimFlag.jsx
+++ b/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimFlag.jsx
@@ -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,
@@ -68,5 +68,5 @@ const mapStateToProps = ({ featureFlags: { flags } }) => {
};
export default connect(mapStateToProps)(
- withStyles(styles)(FacilityDetailsClaimFlag),
+ withStyles(facilityDetailsClaimFlagStyles)(FacilityDetailsClaimFlag),
);
diff --git a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx
index 94b3a53dc..7149310fb 100644
--- a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx
+++ b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx
@@ -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,
@@ -60,4 +60,6 @@ const ProductionLocationDetailClosureStatus = ({
);
};
-export default withStyles(styles)(ProductionLocationDetailClosureStatus);
+export default withStyles(productionLocationDetailClosureStatusStyles)(
+ ProductionLocationDetailClosureStatus,
+);
diff --git a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx
index d2a8bfd53..426754f71 100644
--- a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx
+++ b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx
@@ -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 }) => (
@@ -12,4 +12,6 @@ const ProductionLocationDetailsDataSourcesInfo = ({ classes, className }) => (
);
-export default withStyles(styles)(ProductionLocationDetailsDataSourcesInfo);
+export default withStyles(productionLocationDetailsDataSourcesInfoStyles)(
+ ProductionLocationDetailsDataSourcesInfo,
+);
diff --git a/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx b/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx
index 067edb292..04069dc81 100644
--- a/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx
+++ b/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx
@@ -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 }) => (
@@ -13,4 +13,6 @@ const ProductionLocationDetailsTitle = ({ classes }) => (
);
-export default withStyles(styles)(ProductionLocationDetailsTitle);
+export default withStyles(productionLocationDetailsTitleStyles)(
+ ProductionLocationDetailsTitle,
+);
diff --git a/src/react/src/components/ProductionLocation/PartnerSection/AssessmentsAndAudits/AssessmentsAndAudits.jsx b/src/react/src/components/ProductionLocation/PartnerSection/AssessmentsAndAudits/AssessmentsAndAudits.jsx
index cdd06b5f6..607ea57cc 100644
--- a/src/react/src/components/ProductionLocation/PartnerSection/AssessmentsAndAudits/AssessmentsAndAudits.jsx
+++ b/src/react/src/components/ProductionLocation/PartnerSection/AssessmentsAndAudits/AssessmentsAndAudits.jsx
@@ -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 }) => (
@@ -12,4 +12,4 @@ const AssessmentsAndAudits = ({ classes }) => (
);
-export default withStyles(styles)(AssessmentsAndAudits);
+export default withStyles(assessmentsAndAuditsStyles)(AssessmentsAndAudits);
diff --git a/src/react/src/components/ProductionLocation/PartnerSection/Certifications/Certifications.jsx b/src/react/src/components/ProductionLocation/PartnerSection/Certifications/Certifications.jsx
index 4c0a2055e..195d0903d 100644
--- a/src/react/src/components/ProductionLocation/PartnerSection/Certifications/Certifications.jsx
+++ b/src/react/src/components/ProductionLocation/PartnerSection/Certifications/Certifications.jsx
@@ -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 certificationsStyles from './styles';
const Certifications = ({ classes }) => (
@@ -12,4 +12,4 @@ const Certifications = ({ classes }) => (
);
-export default withStyles(styles)(Certifications);
+export default withStyles(certificationsStyles)(Certifications);
diff --git a/src/react/src/components/ProductionLocation/PartnerSection/Emissions/Emissions.jsx b/src/react/src/components/ProductionLocation/PartnerSection/Emissions/Emissions.jsx
index 16708d52e..eecebc8ff 100644
--- a/src/react/src/components/ProductionLocation/PartnerSection/Emissions/Emissions.jsx
+++ b/src/react/src/components/ProductionLocation/PartnerSection/Emissions/Emissions.jsx
@@ -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 emissionsStyles from './styles';
const Emissions = ({ classes }) => (
@@ -12,4 +12,4 @@ const Emissions = ({ classes }) => (
);
-export default withStyles(styles)(Emissions);
+export default withStyles(emissionsStyles)(Emissions);
diff --git a/src/react/src/components/ProductionLocation/PartnerSection/ParentSectionItem/ParentSectionItem.jsx b/src/react/src/components/ProductionLocation/PartnerSection/ParentSectionItem/ParentSectionItem.jsx
index 7961fccbe..0080e87ec 100644
--- a/src/react/src/components/ProductionLocation/PartnerSection/ParentSectionItem/ParentSectionItem.jsx
+++ b/src/react/src/components/ProductionLocation/PartnerSection/ParentSectionItem/ParentSectionItem.jsx
@@ -6,7 +6,7 @@ import Switch from '@material-ui/core/Switch';
import InfoIcon from '@material-ui/icons/Info';
import DialogTooltip from '../../../../components/Contribute/DialogTooltip';
-import styles from './styles';
+import parentSectionItemStyles from './styles';
const ParentSectionItem = ({ classes, title, tooltipText, disclaimer }) => (
@@ -36,4 +36,4 @@ const ParentSectionItem = ({ classes, title, tooltipText, disclaimer }) => (
);
-export default withStyles(styles)(ParentSectionItem);
+export default withStyles(parentSectionItemStyles)(ParentSectionItem);
diff --git a/src/react/src/components/ProductionLocation/PartnerSection/PartnerDataContainer/PartnerDataContainer.jsx b/src/react/src/components/ProductionLocation/PartnerSection/PartnerDataContainer/PartnerDataContainer.jsx
index 6a9eea76e..7967e5c3c 100644
--- a/src/react/src/components/ProductionLocation/PartnerSection/PartnerDataContainer/PartnerDataContainer.jsx
+++ b/src/react/src/components/ProductionLocation/PartnerSection/PartnerDataContainer/PartnerDataContainer.jsx
@@ -5,7 +5,7 @@ import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import ParentSectionItem from '../ParentSectionItem/ParentSectionItem';
-import styles from './styles';
+import partnerDataContainerStyles from './styles';
function PartnerDataContainer({ classes }) {
return (
@@ -64,4 +64,4 @@ function PartnerDataContainer({ classes }) {
);
}
-export default withStyles(styles)(PartnerDataContainer);
+export default withStyles(partnerDataContainerStyles)(PartnerDataContainer);
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx
index dcdfb5397..856c3a67c 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx
@@ -9,7 +9,7 @@ import { facilitiesRoute } from '../../../util/constants';
import ProductionLocationDetailsContent from '../ProductionLocationDetailsContent/ProductionLocationDetailsContent';
-import styles from './styles';
+import productionLocationDetailsStyles from './styles';
function ProductionLocationDetails({
classes,
@@ -50,4 +50,4 @@ function mapDispatchToProps(dispatch) {
export default connect(
mapStateToProps,
mapDispatchToProps,
-)(withStyles(styles)(ProductionLocationDetails));
+)(withStyles(productionLocationDetailsStyles)(ProductionLocationDetails));
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx
index 56012277c..da0827d6f 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx
@@ -1,6 +1,12 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { connect } from 'react-redux';
+import { Redirect, withRouter } from 'react-router';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
+import CircularProgress from '@material-ui/core/CircularProgress';
+
+import isEmpty from 'lodash/isEmpty';
+import isArray from 'lodash/isArray';
import BackToSearch from '../Sidebar/BackToSearch/BackToSearch';
import NavBar from '../Sidebar/NavBar/NavBar';
@@ -8,15 +14,84 @@ import SupplyChain from '../Sidebar/SupplyChain/SupplyChain';
import ContributeFields from '../Sidebar/ContributeFields/ContributeFields';
import ProductionLocationDetailsContent from '../ProductionLocationDetailsContent/ProductionLocationDetailsContent';
-import styles from './styles';
+import {
+ getLastPathParameter,
+ makeFacilityDetailLinkOnRedirect,
+ shouldUseProductionLocationPage,
+} from '../../../util/util';
+import {
+ fetchSingleFacility,
+ resetSingleFacility,
+} from '../../../actions/facilities';
+
+import productionLocationDetailsContainerStyles from './styles';
+
+function ProductionLocationDetailsContainer({
+ classes,
+ history,
+ location,
+ match: { params: { osID } = {} } = {},
+ data,
+ fetching,
+ error,
+ contributors,
+ useProductionLocationPage,
+ fetchFacility,
+ clearFacility,
+}) {
+ const normalizedOsID =
+ getLastPathParameter(location?.pathname || '') ||
+ getLastPathParameter(osID) ||
+ osID;
+
+ useEffect(() => {
+ fetchFacility(normalizedOsID, contributors);
+ }, [normalizedOsID, contributors, fetchFacility]);
+
+ // Run cleanup only on unmount.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(() => () => clearFacility(), []);
+
+ if (fetching) {
+ return (
+
+
+
+ );
+ }
+
+ if (error && error.length) {
+ return (
+
+
+ {error.map(err => (
+ -
+ {err}
+
+ ))}
+
+
+ );
+ }
+
+ if (data?.id && data?.id !== osID) {
+ return (
+
+ );
+ }
-function ProductionLocationDetailsContainer({ classes, history }) {
return (
-
+
-
+
@@ -26,4 +101,37 @@ function ProductionLocationDetailsContainer({ classes, history }) {
);
}
-export default withStyles(styles)(ProductionLocationDetailsContainer);
+const mapStateToProps = ({
+ facilities: {
+ singleFacility: { data, fetching, error },
+ },
+ filters: { contributors },
+ featureFlags,
+}) => ({
+ data,
+ fetching,
+ error,
+ contributors: contributors || [],
+ useProductionLocationPage: shouldUseProductionLocationPage(featureFlags),
+});
+
+const mapDispatchToProps = dispatch => ({
+ fetchFacility: (id, contributorId) => {
+ const hasContributors =
+ isArray(contributorId) && !isEmpty(contributorId);
+ const contributors = hasContributors ? contributorId : null;
+ return dispatch(fetchSingleFacility(id, 0, contributors, true));
+ },
+ clearFacility: () => dispatch(resetSingleFacility()),
+});
+
+export default withRouter(
+ connect(
+ mapStateToProps,
+ mapDispatchToProps,
+ )(
+ withStyles(productionLocationDetailsContainerStyles)(
+ ProductionLocationDetailsContainer,
+ ),
+ ),
+);
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/styles.js b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/styles.js
index 259f72481..e65eaf5c5 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/styles.js
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/styles.js
@@ -4,4 +4,19 @@ export default theme =>
background: theme.palette.background.grey,
padding: '48px 5% 120px 5%',
}),
+ loadingRoot: Object.freeze({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '60vh',
+ background: theme.palette.background.grey,
+ }),
+ errorList: Object.freeze({
+ listStyle: 'none',
+ padding: theme.spacing.unit * 2,
+ }),
+ errorItem: Object.freeze({
+ color: theme.palette.error.main,
+ marginBottom: theme.spacing.unit,
+ }),
});
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx
index 5a940bf26..3c4f9d57f 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx
@@ -1,14 +1,8 @@
-import React, { useEffect } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
-import { Redirect, withRouter } from 'react-router';
-import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Divider from '@material-ui/core/Divider';
-import CircularProgress from '@material-ui/core/CircularProgress';
-
-import isEmpty from 'lodash/isEmpty';
-import isArray from 'lodash/isArray';
import ClaimFlag from '../Heading/ClaimFlag/ClaimFlag';
import LocationTitle from '../Heading/LocationTitle/LocationTitle';
@@ -18,161 +12,31 @@ import ClaimDataContainer from '../ClaimSection/ClaimDataContainer/ClaimDataCont
import PartnerDataContainer from '../PartnerSection/PartnerDataContainer/PartnerDataContainer';
import DetailsMap from '../ProductionLocationDetailsMap/ProductionLocationDetailsMap';
-import {
- makeFacilityDetailLinkOnRedirect,
- shouldUseProductionLocationPage,
- getLastPathParameter,
-} from '../../../util/util';
-
-import {
- fetchSingleFacility,
- resetSingleFacility,
-} from '../../../actions/facilities';
-
-import { facilityPropType } from '../../../util/propTypes';
-import styles from './styles';
-
-const ProductionLocationDetailsContent = ({
- classes,
- data,
- fetching,
- error,
- contributors,
- fetchFacility,
- clearFacility,
- location,
- match: {
- params: { osID },
- },
- useProductionLocationPage,
-}) => {
- const normalizedOsID =
- getLastPathParameter(location?.pathname || '') ||
- getLastPathParameter(osID) ||
- osID;
-
- useEffect(() => {
- fetchFacility(normalizedOsID, contributors);
- }, [normalizedOsID, contributors]);
-
- // Run cleanup only on unmount; clearFacility from connect is stable for this use.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => () => clearFacility(), []);
-
- if (fetching) {
- return (
-
-
-
- );
- }
-
- if (error && error.length) {
- return (
-
-
- {error.map(err => (
- -
- {err}
-
- ))}
-
-
- );
- }
-
- if (data?.id && data?.id !== osID) {
- return (
-
- );
- }
+import productionLocationDetailsContentStyles from './styles';
- return (
-
-
-
-
-
-
-
-
-
-
-
+const ProductionLocationDetailsContent = ({ classes }) => (
+
- );
-};
+
+
+
+
+
+
+
+
+);
ProductionLocationDetailsContent.propTypes = {
classes: PropTypes.object.isRequired,
- data: facilityPropType,
- fetching: PropTypes.bool.isRequired,
- error: PropTypes.arrayOf(PropTypes.string),
- contributors: PropTypes.array,
- fetchFacility: PropTypes.func.isRequired,
- clearFacility: PropTypes.func.isRequired,
- location: PropTypes.shape({
- pathname: PropTypes.string,
- search: PropTypes.string,
- }).isRequired,
- match: PropTypes.shape({
- params: PropTypes.shape({
- osID: PropTypes.string,
- }).isRequired,
- }).isRequired,
- useProductionLocationPage: PropTypes.bool.isRequired,
-};
-
-ProductionLocationDetailsContent.defaultProps = {
- data: null,
- error: [],
- contributors: [],
};
-function mapStateToProps({
- facilities: {
- singleFacility: { data, fetching, error },
- },
- filters: { contributors },
- featureFlags,
-}) {
- return {
- data,
- fetching,
- error,
- contributors,
- useProductionLocationPage: shouldUseProductionLocationPage(
- featureFlags,
- ),
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- fetchFacility: (id, contributorId) => {
- const hasContributors =
- isArray(contributorId) && !isEmpty(contributorId);
- const contributors = hasContributors ? contributorId : null;
-
- return dispatch(fetchSingleFacility(id, 0, contributors, true));
- },
- clearFacility: () => dispatch(resetSingleFacility()),
- };
-}
-
-export default withRouter(
- connect(
- mapStateToProps,
- mapDispatchToProps,
- )(withStyles(styles)(ProductionLocationDetailsContent)),
+export default withStyles(productionLocationDetailsContentStyles)(
+ ProductionLocationDetailsContent,
);
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsGeneralFields/ProductionLocationDetailsGeneralFields.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsGeneralFields/ProductionLocationDetailsGeneralFields.jsx
index a4ac2f40f..1585f5f91 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsGeneralFields/ProductionLocationDetailsGeneralFields.jsx
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsGeneralFields/ProductionLocationDetailsGeneralFields.jsx
@@ -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 productionLocationDetailsGeneralFieldsStyles from './styles';
/**
* Render extended fields, activity reports.
@@ -16,4 +16,6 @@ const ProductionLocationDetailsGeneralFields = ({ classes }) => (
);
-export default withStyles(styles)(ProductionLocationDetailsGeneralFields);
+export default withStyles(productionLocationDetailsGeneralFieldsStyles)(
+ ProductionLocationDetailsGeneralFields,
+);
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsLocation/ProductionLocationDetailsLocation.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsLocation/ProductionLocationDetailsLocation.jsx
index 171eed129..d85c84ee1 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsLocation/ProductionLocationDetailsLocation.jsx
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsLocation/ProductionLocationDetailsLocation.jsx
@@ -1,10 +1,12 @@
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
-import styles from './styles';
+import productionLocationDetailsLocationStyles from './styles';
const ProductionLocationDetailsLocation = () => (
<>{/** TODO: refer to FacilityDetailsItem */}>
);
-export default withStyles(styles)(ProductionLocationDetailsLocation);
+export default withStyles(productionLocationDetailsLocationStyles)(
+ ProductionLocationDetailsLocation,
+);
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsMap/ProductionLocationDetailsMap.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsMap/ProductionLocationDetailsMap.jsx
index 7d09bfc16..3b509f81f 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsMap/ProductionLocationDetailsMap.jsx
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsMap/ProductionLocationDetailsMap.jsx
@@ -4,7 +4,7 @@ import { withStyles } from '@material-ui/core/styles';
import Map from '../../Map';
-import styles from './styles';
+import productionLocationDetailsMapStyles from './styles';
const ProductionLocationDetailsMap = ({ classes }) => (
@@ -15,4 +15,6 @@ const ProductionLocationDetailsMap = ({ classes }) => (
);
-export default withStyles(styles)(ProductionLocationDetailsMap);
+export default withStyles(productionLocationDetailsMapStyles)(
+ ProductionLocationDetailsMap,
+);
diff --git a/src/react/src/components/ProductionLocation/Shared/Button/Button.jsx b/src/react/src/components/ProductionLocation/Shared/Button/Button.jsx
new file mode 100644
index 000000000..47f456d1c
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Shared/Button/Button.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { func, string, oneOf } from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
+
+import MaterialButton from '@material-ui/core/Button';
+
+import buttonStyles from './styles';
+import { VARIANT } from './constants';
+
+const Button = ({ classes, variant, label, onClick, ...rest }) => (
+
+ {label}
+
+);
+
+Button.propTypes = {
+ label: string.isRequired,
+ onClick: func.isRequired,
+ variant: oneOf([VARIANT.outlined, VARIANT.filled]),
+};
+
+Button.defaultProps = {
+ variant: VARIANT.filled,
+};
+
+export default withStyles(buttonStyles)(Button);
diff --git a/src/react/src/components/ProductionLocation/Shared/Button/constants.js b/src/react/src/components/ProductionLocation/Shared/Button/constants.js
new file mode 100644
index 000000000..b371fb015
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Shared/Button/constants.js
@@ -0,0 +1,6 @@
+export const VARIANT = {
+ outlined: 'outlined',
+ filled: 'filled',
+};
+
+export default VARIANT;
diff --git a/src/react/src/components/ProductionLocation/Shared/Button/styles.js b/src/react/src/components/ProductionLocation/Shared/Button/styles.js
new file mode 100644
index 000000000..59cd8e1ea
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Shared/Button/styles.js
@@ -0,0 +1,31 @@
+const commonButtonStyles = Object.freeze({
+ borderRadius: 0,
+ padding: '8px 16px',
+ fontWeight: 900,
+ boxShadow: 'none',
+ '&:hover': Object.freeze({
+ boxShadow: 'none',
+ }),
+});
+
+export default theme =>
+ Object.freeze({
+ outlined: Object.freeze({
+ ...commonButtonStyles,
+ backgroundColor: 'transparent',
+ border: '1px solid rgb(13, 17, 40)',
+ '&:hover': Object.freeze({
+ ...commonButtonStyles['&:hover'],
+ backgroundColor: 'rgba(0, 0, 0, 0.08)',
+ }),
+ }),
+ filled: Object.freeze({
+ ...commonButtonStyles,
+ backgroundColor: theme.palette.action.main,
+ border: 'none',
+ '&:hover': Object.freeze({
+ ...commonButtonStyles['&:hover'],
+ backgroundColor: theme.palette.action.dark,
+ }),
+ }),
+ });
diff --git a/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx b/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx
index ab6573cec..1ba1ecd95 100644
--- a/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx
+++ b/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx
@@ -7,7 +7,7 @@ import ArrowBack from '@material-ui/icons/ArrowBackIos';
import { resetSingleFacility } from '../../../../actions/facilities';
import { facilitiesRoute } from '../../../../util/constants';
-import styles from './styles';
+import productionLocationDetailsBackToSearchStyles from './styles';
function ProductionLocationDetailsBackToSearch({
classes,
@@ -40,4 +40,8 @@ function mapDispatchToProps(dispatch) {
export default connect(
null,
mapDispatchToProps,
-)(withStyles(styles)(ProductionLocationDetailsBackToSearch));
+)(
+ withStyles(productionLocationDetailsBackToSearchStyles)(
+ ProductionLocationDetailsBackToSearch,
+ ),
+);
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx
index d1bf1b835..8580350b6 100644
--- a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx
@@ -1,12 +1,18 @@
import React from 'react';
import { Link } from 'react-router-dom';
+import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
-
-import MenuList from '@material-ui/core/MenuList';
-import MenuItem from '@material-ui/core/MenuItem';
+import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
-import { facilityDetailsActions } from '../../../../util/constants';
+import Add from '@material-ui/icons/Add';
+import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+import HighlightOff from '@material-ui/icons/HighlightOff';
+
+import { ReactComponent as CopyIcon } from './icons/copy.svg';
+import { ReactComponent as ShieldXIcon } from './icons/shield-x.svg';
+
+import ShowOnly from '../../../ShowOnly';
import {
makeContributeProductionLocationUpdateURL,
@@ -14,39 +20,132 @@ import {
makeDisputeClaimEmailLink,
} from '../../../../util/util';
-import styles from './styles';
-
-const ProductionLocationDetailsContributeFields = ({ classes, osId }) => (
-
-);
+import useReportStatusDialog from './hooks';
+import shouldShowDisputeClaim from './utils';
+import ReportFacilityStatusDialog from './ReportFacilityStatusDialog/ReportFacilityStatusDialog';
+import productionLocationSidebarContributeFieldsStyles from './styles';
-export default withStyles(styles)(ProductionLocationDetailsContributeFields);
+const ProductionLocationDetailsContributeFields = ({
+ classes,
+ osId,
+ showDisputeClaim,
+ isClosed,
+}) => {
+ const [showDialog, openDialog, closeDialog] = useReportStatusDialog();
+
+ return (
+
+
+ Contribute to this profile
+
+
+ Help improve supply chain transparency
+
+
+
+
+
+
+ Suggest Correction
+
+
+
+
+
+
+
+ Report Duplicate
+
+
+
+
+
+
+
+
+ Dispute Claim
+
+
+
+
+
+ e.key === 'Enter' && openDialog()}
+ className={classes.actionItem}
+ data-testid="contribute-report-status"
+ >
+ {isClosed ? (
+
+ ) : (
+
+ )}
+
+ {isClosed ? 'Report Reopened' : 'Report Closed'}
+
+
+
+
+
+
+ );
+};
+
+const mapStateToProps = (
+ {
+ facilities: {
+ singleFacility: { data },
+ },
+ auth: { user },
+ },
+ { osId },
+) => ({
+ showDisputeClaim: shouldShowDisputeClaim(data, osId, user),
+ isClosed: data?.properties?.is_closed,
+});
+
+export default connect(mapStateToProps)(
+ withStyles(productionLocationSidebarContributeFieldsStyles)(
+ ProductionLocationDetailsContributeFields,
+ ),
+);
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/ReportFacilityStatusDialog.jsx b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/ReportFacilityStatusDialog.jsx
new file mode 100644
index 000000000..4624fdfdb
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/ReportFacilityStatusDialog.jsx
@@ -0,0 +1,201 @@
+import React from 'react';
+import { bool, func } from 'prop-types';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+import { withStyles } from '@material-ui/core/styles';
+
+import DialogTitle from '@material-ui/core/DialogTitle';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogContentText from '@material-ui/core/DialogContentText';
+import TextField from '@material-ui/core/TextField';
+import Typography from '@material-ui/core/Typography';
+import Divider from '@material-ui/core/Divider';
+
+import Button from '../../../Shared/Button/Button';
+import DashboardActivityReportToast from '../../../../DashboardActivityReportToast';
+
+import {
+ createDashboardActivityReport,
+ resetDashbooardActivityReports,
+} from '../../../../../actions/dashboardActivityReports';
+
+import { facilityDetailsPropType } from '../../../../../util/propTypes';
+import { authLoginFormRoute } from '../../../../../util/constants';
+import { VARIANT } from '../../../Shared/Button/constants';
+
+import useReportReason from './hooks';
+import reportFacilityStatusDialogStyles from './styles';
+
+const ReportFacilityStatusDialog = ({
+ data,
+ user,
+ dashboardActivityReports: { activityReports },
+ submitReport,
+ resetReports,
+ open,
+ onClose,
+ classes,
+}) => {
+ const [reasonForReport, setReportReason, resetReason] = useReportReason();
+
+ if (!data || !data.properties) {
+ return null;
+ }
+
+ const closeDialog = () => {
+ onClose();
+ resetReason();
+ };
+
+ const handleSubmit = () => {
+ if (!reasonForReport.length) return;
+ const closureState = data.properties.is_closed ? 'OPEN' : 'CLOSED';
+ submitReport({ id: data.id, reasonForReport, closureState });
+ closeDialog();
+ };
+
+ const loginButton = (
+
+ );
+
+ const dialog = (
+
+ );
+
+ return (
+
+ {dialog}
+
+
+ );
+};
+
+ReportFacilityStatusDialog.propTypes = {
+ data: facilityDetailsPropType,
+ open: bool,
+ onClose: func.isRequired,
+};
+
+ReportFacilityStatusDialog.defaultProps = {
+ data: null,
+ open: false,
+};
+
+const mapStateToProps = ({
+ dashboardActivityReports,
+ facilities: {
+ singleFacility: { data },
+ },
+ auth: {
+ user: { user },
+ },
+}) => ({
+ dashboardActivityReports,
+ user,
+ data,
+});
+
+const mapDispatchToProps = dispatch => ({
+ submitReport: payload => dispatch(createDashboardActivityReport(payload)),
+ resetReports: () => dispatch(resetDashbooardActivityReports()),
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(withStyles(reportFacilityStatusDialogStyles)(ReportFacilityStatusDialog));
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/hooks.js b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/hooks.js
new file mode 100644
index 000000000..6743fdac7
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/hooks.js
@@ -0,0 +1,13 @@
+import { useState } from 'react';
+
+const useReportReason = () => {
+ const [reason, setReason] = useState('');
+
+ const resetReason = () => {
+ setReason('');
+ };
+
+ return [reason, setReason, resetReason];
+};
+
+export default useReportReason;
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/styles.js b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/styles.js
new file mode 100644
index 000000000..f7c5e08f5
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ReportFacilityStatusDialog/styles.js
@@ -0,0 +1,30 @@
+export default () =>
+ Object.freeze({
+ facilityName: Object.freeze({
+ padding: '5px 0 15px',
+ }),
+ description: Object.freeze({
+ fontWeight: 'bold',
+ color: 'rgb(27, 27, 26)',
+ fontSize: '16px',
+ }),
+ dialogActionsStyles: Object.freeze({
+ display: 'flex',
+ justifyContent: 'space-evenly',
+ alignItems: 'center',
+ padding: '10px',
+ }),
+ dialogPaper: Object.freeze({
+ borderRadius: 0,
+ maxWidth: '700px',
+ width: '100%',
+ }),
+ dialogTextFieldStyles: Object.freeze({
+ width: '100%',
+ marginTop: '10px',
+ minWidth: '300px',
+ '& fieldset': Object.freeze({
+ borderRadius: 0,
+ }),
+ }),
+ });
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/hooks.js b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/hooks.js
new file mode 100644
index 000000000..a528e3c3c
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/hooks.js
@@ -0,0 +1,12 @@
+import { useState } from 'react';
+
+const useReportStatusDialog = () => {
+ const [showDialog, setShowDialog] = useState(false);
+
+ const openDialog = () => setShowDialog(true);
+ const closeDialog = () => setShowDialog(false);
+
+ return [showDialog, openDialog, closeDialog];
+};
+
+export default useReportStatusDialog;
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/icons/copy.svg b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/icons/copy.svg
new file mode 100644
index 000000000..1f21b5021
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/icons/copy.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/icons/shield-x.svg b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/icons/shield-x.svg
new file mode 100644
index 000000000..9c0051f91
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/icons/shield-x.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js
index 46122fb3c..b96125d25 100644
--- a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js
@@ -1,10 +1,55 @@
+import commonStyles from '../../commonStyles';
+
export default theme =>
Object.freeze({
- container: Object.freeze({
- backgroundColor: 'white',
+ ...commonStyles(theme),
+ contributeSectionContainer: Object.freeze({
+ padding: '12px',
marginBottom: theme.spacing.unit,
}),
title: Object.freeze({
- marginBottom: theme.spacing.unit,
+ fontWeight: 600,
+ fontSize: '1.125rem',
+ }),
+ subtitle: Object.freeze({
+ color: theme.palette.text.secondary,
+ fontSize: '0.875rem',
+ marginBottom: '12px',
+ }),
+ actionsList: Object.freeze({
+ width: '100%',
+ flexDirection: 'column',
+ margin: 0,
+ gap: '6px',
+ }),
+ actionItemWrapper: Object.freeze({}),
+ actionItem: Object.freeze({
+ display: 'flex',
+ alignItems: 'center',
+ padding: '0 12px',
+ height: '36px',
+ gap: '8px',
+ textDecoration: 'none',
+ color: theme.palette.text.primary,
+ transition: 'background-color 0.2s ease',
+ width: '100%',
+ boxSizing: 'border-box',
+ cursor: 'pointer',
+ background: 'none',
+ border: 'none',
+ '&:hover': {
+ backgroundColor: 'rgba(0, 0, 0, 0.08)',
+ },
+ }),
+ actionIcon: Object.freeze({
+ fontSize: 16,
+ width: 16,
+ height: 16,
+ flexShrink: 0,
+ color: theme.palette.text.primary,
+ }),
+ actionLabel: Object.freeze({
+ fontWeight: 500,
+ fontSize: '1rem',
}),
});
diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/utils.js b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/utils.js
new file mode 100644
index 000000000..a15a54b08
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/utils.js
@@ -0,0 +1,33 @@
+import get from 'lodash/get';
+import includes from 'lodash/includes';
+
+import { facilityClaimStatusChoicesEnum } from '../../../../util/constants';
+
+const shouldShowDisputeClaim = (data, osID, user) => {
+ const isPendingClaim =
+ get(data, 'properties.claim_info.status') ===
+ facilityClaimStatusChoicesEnum.PENDING;
+ const isClaimed = !isPendingClaim && !!get(data, 'properties.claim_info');
+
+ const {
+ approved: currentUserApprovedClaimedFacilities = [],
+ pending: currentUserPendingClaimedFacilities = [],
+ } = get(user, 'user.claimed_facility_ids', { approved: [], pending: [] });
+
+ const facilityIsClaimedByCurrentUser = includes(
+ currentUserApprovedClaimedFacilities,
+ osID,
+ );
+
+ const userHasPendingFacilityClaim =
+ includes(currentUserPendingClaimedFacilities, osID) &&
+ !facilityIsClaimedByCurrentUser;
+
+ return (
+ !facilityIsClaimedByCurrentUser &&
+ !userHasPendingFacilityClaim &&
+ isClaimed
+ );
+};
+
+export default shouldShowDisputeClaim;
diff --git a/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx b/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx
index 02e4ed877..506d1ea59 100644
--- a/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx
+++ b/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx
@@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';
import Typography from '@material-ui/core/Typography';
-import styles from './styles';
+import navBarStyles from './styles';
const NavBar = ({ classes }) => (
@@ -42,4 +42,4 @@ const NavBar = ({ classes }) => (
);
-export default withStyles(styles)(NavBar);
+export default withStyles(navBarStyles)(NavBar);
diff --git a/src/react/src/components/ProductionLocation/Sidebar/SupplyChain/SupplyChain.jsx b/src/react/src/components/ProductionLocation/Sidebar/SupplyChain/SupplyChain.jsx
index 887ea5677..6af139ac4 100644
--- a/src/react/src/components/ProductionLocation/Sidebar/SupplyChain/SupplyChain.jsx
+++ b/src/react/src/components/ProductionLocation/Sidebar/SupplyChain/SupplyChain.jsx
@@ -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 productionLocationDetailsSupplyChainStyles from './styles';
const ProductionLocationDetailsSupplyChain = ({ classes }) => (
@@ -19,4 +19,6 @@ const ProductionLocationDetailsSupplyChain = ({ classes }) => (
);
-export default withStyles(styles)(ProductionLocationDetailsSupplyChain);
+export default withStyles(productionLocationDetailsSupplyChainStyles)(
+ ProductionLocationDetailsSupplyChain,
+);
diff --git a/src/react/src/components/ProductionLocation/commonStyles.js b/src/react/src/components/ProductionLocation/commonStyles.js
new file mode 100644
index 000000000..cece16ea7
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/commonStyles.js
@@ -0,0 +1,7 @@
+export default theme =>
+ Object.freeze({
+ container: Object.freeze({
+ backgroundColor: theme.palette.background.white,
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
+ }),
+ });