diff --git a/doc/frontend.md b/doc/frontend.md new file mode 100644 index 000000000..b27941a09 --- /dev/null +++ b/doc/frontend.md @@ -0,0 +1,156 @@ +# Typography Guide + +How to use typography across the application: which shared styles to use, how to combine them with MUI `Typography`, and when to use each. + +--- + +## 1. Source of truth + +- **File:** `src/react/src/util/typographyStyles.js` +- **Usage:** Call `getTypographyStyles(theme)` in your component's `withStyles` (or `makeStyles`) and spread the returned keys into your class objects; then pass those classes to MUI `Typography` via `className={classes.xyz}`. +- **Rule:** Prefer these tokens over hardcoding font sizes/weights so headings, labels, body text, and links stay consistent. +- **Body text size:** Use `1rem` for body and paragraph text (section description, bodyText, inline highlight). In this app the root font size is the browser default (typically 16px), so 1rem resolves accordingly. +- **MUI v3 Typography variants:** Use `component` for semantics (e.g. `component="h1"`) and `variant` for MUI’s styling. Valid variants are: `"display4"`, `"display3"`, `"display2"`, `"display1"`, `"headline"`, `"title"`, `"subheading"`, `"body2"`, `"body1"`, `"caption"`, `"button"`, `"srOnly"`, `"inherit"`. Do not use `variant="h1"` / `"h2"` / `"h3"` (they are not supported in MUI v3). Use `variant="headline"` for page title, `variant="title"` for major/section headings, `variant="subheading"` for sub-sections. + +--- + +## 2. Semantic roles and which style to use + +| Role | When to use | Style key | Typical MUI usage | +|------|-------------|-----------|-------------------| +| **Page title** | Main title of a page (e.g. facility/location name) | (custom from `formLabelTight`) | `` | +| **Major section** | Big section blocks (e.g. claim intro) | (custom as needed) | `component="h2"`, `variant="title"` | +| **Section title** | Subsections ("Understanding Data Sources", "Partner Data", etc.) | `sectionTitle` | `component="h3"`, `variant="title"`, `className` with `...typography.sectionTitle` | +| **Field / UI label** | Form labels, bold labels ("OS ID:", "CLAIMED PROFILE", "Claimed", "Crowdsourced") | `formLabel` or `formLabelTight` | `component="span"` or `"label"`, `variant="body1"`, `className` with `...typography.formLabelTight` | +| **Body / paragraph** | Normal paragraphs, descriptions, "Show more" labels | `bodyText` | `component="p"` or `"span"`, `variant="body1"`, `className` with `...typography.bodyText` | +| **Section description** | Intro or explanatory block under a section | `sectionDescription` | `component="p"`, `variant="body1"`, `className` with `...typography.sectionDescription` | +| **Inline highlight** | Short emphasized bits (name, date, ID value) inline in text | `inlineHighlight` | `component="span"`, `className` with `...typography.inlineHighlight` | + +--- + +## 3. Style attributes (from `typographyStyles.js`) + +- **formLabel / formLabelRoot / formLabelTight** + Base: `fontSize: '21px'`, `fontWeight: 600`. + `formLabel` adds `margin: '24px 0 8px 0'`; `formLabelRoot` uses `marginTop: 0`, `marginBottom: '8px'`; `formLabelTight` has no extra margin. + Use for field labels and strong short labels. + +- **sectionTitle** + `fontSize: '24px'`, `fontWeight: theme.typography.fontWeightSemiBold`, `marginTop: '25px'`. + Use for h3-level section titles. + +- **sectionDescription** + `fontSize: '1rem'`, `marginBottom: '10px'`. + Use for the first paragraph or intro under a section. + +- **bodyText** + `fontSize: '1rem'`, `color: theme.palette.text.secondary`. + Use for regular body and secondary text. + +- **inlineHighlight** + `fontSize: '1rem'`, `fontWeight: 500`, `color: theme.palette.text.primary`, `display: 'inline'`. + Use for emphasized inline pieces (e.g. facility name, date, OS ID value). + +--- + +## 4. Headings + +- **h1 – Page title** + One per page (e.g. facility/location name). + Example: `component="h1"`, `variant="headline"`, `className` from styles that extend `formLabelTight` (e.g. 28px, 700 for prominence). + +- **h2 – Major section** + Top-level sections (e.g. claim intro). + `component="h2"`, `variant="title"`, plus custom class if needed. + +- **h3 – Section title** + Subsections ("Understanding Data Sources", "Partner Data", "Interactive map", etc.). + `component="h3"`, `variant="title"`, `className` with `...typography.sectionTitle` (and margin overrides as needed, e.g. `marginTop: 0`). + +- **h4 / h5 / h6** + Use sparingly for sub-subsections (e.g. ClaimFlag uses `h4` for "CLAIMED PROFILE"). + Prefer a class that extends `formLabelTight` or `sectionTitle` so sizing stays consistent. Use MUI v3 variants such as `"title"` or `"subheading"` (not `"h4"`/`"h5"`/`"h6"`). + +--- + +## 5. Regular text + +- **Paragraphs:** + `component="p"`, `variant="body1"`, and a class that includes `...typography.bodyText` (and optionally `...typography.sectionDescription` for intro blocks). + Body text uses `fontSize: '1rem'`. Override `fontSize` or `marginBottom` only when needed. + +- **Inline text / labels:** + `component="span"`, `variant="body1"`, and either `formLabelTight` (labels) or `bodyText` (secondary inline). + +--- + +## 6. Links + +- **Color:** `theme.palette.primary.main`. +- **Implementation:** + Use `` or `` with a style like `link: { color: theme.palette.primary.main }`. + For "Learn more →" in tooltips, inline `style={{ color: 'white' }}` is used for contrast on dark tooltip background; elsewhere use the theme primary color. +- **Optional:** Add `textDecoration: 'none'` and `'&:hover': { textDecoration: 'underline' }` for inline links (e.g. in DataSourcesInfo `learnMoreLink`). + +--- + +## 7. Usage pattern + +1. In the component's `styles.js`: + `import { getTypographyStyles } from '../../path/to/typographyStyles';` + then `const typography = getTypographyStyles(theme);` and use `...typography.sectionTitle`, etc., in your class objects. + +2. In the JSX: + Use MUI `Typography` with both: + - **Semantic props:** `component` (e.g. `component="h3"`) for the HTML element and `variant` for MUI v3 styling (e.g. `variant="title"` or `variant="subheading"` — use only valid MUI v3 variants, not `"h1"`/`"h2"`/`"h3"`). + - **Visual style:** `className={classes.yourClass}` where `yourClass` spreads the right typography token and any local overrides. + +3. Keep **one** clear hierarchy per page: one h1, then h2/h3 as needed; use the table in §2 to pick the right style key and component/variant. + +--- + +## 8. Quick reference + +| Use case | component | variant | Typography style key | +|----------|-----------|--------|----------------------| +| Page title (e.g. facility name) | h1 | headline | formLabelTight (custom size/weight) | +| Section title | h3 | title | sectionTitle | +| Field / subsection label | span | body1 | formLabelTight | +| Paragraph / body | p | body1 | bodyText or sectionDescription | +| Inline emphasis (name, ID, date) | span | — | inlineHighlight | +| Links | a or Link | — | color: theme.palette.primary.main | + +--- + +## 9. Container styles (commonStyles) + +- **File:** `src/react/src/components/ProductionLocation/commonStyles.js` +- **Purpose:** Shared base styles for section containers (e.g. cards, content blocks) so background and shadow stay consistent across the Production Location UI. + +### What it provides + +`commonStyles(theme)` returns an object with a `container` key: + +- **container:** `backgroundColor: theme.palette.background.white`, `boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'` + +### How to use it + +Spread `commonStyles(theme).container` into your own container class and add any overrides (margin, padding, etc.). This keeps the same background and shadow while letting each component set its own spacing. + +**Import in your styles file:** + +```js +import commonStyles from '../../commonStyles'; // adjust path to your component +``` + +**Example – extend and override:** + +```js +container: Object.freeze({ + ...commonStyles(theme).container, + marginBottom: spacing * 3, + padding: '20px 20px 20px 36px', +}), +``` + +Here the container gets the shared `backgroundColor` and `boxShadow` from `commonStyles`, plus component-specific `marginBottom` and `padding`. Use this pattern for any Production Location section that should look like a card (e.g. LocationTitle, DataSourcesInfo). diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index f01886e24..4d03abf88 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -37,6 +37,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html * [OSDEV-2374](https://opensupplyhub.atlassian.net/browse/OSDEV-2374) - Created UI for Claim and Closure status banners. * [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. +* [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. ### Release instructions * Ensure that the following commands are included in the `post_deployment` command: diff --git a/src/react/src/__tests__/components/DataSourcesInfo.test.jsx b/src/react/src/__tests__/components/DataSourcesInfo.test.jsx new file mode 100644 index 000000000..e7d655934 --- /dev/null +++ b/src/react/src/__tests__/components/DataSourcesInfo.test.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; +import ProductionLocationDetailsDataSourcesInfo from '../../components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo'; + +jest.mock('../../components/Contribute/DialogTooltip', () => { + function MockDialogTooltip({ childComponent }) { + return <>{childComponent}; + } + return MockDialogTooltip; +}); + +const theme = createMuiTheme(); + +const renderDataSourcesInfo = (props = {}) => + render( + + + , + ); + +describe('ProductionLocation DataSourcesInfo', () => { + test('renders without crashing', () => { + renderDataSourcesInfo(); + + expect( + screen.getByRole('heading', { name: 'Understanding Data Sources' }), + ).toBeInTheDocument(); + }); + + test('renders section title "Understanding Data Sources"', () => { + renderDataSourcesInfo(); + + expect( + screen.getByRole('heading', { level: 3, name: 'Understanding Data Sources' }), + ).toBeInTheDocument(); + }); + + test('shows "Open" label when toggle is off', () => { + renderDataSourcesInfo(); + + expect(screen.getByText('Open')).toBeInTheDocument(); + expect(screen.queryByText('Close')).not.toBeInTheDocument(); + }); + + test('shows "Close" label when toggle is switched on', () => { + renderDataSourcesInfo(); + + const switchControl = screen.getByRole('checkbox', { + name: /show extra info under each data source/i, + }); + fireEvent.click(switchControl); + + expect(screen.getByText('Close')).toBeInTheDocument(); + expect(screen.queryByText('Open')).not.toBeInTheDocument(); + }); + + test('renders all three data source items', () => { + renderDataSourcesInfo(); + + expect(screen.getByText('Claimed')).toBeInTheDocument(); + expect(screen.getByText('Crowdsourced')).toBeInTheDocument(); + expect(screen.getByText('Partner Data')).toBeInTheDocument(); + }); + + test('toggle switch shows subsection text when opened', () => { + renderDataSourcesInfo(); + + expect( + screen.queryByText(/General information & operational details submitted by production location/), + ).not.toBeInTheDocument(); + + const switchControl = screen.getByRole('checkbox', { + name: /show extra info under each data source/i, + }); + fireEvent.click(switchControl); + + expect( + screen.getByText(/General information & operational details submitted by production location/), + ).toBeInTheDocument(); + }); + + test('renders info button for data sources tooltip', () => { + renderDataSourcesInfo(); + + expect( + screen.getByRole('button', { + name: /more information about data sources/i, + }), + ).toBeInTheDocument(); + }); + + test('applies custom className when provided', () => { + const { container } = renderDataSourcesInfo({ + className: 'custom-class', + }); + + const wrapper = container.querySelector('.custom-class'); + expect(wrapper).toBeInTheDocument(); + }); +}); diff --git a/src/react/src/__tests__/components/LocationTitle.test.jsx b/src/react/src/__tests__/components/LocationTitle.test.jsx new file mode 100644 index 000000000..1ece17077 --- /dev/null +++ b/src/react/src/__tests__/components/LocationTitle.test.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; +import ProductionLocationDetailsTitle from '../../components/ProductionLocation/Heading/LocationTitle/LocationTitle'; + +jest.mock('react-toastify', () => ({ + toast: jest.fn(), +})); + +jest.mock('../../components/CopySearch', () => { + function MockCopySearch({ children }) { + return <>{children}; + } + return MockCopySearch; +}); + +jest.mock('../../components/Contribute/DialogTooltip', () => { + function MockDialogTooltip({ childComponent }) { + return <>{childComponent}; + } + return MockDialogTooltip; +}); + +const theme = createMuiTheme(); + +const renderLocationTitle = (props = {}) => + render( + + + , + ); + +describe('ProductionLocation LocationTitle', () => { + test('renders without crashing when data is null', () => { + renderLocationTitle({ data: null }); + + expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/OS ID:/); + }); + + test('renders location name from data.properties.name', () => { + const data = { + properties: { + name: 'Test Facility Name', + os_id: 'CN2021250D1DTN7', + }, + }; + + renderLocationTitle({ data }); + + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Test Facility Name'); + }); + + test('renders OS ID from data.properties.os_id', () => { + const data = { + properties: { + name: 'Test Facility', + os_id: 'CN2021250D1DTN7', + }, + }; + + renderLocationTitle({ data }); + + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('OS ID: CN2021250D1DTN7'); + }); + + test('shows Copy Link and Copy OS ID buttons when os_id is present', () => { + const data = { + properties: { + name: 'Test Facility', + os_id: 'CN2021250D1DTN7', + }, + }; + + renderLocationTitle({ data }); + + expect(screen.getByRole('button', { name: /copy link/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /copy os id/i })).toBeInTheDocument(); + }); + + test('does not show copy buttons when os_id is missing', () => { + const data = { + properties: { + name: 'Test Facility', + }, + }; + + renderLocationTitle({ data }); + + expect(screen.queryByRole('button', { name: /copy link/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /copy os id/i })).not.toBeInTheDocument(); + }); + + test('does not show copy buttons when data is null', () => { + renderLocationTitle({ data: null }); + + expect(screen.queryByRole('button', { name: /copy link/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /copy os id/i })).not.toBeInTheDocument(); + }); + + test('renders info button for OS ID tooltip', () => { + const data = { + properties: { + name: 'Test', + os_id: 'US123', + }, + }; + + renderLocationTitle({ data }); + + expect( + screen.getByRole('button', { name: /more information about os id/i }), + ).toBeInTheDocument(); + }); +}); diff --git a/src/react/src/components/Contribute/DialogTooltip.jsx b/src/react/src/components/Contribute/DialogTooltip.jsx index aa2310e9b..860aaa237 100644 --- a/src/react/src/components/Contribute/DialogTooltip.jsx +++ b/src/react/src/components/Contribute/DialogTooltip.jsx @@ -93,7 +93,6 @@ const DialogTooltip = ({ 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 && ( + + + + + + + + + toast('Copied OS ID to clipboard') + } + > + + + + + )} +
+
+ ); +}; + +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',