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 (
+
+ );
+};
+
+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"