diff --git a/cypress/e2e/searchWork.test.ts b/cypress/e2e/searchWork.test.ts index a03e51ca..83f48fd8 100644 --- a/cypress/e2e/searchWork.test.ts +++ b/cypress/e2e/searchWork.test.ts @@ -29,7 +29,8 @@ describe('Search Works', () => { expect($facet.eq(4)).to.contain('Language') expect($facet.eq(5)).to.contain('Field of Science') expect($facet.eq(6)).to.contain('Registration Agency') - expect($facet.eq(7)).to.contain('Repository Type') + expect($facet.eq(7)).to.contain('Repository') + expect($facet.eq(8)).to.contain('Repository Type') }) }) diff --git a/package.json b/package.json index 850d13d3..5635cf82 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "linkify-react": "^4.2.0", "linkifyjs": "^4.2.0", "maltipoo": "https://github.com/datacite/maltipoo#2.0.3", - "next": "15.4.8", + "next": "15.4.10", "next-plausible": "^3.12.5", "node-fetch": "^2.6.0", "nuqs": "^1.16.0", diff --git a/src/app/(main)/doi.org/[...doi]/Content.tsx b/src/app/(main)/doi.org/[...doi]/Content.tsx index 64aee9a5..bbe723df 100644 --- a/src/app/(main)/doi.org/[...doi]/Content.tsx +++ b/src/app/(main)/doi.org/[...doi]/Content.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Suspense } from 'react' import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' @@ -15,6 +15,8 @@ import DownloadMetadata from 'src/components/DownloadMetadata/DownloadMetadata' import Work from 'src/components/Work/Work' // import { isProject } from 'src/utils/helpers'; import ExportMetadata from 'src/components/DownloadMetadata/ExportMetadata' +import RelatedContent from './RelatedContent' +import RelatedAggregateGraph from './RelatedAggregateGraph' interface Props { doi: string @@ -27,6 +29,17 @@ export default async function Content(props: Props) { const { data, error } = await fetchDoi(doi) + const relatedIdentifiers = data?.work.relatedIdentifiers || [] + const relatedDois: string[] = Array.from(new Set( + relatedIdentifiers + ?.filter((identifier) => identifier.relatedIdentifierType === 'DOI') + ?.map((identifier) => { + const match = identifier.relatedIdentifier?.match(/(?:https?:\/\/doi\.org\/)?(.+)/) + const doi = match ? match[1] : identifier.relatedIdentifier + return doi && doi.toLowerCase() + }) || [] + )).slice(0, 150) + if (error) return ( @@ -42,6 +55,7 @@ export default async function Content(props: Props) { : 'https://doi.org/' + work.doi return ( + <> @@ -82,6 +96,13 @@ export default async function Content(props: Props) { + + + + + + + ) } diff --git a/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx b/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx index 75c797a9..8082b6f5 100644 --- a/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx +++ b/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx @@ -1,7 +1,7 @@ 'use client' import React from 'react' -import { useParams, useSearchParams } from 'next/navigation' +import { useSearchParams } from 'next/navigation' import Container from 'react-bootstrap/Container' import Col from 'react-bootstrap/Col' import Row from 'react-bootstrap/Row' @@ -9,7 +9,7 @@ import Row from 'react-bootstrap/Row' import Loading from 'src/components/Loading/Loading' import { useDoiRelatedContentQuery } from 'src/data/queries/doiRelatedContentQuery' -import { Works } from 'src/data/types' +import { Work, Works } from 'src/data/types' import Error from 'src/components/Error/Error' import WorksListing from 'src/components/WorksListing/WorksListing' @@ -17,18 +17,18 @@ import mapSearchparams from './mapSearchParams' import { isDMP, isProject } from 'src/utils/helpers' interface Props { + work: Work, + relatedDois: string[], isBot?: boolean } export default function RelatedContent(props: Props) { - const { isBot = false } = props - const doiParams = useParams().doi as string[] - const doi = decodeURIComponent(doiParams.join('/')) + const { isBot = false, work, relatedDois } = props const searchParams = useSearchParams() - const { variables, connectionType } = mapSearchparams(Object.fromEntries(searchParams.entries()) as any) + const { variables } = mapSearchparams(Object.fromEntries(searchParams.entries()) as any) - const vars = { id: doi, ...variables } + const vars = { relatedToDoi: work.doi, relatedDois, ...variables } const { loading, data, error } = useDoiRelatedContentQuery(vars) if (isBot) return null @@ -42,55 +42,31 @@ export default function RelatedContent(props: Props) { - if (!data) return + if ((!data || data.works.totalCount === 0) && !searchParams) return - const showSankey = isDMP(data.work) || isProject(data.work) - const relatedWorks = data.work + const relatedWorks = (data?.works ?? { + nodes: [], + totalCount: 0, + pageInfo: { endCursor: '', hasNextPage: false } + }) as Works + + const showSankey = isDMP(work) || isProject(work) - const allRelatedCount = relatedWorks.allRelated?.totalCount || 0 - const referenceCount = relatedWorks.references?.totalCount || 0 - const citationCount = relatedWorks.citations?.totalCount || 0 - const partCount = relatedWorks.parts?.totalCount || 0 - const partOfCount = relatedWorks.partOf?.totalCount || 0 - const otherRelatedCount = relatedWorks.otherRelated?.totalCount || 0 - - if (referenceCount + citationCount + partCount + partOfCount + otherRelatedCount === 0) return '' - - const url = '/doi.org/' + relatedWorks.doi + '/?' - - const connectionTypeCounts = { - allRelated: allRelatedCount, - references: referenceCount, - citations: citationCount, - parts: partCount, - partOf: partOfCount, - otherRelated: otherRelatedCount - } - - const defaultConnectionType = - allRelatedCount > 0 ? 'allRelated' : - referenceCount > 0 ? 'references' : - citationCount > 0 ? 'citations' : - partCount > 0 ? 'parts' : - partOfCount > 0 ? 'partOf' : 'otherRelated' - - const displayedConnectionType = connectionType ? connectionType : defaultConnectionType + const defaultConnectionType = 'allRelated' + const connectionType = searchParams.get('connection-type') || defaultConnectionType //convert camel case to title and make first letter uppercase //convert connectionType to title, allRelated becomes All Related Wokrs, references becomes References, citations becomes Citations, parts becomes Parts, partOf becomes Part Of, and otherRelated becomes Other Works const displayedConnectionTitle = - displayedConnectionType === 'allRelated' ? 'All Related Works' : - displayedConnectionType === 'otherRelated' ? 'Other Works' : - displayedConnectionType.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()) - - - + connectionType === 'allRelated' ? 'All Related Works' : + connectionType === 'otherRelated' ? 'Other Works' : + connectionType.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()) - const works: Works = displayedConnectionType in relatedWorks ? - relatedWorks[displayedConnectionType] : - { totalCount: 0, nodes: [] } + const url = '/doi.org/' + work.doi + '/?' - const hasNextPage = works.pageInfo ? works.pageInfo.hasNextPage : false - const endCursor = works.pageInfo ? works.pageInfo.endCursor : '' + const hasNextPage = relatedWorks.pageInfo.hasNextPage + const endCursor = relatedWorks.pageInfo + ? relatedWorks.pageInfo.endCursor + : '' return ( @@ -101,14 +77,14 @@ export default function RelatedContent(props: Props) { 25} + hasPagination={relatedWorks.totalCount > 25} hasNextPage={hasNextPage} model={'doi'} url={url} diff --git a/src/app/(main)/doi.org/[...doi]/mapSearchParams.ts b/src/app/(main)/doi.org/[...doi]/mapSearchParams.ts index bc94e7da..db32df5a 100644 --- a/src/app/(main)/doi.org/[...doi]/mapSearchParams.ts +++ b/src/app/(main)/doi.org/[...doi]/mapSearchParams.ts @@ -26,9 +26,10 @@ export default function mapSearchparams(searchParams: SearchParams) { license: searchParams.license, fieldOfScience: searchParams['field-of-science'], registrationAgency: searchParams['registration-agency'], + clientId: searchParams['client-id'], + connectionType: searchParams['connection-type'], }, - connectionType: searchParams['connection-type'], isBot: false } } diff --git a/src/app/(main)/doi.org/[...doi]/page.tsx b/src/app/(main)/doi.org/[...doi]/page.tsx index fda13004..ce78b377 100644 --- a/src/app/(main)/doi.org/[...doi]/page.tsx +++ b/src/app/(main)/doi.org/[...doi]/page.tsx @@ -9,8 +9,6 @@ import { rorFromUrl } from 'src/utils/helpers' import { fetchCrossrefFunder } from 'src/data/queries/crossrefFunderQuery' import Content from './Content' import { fetchDoi } from 'src/data/queries/doiQuery' -import RelatedContent from './RelatedContent' -import RelatedAggregateGraph from './RelatedAggregateGraph' import Loading from 'src/components/Loading/Loading' import { COMMONS_URL, LOGO_URL } from 'src/data/constants' @@ -94,10 +92,6 @@ export default async function Page(props: Props) { }> - - - - } diff --git a/src/app/(main)/doi.org/page.tsx b/src/app/(main)/doi.org/page.tsx index ba59d2d7..2a14beb3 100644 --- a/src/app/(main)/doi.org/page.tsx +++ b/src/app/(main)/doi.org/page.tsx @@ -21,7 +21,8 @@ export default async function SearchDoiPage(props: Props) { resourceTypeId: vars['resource-type'], fieldOfScience: vars['field-of-science'], registrationAgency: vars['registration-agency'], - clientType: vars['repository-type'] + clientType: vars['repository-type'], + clientId: vars['client-id'], } // Show example text if there is no query diff --git a/src/app/(main)/orcid.org/[orcid]/mapSearchParams.ts b/src/app/(main)/orcid.org/[orcid]/mapSearchParams.ts index 31704398..db028949 100644 --- a/src/app/(main)/orcid.org/[orcid]/mapSearchParams.ts +++ b/src/app/(main)/orcid.org/[orcid]/mapSearchParams.ts @@ -24,6 +24,7 @@ export default function mapSearchparams(searchParams: SearchParams) { license: searchParams.license, fieldOfScience: searchParams['field-of-science'], registrationAgency: searchParams['registration-agency'], + clientId: searchParams['client-id'], }, isBot: false diff --git a/src/app/(main)/ror.org/[rorid]/Content.tsx b/src/app/(main)/ror.org/[rorid]/Content.tsx index f22d5eaf..9ebcae0b 100644 --- a/src/app/(main)/ror.org/[rorid]/Content.tsx +++ b/src/app/(main)/ror.org/[rorid]/Content.tsx @@ -13,7 +13,6 @@ import DownloadReports from 'src/components/DownloadReports/DownloadReports' import OrganizationMetadata from 'src/components/OrganizationMetadata/OrganizationMetadata' import SummarySearchMetrics from 'src/components/SummarySearchMetrics/SummarySearchMetrics' import Loading from 'src/components/Loading/Loading' -import RelatedContent from 'src/app/(main)/ror.org/[rorid]/RelatedContent'; interface Props { rorid: string @@ -25,7 +24,6 @@ export default function Content(props: Props) { const { data, error, loading } = useROROrganization(rorId) if (loading) return const organization = data?.organization || {} as OrganizationType - const rorFundingIds = organization.identifiers?.filter((id) => id.identifierType === 'fundref').map((id) => id.identifier) || [] if (error || !organization) return ( @@ -35,43 +33,42 @@ export default function Content(props: Props) { return ( <> - - - - - </Col> - </Row> + <Container fluid> + <Row className="mb-4"> + <Col md={{ offset: 3 }}> + <Title title={organization.name} titleLink={organization.id} link={organization.id} /> + </Col> + </Row> - <Row> - <Col md={3}> - <DownloadReports - links={[ - { - title: 'Related Works (CSV)', - helpText: 'Includes descriptions and formatted citations in APA style for up to 200 DOIs associated with this organization.', - type: 'ror/related-works', - }, - { - title: 'Funders (CSV)', - helpText: 'Includes up to 200 funders associated with related works.', - type: 'ror/funders', - } - ]} - variables={{ rorId }} - /> - </Col> - <Col md={9} className="px-0"> - <SummarySearchMetrics rorId={organization.id} rorFundingIds={rorFundingIds} /> - {organization.inceptionYear && ( - <p className="mb-3">Founded {organization.inceptionYear}</p> - )} - <OrganizationMetadata metadata={organization} - linkToExternal={false} - showTitle={false} /> - </Col> - </Row> - </Container> - <RelatedContent isBot={props.isBot} rorId={rorId} rorFundingIds={rorFundingIds} /> + <Row> + <Col md={3}> + <DownloadReports + links={[ + { + title: 'Related Works (CSV)', + helpText: 'Includes descriptions and formatted citations in APA style for up to 200 DOIs associated with this organization.', + type: 'ror/related-works', + }, + { + title: 'Funders (CSV)', + helpText: 'Includes up to 200 funders associated with related works.', + type: 'ror/funders', + } + ]} + variables={{ rorId }} + /> + </Col> + <Col md={9} className="px-0"> + <SummarySearchMetrics rorId={organization.id}/> + {organization.inceptionYear && ( + <p className="mb-3">Founded {organization.inceptionYear}</p> + )} + <OrganizationMetadata metadata={organization} + linkToExternal={false} + showTitle={false} /> + </Col> + </Row> + </Container> </> ) } diff --git a/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx b/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx index 5dfabcf1..b15da90d 100644 --- a/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx +++ b/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx @@ -17,17 +17,18 @@ import mapSearchparams from './mapSearchParams'; interface Props { isBot?: boolean rorId: string - rorFundingIds: string[] } export default function RelatedContent(props: Props) { - const { isBot = false, rorId, rorFundingIds } = props + const { isBot = false, rorId } = props const searchParams = useSearchParams() const { variables } = mapSearchparams(Object.fromEntries(searchParams.entries()) as any) - const vars = { rorId, rorFundingIds, ...variables } + const vars = { rorId, ...variables } const { loading, data, error } = useOrganizationRelatedContentQuery(vars) + + const showMetrics = searchParams.size > 0 if (isBot) return null @@ -60,7 +61,9 @@ export default function RelatedContent(props: Props) { </Row> <WorksListing works={relatedWorks} + vars={vars} loading={loading} + showMetrics={showMetrics} showAnalytics={true} showClaimStatus={true} hasPagination={relatedWorks.totalCount > 25} diff --git a/src/app/(main)/ror.org/[rorid]/mapSearchParams.ts b/src/app/(main)/ror.org/[rorid]/mapSearchParams.ts index b0d5f7d9..5ebc90c6 100644 --- a/src/app/(main)/ror.org/[rorid]/mapSearchParams.ts +++ b/src/app/(main)/ror.org/[rorid]/mapSearchParams.ts @@ -25,7 +25,9 @@ export default function mapSearchparams(searchParams: SearchParams) { fieldOfScience: searchParams['field-of-science'], license: searchParams.license, registrationAgency: searchParams['registration-agency'], - clientType: searchParams['repository-type'] + clientId: searchParams['client-id'], + clientType: searchParams['repository-type'], + organizationRelationType: searchParams['organization-relation-type'] || 'allRelated', }, isBot: false diff --git a/src/app/(main)/ror.org/[rorid]/page.tsx b/src/app/(main)/ror.org/[rorid]/page.tsx index 5b9bf01a..fabc6ae5 100644 --- a/src/app/(main)/ror.org/[rorid]/page.tsx +++ b/src/app/(main)/ror.org/[rorid]/page.tsx @@ -4,6 +4,7 @@ import { notFound } from 'next/navigation' import Content from './Content' import { RORV2Client } from 'src/data/clients/ror-v2-client' import { COMMONS_URL, LOGO_URL } from 'src/data/constants' +import RelatedContent from './RelatedContent' interface Props { params: Promise<{ @@ -56,6 +57,7 @@ export default async function Page(props: Props) { return ( <> <Content rorid={rorid} /> + <RelatedContent rorId={rorid} /> </> ) } diff --git a/src/components/FacetList/FacetList.tsx b/src/components/FacetList/FacetList.tsx index 93c504aa..847201d9 100644 --- a/src/components/FacetList/FacetList.tsx +++ b/src/components/FacetList/FacetList.tsx @@ -1,15 +1,11 @@ 'use client' import React from 'react' -import OverlayTrigger from 'react-bootstrap/OverlayTrigger' -import Tooltip from 'react-bootstrap/Tooltip' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import FacetListItem from './FacetListItem' import { Facet } from 'src/data/types' -import { faQuestionCircle } from '@fortawesome/free-regular-svg-icons' import Accordion from 'react-bootstrap/Accordion'; import styles from './FacetList.module.scss' - +import { InfoTooltip } from '../InfoTooltip/InfoTooltip'; interface FacetListProps { data: Facet[] | undefined @@ -26,21 +22,6 @@ interface FacetListProps { tooltipText?: string } -// Custom InfoTooltip component -const InfoTooltip = ({ text }) => ( - <OverlayTrigger - placement="top" - overlay={<Tooltip>{text}</Tooltip>} - > - <span - onClick={(e) => e.stopPropagation()} - className="ms-2" - style={{ cursor: 'help' }} - > <FontAwesomeIcon icon={faQuestionCircle} /> - </span> - </OverlayTrigger> -); - export default function FacetList(props: FacetListProps) { const { data, title, id, param, url, value, checked, radio } = props if (!data || data.length === 0) return null @@ -62,6 +43,7 @@ export default function FacetList(props: FacetListProps) { value={value && value(facet.id)} checked={checked && checked(i)} radio={radio} + tooltipText={facet.tooltipText} /> ))} </Accordion.Body> diff --git a/src/components/FacetList/FacetListItem.tsx b/src/components/FacetList/FacetListItem.tsx index 258f40a9..26082fbf 100644 --- a/src/components/FacetList/FacetListItem.tsx +++ b/src/components/FacetList/FacetListItem.tsx @@ -1,8 +1,7 @@ 'use client' import React from 'react' -import Link from 'next/link' -import { useSearchParams } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSquare, faCheckSquare, @@ -11,28 +10,45 @@ import { import { Facet } from 'src/data/types' import styles from './FacetListItem.module.scss' import ListGroup from 'react-bootstrap/ListGroup'; +import { InfoTooltip } from '../InfoTooltip/InfoTooltip'; +import ContentLoader from "react-content-loader" interface Props { facet: Facet param: string url: string - + tooltipText?: string checked?: boolean radio?: boolean value?: string } +const LoadingCount = () => ( + <ContentLoader + speed={2} + width={40} + height={16} + viewBox="0 0 40 16" + backgroundColor="#f3f3f3" + foregroundColor="#ecebeb" + > + <rect x="0" y="0" rx="0" ry="0" width="40" height="16" /> + </ContentLoader> +) + export default function FacetListItem(props: Props) { const { facet, param, url, + tooltipText, checked = false, radio = false, value: customValue } = props + const router = useRouter() const checkIcon = radio ? faDotCircle : faCheckSquare const uncheckIcon = radio ? faCircle : faSquare @@ -53,13 +69,37 @@ export default function FacetListItem(props: Props) { } params.delete('cursor') + const handleClick = () => { + router.push(url + params.toString(), { scroll: false }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleClick() + } + } + + if (!facet.loading && facet.count === 0) { + return null + } + return ( <ListGroup.Item as="li" key={facet.id}> - <Link prefetch={false} href={url + params.toString()} className={styles.facetlink}> + <div + onClick={handleClick} + onKeyDown={handleKeyDown} + className={styles.facetlink} + role="button" + tabIndex={0} + > <FontAwesomeIcon icon={icon} /> - <span className={styles.facetTitle}>{facet.title}</span> - <span>{facet.count.toLocaleString('en-US')}</span> - </Link> + <span className={styles.facetTitle}> + {facet.title} + {tooltipText && <InfoTooltip text={tooltipText} />} + </span> + {facet.loading ? <LoadingCount /> : <span>{facet.count.toLocaleString('en-US')}</span>} + </div> </ListGroup.Item> ) } diff --git a/src/components/InfoTooltip/InfoTooltip.tsx b/src/components/InfoTooltip/InfoTooltip.tsx new file mode 100644 index 00000000..737254cf --- /dev/null +++ b/src/components/InfoTooltip/InfoTooltip.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import OverlayTrigger from 'react-bootstrap/OverlayTrigger' +import Tooltip from 'react-bootstrap/Tooltip' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faQuestionCircle } from '@fortawesome/free-regular-svg-icons' + +interface InfoTooltipProps { + text: string +} + +export const InfoTooltip = ({ text }: InfoTooltipProps) => ( + <OverlayTrigger + placement="top" + overlay={<Tooltip id="infoTooltip">{text}</Tooltip>} + > + <span + onClick={(e) => e.stopPropagation()} + className="ms-2" + style={{ cursor: 'help' }} + role="button" + tabIndex={0} + aria-label={text} + > + <FontAwesomeIcon icon={faQuestionCircle} aria-hidden="true" /> + </span> + </OverlayTrigger> +) \ No newline at end of file diff --git a/src/components/MetricsDisplay/MetricsDisplay.module.scss b/src/components/MetricsDisplay/MetricsDisplay.module.scss index a02e80b5..61c9fe4f 100644 --- a/src/components/MetricsDisplay/MetricsDisplay.module.scss +++ b/src/components/MetricsDisplay/MetricsDisplay.module.scss @@ -1,6 +1,5 @@ .metrics { grid-row: 2; - padding-bottom: 30px; max-width: 600px; dl { @@ -12,11 +11,12 @@ dt { grid-row: 2; font-size: 1.2rem; + padding-bottom: 1rem; } dd{ grid-row: 1; - border-top: 2px solid black; font-size: 1.5rem; + padding-top: 1rem; } dt, dd{ line-height: 1; diff --git a/src/components/MetricsDisplay/MetricsDisplay.tsx b/src/components/MetricsDisplay/MetricsDisplay.tsx index 1da31e83..f341e827 100644 --- a/src/components/MetricsDisplay/MetricsDisplay.tsx +++ b/src/components/MetricsDisplay/MetricsDisplay.tsx @@ -19,9 +19,11 @@ type Props = { views?: string downloads?: string } + + displayWorksTotal?: boolean } -export function MetricsDisplay({ counts, links = {} }: Props) { +export function MetricsDisplay({ counts, links = {}, displayWorksTotal = true }: Props) { const metricsData = [ { @@ -47,6 +49,7 @@ export function MetricsDisplay({ counts, links = {} }: Props) { ]; const metricList = metricsData.filter(metric => metric.count && metric.count > 0).map((metric, index) => + metric.label === "Works" && !displayWorksTotal ? null : <React.Fragment key={"metric-" + index}> <dd>{compactNumbers(metric.count || 0)}</dd> <dt>{metric.label} <HelpIcon link={metric.link} size={20} position='inline' /></dt> diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 96b2b2ad..247200cf 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -17,8 +17,11 @@ export default function SearchBox({ path }: Props) { const [searchInput, setSearchInput] = useState(params?.get('filterQuery') || '') const onSubmit = () => { - if (router) - router.push(`${path}?filterQuery=${searchInput}`) + if (router) { + const newParams = new URLSearchParams(params) + newParams.set('filterQuery', searchInput) + router.push(`${path}?${newParams}`) + } } const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { @@ -33,7 +36,9 @@ export default function SearchBox({ path }: Props) { const onSearchClear = () => { setSearchInput('') - router.replace(path); + const newParams = new URLSearchParams(params) + newParams.delete('filterQuery') + router.replace(`${path}?${newParams}`) } const searchBoxStyle = { '--bs-heading-color': '#1abc9c', diff --git a/src/components/SummarySearchMetrics/SummarySearchMetrics.tsx b/src/components/SummarySearchMetrics/SummarySearchMetrics.tsx index 218f71e5..52fcea8c 100644 --- a/src/components/SummarySearchMetrics/SummarySearchMetrics.tsx +++ b/src/components/SummarySearchMetrics/SummarySearchMetrics.tsx @@ -42,5 +42,7 @@ export default function SummarySearchMetrics(variables: QueryVar) { if (isLoading) return <SummaryStatsLoader /> if (!data || error) return null - return <MetricsDisplay counts={data} links={links} /> + const displayWorksTotal = !variables.organizationRelationType + + return <MetricsDisplay counts={data} links={links} displayWorksTotal={displayWorksTotal} /> } diff --git a/src/components/WorkFacets/WorkFacets.tsx b/src/components/WorkFacets/WorkFacets.tsx index e7ae00dd..c685038d 100644 --- a/src/components/WorkFacets/WorkFacets.tsx +++ b/src/components/WorkFacets/WorkFacets.tsx @@ -7,12 +7,13 @@ import AuthorsFacet from '../AuthorsFacet/AuthorsFacet' import { Work, Facet } from 'src/data/types' import FacetList from '../FacetList/FacetList' import FacetListGroup from '../FacetList/FacetListGroup' +import { QueryVar, useSearchDoiQuery } from 'src/data/queries/searchDoiQuery' interface Props { data: Facets model: string url: string - connectionTypesCounts?: { references: number, citations: number, parts: number, partOf: number, otherRelated: number, allRelated: number } + vars?: QueryVar } interface Facets { @@ -26,6 +27,7 @@ interface Facets { registrationAgencies?: Facet[] authors?: Facet[] creatorsAndContributors?: Facet[] + clients?: Facet[] clientTypes?: Facet[] nodes: Work[] } @@ -38,7 +40,7 @@ export default function WorkFacets({ data, model, url, - connectionTypesCounts + vars }: Props) { // get current query parameters from next router @@ -47,17 +49,52 @@ export default function WorkFacets({ // remove %2F? at the end of url const path = url.substring(0, url.length - 2) - const connectionTypeList: Facet[] = connectionTypesCounts ? [ - { id: 'allRelated', title: 'All', count: connectionTypesCounts.allRelated }, - { id: 'references', title: 'References', count: connectionTypesCounts.references }, - { id: 'citations', title: 'Citations', count: connectionTypesCounts.citations }, - { id: 'parts', title: 'Parts', count: connectionTypesCounts.parts }, - { id: 'partOf', title: 'Is Part Of', count: connectionTypesCounts.partOf }, - { id: 'otherRelated', title: 'Other', count: connectionTypesCounts.otherRelated } - ] : [] + const useConnectionQuery = (connectionType: string) => + vars?.relatedToDoi + ? useSearchDoiQuery({ ...vars, connectionType, pageSize: 0 }) + : { loading: false, data: undefined, error: undefined } + + const allRelatedQuery = useConnectionQuery('allRelated') + const referencesQuery = useConnectionQuery('references') + const citationsQuery = useConnectionQuery('citations') + const partsQuery = useConnectionQuery('parts') + const partOfQuery = useConnectionQuery('partOf') + const versionOfQuery = useConnectionQuery('versionOf') + const versionsQuery = useConnectionQuery('versions') + const otherRelatedQuery = useConnectionQuery('otherRelated') + + const connectionTypeList: Facet[] = [ + { id: 'allRelated', title: 'All', count: allRelatedQuery.data?.works?.totalCount ?? 0, loading: allRelatedQuery.loading }, + { id: 'references', title: 'References', count: referencesQuery.data?.works?.totalCount ?? 0, loading: referencesQuery.loading }, + { id: 'citations', title: 'Citations', count: citationsQuery.data?.works?.totalCount ?? 0, loading: citationsQuery.loading }, + { id: 'parts', title: 'Parts', count: partsQuery.data?.works?.totalCount ?? 0, loading: partsQuery.loading }, + { id: 'partOf', title: 'Is Part Of', count: partOfQuery.data?.works?.totalCount ?? 0, loading: partOfQuery.loading }, + { id: 'versionOf', title: 'Version Of', count: versionOfQuery.data?.works?.totalCount ?? 0, loading: versionOfQuery.loading }, + { id: 'versions', title: 'Versions', count: versionsQuery.data?.works?.totalCount ?? 0, loading: versionsQuery.loading }, + { id: 'otherRelated', title: 'Other', count: otherRelatedQuery.data?.works?.totalCount ?? 0, loading: otherRelatedQuery.loading } + ] + + const useOrganizationRelationQuery = (organizationRelationType: string) => + vars?.rorId + ? useSearchDoiQuery({ ...vars, organizationRelationType, pageSize: 0 }) + : { loading: false, data: undefined, error: undefined } + + const organizationAllRelatedQuery = useOrganizationRelationQuery('allRelated') + const organizationCreatedOrContributedByAffiliatedResearcherQuery = useOrganizationRelationQuery('createdOrContributedByAffiliatedResearcher') + const organizationCreatedContributedOrPublishedByQuery = useOrganizationRelationQuery('createdContributedOrPublishedBy') + const organizationFundedByQuery = useOrganizationRelationQuery('fundedBy') + + const organizationRelationTypeList: Facet[] = [ + { id: "allRelated", title: "All", count: organizationAllRelatedQuery.data?.works?.totalCount ?? 0, loading: organizationAllRelatedQuery.loading }, + { id: "createdOrContributedByAffiliatedResearcher", title: "By Affiliated Researchers", tooltipText: "Works created or contributed by researchers affiliated with the organization.", count: organizationCreatedOrContributedByAffiliatedResearcherQuery.data?.works?.totalCount ?? 0, loading: organizationCreatedOrContributedByAffiliatedResearcherQuery.loading }, + { id: "createdContributedOrPublishedBy", title: "Created By", tooltipText: "Works created, contributed, or published by the organization.", count: organizationCreatedContributedOrPublishedByQuery.data?.works?.totalCount ?? 0, loading: organizationCreatedContributedOrPublishedByQuery.loading }, + { id: "fundedBy", title: "Funded By", tooltipText: "Works funded by the organization and its child organizations.", count: organizationFundedByQuery.data?.works?.totalCount ?? 0, loading: organizationFundedByQuery.loading }, + // OMP relationships are included in allRelated, but we don't document or explain this functionality ATM. + // { id: "connectedToOrganizationOMPs", title: "Related to OMPs", tooltipText: "Works related to Output Management Plans associated with the organization.", count: 0 }, + ] const isConnectionTypeSet = searchParams?.has('connection-type') - const totalConnectionTypeCount = connectionTypesCounts ? connectionTypesCounts.references + connectionTypesCounts.citations + connectionTypesCounts.parts + connectionTypesCounts.partOf + connectionTypesCounts.otherRelated : 0 + const isOrganizationRelationTypeSet = searchParams?.has('organization-relation-type') const defaultActiveKeys = [ "authors-facets", @@ -68,8 +105,9 @@ export default function WorkFacets({ "language-facets", "field-of-science-facets", "registration-agency-facets", - "conection-type-facets", - "repository-type-facets" + "repository-type-facets", + "organization-relation-type-facets", + "repository-facets" ] return ( @@ -79,9 +117,9 @@ export default function WorkFacets({ )} <FacetListGroup defaultActiveKey={defaultActiveKeys} > - {totalConnectionTypeCount > 0 && ( + {url.startsWith('/doi.org/') && ( <FacetList - data={connectionTypeList.filter(f => f.count > 0)} + data={connectionTypeList} title="Connection Types" id="connection-type-facets" param="connection-type" @@ -91,6 +129,18 @@ export default function WorkFacets({ /> )} + {url.startsWith('/ror.org') && ( + <FacetList + data={organizationRelationTypeList} + title="Organization Relation Types" + id="organization-relation-type-facets" + param="organization-relation-type" + url={url} + checked={(i) => !isOrganizationRelationTypeSet && i == 0} + radio + /> + )} + {model == "person" ? <AuthorsFacet authors={data.authors || []} title="Co-Authors" url={url} model={model} /> : <AuthorsFacet authors={data.creatorsAndContributors || []} title="Creators & Contributors" url={url} model={model} /> @@ -144,14 +194,28 @@ export default function WorkFacets({ param="registration-agency" url={url} /> - <FacetList - data={data.clientTypes} - title="Repository Type" - id="repository-type-facets" - param="repository-type" - tooltipText='The type of DataCite Repository where a DOI is stored.' - url={url} - /> + + {!url.startsWith('/repositories') && ( + <> + <FacetList + data={data.clients} + title="Repository" + id="repository-facets" + param="client-id" + tooltipText='The DataCite Repository where a DOI is stored.' + url={url} + /> + + <FacetList + data={data.clientTypes} + title="Repository Type" + id="repository-type-facets" + param="repository-type" + tooltipText='The type of DataCite Repository where a DOI is stored.' + url={url} + /> + </> + )} </FacetListGroup> </> ) diff --git a/src/components/WorksDashboard/WorksDashboard.module.scss b/src/components/WorksDashboard/WorksDashboard.module.scss index 13e05bdb..e69de29b 100644 --- a/src/components/WorksDashboard/WorksDashboard.module.scss +++ b/src/components/WorksDashboard/WorksDashboard.module.scss @@ -1,3 +0,0 @@ -.graphsContainer { - margin-top: 3rem; -} diff --git a/src/components/WorksDashboard/WorksDashboard.tsx b/src/components/WorksDashboard/WorksDashboard.tsx index 39596524..61346e39 100644 --- a/src/components/WorksDashboard/WorksDashboard.tsx +++ b/src/components/WorksDashboard/WorksDashboard.tsx @@ -8,7 +8,6 @@ import { Works } from 'src/data/types' import ProductionChart from '../ProductionChart/ProductionChart' import HorizontalStackedBarChart from '../HorizontalStackedBarChart/HorizontalStackedBarChart' import { resourceTypeDomain, resourceTypeRange, licenseRange, otherDomain, otherRange } from '../../data/color_palettes' -import styles from './WorksDashboard.module.scss' import { getTopFive, toBarRecord } from 'src/utils/helpers' import VerticalBarChart from '../VerticalBarChart/VerticalBarChart' @@ -42,7 +41,7 @@ function WorksDashboard({ works, show = {}, children }: Props) { const licenses = getTopFive(licensesData.map(toBarRecord)) return ( - <div className={styles.graphsContainer}> + <div className={'mt-5 mb-5'}> {children && <Row> {children} diff --git a/src/components/WorksListing/WorksListing.tsx b/src/components/WorksListing/WorksListing.tsx index 4d81c89c..849cc256 100644 --- a/src/components/WorksListing/WorksListing.tsx +++ b/src/components/WorksListing/WorksListing.tsx @@ -14,9 +14,13 @@ import NoResults from 'src/components/NoResults/NoResults' import Pager from 'src/components/Pager/Pager' import WorksDashboard, { ShowCharts } from 'src/components/WorksDashboard/WorksDashboard' import SankeyGraph, { multilevelToSankey } from 'src/components/SankeyGraph/SankeyGraph' +import SummarySearchMetrics from 'src/components/SummarySearchMetrics/SummarySearchMetrics' +import { QueryVar } from 'src/data/queries/searchDoiQuery' interface Props { works: Works + vars?: QueryVar + showMetrics?: boolean showAnalytics: boolean showSankey?: boolean sankeyTitle?: string @@ -34,8 +38,9 @@ interface Props { export default function WorksListing({ works, + vars, + showMetrics, showAnalytics, - connectionTypesCounts, showSankey, sankeyTitle = 'Contributions to Related Works', showClaimStatus, @@ -58,7 +63,7 @@ export default function WorksListing({ model={model} url={url} data={works} - connectionTypesCounts={connectionTypesCounts} + vars={vars} /> ) } @@ -73,6 +78,9 @@ export default function WorksListing({ if (hasNoWorks) return renderNoWorks() return ( <> + {showMetrics && <div className="mt-3"> + <SummarySearchMetrics {...(vars || {})}/> + </div>} {showAnalytics && <WorksDashboard works={works} show={show} />} {showSankey && <Row> <Col xs={12}> diff --git a/src/data/constants.ts b/src/data/constants.ts index 05a9a71c..f2c36aa1 100644 --- a/src/data/constants.ts +++ b/src/data/constants.ts @@ -74,6 +74,7 @@ export const FACETS = { 'registrationAgencies', 'authors', 'creatorsAndContributors', + 'clients', 'clientTypes', // personToWorkTypesMultilevel: [] ], diff --git a/src/data/queries/doiRelatedContentQuery.ts b/src/data/queries/doiRelatedContentQuery.ts index d25effaa..63e40c0c 100644 --- a/src/data/queries/doiRelatedContentQuery.ts +++ b/src/data/queries/doiRelatedContentQuery.ts @@ -1,7 +1,10 @@ -import { gql, useQuery } from "@apollo/client"; +import { gql } from "@apollo/client"; import { workConnection, workFragment } from "src/data/queries/queryFragments"; -import { QueryData } from "src/data/queries/doiQuery"; import { QueryVar } from "src/data/queries/searchDoiQuery"; +import { useSearchDoiFacetsQuery } from "src/data/queries/searchDoiFacetsQuery"; +import { useSearchDoiQuery } from "src/data/queries/searchDoiQuery"; +import { FACETS } from "src/data/constants"; +import { Works } from "src/data/types"; export function buildFilterQuery(variables: QueryVar) { const queryParts = [ @@ -14,17 +17,28 @@ export function buildFilterQuery(variables: QueryVar) { } export function useDoiRelatedContentQuery(variables: QueryVar) { - const filterQuery = buildFilterQuery(variables) + const results = useSearchDoiQuery(variables) + const facets = useSearchDoiFacetsQuery(variables, [...FACETS.DEFAULT, ...FACETS.METRICS]) - const { loading, data, error } = useQuery<QueryData, QueryVar>( - RELATED_CONTENT_QUERY, - { - variables: { ...variables, filterQuery }, - errorPolicy: 'all' - } - ) + const loading = results.loading || facets.loading; + const error = results.error || facets.error; + + if (loading || error) return { loading, data: undefined, error } + + + const works = { + ...results.data?.works || {}, + ...facets.data?.works + } - return { loading, data, error } + return { + ...results, + data: { works } as QueryData, + } +} + +export interface QueryData { + works: Works } @@ -155,5 +169,4 @@ export const RELATED_CONTENT_QUERY = gql` ${workFragment} `; - -export type { QueryVar, QueryData } +export type { QueryVar } diff --git a/src/data/queries/searchDoiFacetsQuery.ts b/src/data/queries/searchDoiFacetsQuery.ts index 707c009d..976d428c 100644 --- a/src/data/queries/searchDoiFacetsQuery.ts +++ b/src/data/queries/searchDoiFacetsQuery.ts @@ -35,6 +35,7 @@ function convertToQueryData(json: any): QueryData { fieldsOfScience: meta.fieldsOfScience?.slice(0, 10), affiliations: meta.affiliations, repositories: [], + clients: meta.clients?.slice(0, 10), registrationAgencies: meta.registrationAgencies, funders: meta.funders, authors: meta.authors?.slice(0, 10), diff --git a/src/data/queries/searchDoiQuery.ts b/src/data/queries/searchDoiQuery.ts index 326911d0..78cfef4f 100644 --- a/src/data/queries/searchDoiQuery.ts +++ b/src/data/queries/searchDoiQuery.ts @@ -10,18 +10,71 @@ function extractRORId(rorString: string): string { return rorString.replace('https://', '').replace('ror.org/', '') } -function buildOrgQuery(rorId: string | undefined, rorFundingIds: string[]): string { +function buildOrgQuery(rorId: string | undefined, organizationRelationType: string | undefined): string { if (!rorId) return '' const id = 'ror.org/' + extractRORId(rorId) const urlId = `"https://${id}"` - const rorFundingIdsQuery = rorFundingIds.map(id => '"https://doi.org/' + id + '"').join(' OR ') - return `((organization_id:${id} OR affiliation_id:${id} OR related_dmp_organization_id:${id} OR provider.ror_id:${urlId}) OR funding_references.funderIdentifier:(${urlId} ${rorFundingIdsQuery && `OR ${rorFundingIdsQuery}`}))` + + const fundedByQuery = `funder_rors:${urlId}` + const fundedByChildOrganizations = `funder_parent_rors:${urlId}` + const createdContributedOrPublishedBy = `(organization_id:${id} OR provider.ror_id:${urlId})` + const createdOrContributedByAffiliatedResearcher = `affiliation_id:${id}` + const connectedToOrganizationOMPs = `related_dmp_organization_id:${id}` + + switch (organizationRelationType) { + case 'fundedBy': + return `(${fundedByQuery} OR ${fundedByChildOrganizations})` + case 'createdContributedOrPublishedBy': + return `(${createdContributedOrPublishedBy})` + case 'createdOrContributedByAffiliatedResearcher': + return `(${createdOrContributedByAffiliatedResearcher})` + case 'connectedToOrganizationOMPs': + return `(${connectedToOrganizationOMPs})` + default: + return `(${createdContributedOrPublishedBy} OR ${createdOrContributedByAffiliatedResearcher} OR ${connectedToOrganizationOMPs} OR ${fundedByQuery} OR ${fundedByChildOrganizations})` + } + } +function buildRelatedToDoiQuery(relatedToDoi: string | undefined, relatedDois: string[] | undefined, connectionType: string | undefined): string { + if (!relatedToDoi) return '' + const citationsQuery = '(reference_ids:"' + relatedToDoi + '")' + const referencesQuery = '(citation_ids:"' + relatedToDoi + '")' + + const partOfQuery = '(part_ids:"' + relatedToDoi + '")' + const isPartOfQuery = '(part_of_ids:"' + relatedToDoi + '")' + const versionOfQuery = '(version_ids:"' + relatedToDoi + '")' + const versionsQuery = '(version_of_ids:"' + relatedToDoi + '")' + + const outwardRelatedDois = relatedDois && relatedDois.length > 0 && '(doi:(' + relatedDois?.map(doi => '"' + doi + '"').join(' OR ') + '))' + const inwardRelatedDois = relatedToDoi && '(relatedIdentifiers.relatedIdentifier:("https://doi.org/' + relatedToDoi?.toLowerCase() + '" OR "' + relatedToDoi?.toLowerCase() + '") AND agency:"datacite")' + + switch (connectionType) { + case 'citations': + return citationsQuery + case 'references': + return referencesQuery + case 'parts': + return partOfQuery + case 'partOf': + return isPartOfQuery + case 'versionOf': + return versionOfQuery + case 'versions': + return versionsQuery + case 'otherRelated': + return ('(' + [outwardRelatedDois, inwardRelatedDois].filter(Boolean).join(' OR ') + ') AND NOT (' + [citationsQuery, referencesQuery, partOfQuery, isPartOfQuery, versionOfQuery, versionsQuery].join(' OR ') + ')') + default: + return ('(' + [citationsQuery, referencesQuery, partOfQuery, isPartOfQuery, versionOfQuery, versionsQuery, outwardRelatedDois, inwardRelatedDois].filter(Boolean).join(' OR ') + ')') + } +} + + export function buildQuery(variables: QueryVar): string { const queryParts = [ variables.query, - buildOrgQuery(variables.rorId, variables.rorFundingIds || []), + buildOrgQuery(variables.rorId || undefined, variables.organizationRelationType || undefined), + buildRelatedToDoiQuery(variables.relatedToDoi || undefined, variables.relatedDois || undefined, variables.connectionType || undefined), variables.language ? `language:${variables.language}` : '', variables.registrationAgency ? `agency:${variables.registrationAgency}` : '', variables.userId ? `creators_and_contributors.nameIdentifiers.nameIdentifier:(${variables.userId} OR "https://orcid.org/${variables.userId}")` : '', @@ -73,7 +126,8 @@ function buildDoiSearchParams(variables: QueryVar, count?: number): URLSearchPar include_other_registration_agencies: 'true' }) - if (count) searchParams.append('page[size]', count.toString()) + const pageSize = variables.pageSize ?? count + if (pageSize !== undefined) searchParams.append('page[size]', pageSize.toString()) searchParams.append('page[number]', variables.cursor || '1') // Default to 'relevance' if sort is missing or invalid @@ -208,8 +262,8 @@ export interface QueryData { export interface QueryVar { query?: string filterQuery?: string - rorId?: string - rorFundingIds?: string[] + rorId?: string, + organizationRelationType?: string, userId?: string clientId?: string cursor?: string @@ -221,7 +275,11 @@ export interface QueryVar { license?: string registrationAgency?: string clientType?: string + relatedToDoi?: string + relatedDois?: string[] + connectionType?: string sort?: SortOption + pageSize?: number } diff --git a/src/data/types.ts b/src/data/types.ts index f892ceea..72c026c5 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -41,6 +41,8 @@ export type Work = WorkMetadata & { name: string } + relatedIdentifiers?: RelatedIdentifier[] + registered?: Date formattedCitation?: string claims?: Claim[] @@ -74,6 +76,7 @@ export type Works = { funders: Facet[] authors?: Facet[] creatorsAndContributors?: Facet[] + clients?: Facet[] clientTypes?: Facet[] citations?: Facet[] views?: Facet[] @@ -322,10 +325,22 @@ type Identifier = { identifierUrl: string } +type RelatedIdentifier = { + relatedIdentifier: string + relatedIdentifierType: string + relationType: string + resourceTypeGeneral?: string + relatedMetadataScheme?: string + schemeUri?: string + schemeType?: string +} + export type Facet = { id: string title: string count: number + tooltipText?: string + loading?: boolean } export type MultilevelFacet = Facet & { diff --git a/yarn.lock b/yarn.lock index 1a90c20a..7a53857a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1466,10 +1466,10 @@ "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.10.0" -"@next/env@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.4.8.tgz#f41741d07651958bccb31fb685da0303a9ef1373" - integrity sha512-LydLa2MDI1NMrOFSkO54mTc8iIHSttj6R6dthITky9ylXV2gCGi0bHQjVCtLGRshdRPjyh2kXbxJukDtBWQZtQ== +"@next/env@15.4.10": + version "15.4.10" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.4.10.tgz#a794b738d043d9e98ea435bd45254899f7f77714" + integrity sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg== "@next/eslint-plugin-next@15.4.6": version "15.4.6" @@ -7393,12 +7393,12 @@ next-plausible@^3.12.5: resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-3.12.5.tgz#e8776b59c014b52d4238d4cb72ca88c8c3115cf4" integrity sha512-l1YMuTI9akb2u7z4hyTuxXpudy8KfSteRNXCYpWpnhAoBjaWQlv6sITai1TwcR7wWvVW8DFbLubvMQAsirAjcA== -next@15.4.8: - version "15.4.8" - resolved "https://registry.yarnpkg.com/next/-/next-15.4.8.tgz#0f20a6cad613dc34547fa6519b2d09005ac370ca" - integrity sha512-jwOXTz/bo0Pvlf20FSb6VXVeWRssA2vbvq9SdrOPEg9x8E1B27C2rQtvriAn600o9hH61kjrVRexEffv3JybuA== +next@15.4.10: + version "15.4.10" + resolved "https://registry.yarnpkg.com/next/-/next-15.4.10.tgz#4ee237d4eb16289f6e16167fbed59d8ada86aa59" + integrity sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ== dependencies: - "@next/env" "15.4.8" + "@next/env" "15.4.10" "@swc/helpers" "0.5.15" caniuse-lite "^1.0.30001579" postcss "8.4.31"