diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index 3795cc1b5..8ab7ae025 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -10,7 +10,6 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html * Release date: January 25, 2025 ### Database changes - #### Migrations: #### Scheme changes @@ -23,12 +22,12 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html ### Bugfix ### What's new +* [OSDEV-40](https://opensupplyhub.atlassian.net/browse/OSDEV-40) - Created new page for `/contribute` to choose between multiple & single location upload. Replaced current multiple list upload to `/contribute/multiple-locations`. Changed `Upload Data` to `Add Data` text. ### Release instructions: * Ensure that the following commands are included in the `post_deployment` command: * `migrate` * `reindex_database` -* Run `[Release] Deploy` pipeline for the target environment with the flag `Clear the custom OpenSearch indexes and templates` set to true - to refresh the index mappings for the `moderation-events` index after disabling dynamic mapping for the new fields that don't have an explicit mapping defined. The `production-locations` will also be affected since it will clean all of our custom indexes and templates within the OpenSearch cluster. ## Release 1.27.0 diff --git a/src/react/src/Routes.jsx b/src/react/src/Routes.jsx index 36990e1ed..1a22f3435 100644 --- a/src/react/src/Routes.jsx +++ b/src/react/src/Routes.jsx @@ -14,6 +14,7 @@ import RegisterForm from './components/RegisterForm'; import ResetPasswordForm from './components/ResetPasswordForm'; import LoginForm from './components/LoginForm'; import Contribute from './components/Contribute'; +import AddLocationData from './components/AddLocationData'; import Homepage from './components/Homepage'; import FacilityLists from './components/FacilityLists'; import FacilityListItems from './components/FacilityListItems'; @@ -46,6 +47,7 @@ import { authResetPasswordFormRoute, authConfirmRegistrationRoute, contributeRoute, + multipleLocationRoute, listsRoute, facilityListItemsRoute, facilitiesRoute, @@ -142,6 +144,10 @@ class Routes extends Component { + { + const mockAuthorizedState = { + auth: { + user: { user: { isAnon: false } }, + session: { fetching: false }, + }, + }; + + const mockNotAuthorizedState = { + auth: { + user: { user: { isAnon: true } }, + session: { fetching: false }, + }, + }; + + const renderComponent = (preloadedState = {}) => { + const theme = createMuiTheme({ + palette: { + action: { main: '#000', dark: '#333' }, // Define a valid palette + getContrastText: jest.fn(() => '#fff'), // Mock the method to return a contrast color + }, + }); + return renderWithProviders( + + + + + , + { preloadedState } + ); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders for the authorized user', () => { + const { getByText } = renderComponent(mockAuthorizedState); + expect(getByText('Add production location data to OS Hub')).toBeInTheDocument(); + }); + + it('renders for the unauthorized user', () => { + const { getByText } = renderComponent(mockNotAuthorizedState); + expect(getByText('Log in to contribute to Open Supply Hub')).toBeInTheDocument(); + }); +}); diff --git a/src/react/src/components/AddLocationData.jsx b/src/react/src/components/AddLocationData.jsx new file mode 100644 index 000000000..f5fef9327 --- /dev/null +++ b/src/react/src/components/AddLocationData.jsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { withStyles, withTheme } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import CircularProgress from '@material-ui/core/CircularProgress'; + +import AppGrid from './AppGrid'; + +import { openInNewTab } from '../util/util'; +import { + authLoginFormRoute, + InfoLink, + InfoPaths, + contributeProductionLocationRoute, + multipleLocationRoute, +} from '../util/constants'; +import { makeAddLocationStyles } from '../util/styles'; + +import MessyIcon from './MessyIcon'; +import PlaylistIcon from './PlaylistIcon'; +import PinDropIcon from './PinDropIcon'; +import RectangleCardFigure from './RectangleCardFigure'; +import SliceCardFigure from './SliceCardFigure'; +import SliceMessyFigure from './SliceMessyFigure'; +import SliceMessyDuoFigure from './SliceMessyDuoFigure'; + +function AddLocationData({ classes, userHasSignedIn, fetchingSessionSignIn }) { + if (fetchingSessionSignIn) { + return ( + + + + + + + + ); + } + + if (!userHasSignedIn) { + return ( + + + + + Log in to contribute to Open Supply Hub + + + + + ); + } + + return ( +
+ + Add production location data to OS Hub + + + Contribute your data here to help build the world’s most + complete, open and accessible map of global production: + +
+ +
+ +
+
+ + + Upload a dataset with multiple production locations + using a{' '} + + spreadsheet. + + + + This option is best if you have a large number of + production locations to contribute. + + +
+ +
+ + Have messy data? + + + We can get it ready for you. All you need to + do is upload your data and we’ll take care + of the rest. + +
+
+ +
+
+ +
+ +
+
+
+ +
+ + + Add data for a +
+ + single production location. + +
+ + This option is best if you want to register your + production location or contribute data for one + production location at a time. + + +
+
+ +
+
+
+
+ ); +} + +AddLocationData.propTypes = { + classes: PropTypes.object.isRequired, + userHasSignedIn: PropTypes.bool.isRequired, + fetchingSessionSignIn: PropTypes.bool.isRequired, +}; + +function mapStateToProps({ + auth: { + user: { user }, + session: { fetching }, + }, +}) { + return { + userHasSignedIn: !user.isAnon, + fetchingSessionSignIn: fetching, + }; +} + +export default connect(mapStateToProps)( + withTheme()(withStyles(makeAddLocationStyles)(AddLocationData)), +); diff --git a/src/react/src/components/MessyIcon.jsx b/src/react/src/components/MessyIcon.jsx new file mode 100644 index 000000000..e1b0fd70e --- /dev/null +++ b/src/react/src/components/MessyIcon.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default function MessyIcon() { + return ( + + + + + + + + ); +} diff --git a/src/react/src/components/PinDropIcon.jsx b/src/react/src/components/PinDropIcon.jsx new file mode 100644 index 000000000..990849442 --- /dev/null +++ b/src/react/src/components/PinDropIcon.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default function PinDropIcon() { + return ( + + + + ); +} diff --git a/src/react/src/components/PlaylistIcon.jsx b/src/react/src/components/PlaylistIcon.jsx new file mode 100644 index 000000000..6c4967c6e --- /dev/null +++ b/src/react/src/components/PlaylistIcon.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default function PlaylistIcon() { + return ( + + + + ); +} diff --git a/src/react/src/components/RectangleCardFigure.jsx b/src/react/src/components/RectangleCardFigure.jsx new file mode 100644 index 000000000..e05a23db4 --- /dev/null +++ b/src/react/src/components/RectangleCardFigure.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default function RectangleCardFigure() { + return ( + + + + + + + + + + + + ); +} diff --git a/src/react/src/components/SliceCardFigure.jsx b/src/react/src/components/SliceCardFigure.jsx new file mode 100644 index 000000000..957293bd2 --- /dev/null +++ b/src/react/src/components/SliceCardFigure.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default function SliceCardFigure() { + return ( + + + + + ); +} diff --git a/src/react/src/components/SliceMessyDuoFigure.jsx b/src/react/src/components/SliceMessyDuoFigure.jsx new file mode 100644 index 000000000..d91db3b4d --- /dev/null +++ b/src/react/src/components/SliceMessyDuoFigure.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default function SliceMessyDuoFigure() { + return ( + + + + + ); +} diff --git a/src/react/src/components/SliceMessyFigure.jsx b/src/react/src/components/SliceMessyFigure.jsx new file mode 100644 index 000000000..c86355b81 --- /dev/null +++ b/src/react/src/components/SliceMessyFigure.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default function SliceMessyFigure() { + return ( + + + + ); +} diff --git a/src/react/src/util/COLOURS.js b/src/react/src/util/COLOURS.js index 1e624a73b..09f5a662e 100644 --- a/src/react/src/util/COLOURS.js +++ b/src/react/src/util/COLOURS.js @@ -26,5 +26,7 @@ export default { PALE_LIGHT_YELLOW: '#FFF2CE', ACCENT_GREY: '#E7E8EA', + // Purples + LIGHT_PURPLE: '#8428FA21', PURPLE: '#8428FA', }; diff --git a/src/react/src/util/constants.jsx b/src/react/src/util/constants.jsx index 779b49a75..8e65dc850 100644 --- a/src/react/src/util/constants.jsx +++ b/src/react/src/util/constants.jsx @@ -320,6 +320,7 @@ export const authRegisterFormRoute = '/auth/register'; export const authResetPasswordFormRoute = '/auth/resetpassword/:uid'; export const authConfirmRegistrationRoute = '/auth/confirm/:uid'; export const contributeRoute = '/contribute'; +export const multipleLocationRoute = '/contribute/multiple-locations'; export const listsRoute = '/lists'; export const facilityListItemsRoute = '/lists/:listID'; export const facilitiesRoute = '/facilities'; @@ -348,8 +349,7 @@ export const claimedFacilitiesRoute = '/claimed'; export const claimedFacilitiesDetailRoute = '/claimed/:claimID'; export const dashboardClaimsDetailsRoute = '/dashboard/claims/:claimID'; export const aboutClaimedFacilitiesRoute = `${InfoLink}/${InfoPaths.claimedFacilities}`; -export const contributeProductionLocationRoute = - '/contribute/production-location'; +export const contributeProductionLocationRoute = '/contribute/single-location'; export const searchByOsIdResultRoute = '/contribute/production-location/search'; export const productionLocationInfoRoute = '/contribute/production-location/info'; @@ -906,7 +906,7 @@ export const NavbarItems = [ { type: 'auth' }, { type: 'button', - label: 'Upload Data', + label: 'Add Data', href: '/contribute', internal: true, }, diff --git a/src/react/src/util/styles.js b/src/react/src/util/styles.js index 4566ddfce..a883c870d 100644 --- a/src/react/src/util/styles.js +++ b/src/react/src/util/styles.js @@ -1098,3 +1098,173 @@ export const inputErrorText = theme => color: COLOURS.RED, }), }); + +export const makeAddLocationStyles = theme => + Object.freeze({ + buttonStyle: Object.freeze({ + textTransform: 'none', + borderRadius: 0, + margin: '20px', + padding: '15px 25px 15px 25px', + display: 'center', + fontWeight: theme.typography.fontWeightExtraBold, + fontSize: '16px', + backgroundColor: theme.palette.action.main, + color: theme.palette.getContrastText(theme.palette.action.main), + '&:hover': { + backgroundColor: theme.palette.action.dark, + }, + }), + container: Object.freeze({ + backgroundColor: COLOURS.LIGHT_GREY, + }), + title: Object.freeze({ + paddingLeft: '5%', + paddingRight: '5%', + paddingTop: '25px', + color: COLOURS.NEAR_BLACK, + fontWeight: 'bold', + marginBottom: '1rem', + }), + description: Object.freeze({ + paddingLeft: '5%', + paddingRight: '5%', + paddingTop: '5px', + marginBottom: '2rem', + fontWeight: 'bold', + }), + dataOptions: Object.freeze({ + display: 'flex', + paddingLeft: '5%', + paddingRight: '5%', + gap: '50px', + paddingBottom: '5%', + flexWrap: 'wrap', + flexDirection: 'row', + [theme.breakpoints.down('md')]: { + flexDirection: 'column', + display: 'center', + }, + }), + card: Object.freeze({ + backgroundColor: COLOURS.WHITE, + boxShadow: 'none', + padding: '60px 25px 25px 25px', + width: '45%', + position: 'relative', + textAlign: 'center', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }), + cardTitle: Object.freeze({ + fontSize: '32px', + margin: '0 auto', + maxWidth: '80%', + textAlign: 'center', + paddingTop: '15px', + paddingBottom: '15px', + fontWeight: '300', + lineHeight: '1.0', + }), + cardSub: Object.freeze({ + fontSize: '16px', + margin: '0 auto', + maxWidth: '50%', + textAlign: 'center', + paddingBottom: '5px', + fontWeight: theme.typography.fontWeightSemiBold, + }), + cardRectangleView: Object.freeze({ + position: 'absolute', + top: 0, + right: 0, + }), + cardSliceView: Object.freeze({ + position: 'absolute', + bottom: -5, + right: 0, + }), + cardSliceDuoView: Object.freeze({ + position: 'absolute', + top: 0, + right: '25%', + }), + cardIcon: Object.freeze({ + color: COLOURS.NEAR_BLACK, + textAlign: 'center', + alignItems: 'center', + }), + highlight: Object.freeze({ + color: COLOURS.NEAR_BLACK, + fontWeight: theme.typography.fontWeightSemiBold, + }), + messyData: Object.freeze({ + backgroundColor: COLOURS.LIGHT_PURPLE, + marginTop: '50px', + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + padding: '30px', + position: 'relative', + maxHeight: '56px', + [theme.breakpoints.down('md')]: { + flexDirection: 'column', + maxHeight: '25%', + }, + }), + messyContent: Object.freeze({ + display: 'flex', + flexDirection: 'column', + marginLeft: '20px', + textAlign: 'left', + flex: 1, + }), + messyTitle: Object.freeze({ + color: COLOURS.NEAR_BLACK, + maxWidth: '75%', + fontWeight: theme.typography.fontWeightSemiBold, + fontSize: '24px', + [theme.breakpoints.down('md')]: { + fontSize: '16px', + textAlign: 'center', + maxWidth: '100%', + }, + }), + messySub: Object.freeze({ + color: COLOURS.NEAR_BLACK, + marginBottom: '10px', + maxWidth: '65%', + fontWeight: theme.typography.fontWeightSemiBold, + fontSize: '16px', + [theme.breakpoints.down('md')]: { + fontSize: '12px', + textAlign: 'center', + maxWidth: '100%', + }, + }), + messyIcon: Object.freeze({ + color: COLOURS.NEAR_BLACK, + }), + secondaryButton: Object.freeze({ + backgroundColor: COLOURS.WHITE, + color: COLOURS.NEAR_BLACK, + fontWeight: theme.typography.fontWeightExtraBold, + fontSize: '16px', + border: 'none', + padding: '1rem 1.5rem', + cursor: 'pointer', + position: 'absolute', + right: '20px', + top: '50%', + transform: 'translateY(-50%)', + [theme.breakpoints.down('md')]: { + fontSize: '12px', + position: 'relative', + right: '0px', + top: '20px', + }, + }), + });