diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index 4d03abf88..7190c1b16 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -38,6 +38,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html * [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. +* [OSDEV-2366](https://opensupplyhub.atlassian.net/browse/OSDEV-2366) - Added "Jump to" section to the sidebar with links to the different sections of the Production Location page. ### Release instructions * Ensure that the following commands are included in the `post_deployment` command: diff --git a/src/react/src/__tests__/components/NavBar.test.js b/src/react/src/__tests__/components/NavBar.test.js new file mode 100644 index 000000000..2d470f2b1 --- /dev/null +++ b/src/react/src/__tests__/components/NavBar.test.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { screen, fireEvent } from '@testing-library/react'; +import renderWithProviders from '../../util/testUtils/renderWithProviders'; +import NavBar from '../../components/ProductionLocation/Sidebar/NavBar/NavBar'; + +const renderNavBar = () => + renderWithProviders( + + + , + ); + +describe('NavBar', () => { + test('renders the "Jump to" heading', () => { + renderNavBar(); + + expect(screen.getByText('Jump to')).toBeInTheDocument(); + }); + + test('renders all three navigation items', () => { + renderNavBar(); + + expect(screen.getByText('Overview')).toBeInTheDocument(); + expect(screen.getByText('General Information')).toBeInTheDocument(); + expect(screen.getByText('Operational Details')).toBeInTheDocument(); + }); + + test('clicking a link scrolls to the matching section', () => { + renderNavBar(); + + const scrollIntoView = jest.fn(); + const target = document.createElement('div'); + target.id = 'overview'; + target.scrollIntoView = scrollIntoView; + document.body.appendChild(target); + + fireEvent.click(screen.getByText('Overview')); + + expect(scrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'start', + }); + + document.body.removeChild(target); + }); + + test('clicking a link does nothing when the target element is missing', () => { + renderNavBar(); + + expect(() => { + fireEvent.click(screen.getByText('General Information')); + }).not.toThrow(); + }); +}); diff --git a/src/react/src/__tests__/components/ProductionLocationDetailsBackToSearch.test.js b/src/react/src/__tests__/components/ProductionLocationDetailsBackToSearch.test.js new file mode 100644 index 000000000..9d7376c7f --- /dev/null +++ b/src/react/src/__tests__/components/ProductionLocationDetailsBackToSearch.test.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import ProductionLocationDetailsBackToSearch from '../../components/ProductionLocation/Sidebar/BackToSearch/BackToSearch'; +import renderWithProviders from '../../util/testUtils/renderWithProviders'; +import { setupStore } from '../../configureStore'; + +describe('ProductionLocationDetailsBackToSearch component', () => { + const mockPush = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the back to search link', () => { + const { getByText } = renderWithProviders( + , + ); + + expect(getByText('Back to search results')).toBeInTheDocument(); + }); + + it('renders a link pointing to the facilities route', () => { + const { container } = renderWithProviders( + , + ); + + const link = container.querySelector('a'); + expect(link).toHaveAttribute('href', '/facilities'); + }); + + it('renders the back arrow icon', () => { + const { container } = renderWithProviders( + , + ); + + const svg = container.querySelector('svg'); + expect(svg).toBeInTheDocument(); + }); + + it('dispatches resetSingleFacility when clicked', () => { + const reduxStore = setupStore({}); + const dispatchSpy = jest.spyOn(reduxStore, 'dispatch'); + + const { getByText } = renderWithProviders( + , + { reduxStore }, + ); + + fireEvent.click(getByText('Back to search results')); + + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ type: 'RESET_SINGLE_FACILITY' }), + ); + }); +}); diff --git a/src/react/src/components/Icons/GeneralInformation.jsx b/src/react/src/components/Icons/GeneralInformation.jsx new file mode 100644 index 000000000..4192825d1 --- /dev/null +++ b/src/react/src/components/Icons/GeneralInformation.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export default function GeneralInformation(props) { + return ( + + + + + ); +} diff --git a/src/react/src/components/Icons/OperationalDetails.jsx b/src/react/src/components/Icons/OperationalDetails.jsx new file mode 100644 index 000000000..c8b3a0db6 --- /dev/null +++ b/src/react/src/components/Icons/OperationalDetails.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export default function OperationalDetails(props) { + return ( + + + + + ); +} diff --git a/src/react/src/components/Icons/Overview.jsx b/src/react/src/components/Icons/Overview.jsx new file mode 100644 index 000000000..9ca67bc22 --- /dev/null +++ b/src/react/src/components/Icons/Overview.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +export default function Overview(props) { + return ( + + + + + + + + + + ); +} diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx index 0d5c4dca5..c06e7a03b 100644 --- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx +++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContainer/ProductionLocationDetailsContainer.jsx @@ -91,9 +91,11 @@ function ProductionLocationDetailsContainer({ - - - + + + + + color: theme.palette.error.main, marginBottom: theme.spacing.unit, }), + sidebar: { + [theme.breakpoints.up('md')]: { + position: 'sticky', + top: '10px', + alignSelf: 'flex-start', + }, + [theme.breakpoints.up('lg')]: { + top: '120px', + }, + }, }); diff --git a/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx b/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx index 1ba1ecd95..5dabb0d02 100644 --- a/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx +++ b/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/BackToSearch.jsx @@ -1,8 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import ArrowBack from '@material-ui/icons/ArrowBackIos'; +import ArrowRightAlt from '@material-ui/icons/ArrowRightAlt'; import { resetSingleFacility } from '../../../../actions/facilities'; import { facilitiesRoute } from '../../../../util/constants'; @@ -14,19 +13,22 @@ function ProductionLocationDetailsBackToSearch({ clearFacility, history: { push }, }) { + const onClick = event => { + event.preventDefault(); + clearFacility(); + push(facilitiesRoute); + }; + return (
- + + Back to search results +
); } diff --git a/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/styles.js b/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/styles.js index 5b58f2129..60a218add 100644 --- a/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/styles.js +++ b/src/react/src/components/ProductionLocation/Sidebar/BackToSearch/styles.js @@ -1,12 +1,25 @@ export default theme => ({ buttonContainer: { - alignItems: 'center', - backgroundColor: 'white', - marginBottom: theme.spacing.unit, + marginBottom: theme.spacing.unit * 2, }, - backButton: { + backLink: { + fontSize: `1.25rem`, textTransform: 'none', width: '100%', fontWeight: 700, + display: 'flex', + alignItems: 'center', + textDecoration: 'none', + }, + icon: { + transform: 'rotate(180deg)', + fontSize: `1.75rem`, + }, + text: { + fontSize: `1.25rem`, + fontWeight: 700, + display: 'inline-block', + marginBottom: theme.spacing.unit / 2, + marginLeft: theme.spacing.unit / 2, }, }); diff --git a/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx b/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx index 506d1ea59..62dfab2b8 100644 --- a/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx +++ b/src/react/src/components/ProductionLocation/Sidebar/NavBar/NavBar.jsx @@ -6,38 +6,69 @@ import MenuItem from '@material-ui/core/MenuItem'; import MenuList from '@material-ui/core/MenuList'; import Typography from '@material-ui/core/Typography'; +import Overview from '../../../Icons/Overview'; +import GeneralInformation from '../../../Icons/GeneralInformation'; +import OperationalDetails from '../../../Icons/OperationalDetails'; + import navBarStyles from './styles'; +const navItems = [ + { to: '#overview', label: 'Overview', Icon: Overview }, + { + to: '#general-information', + label: 'General Information', + Icon: GeneralInformation, + }, + { + to: '#operational-details', + label: 'Operational Details', + Icon: OperationalDetails, + }, +]; + +const handleClick = (event, to) => { + event.preventDefault(); + const id = to.replace('#', ''); + const element = document.getElementById(id); + if (!element) return; + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); +}; + const NavBar = ({ classes }) => ( -
+
Jump to - - - Overview - - - Location - - - Claimed data - - - Assessments and Audits - - - Certifications - - - Emissions - - - Living Wage - - - Grievance Mechanism - + + {navItems.map(({ to, label, Icon, active }) => ( + + handleClick(event, to)} + className={classes.link} + > + + + {label} + + + + ))}
); diff --git a/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js b/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js index 46122fb3c..309edc583 100644 --- a/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js +++ b/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js @@ -1,10 +1,60 @@ +import COLOURS from '../../../../util/COLOURS'; +import commonStyles from '../../commonStyles'; + export default theme => Object.freeze({ - container: Object.freeze({ - backgroundColor: 'white', + ...commonStyles(theme), + navContainer: Object.freeze({ + padding: '12px', marginBottom: theme.spacing.unit, }), title: Object.freeze({ - marginBottom: theme.spacing.unit, + fontWeight: 600, + fontSize: '1.125rem', + margin: '0 0 12px 0', + }), + menuList: Object.freeze({ + padding: 0, + }), + menuItem: Object.freeze({ + display: 'flex', + alignItems: 'center', + padding: '0 12px', + height: '36px', + borderRadius: 0, + gap: '8px', + transition: 'background-color 0.2s ease', + '&:hover': { + backgroundColor: COLOURS.HOVER_GREY, + }, + }), + menuItemActive: Object.freeze({ + color: `${theme.palette.primary.main}14`, + }), + menuIcon: Object.freeze({ + fontSize: 18, + width: 18, + height: 18, + flexShrink: 0, + color: theme.palette.text.secondary, + }), + menuIconActive: Object.freeze({ + color: theme.palette.primary.main, + }), + menuLabel: Object.freeze({ + fontSize: '1rem', + color: COLOURS.BLACK, + }), + menuLabelActive: Object.freeze({ + fontWeight: 600, + color: theme.palette.primary.main, + }), + link: Object.freeze({ + display: 'flex', + alignItems: 'center', + gap: '8px', + textDecoration: 'none', + color: 'inherit', + width: '100%', }), });