Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/react/src/__tests__/components/FacilityDetailsDetail.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import FacilityDetailsDetail from './../../components/FacilityDetailsDetail';

describe('FacilityDetailsDetail', () => {
Expand Down Expand Up @@ -163,5 +164,48 @@ describe('FacilityDetailsDetail', () => {

expect(container.querySelector('.unitText')).not.toBeInTheDocument();
});

test('renders secondary as plain text when no contributor link props', () => {
const props = {
primary: 'Test Primary',
secondary: 'October 1, 2025 by Acme Corp',
classes: {
root: 'root',
primaryText: 'primaryText',
secondaryText: 'secondaryText',
},
};

render(<FacilityDetailsDetail {...props} />);

expect(screen.getByText('October 1, 2025 by Acme Corp')).toBeInTheDocument();
});

test('renders contributor name as profile link when contributorProfileUrl and contributorName are present', () => {
const props = {
primary: 'Test Value',
contributorProfileUrl: '/profile/10',
contributorName: 'Acme Corp',
secondaryDate: 'October 1, 2025',
classes: {
root: 'root',
primaryText: 'primaryText',
secondaryText: 'secondaryText',
contributorLink: 'contributorLink',
},
};

render(
<MemoryRouter>
<FacilityDetailsDetail {...props} />
</MemoryRouter>,
);

expect(screen.getByText('Acme Corp')).toBeInTheDocument();
const link = screen.getByRole('link', { name: /View profile of Acme Corp/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/profile/10');
expect(screen.getByText(/October 1, 2025 by/)).toBeInTheDocument();
});
});

27 changes: 27 additions & 0 deletions src/react/src/__tests__/utils.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2650,6 +2650,33 @@ describe('formatExtendedField', () => {
expect(result.isVerified).toBe(true);
expect(result.isFromClaim).toBe(false);
});

it('includes contributor profile link fields when contributor_id is present', () => {
const result = formatExtendedField({
...baseProps,
value: ['Val'],
field_name: 'name',
contributor_id: 42,
});

expect(result.contributor_id).toBe(42);
expect(result.contributor_name).toBe('Test Contributor');
expect(result.contributorProfileUrl).toBe('/profile/42');
expect(result.contributorName).toBe('Test Contributor');
expect(result.secondaryDate).toBe('October 1, 2025');
});

it('sets contributor link fields to null when contributor_id is absent', () => {
const result = formatExtendedField({
...baseProps,
value: ['Val'],
field_name: 'name',
});

expect(result.contributor_id).toBeNull();
expect(result.contributorProfileUrl).toBeNull();
expect(result.secondaryDate).toBe('October 1, 2025');
});
});

describe('processDromoResults', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ const FacilityDetailsContributorsDrawer = ({
<Link
to={makeProfileRouteLink(contributor.id)}
className={`${classes.primaryText} ${classes.link}`}
target="_blank"
rel="noopener noreferrer"
>
{contributor.contributor_name}
</Link>
Expand Down
146 changes: 105 additions & 41 deletions src/react/src/components/FacilityDetailsDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Tooltip from '@material-ui/core/Tooltip';
import { Link } from 'react-router-dom';

import ShowOnly from './ShowOnly';
import BadgeVerified from './BadgeVerified';
Expand Down Expand Up @@ -54,6 +55,18 @@ const detailsStyles = theme =>
lineHeight: '21px',
verticalAlign: 'baseline',
},
contributorLink: {
color: theme.palette.primary.main,
textDecoration: 'none',
fontWeight: 500,
'&:hover': {
textDecoration: 'underline',
},
'&:focus': {
outline: '2px solid',
outlineOffset: '2px',
},
},
});

const CLAIM_EXPLANATORY_TEXT =
Expand All @@ -74,50 +87,94 @@ const FacilityDetailsDetail = ({
isFromClaim,
classes,
partnerConfigFields,
}) => (
<div className={classes.root} data-testid="facility-details-detail">
<ShowOnly when={isVerified || isFromClaim}>
<div className={classes.badgeWrapper}>
<ShowOnly when={isVerified && !isFromClaim}>
<BadgeVerified />
</ShowOnly>
<FeatureFlag flag={CLAIM_A_FACILITY}>
<ShowOnly when={isFromClaim}>
<Tooltip title={CLAIM_EXPLANATORY_TEXT}>
<BadgeVerified />
</Tooltip>
</ShowOnly>
</FeatureFlag>
</div>
</ShowOnly>
<div>
<Typography className={classes.primaryText} component="div">
{jsonSchema ? (
<PartnerFieldSchemaValue
value={primary}
jsonSchema={jsonSchema}
partnerConfigFields={partnerConfigFields}
/>
) : (
primary || locationLabeled
)}
{unit ? <span className={classes.unitText}>{unit}</span> : null}
contributorProfileUrl,
contributorName,
secondaryDate,
}) => {
const renderSecondary = () => {
if (!secondary && !(contributorProfileUrl && contributorName)) {
return null;
}
if (contributorProfileUrl && contributorName) {
return (
<Typography className={classes.secondaryText} component="span">
{secondaryDate ? (
<>
{secondaryDate} by{' '}
<Link
to={contributorProfileUrl}
className={classes.contributorLink}
aria-label={`View profile of ${contributorName}`}
target="_blank"
rel="noopener noreferrer"
>
{contributorName}
</Link>
</>
) : (
<Link
to={contributorProfileUrl}
className={classes.contributorLink}
aria-label={`View profile of ${contributorName}`}
target="_blank"
rel="noopener noreferrer"
>
{contributorName}
</Link>
)}
</Typography>
);
}
return (
<Typography className={classes.secondaryText}>
{secondary}
</Typography>
{sourceBy ? (
<Typography
className={classes.sourceText}
component="div"
dangerouslySetInnerHTML={{ __html: sourceBy }}
/>
) : null}
{secondary ? (
<Typography className={classes.secondaryText}>
{secondary}
);
};

return (
<div className={classes.root} data-testid="facility-details-detail">
<ShowOnly when={isVerified || isFromClaim}>
<div className={classes.badgeWrapper}>
<ShowOnly when={isVerified && !isFromClaim}>
<BadgeVerified />
</ShowOnly>
<FeatureFlag flag={CLAIM_A_FACILITY}>
<ShowOnly when={isFromClaim}>
<Tooltip title={CLAIM_EXPLANATORY_TEXT}>
<BadgeVerified />
</Tooltip>
</ShowOnly>
</FeatureFlag>
</div>
</ShowOnly>
<div>
<Typography className={classes.primaryText} component="div">
{jsonSchema ? (
<PartnerFieldSchemaValue
value={primary}
jsonSchema={jsonSchema}
partnerConfigFields={partnerConfigFields}
/>
) : (
primary || locationLabeled
)}
{unit ? (
<span className={classes.unitText}>{unit}</span>
) : null}
</Typography>
) : null}
{sourceBy ? (
<Typography
className={classes.sourceText}
component="div"
dangerouslySetInnerHTML={{ __html: sourceBy }}
/>
) : null}
{renderSecondary()}
</div>
</div>
</div>
);
);
};

FacilityDetailsDetail.propTypes = {
primary: PropTypes.oneOfType([
Expand All @@ -136,13 +193,17 @@ FacilityDetailsDetail.propTypes = {
baseUrl: PropTypes.string,
displayText: PropTypes.string,
}),
contributorProfileUrl: PropTypes.string,
contributorName: PropTypes.string,
secondaryDate: PropTypes.string,
classes: PropTypes.shape({
root: PropTypes.string,
badgeWrapper: PropTypes.string,
primaryText: PropTypes.string,
secondaryText: PropTypes.string,
sourceText: PropTypes.string,
unitText: PropTypes.string,
contributorLink: PropTypes.string,
}).isRequired,
};

Expand All @@ -156,6 +217,9 @@ FacilityDetailsDetail.defaultProps = {
isVerified: false,
isFromClaim: false,
partnerConfigFields: null,
contributorProfileUrl: null,
contributorName: null,
secondaryDate: null,
};

export default withStyles(detailsStyles)(FacilityDetailsDetail);
15 changes: 15 additions & 0 deletions src/react/src/components/FacilityDetailsItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const FacilityDetailsItem = ({
additionalContentTextPlural = 'entries',
partnerConfigFields,
showDivider,
contributorProfileUrl,
contributorName,
secondaryDate,
}) => {
const [isOpen, setIsOpen] = useState(false);
const hasAdditionalContent = !embed && !!additionalContent?.length;
Expand All @@ -73,6 +76,9 @@ const FacilityDetailsItem = ({
isVerified={isVerified}
isFromClaim={isFromClaim}
partnerConfigFields={partnerConfigFields}
contributorProfileUrl={contributorProfileUrl}
contributorName={contributorName}
secondaryDate={secondaryDate}
/>
<ShowOnly when={hasAdditionalContent}>
<Button
Expand Down Expand Up @@ -110,6 +116,9 @@ const FacilityDetailsItem = ({
isVerified={isVerified}
isFromClaim={isFromClaim}
partnerConfigFields={partnerConfigFields}
contributorProfileUrl={contributorProfileUrl}
contributorName={contributorName}
secondaryDate={secondaryDate}
/>
</div>
{isOpen &&
Expand Down Expand Up @@ -164,6 +173,9 @@ FacilityDetailsItem.propTypes = {
displayText: PropTypes.string,
}),
showDivider: PropTypes.bool,
contributorProfileUrl: PropTypes.string,
contributorName: PropTypes.string,
secondaryDate: PropTypes.string,
};

FacilityDetailsItem.defaultProps = {
Expand All @@ -184,6 +196,9 @@ FacilityDetailsItem.defaultProps = {
additionalContentTextPlural: 'entries',
partnerConfigFields: null,
showDivider: false,
contributorProfileUrl: null,
contributorName: null,
secondaryDate: null,
};

export default withStyles(detailsStyles)(FacilityDetailsItem);
15 changes: 15 additions & 0 deletions src/react/src/components/FacilityDetailsLocation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import partition from 'lodash/partition';
import FacilityDetailsItem from './FacilityDetailsItem';

import { facilityDetailsActions } from '../util/constants';
import { makeProfileRouteLink } from '../util/util';

const getDetailsText = ({
embed,
Expand Down Expand Up @@ -70,13 +71,27 @@ const FacilityDetailsLocation = ({ data, embed }) => {
secondary={detailsText}
embed={embed}
isFromClaim={canonicalLocationData?.is_from_claim}
contributorProfileUrl={
canonicalLocationData?.contributor_id
? makeProfileRouteLink(
canonicalLocationData.contributor_id,
)
: null
}
contributorName={
canonicalLocationData?.contributor_name ?? null
}
additionalContent={otherLocationsData
.filter(item => !item.has_invalid_location)
.map((item, i) => ({
primary: `${item.lat}, ${item.lng}`,
secondary: item.contributor_name,
key: `${item.lng}, ${item.lat} - ${i}`,
isFromClaim: item.is_from_claim,
contributorProfileUrl: item.contributor_id
? makeProfileRouteLink(item.contributor_id)
: null,
contributorName: item.contributor_name ?? null,
}))}
/>
</>
Expand Down
9 changes: 9 additions & 0 deletions src/react/src/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1819,6 +1819,7 @@ export const formatExtendedField = ({
label,
json_schema,
contributor_name,
contributor_id,
is_from_claim,
is_verified,
formatValue = rawValue => rawValue,
Expand All @@ -1840,6 +1841,14 @@ export const formatExtendedField = ({
isVerified: is_verified,
isFromClaim: is_from_claim,
key: uuidv4(),
contributor_id: contributor_id ?? null,
contributor_name: contributor_name ?? null,
contributorProfileUrl:
contributor_id != null
? makeProfileRouteLink(contributor_id)
: null,
contributorName: contributor_name ?? null,
secondaryDate: created_at ? formatAttribution(created_at) : null,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We introduced secondaryDate so we could turn only the contributor name into a link and keep the date as plain text, without parsing a single combined string.

};
};

Expand Down
Loading