diff --git a/src/react/src/__tests__/components/LocationTitle.test.jsx b/src/react/src/__tests__/components/LocationTitle.test.jsx index 1ece17077..76292a30b 100644 --- a/src/react/src/__tests__/components/LocationTitle.test.jsx +++ b/src/react/src/__tests__/components/LocationTitle.test.jsx @@ -3,24 +3,6 @@ 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 = {}) => @@ -35,81 +17,30 @@ describe('ProductionLocation LocationTitle', () => { renderLocationTitle({ data: null }); expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); - expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/OS ID:/); + expect(screen.getByText('Location Name')).toBeInTheDocument(); }); 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(); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent( + 'Test Facility Name', + ); }); - test('does not show copy buttons when os_id is missing', () => { + test('renders empty heading when data.properties.name 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', - }, + properties: {}, }; renderLocationTitle({ data }); - expect( - screen.getByRole('button', { name: /more information about os id/i }), - ).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(''); }); }); diff --git a/src/react/src/__tests__/components/OsIdBadge.test.jsx b/src/react/src/__tests__/components/OsIdBadge.test.jsx new file mode 100644 index 000000000..c5644d407 --- /dev/null +++ b/src/react/src/__tests__/components/OsIdBadge.test.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; +import ProductionLocationDetailsOsIdBadge from '../../components/ProductionLocation/Heading/osIdBadge/OsIdBadge'; + +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 renderOsIdBadge = (props = {}) => + render( + + + , + ); + +describe('ProductionLocationDetailsOsIdBadge', () => { + test('renders without crashing', () => { + renderOsIdBadge({ osId: 'CN2021250D1DTN7' }); + + expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); + }); + + test('renders OS ID label and value', () => { + renderOsIdBadge({ osId: 'CN2021250D1DTN7' }); + + expect( + screen.getByRole('heading', { level: 2 }), + ).toHaveTextContent('OS ID: CN2021250D1DTN7'); + }); + + test('renders info button for OS ID tooltip', () => { + renderOsIdBadge({ osId: 'CN2021250D1DTN7' }); + + expect( + screen.getByRole('button', { + name: /more information about os id/i, + }), + ).toBeInTheDocument(); + }); + + test('shows Copy Link and Copy OS ID buttons when osId is present', () => { + renderOsIdBadge({ osId: 'CN2021250D1DTN7' }); + + 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 osId is empty', () => { + renderOsIdBadge({ osId: '' }); + + expect(screen.queryByRole('button', { name: /copy link/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /copy os id/i })).not.toBeInTheDocument(); + }); +}); diff --git a/src/react/src/components/ProductionLocation/Heading/ClaimFlag/styles.js b/src/react/src/components/ProductionLocation/Heading/ClaimFlag/styles.js index 470ec98b1..e60762923 100644 --- a/src/react/src/components/ProductionLocation/Heading/ClaimFlag/styles.js +++ b/src/react/src/components/ProductionLocation/Heading/ClaimFlag/styles.js @@ -7,8 +7,7 @@ export default theme => { return Object.freeze({ ...typography, root: { - marginTop: spacing * 3, - marginBottom: spacing * 3, + marginBottom: spacing * 2, flexDirection: 'column', padding: 0, marginLeft: 0, diff --git a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js index cd86f2b1d..383faa56a 100644 --- a/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js +++ b/src/react/src/components/ProductionLocation/Heading/ClosureStatus/styles.js @@ -5,7 +5,7 @@ export default theme => borderRadius: 0, display: 'flex', justifyContent: 'flex-start', - marginBottom: theme.spacing.unit * 3, + marginBottom: theme.spacing.unit * 2, }, contentContainer: { width: '100%', diff --git a/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx b/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx index ccde1583d..a434ace8e 100644 --- a/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx +++ b/src/react/src/components/ProductionLocation/Heading/LocationTitle/LocationTitle.jsx @@ -1,31 +1,19 @@ 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 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 */} + + Location Name + { > {locationName} -
- - - OS ID: {osId} - - - - Learn more → - -

- } - interactive - childComponent={ - - - - } - /> -
- {osId && ( - - - - - - - - - toast('Copied OS ID to clipboard') - } - > - - - - - )} -
); }; diff --git a/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js b/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js index ff2a31522..fd835ca72 100644 --- a/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js +++ b/src/react/src/components/ProductionLocation/Heading/LocationTitle/styles.js @@ -1,14 +1,11 @@ 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({ - ...commonStyles(theme).container, - marginBottom: spacing * 3, - padding: '20px 20px 20px 36px', + padding: '0 20px 0 0', }), title: Object.freeze({ ...typography.formLabelTight, @@ -17,48 +14,8 @@ export default theme => { 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', + titleAccent: Object.freeze({ + ...typography.bodyText, }), }); }; diff --git a/src/react/src/components/ProductionLocation/Heading/osIdBadge/OsIdBadge.jsx b/src/react/src/components/ProductionLocation/Heading/osIdBadge/OsIdBadge.jsx new file mode 100644 index 000000000..2e26cdcf8 --- /dev/null +++ b/src/react/src/components/ProductionLocation/Heading/osIdBadge/OsIdBadge.jsx @@ -0,0 +1,104 @@ +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 InfoIcon from '@material-ui/icons/Info'; + +import CopySearch from '../../../CopySearch'; +import ContentCopyIcon from '../../../ContentCopyIcon'; +import DialogTooltip from '../../../Contribute/DialogTooltip'; + +import productionLocationDetailsOsIdBadgeStyles from './styles'; +import { OS_ID_TOOLTIP_TEXT, OS_ID_LEARN_MORE_URL } from './constants'; + +const ProductionLocationDetailsOsIdBadge = ({ classes, osId }) => ( +
+
+ + OS ID: {osId} + + + + Learn more → + +

+ } + interactive + childComponent={ + + + + } + /> +
+ {osId && ( +
+ + toast('Copied OS ID to clipboard')} + > + + + + + + + + +
+ )} +
+); + +ProductionLocationDetailsOsIdBadge.propTypes = { + classes: PropTypes.object.isRequired, + osId: PropTypes.string.isRequired, +}; + +export default withStyles(productionLocationDetailsOsIdBadgeStyles)( + ProductionLocationDetailsOsIdBadge, +); diff --git a/src/react/src/components/ProductionLocation/Heading/osIdBadge/constants.js b/src/react/src/components/ProductionLocation/Heading/osIdBadge/constants.js new file mode 100644 index 000000000..9e1e21819 --- /dev/null +++ b/src/react/src/components/ProductionLocation/Heading/osIdBadge/constants.js @@ -0,0 +1,5 @@ +export 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.'; + +export const OS_ID_LEARN_MORE_URL = + 'https://info.opensupplyhub.org/resources/os-id'; diff --git a/src/react/src/components/ProductionLocation/Heading/osIdBadge/styles.js b/src/react/src/components/ProductionLocation/Heading/osIdBadge/styles.js new file mode 100644 index 000000000..1b458b732 --- /dev/null +++ b/src/react/src/components/ProductionLocation/Heading/osIdBadge/styles.js @@ -0,0 +1,62 @@ +import COLOURS from '../../../../util/COLOURS'; +import { getTypographyStyles } from '../../../../util/typographyStyles'; + +export default theme => { + const typography = getTypographyStyles(theme); + const spacing = theme.spacing.unit ?? 8; + return Object.freeze({ + osIdRow: Object.freeze({ + display: 'flex', + flexWrap: 'wrap', + alignItems: 'center', + gap: `${spacing}px ${spacing * 2}px`, + padding: '12px', + border: `1px solid ${COLOURS.LIGHT_PURPLE_BORDER}`, + backgroundColor: 'rgba(128, 64, 191, 0.05)', + marginBottom: theme.spacing.unit * 2, + }), + osIdValueWithTooltip: Object.freeze({ + display: 'inline-flex', + alignItems: 'center', + gap: spacing * 0.5, + }), + osIdLabel: Object.freeze({ + fontWeight: 'bold', + ...typography.formLabelTight, + }), + osIdValue: Object.freeze({ + fontWeight: 'bold', + fontSize: '1.75rem', + }), + 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({ + backgroundColor: COLOURS.WHITE, + display: 'inline-flex', + marginLeft: spacing * 2, + }), + copyButtonWrapFirst: Object.freeze({ + backgroundColor: COLOURS.WHITE, + 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/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx index bb9301483..0d5c4dca5 100644 --- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx +++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx @@ -88,7 +88,7 @@ function ProductionLocationDetailsContainer({ } return ( - + diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx index 3d491b8d2..46e59c711 100644 --- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx +++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; +import get from 'lodash/get'; import Grid from '@material-ui/core/Grid'; import Divider from '@material-ui/core/Divider'; @@ -16,6 +17,7 @@ import DetailsMap from '../ProductionLocationDetailsMap/ProductionLocationDetail import { facilityClaimStatusChoicesEnum } from '../../../util/constants'; import productionLocationDetailsContentStyles from './styles'; +import OsIdBadge from '../Heading/osIdBadge/OsIdBadge'; const ProductionLocationDetailsContent = ({ classes, @@ -29,6 +31,7 @@ const ProductionLocationDetailsContent = ({ data?.properties?.claim_info?.status === facilityClaimStatusChoicesEnum.PENDING; const isClaimed = !isPendingClaim && !!data?.properties?.claim_info; + const osId = get(data, 'properties.os_id', '') || ''; return (
@@ -40,6 +43,7 @@ const ProductionLocationDetailsContent = ({ claimInfo={data?.properties?.claim_info} isEmbed={!!embed} /> + Object.freeze({ - container: Object.freeze({}), + container: Object.freeze({ + [theme.breakpoints.up('md')]: { + paddingLeft: '40px', + }, + }), containerItem: Object.freeze({ marginBottom: theme.spacing.unit, }),