From dc9bd7f9cd9ba698fe04479c62a685b535a83ab1 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Mon, 16 Dec 2024 14:49:01 +0000 Subject: [PATCH 1/7] add query term filtering to get user publications function and standardise response format --- .../__tests__/getUserPublications.test.ts | 29 ++++++++++++++----- .../components/user/schema/getPublications.ts | 4 +++ api/src/components/user/service.ts | 17 +++++++++-- api/src/lib/interface.ts | 1 + 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/api/src/components/user/__tests__/getUserPublications.test.ts b/api/src/components/user/__tests__/getUserPublications.test.ts index 2e6611759..71bd6bfaf 100644 --- a/api/src/components/user/__tests__/getUserPublications.test.ts +++ b/api/src/components/user/__tests__/getUserPublications.test.ts @@ -12,9 +12,9 @@ describe("Get a given user's publications", () => { .query({ apiKey: 123456789, offset: 0, limit: 100 }); expect(publications.status).toEqual(200); - expect(publications.body.results.length).toEqual(25); + expect(publications.body.data.length).toEqual(25); expect( - publications.body.results.some( + publications.body.data.some( (publication) => publication.versions.some((version) => version.currentStatus === 'DRAFT') as boolean ) ).toEqual(true); @@ -24,9 +24,9 @@ describe("Get a given user's publications", () => { const publications = await testUtils.agent.get('/users/test-user-1/publications'); expect(publications.status).toEqual(200); - expect(publications.body.results.length).toEqual(10); + expect(publications.body.data.length).toEqual(10); expect( - publications.body.results.some( + publications.body.data.some( (publication) => publication.versions.some((version) => version.currentStatus === 'DRAFT') as boolean ) ).toEqual(false); @@ -36,9 +36,9 @@ describe("Get a given user's publications", () => { const publications = await testUtils.agent.get('/users/test-user-1/publications').query({ apiKey: 987654321 }); expect(publications.status).toEqual(200); - expect(publications.body.results.length).toEqual(10); + expect(publications.body.data.length).toEqual(10); expect( - publications.body.results.some( + publications.body.data.some( (publication) => publication.versions.some((version) => version.currentStatus === 'DRAFT') as boolean ) ).toEqual(false); @@ -49,8 +49,23 @@ describe("Get a given user's publications", () => { .get('/users/user-does-not-exist/publications') .query({ apiKey: 987654321 }); - expect(publications.body.results).toBe(undefined); + expect(publications.body.data).toBe(undefined); expect(publications.body.message).toBe('User not found'); expect(publications.status).toEqual(400); }); + + test('Results can be filtered by a query term', async () => { + const queryTerm = 'interpretation'; + const publications = await testUtils.agent.get('/users/test-user-1/publications').query({ query: queryTerm }); + + expect(publications.status).toEqual(200); + expect(publications.body.data.length).toEqual(1); + expect( + publications.body.data.every((publication) => + publication.versions.some( + (version) => version.isLatestLiveVersion && version.title.toLowerCase().includes(queryTerm) + ) + ) + ).toEqual(true); + }); }); diff --git a/api/src/components/user/schema/getPublications.ts b/api/src/components/user/schema/getPublications.ts index 0cdd76db2..fc6e7006f 100644 --- a/api/src/components/user/schema/getPublications.ts +++ b/api/src/components/user/schema/getPublications.ts @@ -13,6 +13,10 @@ const getPublicationsSchema: I.JSONSchemaType = { minimum: 1, default: 10 }, + query: { + type: 'string', + nullable: true + }, versionStatus: { type: 'string', nullable: true diff --git a/api/src/components/user/service.ts b/api/src/components/user/service.ts index 796e0d7c4..bf0aff8f1 100644 --- a/api/src/components/user/service.ts +++ b/api/src/components/user/service.ts @@ -250,7 +250,20 @@ export const getPublications = async ( } : {} : // But if the user is not the owner, get only publications that have a published version - { versions: { some: { isLatestLiveVersion: true } } }) + { versions: { some: { isLatestLiveVersion: true } } }), + // And, if a query is supplied, where the query matches the latest live title. + ...(params.query + ? { + versions: { + some: { + isLatestLiveVersion: true, + title: { + search: params.query + ':*' + } + } + } + } + : {}) }; const userPublications = await client.prisma.publication.findMany({ @@ -391,7 +404,7 @@ export const getPublications = async ( } }); - return { offset, limit, total: totalUserPublications, results: sortedPublications }; + return { data: sortedPublications, metadata: { offset, limit, total: totalUserPublications } }; }; export const getUserList = async () => { diff --git a/api/src/lib/interface.ts b/api/src/lib/interface.ts index 6ed5c94c5..1524d41b5 100644 --- a/api/src/lib/interface.ts +++ b/api/src/lib/interface.ts @@ -733,6 +733,7 @@ export interface UpdateAffiliationsBody { export interface UserPublicationsFilters { offset: number; limit: number; + query?: string; versionStatus?: string; } From 21dc33029fd796b32e2c93f0af70e0c618475841 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Tue, 17 Dec 2024 10:29:25 +0000 Subject: [PATCH 2/7] extract search interface out of page, use it to show author publications; move some search components --- .../LoggedIn/livePublication.e2e.spec.ts | 22 -- e2e/tests/LoggedOut/profile.e2e.spec.ts | 101 +++---- .../__tests__/components/SearchPage.test.tsx | 66 +++-- ui/src/components/Search/Interface/index.tsx | 254 ++++++++++++++++++ ui/src/components/Search/Page/index.tsx | 61 +++++ .../{SearchResult => Search/Result}/index.tsx | 0 .../Toggle}/Desktop/index.tsx | 0 .../Toggle}/Mobile/index.tsx | 0 .../{SearchToggle => Search/Toggle}/index.tsx | 4 +- ui/src/components/SearchPage/index.tsx | 253 ----------------- ui/src/components/index.tsx | 11 +- ui/src/lib/helpers.tsx | 11 + ui/src/lib/interfaces.ts | 9 +- ui/src/pages/account.tsx | 6 +- ui/src/pages/authors/[id]/index.tsx | 210 +++++++-------- ui/src/pages/search/authors/index.tsx | 29 +- ui/src/pages/search/organisations/index.tsx | 23 +- ui/src/pages/search/publications/index.tsx | 54 +--- ui/src/pages/search/topics/index.tsx | 1 + 19 files changed, 554 insertions(+), 561 deletions(-) create mode 100644 ui/src/components/Search/Interface/index.tsx create mode 100644 ui/src/components/Search/Page/index.tsx rename ui/src/components/{SearchResult => Search/Result}/index.tsx (100%) rename ui/src/components/{SearchToggle => Search/Toggle}/Desktop/index.tsx (100%) rename ui/src/components/{SearchToggle => Search/Toggle}/Mobile/index.tsx (100%) rename ui/src/components/{SearchToggle => Search/Toggle}/index.tsx (67%) delete mode 100644 ui/src/components/SearchPage/index.tsx diff --git a/e2e/tests/LoggedIn/livePublication.e2e.spec.ts b/e2e/tests/LoggedIn/livePublication.e2e.spec.ts index f7292da5a..93b322899 100644 --- a/e2e/tests/LoggedIn/livePublication.e2e.spec.ts +++ b/e2e/tests/LoggedIn/livePublication.e2e.spec.ts @@ -102,28 +102,6 @@ test.describe('Live Publication', () => { await testFlagging(page, 'cl3fz14dr0001es6i5ji51rq4', 'testing the flagging functionality'); }); - test('Author profile', async ({ browser }) => { - const page = await Helpers.users.getPageAsUser(browser); - await page.goto(`/publications/publication-user-6-hypothesis-1-live`, { waitUntil: 'domcontentloaded' }); - - // Check and click author link - await page.getByRole('link', { name: 'G. Murphy' }).click(); - await page.waitForURL(`/authors/test-user-6-grace-murphy`); - - // Check name - await expect(page.locator(PageModel.authorInfo.name)).toBeVisible(); - - // Check ORCID data sections - for await (const orcidDataSection of PageModel.profilePage.orcidDataSections) { - await expect(page.locator(orcidDataSection)).toBeVisible(); - } - - // Check Author publications section - await page.locator(PageModel.profilePage.showAll).click(); - await page.waitForSelector(PageModel.profilePage.result); - await expect(page.locator(PageModel.profilePage.result)).toBeVisible(); - }); - test('Download pdf/json', async ({ browser, headless }) => { const page = await Helpers.users.getPageAsUser(browser); await page.goto(`/publications/cl3fz14dr0001es6i5ji51rq4`, { waitUntil: 'domcontentloaded' }); diff --git a/e2e/tests/LoggedOut/profile.e2e.spec.ts b/e2e/tests/LoggedOut/profile.e2e.spec.ts index c6ce45db1..e9a1e67c8 100644 --- a/e2e/tests/LoggedOut/profile.e2e.spec.ts +++ b/e2e/tests/LoggedOut/profile.e2e.spec.ts @@ -1,65 +1,76 @@ -import { expect, Page, test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { PageModel } from '../PageModel'; -test.describe('Octopus profile', () => { - let page: Page; +test.describe('User profiles', () => { + test('Visit an author profile', async ({ browser }) => { + const page = await browser.newPage(); + await page.goto(`/publications/publication-user-6-hypothesis-1-live`, { waitUntil: 'domcontentloaded' }); - test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); + // Check and click author link + await page.getByRole('link', { name: 'G. Murphy' }).click(); + await page.waitForURL(`/authors/test-user-6-grace-murphy`); + + // Check name + await expect(page.locator(PageModel.authorInfo.name)).toBeVisible(); + + // Check ORCID data sections + for await (const orcidDataSection of PageModel.profilePage.orcidDataSections) { + await expect(page.locator(orcidDataSection)).toBeVisible(); + } + + // Check publications section + await expect(page.locator(PageModel.organisationalUserInfo.octopusPublications)).toBeVisible(); + }); + + test("Explore a user's publications", async ({ browser }) => { + const page = await browser.newPage(); // navigate to Octopus profile page await page.goto(`/authors/octopus`, { waitUntil: 'domcontentloaded' }); await expect(page).toHaveTitle('Author: Octopus - Octopus | Built for Researchers'); - }); - - test.afterAll(async () => { - await page.close(); - }); - - test('Octopus publications pagination', async () => { - // check user name + // Check user name. await expect(page.locator('h1')).toHaveText('Octopus'); - // check Octopus publications section const octopusPublicationsHeader = page.locator(PageModel.organisationalUserInfo.octopusPublications); - await expect(octopusPublicationsHeader).toBeVisible(); - - await page.waitForLoadState('networkidle'); - const octopusPublicationsSection = octopusPublicationsHeader.locator('xpath=..'); - // initially, only 10 publications should be visible - expect(await octopusPublicationsSection.locator('a').count()).toEqual(10); - - // press "Show More" button to see more publications - await expect(page.locator("'Show More'")).toBeVisible(); - await Promise.all([ - page.waitForResponse( - (response) => response.url().includes('/users/octopus/publications?offset=10&limit=10') && response.ok() - ), - page.click("'Show More'") - ]); - - // wait for publications to be rendered - 50ms per each - await page.waitForTimeout(500); + // Initially, 20 publications should be visible. + await expect(await octopusPublicationsSection.locator('a').count()).toEqual(20); + await expect(page.getByText(/Showing 1 - 20 of \d+/)).toBeVisible(); - // the next 10 pubs should be loaded - expect(await octopusPublicationsSection.locator('a').count()).toEqual(20); + // Change page size. + await page.getByLabel('Showing').selectOption('10'); + await page.waitForResponse( + (response) => + response.request().method() === 'GET' && + response.url().includes('/users/octopus/publications?offset=0&limit=10') + ); + await expect(await octopusPublicationsSection.locator('a').count()).toEqual(10); + await expect(page.getByText(/Showing 1 - 10 of \d+/)).toBeVisible(); - // press "Show More" button again - await Promise.all([ - page.waitForResponse( - (response) => response.url().includes('/users/octopus/publications?offset=20&limit=10') && response.ok() - ), - page.click("'Show More'") - ]); + // Change page. + await page.getByLabel('Next').click(); + await page.waitForResponse( + (response) => + response.request().method() === 'GET' && + response.url().includes('/users/octopus/publications?offset=10&limit=10') + ); + await expect(page.getByText(/Showing 11 - 20 of \d+/)).toBeVisible(); - // wait for publications to be rendered - 50ms per each - await page.waitForTimeout(500); + // Enter a query term and filter results. + await page.getByLabel('Quick search').fill('muco-cutaneous'); + await octopusPublicationsSection.getByRole('button', { name: 'Search' }).click(); + await page.waitForResponse( + (response) => + response.request().method() === 'GET' && + response.url().includes('/users/octopus/publications?offset=0&limit=10&query=muco-cutaneous') + ); - // 30 publications should now be visible in the UI - expect(await octopusPublicationsSection.locator('a').count()).toEqual(30); + // Expect 1 result and disabled prev/next buttons. + await expect(await octopusPublicationsSection.locator('a').count()).toEqual(1); + await expect(page.getByLabel('Previous')).toBeDisabled(); + await expect(page.getByLabel('Next')).toBeDisabled(); }); }); diff --git a/ui/src/__tests__/components/SearchPage.test.tsx b/ui/src/__tests__/components/SearchPage.test.tsx index 2e1e53552..5cd796e07 100644 --- a/ui/src/__tests__/components/SearchPage.test.tsx +++ b/ui/src/__tests__/components/SearchPage.test.tsx @@ -3,7 +3,6 @@ import * as TestUtils from '@/testUtils'; import { render, screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; -import { resolve } from 'path'; jest.mock('next/router', () => ({ useRouter: jest.fn() @@ -36,26 +35,10 @@ describe('Basic search page', () => { expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Search results'); }); - it('Search type select is present', () => { - expect(screen.getByRole('combobox', { name: 'Searching' })).toBeInTheDocument(); - }); - - it('Search type select has expected options', () => { - const searchTypeSelect = screen.getByRole('combobox', { name: 'Searching' }); - expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Publications' })); - expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Authors' })); - expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Topics' })); - expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Organisations' })); - expect(searchTypeSelect.children).toHaveLength(4); - }); - - it('Search type select has value "publications"', () => { - expect(screen.getByRole('combobox', { name: 'Searching' })).toHaveValue('publications'); + it('Search type select is not present', () => { + expect(screen.queryByRole('combobox', { name: 'Searching' })).not.toBeInTheDocument(); }); - // TODO: test that changing search type select calls useRouter's push with the appropriate path. - // Couldn't get this to work. - it('Page size select is present', () => { expect(screen.getByRole('combobox', { name: 'Showing' })).toBeInTheDocument(); }); @@ -110,6 +93,51 @@ describe('Basic search page', () => { }); }); +describe('Search page with search type select', () => { + const handleSearchFormSubmit = jest.fn((e) => { + e.preventDefault(); + }); + const setLimit = jest.fn(); + + beforeEach(() => { + render( + + ); + }); + + it('Search type select is present', () => { + expect(screen.getByRole('combobox', { name: 'Searching' })).toBeInTheDocument(); + }); + + it('Search type select has expected options', () => { + const searchTypeSelect = screen.getByRole('combobox', { name: 'Searching' }); + expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Publications' })); + expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Authors' })); + expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Topics' })); + expect(searchTypeSelect).toContainElement(screen.getByRole('option', { name: 'Organisations' })); + expect(searchTypeSelect.children).toHaveLength(4); + }); + + it('Search type select has value "publications"', () => { + expect(screen.getByRole('combobox', { name: 'Searching' })).toHaveValue('publications'); + }); + + // TODO: test that changing search type select calls useRouter's push with the appropriate path. + // Couldn't get this to work. +}); + describe('Search page with filters', () => { const resetFilters = jest.fn(); diff --git a/ui/src/components/Search/Interface/index.tsx b/ui/src/components/Search/Interface/index.tsx new file mode 100644 index 000000000..634b77ceb --- /dev/null +++ b/ui/src/components/Search/Interface/index.tsx @@ -0,0 +1,254 @@ +import React from 'react'; +import * as Framer from 'framer-motion'; +import * as Router from 'next/router'; +import * as SolidIcons from '@heroicons/react/24/solid'; + +import * as Components from '@/components'; +import * as Helpers from '@/helpers'; +import * as Interfaces from '@/interfaces'; +import * as Types from '@/types'; + +type SearchResults = + | Interfaces.PublicationVersion[] + | Interfaces.CoreUser[] + | Pick[]; +type Props = { + error?: string; + filters?: React.ReactNode; + handleSearchFormSubmit: React.ReactEventHandler; + isValidating: boolean; + limit: number; + noResultsMessage?: string; + offset: number; + pageSizes?: number[]; + query: string | null; + resetFilters?: () => void; + results: SearchResults; + searchType: Types.SearchType; + setLimit: (limit: React.SetStateAction) => void; + setOffset: (offset: React.SetStateAction) => void; + showSearchTypeSwitch?: boolean; + total: number; +}; + +const SearchInterface = React.forwardRef( + (props: Props, searchInputRef: React.ForwardedRef): React.ReactElement => { + const router = Router.useRouter(); + const upperPageBound = props.limit + props.offset > props.total ? props.total : props.limit + props.offset; + + return ( +
+
+
+ Search options + + {props.showSearchTypeSwitch && ( + + )} + +
+
+ +
+
+ {props.filters && ( + + )} +
+
+ {props.error ? props.error : `${props.total} result${props.total !== 1 ? 's' : ''}`} +
+ {props.error ? ( + + ) : ( + + {!props.error && props.total === 0 && !props.isValidating && ( + + )} + {props.results.length ? ( +
+ {props.results.map((result, index: number) => { + let classes = ''; + + if (index === 0) { + classes += 'rounded-t'; + } + + if (index === props.results.length - 1) { + classes += '!border-b-transparent !rounded-b'; + } + + return props.searchType === 'publication-versions' ? ( + + ) : props.searchType === 'authors' || props.searchType === 'organisations' ? ( + + ) : props.searchType == 'topics' ? ( + + ) : ( + <> + ); + })} +
+ ) : null} + {!props.isValidating && props.total > 0 && ( + +
+ { + props.setOffset(props.offset - props.limit); + Helpers.scrollTopSmooth(); + }} + disabled={props.offset === 0} + title="Previous" + /> + { + props.setOffset(props.offset + props.limit); + Helpers.scrollTopSmooth(); + }} + disabled={props.limit + props.offset >= props.total} + title="Next" + /> +
+ + Showing {props.offset + 1} - {upperPageBound} of {props.total} + +
+ )} +
+ )} +
+
+ ); + } +); +SearchInterface.displayName = 'SearchInterface'; + +export default SearchInterface; diff --git a/ui/src/components/Search/Page/index.tsx b/ui/src/components/Search/Page/index.tsx new file mode 100644 index 000000000..1ceb724f6 --- /dev/null +++ b/ui/src/components/Search/Page/index.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import * as Components from '@/components'; +import * as Interfaces from '@/interfaces'; +import * as Types from '@/types'; + +type SearchResults = + | Interfaces.PublicationVersion[] + | Interfaces.CoreUser[] + | Pick[]; +type Props = { + error?: string; + filters?: React.ReactNode; + handleSearchFormSubmit: React.ReactEventHandler; + isValidating: boolean; + limit: number; + offset: number; + noResultsMessage?: string; + query: string | null; + resetFilters?: () => void; + results: SearchResults; + searchType: Types.SearchType; + setLimit: (limit: React.SetStateAction) => void; + setOffset: (offset: React.SetStateAction) => void; + showSearchTypeSwitch?: boolean; + total: number; +}; + +const SearchPage = React.forwardRef( + (props: Props, searchInputRef: React.ForwardedRef): React.ReactElement => { + return ( + <> +
+ +
+
+ +
+ + ); + } +); + +export default SearchPage; diff --git a/ui/src/components/SearchResult/index.tsx b/ui/src/components/Search/Result/index.tsx similarity index 100% rename from ui/src/components/SearchResult/index.tsx rename to ui/src/components/Search/Result/index.tsx diff --git a/ui/src/components/SearchToggle/Desktop/index.tsx b/ui/src/components/Search/Toggle/Desktop/index.tsx similarity index 100% rename from ui/src/components/SearchToggle/Desktop/index.tsx rename to ui/src/components/Search/Toggle/Desktop/index.tsx diff --git a/ui/src/components/SearchToggle/Mobile/index.tsx b/ui/src/components/Search/Toggle/Mobile/index.tsx similarity index 100% rename from ui/src/components/SearchToggle/Mobile/index.tsx rename to ui/src/components/Search/Toggle/Mobile/index.tsx diff --git a/ui/src/components/SearchToggle/index.tsx b/ui/src/components/Search/Toggle/index.tsx similarity index 67% rename from ui/src/components/SearchToggle/index.tsx rename to ui/src/components/Search/Toggle/index.tsx index de20cc4d4..099595a54 100644 --- a/ui/src/components/SearchToggle/index.tsx +++ b/ui/src/components/Search/Toggle/index.tsx @@ -5,8 +5,8 @@ import * as Components from '@/components'; const Search: React.FC = (): React.ReactElement => { return ( <> - - + + ); }; diff --git a/ui/src/components/SearchPage/index.tsx b/ui/src/components/SearchPage/index.tsx deleted file mode 100644 index a288a0b35..000000000 --- a/ui/src/components/SearchPage/index.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import React from 'react'; -import * as Framer from 'framer-motion'; -import * as Router from 'next/router'; -import * as SolidIcons from '@heroicons/react/24/solid'; - -import * as Components from '@/components'; -import * as Helpers from '@/helpers'; -import * as Interfaces from '@/interfaces'; -import * as Types from '@/types'; - -type Props = { - error?: string; - filters?: React.ReactNode; - handleSearchFormSubmit: React.ReactEventHandler; - isValidating: boolean; - limit: number; - offset: number; - query: string | null; - resetFilters?: () => void; - searchType: Types.SearchType; - setLimit: (limit: React.SetStateAction) => void; - setOffset: (offset: React.SetStateAction) => void; - total: number; -} & ( - | { - results: Interfaces.PublicationVersion[]; - } - | { - results: Interfaces.CoreUser[]; - } - | { - results: Pick[]; - } -); - -const SearchPage = React.forwardRef( - (props: Props, searchInputRef: React.ForwardedRef): React.ReactElement => { - const router = Router.useRouter(); - - const upperPageBound = props.limit + props.offset > props.total ? props.total : props.limit + props.offset; - - return ( - <> -
- -
-
-
-
- Search options - - - - -
-
- -
-
- {props.filters && ( - - )} -
-
- {props.error ? props.error : `${props.total} result${props.total !== 1 ? 's' : ''}`} -
- {props.error ? ( - - ) : ( - - {!props.error && !props.results.length && !props.isValidating && ( - - )} - - {props.results.length && ( - <> -
- {props.results.map((result, index: number) => { - let classes = ''; - - if (index === 0) { - classes += 'rounded-t'; - } - - if (index === props.results.length - 1) { - classes += '!border-b-transparent !rounded-b'; - } - - return props.searchType === 'publication-versions' ? ( - - ) : props.searchType === 'authors' || - props.searchType === 'organisations' ? ( - - ) : props.searchType == 'topics' ? ( - - ) : ( - <> - ); - })} -
- - {!props.isValidating && !!props.results.length && ( - -
- { - props.setOffset(props.offset - props.limit); - Helpers.scrollTopSmooth(); - }} - disabled={props.offset === 0} - title="Previous" - /> - { - props.setOffset(props.offset + props.limit); - Helpers.scrollTopSmooth(); - }} - disabled={props.limit + props.offset >= props.total} - title="Next" - /> -
- - Showing {props.offset + 1} - {upperPageBound} of {props.total} - -
- )} - - )} -
- )} -
-
- - ); - } -); -SearchPage.displayName = 'SearchPage'; - -export default SearchPage; diff --git a/ui/src/components/index.tsx b/ui/src/components/index.tsx index 642fc7594..d6bd8a386 100644 --- a/ui/src/components/index.tsx +++ b/ui/src/components/index.tsx @@ -89,11 +89,12 @@ export { default as RelatedPublicationsViewAllModal } from './Publication/Relate export { default as RelatedPublicationsVotingArea } from './Publication/RelatedPublications/VotingArea'; export { default as RequiredIndicator } from './RequiredIndicator'; export { default as ScrollToTop } from './ScrollToTop'; -export { default as Search } from './SearchToggle'; -export { default as SearchDesktop } from './SearchToggle/Desktop'; -export { default as SearchMobile } from './SearchToggle/Mobile'; -export { default as SearchPage } from './SearchPage'; -export { default as SearchResult } from './SearchResult'; +export { default as Search } from './Search/Toggle'; +export { default as SearchInterface } from './Search/Interface'; +export { default as SearchPage } from './Search/Page'; +export { default as SearchResult } from './Search/Result'; +export { default as SearchToggleDesktop } from './Search/Toggle/Desktop'; +export { default as SearchToggleMobile } from './Search/Toggle/Mobile'; export { default as SectionBreak } from './SectionBreak'; export { default as Tabs } from './Tabs'; export { default as TextEditor } from './TextEditor'; diff --git a/ui/src/lib/helpers.tsx b/ui/src/lib/helpers.tsx index 99613a628..284c0bab6 100644 --- a/ui/src/lib/helpers.tsx +++ b/ui/src/lib/helpers.tsx @@ -649,3 +649,14 @@ export const isPublicationVersionExemptFromReversioning = (publicationVersion: I const isARI = publication.externalSource === 'ARI'; return isPeerReview || isARI; }; + +// Get a nextJS context.query param as a string or null. +export const extractNextQueryParam = (param: string | string[] | undefined, checkNumber?: boolean): string | null => { + const rawValue = Array.isArray(param) ? param[0] : param || null; + if (checkNumber) { + // Return null if value cannot be parsed as a number. + return rawValue && !Number.isNaN(parseInt(rawValue, 10)) ? rawValue : null; + } else { + return rawValue; + } +}; diff --git a/ui/src/lib/interfaces.ts b/ui/src/lib/interfaces.ts index c44b4cc2d..2ea227062 100644 --- a/ui/src/lib/interfaces.ts +++ b/ui/src/lib/interfaces.ts @@ -230,7 +230,7 @@ export interface User extends CoreUser { works: WorksRecord[]; } -export interface SearchResults { +export interface SearchResults { data: T[]; metadata: SearchResultMeta; } @@ -413,13 +413,6 @@ export interface CreationStepWithCompletenessStatus extends CreationStep { status: Types.TabCompletionStatus; } -export interface UserPublicationsResult { - offset: number; - limit: number; - total: number; - results: Publication[]; -} - export interface OrcidAffiliationDate { year: string | null; month: string | null; diff --git a/ui/src/pages/account.tsx b/ui/src/pages/account.tsx index 5a79582c7..e679c1cc2 100644 --- a/ui/src/pages/account.tsx +++ b/ui/src/pages/account.tsx @@ -28,7 +28,7 @@ export const getServerSideProps: Types.GetServerSideProps = Helpers.withServerSe const promises: [ Promise, - Promise, + Promise | void>, Promise ] = [ api @@ -65,7 +65,7 @@ export const getServerSideProps: Types.GetServerSideProps = Helpers.withServerSe type Props = { user: Interfaces.User; - userPublications: Interfaces.UserPublicationsResult; + userPublications: Interfaces.SearchResults; controlRequests: Interfaces.ControlRequest[]; }; @@ -87,7 +87,7 @@ const Account: Types.NextPage = (props): React.ReactElement => { { fallbackData: props.controlRequests, revalidateOnFocus: false } ); - const { data: { results: userPublications = [] } = {} } = useSWR( + const { data: { data: userPublications = [] } = {} } = useSWR>( `${Config.endpoints.users}/${props.user.id}/publications?limit=999`.concat( versionStatusArray.length ? `&versionStatus=${versionStatusArray.join(',')}` : '' ), diff --git a/ui/src/pages/authors/[id]/index.tsx b/ui/src/pages/authors/[id]/index.tsx index 4344cf27e..dcfccb570 100644 --- a/ui/src/pages/authors/[id]/index.tsx +++ b/ui/src/pages/authors/[id]/index.tsx @@ -1,25 +1,26 @@ import React, { useMemo, useState } from 'react'; import Head from 'next/head'; -import useSWRInfinite from 'swr/infinite'; +import useSWR from 'swr'; +import * as Router from 'next/router'; -import * as Framer from 'framer-motion'; -import * as Interfaces from '@/interfaces'; +import * as api from '@/api'; +import * as Assets from '@/assets'; import * as Components from '@/components'; -import * as Layouts from '@/layouts'; import * as Config from '@/config'; -import * as Types from '@/types'; -import * as Assets from '@/assets'; import * as Helpers from '@/helpers'; -import * as api from '@/api'; - -const pageSize = 10; +import * as Interfaces from '@/interfaces'; +import * as Layouts from '@/layouts'; +import * as Types from '@/types'; export const getServerSideProps: Types.GetServerSideProps = async (context) => { const userId = context.query.id; - const userPublicationsUrl = `${Config.endpoints.users}/${userId}/publications?offset=0&limit=${pageSize}`; + const limit = Helpers.extractNextQueryParam(context.query.limit, true); + const offset = Helpers.extractNextQueryParam(context.query.offset, true); + const query = Helpers.extractNextQueryParam(context.query.query); + const userPublicationsUrl = `${Config.endpoints.users}/${userId}/publications?offset=${offset || 0}&limit=${limit || 20}&query=${query || ''}`; const token = Helpers.getJWT(context); let user: Interfaces.User | null = null; - let firstUserPublicationsPage: Interfaces.UserPublicationsResult | null = null; + let publications: Interfaces.SearchResults | undefined = undefined; let error: string | null = null; try { @@ -38,69 +39,102 @@ export const getServerSideProps: Types.GetServerSideProps = async (context) => { try { const response = await api.get(userPublicationsUrl); - firstUserPublicationsPage = response.data; + publications = response.data; } catch (error) { console.log(error); } return { props: { + query, user, userPublicationsUrl, - fallbackData: firstUserPublicationsPage + fallbackData: publications } }; }; type Props = { + query: string | null; user: Interfaces.User; userPublicationsUrl: string; - fallbackData: Interfaces.UserPublicationsResult | null; + fallbackData: Interfaces.SearchResults | undefined; }; const Author: Types.NextPage = (props): React.ReactElement => { - const [hideShowMoreButton, setHideShowMoreButton] = useState(false); - - const { data, setSize } = useSWRInfinite( - (pageIndex, prevPageData) => { - if (pageIndex === 0) { - return props.userPublicationsUrl; - } - - if (prevPageData && !prevPageData.results.length) { - return null; // reached the end - } + const router = Router.useRouter(); + const [query, setQuery] = useState(props.query ? props.query : ''); + const [limit, setLimit] = useState(20); + const [offset, setOffset] = useState(0); + const swrKey = `${Config.endpoints.users}/${props.user.id}/publications?offset=${offset}&limit=${limit}&query=${query}`; + const { + data: response, + error, + isValidating + } = useSWR>(swrKey, null, { + fallbackData: props.fallbackData + }); + const searchInputRef = React.useRef(null); + const missingNames = !props.user.firstName && !props.user.lastName; + const isOrganisationalAccount = props.user.role === 'ORGANISATION'; + const pageTitle = `Author: ${isOrganisationalAccount ? props.user.firstName : props.user.orcid} - ${Config.urls.viewUser.title}`; - return props.userPublicationsUrl.replace('offset=0', `offset=${pageIndex * pageSize}`); - }, - async (url) => { - const response = await api.get(url); - const data = response.data; - const { offset, limit, total } = data; + // The result component expects a publication version. + // The endpoint is expected to return a publication with 1 version (the latest live one). + // So make an array of latest live publication versions from the response. + const publicationVersions: Interfaces.PublicationVersion[] = useMemo( + () => + response?.data + ? response.data + // Explicitly specify our expectation about the first version being latest live for typescript. + .filter((publication) => publication.versions[0].isLatestLiveVersion) + .map((publication) => { + const version = publication.versions[0]; + version.user = { + id: props.user.id, + createdAt: props.user.createdAt, + email: props.user.email || '', + firstName: props.user.firstName, + lastName: props.user.lastName, + orcid: props.user.orcid, + updatedAt: props.user.updatedAt, + role: props.user.role + }; + + version.publication = { + id: publication.id, + type: publication.type, + doi: publication.doi, + url_slug: publication.url_slug, + flagCount: publication.flagCount, + peerReviewCount: publication.peerReviewCount + }; + return version; + }) + : [], + [response] + ); - if (offset + limit >= total) { - setHideShowMoreButton(true); - } + const handleSearchFormSubmit: React.ReactEventHandler = async ( + e: React.SyntheticEvent + ): Promise => { + e.preventDefault(); + const searchTerm = e.currentTarget.searchTerm.value; + const newQuery = { ...router.query, query: searchTerm }; - return data; - }, - { - fallbackData: props.fallbackData ? [props.fallbackData] : undefined + if (!searchTerm) { + delete newQuery.query; // remove query param from browser url } - ); - - const missingNames = !props.user.firstName && !props.user.lastName; - const isOrganisationalAccount = props.user.role === 'ORGANISATION'; - const userPublications = useMemo(() => data?.map((data) => data.results).flat() || [], [data]); - const pageTitle = `Author: ${isOrganisationalAccount ? props.user.firstName : props.user.orcid} - ${Config.urls.viewUser.title}`; + await router.push({ query: newQuery }, undefined, { shallow: true }); + setOffset(0); + setQuery(searchTerm); + }; return ( <> {pageTitle} - - @@ -191,74 +225,22 @@ const Author: Types.NextPage = (props): React.ReactElement => {

Octopus publications

- {userPublications.length ? ( -
- {userPublications.map((publication, index) => { - const version = publication.versions[0]; - if (version && version.isLatestLiveVersion && index <= userPublications.length) { - let classes = ''; - - if (index === 0) { - classes += 'rounded-t-lg '; - } - - if (index === userPublications.length - 1) { - classes += 'rounded-b-lg'; - } - - version.user = { - id: props.user.id, - createdAt: props.user.createdAt, - email: props.user.email || '', - firstName: props.user.firstName, - lastName: props.user.lastName, - orcid: props.user.orcid, - updatedAt: props.user.updatedAt, - role: props.user.role - }; - - version.publication = { - id: publication.id, - type: publication.type, - doi: publication.doi, - url_slug: publication.url_slug, - flagCount: publication.flagCount, - peerReviewCount: publication.peerReviewCount - }; - - return ( - - ); - } - })} - - {!hideShowMoreButton && ( - - - - )} -
- ) : ( - - )} + diff --git a/ui/src/pages/search/authors/index.tsx b/ui/src/pages/search/authors/index.tsx index a55677828..8da929ce3 100644 --- a/ui/src/pages/search/authors/index.tsx +++ b/ui/src/pages/search/authors/index.tsx @@ -13,34 +13,12 @@ import * as Helpers from '@/helpers'; const baseEndpoint = '/users?role=USER'; -/** - * - * @TODO - refactor getServerSideProps - * remove unnecessary if statements - */ - export const getServerSideProps: Types.GetServerSideProps = async (context) => { const searchType: Types.SearchType = 'authors'; - let query: string | string[] | null = null; - let limit: number | string | string[] | null = null; - let offset: number | string | string[] | null = null; - - // default error let error: string | null = null; - - // setting params - if (context.query.query) query = context.query.query; - if (context.query.limit) limit = context.query.limit; - if (context.query.offset) offset = context.query.offset; - - // If multiple of the same params are provided, pick the first - if (Array.isArray(query)) query = query[0]; - if (Array.isArray(limit)) limit = limit[0]; - if (Array.isArray(offset)) offset = offset[0]; - - // params come in as strings, so make sure the value of the string is parsable as a number or ignore it - limit && !Number.isNaN(parseInt(limit, 10)) ? (limit = parseInt(limit, 10)) : (limit = null); - offset && !Number.isNaN(parseInt(offset, 10)) ? (offset = parseInt(offset, 10)) : (offset = null); + const query = Helpers.extractNextQueryParam(context.query.query); + const limit = Helpers.extractNextQueryParam(context.query.limit, true); + const offset = Helpers.extractNextQueryParam(context.query.offset, true); const swrKey = `${baseEndpoint}&search=${encodeURIComponent(query || '')}&limit=${limit || '10'}&offset=${offset || '0'}`; let fallbackData: Interfaces.SearchResults = { @@ -140,6 +118,7 @@ const Authors: Types.NextPage = (props): React.ReactElement => { searchType="authors" setLimit={setLimit} setOffset={setOffset} + showSearchTypeSwitch={true} total={results?.metadata.total || 0} /> diff --git a/ui/src/pages/search/organisations/index.tsx b/ui/src/pages/search/organisations/index.tsx index c2c7ea8a4..b840e43f6 100644 --- a/ui/src/pages/search/organisations/index.tsx +++ b/ui/src/pages/search/organisations/index.tsx @@ -20,26 +20,10 @@ const baseEndpoint = '/users?role=ORGANISATION'; */ export const getServerSideProps: Types.GetServerSideProps = async (context) => { - let query: string | string[] | null = null; - let limit: number | string | string[] | null = null; - let offset: number | string | string[] | null = null; - - // default error let error: string | null = null; - - // setting params - if (context.query.query) query = context.query.query; - if (context.query.limit) limit = context.query.limit; - if (context.query.offset) offset = context.query.offset; - - // If multiple of the same params are provided, pick the first - if (Array.isArray(query)) query = query[0]; - if (Array.isArray(limit)) limit = limit[0]; - if (Array.isArray(offset)) offset = offset[0]; - - // params come in as strings, so make sure the value of the string is parsable as a number or ignore it - limit && !Number.isNaN(parseInt(limit, 10)) ? (limit = parseInt(limit, 10)) : (limit = null); - offset && !Number.isNaN(parseInt(offset, 10)) ? (offset = parseInt(offset, 10)) : (offset = null); + const limit = Helpers.extractNextQueryParam(context.query.limit, true); + const offset = Helpers.extractNextQueryParam(context.query.offset, true); + const query = Helpers.extractNextQueryParam(context.query.query); const swrKey = `${baseEndpoint}&search=${encodeURIComponent(query || '')}&limit=${limit || '10'}&offset=${offset || '0'}`; let fallbackData: Interfaces.SearchResults = { @@ -137,6 +121,7 @@ const Authors: Types.NextPage = (props): React.ReactElement => { searchType={'organisations'} setLimit={setLimit} setOffset={setOffset} + showSearchTypeSwitch={true} total={results?.metadata.total || 0} /> diff --git a/ui/src/pages/search/publications/index.tsx b/ui/src/pages/search/publications/index.tsx index 66afb2093..d159444f9 100644 --- a/ui/src/pages/search/publications/index.tsx +++ b/ui/src/pages/search/publications/index.tsx @@ -1,9 +1,7 @@ import Head from 'next/head'; import React from 'react'; import useSWR from 'swr'; -import * as Framer from 'framer-motion'; import * as Router from 'next/router'; -import * as SolidIcons from '@heroicons/react/24/solid'; import * as api from '@/api'; import * as Components from '@/components'; @@ -88,53 +86,16 @@ const constructQueryParams = (params: { return paramString.join('&'); }; -/** - * - * @TODO - refactor getServerSideProps - * 1. remove unnecessary if statements - * 2. make sure correct publicationTypes are passed via props - */ - export const getServerSideProps: Types.GetServerSideProps = async (context) => { - // defaults to possible query params const searchType: Types.SearchType = 'publication-versions'; - let query: string | string[] | null = null; - let publicationTypes: string | string[] | null = null; - let limit: number | string | string[] | null = null; - let offset: number | string | string[] | null = null; - let dateFrom: string | string[] | null = null; - let dateTo: string | string[] | null = null; - let authorTypes: string | string[] | null = null; - - // defaults to results - let searchResults: { data: Interfaces.PublicationVersion[]; metadata: Interfaces.SearchResultMeta } = { - data: [], - metadata: { - limit: 10, - offset: 0, - total: 0 - } - }; - - // default error let error: string | null = null; - - // setting params - if (context.query.query) query = context.query.query; - if (context.query.type) publicationTypes = context.query.type; - if (context.query.limit) limit = context.query.limit; - if (context.query.offset) offset = context.query.offset; - if (context.query.dateFrom) dateFrom = context.query.dateFrom; - if (context.query.dateTo) dateTo = context.query.dateTo; - if (context.query.authorType) authorTypes = context.query.authorType; - - if (Array.isArray(query)) query = query[0]; - if (Array.isArray(publicationTypes)) publicationTypes = publicationTypes[0]; - if (Array.isArray(limit)) limit = limit[0]; - if (Array.isArray(offset)) offset = offset[0]; - if (Array.isArray(dateFrom)) dateFrom = dateFrom[0]; - if (Array.isArray(dateTo)) dateTo = dateTo[0]; - if (Array.isArray(authorTypes)) authorTypes = authorTypes[0]; + const query = Helpers.extractNextQueryParam(context.query.query); + const publicationTypes = Helpers.extractNextQueryParam(context.query.type); + const limit = Helpers.extractNextQueryParam(context.query.limit, true); + const offset = Helpers.extractNextQueryParam(context.query.offset, true); + const dateFrom = Helpers.extractNextQueryParam(context.query.dateFrom); + const dateTo = Helpers.extractNextQueryParam(context.query.dateTo); + const authorTypes = Helpers.extractNextQueryParam(context.query.authorType); const params = constructQueryParams({ query, @@ -468,6 +429,7 @@ const Publications: Types.NextPage = (props): React.ReactElement => { searchType="publication-versions" setLimit={setLimit} setOffset={setOffset} + showSearchTypeSwitch={true} total={response?.metadata.total || 0} /> diff --git a/ui/src/pages/search/topics/index.tsx b/ui/src/pages/search/topics/index.tsx index 70853769e..95f186dcc 100644 --- a/ui/src/pages/search/topics/index.tsx +++ b/ui/src/pages/search/topics/index.tsx @@ -100,6 +100,7 @@ const Topics: Types.NextPage = (props): React.ReactElement => { searchType="topics" setLimit={setLimit} setOffset={setOffset} + showSearchTypeSwitch={true} total={data?.total || 0} /> From c024cdf897ac64b6cce6c420498be2177a5f67c6 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Tue, 17 Dec 2024 10:32:39 +0000 Subject: [PATCH 3/7] linting fixes --- ui/src/components/Search/Page/index.tsx | 1 + ui/src/pages/authors/[id]/index.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/components/Search/Page/index.tsx b/ui/src/components/Search/Page/index.tsx index 1ceb724f6..afacc6709 100644 --- a/ui/src/components/Search/Page/index.tsx +++ b/ui/src/components/Search/Page/index.tsx @@ -57,5 +57,6 @@ const SearchPage = React.forwardRef( ); } ); +SearchPage.displayName = 'SearchPage'; export default SearchPage; diff --git a/ui/src/pages/authors/[id]/index.tsx b/ui/src/pages/authors/[id]/index.tsx index dcfccb570..56df89b73 100644 --- a/ui/src/pages/authors/[id]/index.tsx +++ b/ui/src/pages/authors/[id]/index.tsx @@ -112,7 +112,7 @@ const Author: Types.NextPage = (props): React.ReactElement => { return version; }) : [], - [response] + [response, props.user] ); const handleSearchFormSubmit: React.ReactEventHandler = async ( From c299faba8d68ddd12c1b2c4eef0c7ac71cb556d9 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Tue, 17 Dec 2024 10:49:39 +0000 Subject: [PATCH 4/7] fix "unsafe return of 'any ' typed value" --- .../components/publicationVersion/__tests__/getAll.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/components/publicationVersion/__tests__/getAll.test.ts b/api/src/components/publicationVersion/__tests__/getAll.test.ts index ccc407864..d29899450 100644 --- a/api/src/components/publicationVersion/__tests__/getAll.test.ts +++ b/api/src/components/publicationVersion/__tests__/getAll.test.ts @@ -52,7 +52,7 @@ describe('Get many publication versions', () => { }); expect(getPublications.status).toEqual(200); - const publicationDates = getPublications.body.data.map((version) => version.publishedDate); + const publicationDates: Date[] = getPublications.body.data.map((version) => version.publishedDate as Date); // Sort a copy of the dates from the results to confirm order. const sortedPublicationDates = [...publicationDates].sort( (a, b) => new Date(b).getTime() - new Date(a).getTime() @@ -67,9 +67,9 @@ describe('Get many publication versions', () => { }); expect(getPublications.status).toEqual(200); - const publicationDates = getPublications.body.data.map((version) => version.publishedDate); + const publicationDates = getPublications.body.data.map((version) => version.publishedDate as Date); // Sort a copy of the dates from the results to confirm order. - const sortedPublicationDates = [...publicationDates].sort( + const sortedPublicationDates: Date[] = [...publicationDates].sort( (a, b) => new Date(a).getTime() - new Date(b).getTime() ); expect(publicationDates).toEqual(sortedPublicationDates); From 38f4092875e08b3ac49b4b5284c898a85d31aec0 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Mon, 16 Dec 2024 09:14:50 +0000 Subject: [PATCH 5/7] get around prisma not finding openssl issue --- api/Dockerfile | 5 ++++- api/prisma/schema.prisma | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index 06c4a12b4..270aa92d7 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -7,7 +7,10 @@ RUN apk add \ curl \ gnupg \ lsb-release \ - wget + wget \ + openssl \ + openssl-dev \ + libc6-compat # Dockerize is needed to sync containers startup ENV DOCKERIZE_VERSION v0.6.1 diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 039108f98..f04c1057b 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -1,7 +1,7 @@ generator client { provider = "prisma-client-js" previewFeatures = ["fullTextSearch"] - binaryTargets = ["native", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] + binaryTargets = ["native", "linux-musl", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] } datasource db { From 781c4fb7a376958c130b4dd591801568b27ba944 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Thu, 19 Dec 2024 09:13:18 +0000 Subject: [PATCH 6/7] remove unnecessary typing --- api/src/components/publicationVersion/__tests__/getAll.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/components/publicationVersion/__tests__/getAll.test.ts b/api/src/components/publicationVersion/__tests__/getAll.test.ts index d29899450..f308ec666 100644 --- a/api/src/components/publicationVersion/__tests__/getAll.test.ts +++ b/api/src/components/publicationVersion/__tests__/getAll.test.ts @@ -52,7 +52,7 @@ describe('Get many publication versions', () => { }); expect(getPublications.status).toEqual(200); - const publicationDates: Date[] = getPublications.body.data.map((version) => version.publishedDate as Date); + const publicationDates = getPublications.body.data.map((version) => version.publishedDate as Date); // Sort a copy of the dates from the results to confirm order. const sortedPublicationDates = [...publicationDates].sort( (a, b) => new Date(b).getTime() - new Date(a).getTime() From bcdc6191a5c1bc8828fe4d743cd4e43af86cf070 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Thu, 19 Dec 2024 09:23:36 +0000 Subject: [PATCH 7/7] rework condition --- ui/src/pages/authors/[id]/index.tsx | 51 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/ui/src/pages/authors/[id]/index.tsx b/ui/src/pages/authors/[id]/index.tsx index 56df89b73..32e7e9be0 100644 --- a/ui/src/pages/authors/[id]/index.tsx +++ b/ui/src/pages/authors/[id]/index.tsx @@ -85,33 +85,30 @@ const Author: Types.NextPage = (props): React.ReactElement => { const publicationVersions: Interfaces.PublicationVersion[] = useMemo( () => response?.data - ? response.data - // Explicitly specify our expectation about the first version being latest live for typescript. - .filter((publication) => publication.versions[0].isLatestLiveVersion) - .map((publication) => { - const version = publication.versions[0]; - version.user = { - id: props.user.id, - createdAt: props.user.createdAt, - email: props.user.email || '', - firstName: props.user.firstName, - lastName: props.user.lastName, - orcid: props.user.orcid, - updatedAt: props.user.updatedAt, - role: props.user.role - }; - - version.publication = { - id: publication.id, - type: publication.type, - doi: publication.doi, - url_slug: publication.url_slug, - flagCount: publication.flagCount, - peerReviewCount: publication.peerReviewCount - }; - return version; - }) - : [], + ?.filter((publication) => publication.versions[0].isLatestLiveVersion) + .map((publication) => { + const version = publication.versions[0]; + version.user = { + id: props.user.id, + createdAt: props.user.createdAt, + email: props.user.email || '', + firstName: props.user.firstName, + lastName: props.user.lastName, + orcid: props.user.orcid, + updatedAt: props.user.updatedAt, + role: props.user.role + }; + + version.publication = { + id: publication.id, + type: publication.type, + doi: publication.doi, + url_slug: publication.url_slug, + flagCount: publication.flagCount, + peerReviewCount: publication.peerReviewCount + }; + return version; + }) || [], [response, props.user] );