diff --git a/packages/datagateway-common/src/api/investigations.test.tsx b/packages/datagateway-common/src/api/investigations.test.tsx index 8c6707b35..12a8b57ff 100644 --- a/packages/datagateway-common/src/api/investigations.test.tsx +++ b/packages/datagateway-common/src/api/investigations.test.tsx @@ -14,8 +14,11 @@ import { useInvestigationSize, useInvestigationSizes, useInvestigationsPaginated, + useSimilarInvestigations, } from './investigations'; +import { SuggestedInvestigation } from '../app.types'; + jest.mock('../handleICATError'); describe('investigation api functions', () => { @@ -1253,4 +1256,100 @@ describe('investigation api functions', () => { expect(document.body.appendChild).toHaveBeenCalledWith(link); }); }); + + describe('useSimilarInvestigations', function () { + const mockSuggestions: SuggestedInvestigation[] = [ + { + id: 1, + visitId: 'visitId', + name: 'Suggested investigation 1 name', + title: 'Suggested investigation 1', + summary: 'Suggested investigation 1 summary', + doi: 'doi1', + score: 0.1, + }, + { + id: 2, + visitId: 'visitId', + name: 'Suggested investigation 2 name', + title: 'Suggested investigation 2', + summary: 'Suggested investigation 2 summary', + doi: 'doi2', + score: 0.9, + }, + { + id: 3, + visitId: 'visitId', + name: 'Suggested investigation 3 name', + title: 'Suggested investigation 3', + summary: 'Suggested investigation 3 summary', + doi: 'doi3', + score: 0.5, + }, + { + id: 4, + visitId: 'visitId', + name: 'Suggested investigation 4 name', + title: 'Suggested investigation 4', + summary: 'Suggested investigation 4 summary', + doi: 'doi4', + score: 0.4, + }, + ]; + + it('queries for investigations similar to the given investigation and sorts the result by their relevance from the most relevant to the least', async () => { + (axios.get as jest.Mock).mockResolvedValue({ + data: mockSuggestions, + }); + + const { result, waitFor } = renderHook( + () => + useSimilarInvestigations({ + investigation: mockData[0], + }), + { wrapper: createReactQueryWrapper() } + ); + + await waitFor(() => result.current.isSuccess); + + expect(result.current.data).toEqual([ + { + id: 2, + visitId: 'visitId', + name: 'Suggested investigation 2 name', + title: 'Suggested investigation 2', + summary: 'Suggested investigation 2 summary', + doi: 'doi2', + score: 0.9, + }, + { + id: 3, + visitId: 'visitId', + name: 'Suggested investigation 3 name', + title: 'Suggested investigation 3', + summary: 'Suggested investigation 3 summary', + doi: 'doi3', + score: 0.5, + }, + { + id: 4, + visitId: 'visitId', + name: 'Suggested investigation 4 name', + title: 'Suggested investigation 4', + summary: 'Suggested investigation 4 summary', + doi: 'doi4', + score: 0.4, + }, + { + id: 1, + visitId: 'visitId', + name: 'Suggested investigation 1 name', + title: 'Suggested investigation 1', + summary: 'Suggested investigation 1 summary', + doi: 'doi1', + score: 0.1, + }, + ]); + }); + }); }); diff --git a/packages/datagateway-common/src/api/investigations.tsx b/packages/datagateway-common/src/api/investigations.tsx index 83fd6fb40..fe99fc792 100644 --- a/packages/datagateway-common/src/api/investigations.tsx +++ b/packages/datagateway-common/src/api/investigations.tsx @@ -11,6 +11,7 @@ import { FiltersType, Investigation, SortType, + SuggestedInvestigation, } from '../app.types'; import { StateType } from '../state/app.types'; import { @@ -542,3 +543,40 @@ export const downloadInvestigation = ( link.click(); link.remove(); }; + +export const useSimilarInvestigations = ({ + investigation, +}: { + investigation: Investigation; +}): UseQueryResult => { + // TODO: Remove this hardcoded URL + const baseUrl = 'http://172.16.103.71:4001/api'; + + return useQuery< + SuggestedInvestigation[], + AxiosError, + SuggestedInvestigation[], + ['similarInvestigations', Investigation['id']] + >( + ['similarInvestigations', investigation.id], + () => + axios + .get( + `/v2/similar/doc/?id=${investigation.id}`, + { + baseURL: baseUrl, + } + ) + .then((response) => response.data), + { + select: (data) => { + data.sort((resultA, resultB) => resultB.score - resultA.score); + return data; + }, + onError: (error) => { + handleICATError(error); + }, + retry: retryICATErrors, + } + ); +}; diff --git a/packages/datagateway-common/src/app.types.tsx b/packages/datagateway-common/src/app.types.tsx index 4f9b5c34c..6b11aaa50 100644 --- a/packages/datagateway-common/src/app.types.tsx +++ b/packages/datagateway-common/src/app.types.tsx @@ -74,6 +74,18 @@ export interface InvestigationInstrument { investigation?: Investigation; } +export interface SuggestedInvestigation { + doi: string; + name: string; + title: string; + id: number; + summary: string; + visitId: string; + startDate?: string; + endDate?: string; + score: number; +} + export interface Instrument { id: number; name: string; diff --git a/packages/datagateway-dataview/public/res/default.json b/packages/datagateway-dataview/public/res/default.json index 6f40ef68a..f4883ed96 100644 --- a/packages/datagateway-dataview/public/res/default.json +++ b/packages/datagateway-dataview/public/res/default.json @@ -144,6 +144,19 @@ }, "type": { "id": "Type ID" + }, + "landingPage": { + "extraInfo": "Extra info", + "similarInvestigations": "Similar investigations", + "findingSimilarInvestigations": "Finding similar investigations…", + "noSuggestion": "No suggestion available", + "similarInvestigationListPaginationLabel": { + "pageButton": "Go to page {{page}} of similar investigations", + "firstPageButton": "Go to the first page of similar investigations", + "lastPageButton": "Go to the last page of similar investigations", + "nextButton": "Go to the next page of similar investigations", + "prevButton": "Go to the previous page of similar investigations" + } } }, "datasets": { diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx index 80b9105de..165049322 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx @@ -5,8 +5,7 @@ import configureStore from 'redux-mock-store'; import { StateType } from '../../../state/app.types'; import { dGCommonInitialState, - useInvestigation, - useInvestigationSizes, + SuggestedInvestigation, } from 'datagateway-common'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; @@ -21,17 +20,46 @@ import { } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event/setup/setup'; import userEvent from '@testing-library/user-event'; - -jest.mock('datagateway-common', () => { - const originalModule = jest.requireActual('datagateway-common'); - - return { - __esModule: true, - ...originalModule, - useInvestigation: jest.fn(), - useInvestigationSizes: jest.fn(), - }; -}); +import axios, { AxiosResponse } from 'axios'; + +const mockSuggestions: SuggestedInvestigation[] = [ + { + id: 1, + visitId: 'visitId', + name: 'Suggested investigation 1 name', + title: 'Suggested investigation 1', + summary: 'Suggested investigation 1 summary', + doi: 'doi1', + score: 0.9, + }, + { + id: 2, + visitId: 'visitId', + name: 'Suggested investigation 2 name', + title: 'Suggested investigation 2', + summary: 'Suggested investigation 2 summary', + doi: 'doi2', + score: 0.9, + }, + { + id: 3, + visitId: 'visitId', + name: 'Suggested investigation 3 name', + title: 'Suggested investigation 3', + summary: 'Suggested investigation 3 summary', + doi: 'doi3', + score: 0.9, + }, + { + id: 4, + visitId: 'visitId', + name: 'Suggested investigation 4 name', + title: 'Suggested investigation 4', + summary: 'Suggested investigation 4 summary', + doi: 'doi4', + score: 0.9, + }, +]; describe('ISIS Investigation Landing page', () => { const mockStore = configureStore([thunk]); @@ -182,6 +210,26 @@ describe('ISIS Investigation Landing page', () => { const noSamples: never[] = []; const noPublication: never[] = []; + const mockAxiosGet = ( + url: string + ): Promise> | undefined => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: initialData, + }); + } + + if (url.includes('/user/getSize')) { + return Promise.resolve({ data: 1 }); + } + + if (url.includes('/similar')) { + return Promise.resolve({ + data: mockSuggestions, + }); + } + }; + beforeEach(() => { state = JSON.parse( JSON.stringify({ @@ -192,22 +240,14 @@ describe('ISIS Investigation Landing page', () => { history = createMemoryHistory(); user = userEvent.setup(); - (useInvestigation as jest.Mock).mockReturnValue({ - data: initialData, - }); - (useInvestigationSizes as jest.Mock).mockReturnValue([ - { - isSuccess: true, - data: 1, - }, - ]); + axios.get = jest.fn().mockImplementation(mockAxiosGet); }); afterEach(() => { jest.clearAllMocks(); }); - it('renders landing for investigation correctly', () => { + it('renders landing for investigation correctly', async () => { renderComponent(); // branding should be visible @@ -218,7 +258,7 @@ describe('ISIS Investigation Landing page', () => { expect(screen.getByText('doi_constants.branding.body')).toBeInTheDocument(); // investigation details should be visible - expect(screen.getByText('Test title 1')).toBeInTheDocument(); + expect(await screen.findByText('Test title 1')).toBeInTheDocument(); expect(screen.getByText('foo bar')).toBeInTheDocument(); // publisher section should be visible @@ -299,14 +339,36 @@ describe('ISIS Investigation Landing page', () => { name: 'buttons.add_to_cart', }) ).toBeInTheDocument(); + + await user.click( + screen.getByRole('button', { + name: 'investigations.landingPage.similarInvestigations', + }) + ); + + expect( + await screen.findByRole('link', { + name: 'Suggested investigation 1', + }) + ).toHaveAttribute('href', 'https://doi.org/doi1'); + expect( + await screen.findByRole('link', { + name: 'Suggested investigation 2', + }) + ).toHaveAttribute('href', 'https://doi.org/doi2'); + expect( + await screen.findByRole('link', { + name: 'Suggested investigation 3', + }) + ).toHaveAttribute('href', 'https://doi.org/doi3'); }); describe('renders datasets for the investigation correctly', () => { - it('for facility cycle hierarchy and normal view', () => { + it('for facility cycle hierarchy and normal view', async () => { renderComponent(); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1' @@ -329,13 +391,13 @@ describe('ISIS Investigation Landing page', () => { ).toBeInTheDocument(); }); - it('for facility cycle hierarchy and card view', () => { + it('for facility cycle hierarchy and card view', async () => { history.replace('/?view=card'); renderComponent(); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1?view=card' @@ -358,11 +420,11 @@ describe('ISIS Investigation Landing page', () => { ).toBeInTheDocument(); }); - it('for data publication hierarchy and normal view', () => { + it('for data publication hierarchy and normal view', async () => { renderComponent(true); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1' @@ -385,13 +447,13 @@ describe('ISIS Investigation Landing page', () => { ).toBeInTheDocument(); }); - it('for data publication hierarchy and card view', () => { + it('for data publication hierarchy and card view', async () => { history.push('/?view=card'); renderComponent(true); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1?view=card' @@ -479,9 +541,15 @@ describe('ISIS Investigation Landing page', () => { }); it('users displayed correctly', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], investigationUsers: investigationUser }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [{ ...initialData[0], investigationUsers: investigationUser }], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect( @@ -496,9 +564,15 @@ describe('ISIS Investigation Landing page', () => { }); it('renders text "No samples" when no data is present', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], samples: noSamples }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [{ ...initialData[0], samples: noSamples }], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect( @@ -507,9 +581,15 @@ describe('ISIS Investigation Landing page', () => { }); it('renders text "No publications" when no data is present', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], publications: noPublication }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [{ ...initialData[0], publications: noPublication }], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect( @@ -520,9 +600,15 @@ describe('ISIS Investigation Landing page', () => { }); it('publications displayed correctly', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], publications: publication }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [{ ...initialData[0], publications: publication }], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect( @@ -531,18 +617,30 @@ describe('ISIS Investigation Landing page', () => { }); it('samples displayed correctly', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], samples: sample }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [{ ...initialData[0], samples: sample }], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect(await screen.findByText('Sample')).toBeInTheDocument(); }); it('displays citation correctly when study missing', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], studyInvestigations: undefined }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [{ ...initialData[0], studyInvestigations: undefined }], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect( @@ -553,9 +651,17 @@ describe('ISIS Investigation Landing page', () => { }); it('displays citation correctly with one user', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], investigationUsers: [investigationUser[0]] }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [ + { ...initialData[0], investigationUsers: [investigationUser[0]] }, + ], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect( @@ -566,9 +672,15 @@ describe('ISIS Investigation Landing page', () => { }); it('displays citation correctly with multiple users', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], investigationUsers: investigationUser }], + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [{ ...initialData[0], investigationUsers: investigationUser }], + }); + } + return mockAxiosGet(url); }); + renderComponent(); expect( @@ -592,4 +704,29 @@ describe('ISIS Investigation Landing page', () => { await screen.findByRole('link', { name: 'Data Publication Pid' }) ).toHaveAttribute('href', 'https://doi.org/Data Publication Pid'); }); + + it('hides suggested section when the investigation does not have a summary', async () => { + axios.get = jest.fn().mockImplementation((url: string) => { + if (url.includes('/investigations')) { + return Promise.resolve({ + data: [ + { + ...initialData[0], + summary: undefined, + }, + ], + }); + } + return mockAxiosGet(url); + }); + + renderComponent(); + + expect(await screen.findByText('Test title 1')).toBeInTheDocument(); + expect( + screen.queryByRole('button', { + name: 'investigations.landingPage.similarInvestigations', + }) + ).toBeNull(); + }); }); diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx index d296e6150..aa93adc75 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx @@ -7,6 +7,9 @@ import { Tab, Tabs, Typography, + Accordion, + AccordionDetails, + AccordionSummary, } from '@mui/material'; import { Assessment, @@ -16,6 +19,7 @@ import { Public, Save, Storage, + ExpandMore, } from '@mui/icons-material'; import { Dataset, @@ -39,6 +43,7 @@ import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; import CitationFormatter from '../../citationFormatter.component'; import Branding from './isisBranding.component'; +import SuggestedInvestigationsSection from './suggestedInvestigationsSection.component'; const Subheading = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), @@ -415,27 +420,37 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { {/* Short format information */} - {shortInfo.map( - (field, i) => - data?.[0] && - field.content(data[0] as Investigation) && ( - - - {field.icon} - {field.label}: - - - - {field.content(data[0] as Investigation)} - - - - ) + {data && data[0]?.summary && ( + )} + + }> + {t('investigations.landingPage.extraInfo')} + + + {shortInfo.map( + (field, i) => + data?.[0] && + field.content(data[0] as Investigation) && ( + + + {field.icon} + {field.label}: + + + + {field.content(data[0] as Investigation)} + + + + ) + )} + + {/* Actions */} { + function Wrapper({ children }: { children: React.ReactNode }): JSX.Element { + return ( + + {children} + + ); + } + + it('should render a paginated list of suggested investigations for the given investigation', async () => { + axios.get = jest.fn().mockImplementation( + (): Promise>> => + Promise.resolve({ + data: mockSuggestions, + }) + ); + + const user = userEvent.setup(); + + render( + , + { wrapper: Wrapper } + ); + + await user.click( + await screen.findByRole('button', { + name: 'investigations.landingPage.similarInvestigations', + }) + ); + + const suggestionLinks = await screen.findAllByRole('link'); + expect(suggestionLinks).toHaveLength(5); + + expect( + screen.getByRole('link', { name: 'Suggested investigation 1' }) + ).toHaveAttribute('href', 'https://doi.org/doi1'); + expect( + screen.getByRole('link', { name: 'Suggested investigation 2' }) + ).toHaveAttribute('href', 'https://doi.org/doi2'); + expect( + screen.getByRole('link', { name: 'Suggested investigation 3' }) + ).toHaveAttribute('href', 'https://doi.org/doi3'); + expect( + screen.getByRole('link', { name: 'Suggested investigation 4' }) + ).toHaveAttribute('href', 'https://doi.org/doi4'); + expect( + screen.getByRole('link', { name: 'Suggested investigation 5' }) + ).toHaveAttribute('href', 'https://doi.org/doi5'); + expect( + screen.queryByRole('link', { name: 'Suggested investigation 6' }) + ).toBeNull(); + + // go to the next page + await user.click( + screen.getByRole('button', { + name: 'investigations.landingPage.similarInvestigationListPaginationLabel.nextButton', + }) + ); + + expect(await screen.findAllByRole('link')).toHaveLength(1); + expect( + screen.getByRole('link', { name: 'Suggested investigation 6' }) + ).toBeInTheDocument(); + }); + + it('should show loading label and be un-expandable when fetching suggestions', () => { + axios.get = jest.fn().mockImplementation( + () => + new Promise((_) => { + // never resolve the promise to pretend the query is loading + }) + ); + + render( + , + { wrapper: Wrapper } + ); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect( + screen.getByRole('button', { + name: 'investigations.landingPage.findingSimilarInvestigations', + }) + ).toHaveAttribute('aria-disabled', 'true'); + }); + + it('should show empty message when no suggestion is available for the investigation', async () => { + axios.get = jest.fn().mockImplementation( + (): Promise>> => + Promise.resolve({ + data: [], + }) + ); + + const user = userEvent.setup(); + + render( + , + { wrapper: Wrapper } + ); + + await user.click( + await screen.findByRole('button', { + name: 'investigations.landingPage.similarInvestigations', + }) + ); + + expect( + await screen.findByText('investigations.landingPage.noSuggestion') + ).toBeInTheDocument(); + }); +}); diff --git a/packages/datagateway-dataview/src/views/landing/isis/suggestedInvestigationsSection.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/suggestedInvestigationsSection.component.tsx new file mode 100644 index 000000000..3b4debcbd --- /dev/null +++ b/packages/datagateway-dataview/src/views/landing/isis/suggestedInvestigationsSection.component.tsx @@ -0,0 +1,208 @@ +import { ExpandMore } from '@mui/icons-material'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + CircularProgress, + Divider, + List, + ListItem, + Pagination, + Stack, + Typography, + Chip, + Box, + Link, +} from '@mui/material'; +import React from 'react'; +import { + Investigation, + SuggestedInvestigation, + useSimilarInvestigations, +} from 'datagateway-common'; +import { useTranslation } from 'react-i18next'; +import { blue } from '@mui/material/colors'; + +interface SuggestedSectionProps { + investigation: Investigation; +} + +const SUGGESTION_COUNT_PER_PAGE = 5; + +function SuggestedInvestigationsSection({ + investigation, +}: SuggestedSectionProps): JSX.Element { + const [t] = useTranslation(); + const { data: suggestedResults, isLoading } = useSimilarInvestigations({ + investigation, + }); + + return ( + + : } + > + + {isLoading + ? t('investigations.landingPage.findingSimilarInvestigations') + : t('investigations.landingPage.similarInvestigations')} + + + + {suggestedResults && suggestedResults.length > 0 ? ( + + ) : ( + + {t('investigations.landingPage.noSuggestion')} + + )} + + + ); +} + +function ScoreChip({ + score, + lowestScore, + highestScore, + id, +}: { + score: number; + lowestScore: number; + highestScore: number; + id: number; +}): JSX.Element { + const relevanceScore = + lowestScore === highestScore + ? 1 + : Math.round( + ((score - lowestScore) / (highestScore - lowestScore)) * 10 + ) / 10; + const colorShade = Math.max( + relevanceScore * 1000 - 100, + 50 + ) as keyof typeof blue; + const chipColour = blue[`${colorShade}`]; + const suggestionScore = Math.round(score * 100); + return ( + theme.palette.getContrastText(chipColour), + backgroundColor: chipColour, + }} + /> + ); +} + +function SuggestionList({ + suggestions, +}: { + suggestions: SuggestedInvestigation[]; +}): JSX.Element { + const [t] = useTranslation(); + const [currentPage, setCurrentPage] = React.useState(1); + + const pageCount = Math.ceil(suggestions.length / SUGGESTION_COUNT_PER_PAGE); + + function constructFullDoiUrl(doi: string): string { + return `https://doi.org/${doi}`; + } + + function changeCurrentPage( + _event: React.ChangeEvent, + newPageNumber: number + ): void { + setCurrentPage(newPageNumber); + } + + const paginationOffset = (currentPage - 1) * SUGGESTION_COUNT_PER_PAGE; + const displayedSuggestions = suggestions.slice( + paginationOffset, + paginationOffset + SUGGESTION_COUNT_PER_PAGE + ); + + const lowestScore = Math.min(...suggestions.map((s) => s.score)); + const highestScore = Math.max(...suggestions.map((s) => s.score)); + + return ( + + { + switch (type) { + case 'page': + return t( + 'investigations.landingPage.similarInvestigationListPaginationLabel.pageButton', + { page } + ); + + case 'first': + return t( + 'investigations.landingPage.similarInvestigationListPaginationLabel.firstPageButton' + ); + + case 'last': + return t( + 'investigations.landingPage.similarInvestigationListPaginationLabel.lastPageButton' + ); + + case 'next': + return t( + 'investigations.landingPage.similarInvestigationListPaginationLabel.nextButton' + ); + + case 'previous': + return t( + 'investigations.landingPage.similarInvestigationListPaginationLabel.prevButton' + ); + } + }} + shape="rounded" + size="small" + sx={{ pb: 2 }} + /> + + {displayedSuggestions.map((suggestion, i) => { + const isLastItem = i === SUGGESTION_COUNT_PER_PAGE - 1; + return ( + + + + {suggestion.doi ? ( + + {suggestion.title} + + ) : ( + {suggestion.title} + )} + + + + {!isLastItem && } + + ); + })} + + + ); +} + +export default SuggestedInvestigationsSection;