setOpen(false) : undefined}
title={titleContent}
diff --git a/src/react/src/components/HandshakeIcon.jsx b/src/react/src/components/HandshakeIcon.jsx
new file mode 100644
index 000000000..1dc66fcd3
--- /dev/null
+++ b/src/react/src/components/HandshakeIcon.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import SvgIcon from '@material-ui/core/SvgIcon';
+
+export default function HandshakeIcon(props) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx
index 9ad74a9c8..302c73179 100644
--- a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx
+++ b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/ClosureStatus.jsx
@@ -64,7 +64,7 @@ const ProductionLocationDetailClosureStatus = ({
};
ProductionLocationDetailClosureStatus.propTypes = {
- data: PropTypes.object.isRequired,
+ data: PropTypes.object,
clearFacility: PropTypes.func.isRequired,
classes: PropTypes.shape({
status: PropTypes.string,
@@ -80,6 +80,7 @@ ProductionLocationDetailClosureStatus.propTypes = {
};
ProductionLocationDetailClosureStatus.defaultProps = {
+ data: null,
useProductionLocationPage: false,
search: '',
};
diff --git a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js
index 9c076eee9..cd86f2b1d 100644
--- a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js
+++ b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js
@@ -5,6 +5,7 @@ export default theme =>
borderRadius: 0,
display: 'flex',
justifyContent: 'flex-start',
+ marginBottom: theme.spacing.unit * 3,
},
contentContainer: {
width: '100%',
@@ -43,7 +44,7 @@ export default theme =>
},
text: {
color: 'rgb(255, 255, 255)',
- fontSize: '14px',
+ fontSize: '1rem',
textAlign: 'left',
},
statusPending: {
diff --git a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourceItem.jsx b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourceItem.jsx
new file mode 100644
index 000000000..c74f8ac8f
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourceItem.jsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Grid from '@material-ui/core/Grid';
+import Typography from '@material-ui/core/Typography';
+
+const DataSourceItem = ({
+ classes,
+ Icon,
+ iconClassName,
+ labelClassName,
+ title,
+ subsectionText,
+ showSubsectionInfo,
+ showLearnMore,
+ learnMoreUrl,
+}) => (
+
+
+
+
+ {title}
+
+
+ {/* Hidden text with margin-left to align with subtitle */}
+ {showSubsectionInfo && (
+
+
+ {subsectionText}
+ {showLearnMore && learnMoreUrl && (
+ <>
+
+
+ Learn more →
+
+ >
+ )}
+
+
+ )}
+
+);
+
+DataSourceItem.propTypes = {
+ classes: PropTypes.object.isRequired,
+ Icon: PropTypes.func.isRequired,
+ iconClassName: PropTypes.string.isRequired,
+ labelClassName: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ subsectionText: PropTypes.string.isRequired,
+ showSubsectionInfo: PropTypes.bool.isRequired,
+ showLearnMore: PropTypes.bool,
+ learnMoreUrl: PropTypes.string,
+};
+
+DataSourceItem.defaultProps = {
+ showLearnMore: false,
+ learnMoreUrl: null,
+};
+
+export default DataSourceItem;
diff --git a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx
index 426754f71..6dac4ff06 100644
--- a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx
+++ b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx
@@ -1,16 +1,102 @@
-import React from 'react';
+import React, { useState } from 'react';
import Typography from '@material-ui/core/Typography';
+import IconButton from '@material-ui/core/IconButton';
+import Grid from '@material-ui/core/Grid';
+import Switch from '@material-ui/core/Switch';
import { withStyles } from '@material-ui/core/styles';
+import InfoIcon from '@material-ui/icons/Info';
+import DialogTooltip from '../../../Contribute/DialogTooltip';
+import DataSourceItem from './DataSourceItem';
+import {
+ DATA_SOURCES_TOOLTIP_TEXT,
+ DATA_SOURCES_LEARN_MORE_URL,
+ DATA_SOURCES_ITEMS,
+} from './constants';
import productionLocationDetailsDataSourcesInfoStyles from './styles';
-const ProductionLocationDetailsDataSourcesInfo = ({ classes, className }) => (
-
-
- Understanding Data Sources
-
-
-);
+const ProductionLocationDetailsDataSourcesInfo = ({ classes, className }) => {
+ const [showSubsectionInfo, setShowSubsectionInfo] = useState(false);
+
+ return (
+
+
+
+ Understanding Data Sources
+
+
+
+ Learn more →
+
+
+ }
+ interactive
+ childComponent={
+
+
+
+ }
+ />
+
+
+ {showSubsectionInfo ? 'Close' : 'Open'}
+
+ setShowSubsectionInfo(e.target.checked)}
+ color="primary"
+ size="small"
+ className={classes.switch}
+ inputProps={{
+ 'aria-label':
+ 'Show extra info under each data source',
+ }}
+ />
+
+
+
+ {DATA_SOURCES_ITEMS.map(item => (
+
+ ))}
+
+
+ );
+};
export default withStyles(productionLocationDetailsDataSourcesInfoStyles)(
ProductionLocationDetailsDataSourcesInfo,
diff --git a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/constants.js b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/constants.js
new file mode 100644
index 000000000..4f8ba561f
--- /dev/null
+++ b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/constants.js
@@ -0,0 +1,40 @@
+import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+import People from '@material-ui/icons/People';
+
+import HandshakeIcon from '../../../HandshakeIcon';
+
+export const DATA_SOURCES_TOOLTIP_TEXT =
+ 'Open Supply Hub is collaboratively mapping global supply chains. This model means that data comes into the platform in a few ways.';
+export const DATA_SOURCES_LEARN_MORE_URL =
+ 'https://info.opensupplyhub.org/resources/an-open-data-model';
+
+export const DATA_SOURCES_ITEMS = Object.freeze([
+ Object.freeze({
+ Icon: CheckCircleOutline,
+ iconClassNameKey: 'iconClaimed',
+ labelClassNameKey: 'labelClaimed',
+ title: 'Claimed',
+ subsectionText:
+ 'General information & operational details submitted by production location',
+ learnMoreUrl:
+ 'https://info.opensupplyhub.org/resources/claim-a-facility',
+ }),
+ Object.freeze({
+ Icon: People,
+ iconClassNameKey: 'iconCrowdsourced',
+ labelClassNameKey: 'labelCrowdsourced',
+ title: 'Crowdsourced',
+ subsectionText:
+ "General information shared by supply chain stakeholders & OS Hub's research team",
+ learnMoreUrl: DATA_SOURCES_LEARN_MORE_URL,
+ }),
+ Object.freeze({
+ Icon: HandshakeIcon,
+ iconClassNameKey: 'iconPartner',
+ labelClassNameKey: 'labelPartner',
+ title: 'Partner Data',
+ subsectionText:
+ 'Additional social or environmental information shared by third party platforms',
+ learnMoreUrl: 'https://info.opensupplyhub.org/data-integrations',
+ }),
+]);
diff --git a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/styles.js b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/styles.js
index 65fa1494c..c645e9589 100644
--- a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/styles.js
+++ b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/styles.js
@@ -1,9 +1,119 @@
-export default theme =>
- Object.freeze({
+import { getTypographyStyles } from '../../../../util/typographyStyles';
+import COLOURS from '../../../../util/COLOURS';
+import commonStyles from '../../commonStyles';
+
+export default theme => {
+ const typography = getTypographyStyles(theme);
+ const spacing = theme.spacing.unit ?? 8;
+ return Object.freeze({
container: Object.freeze({
- backgroundColor: 'white',
+ ...commonStyles(theme).container,
+ padding: '20px 20px 20px 36px',
}),
- title: Object.freeze({
- marginBottom: theme.spacing.unit,
+ titleRow: Object.freeze({
+ display: 'flex',
+ flexWrap: 'nowrap',
+ alignItems: 'center',
+ marginBottom: spacing * 2,
+ }),
+ sectionTitle: Object.freeze({
+ ...typography.sectionTitle,
+ marginTop: 0,
+ marginBottom: 0,
+ marginRight: 0,
+ }),
+ infoButton: Object.freeze({
+ marginLeft: spacing * 0.5,
+ padding: spacing * 0.5,
+ color: theme.palette.text.secondary,
+ '&:hover': {
+ color: theme.palette.text.primary,
+ backgroundColor: theme.palette.action.hover,
+ },
+ }),
+ switchWrap: Object.freeze({
+ display: 'flex',
+ alignItems: 'center',
+ marginLeft: 'auto',
+ }),
+ switchLabel: Object.freeze({
+ ...typography.bodyText,
+ fontSize: '0.875rem',
+ }),
+ switch: Object.freeze({}),
+ descriptionList: Object.freeze({
+ marginTop: 0,
+ '& > *:nth-child(2)': {
+ [theme.breakpoints.up('md')]: {
+ paddingLeft: spacing * 2,
+ paddingRight: spacing * 2,
+ },
+ },
+ }),
+ descriptionItem: Object.freeze({
+ display: 'block',
+ }),
+ itemRow: Object.freeze({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px',
+ }),
+ itemHiddenTextWrap: Object.freeze({
+ marginTop: spacing * 0.5,
+ marginLeft: 20 + spacing,
+ }),
+ iconCrowdsourced: Object.freeze({
+ flexShrink: 0,
+ width: 20,
+ height: 20,
+ fontSize: 20,
+ color: COLOURS.ORANGE,
+ }),
+ iconClaimed: Object.freeze({
+ flexShrink: 0,
+ width: 20,
+ height: 20,
+ fontSize: 20,
+ color: COLOURS.DARK_GREEN,
+ }),
+ iconPartner: Object.freeze({
+ flexShrink: 0,
+ width: 20,
+ height: 20,
+ fontSize: 20,
+ color: COLOURS.TEAL_GREEN,
+ }),
+ label: Object.freeze({
+ ...typography.formLabelTight,
+ fontSize: '1.125rem',
+ }),
+ labelClaimed: Object.freeze({
+ ...typography.formLabelTight,
+ fontSize: '1.125rem',
+ color: COLOURS.DARK_GREEN,
+ }),
+ labelCrowdsourced: Object.freeze({
+ ...typography.formLabelTight,
+ fontSize: '1.125rem',
+ color: COLOURS.ORANGE,
+ }),
+ labelPartner: Object.freeze({
+ ...typography.formLabelTight,
+ fontSize: '1.125rem',
+ color: COLOURS.TEAL_GREEN,
+ }),
+ subsectionText: Object.freeze({
+ ...typography.bodyText,
+ fontSize: '0.875rem',
+ marginTop: 0,
+ marginBottom: 0,
+ }),
+ learnMoreLink: Object.freeze({
+ color: theme.palette.primary.main,
+ textDecoration: 'none',
+ '&:hover': {
+ textDecoration: 'underline',
+ },
}),
});
+};
diff --git a/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx b/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx
index 04069dc81..ccde1583d 100644
--- a/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx
+++ b/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx
@@ -1,17 +1,128 @@
import React from 'react';
+import PropTypes from 'prop-types';
import Typography from '@material-ui/core/Typography';
+import Button from '@material-ui/core/Button';
+import IconButton from '@material-ui/core/IconButton';
import { withStyles } from '@material-ui/core/styles';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { toast } from 'react-toastify';
+import get from 'lodash/get';
+import InfoIcon from '@material-ui/icons/Info';
+
+import CopySearch from '../../../CopySearch';
+import ContentCopyIcon from '../../../ContentCopyIcon';
+import DialogTooltip from '../../../Contribute/DialogTooltip';
import productionLocationDetailsTitleStyles from './styles';
-const ProductionLocationDetailsTitle = ({ classes }) => (
-
-
- Production location title
-
- OS ID: xxxxxxxxxxxxxxx
-
-);
+const OS_ID_TOOLTIP_TEXT =
+ 'The OS ID is a free, unique identifier automatically assigned to each production location in OS Hub. Use it to track this location across systems, share it with partners, or reference it in compliance documentation.';
+const OS_ID_LEARN_MORE_URL = 'https://info.opensupplyhub.org/resources/os-id';
+
+const ProductionLocationDetailsTitle = ({ classes, data }) => {
+ const locationName = get(data, 'properties.name', '') || '';
+ const osId = get(data, 'properties.os_id', '') || '';
+
+ return (
+
+ {/* h1: Page title per typographyStyles */}
+
+ {locationName}
+
+
+
+
+ OS ID: {osId}
+
+
+
+ Learn more →
+
+
+ }
+ interactive
+ childComponent={
+
+
+
+ }
+ />
+
+ {osId && (
+
+
+
+
+
+
+ Copy Link
+
+
+
+
+
+
+ toast('Copied OS ID to clipboard')
+ }
+ >
+
+
+
+ Copy OS ID
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+ProductionLocationDetailsTitle.propTypes = {
+ classes: PropTypes.object.isRequired,
+ data: PropTypes.object,
+};
+
+ProductionLocationDetailsTitle.defaultProps = {
+ data: null,
+};
export default withStyles(productionLocationDetailsTitleStyles)(
ProductionLocationDetailsTitle,
diff --git a/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js b/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js
index 65fa1494c..ff2a31522 100644
--- a/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js
+++ b/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js
@@ -1,9 +1,64 @@
-export default theme =>
- Object.freeze({
+import { getTypographyStyles } from '../../../../util/typographyStyles';
+import commonStyles from '../../commonStyles';
+
+export default theme => {
+ const typography = getTypographyStyles(theme);
+ const spacing = theme.spacing.unit ?? 8;
+ return Object.freeze({
container: Object.freeze({
- backgroundColor: 'white',
+ ...commonStyles(theme).container,
+ marginBottom: spacing * 3,
+ padding: '20px 20px 20px 36px',
}),
title: Object.freeze({
- marginBottom: theme.spacing.unit,
+ ...typography.formLabelTight,
+ fontSize: '3rem',
+ fontWeight: 700,
+ marginTop: 0,
+ marginBottom: spacing,
+ }),
+ osIdRow: Object.freeze({
+ display: 'flex',
+ flexWrap: 'wrap',
+ alignItems: 'center',
+ gap: `${spacing}px ${spacing * 2}px`,
+ }),
+ osIdValueWithTooltip: Object.freeze({
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: spacing * 0.5,
+ }),
+ osIdValue: Object.freeze({
+ ...typography.inlineHighlight,
+ fontSize: '21px',
+ }),
+ osIdInfoButton: Object.freeze({
+ padding: spacing * 0.5,
+ color: theme.palette.text.secondary,
+ '&:hover': {
+ color: theme.palette.text.primary,
+ backgroundColor: theme.palette.action.hover,
+ },
+ }),
+ osIdActions: Object.freeze({
+ display: 'inline-flex',
+ flexWrap: 'wrap',
+ marginLeft: 'auto',
+ }),
+ copyButtonWrap: Object.freeze({
+ display: 'inline-flex',
+ marginLeft: spacing * 2,
+ }),
+ copyButtonWrapFirst: Object.freeze({
+ marginLeft: 0,
+ }),
+ copyButton: Object.freeze({
+ textTransform: 'none',
+ minWidth: 'auto',
+ }),
+ buttonText: Object.freeze({
+ marginLeft: spacing * 0.5,
+ fontSize: '14px',
}),
});
+};
diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx
index 4039e799a..8ad8cd3de 100644
--- a/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx
+++ b/src/react/src/components/ProductionLocation/ProductionLocationDetails/ProductionLocationDetails.jsx
@@ -40,7 +40,7 @@ function ProductionLocationDetails({
Production Location Details
-
+
{
marginTop: '25px',
}),
sectionDescription: Object.freeze({
- fontSize: '18px',
+ fontSize: '1rem', // 16px
marginBottom: '10px',
}),
bodyText: Object.freeze({
- fontSize: '18px',
+ fontSize: '1rem', // 16px
color: theme.palette.text.secondary,
}),
inlineHighlight: Object.freeze({
- fontSize: '18px',
+ fontSize: '1rem', // 16px
fontWeight: 500,
color: theme.palette.text.primary,
display: 'inline',