From a4b0485a189df13f0de058df7f0e0089de9d4899 Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:28:11 -0500 Subject: [PATCH 1/2] fix(ResultList): fetch citation lazily on share menu open Removes eager batch citation fetch from SimpleResultList that fired export/agu API calls on every page load. Citations are now fetched lazily in ItemResourceDropdowns when the user opens the share dropdown. - Add useGetExportCitation to ItemResourceDropdowns gated on menu open - Remove batch fetch, bibcodes, defaultCitations from SimpleResultList - Remove defaultCitation prop threading through Item - Add tests for lazy fetch behavior --- src/components/ResultList/Item/Item.tsx | 4 +- .../Item/ItemResourceDropdowns.test.tsx | 98 +++++++++++++++++++ .../ResultList/Item/ItemResourceDropdowns.tsx | 29 ++++-- .../ResultList/SimpleResultList.tsx | 35 +------ 4 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 src/components/ResultList/Item/ItemResourceDropdowns.test.tsx diff --git a/src/components/ResultList/Item/Item.tsx b/src/components/ResultList/Item/Item.tsx index d3ab8950f..e3b863f2a 100644 --- a/src/components/ResultList/Item/Item.tsx +++ b/src/components/ResultList/Item/Item.tsx @@ -52,7 +52,6 @@ export interface IItemProps { highlights?: Record; extraInfo?: string; linkNewTab?: boolean; - defaultCitation: string; } export const Item = (props: IItemProps): ReactElement => { @@ -67,7 +66,6 @@ export const Item = (props: IItemProps): ReactElement => { highlights, extraInfo, linkNewTab = false, - defaultCitation = '', } = props; const { bibcode, pubdate, title = ['Untitled'], author = [], author_count, pub } = doc; const encodedCanonicalID = bibcode ? encodeURIComponent(bibcode) : ''; @@ -152,7 +150,7 @@ export const Item = (props: IItemProps): ReactElement => { - {!isClient || hideActions ? null : } + {!isClient || hideActions ? null : } diff --git a/src/components/ResultList/Item/ItemResourceDropdowns.test.tsx b/src/components/ResultList/Item/ItemResourceDropdowns.test.tsx new file mode 100644 index 000000000..0dcf4ee65 --- /dev/null +++ b/src/components/ResultList/Item/ItemResourceDropdowns.test.tsx @@ -0,0 +1,98 @@ +import { render, waitFor } from '@/test-utils'; +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { ItemResourceDropdowns } from './ItemResourceDropdowns'; +import { IDocsEntity } from '@/api/search/types'; + +const mocks = vi.hoisted(() => ({ + useRouter: vi.fn(() => ({ + query: {}, + asPath: '/', + push: vi.fn(), + events: { on: vi.fn(), off: vi.fn() }, + })), + useSettings: vi.fn(() => ({ + settings: { defaultCitationFormat: 'agu' }, + })), + useGetExportCitation: vi.fn(() => ({ data: undefined as { export: string } | undefined })), +})); + +vi.mock('next/router', () => ({ useRouter: mocks.useRouter })); +vi.mock('@/lib/useSettings', () => ({ useSettings: mocks.useSettings })); +vi.mock('@/api/export/export', () => ({ + useGetExportCitation: mocks.useGetExportCitation, +})); + +const makeDoc = (overrides?: Partial): IDocsEntity => + ({ + bibcode: '2020ApJ...123..456A', + title: ['Test Paper'], + author: ['Author, A.'], + pubdate: '2020-01-00', + citation_count: 0, + reference_count: 0, + esources: [], + property: [], + ...overrides, + } as unknown as IDocsEntity); + +describe('ItemResourceDropdowns', () => { + afterEach(() => { + mocks.useGetExportCitation.mockClear(); + mocks.useGetExportCitation.mockReturnValue({ data: undefined }); + }); + + test('does not fetch citation on mount', () => { + render(); + + // The hook should be called with enabled: false on initial render + // because isShareOpen starts as false + expect(mocks.useGetExportCitation).toHaveBeenCalledWith( + expect.objectContaining({ + format: 'agu', + bibcode: ['2020ApJ...123..456A'], + }), + expect.objectContaining({ enabled: false }), + ); + }); + + test('fetches citation when share menu is opened', async () => { + const { user, getByLabelText } = render(); + + mocks.useGetExportCitation.mockClear(); + + const shareButton = getByLabelText('share options'); + await user.click(shareButton); + + // After opening the share menu, the hook should be called + // with enabled: true + await waitFor(() => { + expect(mocks.useGetExportCitation).toHaveBeenCalledWith( + expect.objectContaining({ + format: 'agu', + bibcode: ['2020ApJ...123..456A'], + }), + expect.objectContaining({ enabled: true }), + ); + }); + }); + + test('uses citation data from hook in CopyMenuItem', async () => { + mocks.useGetExportCitation.mockReturnValue({ + data: { export: 'Author, A. (2020). Test Paper.' }, + }); + + const { getByLabelText, user, getByText } = render(); + + const shareButton = getByLabelText('share options'); + await user.click(shareButton); + + // The Copy Citation menu item should be visible + expect(getByText('Copy Citation')).toBeInTheDocument(); + }); + + test('renders share options button', () => { + const { getByLabelText } = render(); + + expect(getByLabelText('share options')).toBeInTheDocument(); + }); +}); diff --git a/src/components/ResultList/Item/ItemResourceDropdowns.tsx b/src/components/ResultList/Item/ItemResourceDropdowns.tsx index 3c5949162..cd5409abc 100644 --- a/src/components/ResultList/Item/ItemResourceDropdowns.tsx +++ b/src/components/ResultList/Item/ItemResourceDropdowns.tsx @@ -7,6 +7,7 @@ import { MenuList, Tooltip, useClipboard, + useDisclosure, useToast, } from '@chakra-ui/react'; import { LockIcon, UnlockIcon } from '@chakra-ui/icons'; @@ -18,9 +19,12 @@ import { MouseEventHandler, ReactElement, useEffect } from 'react'; import { isBrowser } from '@/utils/common/guards'; import { IDocsEntity } from '@/api/search/types'; import { CopyMenuItem } from '@/components/CopyButton'; +import { useGetExportCitation } from '@/api/export/export'; +import { useSettings } from '@/lib/useSettings'; export interface IItemResourceDropdownsProps { doc: IDocsEntity; + /** @deprecated No longer used — citation is fetched lazily. */ defaultCitation?: string; } @@ -30,9 +34,21 @@ export interface IItem { path?: string; } -export const ItemResourceDropdowns = ({ doc, defaultCitation }: IItemResourceDropdownsProps): ReactElement => { +export const ItemResourceDropdowns = ({ doc }: IItemResourceDropdownsProps): ReactElement => { const router = useRouter(); const toast = useToast(); + const { isOpen: isShareOpen, onOpen: onShareOpen, onClose: onShareClose } = useDisclosure(); + const { settings } = useSettings(); + + const { data: citationData } = useGetExportCitation( + { + format: settings.defaultCitationFormat, + bibcode: [doc.bibcode], + }, + { enabled: isShareOpen && !!doc.bibcode }, + ); + + const citation = citationData?.export ?? ''; const { hasCopied, onCopy, setValue, value } = useClipboard(''); @@ -136,7 +152,7 @@ export const ItemResourceDropdowns = ({ doc, defaultCitation }: IItemResourceDro }; const handleCitationCopied = () => { - if (!!defaultCitation) { + if (!!citation) { toast({ status: 'info', title: 'Copied to Clipboard' }); } else { toast({ status: 'error', title: 'There was a problem fetching citation. Try reloading the page.' }); @@ -217,7 +233,7 @@ export const ItemResourceDropdowns = ({ doc, defaultCitation }: IItemResourceDro {/* share menu */} - + Copy URL - + diff --git a/src/components/ResultList/SimpleResultList.tsx b/src/components/ResultList/SimpleResultList.tsx index 5eb53e0e2..36300e54c 100644 --- a/src/components/ResultList/SimpleResultList.tsx +++ b/src/components/ResultList/SimpleResultList.tsx @@ -1,13 +1,11 @@ import { Alert, AlertIcon, Box, Flex, Text, VisuallyHidden } from '@chakra-ui/react'; import { useIsClient } from '@/lib/useIsClient'; import PT from 'prop-types'; -import { HTMLAttributes, ReactElement, useMemo } from 'react'; +import { HTMLAttributes, ReactElement } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { Item, IItemProps } from './Item'; import { useHighlights } from './useHighlights'; import { IDocsEntity } from '@/api/search/types'; -import { useGetExportCitation } from '@/api/export/export'; -import { useSettings } from '@/lib/useSettings'; import { handleBoundaryError } from '@/lib/errorHandler'; export interface ISimpleResultListProps extends HTMLAttributes { @@ -45,7 +43,9 @@ const ItemErrorFallback = ({ bibcode }: { bibcode: string } & FallbackProps) => */ const SafeItem = (props: IItemProps) => ( handleBoundaryError(error, errorInfo, { component: 'Item', bibcode: props.doc.bibcode })} + onError={(error, errorInfo) => + handleBoundaryError(error, errorInfo, { component: 'Item', bibcode: props.doc.bibcode }) + } fallbackRender={(fallbackProps) => } > @@ -68,32 +68,6 @@ export const SimpleResultList = (props: ISimpleResultListProps): ReactElement => const { highlights, showHighlights, isFetchingHighlights } = useHighlights(); - const { settings } = useSettings(); - const { defaultCitationFormat } = settings; - - const bibcodes = docs.map((d) => d.bibcode).sort(); - - const { data: citationData } = useGetExportCitation( - { - format: defaultCitationFormat, - bibcode: bibcodes, - sort: ['bibcode asc'], - outputformat: 2, - }, - { enabled: !!settings?.defaultCitationFormat }, - ); - - // a map from bibcode to citation - const defaultCitations = useMemo(() => { - const citationSet = new Map(); - if (!!citationData) { - citationData.docs.map((doc) => { - citationSet.set(doc.bibcode, doc.reference); - }); - } - return citationSet; - }, [citationData]); - return ( highlights={highlights?.[index] ?? {}} isFetchingHighlights={allowHighlight && isFetchingHighlights} useNormCite={useNormCite} - defaultCitation={defaultCitations?.get(doc.bibcode)} /> ))} From b6cb21daef4b85eb6f0f02d48603b2d7c4deb7b1 Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:34:38 -0500 Subject: [PATCH 2/2] fix(SimpleLink): move event handlers to child for legacyBehavior When legacyBehavior is true (default), Next.js Link expects onMouseEnter and onTouchStart to be on the child element, not the Link itself. --- src/components/SimpleLink/SimpleLink.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SimpleLink/SimpleLink.tsx b/src/components/SimpleLink/SimpleLink.tsx index 0adcc2aa1..6360294d7 100644 --- a/src/components/SimpleLink/SimpleLink.tsx +++ b/src/components/SimpleLink/SimpleLink.tsx @@ -37,8 +37,6 @@ export const SimpleLink = forwardRef( href={href} prefetch={prefetch} passHref={passHref} - onTouchStart={onTouchStart} - onMouseEnter={onMouseEnter} onNavigate={onNavigate} locale={locale} shallow={shallow} @@ -50,6 +48,8 @@ export const SimpleLink = forwardRef(