From 65c1394ce6fef3eaaaea25120daeb243b4c20fc1 Mon Sep 17 00:00:00 2001 From: Vadim Kovalenko <90321493+VadimKovalenkoSNF@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:30:18 +0400 Subject: [PATCH] [OSDEV-1132] implement thanks for submitting screen FE (#436) Implement [OSDEV-1132](https://opensupplyhub.atlassian.net/browse/OSDEV-1132) Created FE for the "thanks for submitting screen" when submit new production location. [OSDEV-1132]: https://opensupplyhub.atlassian.net/browse/OSDEV-1132?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- doc/release/RELEASE-NOTES.md | 1 + src/react/package.json | 1 + .../components/DialogTooltip.test.js | 60 ++++++ .../ProductionLocationDialog.test.js | 74 +++++++ .../src/components/ConfirmActionButton.jsx | 3 +- .../components/Contribute/DialogTooltip.jsx | 58 ++++++ .../Contribute/ProductionLocationDialog.jsx | 193 ++++++++++++++++++ .../FacilityDetailsContributors.jsx | 4 +- .../src/components/FacilityDetailsItem.jsx | 4 +- .../src/components/FacilityListSummary.jsx | 3 +- src/react/src/util/COLOURS.js | 3 +- src/react/src/util/styles.js | 193 +++++++++++++++--- src/react/yarn.lock | 5 + 13 files changed, 562 insertions(+), 40 deletions(-) create mode 100644 src/react/src/__tests__/components/DialogTooltip.test.js create mode 100644 src/react/src/__tests__/components/ProductionLocationDialog.test.js create mode 100644 src/react/src/components/Contribute/DialogTooltip.jsx create mode 100644 src/react/src/components/Contribute/ProductionLocationDialog.jsx diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index bbd812e6d..fbf3c72fc 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -56,6 +56,7 @@ This issue has been fixed by adding additional requests to delete the appropriat * [OSDEV-1482](https://opensupplyhub.atlassian.net/browse/OSDEV-1482) - The `GET api/v1/moderation-events/{moderation_id}` endpoint returns a single response instead of an array containing one item. ### What's new +* [OSDEV-1132](https://opensupplyhub.atlassian.net/browse/OSDEV-1132) - Added FE for the "thanks for submitting" screen when user submits production location's data. * [OSDEV-1373](https://opensupplyhub.atlassian.net/browse/OSDEV-1373) - The tab `Search by Name and Address.` on the Production Location Search screen has been implemented. There are three required properties (name, address, country). The "Search" button becomes clickable after filling out inputs, creates a link with parameters, and allows users to proceed to the results screen. * [OSDEV-1175](https://opensupplyhub.atlassian.net/browse/OSDEV-1175) - New Moderation Queue Page was integrated with `GET api/v1/moderation-events/` endpoint that include pagination, sorting and filtering. diff --git a/src/react/package.json b/src/react/package.json index 0c2a4333e..d24a97cff 100644 --- a/src/react/package.json +++ b/src/react/package.json @@ -7,6 +7,7 @@ "@craco/craco": "6.1.1", "@material-ui/core": "3.1.0", "@material-ui/icons": "3.0.1", + "@popperjs/core": "^2.11.8", "@reduxjs/toolkit": "^2.2.7", "@rollbar/react": "^0.11.2", "@turf/distance": "6.0.1", diff --git a/src/react/src/__tests__/components/DialogTooltip.test.js b/src/react/src/__tests__/components/DialogTooltip.test.js new file mode 100644 index 000000000..ccf68f2fa --- /dev/null +++ b/src/react/src/__tests__/components/DialogTooltip.test.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import DialogTooltip from '../../components/Contribute/DialogTooltip'; + +global.cancelAnimationFrame = jest.fn(); + +jest.mock('@popperjs/core', () => ({ + __esModule: true, + default: jest.fn(() => ({ + destroy: jest.fn(), + update: jest.fn(), + scheduleUpdate: jest.fn(), + enableEventListeners: jest.fn(), + disableEventListeners: jest.fn(), + setOptions: jest.fn(), + })), +})); + +beforeAll(() => { + global.Node = global.Node || {}; +}); + +describe('DialogTooltip Component', () => { + test('renders tooltip on hover', async () => { + const mockChildComponent = Hover over this element; + const mockText = "You'll be able to claim the location after the moderation is done"; + + render( + + ); + + expect(screen.queryByText(mockText)).not.toBeInTheDocument(); + + fireEvent.mouseEnter(screen.getByText('Hover over this element')); + await waitFor(() => { + expect(screen.getByText(mockText)).toBeInTheDocument(); + }); + }); + + test('hides tooltip on mouse leave', async () => { + const mockChildComponent = Hover over this element; + const mockText = "Test tooltip"; + + render( + + ); + + const element = screen.getByText('Hover over this element'); + + fireEvent.mouseEnter(element); + await waitFor(() => { + expect(screen.getByText(mockText)).toBeInTheDocument(); + }); + + fireEvent.mouseLeave(element); + await waitFor(() => { + expect(screen.queryByText(mockText)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/react/src/__tests__/components/ProductionLocationDialog.test.js b/src/react/src/__tests__/components/ProductionLocationDialog.test.js new file mode 100644 index 000000000..44b9f7c46 --- /dev/null +++ b/src/react/src/__tests__/components/ProductionLocationDialog.test.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { BrowserRouter as Router, useHistory } from 'react-router-dom'; +import UserEvent from "user-event"; +import ProductionLocationDialog from '../../components/Contribute/ProductionLocationDialog'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: jest.fn(), +})); + +const mockHistoryPush = jest.fn(); + +beforeEach(() => { + useHistory.mockReturnValue({ + push: mockHistoryPush, + }); +}); + +test('renders dialog content', () => { + render( + + + + ); + + expect(screen.getByText(/Thanks for adding data for this production location!/i)).toBeInTheDocument(); + expect(screen.getByText(/Facility name/i)).toBeInTheDocument(); + expect(screen.getByText(/OS ID/i)).toBeInTheDocument(); + expect(screen.getByText(/Pending/i)).toBeInTheDocument(); +}); + +test('should render multiple instances of text element', () => { + const text = "Unifill Composite Dyeing Mills Ltd."; + + render( + + + + ); + + const elements = screen.getAllByText(new RegExp(text, 'i')); + + expect(elements).toHaveLength(2); +}); + +test('navigates when "Search OS Hub" button is clicked', () => { + render( + + + + ); + + const button = screen.getByText(/Search OS Hub/i); + UserEvent.click(button); + + expect(mockHistoryPush).toHaveBeenCalledWith('/'); +}); + +test('calls console log when "Submit another Location" button is clicked', () => { + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + + render( + + + + ); + + const button = screen.getByText(/Submit another Location/i); + UserEvent.click(button); + + expect(consoleLogSpy).toHaveBeenCalledWith('submit another location'); + consoleLogSpy.mockRestore(); +}); diff --git a/src/react/src/components/ConfirmActionButton.jsx b/src/react/src/components/ConfirmActionButton.jsx index 08340a503..79296ff73 100644 --- a/src/react/src/components/ConfirmActionButton.jsx +++ b/src/react/src/components/ConfirmActionButton.jsx @@ -7,6 +7,7 @@ import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; import { useMergeButtonClickHandler } from './../util/hooks'; import { CONFIRM_ACTION, MERGE_ACTION, REJECT_ACTION } from '../util/constants'; +import COLOURS from '../util/COLOURS'; const actionDialogStates = Object.freeze({ none: 'none', @@ -151,7 +152,7 @@ const ConfirmActionButton = ({
  • name: {facilityMatchToReject?.name}
  • address: {facilityMatchToReject?.address}
  • -
    +
    ))} diff --git a/src/react/src/components/Contribute/DialogTooltip.jsx b/src/react/src/components/Contribute/DialogTooltip.jsx new file mode 100644 index 000000000..1c79e5f80 --- /dev/null +++ b/src/react/src/components/Contribute/DialogTooltip.jsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { shape, string, node } from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Tooltip from '@material-ui/core/Tooltip'; +import { makeDialogTooltipStyles } from '../../util/styles'; + +const DialogTooltip = ({ text, childComponent, classes }) => { + const [arrowRef, setArrowRef] = useState(null); + return ( + + {text} + + + } + classes={{ + tooltip: classes.tooltipStyles, + popper: classes.popperStyles, + tooltipPlacementLeft: classes.placementLeft, + tooltipPlacementRight: classes.placementRight, + tooltipPlacementTop: classes.placementTop, + tooltipPlacementBottom: classes.placementBottom, + }} + PopperProps={{ + popperOptions: { + modifiers: { + arrow: { + enabled: Boolean(arrowRef), + element: arrowRef, + }, + }, + }, + }} + > + {childComponent} + + ); +}; + +DialogTooltip.propTypes = { + text: string.isRequired, + childComponent: node.isRequired, + classes: shape({ + arrow: shape({}).isRequired, + tooltipStyles: shape({}).isRequired, + popperStyles: shape({}).isRequired, + placementLeft: shape({}).isRequired, + placementRight: shape({}).isRequired, + placementTop: shape({}).isRequired, + placementBottom: shape({}).isRequired, + }).isRequired, +}; + +export default withStyles(makeDialogTooltipStyles)(DialogTooltip); diff --git a/src/react/src/components/Contribute/ProductionLocationDialog.jsx b/src/react/src/components/Contribute/ProductionLocationDialog.jsx new file mode 100644 index 000000000..174934586 --- /dev/null +++ b/src/react/src/components/Contribute/ProductionLocationDialog.jsx @@ -0,0 +1,193 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import { useHistory } from 'react-router-dom'; +import Button from '@material-ui/core/Button'; +import Chip from '@material-ui/core/Chip'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; +import DialogTooltip from './DialogTooltip'; +import { mainRoute } from '../../util/constants'; +import { makeProductionLocationDialogStyles } from '../../util/styles'; + +const infoIcon = classes => ( + +); + +const claimButton = classes => ( + + + +); + +const ProductionLocationDialog = ({ classes }) => { + const history = useHistory(); + + return ( + +
    + +

    + Thanks for adding data for this production location! +

    +
    + + + Do you own or manage this location? If so, you can now + claim your production location to have a complete, + credible and confirmed profile with a green banner and + claimed badge. You’ll be able to add more information, + like contact details, certifications, native language + name, and more. + +
    + + + + Facility name + + + Unifill Composite Dyeing Mills Ltd. + + + Address + + + Gobindobari, Bhabanipur, Kashimpur, Gazipur, + Bangladesh. Gazipur - 1704 - DHAKA - Bangladesh + March 23, 2022 by The WikiRate Project 7 more + contributions + + + Location type + + + Textile or Material Production March 23, 2022 by + The WikiRate Project 7 more contributions + + + Number of workers + + + 1,000 - 5,000 + + + + + OS ID + + + + + US20243236AZ1R0 + + + + {}} + className={classes.osIdStatusBadge} + deleteIcon={ + + } + /> + + + + Product type + + + Blouses Shirts + + + Processing type + + + Textile or Material Production + + + Parent company + + + Unifill Composite Dyeing Mills Ltd. + + + +
    + + + + + + + +
    +
    + ); +}; + +export default withStyles(makeProductionLocationDialogStyles)( + ProductionLocationDialog, +); diff --git a/src/react/src/components/FacilityDetailsContributors.jsx b/src/react/src/components/FacilityDetailsContributors.jsx index ddce30767..5577869c6 100644 --- a/src/react/src/components/FacilityDetailsContributors.jsx +++ b/src/react/src/components/FacilityDetailsContributors.jsx @@ -6,7 +6,7 @@ import FacilityDetailsShowContributorsButton from './FacilityDetailsShowContribu import { splitContributorsIntoPublicAndNonPublic } from '../util/util'; -const detailssStyles = theme => +const detailsStyles = theme => Object.freeze({ root: { color: '#191919', @@ -53,4 +53,4 @@ const FacilityDetailsContributors = ({ classes, contributors }) => { ); }; -export default withStyles(detailssStyles)(FacilityDetailsContributors); +export default withStyles(detailsStyles)(FacilityDetailsContributors); diff --git a/src/react/src/components/FacilityDetailsItem.jsx b/src/react/src/components/FacilityDetailsItem.jsx index 3ae4bf819..1213d77fc 100644 --- a/src/react/src/components/FacilityDetailsItem.jsx +++ b/src/react/src/components/FacilityDetailsItem.jsx @@ -7,7 +7,7 @@ import FacilityDetailsDetail from './FacilityDetailsDetail'; import TitledDrawer from './TitledDrawer'; import ShowOnly from './ShowOnly'; -const detailssStyles = theme => +const detailsStyles = theme => Object.freeze({ item: { paddingTop: theme.spacing.unit * 3, @@ -109,4 +109,4 @@ const FacilityDetailsItem = ({ ); }; -export default withStyles(detailssStyles)(FacilityDetailsItem); +export default withStyles(detailsStyles)(FacilityDetailsItem); diff --git a/src/react/src/components/FacilityListSummary.jsx b/src/react/src/components/FacilityListSummary.jsx index 2eb5279e9..922d1c266 100644 --- a/src/react/src/components/FacilityListSummary.jsx +++ b/src/react/src/components/FacilityListSummary.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { string } from 'prop-types'; +import COLOURS from '../util/COLOURS'; const facilityListSummaryStyles = Object.freeze({ nameStyles: Object.freeze({ @@ -43,7 +44,7 @@ function FacilityListSummary({ name, description, id, contributor }) {

    {description}

    -
    +
    ); } diff --git a/src/react/src/util/COLOURS.js b/src/react/src/util/COLOURS.js index c7f45964c..7d6baf494 100644 --- a/src/react/src/util/COLOURS.js +++ b/src/react/src/util/COLOURS.js @@ -18,10 +18,11 @@ export default { GREY: '#D2D2D2', LIGHT_GREY: '#F9F7F7', DARK_GREY: '#6E707E', - DARK_SLATE_GRAY: '#3D4153', + DARK_SLATE_GREY: '#3D4153', NEAR_BLACK: '#0D1128', // Accent Colors NAVIGATION: '#FCCF3F', PALE_LIGHT_YELLOW: '#FFF2CE', + ACCENT_GREY: '#E7E8EA', }; diff --git a/src/react/src/util/styles.js b/src/react/src/util/styles.js index 9c24a4b0e..de55be8a4 100644 --- a/src/react/src/util/styles.js +++ b/src/react/src/util/styles.js @@ -474,68 +474,195 @@ export const makeSearchByOsIdResultStyles = theme => }), }); -export const makeBackToSearchButtonStyles = theme => - Object.freeze({ - backButtonRootStyles: Object.freeze({ - textTransform: 'none', - fontSize: '18px', - fontWeight: theme.typography.fontWeightSemiBoldPlus, - }), - backButtonLabelStyles: Object.freeze({ - display: 'flex', - alignItems: 'center', - gap: '8px', - }), - }); - -const arrowGenerator = color => ({ - '&[x-placement*="bottom"] $arrowStyles': { +const arrowGenerator = (color, selector) => ({ + [`&[x-placement*="bottom"] ${selector}`]: Object.freeze({ top: 0, left: 0, marginTop: '-0.95em', width: '3em', height: '1em', - '&::before': { + '&::before': Object.freeze({ borderWidth: '0 1em 1em 1em', borderColor: `transparent transparent ${color} transparent`, - }, - }, - '&[x-placement*="top"] $arrowStyles': { + }), + }), + [`&[x-placement*="top"] ${selector}`]: Object.freeze({ bottom: 0, left: 0, marginBottom: '-0.95em', width: '3em', height: '1em', - '&::before': { + '&::before': Object.freeze({ borderWidth: '1em 1em 0 1em', borderColor: `${color} transparent transparent transparent`, - }, - }, - '&[x-placement*="right"] $arrowStyles': { + }), + }), + [`&[x-placement*="right"] ${selector}`]: Object.freeze({ left: 0, marginLeft: '-0.95em', height: '3em', width: '1em', - '&::before': { + '&::before': Object.freeze({ borderWidth: '1em 1em 1em 0', borderColor: `transparent ${color} transparent transparent`, - }, - }, - '&[x-placement*="left"] $arrowStyles': { + }), + }), + [`&[x-placement*="left"] ${selector}`]: Object.freeze({ right: 0, marginRight: '-0.95em', height: '3em', width: '1em', - '&::before': { + '&::before': Object.freeze({ borderWidth: '1em 0 1em 1em', borderColor: `transparent transparent transparent ${color}`, - }, - }, + }), + }), }); +export const makeDialogTooltipStyles = () => + Object.freeze({ + arrow: Object.freeze({ + position: 'absolute', + fontSize: 6, + width: '3em', + height: '3em', + '&::before': Object.freeze({ + content: '""', + margin: 'auto', + display: 'block', + width: 0, + height: 0, + borderStyle: 'solid', + }), + }), + popperStyles: Object.freeze( + arrowGenerator(COLOURS.DARK_SLATE_GREY, '$arrow'), + ), + tooltipStyles: Object.freeze({ + fontSize: '14px', + backgroundColor: COLOURS.DARK_SLATE_GREY, + }), + placementLeft: Object.freeze({ + margin: '0 8px', + }), + placementRight: Object.freeze({ + margin: '0 8px', + }), + placementTop: Object.freeze({ + margin: '8px 0', + }), + placementBottom: Object.freeze({ + margin: '8px 0', + }), + }); + +export const makeProductionLocationDialogStyles = theme => + Object.freeze({ + modalContainerWrapper: Object.freeze({ + padding: '20px 60px', + [theme.breakpoints.down('md')]: { + padding: 0, + }, + }), + label: Object.freeze({ + fontSize: '14px', + textTransform: 'uppercase', + fontWeight: theme.typography.fontWeightExtraBold, + }), + titleContentStyle: Object.freeze({ + fontSize: '32px', + textAlign: 'center', + fontWeight: theme.typography.fontWeightBold, + lineHeight: 1, + }), + titleInnerContentStyle: Object.freeze({ + fontSize: '32px', + margin: 0, + lineHeight: '1.1', + fontWeight: theme.typography.fontWeightBold, + }), + primaryText: Object.freeze({ + marginBottom: '20px', + }), + osIDText: Object.freeze({ + lineHeight: '2.3', + }), + leftContainerColumn: Object.freeze({ + paddingRight: '10px', + }), + rightContainerColumn: Object.freeze({ + paddingRight: '10px', + }), + separator: Object.freeze({ + margin: '20px 0', + color: COLOURS.GREY, + }), + dialogContentStyles: Object.freeze({ + textAlign: 'center', + fontSize: '16px', + fontWeight: theme.typography.fontWeightSemiBold, + }), + buttonContentStyle: Object.freeze({ + justifyContent: 'space-between', + alignItems: 'center', + padding: '0 15px', + [theme.breakpoints.down('md')]: { + justifyContent: 'initial', + flexDirection: 'column', + }, + }), + osIdStatusBadge: Object.freeze({ + backgroundColor: COLOURS.ACCENT_GREY, + marginLeft: '10px', + fontWeight: theme.typography.fontWeightBold, + }), + osIdStatusBadgeIcon: Object.freeze({ + color: COLOURS.DARK_GREY, + marginRight: '5px', + }), + button: Object.freeze({ + fontWeight: theme.typography.fontWeightBold, + textTransform: 'none', + paddingLeft: '30px', + paddingRight: '30px', + boxShadow: 'none', + [theme.breakpoints.down('md')]: { + width: '100%', + marginBottom: '16px', + }, + }), + claimTooltipWrapper: Object.freeze({ + display: 'block', + cursor: 'not-allowed', + [theme.breakpoints.down('md')]: { + width: '100%', + }, + }), + claimButton: Object.freeze({ + backgroundColor: COLOURS.NAVIGATION, + }), + }); + +export const makeBackToSearchButtonStyles = theme => + Object.freeze({ + backButtonRootStyles: Object.freeze({ + textTransform: 'none', + fontSize: '18px', + fontWeight: theme.typography.fontWeightSemiBoldPlus, + }), + backButtonLabelStyles: Object.freeze({ + display: 'flex', + alignItems: 'center', + gap: '8px', + }), + }); + export const makePreviousOsIdTooltipStyles = theme => Object.freeze({ - arrowPopperStyles: arrowGenerator(COLOURS.DARK_SLATE_GRAY), + arrowPopperStyles: arrowGenerator( + COLOURS.DARK_SLATE_GREY, + '$arrowStyles', + ), arrowStyles: Object.freeze({ position: 'absolute', fontSize: 6, @@ -551,7 +678,7 @@ export const makePreviousOsIdTooltipStyles = theme => }, }), tooltipStyles: Object.freeze({ - backgroundColor: COLOURS.DARK_SLATE_GRAY, + backgroundColor: COLOURS.DARK_SLATE_GREY, color: COLOURS.WHITE, maxWidth: '149px', boxShadow: '0px 4px 4px 0px #00000040', diff --git a/src/react/yarn.lock b/src/react/yarn.lock index 7d6c351db..a2ce9b4f3 100644 --- a/src/react/yarn.lock +++ b/src/react/yarn.lock @@ -2015,6 +2015,11 @@ schema-utils "^2.6.5" source-map "^0.7.3" +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@reduxjs/toolkit@^2.2.7": version "2.2.7" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.7.tgz#199e3d10ccb39267cb5aee92c0262fd9da7fdfb2"