diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index 7190c1b16..565a11c7c 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -20,6 +20,10 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html * Updated the GET `api/facilities/` and `api/facilities/{os_id}/` endpoints to include `contributor_type` (the raw type from the database) for both public and anonymous sources. Each contributor entry now also includes a `count` field (1 for public contributors and an aggregated count for anonymous entries of the same type), allowing the front end to display and sum counts by type (e.g., “18 Brands”, “9 Suppliers”). * Additionally, updated GET `api/facilities/{os_id}/` to return the claim request creation date. All of this information is required for the redesigned Production Location page - specifically for the claim banner - as well as for the supply chain network. * [OSDEV-2369](https://opensupplyhub.atlassian.net/browse/OSDEV-2369) - Moved single-facility data loading and redirect logic into the Production Location details container so the sidebar (including the "Contribute to this profile" section) and main content render with consistent facility data. +* [OSDEV-2370](https://opensupplyhub.atlassian.net/browse/OSDEV-2370) - Created reusable data point and drawer components for the Production Location page redesign: + * Introduced shared `IconComponent` (interactive tooltip with icon) and `LearnMoreLink`; refactored OS ID badge, Data Sources, and Claim status to use them. + * Added `DataPoint` component (label, value, status, contributor link, date, and optional "data sources" drawer trigger) and `ContributionsDrawer` with promoted source and list of contribution cards linking to contributor profiles. + * Claim form profile step and related tooltips now use `IconComponent`. ### Architecture/Environment changes * Increased the CPU and memory allocation for the DedupeHub container to `8 CPU` and `40 GB` in the Terraform deployment configuration to address memory overload issues during production location reindexing for the `Test` environment. diff --git a/src/react/src/__tests__/components/ContributionsDrawer.test.jsx b/src/react/src/__tests__/components/ContributionsDrawer.test.jsx new file mode 100644 index 000000000..ab140dd47 --- /dev/null +++ b/src/react/src/__tests__/components/ContributionsDrawer.test.jsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; + +import ContributionsDrawer from '../../components/ProductionLocation/ContributionsDrawer/ContributionsDrawer'; + +const theme = createMuiTheme(); + +const renderContributionsDrawer = (props = {}) => + render( + + + + + , + ); + +function DrawerWithTrigger() { + const [open, setOpen] = useState(false); + return ( + + + - - ); - } - - const cloned = React.cloneElement(childComponent, { - onFocus: e => { - onEnter(); - childComponent.props.onFocus?.(e); - }, - onBlur: e => { - onLeave(); - childComponent.props.onBlur?.(e); - }, - onKeyDown: handleKeyDownForClone, - 'aria-describedby': tooltipId, - }); - return ( - - {cloned} - - ); -}; - -InteractiveTrigger.propTypes = { - childComponent: node.isRequired, - tooltipId: string.isRequired, - onEnter: func.isRequired, - onLeave: func.isRequired, -}; - -export default InteractiveTrigger; diff --git a/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/ProfileStep.jsx b/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/ProfileStep.jsx index eb786a5c8..848841b40 100644 --- a/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/ProfileStep.jsx +++ b/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/ProfileStep.jsx @@ -8,12 +8,10 @@ import Build from '@material-ui/icons/Build'; import VerifiedUser from '@material-ui/icons/VerifiedUser'; import Spa from '@material-ui/icons/Spa'; -import Tooltip from '@material-ui/core/Tooltip'; -import HelpOutline from '@material-ui/icons/HelpOutline'; -import IconButton from '@material-ui/core/IconButton'; import Switch from '@material-ui/core/Switch'; import FormFieldTitle from '../../../Shared/FormFieldTitle/FormFieldTitle'; import FormFieldHint from '../../../Shared/FormFieldHint/FormFieldHint'; +import IconComponent from '../../../../Shared/IconComponent/IconComponent'; import DialogTooltip from '../../../../Contribute/DialogTooltip'; import StyledSelect from '../../../../Filters/StyledSelect'; import InputErrorText from '../../../../Contribute/InputErrorText'; @@ -136,21 +134,10 @@ const ProfileStep = ({ label={ <> Production Location Name in Native Language - - - - - + /> } classes={{ title: classes.formLabel }} @@ -192,25 +179,10 @@ const ProfileStep = ({ label={ <> Company Phone - - - - - + /> } classes={{ title: classes.formLabel }} @@ -264,27 +236,10 @@ const ProfileStep = ({ label={ <> Company Website - - - - - + /> } classes={{ title: classes.formLabel }} @@ -342,21 +297,10 @@ const ProfileStep = ({ label={ <> Production Location Description - - - - - + /> } classes={{ title: classes.formLabel }} @@ -481,25 +425,10 @@ const ProfileStep = ({ label={ <> Office Name - - - - - + /> } classes={{ title: classes.formLabel }} @@ -542,25 +471,10 @@ const ProfileStep = ({ label={ <> Office Address - - - - - + /> } classes={{ title: classes.formLabel }} @@ -702,25 +616,10 @@ const ProfileStep = ({ label={ <> Location Type(s) - - - - - + /> } classes={{ title: classes.formLabel }} @@ -776,25 +675,10 @@ const ProfileStep = ({ label={ <> Processing Type(s) - - - - - + /> } classes={{ title: classes.formLabel }} @@ -858,25 +742,10 @@ const ProfileStep = ({ label={ <> Product Types - - - - - + /> } classes={{ title: classes.formLabel }} @@ -909,25 +778,10 @@ const ProfileStep = ({ label={ <> Number of Workers - - - - - + /> } classes={{ title: classes.formLabel }} @@ -969,25 +823,10 @@ const ProfileStep = ({ label={ <> Percentage of Female Workers - - - - - + /> } classes={{ title: classes.formLabel }} @@ -1037,25 +876,10 @@ const ProfileStep = ({ label={ <> Minimum Order Quantity - - - - - + /> } classes={{ title: classes.formLabel }} @@ -1112,25 +936,10 @@ const ProfileStep = ({ label={ <> Average Lead Time - - - - - + /> } classes={{ title: classes.formLabel }} @@ -1221,21 +1030,10 @@ const ProfileStep = ({ label={ <> Affiliations - - - - - + /> } classes={{ title: classes.formLabel }} diff --git a/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/styles.js b/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/styles.js index 0ff36fc6e..46caebd9c 100644 --- a/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/styles.js +++ b/src/react/src/components/InitialClaimFlow/ClaimForm/Steps/ProfileStep/styles.js @@ -44,6 +44,9 @@ export const profileStepStyles = theme => fontSize: '21px', fontWeight: 600, }), + helpTooltip: Object.freeze({ + marginLeft: theme.spacing.unit / 2, + }), inputStyles: Object.freeze({ backgroundColor: COLOURS.WHITE, fontSize: '18px', @@ -126,25 +129,6 @@ export const profileStepStyles = theme => greenBg: Object.freeze({ backgroundColor: COLOURS.LIGHT_GREEN, }), - helpIcon: Object.freeze({ - fontSize: '1rem', - color: COLOURS.DARK_GREY, - marginLeft: theme.spacing.unit / 2, - }), - helpIconButton: Object.freeze({ - padding: 0, - '&:hover': { - backgroundColor: 'transparent', - }, - }), - tooltip: Object.freeze({ - fontSize: '14px', - backgroundColor: COLOURS.WHITE, - color: 'rgba(0, 0, 0, 0.87)', - border: `1px solid ${COLOURS.GREY}`, - padding: theme.spacing.unit * 1.5, - maxWidth: 300, - }), }); export default profileStepStyles; diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionCard/ContributionCard.jsx b/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionCard/ContributionCard.jsx new file mode 100644 index 000000000..b025ee546 --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionCard/ContributionCard.jsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { + object, + string, + oneOfType, + instanceOf, + bool, + number, +} from 'prop-types'; +import { Link } from 'react-router-dom'; +import Typography from '@material-ui/core/Typography'; +import { withStyles } from '@material-ui/core/styles'; +import ScheduleIcon from '@material-ui/icons/Schedule'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; + +import { makeProfileRouteLink, formatDate } from '../../../../util/util'; +import { DATE_FORMATS } from '../../../../util/constants'; +import contributionCardStyles from './styles'; + +const ContributionCard = ({ + classes, + value, + sourceName, + date, + promoted, + userId, + 'data-testid': dataTestId, +}) => ( +
+ + {value} + +
+
+ {sourceName && + (userId != null ? ( + + {sourceName} + + + ) : ( + + {sourceName} + + ))} +
+
+ {date ? ( + + + {formatDate(date, DATE_FORMATS.LONG)} + + ) : null} +
+
+
+); + +ContributionCard.propTypes = { + classes: object.isRequired, + value: string.isRequired, + sourceName: string, + date: oneOfType([string, instanceOf(Date)]), + promoted: bool, + userId: oneOfType([string, number]), + 'data-testid': string, +}; + +ContributionCard.defaultProps = { + sourceName: null, + date: null, + promoted: false, + userId: null, + 'data-testid': undefined, +}; + +export default withStyles(contributionCardStyles)(ContributionCard); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionCard/styles.js b/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionCard/styles.js new file mode 100644 index 000000000..4df28de9f --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionCard/styles.js @@ -0,0 +1,94 @@ +import COLOURS from '../../../../util/COLOURS'; + +export default () => + Object.freeze({ + contributionCard: Object.freeze({ + backgroundColor: COLOURS.WHITE, + border: `1px solid ${COLOURS.LIGHT_BORDER_GREY}`, + padding: '16px', + marginBottom: '8px', + }), + contributionCardPromoted: Object.freeze({ + backgroundColor: COLOURS.LIGHT_LAVENDER, + }), + contributionValueContainer: Object.freeze({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }), + contributionValue: Object.freeze({ + fontWeight: 600, + fontSize: '1.125rem', + lineHeight: 1.4, + color: COLOURS.NEAR_BLACK, + marginBottom: '8px', + }), + contributionValuePromoted: Object.freeze({ + fontSize: '1.25rem', + }), + contributionSourceContainer: Object.freeze({ + maxWidth: '45%', + }), + contributionSource: Object.freeze({ + fontSize: '1rem', + lineHeight: 1.4, + color: COLOURS.PURPLE, + }), + contributionSourcePromoted: Object.freeze({ + fontSize: '1.1rem', + }), + contributionSourceLink: Object.freeze({ + display: 'inline-flex', + alignItems: 'center', + gap: '4px', + fontSize: '1rem', + lineHeight: 1.4, + color: COLOURS.PURPLE, + textDecoration: 'none', + cursor: 'pointer', + '&:hover': Object.freeze({ + textDecoration: 'underline', + }), + }), + contributionSourceLinkPromoted: Object.freeze({ + fontSize: '1.1rem', + }), + contributionSourceIcon: Object.freeze({ + fontSize: '0.875rem', + color: COLOURS.PURPLE, + marginLeft: '4px', + }), + contributionSourceIconPromoted: Object.freeze({ + fontSize: '1rem', + }), + contributionMetaContainer: Object.freeze({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + fontSize: '0.875rem', + color: COLOURS.DARK_GREY, + gap: '8px', + maxWidth: '45%', + }), + contributionLink: Object.freeze({ + padding: '4px', + color: COLOURS.DARK_GREY, + }), + dateWithIcon: Object.freeze({ + display: 'inline-flex', + alignItems: 'center', + fontSize: '1rem', + }), + dateWithIconPromoted: Object.freeze({ + fontSize: '1.1rem', + }), + dateIcon: Object.freeze({ + marginRight: '4px', + marginTop: '2px', + fontSize: '0.8125rem', + color: COLOURS.DARK_GREY, + }), + dateIconPromoted: Object.freeze({ + fontSize: '0.9rem', + }), + }); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionsDrawer.jsx b/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionsDrawer.jsx new file mode 100644 index 000000000..7c8be5d05 --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/ContributionsDrawer.jsx @@ -0,0 +1,181 @@ +import React from 'react'; +import { + object, + bool, + func, + string, + shape, + oneOfType, + instanceOf, + arrayOf, + number, +} from 'prop-types'; +import Drawer from '@material-ui/core/Drawer'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import { withStyles } from '@material-ui/core/styles'; +import Divider from '@material-ui/core/Divider'; +import CloseIcon from '@material-ui/icons/Close'; +import PeopleOutlineIcon from '@material-ui/icons/PeopleOutline'; + +import ContributionCard from './ContributionCard/ContributionCard'; +import InfoBox from './InfoBox/InfoBox'; +import { + DEFAULT_TITLE, + PROMOTED_SECTION_LABEL, + INFO_PROMOTED_TITLE, + INFO_CONTRIBUTIONS_TEXT, + LEARN_MORE_LABEL, + LEARN_MORE_OPEN_DATA_MODEL_URL, +} from './constants'; +import InfoPromotedText from './InfoPromotedText/InfoPromotedText'; +import DrawerSubtitle from './DrawerSubtitle/DrawerSubtitle'; +import { + getContributorCount, + getContributionsCount, + getContributionsSectionLabel, +} from './utils'; +import contributionsDrawerStyles from './styles'; + +const ContributionsDrawer = ({ + classes, + open, + onClose, + fieldName, + promotedContribution, + contributions, +}) => { + const contributionsCount = getContributionsCount(contributions); + const contributorCount = getContributorCount([ + ...contributions, + promotedContribution, + ]); + const sectionLabel = getContributionsSectionLabel(contributions); + + return ( + +
+
+
+ + + {DEFAULT_TITLE} + +
+ + + +
+ + + {promotedContribution ? ( + <> + + {PROMOTED_SECTION_LABEL} + + + + + + + ) : null} + + + {sectionLabel} + + + {INFO_CONTRIBUTIONS_TEXT} + + {contributionsCount > 0 ? ( +
+ {contributions.map((item, index) => ( + + ))} +
+ ) : null} +
+
+ ); +}; + +ContributionsDrawer.propTypes = { + classes: object.isRequired, + open: bool.isRequired, + onClose: func.isRequired, + fieldName: string, + promotedContribution: shape({ + value: string, + sourceName: string, + date: oneOfType([string, instanceOf(Date)]), + linkUrl: string, + userId: oneOfType([string, number]), + }), + contributions: arrayOf( + shape({ + id: oneOfType([string, number]), + value: string, + sourceName: string, + date: oneOfType([string, instanceOf(Date)]), + linkUrl: string, + userId: oneOfType([string, number]), + }), + ), +}; + +ContributionsDrawer.defaultProps = { + fieldName: null, + promotedContribution: null, + contributions: [], +}; + +export default withStyles(contributionsDrawerStyles)(ContributionsDrawer); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/DrawerSubtitle/DrawerSubtitle.jsx b/src/react/src/components/ProductionLocation/ContributionsDrawer/DrawerSubtitle/DrawerSubtitle.jsx new file mode 100644 index 000000000..86d7fedba --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/DrawerSubtitle/DrawerSubtitle.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { object, string, number } from 'prop-types'; +import Typography from '@material-ui/core/Typography'; +import { withStyles } from '@material-ui/core/styles'; + +import drawerSubtitleStyles from './styles'; + +const DrawerSubtitle = ({ classes, fieldName, contributorCount }) => { + if (fieldName == null || fieldName === '') { + return null; + } + const count = contributorCount; + const orgText = + count === 1 ? '1 organization has' : `${count} organizations have`; + return ( + + {orgText} contributed data for{' '} + {fieldName} + + ); +}; + +DrawerSubtitle.propTypes = { + classes: object.isRequired, + fieldName: string, + contributorCount: number.isRequired, +}; + +DrawerSubtitle.defaultProps = { + fieldName: null, +}; + +export default withStyles(drawerSubtitleStyles)(DrawerSubtitle); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/DrawerSubtitle/styles.js b/src/react/src/components/ProductionLocation/ContributionsDrawer/DrawerSubtitle/styles.js new file mode 100644 index 000000000..02acfbeae --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/DrawerSubtitle/styles.js @@ -0,0 +1,17 @@ +import COLOURS from '../../../../util/COLOURS'; + +export default () => + Object.freeze({ + subtitle: Object.freeze({ + marginTop: '8px', + marginBottom: '0px', + fontSize: '1rem', + lineHeight: 1.4, + color: COLOURS.DARK_GREY, + paddingBottom: '16px', + }), + fieldName: Object.freeze({ + fontWeight: 600, + color: COLOURS.NEAR_BLACK, + }), + }); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoBox/InfoBox.jsx b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoBox/InfoBox.jsx new file mode 100644 index 000000000..048a4dae6 --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoBox/InfoBox.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { object, string, node, oneOf } from 'prop-types'; +import Typography from '@material-ui/core/Typography'; +import InfoIcon from '@material-ui/icons/InfoOutlined'; +import { withStyles } from '@material-ui/core/styles'; + +import infoBoxStyles from './styles'; + +const InfoBox = ({ + classes, + title, + children, + variant, + learnMoreUrl, + learnMoreLabel, +}) => { + const isPromoted = variant === 'promoted'; + const boxClass = isPromoted + ? classes.infoBoxPromoted + : classes.infoBoxContributions; + const titleClass = isPromoted + ? classes.infoTitle + : `${classes.infoTitle} ${classes.infoTitleBlue}`; + const infoTextClass = isPromoted + ? classes.infoTextPromoted + : classes.infoTextContributions; + const learnMoreLinkClass = isPromoted + ? classes.learnMoreLinkPromoted + : classes.learnMoreLinkContributions; + const showInfoIcon = !isPromoted; + + const content = ( + <> + {title ? ( + + {title} + + ) : null} + + {children} + + {learnMoreUrl && learnMoreLabel ? ( + + {learnMoreLabel} → + + ) : null} + + ); + + return ( +
+ {showInfoIcon ? ( +
+ +
{content}
+
+ ) : ( + content + )} +
+ ); +}; + +InfoBox.propTypes = { + classes: object.isRequired, + title: string, + children: node.isRequired, + variant: oneOf(['promoted', 'contributions']), + learnMoreUrl: string, + learnMoreLabel: string, +}; + +InfoBox.defaultProps = { + title: null, + variant: 'contributions', + learnMoreUrl: null, + learnMoreLabel: null, +}; + +export default withStyles(infoBoxStyles)(InfoBox); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoBox/styles.js b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoBox/styles.js new file mode 100644 index 000000000..06d202b4a --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoBox/styles.js @@ -0,0 +1,70 @@ +import COLOURS from '../../../../util/COLOURS'; + +export default () => + Object.freeze({ + infoBox: Object.freeze({ + padding: '16px', + marginBottom: '16px', + }), + infoBoxPromoted: Object.freeze({ + backgroundColor: COLOURS.LIGHT_LAVENDER, + border: `1px solid ${COLOURS.LIGHT_LAVENDER_BORDER}`, + }), + infoBoxContributions: Object.freeze({ + backgroundColor: COLOURS.EXTRA_LIGHT_BLUE, + border: `1px solid ${COLOURS.LIGHT_SKY_BLUE}`, + }), + infoTitle: Object.freeze({ + fontSize: '0.875rem', + lineHeight: 1.4, + color: COLOURS.PURPLE, + marginBottom: '8px', + }), + infoTitleBlue: Object.freeze({ + color: COLOURS.MATERIAL_BLUE, + }), + infoBoxWithIcon: Object.freeze({ + display: 'flex', + alignItems: 'flex-start', + gap: '8px', + }), + infoIcon: Object.freeze({ + fontSize: '1.25rem', + color: COLOURS.MATERIAL_BLUE, + flexShrink: 0, + marginTop: '2px', + marginRight: '8px', + }), + infoBoxContent: Object.freeze({ + flex: 1, + }), + infoTextPromoted: Object.freeze({ + fontSize: '0.875rem', + lineHeight: 1.5, + color: COLOURS.DARK_GREY, + }), + infoTextContributions: Object.freeze({ + fontSize: '1rem', + lineHeight: 1.5, + color: COLOURS.DARK_GREY, + }), + learnMoreLinkContributions: Object.freeze({ + marginTop: '8px', + display: 'inline-flex', + alignItems: 'center', + color: COLOURS.MATERIAL_BLUE, + fontSize: '1rem', + textDecoration: 'none', + '&:hover': Object.freeze({ + textDecoration: 'underline', + }), + }), + learnMoreLinkPromoted: Object.freeze({ + marginTop: '8px', + display: 'inline-flex', + alignItems: 'center', + color: COLOURS.MATERIAL_BLUE, + fontSize: '0.875rem', + textDecoration: 'none', + }), + }); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoPromotedText/InfoPromotedText.jsx b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoPromotedText/InfoPromotedText.jsx new file mode 100644 index 000000000..5d9f55745 --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoPromotedText/InfoPromotedText.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { object } from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; + +import infoPromotedTextStyles from './styles'; + +const InfoPromotedText = ({ classes }) => ( + <> + OS Hub automatically prioritizes data in this order: (1) claimed + locations where owners/managers submitted data, (2) most frequently + submitted values. The OS Hub team also actively moderates to promote + quality data. To request reordering, email{' '} + + Support + {' '} + with the OS ID, preferred data entry, and justification. + +); + +InfoPromotedText.propTypes = { + classes: object.isRequired, +}; + +export default withStyles(infoPromotedTextStyles)(InfoPromotedText); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoPromotedText/styles.js b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoPromotedText/styles.js new file mode 100644 index 000000000..033358261 --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/InfoPromotedText/styles.js @@ -0,0 +1,12 @@ +import COLOURS from '../../../../util/COLOURS'; + +export default () => + Object.freeze({ + supportLink: Object.freeze({ + color: COLOURS.PURPLE, + textDecoration: 'none', + '&:hover': Object.freeze({ + textDecoration: 'underline', + }), + }), + }); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/constants.js b/src/react/src/components/ProductionLocation/ContributionsDrawer/constants.js new file mode 100644 index 000000000..c0b2ae76f --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/constants.js @@ -0,0 +1,9 @@ +export const DEFAULT_TITLE = 'All Data Sources'; +export const PROMOTED_SECTION_LABEL = 'Highlighted Data Source'; +export const CONTRIBUTIONS_SECTION_LABEL = 'Other Data Sources'; +export const INFO_PROMOTED_TITLE = 'Why is this data source displayed first?'; +export const INFO_CONTRIBUTIONS_TEXT = + 'Multiple organizations may have shared information for this data point. You can see the list of historical data sources below. Click on the organization name to learn more about them and the data they have shared'; +export const LEARN_MORE_LABEL = 'Learn more about our open data model'; +export const LEARN_MORE_OPEN_DATA_MODEL_URL = + 'https://info.opensupplyhub.org/resources/an-open-data-model'; diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/styles.js b/src/react/src/components/ProductionLocation/ContributionsDrawer/styles.js new file mode 100644 index 000000000..ab766e8fb --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/styles.js @@ -0,0 +1,54 @@ +import COLOURS from '../../../util/COLOURS'; + +export default () => + Object.freeze({ + drawerPaper: Object.freeze({ + width: '390px', + boxShadow: '-4px 0 24px rgba(0,0,0,0.12)', + }), + drawerContent: Object.freeze({ + padding: '24px', + overflowY: 'auto', + height: '100%', + }), + header: Object.freeze({ + display: 'flex', + alignItems: 'flex-start', + justifyContent: 'space-between', + marginBottom: '12px', + }), + headerLeft: Object.freeze({ + display: 'flex', + alignItems: 'center', + flexWrap: 'wrap', + gap: '8px', + }), + titleIcon: Object.freeze({ + fontSize: '1.5rem', + color: COLOURS.PURPLE, + }), + title: Object.freeze({ + fontWeight: 700, + fontSize: '1.4rem', + lineHeight: 1.3, + color: COLOURS.NEAR_BLACK, + marginLeft: '10px', + }), + closeButton: Object.freeze({ + margin: '-8px', + color: COLOURS.DARK_GREY, + '&:hover': Object.freeze({ + backgroundColor: COLOURS.HOVER_GREY, + color: COLOURS.PURPLE, + }), + }), + sectionLabel: Object.freeze({ + fontSize: '0.9rem', + color: COLOURS.DARK_GREY, + textTransform: 'uppercase', + letterSpacing: '0.5px', + marginTop: '16px', + marginBottom: '8px', + lineHeight: 1.2, + }), + }); diff --git a/src/react/src/components/ProductionLocation/ContributionsDrawer/utils.js b/src/react/src/components/ProductionLocation/ContributionsDrawer/utils.js new file mode 100644 index 000000000..60bc970b6 --- /dev/null +++ b/src/react/src/components/ProductionLocation/ContributionsDrawer/utils.js @@ -0,0 +1,24 @@ +import { CONTRIBUTIONS_SECTION_LABEL } from './constants'; + +export const getContributionsCount = contributions => + Array.isArray(contributions) ? contributions.length : 0; + +export const getContributionsSectionLabel = contributions => { + const count = getContributionsCount(contributions); + return count > 0 + ? `${CONTRIBUTIONS_SECTION_LABEL} (${count})` + : CONTRIBUTIONS_SECTION_LABEL; +}; + +export const getContributorCount = contributions => { + if (!Array.isArray(contributions) || contributions.length === 0) { + return 0; + } + const definedIds = contributions + .map(contribution => contribution?.userId) + .filter(id => id != null); + + const uniqueCount = new Set(definedIds).size; + const anonymousCount = contributions.length - definedIds.length; + return uniqueCount + anonymousCount; +}; diff --git a/src/react/src/components/ProductionLocation/DataPoint/DataPoint.jsx b/src/react/src/components/ProductionLocation/DataPoint/DataPoint.jsx new file mode 100644 index 000000000..e42b8b67d --- /dev/null +++ b/src/react/src/components/ProductionLocation/DataPoint/DataPoint.jsx @@ -0,0 +1,220 @@ +import React from 'react'; +import { + object, + string, + oneOfType, + node, + instanceOf, + oneOf, + shape, + array, + func, + number, +} from 'prop-types'; +import Typography from '@material-ui/core/Typography'; +import Chip from '@material-ui/core/Chip'; +import Grid from '@material-ui/core/Grid'; +import { withStyles } from '@material-ui/core/styles'; +import { Link } from 'react-router-dom'; +import ScheduleIcon from '@material-ui/icons/Schedule'; +import PersonIcon from '@material-ui/icons/PersonOutline'; + +import IconComponent from '../../Shared/IconComponent/IconComponent'; +import { profileRoute, DATE_FORMATS } from '../../../util/constants'; +import { formatDate } from '../../../util/util'; +import getSourcesCount from './utils'; +import SourcesButton from './SourcesButton/SourcesButton'; +import { STATUS_CLAIMED, STATUS_CROWDSOURCED } from './constants'; +import dataPointStyles from './styles'; + +const DataPoint = ({ + classes, + label, + value, + tooltipText, + statusLabel, + contributorName, + userId, + date, + drawerData, + onOpenDrawer, + renderDrawer, +}) => { + const sourcesCount = getSourcesCount(drawerData); + const showSourcesButton = sourcesCount > 0 && onOpenDrawer && renderDrawer; + + const getStatusChipClass = () => { + if (statusLabel === STATUS_CLAIMED) return classes.claimedChip; + if (statusLabel === STATUS_CROWDSOURCED) + return classes.crowdsourcedChip; + return null; + }; + + const tooltipIcon = tooltipText ? ( + + ) : null; + + return ( + + + + + {label} + + + + {tooltipIcon} + + + + + + {value} + + + + + {(contributorName || statusLabel) && ( + + {statusLabel ? ( + + + + ) : null} + {contributorName ? ( + + + + {userId != null ? ( + + {contributorName} + + ) : ( + + {contributorName} + + )} + + + ) : null} + + )} + {(date || showSourcesButton) && ( + + {date ? ( + <> + + + + + {formatDate( + date, + DATE_FORMATS.LONG, + )} + + + + + ) : null} + {showSourcesButton && ( + + + + )} + + )} + + + {renderDrawer && typeof renderDrawer === 'function' + ? renderDrawer() + : null} + + ); +}; + +DataPoint.propTypes = { + classes: object.isRequired, + label: string.isRequired, + value: oneOfType([string, node]).isRequired, + tooltipText: string, + statusLabel: oneOf([STATUS_CLAIMED, STATUS_CROWDSOURCED]), + contributorName: string, + userId: oneOfType([string, number]), + date: oneOfType([string, instanceOf(Date)]), + drawerData: shape({ + promotedContribution: object, + contributions: array, + title: string, + subtitle: oneOfType([string, node]), + }), + onOpenDrawer: func, + renderDrawer: func, +}; + +DataPoint.defaultProps = { + tooltipText: null, + statusLabel: null, + contributorName: null, + userId: null, + date: null, + drawerData: null, + onOpenDrawer: null, + renderDrawer: null, +}; + +export default withStyles(dataPointStyles)(DataPoint); diff --git a/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/SourcesButton.jsx b/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/SourcesButton.jsx new file mode 100644 index 000000000..346daab5a --- /dev/null +++ b/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/SourcesButton.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { object, number, func } from 'prop-types'; +import Button from '@material-ui/core/Button'; +import { withStyles } from '@material-ui/core/styles'; + +import getSourcesButtonLabel from './utils'; +import sourcesButtonStyles from './styles'; + +const SourcesButton = ({ classes, sourcesCount, onOpenDrawer }) => { + const sourcesButtonLabel = getSourcesButtonLabel(sourcesCount); + + return ( + + ); +}; + +SourcesButton.propTypes = { + classes: object.isRequired, + sourcesCount: number.isRequired, + onOpenDrawer: func.isRequired, +}; + +export default withStyles(sourcesButtonStyles)(SourcesButton); diff --git a/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/styles.js b/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/styles.js new file mode 100644 index 000000000..edead9d23 --- /dev/null +++ b/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/styles.js @@ -0,0 +1,18 @@ +import COLOURS from '../../../../util/COLOURS'; + +export default () => + Object.freeze({ + button: Object.freeze({ + textTransform: 'none', + fontWeight: 600, + fontSize: '1.125rem', + lineHeight: 1.7, + padding: '0px', + minHeight: '0px', + color: COLOURS.PURPLE, + '&:hover': Object.freeze({ + backgroundColor: 'transparent', + textDecoration: 'underline', + }), + }), + }); diff --git a/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/utils.js b/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/utils.js new file mode 100644 index 000000000..921b81fe1 --- /dev/null +++ b/src/react/src/components/ProductionLocation/DataPoint/SourcesButton/utils.js @@ -0,0 +1,6 @@ +const getSourcesButtonLabel = sourcesCount => { + const sourceWord = sourcesCount === 1 ? 'source' : 'sources'; + return `+${sourcesCount} data ${sourceWord}`; +}; + +export default getSourcesButtonLabel; diff --git a/src/react/src/components/ProductionLocation/DataPoint/constants.js b/src/react/src/components/ProductionLocation/DataPoint/constants.js new file mode 100644 index 000000000..5fc2e7665 --- /dev/null +++ b/src/react/src/components/ProductionLocation/DataPoint/constants.js @@ -0,0 +1,2 @@ +export const STATUS_CLAIMED = 'Claimed'; +export const STATUS_CROWDSOURCED = 'Crowdsourced'; diff --git a/src/react/src/components/ProductionLocation/DataPoint/styles.js b/src/react/src/components/ProductionLocation/DataPoint/styles.js new file mode 100644 index 000000000..a2c386003 --- /dev/null +++ b/src/react/src/components/ProductionLocation/DataPoint/styles.js @@ -0,0 +1,165 @@ +import COLOURS from '../../../util/COLOURS'; + +export default () => + Object.freeze({ + root: Object.freeze({ + paddingTop: '12px', + paddingBottom: '12px', + flexWrap: 'nowrap', + '&:hover $tooltipIcon': Object.freeze({ + opacity: 1, + '& > svg': Object.freeze({ + '&:hover': Object.freeze({ + color: COLOURS.PURPLE, + }), + }), + }), + }), + labelColumn: Object.freeze({ + width: '165px', + flexDirection: 'row', + }), + labelItem: Object.freeze({ + width: '80%', + }), + label: Object.freeze({ + fontSize: '1.125rem', + lineHeight: 1.7, + color: COLOURS.DARK_GREY, + }), + tooltipIconItem: Object.freeze({ + display: 'flex', + alignItems: 'flex-start', + width: '20%', + }), + tooltipIcon: Object.freeze({ + opacity: 0, + }), + valueColumn: Object.freeze({ + minWidth: 0, + flex: 1, + overflow: 'hidden', + flexDirection: 'column', + }), + valueWithTooltip: Object.freeze({ + display: 'inline-flex', + alignItems: 'center', + marginBottom: '4px', + }), + value: Object.freeze({ + fontSize: '1.125rem', + fontWeight: 600, + lineHeight: 1.4, + }), + statusChip: Object.freeze({ + height: '24px', + fontSize: '1rem', + fontWeight: 600, + borderRadius: 0, + marginRight: '8px', + '& .MuiChip-label': Object.freeze({ + paddingLeft: '8px', + paddingRight: '8px', + }), + }), + claimedChip: Object.freeze({ + backgroundColor: COLOURS.CLAIMED_CHIP_BG, + color: COLOURS.DARK_GREEN, + }), + crowdsourcedChip: Object.freeze({ + backgroundColor: COLOURS.CROWDSOURCED_CHIP_BG, + color: COLOURS.CROWDSOURCED_CHIP_TEXT, + }), + metaRowContainer: Object.freeze({ + marginLeft: '-16px', + }), + metaRow: Object.freeze({ + fontSize: '1rem', + width: 'fit-content', + lineHeight: 1.43, + color: COLOURS.DARK_GREY, + marginLeft: '16px', + alignItems: 'center', + flexWrap: 'wrap', + }), + metaRowSecondary: Object.freeze({ + fontSize: '1rem', + lineHeight: 1.43, + color: COLOURS.DARK_GREY, + width: 'fit-content', + alignItems: 'center', + flexWrap: 'wrap', + }), + contributorItem: Object.freeze({ + width: 'fit-content', + }), + contributor: Object.freeze({ + display: 'inline-flex', + alignItems: 'center', + }), + personIcon: Object.freeze({ + marginRight: '4px', + marginTop: '2px', + fontSize: '1rem', + color: COLOURS.DARK_GREY, + }), + contributorName: Object.freeze({ + fontSize: '1.125rem', + lineHeight: 1.7, + color: COLOURS.DARK_GREY, + }), + contributorNameLink: Object.freeze({ + textDecoration: 'none', + cursor: 'pointer', + fontWeight: 500, + '&:hover': Object.freeze({ + textDecoration: 'underline', + }), + }), + dateItem: Object.freeze({ + position: 'relative', + marginLeft: '16px', + '&::before': Object.freeze({ + content: "'·'", + fontSize: '1.25rem', + position: 'absolute', + left: '-9px', + top: '50%', + transform: 'translateY(-50%)', + color: COLOURS.DARK_GREY, + }), + }), + dateBlock: Object.freeze({ + display: 'inline-flex', + alignItems: 'center', + }), + dateIcon: Object.freeze({ + marginRight: '4px', + fontSize: '0.8125rem', + color: COLOURS.DARK_GREY, + marginTop: '3px', + }), + dateText: Object.freeze({ + fontSize: '1.125rem', + lineHeight: 1.7, + color: COLOURS.DARK_GREY, + }), + metaSeparator: Object.freeze({ + marginLeft: '4px', + marginRight: '4px', + color: COLOURS.DARK_GREY, + }), + sourcesButtonItem: Object.freeze({ + marginLeft: '16px', + position: 'relative', + '&::before': Object.freeze({ + content: "'·'", + fontSize: '1.25rem', + position: 'absolute', + left: '-9px', + top: '50%', + transform: 'translateY(-50%)', + color: COLOURS.DARK_GREY, + }), + }), + }); diff --git a/src/react/src/components/ProductionLocation/DataPoint/utils.js b/src/react/src/components/ProductionLocation/DataPoint/utils.js new file mode 100644 index 000000000..a0708fe97 --- /dev/null +++ b/src/react/src/components/ProductionLocation/DataPoint/utils.js @@ -0,0 +1,6 @@ +export default function getSourcesCount(drawerData) { + if (!drawerData) return 0; + return Array.isArray(drawerData.contributions) + ? drawerData.contributions.length + : 0; +} diff --git a/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimStatusRow.jsx b/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimStatusRow.jsx index 30a69a532..6b9861a4a 100644 --- a/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimStatusRow.jsx +++ b/src/react/src/components/ProductionLocation/Heading/ClaimFlag/ClaimStatusRow.jsx @@ -2,11 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import InfoIcon from '@material-ui/icons/Info'; +import InfoOutlined from '@material-ui/icons/InfoOutlined'; import BadgeClaimed from '../../../BadgeClaimed'; -import DialogTooltip from '../../../Contribute/DialogTooltip'; +import IconComponent from '../../../Shared/IconComponent/IconComponent'; +import LearnMoreLink from '../../Shared/LearnMoreLink/LearnMoreLink'; import { getMainText, getClaimFlagStateClassName } from './utils'; import { @@ -58,41 +58,17 @@ const ClaimStatusRow = ({ classes, isClaimed, isPending }) => { {getMainText(isClaimed, isPending)} {isClaimed && ( - - + {CLAIMED_PROFILE_TOOLTIP_TEXT} + - Learn more → - -

- } - interactive - childComponent={ - - - + } + icon={InfoOutlined} + className={classes.infoButton} /> )} diff --git a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx index 6dac4ff06..93bcb08b7 100644 --- a/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx +++ b/src/react/src/components/ProductionLocation/Heading/DataSourcesInfo/DataSourcesInfo.jsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; import Grid from '@material-ui/core/Grid'; import Switch from '@material-ui/core/Switch'; import { withStyles } from '@material-ui/core/styles'; -import InfoIcon from '@material-ui/icons/Info'; +import InfoOutlined from '@material-ui/icons/InfoOutlined'; -import DialogTooltip from '../../../Contribute/DialogTooltip'; +import IconComponent from '../../../Shared/IconComponent/IconComponent'; +import LearnMoreLink from '../../Shared/LearnMoreLink/LearnMoreLink'; import DataSourceItem from './DataSourceItem'; import { DATA_SOURCES_TOOLTIP_TEXT, @@ -28,31 +28,16 @@ const ProductionLocationDetailsDataSourcesInfo = ({ classes, className }) => { > Understanding Data Sources - - - Learn more → - -

- } - interactive - childComponent={ - - - + + {DATA_SOURCES_TOOLTIP_TEXT} + + } + icon={InfoOutlined} + className={classes.infoButton} + data-testid="data-sources-info-tooltip" />
{ />
- + {DATA_SOURCES_ITEMS.map(item => ( { }), titleAccent: Object.freeze({ ...typography.bodyText, + fontSize: '1.15rem', }), }); }; diff --git a/src/react/src/components/ProductionLocation/Heading/osIdBadge/OsIdBadge.jsx b/src/react/src/components/ProductionLocation/Heading/OsIdBadge/OsIdBadge.jsx similarity index 71% rename from src/react/src/components/ProductionLocation/Heading/osIdBadge/OsIdBadge.jsx rename to src/react/src/components/ProductionLocation/Heading/OsIdBadge/OsIdBadge.jsx index 2e26cdcf8..d86273d02 100644 --- a/src/react/src/components/ProductionLocation/Heading/osIdBadge/OsIdBadge.jsx +++ b/src/react/src/components/ProductionLocation/Heading/OsIdBadge/OsIdBadge.jsx @@ -2,15 +2,15 @@ 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 InfoOutlined from '@material-ui/icons/InfoOutlined'; import CopySearch from '../../../CopySearch'; import ContentCopyIcon from '../../../ContentCopyIcon'; -import DialogTooltip from '../../../Contribute/DialogTooltip'; +import IconComponent from '../../../Shared/IconComponent/IconComponent'; +import LearnMoreLink from '../../Shared/LearnMoreLink/LearnMoreLink'; import productionLocationDetailsOsIdBadgeStyles from './styles'; import { OS_ID_TOOLTIP_TEXT, OS_ID_LEARN_MORE_URL } from './constants'; @@ -25,31 +25,16 @@ const ProductionLocationDetailsOsIdBadge = ({ classes, osId }) => ( > OS ID: {osId} - - - Learn more → - -

- } - interactive - childComponent={ - - - + + {OS_ID_TOOLTIP_TEXT} + + } + icon={InfoOutlined} + className={classes.osIdInfoButton} + data-testid="os-id-badge-info" /> {osId && ( diff --git a/src/react/src/components/ProductionLocation/Heading/osIdBadge/constants.js b/src/react/src/components/ProductionLocation/Heading/OsIdBadge/constants.js similarity index 100% rename from src/react/src/components/ProductionLocation/Heading/osIdBadge/constants.js rename to src/react/src/components/ProductionLocation/Heading/OsIdBadge/constants.js diff --git a/src/react/src/components/ProductionLocation/Heading/osIdBadge/styles.js b/src/react/src/components/ProductionLocation/Heading/OsIdBadge/styles.js similarity index 100% rename from src/react/src/components/ProductionLocation/Heading/osIdBadge/styles.js rename to src/react/src/components/ProductionLocation/Heading/OsIdBadge/styles.js diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetails/styles.js b/src/react/src/components/ProductionLocation/ProductionLocationDetails/styles.js index 692ecc0f3..610940824 100644 --- a/src/react/src/components/ProductionLocation/ProductionLocationDetails/styles.js +++ b/src/react/src/components/ProductionLocation/ProductionLocationDetails/styles.js @@ -1,6 +1,8 @@ +import COLOURS from '../../../util/COLOURS'; + export default theme => ({ container: { - backgroundColor: '#F9F7F7', + backgroundColor: COLOURS.LIGHT_GREY, height: '100%', display: 'flex', flexDirection: 'column', diff --git a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx index 46e59c711..c7d9354f2 100644 --- a/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx +++ b/src/react/src/components/ProductionLocation/ProductionLocationDetailsContent/ProductionLocationDetailsContent.jsx @@ -17,7 +17,7 @@ import DetailsMap from '../ProductionLocationDetailsMap/ProductionLocationDetail import { facilityClaimStatusChoicesEnum } from '../../../util/constants'; import productionLocationDetailsContentStyles from './styles'; -import OsIdBadge from '../Heading/osIdBadge/OsIdBadge'; +import OsIdBadge from '../Heading/OsIdBadge/OsIdBadge'; const ProductionLocationDetailsContent = ({ classes, @@ -53,7 +53,7 @@ const ProductionLocationDetailsContent = ({ - + diff --git a/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/LearnMoreLink.jsx b/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/LearnMoreLink.jsx new file mode 100644 index 000000000..211d48c2c --- /dev/null +++ b/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/LearnMoreLink.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { string, node, object } from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; + +import learnMoreLinkStyles from './styles'; +import DEFAULT_LINK_TEXT from './constants'; + +const LearnMoreLink = ({ href, children, classes }) => ( + +); + +LearnMoreLink.propTypes = { + href: string.isRequired, + children: node, + classes: object.isRequired, +}; + +LearnMoreLink.defaultProps = { + children: DEFAULT_LINK_TEXT, +}; + +export default withStyles(learnMoreLinkStyles)(LearnMoreLink); diff --git a/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/constants.js b/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/constants.js new file mode 100644 index 000000000..edc95d4c3 --- /dev/null +++ b/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/constants.js @@ -0,0 +1,3 @@ +export const DEFAULT_LINK_TEXT = 'Learn more →'; + +export default DEFAULT_LINK_TEXT; diff --git a/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/styles.js b/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/styles.js new file mode 100644 index 000000000..8f42e9333 --- /dev/null +++ b/src/react/src/components/ProductionLocation/Shared/LearnMoreLink/styles.js @@ -0,0 +1,16 @@ +import COLOURS from '../../../../util/COLOURS'; + +export default () => + Object.freeze({ + linkContainer: Object.freeze({ + marginTop: '8px', + marginBottom: '0px', + }), + link: Object.freeze({ + color: COLOURS.PURPLE, + textDecoration: 'none', + '&:hover': Object.freeze({ + textDecoration: 'underline', + }), + }), + }); diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx index 8580350b6..c10bf0076 100644 --- a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx +++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/ContributeFields.jsx @@ -46,7 +46,7 @@ const ProductionLocationDetailsContributeFields = ({ Contribute to this profile - Help improve supply chain transparency + Ways you can improve data on this page @@ -118,7 +118,9 @@ const ProductionLocationDetailsContributeFields = ({ variant="body1" className={classes.actionLabel} > - {isClosed ? 'Report Reopened' : 'Report Closed'} + {isClosed + ? 'Report Reopened' + : 'Report Closure / Move'} diff --git a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js index b96125d25..ff5c0c5aa 100644 --- a/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js +++ b/src/react/src/components/ProductionLocation/Sidebar/ContributeFields/styles.js @@ -9,11 +9,11 @@ export default theme => }), title: Object.freeze({ fontWeight: 600, - fontSize: '1.125rem', + fontSize: '1.25rem', }), subtitle: Object.freeze({ color: theme.palette.text.secondary, - fontSize: '0.875rem', + fontSize: '1rem', marginBottom: '12px', }), actionsList: Object.freeze({ @@ -50,6 +50,6 @@ export default theme => }), actionLabel: Object.freeze({ fontWeight: 500, - fontSize: '1rem', + fontSize: '1.15rem', }), }); diff --git a/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js b/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js index 309edc583..a2b0a6912 100644 --- a/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js +++ b/src/react/src/components/ProductionLocation/Sidebar/NavBar/styles.js @@ -10,7 +10,7 @@ export default theme => }), title: Object.freeze({ fontWeight: 600, - fontSize: '1.125rem', + fontSize: '1.25rem', margin: '0 0 12px 0', }), menuList: Object.freeze({ @@ -42,8 +42,8 @@ export default theme => color: theme.palette.primary.main, }), menuLabel: Object.freeze({ - fontSize: '1rem', - color: COLOURS.BLACK, + fontSize: '1.15rem', + fontWeight: 500, }), menuLabelActive: Object.freeze({ fontWeight: 600, diff --git a/src/react/src/components/Shared/IconComponent/IconComponent.jsx b/src/react/src/components/Shared/IconComponent/IconComponent.jsx new file mode 100644 index 000000000..1907cc2ef --- /dev/null +++ b/src/react/src/components/Shared/IconComponent/IconComponent.jsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { string, number, shape, func, oneOfType, node } from 'prop-types'; +import Tooltip from '@material-ui/core/Tooltip'; +import HelpOutline from '@material-ui/icons/HelpOutline'; +import { withStyles } from '@material-ui/core/styles'; + +import useInteractiveTooltip from './hooks'; +import iconComponentStyles from './styles'; + +const IconComponent = ({ + title, + placement, + enterDelay, + classes, + className, + icon: Icon, + 'data-testid': dataTestId, +}) => { + const { + open, + setOpen, + handleTriggerEnter, + handleTriggerLeave, + handlePopperEnter, + handlePopperLeave, + } = useInteractiveTooltip(); + + return ( + setOpen(false)} + classes={{ popper: classes.popper, tooltip: classes.tooltip }} + PopperProps={{ + onMouseEnter: handlePopperEnter, + onMouseLeave: handlePopperLeave, + }} + > + + + + + ); +}; + +IconComponent.defaultProps = { + placement: 'top', + enterDelay: 0, + className: undefined, + icon: HelpOutline, + 'data-testid': undefined, +}; + +IconComponent.propTypes = { + title: oneOfType([string, node]).isRequired, + placement: string, + enterDelay: number, + classes: shape({ + popper: string, + tooltip: string.isRequired, + defaultTooltipIcon: string, + icon: string.isRequired, + }).isRequired, + className: string, + icon: func, + 'data-testid': string, +}; + +export default withStyles(iconComponentStyles)(IconComponent); diff --git a/src/react/src/components/Shared/IconComponent/constants.js b/src/react/src/components/Shared/IconComponent/constants.js new file mode 100644 index 000000000..f0c1cee07 --- /dev/null +++ b/src/react/src/components/Shared/IconComponent/constants.js @@ -0,0 +1,3 @@ +const LEAVE_TRIGGER_DELAY_MS = 150; + +export default LEAVE_TRIGGER_DELAY_MS; diff --git a/src/react/src/components/Shared/IconComponent/hooks.js b/src/react/src/components/Shared/IconComponent/hooks.js new file mode 100644 index 000000000..0d6885529 --- /dev/null +++ b/src/react/src/components/Shared/IconComponent/hooks.js @@ -0,0 +1,49 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; + +import LEAVE_TRIGGER_DELAY_MS from './constants'; + +const useInteractiveTooltip = () => { + const [open, setOpen] = useState(false); + const closeTimerRef = useRef(null); + + const clearCloseTimer = useCallback(() => { + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + } + }, []); + + const handleTriggerEnter = useCallback(() => { + clearCloseTimer(); + setOpen(true); + }, [clearCloseTimer]); + + const handleTriggerLeave = useCallback(() => { + clearCloseTimer(); + closeTimerRef.current = setTimeout( + () => setOpen(false), + LEAVE_TRIGGER_DELAY_MS, + ); + }, [clearCloseTimer]); + + const handlePopperEnter = useCallback(() => { + clearCloseTimer(); + setOpen(true); + }, [clearCloseTimer]); + + const handlePopperLeave = useCallback(() => setOpen(false), []); + + useEffect(() => () => clearCloseTimer(), [clearCloseTimer]); + + return { + open, + setOpen, + clearCloseTimer, + handleTriggerEnter, + handleTriggerLeave, + handlePopperEnter, + handlePopperLeave, + }; +}; + +export default useInteractiveTooltip; diff --git a/src/react/src/components/Shared/IconComponent/styles.js b/src/react/src/components/Shared/IconComponent/styles.js new file mode 100644 index 000000000..3837b0340 --- /dev/null +++ b/src/react/src/components/Shared/IconComponent/styles.js @@ -0,0 +1,27 @@ +import COLOURS from '../../../util/COLOURS'; + +export default theme => + Object.freeze({ + popper: Object.freeze({ + opacity: 1, + }), + defaultTooltipIcon: Object.freeze({ + display: 'inline-flex', + }), + tooltip: Object.freeze({ + fontSize: '0.875rem', + backgroundColor: COLOURS.WHITE, + color: COLOURS.BLACK, + border: `1px solid ${COLOURS.GREY}`, + padding: `${theme.spacing.unit * 1.5}px`, + maxWidth: '300px', + }), + tooltipVisible: Object.freeze({ + opacity: '1 !important', + }), + icon: Object.freeze({ + fontSize: '1rem', + color: COLOURS.DARK_GREY, + cursor: 'pointer', + }), + }); diff --git a/src/react/src/util/COLOURS.js b/src/react/src/util/COLOURS.js index 1944e9ba3..3f5d81652 100644 --- a/src/react/src/util/COLOURS.js +++ b/src/react/src/util/COLOURS.js @@ -6,11 +6,13 @@ export default { LIGHT_MATERIAL_BLUE: '#1565c0', EXTRA_LIGHT_BLUE: '#e3f2fd', LIGHT_BLUE_BORDER: '#90caf9', + LIGHT_SKY_BLUE: '#C0DBFE', DARK_BLUE: '#0d47a1', // Greens GREEN: '#E0F5E3', DARK_GREEN: '#4A9957', + CLAIMED_CHIP_BG: '#F0FDF480', MINT_GREEN: '#C0EBC7', OLIVA_GREEN: '#799679', MATERIAL_GREEN: '#388e3c', @@ -34,12 +36,16 @@ export default { PURPLE_TEXT: '#6a1b9a', LIGHT_PURPLE_BG: '#f3e5f5', LIGHT_PURPLE_BORDER: '#ce93d8', + LIGHT_LAVENDER: '#F8F5FB', + LIGHT_LAVENDER_BORDER: '#E2D1F0', // Oranges/Ambers AMBER: '#ffc107', DARK_AMBER: '#ffa000', DEEP_ORANGE: '#e65100', ORANGE: '#f57c00', + CROWDSOURCED_CHIP_BG: '#FFF7ED80', + CROWDSOURCED_CHIP_TEXT: '#C2410C', AMBER_YELLOW: '#fbc02d', LIGHT_AMBER: '#fff8e1', AMBER_50: '#fffbeb',