From dc2ac475f648ebcb49b27ce6e9f130a9f23ef597 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Fri, 11 Sep 2026 09:30:33 +0000 Subject: [PATCH 1/2] feat: introduce CommonsLayout to replace repeated Row/Col sidebar layouts --- src/app/(main)/doi.org/[...doi]/Content.tsx | 89 ++++++------- .../doi.org/[...doi]/RelatedContent.tsx | 120 ++++++++++-------- src/app/(main)/orcid.org/[orcid]/Content.tsx | 27 ++-- .../orcid.org/[orcid]/RelatedContent.tsx | 82 +++++++----- .../repositories/[...repoid]/Content.tsx | 58 +++++---- .../[...repoid]/RelatedContent.tsx | 75 ++++++----- src/app/(main)/ror.org/[rorid]/Content.tsx | 94 +++++++------- .../(main)/ror.org/[rorid]/RelatedContent.tsx | 112 ++++++++-------- .../CommonsLayout/CommonsLayout.test.tsx | 49 +++++++ .../CommonsLayout/CommonsLayout.tsx | 48 +++++++ .../SearchOrganization/SearchOrganization.tsx | 45 ++++--- src/components/SearchPerson/SearchPerson.tsx | 22 ++-- .../SearchRepository/SearchRepository.tsx | 62 ++++----- src/components/SearchWork/SearchWork.tsx | 29 ++--- src/components/WorksListing/WorksListing.tsx | 24 ++-- 15 files changed, 538 insertions(+), 398 deletions(-) create mode 100644 src/components/CommonsLayout/CommonsLayout.test.tsx create mode 100644 src/components/CommonsLayout/CommonsLayout.tsx diff --git a/src/app/(main)/doi.org/[...doi]/Content.tsx b/src/app/(main)/doi.org/[...doi]/Content.tsx index fdb4cc2b1..83f75a982 100644 --- a/src/app/(main)/doi.org/[...doi]/Content.tsx +++ b/src/app/(main)/doi.org/[...doi]/Content.tsx @@ -1,5 +1,4 @@ import React from 'react' -import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' @@ -11,9 +10,9 @@ import TitleComponent from 'src/components/Title/Title' import Claim from 'src/components/Claim/Claim' import CiteAs from 'src/components/CiteAs/CiteAs' import DownloadMetadata from 'src/components/DownloadMetadata/DownloadMetadata' -// import DownloadReports from 'src/components/DownloadReports/DownloadReports' import Work from 'src/components/Work/Work' import ExportMetadata from 'src/components/DownloadMetadata/ExportMetadata' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { doi: string @@ -24,13 +23,14 @@ export default async function Content(props: Props) { const { data, error } = await fetchDoi(doi) - if (error) return ( - - - - ) + if (error) + return ( + + + + ) - const work = data?.work || {} as WorkType + const work = data?.work || ({} as WorkType) const title = work.titles[0]?.title || '' const handleUrl = @@ -39,46 +39,41 @@ export default async function Content(props: Props) { : 'https://doi.org/' + work.doi return ( - - - - - - + <> + + + - - - - - } /> - - {work.registrationAgency.id == "datacite" && ( - - + + + + } + /> - )} - - - - - {/* */} - {/* {!isBot && } */} - {/* */} - - - - - - - + {work.registrationAgency.id == 'datacite' && ( + + + + )} + + + + + + } + > + + + ) } diff --git a/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx b/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx index 543cdd76b..cbe2b3a44 100644 --- a/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx +++ b/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx @@ -2,9 +2,8 @@ import React from 'react' import { useParams, useSearchParams } from 'next/navigation' -import Container from 'react-bootstrap/Container' -import Col from 'react-bootstrap/Col' import Row from 'react-bootstrap/Row' +import Col from 'react-bootstrap/Col' import CommonsError from 'src/components/Error/Error' import Loading from 'src/components/Loading/Loading' @@ -12,78 +11,91 @@ import WorksListing, { SortBy } from 'src/components/WorksListing/WorksListing' import SearchBox from 'src/components/SearchBox/SearchBox' import mapSearchparams from './mapSearchParams' import { useRelatedContentManager } from 'src/data/managers/RelatedContentManager' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' function getQueryVariables(doi: string, searchParams: URLSearchParams) { - const { variables, connectionType } = mapSearchparams(Object.fromEntries(searchParams.entries()) as any) + const { variables, connectionType } = mapSearchparams( + Object.fromEntries(searchParams.entries()) as any + ) return { relatedDoi: doi, ...variables, connectionType } } export default function RelatedContent() { - const doiParams = useParams().doi as string[] const doi = decodeURIComponent(doiParams.join('/')) const searchParams = useSearchParams() - + const vars = getQueryVariables(doi, searchParams) const manager = useRelatedContentManager(doi, vars) - if (manager.isLoading) return + if (manager.isLoading) + return ( + + + + ) if (manager.hasError) - return - - - - + return ( + + + + ) - if (!manager.hasData || !manager.hasAnyRelatedWorks) return ( - - - -

Related Works

- -
- - + if (!manager.hasData || !manager.hasAnyRelatedWorks) + return ( + <> + +

+ Related Works +

+
+

No related works found for this DOI.

- -
-
- ) + + + ) const { works, title: displayedConnectionTitle } = manager.selectedContent const { hasPagination, hasNextPage, endCursor } = manager.pagination const url = '/doi.org/' + doi + '/' return ( - - - - - - -

Related Works

- -
- -
- - } - /> - -
+ <> + + + +

+ Related Works +

+ + + + +
+
+ + } + /> + ) } diff --git a/src/app/(main)/orcid.org/[orcid]/Content.tsx b/src/app/(main)/orcid.org/[orcid]/Content.tsx index a011ae9bd..64cec2c5a 100644 --- a/src/app/(main)/orcid.org/[orcid]/Content.tsx +++ b/src/app/(main)/orcid.org/[orcid]/Content.tsx @@ -1,13 +1,11 @@ import React from 'react' -import Container from 'react-bootstrap/Container' -import Row from 'react-bootstrap/Row' -import Col from 'react-bootstrap/Col' import { Person as PersonType } from 'src/data/types' import { fetchPerson } from 'src/data/queries/personQuery' import Error from 'src/components/Error/Error' import Person from 'src/components/Person/Person' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { orcid: string @@ -19,21 +17,18 @@ export default async function Content(props: Props) { const { data, error } = await fetchPerson(orcid) - if (error) return ( - - - - ) + if (error) + return ( + + + + ) - const person = data?.person || {} as PersonType + const person = data?.person || ({} as PersonType) return ( - - - - - - - + + + ) } diff --git a/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx b/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx index dbe961205..4f3bde179 100644 --- a/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx +++ b/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx @@ -1,7 +1,6 @@ 'use client' import React from 'react' -import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' import Loading from 'src/components/Loading/Loading' @@ -11,9 +10,10 @@ import { usePersonRelatedContentQuery } from 'src/data/queries/personRelatedCont import Error from 'src/components/Error/Error' import WorksListing, { SortBy } from 'src/components/WorksListing/WorksListing' import SearchBox from 'src/components/SearchBox/SearchBox' -import { pluralize } from 'src/utils/helpers'; +import { pluralize } from 'src/utils/helpers' import { useParams, useSearchParams } from 'next/navigation' import mapSearchparams from './mapSearchParams' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { isBot?: boolean @@ -24,7 +24,9 @@ export default function RelatedContent(props: Props) { const orcid = useParams().orcid as string const searchParams = useSearchParams() - const { variables } = mapSearchparams(Object.fromEntries(searchParams.entries()) as any) + const { variables } = mapSearchparams( + Object.fromEntries(searchParams.entries()) as any + ) const vars = { userId: orcid, ...variables } @@ -32,14 +34,22 @@ export default function RelatedContent(props: Props) { if (isBot) return null - if (loading) return + if (loading) + return ( + + + + ) if (error) - return - - - - + return ( + + + + ) if (!data) return @@ -48,33 +58,37 @@ export default function RelatedContent(props: Props) { const hasNextPage = relatedWorks.pageInfo ? relatedWorks.pageInfo.hasNextPage : false - const endCursor = relatedWorks.pageInfo - ? relatedWorks.pageInfo.endCursor - : '' + const endCursor = relatedWorks.pageInfo ? relatedWorks.pageInfo.endCursor : '' const url = '/orcid.org/' + orcid + '/' - return ( - - - - + return ( + <> + -

{pluralize(relatedWorks.totalCount, 'Work')}

- + +

+ {pluralize(relatedWorks.totalCount, 'Work')} +

+ + + +
- -
- 25} - hasNextPage={hasNextPage} - model={'person'} - url={url + '?'} - endCursor={endCursor} - searchBox={} - /> -
) + + 25} + hasNextPage={hasNextPage} + model={'person'} + url={url + '?'} + endCursor={endCursor} + searchBox={ + + } + /> + + ) } diff --git a/src/app/(main)/repositories/[...repoid]/Content.tsx b/src/app/(main)/repositories/[...repoid]/Content.tsx index 5a4b375dc..2c7d8ae24 100644 --- a/src/app/(main)/repositories/[...repoid]/Content.tsx +++ b/src/app/(main)/repositories/[...repoid]/Content.tsx @@ -1,14 +1,13 @@ import React from 'react' -import Row from 'react-bootstrap/Row' -import Col from 'react-bootstrap/Col' import { Repository as RepositoryType } from 'src/data/types' -import { fetchRepository, QueryVar } from 'src/data/queries/repositoryQuery'; +import { fetchRepository, QueryVar } from 'src/data/queries/repositoryQuery' import Error from 'src/components/Error/Error' import Title from 'src/components/Title/Title' import { RepositorySidebar } from 'src/components/RepositoryDetail/RepositorySidebar' import { RepositoryDetail } from 'src/components/RepositoryDetail/RepositoryDetail' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { variables: QueryVar @@ -20,29 +19,36 @@ export default async function Content(props: Props) { const { data, error } = await fetchRepository(variables.id) - if (error) return ( - - - - ) - - const repository = data?.repository || {} as RepositoryType - - - return <> - - - - - -

Repository Sidebar

- - - + if (error) + return ( + + + + ) + + const repository = data?.repository || ({} as RepositoryType) + + return ( + <> + + + +

Repository Sidebar

+ + + } + >

Repository Details

- + <Title + title={repository.name} + titleLink={repository.url} + offset={false} + /> <RepositoryDetail repo={repository} /> - </Col> - </Row> - </> + </CommonsLayout> + </> + ) } diff --git a/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx b/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx index 8728253dd..52366d77c 100644 --- a/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx +++ b/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx @@ -1,20 +1,20 @@ 'use client' import React from 'react' -import Container from 'react-bootstrap/Container' -import Col from 'react-bootstrap/Col' import Row from 'react-bootstrap/Row' +import Col from 'react-bootstrap/Col' import Loading from 'src/components/Loading/Loading' import { useRepositoryRelatedContentQuery } from 'src/data/queries/repositoryRelatedContentQuery' import Error from 'src/components/Error/Error' import WorksListing, { SortBy } from 'src/components/WorksListing/WorksListing' -import { pluralize } from 'src/utils/helpers'; -import { useSearchParams } from 'next/navigation'; -import mapSearchparams from './mapSearchParams'; +import { pluralize } from 'src/utils/helpers' +import { useSearchParams } from 'next/navigation' +import mapSearchparams from './mapSearchParams' import { Repository } from 'src/data/types' import SearchBox from 'src/components/SearchBox/SearchBox' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { repository: Repository @@ -24,47 +24,56 @@ export default function RelatedContent({ repository }: Props) { const clientId = repository.clientId const searchParams = useSearchParams() - const { variables } = mapSearchparams(Object.fromEntries(searchParams.entries()) as any) + const { variables } = mapSearchparams( + Object.fromEntries(searchParams.entries()) as any + ) const vars = { clientId, ...variables } - const { loading, data, error, facetsLoading } = useRepositoryRelatedContentQuery(vars) + const { loading, data, error, facetsLoading } = + useRepositoryRelatedContentQuery(vars) - if (loading) return <Row><Loading /></Row> + if (loading) + return ( + <Row> + <Loading /> + </Row> + ) if (error) - return <Row> - <Col md={{ offset: 3 }} className="panel panel-transparent"> - <Error title="An error occured loading related content." message={error.message} /> - </Col> - </Row> + return ( + <CommonsLayout mainClassName="panel panel-transparent"> + <Error + title="An error occured loading related content." + message={error.message} + /> + </CommonsLayout> + ) if (!data) return const relatedWorks = data.works const hasNextPage = relatedWorks.totalCount > 25 - const endCursor = relatedWorks.pageInfo - ? relatedWorks.pageInfo.endCursor - : '' + const endCursor = relatedWorks.pageInfo ? relatedWorks.pageInfo.endCursor : '' const totalCount = relatedWorks.totalCount const url = '/repositories/' + clientId + '/' return ( - <Container fluid> - <Row className="mt-5"> - <Col md={3} className="d-none d-md-block"> - </Col> - <Col md={9}> - <Row className="border-bottom ms-1 mb-3"> - <Col className="ps-0"> - <h2 className="visually-hidden">Related Works Results Summary</h2> - <h3 className="member-results border-0 mb-0">{pluralize(totalCount, 'Work')}</h3> - </Col> - <Col xs="auto"><SortBy /></Col> - </Row> - </Col> - </Row> + <> + <CommonsLayout className="mt-5"> + <Row className="border-bottom ms-1 mb-3"> + <Col className="ps-0"> + <h2 className="visually-hidden">Related Works Results Summary</h2> + <h3 className="member-results border-0 mb-0"> + {pluralize(totalCount, 'Work')} + </h3> + </Col> + <Col xs="auto"> + <SortBy /> + </Col> + </Row> + </CommonsLayout> <WorksListing works={relatedWorks} loading={loading} @@ -77,8 +86,10 @@ export default function RelatedContent({ repository }: Props) { url={url + '?'} endCursor={endCursor} show={{ all: true }} - searchBox={<SearchBox path={url} placeholder="Search within these works..." />} + searchBox={ + <SearchBox path={url} placeholder="Search within these works..." /> + } /> - </Container> + </> ) } diff --git a/src/app/(main)/ror.org/[rorid]/Content.tsx b/src/app/(main)/ror.org/[rorid]/Content.tsx index 03e1c0840..7881a17a3 100644 --- a/src/app/(main)/ror.org/[rorid]/Content.tsx +++ b/src/app/(main)/ror.org/[rorid]/Content.tsx @@ -1,11 +1,8 @@ 'use client' import React from 'react' -import Container from 'react-bootstrap/Container' -import Row from 'react-bootstrap/Row' -import Col from 'react-bootstrap/Col' import type { MinimalOrganization as OrganizationType } from 'src/data/queries/searchOrganizationQuery' -import { useROROrganization } from 'src/data/queries/searchOrganizationQuery'; +import { useROROrganization } from 'src/data/queries/searchOrganizationQuery' import Error from 'src/components/Error/Error' import Title from 'src/components/Title/Title' @@ -13,6 +10,7 @@ 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 CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { rorid: string @@ -22,52 +20,58 @@ export default function Content(props: Props) { const { rorid: rorId } = props const { data, error, loading } = useROROrganization(rorId) if (loading) return <Loading /> - const organization = data?.organization || {} as OrganizationType + const organization = data?.organization || ({} as OrganizationType) - if (error || !organization) return ( - <Col md={{ span: 9, offset: 3 }}> - <Error title="An error occured fetching the Organization." /> - </Col> - ) + if (error || !organization) + return ( + <CommonsLayout> + <Error title="An error occured fetching the Organization." /> + </CommonsLayout> + ) return ( <> - <Container fluid> - <Row className="mb-4"> - <Col md={{ offset: 3 }}> - <Title title={organization.name} titleLink={organization.id} link={organization.id} /> - </Col> - </Row> + <CommonsLayout className="mb-4"> + <Title + title={organization.name} + titleLink={organization.id} + link={organization.id} + /> + </CommonsLayout> - <Row> - <Col md={3} className="pe-4"> - <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> + <CommonsLayout + sidebarClassName="pe-4" + mainClassName="px-0" + sidebar={ + <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 }} + /> + } + > + <SummarySearchMetrics rorId={organization.id} /> + {organization.inceptionYear && ( + <p className="mb-3">Founded {organization.inceptionYear}</p> + )} + <OrganizationMetadata + metadata={organization} + linkToExternal={false} + showTitle={false} + /> + </CommonsLayout> </> ) } diff --git a/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx b/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx index d2e7b2d8e..f3c0157bc 100644 --- a/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx +++ b/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx @@ -1,9 +1,8 @@ 'use client' import React from 'react' -import Container from 'react-bootstrap/Container' -import Col from 'react-bootstrap/Col' import Row from 'react-bootstrap/Row' +import Col from 'react-bootstrap/Col' import Loading from 'src/components/Loading/Loading' import CommonsError from 'src/components/Error/Error' import WorksListing, { SortBy } from 'src/components/WorksListing/WorksListing' @@ -12,40 +11,47 @@ import mapSearchparams from './mapSearchParams' import { useOrganizationRelatedContentManager } from 'src/data/managers/OrganizationRelatedContentManager' import SummarySearchMetrics from 'src/components/SummarySearchMetrics/SummarySearchMetrics' import SearchBox from 'src/components/SearchBox/SearchBox' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' export default function RelatedContent() { const rorId = useParams().rorid as string const searchParams = useSearchParams() - const { variables } = mapSearchparams(Object.fromEntries(searchParams.entries()) as any) + const { variables } = mapSearchparams( + Object.fromEntries(searchParams.entries()) as any + ) const vars = { rorId, ...variables } const manager = useOrganizationRelatedContentManager(vars) - if (manager.isLoading) return <Row><Loading /></Row> - - if (manager.hasError) + if (manager.isLoading) return ( <Row> - <Col md={{ offset: 3 }} className="panel panel-transparent"> - <CommonsError title="An error occurred loading related content." message={manager.errorMessage} /> - </Col> + <Loading /> </Row> ) + if (manager.hasError) + return ( + <CommonsLayout mainClassName="panel panel-transparent"> + <CommonsError + title="An error occurred loading related content." + message={manager.errorMessage} + /> + </CommonsLayout> + ) + if (!manager.hasData || !manager.hasAnyRelatedWorks) return ( - <Container fluid> - <Row> - <Col md={{ offset: 3 }}> - <h3 className="member-results" id="title">Related Works</h3> - </Col> - </Row> - <Row> - <Col md={{ offset: 3 }} className="panel panel-transparent"> - <p>No related works found for this organization.</p> - </Col> - </Row> - </Container> + <> + <CommonsLayout> + <h3 className="member-results" id="title"> + Related Works + </h3> + </CommonsLayout> + <CommonsLayout mainClassName="panel panel-transparent"> + <p>No related works found for this organization.</p> + </CommonsLayout> + </> ) const { works } = manager.selectedContent @@ -53,35 +59,39 @@ export default function RelatedContent() { const url = '/ror.org/' + vars.rorId + '/' return ( - <Container fluid className="mt-5"> - <Row> - <Col md={{ offset: 3 }} className="px-0"> - <Row className="border-bottom ms-1 mb-3"> - <Col className="ps-0"><h3 className="member-results border-0 mb-0">Related Works</h3></Col> - <Col xs="auto"><SortBy /></Col> - </Row> - </Col> - </Row> - <Row> - <WorksListing - works={works} - loading={manager.isLoading} - loadingFacets={manager.facetsAreLoading || manager.organizationCountsLoading} - organizationRelationTypeCounts={manager.organizationRelationTypeCounts} - showAnalytics={!manager.facetsAreLoading} - showClaimStatus={true} - hasPagination={hasPagination} - hasNextPage={hasNextPage} - model={'organization'} - url={url + '?'} - endCursor={endCursor} - searchBox={<SearchBox path={url} placeholder="Search within these works..." />} - > - <div className="mt-1 mb-5"> - <SummarySearchMetrics {...vars} /> - </div> - </WorksListing> - </Row> - </Container> + <> + <CommonsLayout className="mt-5" mainClassName="px-0"> + <Row className="border-bottom ms-1 mb-3"> + <Col className="ps-0"> + <h3 className="member-results border-0 mb-0">Related Works</h3> + </Col> + <Col xs="auto"> + <SortBy /> + </Col> + </Row> + </CommonsLayout> + <WorksListing + works={works} + loading={manager.isLoading} + loadingFacets={ + manager.facetsAreLoading || manager.organizationCountsLoading + } + organizationRelationTypeCounts={manager.organizationRelationTypeCounts} + showAnalytics={!manager.facetsAreLoading} + showClaimStatus={true} + hasPagination={hasPagination} + hasNextPage={hasNextPage} + model={'organization'} + url={url + '?'} + endCursor={endCursor} + searchBox={ + <SearchBox path={url} placeholder="Search within these works..." /> + } + > + <div className="mt-1 mb-5"> + <SummarySearchMetrics {...vars} /> + </div> + </WorksListing> + </> ) } diff --git a/src/components/CommonsLayout/CommonsLayout.test.tsx b/src/components/CommonsLayout/CommonsLayout.test.tsx new file mode 100644 index 000000000..d94e12731 --- /dev/null +++ b/src/components/CommonsLayout/CommonsLayout.test.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { mount } from '@cypress/react' +import CommonsLayout from './CommonsLayout' + +describe('CommonsLayout Component', () => { + it('sidebar mode renders a 3/9 column split in a fluid container', () => { + mount( + <CommonsLayout sidebar={<span id="sidebar">Sidebar</span>}> + <span id="main">Main</span> + </CommonsLayout> + ) + cy.get('.container-fluid > .row > .col-md-3') + .should('contain', 'Sidebar') + .and('have.class', 'd-none') + .and('have.class', 'd-md-block') + .and('have.class', 'pe-4') + cy.get('.col-md-9').should('contain', 'Main').and('not.have.class', 'pe-4') + }) + + it('main-only mode renders a single offset column', () => { + mount( + <CommonsLayout> + <span id="main">Main</span> + </CommonsLayout> + ) + cy.get('.container-fluid > .row > .col-md-9.offset-md-3') + .should('contain', 'Main') + .and('have.attr', 'id', 'main') + cy.get('.col-md-3').should('not.exist') + }) + + it('applies column class overrides and can omit the container', () => { + mount( + <CommonsLayout + fluid={false} + sidebar={<span>Sidebar</span>} + sidebarClassName="px-4" + mainClassName="px-0" + > + <span>Main</span> + </CommonsLayout> + ) + cy.get('.container-fluid').should('not.exist') + cy.get(':root > .row > .col-md-3') + .should('have.class', 'px-4') + .and('not.have.class', 'pe-4') + cy.get('.col-md-9').should('have.class', 'px-0') + }) +}) diff --git a/src/components/CommonsLayout/CommonsLayout.tsx b/src/components/CommonsLayout/CommonsLayout.tsx new file mode 100644 index 000000000..65276e36a --- /dev/null +++ b/src/components/CommonsLayout/CommonsLayout.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import Container from 'react-bootstrap/Container' +import Row from 'react-bootstrap/Row' +import Col from 'react-bootstrap/Col' + +interface Props { + /** Optional sidebar content. When omitted, children are rendered full-width, offset to the main column. */ + sidebar?: React.ReactNode + /** Main content */ + children?: React.ReactNode + /** Classes for the sidebar column. Override e.g. for 'pe-5' or 'px-4'. */ + sidebarClassName?: string + /** Classes for the main column, e.g. 'px-0'. */ + mainClassName?: string + /** Wrap the grid in a Bootstrap fluid container. */ + fluid?: boolean + /** Optional class applied to the Row wrapper. */ + className?: string +} + +export default function CommonsLayout({ + sidebar, + children, + sidebarClassName = 'd-none d-md-block pe-4', + mainClassName, + fluid = true, + className +}: Props) { + const columns = + sidebar === undefined ? ( + <Col md={{ span: 9, offset: 3 }} className={mainClassName}> + {children} + </Col> + ) : ( + <> + <Col md={3} className={sidebarClassName}> + {sidebar} + </Col> + <Col md={9} className={mainClassName}> + {children} + </Col> + </> + ) + + const row = <Row className={className}>{columns}</Row> + + return fluid ? <Container fluid>{row}</Container> : row +} diff --git a/src/components/SearchOrganization/SearchOrganization.tsx b/src/components/SearchOrganization/SearchOrganization.tsx index 43b8edbf6..710206bd7 100644 --- a/src/components/SearchOrganization/SearchOrganization.tsx +++ b/src/components/SearchOrganization/SearchOrganization.tsx @@ -13,6 +13,7 @@ import Loading from 'src/components/Loading/Loading' import { QueryVar, useRORSearch } from 'src/data/queries/searchOrganizationQuery' import FacetList from 'src/components/FacetList/FacetList' import FacetListGroup from 'src/components/FacetList/FacetListGroup' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' type Props = { variables: QueryVar @@ -25,11 +26,9 @@ export default function SearchOrganizations(props: Props) { const organizations = data?.organizations if (error || !organizations || organizations.nodes.length == 0) return ( - <Row> - <Col md={{ span: 9, offset: 3 }}> - <NoResults /> - </Col> - </Row> + <CommonsLayout> + <NoResults /> + </CommonsLayout> ) const renderResults = () => { @@ -88,24 +87,24 @@ export default function SearchOrganizations(props: Props) { } return (<> - <Row> + <CommonsLayout> <h2 className="visually-hidden">Organization Results Summary</h2> - <Col md={{ span: 9, offset: 3 }}> - {organizations.totalCount > 0 && ( - <h3 className="member-results">{pluralize(organizations.totalCount || 0, 'Organization')}</h3> - )} - </Col> - </Row> - <Row> - <Col md={3} className='px-4'> - <h2 className="visually-hidden">Organization Sidebar</h2> - <h3 className="visually-hidden">Organization Facets</h3> - {renderFacets()} - </Col> - <Col md={9}> - <h2 className="visually-hidden">Organization Listings</h2> - {renderResults()} - </Col> - </Row> + {organizations.totalCount > 0 && ( + <h3 className="member-results">{pluralize(organizations.totalCount || 0, 'Organization')}</h3> + )} + </CommonsLayout> + <CommonsLayout + sidebarClassName='px-4' + sidebar={ + <> + <h2 className="visually-hidden">Organization Sidebar</h2> + <h3 className="visually-hidden">Organization Facets</h3> + {renderFacets()} + </> + } + > + <h2 className="visually-hidden">Organization Listings</h2> + {renderResults()} + </CommonsLayout> </>) } diff --git a/src/components/SearchPerson/SearchPerson.tsx b/src/components/SearchPerson/SearchPerson.tsx index 9e5e7e22b..1437f2f26 100644 --- a/src/components/SearchPerson/SearchPerson.tsx +++ b/src/components/SearchPerson/SearchPerson.tsx @@ -3,7 +3,6 @@ import React from 'react' import Loading from 'src/components/Loading/Loading' import Row from 'react-bootstrap/Row' -import Col from 'react-bootstrap/Col' import PersonMetadata from 'src/components/PersonMetadata/PersonMetadata' import Pager from 'src/components/Pager/Pager' import NoResults from 'src/components/NoResults/NoResults' @@ -11,6 +10,7 @@ import NoResults from 'src/components/NoResults/NoResults' import { QueryVar, useSearchPersonQuery } from 'src/data/queries/searchPersonQuery' import { pluralize } from 'src/utils/helpers' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { variables: QueryVar @@ -23,32 +23,30 @@ export default function SearchPerson(props: Props) { const people = data?.people if (error || !people || people.nodes.length == 0) return ( - <Row> - <Col md={{ span: 9, offset: 3 }}> - <NoResults /> - </Col> - </Row> + <CommonsLayout> + <NoResults /> + </CommonsLayout> ) return (<> <h2 className="visually-hidden">People Results</h2> - <Row><Col md={{ span: 9, offset: 3 }}> + <CommonsLayout> {people.nodes.length > 0 && ( <h3 className="member-results"> {pluralize(people.totalCount, 'Person', false, 'People')} </h3> )} - </Col></Row> + </CommonsLayout> - <Row><Col md={{ span: 9, offset: 3 }}> + <CommonsLayout> {people.nodes.map((item) => ( <PersonMetadata metadata={item} url={'orcid.org?'} key={item.id} /> ))} - </Col></Row> + </CommonsLayout> - <Row><Col md={{ span: 9, offset: 3 }}> + <CommonsLayout> {(people.totalCount || 0) > 25 && ( <Pager url={'/orcid.org?'} @@ -56,6 +54,6 @@ export default function SearchPerson(props: Props) { endCursor={data.people.pageInfo.endCursor || ''} /> )} - </Col></Row> + </CommonsLayout> </>) } diff --git a/src/components/SearchRepository/SearchRepository.tsx b/src/components/SearchRepository/SearchRepository.tsx index 48a0aa5e6..be040ed71 100644 --- a/src/components/SearchRepository/SearchRepository.tsx +++ b/src/components/SearchRepository/SearchRepository.tsx @@ -1,7 +1,6 @@ 'use client' import React from 'react' -import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' import Alert from 'react-bootstrap/Alert' @@ -16,6 +15,7 @@ import RepositoryMetadata from 'src/components/RepositoryMetadata/RepositoryMeta import { QueryVar, useSearchRepositoryQuery } from 'src/data/queries/searchRepositoryQuery' import { pluralize } from 'src/utils/helpers' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' type Props = { @@ -32,22 +32,22 @@ export default function SearchRepositories({ variables }: Props) { const repositories = data?.repositories if (error || !repositories || repositories.nodes.length === 0) return ( - <Col md={{ span: 9, offset: 3 }}> + <CommonsLayout> <div className="alert-works"> <Alert variant="warning"> <p>No repositories found. Try a new search or explore generalist repositories such as:</p> - <ul> - <li><Link href="/repositories/dryad.dryad">Dryad</Link></li> - <li><Link href="/repositories/figshare.ars">Figshare</Link></li> - <li><Link href="/repositories/gdcc.harvard-dv">Harvard Dataverse</Link></li> - <li><Link href="/repositories/bl.mendeley">Mendeley Data</Link></li> - <li><Link href="/repositories/cos.osf">Open Science Framework</Link></li> - <li><Link href="/repositories/cern.zenodo">Zenodo</Link></li> - </ul> + <ul> + <li><Link href="/repositories/dryad.dryad">Dryad</Link></li> + <li><Link href="/repositories/figshare.ars">Figshare</Link></li> + <li><Link href="/repositories/gdcc.harvard-dv">Harvard Dataverse</Link></li> + <li><Link href="/repositories/bl.mendeley">Mendeley Data</Link></li> + <li><Link href="/repositories/cos.osf">Open Science Framework</Link></li> + <li><Link href="/repositories/cern.zenodo">Zenodo</Link></li> + </ul> </Alert> </div> - </Col> + </CommonsLayout> ) @@ -103,25 +103,25 @@ export default function SearchRepositories({ variables }: Props) { </>) } - return (<Container fluid> - <Row> + return (<> + <CommonsLayout> <h2 className="visually-hidden">Repositories Results Summary</h2> - <Col md={{ span: 9, offset: 3 }}> - {repositories.totalCount > 0 && ( - <h3 className="member-results">{pluralize(repositories.totalCount || 0, 'Repository', false, 'Repositories')}</h3> - )} - </Col> - </Row> - <Row> - <Col md={3} className='pe-4'> - <h2 className="visually-hidden">Repositories Sidebar</h2> - <h3 className="visually-hidden">Repositories Facets</h3> - {renderFacets()} - </Col> - <Col md={9}> - <h2 className="visually-hidden">Repositories Listings</h2> - {renderResults()} - </Col> - </Row> - </Container>) + {repositories.totalCount > 0 && ( + <h3 className="member-results">{pluralize(repositories.totalCount || 0, 'Repository', false, 'Repositories')}</h3> + )} + </CommonsLayout> + <CommonsLayout + sidebarClassName='pe-4' + sidebar={ + <> + <h2 className="visually-hidden">Repositories Sidebar</h2> + <h3 className="visually-hidden">Repositories Facets</h3> + {renderFacets()} + </> + } + > + <h2 className="visually-hidden">Repositories Listings</h2> + {renderResults()} + </CommonsLayout> + </>) } diff --git a/src/components/SearchWork/SearchWork.tsx b/src/components/SearchWork/SearchWork.tsx index 78b290cbd..797da20cd 100644 --- a/src/components/SearchWork/SearchWork.tsx +++ b/src/components/SearchWork/SearchWork.tsx @@ -12,6 +12,7 @@ import { useSearchDoiFacetsQuery } from 'src/data/queries/searchDoiFacetsQuery' import NoResults from 'src/components/NoResults/NoResults' import WorksListing, { SortBy } from 'src/components/WorksListing/WorksListing' import { pluralize } from 'src/utils/helpers' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' interface Props { variables: QueryVar @@ -24,29 +25,23 @@ export default function SearchWork(props: Props) { if (loading) return <Row><Loading /></Row> if (error) return ( - <Row> - <Col md={{ span: 9, offset: 3 }}> - <NoResults /> - </Col> - </Row> + <CommonsLayout> + <NoResults /> + </CommonsLayout> ) const works = { ...data?.works, ...facets.data?.works } as Works return (<> - <Row> + <CommonsLayout> <h2 className="visually-hidden">Works Results</h2> - <Col md={3} className="d-none d-md-block"> - </Col> - <Col md={9}> - {works.totalCount > 0 && ( - <Row className="border-bottom ms-1 mb-3"> - <Col className="ps-0"><h3 className="member-results border-0 mb-0">{pluralize(works.totalCount, 'Work')}</h3></Col> - <Col xs="auto"><Suspense><SortBy /></Suspense></Col> - </Row> - )} - </Col> - </Row> + {works.totalCount > 0 && ( + <Row className="border-bottom ms-1 mb-3"> + <Col className="ps-0"><h3 className="member-results border-0 mb-0">{pluralize(works.totalCount, 'Work')}</h3></Col> + <Col xs="auto"><Suspense><SortBy /></Suspense></Col> + </Row> + )} + </CommonsLayout> <WorksListing works={works} loading={false} diff --git a/src/components/WorksListing/WorksListing.tsx b/src/components/WorksListing/WorksListing.tsx index d12db8c23..61d08663e 100644 --- a/src/components/WorksListing/WorksListing.tsx +++ b/src/components/WorksListing/WorksListing.tsx @@ -11,6 +11,7 @@ import { ConnectionTypeCounts, OrganizationRelationTypeCounts, Works } from 'src import Loading from 'src/components/Loading/Loading' import LoadingFacetList from 'src/components/Loading/LoadingFacetList' import NoResults from 'src/components/NoResults/NoResults' +import CommonsLayout from 'src/components/CommonsLayout/CommonsLayout' import Pager from 'src/components/Pager/Pager' import type { ShowCharts } from 'src/components/WorksDashboard/WorksDashboard' @@ -129,17 +130,20 @@ export default function WorksListing({ ) } + const sidebar = (loadingFacets || organizationRelationCountsLoading) + ? <Row><LoadingFacetList count={4} numberOfLines={10} /></Row> + : renderFacets() + return ( - <Row> - <Col md={3} className={'d-none d-md-block' + (['doi.org/?'].includes(url) ? ' px-4' : ' pe-4')}> - {(loadingFacets || organizationRelationCountsLoading) ? <Row><LoadingFacetList count={4} numberOfLines={10} /></Row> : renderFacets()} - </Col> - <Col md={9}> - <h2 className="visually-hidden">Works Search Listing</h2> - {children} - {loading ? <Loading /> : renderWorks()} - </Col> - </Row> + <CommonsLayout + fluid={false} + sidebarClassName={'d-none d-md-block' + (['doi.org/?'].includes(url) ? ' px-4' : ' pe-4')} + sidebar={sidebar} + > + <h2 className="visually-hidden">Works Search Listing</h2> + {children} + {loading ? <Loading /> : renderWorks()} + </CommonsLayout> ) } From 905f79c9f8c10cd960fae3e2b7127a02eef6e36e Mon Sep 17 00:00:00 2001 From: jrhoads <jrhoads@datacite.org> Date: Fri, 11 Sep 2026 16:05:33 +0200 Subject: [PATCH 2/2] refactor: wrap RelatedContent in fluid Container and update tests --- src/app/(main)/doi.org/[...doi]/RelatedContent.tsx | 7 ++++--- src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx | 7 ++++--- src/app/(main)/repositories/[...repoid]/RelatedContent.tsx | 7 ++++--- src/app/(main)/ror.org/[rorid]/RelatedContent.tsx | 7 ++++--- src/components/CommonsLayout/CommonsLayout.test.tsx | 3 ++- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx b/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx index cbe2b3a44..bf40ab651 100644 --- a/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx +++ b/src/app/(main)/doi.org/[...doi]/RelatedContent.tsx @@ -2,6 +2,7 @@ import React from 'react' import { useParams, useSearchParams } from 'next/navigation' +import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' @@ -63,8 +64,8 @@ export default function RelatedContent() { const { hasPagination, hasNextPage, endCursor } = manager.pagination const url = '/doi.org/' + doi + '/' return ( - <> - <CommonsLayout> + <Container fluid> + <CommonsLayout fluid={false}> <Row className="border-bottom ms-1 mb-3"> <Col className="ps-0"> <h3 className="member-results border-0 mb-0" id="title"> @@ -96,6 +97,6 @@ export default function RelatedContent() { <SearchBox path={url} placeholder="Search within these works..." /> } /> - </> + </Container> ) } diff --git a/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx b/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx index 4f3bde179..3868b04d2 100644 --- a/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx +++ b/src/app/(main)/orcid.org/[orcid]/RelatedContent.tsx @@ -1,6 +1,7 @@ 'use client' import React from 'react' +import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' import Loading from 'src/components/Loading/Loading' @@ -62,8 +63,8 @@ export default function RelatedContent(props: Props) { const url = '/orcid.org/' + orcid + '/' return ( - <> - <CommonsLayout className="mt-5"> + <Container fluid> + <CommonsLayout className="mt-5" fluid={false}> <Row className="border-bottom ms-1 mb-3"> <Col className="ps-0"> <h3 className="member-results border-0 mb-0"> @@ -89,6 +90,6 @@ export default function RelatedContent(props: Props) { <SearchBox path={url} placeholder="Search within these works..." /> } /> - </> + </Container> ) } diff --git a/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx b/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx index 52366d77c..9730afa00 100644 --- a/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx +++ b/src/app/(main)/repositories/[...repoid]/RelatedContent.tsx @@ -1,6 +1,7 @@ 'use client' import React from 'react' +import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' import Loading from 'src/components/Loading/Loading' @@ -60,8 +61,8 @@ export default function RelatedContent({ repository }: Props) { const url = '/repositories/' + clientId + '/' return ( - <> - <CommonsLayout className="mt-5"> + <Container fluid> + <CommonsLayout className="mt-5" fluid={false}> <Row className="border-bottom ms-1 mb-3"> <Col className="ps-0"> <h2 className="visually-hidden">Related Works Results Summary</h2> @@ -90,6 +91,6 @@ export default function RelatedContent({ repository }: Props) { <SearchBox path={url} placeholder="Search within these works..." /> } /> - </> + </Container> ) } diff --git a/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx b/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx index f3c0157bc..5a0a0ed8d 100644 --- a/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx +++ b/src/app/(main)/ror.org/[rorid]/RelatedContent.tsx @@ -1,6 +1,7 @@ 'use client' import React from 'react' +import Container from 'react-bootstrap/Container' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' import Loading from 'src/components/Loading/Loading' @@ -59,8 +60,8 @@ export default function RelatedContent() { const url = '/ror.org/' + vars.rorId + '/' return ( - <> - <CommonsLayout className="mt-5" mainClassName="px-0"> + <Container fluid> + <CommonsLayout className="mt-5" mainClassName="px-0" fluid={false}> <Row className="border-bottom ms-1 mb-3"> <Col className="ps-0"> <h3 className="member-results border-0 mb-0">Related Works</h3> @@ -92,6 +93,6 @@ export default function RelatedContent() { <SummarySearchMetrics {...vars} /> </div> </WorksListing> - </> + </Container> ) } diff --git a/src/components/CommonsLayout/CommonsLayout.test.tsx b/src/components/CommonsLayout/CommonsLayout.test.tsx index d94e12731..0047fb170 100644 --- a/src/components/CommonsLayout/CommonsLayout.test.tsx +++ b/src/components/CommonsLayout/CommonsLayout.test.tsx @@ -24,6 +24,7 @@ describe('CommonsLayout Component', () => { </CommonsLayout> ) cy.get('.container-fluid > .row > .col-md-9.offset-md-3') + .find('span#main') .should('contain', 'Main') .and('have.attr', 'id', 'main') cy.get('.col-md-3').should('not.exist') @@ -41,7 +42,7 @@ describe('CommonsLayout Component', () => { </CommonsLayout> ) cy.get('.container-fluid').should('not.exist') - cy.get(':root > .row > .col-md-3') + cy.get('[data-cy-root] .row > .col-md-3') .should('have.class', 'px-4') .and('not.have.class', 'pe-4') cy.get('.col-md-9').should('have.class', 'px-0')