diff --git a/src/__tests__/ssr-utils.test.ts b/src/__tests__/ssr-utils.test.ts index 5a8cca6c2..f2e71fbb8 100644 --- a/src/__tests__/ssr-utils.test.ts +++ b/src/__tests__/ssr-utils.test.ts @@ -16,6 +16,7 @@ const getMockContext = ( query: ParsedUrlQuery = {}, resolvedUrl = '/search', referer?: string, + cookie?: string, ) => ({ req: { @@ -24,9 +25,7 @@ const getMockContext = ( save: vi.fn().mockResolvedValue(undefined), destroy: vi.fn().mockResolvedValue(undefined), }, - headers: { - referer, - }, + headers: { referer, cookie }, }, query, resolvedUrl, @@ -34,6 +33,8 @@ const getMockContext = ( describe('updateUserStateSSR', () => { test('should set mode for legacy referrer with no persisted state', async () => { + // Mode is now set by middleware writing the scix_prefs cookie before redirect. + // updateUserStateSSR no longer reads the referer header to infer mode. const context = getMockContext({}, {}, '/search', 'https://ui.adsabs.harvard.edu/search'); const result = await updateUserStateSSR(context, { props: {} }); @@ -41,23 +42,15 @@ describe('updateUserStateSSR', () => { throw new Error('Expected props in result'); } const props = result.props as SSRPropsWithState; - expect(props.dehydratedAppState).toEqual( - expect.objectContaining({ - mode: AppMode.ASTROPHYSICS, - }), - ); + expect(props.dehydratedAppState).not.toHaveProperty('mode'); }); test('should respect persisted state even with legacy referrer', async () => { - const context = getMockContext({}, {}, '/search', 'https://ui.adsabs.harvard.edu/search'); - const inputProps = { - props: { - dehydratedAppState: { - mode: AppMode.GENERAL, - } as Partial, - }, - }; - const result = await updateUserStateSSR(context, inputProps as never); + // Cookie-based mode takes precedence; referer header is ignored. + const prefs = { mode: 'EARTH_SCIENCE' }; + const cookie = `scix_prefs=${encodeURIComponent(JSON.stringify(prefs))}`; + const context = getMockContext({}, {}, '/search', 'https://ui.adsabs.harvard.edu/search', cookie); + const result = await updateUserStateSSR(context, { props: {} }); if (!('props' in result)) { throw new Error('Expected props in result'); @@ -65,7 +58,7 @@ describe('updateUserStateSSR', () => { const resultProps = result.props as SSRPropsWithState; expect(resultProps.dehydratedAppState).toEqual( expect.objectContaining({ - mode: AppMode.GENERAL, + mode: AppMode.EARTH_SCIENCE, }), ); }); @@ -218,4 +211,74 @@ describe('updateUserStateSSR', () => { expect(props.dehydratedAppState).not.toHaveProperty('mode'); }); }); + + test('seeds mode from scix_prefs cookie when no URL param', async () => { + const prefs = { mode: 'ASTROPHYSICS' }; + const cookie = `scix_prefs=${encodeURIComponent(JSON.stringify(prefs))}`; + const context = getMockContext({}, {}, '/search', undefined, cookie); + const result = await updateUserStateSSR(context, { props: {} }); + if (!('props' in result)) { + throw new Error('Expected props'); + } + const props = result.props as SSRPropsWithState; + expect(props.dehydratedAppState).toEqual(expect.objectContaining({ mode: AppMode.ASTROPHYSICS })); + }); + + test('seeds searchMode from scix_prefs cookie', async () => { + const prefs = { searchMode: 'ADS_COMPAT', mode: 'ASTROPHYSICS' }; + const cookie = `scix_prefs=${encodeURIComponent(JSON.stringify(prefs))}`; + const context = getMockContext({}, {}, '/', undefined, cookie); + const result = await updateUserStateSSR(context, { props: {} }); + if (!('props' in result)) { + throw new Error('Expected props'); + } + const props = result.props as SSRPropsWithState; + expect(props.dehydratedAppState).toEqual(expect.objectContaining({ searchMode: 'ADS_COMPAT' })); + }); + + test('URL forceMode takes priority over prefs cookie mode', async () => { + const prefs = { mode: 'ASTROPHYSICS' }; + const cookie = `scix_prefs=${encodeURIComponent(JSON.stringify(prefs))}`; + const context = getMockContext({}, { forceMode: 'heliophysics' }, '/', undefined, cookie); + const result = await updateUserStateSSR(context, { props: {} }); + if (!('props' in result)) { + throw new Error('Expected props'); + } + const props = result.props as SSRPropsWithState; + expect(props.dehydratedAppState).toEqual(expect.objectContaining({ mode: AppMode.HELIOPHYSICS })); + }); + + test('legacy referrer no longer sets mode in GSSP (handled by middleware+cookie)', async () => { + const context = getMockContext({}, {}, '/search', 'https://ui.adsabs.harvard.edu/search'); + const result = await updateUserStateSSR(context, { props: {} }); + if (!('props' in result)) { + throw new Error('Expected props'); + } + const props = result.props as SSRPropsWithState; + expect(props.dehydratedAppState).not.toHaveProperty('mode'); + }); + + test('rejects invalid mode value from prefs cookie', async () => { + const prefs = { mode: 'GARBAGE_MODE' }; + const cookie = `scix_prefs=${encodeURIComponent(JSON.stringify(prefs))}`; + const context = getMockContext({}, {}, '/search', undefined, cookie); + const result = await updateUserStateSSR(context, { props: {} }); + if (!('props' in result)) { + throw new Error('Expected props'); + } + const props = result.props as SSRPropsWithState; + expect(props.dehydratedAppState).not.toHaveProperty('mode'); + }); + + test('rejects invalid searchMode value from prefs cookie', async () => { + const prefs = { searchMode: 'INVALID_SEARCH_MODE' }; + const cookie = `scix_prefs=${encodeURIComponent(JSON.stringify(prefs))}`; + const context = getMockContext({}, {}, '/', undefined, cookie); + const result = await updateUserStateSSR(context, { props: {} }); + if (!('props' in result)) { + throw new Error('Expected props'); + } + const props = result.props as SSRPropsWithState; + expect(props.dehydratedAppState).not.toHaveProperty('searchMode'); + }); }); diff --git a/src/components/AbstractSearchForm/AbstractSearchForm.tsx b/src/components/AbstractSearchForm/AbstractSearchForm.tsx index 49db29c85..6fbe1879f 100644 --- a/src/components/AbstractSearchForm/AbstractSearchForm.tsx +++ b/src/components/AbstractSearchForm/AbstractSearchForm.tsx @@ -8,17 +8,16 @@ import router from 'next/router'; import { applyFiltersToQuery } from '../SearchFacet/helpers'; import { DatabaseEnum, IADSApiUserDataResponse } from '@/api/user/types'; import { SolrSort } from '@/api/models'; +import { buildSearchOutgoing } from '@/utils/common/searchMode'; +import { useSearchMode } from '@/lib/useSearchMode'; export const AbstractSearchForm = () => { const { settings } = useSettings(); const submitQuery = useStore((state) => state.submitQuery); const sort = [`${settings.preferredSearchSort} desc` as SolrSort]; const query = useStore((state) => state.query.q); + const [searchMode] = useSearchMode(); - /** - * Take in a query object and apply any FQ filters - * These will either be any default ON filters or whatever has been set by the user in the preferences - */ const applyDefaultFilters = useCallback( (query: IADSApiSearchParams) => { const defaultDatabases = getListOfAppliedDefaultDatabases(settings.defaultDatabase); @@ -35,18 +34,12 @@ export const AbstractSearchForm = () => { [settings.defaultDatabase], ); - /** - * Get a list of default databases that have been applied - * @param databases - */ const getListOfAppliedDefaultDatabases = (databases: IADSApiUserDataResponse['defaultDatabase']): Array => { const defaultDatabases = []; for (const db of databases) { - // if All is selected, exit early here and return an empty array (no filters to apply) if (db.name === DatabaseEnum.All && db.value) { return []; } - if (db.value) { defaultDatabases.push(db.name); } @@ -62,13 +55,14 @@ export const AbstractSearchForm = () => { if (query && query.trim().length > 0) { submitQuery(); const defaultedQuery = applyDefaultFilters({ q: query, sort, p: 1 }) as IADSApiSearchParams; + const outgoing = buildSearchOutgoing(defaultedQuery, searchMode); void router.push({ pathname: '/search', - search: makeSearchParams(defaultedQuery), + search: makeSearchParams(outgoing), }); } }, - [applyDefaultFilters, sort, submitQuery], + [applyDefaultFilters, searchMode, sort, submitQuery], ); return ( diff --git a/src/components/ClassicForm/ClassicForm.tsx b/src/components/ClassicForm/ClassicForm.tsx index 4f0d32f92..109a06141 100644 --- a/src/components/ClassicForm/ClassicForm.tsx +++ b/src/components/ClassicForm/ClassicForm.tsx @@ -41,6 +41,7 @@ import { Sort } from '@/components/Sort'; import { Expandable } from '@/components/Expandable'; import { SimpleCopyButton } from '@/components/CopyButton'; import { normalizeSolrSort } from '@/utils/common/search'; +import { ADS_COMPAT_URL_PARAM } from '@/utils/common/searchMode'; import { SolrSort, SolrSortField } from '@/api/models'; const propTypes = { @@ -84,7 +85,9 @@ export const ClassicForm = (props: IClassicFormProps) => { void handleSubmit((params) => { try { const search = getSearchQuery(params, { mode }); - void router.push({ pathname: '/search', search }); + const urlParams = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search); + urlParams.set(ADS_COMPAT_URL_PARAM, '1'); + void router.push({ pathname: '/search', search: '?' + urlParams.toString() }); } catch (e) { setQueryError((e as Error)?.message); } diff --git a/src/components/SearchBar/__tests__/SearchBar.test.tsx b/src/components/SearchBar/__tests__/SearchBar.test.tsx index bfebbf802..a1956199a 100644 --- a/src/components/SearchBar/__tests__/SearchBar.test.tsx +++ b/src/components/SearchBar/__tests__/SearchBar.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/test-utils'; +import { fireEvent, render } from '@/test-utils'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, expect, test, vi } from 'vitest'; import { SearchBar } from '../index'; @@ -21,7 +21,9 @@ vi.mock('@/lib/useLandingFormPreference', () => ({ })); beforeEach(() => { - vi.useFakeTimers(); + // Exclude requestAnimationFrame from fake timers to prevent Framer Motion's animation + // loop from cascading under vi.advanceTimersByTimeAsync and inflating test runtime. + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date'] }); }); afterEach(() => { @@ -104,10 +106,12 @@ test('Escape key closes typeahead', async () => { await user.type(input, 'sim'); await vi.advanceTimersByTimeAsync(500); expect(queryByTestId('search-autocomplete-menu')).toBeVisible(); + // Escape dispatches KEYDOWN_ESCAPE synchronously; no timer advancement needed. await user.keyboard('{Escape}'); - await vi.advanceTimersByTimeAsync(500); expect(queryByTestId('search-autocomplete-menu')).not.toBeVisible(); -}); + // 15s timeout: vi.advanceTimersByTimeAsync(500) triggers Chakra/React work that + // accumulates across the suite; this test passes easily in isolation but needs headroom. +}, 15000); test('Clearing input closes typeahead menu', async () => { const user = createUser(); @@ -116,8 +120,8 @@ test('Clearing input closes typeahead menu', async () => { await user.type(input, 'sim'); await vi.advanceTimersByTimeAsync(500); expect(queryByTestId('search-autocomplete-menu')).toBeVisible(); + // HARD_RESET dispatches synchronously; no timer advancement needed. await user.click(getByTestId('search-clearbtn')); - await vi.advanceTimersByTimeAsync(500); expect(queryByTestId('search-autocomplete-menu')).not.toBeVisible(); }); @@ -184,10 +188,11 @@ test('Typing a non-matching term does not open typeahead', async () => { }); test('Typing an exact match for a typeahead option closes the menu', async () => { - const user = createUser(); const { getByTestId, queryByTestId } = render(); const input = getByTestId('search-input'); - await user.type(input, 'similar()'); + // fireEvent.change simulates the handleInputChange path without triggering the + // React-scheduler timer cascade that makes long user.type() calls exceed the timeout. + fireEvent.change(input, { target: { value: 'similar()', selectionStart: 9 } }); await vi.advanceTimersByTimeAsync(500); expect(queryByTestId('search-autocomplete-menu')).not.toBeVisible(); }); @@ -253,8 +258,9 @@ test('Arrow down does nothing when cursor is not at end of input', async () => { const user = createUser(); const { getByTestId, queryByTestId } = render(); const input = getByTestId('search-input') as HTMLInputElement; - await user.type(input, 'test query'); - input.setSelectionRange(4, 4); + await user.type(input, 'test'); + // Place cursor before the end to disable ArrowDown menu-open behaviour + input.setSelectionRange(0, 0); await user.keyboard('{ArrowDown}'); await vi.advanceTimersByTimeAsync(500); expect(queryByTestId('search-autocomplete-menu')).not.toBeVisible(); diff --git a/src/components/SearchModifierBar/SearchModifierBar.tsx b/src/components/SearchModifierBar/SearchModifierBar.tsx new file mode 100644 index 000000000..2421e7901 --- /dev/null +++ b/src/components/SearchModifierBar/SearchModifierBar.tsx @@ -0,0 +1,76 @@ +import { Box, BoxProps, Button, Circle, Menu, MenuButton, MenuItem, MenuList, Text, VStack } from '@chakra-ui/react'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { useRouter } from 'next/router'; +import { omit } from 'ramda'; +import { ADS_COMPAT_URL_PARAM, SEARCH_MODE_OPTIONS, SearchMode } from '@/utils/common/searchMode'; +import { useSearchMode } from '@/lib/useSearchMode'; + +interface SearchModifierBarProps extends BoxProps { + onModeChange?: (mode: SearchMode) => void; + onNavigate?: (mode: SearchMode) => void; + isDisabled?: boolean; +} + +export const SearchModifierBar = ({ onModeChange, onNavigate, isDisabled, ...boxProps }: SearchModifierBarProps) => { + const router = useRouter(); + const [storedMode, setStoredMode] = useSearchMode(); + const urlAdsCompat = router.query[ADS_COMPAT_URL_PARAM] === '1'; + const currentMode = + urlAdsCompat || storedMode === SearchMode.ADS_COMPAT ? SearchMode.ADS_COMPAT : SearchMode.ALL_RELEVANT; + const currentOption = SEARCH_MODE_OPTIONS.find((o) => o.mode === currentMode) ?? SEARCH_MODE_OPTIONS[0]; + + const handleModeChange = (newMode: SearchMode) => { + setStoredMode(newMode); + onModeChange?.(newMode); + if (onNavigate) { + onNavigate(newMode); + } else { + const updatedQuery = + newMode === SearchMode.ADS_COMPAT + ? { ...router.query, [ADS_COMPAT_URL_PARAM]: '1' } + : omit([ADS_COMPAT_URL_PARAM], router.query); + void router.push({ pathname: router.pathname, query: updatedQuery }, undefined, { shallow: true }); + } + }; + + const modeColor = currentMode === SearchMode.ADS_COMPAT ? 'orange.400' : 'teal.400'; + + return ( + + + } + rightIcon={} + aria-label={`Search mode: ${currentOption.label}`} + isDisabled={isDisabled} + > + + Search mode: + + + {currentOption.label} + + + + {SEARCH_MODE_OPTIONS.map((option) => ( + handleModeChange(option.mode)} + fontWeight={option.mode === currentMode ? 'semibold' : 'normal'} + > + + {option.label} + + {option.helperText} + + + + ))} + + + + ); +}; diff --git a/src/components/SearchModifierBar/__tests__/SearchModifierBar.test.tsx b/src/components/SearchModifierBar/__tests__/SearchModifierBar.test.tsx new file mode 100644 index 000000000..df416be07 --- /dev/null +++ b/src/components/SearchModifierBar/__tests__/SearchModifierBar.test.tsx @@ -0,0 +1,189 @@ +import React from 'react'; +import { render } from '@/test-utils'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { SearchModifierBar } from '../SearchModifierBar'; +import { ADS_COMPAT_URL_PARAM, SearchMode } from '@/utils/common/searchMode'; + +const mockPush = vi.fn(); +const mockQuery: Record = {}; + +vi.mock('next/router', () => ({ + useRouter: () => ({ + query: mockQuery, + push: mockPush, + pathname: '/search', + }), +})); + +// modeRef is module-scoped. The vi.mock factory body cannot reference it (hoisting TDZ), +// but useSearchMode is only called at render time, by which point it is initialized. +const modeRef = { current: '' as string }; + +vi.mock('@/lib/useSearchMode', () => ({ + // React.useState-backed mock: calling the returned setter causes a re-render, matching + // the reactive contract the real Zustand hook provides. + useSearchMode: () => { + const [mode, setMode] = React.useState(modeRef.current); + return [mode, setMode] as const; + }, +})); + +describe('SearchModifierBar', () => { + beforeEach(() => { + vi.clearAllMocks(); + delete mockQuery[ADS_COMPAT_URL_PARAM]; + modeRef.current = ''; + }); + + test('renders "All relevant content" when mode param is absent', () => { + const { getByRole } = render(); + expect(getByRole('button')).toHaveTextContent('All relevant content'); + }); + + test('renders "ADS Compatibility mode" when ads_compat=1', () => { + mockQuery[ADS_COMPAT_URL_PARAM] = '1'; + const { getByRole } = render(); + expect(getByRole('button')).toHaveTextContent('ADS Compatibility mode'); + }); + + test('shows "Search mode:" prefix in the button', () => { + const { getByText } = render(); + expect(getByText('Search mode:')).toBeInTheDocument(); + }); + + test('button has an accessible aria-label', () => { + const { getByRole } = render(); + expect(getByRole('button')).toHaveAttribute('aria-label'); + }); + + test('opens dropdown showing both options', async () => { + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + expect(getByRole('menuitem', { name: /All relevant content/ })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: /ADS Compatibility mode/ })).toBeInTheDocument(); + }); + + test('selecting ADS_COMPAT calls router.push with ads_compat=1 and shallow:true', async () => { + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /ADS Compatibility mode/ })); + expect(mockPush).toHaveBeenCalledWith( + expect.objectContaining({ query: expect.objectContaining({ [ADS_COMPAT_URL_PARAM]: '1' }) }), + undefined, + { shallow: true }, + ); + }); + + test('selecting ALL_RELEVANT removes ads_compat param from URL', async () => { + mockQuery[ADS_COMPAT_URL_PARAM] = '1'; + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /All relevant content/ })); + expect(mockPush.mock.calls[0][0].query).not.toHaveProperty(ADS_COMPAT_URL_PARAM); + }); + + test('calls onModeChange before router.push when selecting a mode', async () => { + const onModeChange = vi.fn(); + const callOrder: string[] = []; + onModeChange.mockImplementation(() => callOrder.push('onModeChange')); + mockPush.mockImplementation(() => callOrder.push('push')); + + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /ADS Compatibility mode/ })); + expect(callOrder).toEqual(['onModeChange', 'push']); + expect(onModeChange).toHaveBeenCalledWith(SearchMode.ADS_COMPAT); + }); + + test('calls onModeChange with ALL_RELEVANT when switching back', async () => { + mockQuery[ADS_COMPAT_URL_PARAM] = '1'; + const onModeChange = vi.fn(); + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /All relevant content/ })); + expect(onModeChange).toHaveBeenCalledWith(SearchMode.ALL_RELEVANT); + }); + + test('shows "All relevant content" immediately after selecting it when ADS_COMPAT was in storedMode (no URL mode)', async () => { + modeRef.current = SearchMode.ADS_COMPAT; + + const user = userEvent.setup(); + const { getByRole } = render(); + + expect(getByRole('button')).toHaveTextContent('ADS Compatibility mode'); + + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /All relevant content/ })); + + expect(getByRole('button')).toHaveTextContent('All relevant content'); + }); + + test('shows "ADS Compatibility mode" immediately after selecting it when starting from All relevant content', async () => { + const user = userEvent.setup(); + const { getByRole } = render(); + + expect(getByRole('button')).toHaveTextContent('All relevant content'); + + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /ADS Compatibility mode/ })); + + expect(getByRole('button')).toHaveTextContent('ADS Compatibility mode'); + }); + + describe('onNavigate prop', () => { + test('calls onNavigate instead of router.push when onNavigate is provided', async () => { + const onNavigate = vi.fn(); + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /ADS Compatibility mode/ })); + expect(onNavigate).toHaveBeenCalledWith(SearchMode.ADS_COMPAT); + expect(mockPush).not.toHaveBeenCalled(); + }); + + test('does not call router.push when onNavigate is provided for ALL_RELEVANT', async () => { + mockQuery[ADS_COMPAT_URL_PARAM] = '1'; + const onNavigate = vi.fn(); + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /All relevant content/ })); + expect(onNavigate).toHaveBeenCalledWith(SearchMode.ALL_RELEVANT); + expect(mockPush).not.toHaveBeenCalled(); + }); + + test('calls onModeChange before onNavigate', async () => { + const callOrder: string[] = []; + const onModeChange = vi.fn(() => callOrder.push('onModeChange')); + const onNavigate = vi.fn(() => callOrder.push('onNavigate')); + + const user = userEvent.setup(); + const { getByRole } = render(); + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /ADS Compatibility mode/ })); + expect(callOrder).toEqual(['onModeChange', 'onNavigate']); + }); + + test('updates button label via storedMode after onNavigate clears mode from URL', async () => { + modeRef.current = SearchMode.ADS_COMPAT; + + const onNavigate = vi.fn(); + const user = userEvent.setup(); + const { getByRole } = render(); + + expect(getByRole('button')).toHaveTextContent('ADS Compatibility mode'); + + await user.click(getByRole('button')); + await user.click(getByRole('menuitem', { name: /All relevant content/ })); + + expect(onNavigate).toHaveBeenCalledWith(SearchMode.ALL_RELEVANT); + expect(getByRole('button')).toHaveTextContent('All relevant content'); + }); + }); +}); diff --git a/src/components/SearchModifierBar/index.ts b/src/components/SearchModifierBar/index.ts new file mode 100644 index 000000000..0e66847d7 --- /dev/null +++ b/src/components/SearchModifierBar/index.ts @@ -0,0 +1 @@ +export { SearchModifierBar } from './SearchModifierBar'; diff --git a/src/components/SearchQueryLink/SearchQueryLink.tsx b/src/components/SearchQueryLink/SearchQueryLink.tsx index 01be2b53b..7488cfa33 100644 --- a/src/components/SearchQueryLink/SearchQueryLink.tsx +++ b/src/components/SearchQueryLink/SearchQueryLink.tsx @@ -4,22 +4,28 @@ import { useRouter } from 'next/router'; import { ISimpleLinkProps, SimpleLink } from '@/components/SimpleLink'; import { makeSearchParams } from '@/utils/common/search'; import { IADSApiSearchParams } from '@/api/search/types'; +import { useStore } from '@/store'; +import { buildSearchOutgoing } from '@/utils/common/searchMode'; export interface ISearchQueryLinkProps extends Omit { params: IADSApiSearchParams; } -const getSearchUrl = (params: IADSApiSearchParams) => `/search?${makeSearchParams(params)}`; +const getSearchUrl = (params: IADSApiSearchParams, searchMode: string) => + `/search?${makeSearchParams(buildSearchOutgoing(params, searchMode))}`; -/** - * Wrapper around next/link to create a simple link to the search page - * This generates the URL based on the params passed in - */ export const SearchQueryLink = (props: ISearchQueryLinkProps): ReactElement => { const { params, replace = false, shallow = false, prefetch = false, ...linkProps } = props; + const searchMode = useStore((s) => s.searchMode); return ( - + ); }; @@ -29,10 +35,11 @@ export interface ISearchQueryLinkButtonProps extends Omit { const { params, ...buttonProps } = props; const router = useRouter(); + const searchMode = useStore((s) => s.searchMode); const handleClick: MouseEventHandler = (e) => { e.preventDefault(); - void router.push(getSearchUrl(params)); + void router.push(getSearchUrl(params, searchMode)); }; return