diff --git a/api/prisma/seed.ts b/api/prisma/seed.ts index de3efcfc5..4cdce3249 100644 --- a/api/prisma/seed.ts +++ b/api/prisma/seed.ts @@ -15,6 +15,19 @@ const createPublications = async (publications: Prisma.PublicationCreateInput[]) versions: { where: { isLatestVersion: true + }, + select: { + title: true, + description: true, + keywords: true, + content: true, + currentStatus: true, + publishedDate: true, + user: { + select: { + role: true + } + } } } } @@ -31,12 +44,10 @@ const createPublications = async (publications: Prisma.PublicationCreateInput[]) id: createdPublication.id, type: createdPublication.type, title: latestVersion.title, - licence: latestVersion.licence, + organisationalAuthor: latestVersion.user.role === 'ORGANISATION', description: latestVersion.description, keywords: latestVersion.keywords, content: latestVersion.content, - language: 'en', - currentStatus: latestVersion.currentStatus, publishedDate: latestVersion.publishedDate, cleanContent: convert(latestVersion.content) } diff --git a/api/prisma/seeds/local/unitTesting/publications.ts b/api/prisma/seeds/local/unitTesting/publications.ts index 03994aa8c..b2c3adfc5 100644 --- a/api/prisma/seeds/local/unitTesting/publications.ts +++ b/api/prisma/seeds/local/unitTesting/publications.ts @@ -979,38 +979,6 @@ const publicationSeeds: Prisma.PublicationCreateInput[] = [ } } }, - { - id: 'research-topic', - doi: '10.82259/cty5-2g27', - type: 'PROBLEM', - linkedTo: { - create: { - publicationToId: 'publication-problem-live', - versionToId: 'publication-problem-live-v1', - draft: false - } - }, - versions: { - create: { - id: 'research-topic-v1', - versionNumber: 1, - title: 'Music', - conflictOfInterestStatus: false, - conflictOfInterestText: '', - content: - 'This is an automatically-generated topic, produced in order to provide authors with a place to attach new Problem publications', - currentStatus: 'LIVE', - isLatestLiveVersion: true, - user: { connect: { id: 'octopus' } }, - publicationStatus: { - create: [ - { status: 'DRAFT', createdAt: '2022-01-20T15:51:42.523Z' }, - { status: 'LIVE', createdAt: '2022-01-20T15:51:42.523Z' } - ] - } - } - } - }, { id: 'publication-peer-review-draft', doi: '10.82259/cty5-2g28', diff --git a/api/scripts/reindex.ts b/api/scripts/reindex.ts index e2c97f63a..52361de9d 100644 --- a/api/scripts/reindex.ts +++ b/api/scripts/reindex.ts @@ -25,6 +25,18 @@ const reindex = async (): Promise => { versions: { where: { isLatestLiveVersion: true + }, + select: { + title: true, + description: true, + keywords: true, + content: true, + publishedDate: true, + user: { + select: { + role: true + } + } } } } @@ -43,12 +55,10 @@ const reindex = async (): Promise => { id: pub.id, type: pub.type, title: latestLiveVersion.title, - licence: latestLiveVersion.licence, + organisationalAuthor: latestLiveVersion.user.role === 'ORGANISATION', description: latestLiveVersion.description, keywords: latestLiveVersion.keywords, content: latestLiveVersion.content, - language: 'en', - currentStatus: latestLiveVersion.currentStatus, publishedDate: latestLiveVersion.publishedDate, cleanContent: convert(latestLiveVersion.content) } diff --git a/api/src/components/publication/__tests__/getPublication.test.ts b/api/src/components/publication/__tests__/getPublication.test.ts index ea706b165..7538903be 100644 --- a/api/src/components/publication/__tests__/getPublication.test.ts +++ b/api/src/components/publication/__tests__/getPublication.test.ts @@ -1,7 +1,7 @@ import * as testUtils from 'lib/testUtils'; describe('View publications + versions', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/components/publication/__tests__/getPublicationLinks.test.ts b/api/src/components/publication/__tests__/getPublicationLinks.test.ts index d498fcada..63398eaa6 100644 --- a/api/src/components/publication/__tests__/getPublicationLinks.test.ts +++ b/api/src/components/publication/__tests__/getPublicationLinks.test.ts @@ -1,7 +1,7 @@ import * as testUtils from 'lib/testUtils'; describe('Get links from a supplied publication', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/components/publication/service.ts b/api/src/components/publication/service.ts index 1e4a62d73..f7bf8a269 100644 --- a/api/src/components/publication/service.ts +++ b/api/src/components/publication/service.ts @@ -350,7 +350,6 @@ export const getOpenSearchPublications = (filters: I.OpenSearchPublicationFilter const must: unknown[] = []; if (filters.search) { - // @ts-ignore must.push({ multi_match: { query: filters.search, @@ -373,6 +372,15 @@ export const getOpenSearchPublications = (filters: I.OpenSearchPublicationFilter }); } + // The endpoint does accept both author types at once, but this is the same as not filtering. + if (filters.authorType && Enum.authorTypes.includes(filters.authorType)) { + must.push({ + term: { + organisationalAuthor: filters.authorType === 'organisational' + } + }); + } + // @ts-ignore query.body.query.bool.must = must; diff --git a/api/src/components/publicationVersion/__tests__/getAll.test.ts b/api/src/components/publicationVersion/__tests__/getAll.test.ts new file mode 100644 index 000000000..ccc407864 --- /dev/null +++ b/api/src/components/publicationVersion/__tests__/getAll.test.ts @@ -0,0 +1,186 @@ +import * as enums from 'lib/enum'; +import * as testUtils from 'lib/testUtils'; + +describe('Get many publication versions', () => { + beforeAll(async () => { + await testUtils.clearDB(); + await testUtils.testSeed(); + await testUtils.openSearchSeed(); + }); + + test('Pages have 10 results by default', async () => { + const getPublications = await testUtils.agent.get('/publication-versions'); + + expect(getPublications.status).toEqual(200); + expect(getPublications.body.metadata).toMatchObject({ + limit: 10, + offset: 0 + }); + expect(getPublications.body.data.length).toEqual(10); + }); + + test('Can limit page size', async () => { + const getPublications = await testUtils.agent.get('/publication-versions').query({ + limit: 5 + }); + + expect(getPublications.status).toEqual(200); + expect(getPublications.body.metadata).toMatchObject({ + limit: 5 + }); + expect(getPublications.body.data.length).toEqual(5); + }); + + test('Can get second page', async () => { + const getPublications = await testUtils.agent.get('/publication-versions').query({ + offset: 10 + }); + + expect(getPublications.status).toEqual(200); + expect(getPublications.body.metadata).toMatchObject({ + limit: 10, + offset: 10 + }); + // This will change if we add more live seed publications. + expect(getPublications.body.data.length).toEqual(5); + }); + + test('Can order by publication date, descending', async () => { + const getPublications = await testUtils.agent.get('/publication-versions').query({ + orderBy: 'publishedDate', + orderDirection: 'desc' + }); + + expect(getPublications.status).toEqual(200); + const publicationDates = getPublications.body.data.map((version) => version.publishedDate); + // 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() + ); + expect(publicationDates).toEqual(sortedPublicationDates); + }); + + test('Can order by publication date, ascending', async () => { + const getPublications = await testUtils.agent.get('/publication-versions').query({ + orderBy: 'publishedDate', + orderDirection: 'asc' + }); + + expect(getPublications.status).toEqual(200); + const publicationDates = getPublications.body.data.map((version) => version.publishedDate); + // Sort a copy of the dates from the results to confirm order. + const sortedPublicationDates = [...publicationDates].sort( + (a, b) => new Date(a).getTime() - new Date(b).getTime() + ); + expect(publicationDates).toEqual(sortedPublicationDates); + }); + + test('Invalid orderBy is rejected', async () => { + const getPublications = await testUtils.agent.get('/publication-versions').query({ + orderBy: 'dinosaur' + }); + + expect(getPublications.status).toEqual(400); + expect(getPublications.body.message).toHaveLength(1); + expect(getPublications.body.message[0].keyword).toEqual('enum'); + }); + + test('Invalid order direction is rejected', async () => { + const getPublications = await testUtils.agent.get('/publication-versions').query({ + orderBy: 'publishedDate', + orderDirection: 'dinosaur' + }); + + expect(getPublications.status).toEqual(400); + expect(getPublications.body.message).toHaveLength(1); + expect(getPublications.body.message[0].keyword).toEqual('enum'); + }); + + test('Can filter by publication type', async () => { + await Promise.all( + enums.publicationTypes.map(async (type) => { + const getPublications = await testUtils.agent.get(`/publication-versions?type=${type}`); + + expect(getPublications.status).toEqual(200); + expect(getPublications.body.data.every((version) => version.publication.type === type)).toEqual(true); + }) + ); + }); + + test('Can filter by multiple publication types at once', async () => { + const getPublications = await testUtils.agent.get('/publication-versions?type=PROBLEM,PROTOCOL'); + + expect(getPublications.status).toEqual(200); + expect( + getPublications.body.data.every((version) => ['PROBLEM', 'PROTOCOL'].includes(version.publication.type)) + ).toEqual(true); + }); + + test('Type filtering rejects invalid types', async () => { + const getPublications = await testUtils.agent.get('/publication-versions?type=DINOSAUR'); + + expect(getPublications.status).toEqual(400); + expect(getPublications.body.message).toHaveLength(1); + expect(getPublications.body.message[0].keyword).toEqual('pattern'); + }); + + test('Can filter by author type', async () => { + const getOrganisationalPublications = await testUtils.agent.get( + '/publication-versions?authorType=organisational' + ); + + expect(getOrganisationalPublications.status).toEqual(200); + // User role isn't returned in body so differentiate results by length (there are fewer organisational publications). + expect(getOrganisationalPublications.body.data).toHaveLength(2); + + const getIndividualPublications = await testUtils.agent.get('/publication-versions?authorType=individual'); + + expect(getIndividualPublications.status).toEqual(200); + expect(getIndividualPublications.body.data).toHaveLength(10); + }); + + test('Can filter by multiple author types at once', async () => { + const getPublications = await testUtils.agent.get('/publication-versions?authorType=individual,organisational'); + + expect(getPublications.status).toEqual(200); + // Includes everything. + expect(getPublications.body.metadata.total).toEqual(15); + }); + + test('Author filtering rejects invalid types', async () => { + const getPublications = await testUtils.agent.get('/publication-versions?authorType=dinosaur'); + + expect(getPublications.status).toEqual(400); + expect(getPublications.body.message).toHaveLength(1); + expect(getPublications.body.message[0].keyword).toEqual('pattern'); + }); + + test('Can filter by search term', async () => { + const getPublications = await testUtils.agent.get('/publication-versions?search=ari'); + + expect(getPublications.status).toEqual(200); + expect(getPublications.body.data).toHaveLength(1); + }); + + test('Can filter by date range', async () => { + const getPublications = await testUtils.agent.get('/publication-versions').query({ + dateFrom: '2024-07-16', + dateTo: '2024-07-16' + }); + + expect(getPublications.status).toEqual(200); + expect(getPublications.body.data).toHaveLength(1); + }); + + test('Can exclude a particular publication by its ID', async () => { + const getAllPublications = await testUtils.agent.get('/publication-versions'); + const allPubsCount = getAllPublications.body.metadata.total; + + const getMostPublications = await testUtils.agent + .get('/publication-versions') + .query({ exclude: 'ari-publication-1' }); + + expect(getMostPublications.status).toEqual(200); + expect(getMostPublications.body.metadata.total).toEqual(allPubsCount - 1); + }); +}); diff --git a/api/src/components/publicationVersion/schema/getAll.ts b/api/src/components/publicationVersion/schema/getAll.ts index 663fcad4f..70a00f33c 100644 --- a/api/src/components/publicationVersion/schema/getAll.ts +++ b/api/src/components/publicationVersion/schema/getAll.ts @@ -1,3 +1,4 @@ +import * as Enum from 'enum'; import * as I from 'interface'; const getAll: I.JSONSchemaType = { @@ -5,10 +6,14 @@ const getAll: I.JSONSchemaType = { properties: { type: { type: 'string', - pattern: - '^((PROBLEM|PROTOCOL|ANALYSIS|REAL_WORLD_APPLICATION|HYPOTHESIS|DATA|INTERPRETATION|PEER_REVIEW)(,)?)+$', + pattern: `^((${Enum.publicationTypes.join('|')})(,)?)+$`, default: 'PROBLEM,PROTOCOL,ANALYSIS,REAL_WORLD_APPLICATION,HYPOTHESIS,DATA,INTERPRETATION,PEER_REVIEW' }, + authorType: { + type: 'string', + pattern: `^((${Enum.authorTypes.join('|')})(,)?)+$`, + nullable: true + }, limit: { type: 'number', default: 10 diff --git a/api/src/components/publicationVersion/service.ts b/api/src/components/publicationVersion/service.ts index 6784f4cfc..d7112acb4 100644 --- a/api/src/components/publicationVersion/service.ts +++ b/api/src/components/publicationVersion/service.ts @@ -1092,7 +1092,7 @@ export const postPublishHook = async (publicationVersion: I.PublicationVersion, id: publicationVersion.versionOf, type: publicationVersion.publication.type, title: publicationVersion.title, - licence: publicationVersion.licence, + organisationalAuthor: publicationVersion.user.role === 'ORGANISATION', description: publicationVersion.description, keywords: publicationVersion.keywords, content: publicationVersion.content, diff --git a/api/src/components/reference/__tests__/getReference.test.ts b/api/src/components/reference/__tests__/getReference.test.ts index 0e16740fb..2153f17ad 100644 --- a/api/src/components/reference/__tests__/getReference.test.ts +++ b/api/src/components/reference/__tests__/getReference.test.ts @@ -1,7 +1,7 @@ import * as testUtils from 'lib/testUtils'; describe('get references', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/components/topic/__tests__/getTopic.test.ts b/api/src/components/topic/__tests__/getTopic.test.ts index 60e48ba1c..2b22e8bde 100644 --- a/api/src/components/topic/__tests__/getTopic.test.ts +++ b/api/src/components/topic/__tests__/getTopic.test.ts @@ -1,7 +1,7 @@ import * as testUtils from 'lib/testUtils'; describe('Get individual topic', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/components/topic/__tests__/getTopics.test.ts b/api/src/components/topic/__tests__/getTopics.test.ts index 0bbb58516..a3399f01a 100644 --- a/api/src/components/topic/__tests__/getTopics.test.ts +++ b/api/src/components/topic/__tests__/getTopics.test.ts @@ -2,7 +2,7 @@ import * as testUtils from 'lib/testUtils'; import * as I from 'lib/interface'; describe('Get Topics', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/components/user/__tests__/getUser.test.ts b/api/src/components/user/__tests__/getUser.test.ts index 040b19f11..57adf0e3f 100644 --- a/api/src/components/user/__tests__/getUser.test.ts +++ b/api/src/components/user/__tests__/getUser.test.ts @@ -1,7 +1,7 @@ import * as testUtils from 'lib/testUtils'; describe('Get individual user', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/components/user/__tests__/getUserList.test.ts b/api/src/components/user/__tests__/getUserList.test.ts index cc63d7ec6..38602f6b7 100644 --- a/api/src/components/user/__tests__/getUserList.test.ts +++ b/api/src/components/user/__tests__/getUserList.test.ts @@ -1,7 +1,7 @@ import * as testUtils from 'lib/testUtils'; describe('Get user list with apiKey', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/components/user/__tests__/getUsers.test.ts b/api/src/components/user/__tests__/getUsers.test.ts index 7e2c5a855..31757c602 100644 --- a/api/src/components/user/__tests__/getUsers.test.ts +++ b/api/src/components/user/__tests__/getUsers.test.ts @@ -2,7 +2,7 @@ import * as testUtils from 'lib/testUtils'; import * as I from 'lib/interface'; describe('Get Users', () => { - beforeEach(async () => { + beforeAll(async () => { await testUtils.clearDB(); await testUtils.testSeed(); }); diff --git a/api/src/lib/enum.ts b/api/src/lib/enum.ts index 93256519b..69128ff9c 100644 --- a/api/src/lib/enum.ts +++ b/api/src/lib/enum.ts @@ -232,3 +232,5 @@ export const publicationTypes: I.PublicationType[] = [ 'REAL_WORLD_APPLICATION', 'PEER_REVIEW' ]; + +export const authorTypes = ['individual', 'organisational']; diff --git a/api/src/lib/interface.ts b/api/src/lib/interface.ts index 688edba94..2a3a996fc 100644 --- a/api/src/lib/interface.ts +++ b/api/src/lib/interface.ts @@ -121,9 +121,9 @@ export interface OpenSearchPublication { id: string; type: PublicationType; title: string | null; - licence?: LicenceType | null; + organisationalAuthor: boolean; + keywords: string[]; description?: string | null; - keywords?: string[]; content?: string | null; cleanContent?: string | null; publishedDate?: Date | null; @@ -198,6 +198,7 @@ export interface OpenSearchPublicationFilters { limit: number; offset: number; type: string; + authorType?: 'individual' | 'organisational'; exclude?: string; dateFrom?: string; dateTo?: string; diff --git a/api/src/lib/testUtils.ts b/api/src/lib/testUtils.ts index 91d5b06d9..f55bd9882 100644 --- a/api/src/lib/testUtils.ts +++ b/api/src/lib/testUtils.ts @@ -84,32 +84,63 @@ export const openSearchSeed = async (): Promise => { versions: { where: { isLatestLiveVersion: true + }, + select: { + title: true, + description: true, + keywords: true, + content: true, + publishedDate: true, + user: { + select: { + role: true + } + } } } } }); - for (const publication of publications) { - const latestLiveVersion = publication.versions[0]; + await Promise.all( + publications.map(async (publication) => { + const latestLiveVersion = publication.versions[0]; - await client.search.create({ - index: 'publications', - id: publication.id, - body: { + await client.search.create({ + index: 'publications', id: publication.id, - type: publication.type, - title: latestLiveVersion.title, - licence: latestLiveVersion.licence, - description: latestLiveVersion.description, - keywords: latestLiveVersion.keywords, - content: latestLiveVersion.content, - language: 'en', - currentStatus: latestLiveVersion.currentStatus, - publishedDate: latestLiveVersion.publishedDate, - cleanContent: convert(latestLiveVersion.content) - } - }); - } + body: { + id: publication.id, + type: publication.type, + title: latestLiveVersion.title, + organisationalAuthor: latestLiveVersion.user.role === 'ORGANISATION', + description: latestLiveVersion.description, + keywords: latestLiveVersion.keywords, + content: latestLiveVersion.content, + publishedDate: latestLiveVersion.publishedDate, + cleanContent: convert(latestLiveVersion.content) + } + }); + }) + ); + + // Wait until things show up in the index. + const maxWaitAttempts = 5; + let waitAttempts = 0; + + do { + const allResults = await client.search.search({ index: 'publications', body: { query: { match_all: {} } } }); + + if (allResults.body.hits.total.value > 0) { + return; + } else { + // Wait a second before trying again. + await new Promise((resolve) => setTimeout(resolve, 1000)); + waitAttempts++; + } + } while (waitAttempts < maxWaitAttempts); + + // If index isn't populated by this time, something is wrong. + throw new Error('Index not populated after seeding.'); }; export const clearDB = async (): Promise => { diff --git a/e2e/tests/LoggedIn/publish.e2e.spec.ts b/e2e/tests/LoggedIn/publish.e2e.spec.ts index eaafb44f5..a21f1a11d 100644 --- a/e2e/tests/LoggedIn/publish.e2e.spec.ts +++ b/e2e/tests/LoggedIn/publish.e2e.spec.ts @@ -660,6 +660,7 @@ const checkPublicationOnAccountPage = async ( navigate?: boolean ) => { if (navigate) { + await page.waitForLoadState('networkidle'); await page.goto(`/account`); } const publicationContainer = await page.getByTestId('publication-' + publicationDetails.id); diff --git a/ui/src/config/values.ts b/ui/src/config/values.ts index 291516d37..72882be3f 100644 --- a/ui/src/config/values.ts +++ b/ui/src/config/values.ts @@ -12,6 +12,8 @@ export const publicationTypes: Types.PublicationType[] = [ 'PEER_REVIEW' ]; +export const authorTypes = ['individual', 'organisational']; + export const octopusInformation: Interfaces.OctopusInformation = { publications: { PROBLEM: { diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index c1e7ce1d8..3e67a2a9c 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -70,27 +70,3 @@ export const destroy = async (url: string, token: string | undefined): Promise( - searchType: Types.SearchType, - search: string | null = null, - limit: number | null = null, - offset: number | null = null, - publicationType?: string | null -): Promise> => { - let endpoint: string = searchType === 'authors' ? Config.endpoints.users : Config.endpoints.publicationVersions; - let params: string = ''; - - // Global search params - limit && (params += '&limit=' + limit); - offset && (params += '&offset=' + offset); - search && (params += '&search=' + search); - - // publication specific params - searchType === 'publication-versions' && publicationType && (params += '&type=' + publicationType); - - params.includes('&') && (params = params.replace('&', '?')); - - const response = await get(endpoint + params, undefined); - return response.data; -}; diff --git a/ui/src/lib/interfaces.ts b/ui/src/lib/interfaces.ts index f565374a4..03094f445 100644 --- a/ui/src/lib/interfaces.ts +++ b/ui/src/lib/interfaces.ts @@ -524,15 +524,6 @@ export interface AdditionalInformation { description: string; } -export interface AuthorsPaginatedResults { - data: CoreUser[]; - metadata: { - total: number; - limit: number; - offset: number; - }; -} - export interface AuthorSearchQuery extends ParsedUrlQuery { query?: string; } diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index f12f3b356..2ff30dd39 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -82,8 +82,6 @@ export type JSONValue = unknown; export type SearchType = 'publication-versions' | 'authors' | 'topics'; -export type SearchParameter = Interfaces.PublicationVersion | Interfaces.User; - export type PublicationOrderBySearchOption = 'title' | 'publishedDate'; export type UserOrderBySearchOption = 'updatedAt' | 'createdAt'; diff --git a/ui/src/pages/search/authors/index.tsx b/ui/src/pages/search/authors/index.tsx index 396d62eda..d7ac45724 100644 --- a/ui/src/pages/search/authors/index.tsx +++ b/ui/src/pages/search/authors/index.tsx @@ -43,7 +43,7 @@ export const getServerSideProps: Types.GetServerSideProps = async (context) => { offset && !Number.isNaN(parseInt(offset, 10)) ? (offset = parseInt(offset, 10)) : (offset = null); const swrKey = `/users?search=${encodeURIComponent(query || '')}&limit=${limit || '10'}&offset=${offset || '0'}`; - let fallbackData: Interfaces.AuthorsPaginatedResults = { + let fallbackData: Interfaces.SearchResults = { data: [], metadata: { offset: 0, @@ -53,7 +53,7 @@ export const getServerSideProps: Types.GetServerSideProps = async (context) => { }; try { - fallbackData = (await api.get(swrKey, undefined)).data; + fallbackData = (await api.get(swrKey)).data; } catch (err) { const { message } = err as Interfaces.JSONResponseError; error = message; @@ -97,7 +97,7 @@ const Authors: Types.NextPage = (props): React.ReactElement => { data: results, error, isValidating - } = useSWR(swrKey, { + } = useSWR>(swrKey, { fallback: props.fallback, use: [Helpers.laggy] }); @@ -216,6 +216,13 @@ const Authors: Types.NextPage = (props): React.ReactElement => {
+
+ {typeof results?.metadata?.total === 'number' + ? `${results.metadata.total} result${results.metadata.total !== 1 ? 's' : ''}` + : error && error.message + ? error.message + : ''} +
{props.error ? ( ) : ( diff --git a/ui/src/pages/search/publications/index.tsx b/ui/src/pages/search/publications/index.tsx index e7bd86dff..ba311f0bb 100644 --- a/ui/src/pages/search/publications/index.tsx +++ b/ui/src/pages/search/publications/index.tsx @@ -1,17 +1,17 @@ +import Head from 'next/head'; import React from 'react'; import useSWR from 'swr'; -import Head from 'next/head'; - -import * as Router from 'next/router'; import * as Framer from 'framer-motion'; +import * as Router from 'next/router'; import * as SolidIcons from '@heroicons/react/24/solid'; -import * as Interfaces from '@/interfaces'; + +import * as api from '@/api'; import * as Components from '@/components'; +import * as Config from '@/config'; import * as Helpers from '@/helpers'; +import * as Interfaces from '@/interfaces'; import * as Layouts from '@/layouts'; -import * as Config from '@/config'; import * as Types from '@/types'; -import * as api from '@/api'; // Takes an input date from context or form controls, // sets time to start or end of day as appropriate, @@ -32,6 +32,62 @@ const formatDateForAPI = (rawDate: string, type: 'to' | 'from'): string | null = return date.toISOString(); }; +const constructQueryParams = (params: { + [key in 'query' | 'publicationTypes' | 'limit' | 'offset' | 'dateFrom' | 'dateTo' | 'authorTypes']: string | null; +}): string => { + const { query, publicationTypes, limit, offset, dateFrom, dateTo, authorTypes } = params; + const paramString: string[] = []; + + if (query) { + paramString.push('search=' + encodeURIComponent(query)); + } + + if (publicationTypes) { + // filter valid publication types only + paramString.push( + 'type=' + + publicationTypes + .split(',') + .filter((type) => Config.values.publicationTypes.includes(type as Types.PublicationType)) + .join(',') + ); + } + + // params come in as strings, so make sure the value of the string is parsable as a number or ignore it + if (limit && !Number.isNaN(parseInt(limit, 10))) { + paramString.push('limit=' + limit); + } + if (offset && !Number.isNaN(parseInt(offset, 10))) { + paramString.push('offset=' + offset); + } + + if (dateFrom) { + const dateFromFormatted = formatDateForAPI(dateFrom || '', 'from'); + if (dateFromFormatted) { + paramString.push('dateFrom=' + dateFromFormatted); + } + } + + if (dateTo) { + const dateToFormatted = formatDateForAPI(dateTo || '', 'to'); + if (dateToFormatted) { + paramString.push('dateTo=' + dateToFormatted); + } + } + + if (authorTypes) { + paramString.push( + 'authorType=' + + authorTypes + .split(',') + .filter((type) => Config.values.authorTypes.includes(type)) + .join(',') + ); + } + + return paramString.join('&'); +}; + /** * * @TODO - refactor getServerSideProps @@ -48,6 +104,7 @@ export const getServerSideProps: Types.GetServerSideProps = async (context) => { 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 } = { @@ -69,6 +126,7 @@ export const getServerSideProps: Types.GetServerSideProps = async (context) => { 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]; @@ -76,46 +134,35 @@ export const getServerSideProps: Types.GetServerSideProps = async (context) => { 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 params = constructQueryParams({ + query, + publicationTypes, + limit, + offset, + dateFrom, + dateTo, + authorTypes + }); - if (publicationTypes) { - // filter valid publication types only - publicationTypes = publicationTypes - .split(',') - .filter((type) => Config.values.publicationTypes.includes(type as Types.PublicationType)) - .join(','); - } - - // 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 swrKey = `/${searchType}?${params}`; + let fallbackData: Interfaces.SearchResults = { + data: [], + metadata: { + offset: 0, + limit: 10, + total: 0 + } + }; - // ensure the value of the search type is acceptable try { - const response = await api.search( - searchType, - encodeURIComponent(query || ''), - limit, - offset, - publicationTypes - ); - - searchResults = response; - - error = null; + fallbackData = (await api.get(swrKey)).data; } catch (err) { const { message } = err as Interfaces.JSONResponseError; error = message; } - const dateFromFormatted = formatDateForAPI(dateFrom || '', 'from'); - const dateToFormatted = formatDateForAPI(dateTo || '', 'to'); - - const swrKey = `/${searchType}?search=${encodeURIComponent( - (Array.isArray(query) ? query[0] : query) || '' - )}&type=${publicationTypes}&limit=${limit || '10'}&offset=${offset || '0'}${ - dateFromFormatted ? `&dateFrom=${dateFromFormatted}` : '' - }${dateToFormatted ? `&dateTo=${dateToFormatted}` : ''}`; - return { props: { searchType, @@ -125,8 +172,9 @@ export const getServerSideProps: Types.GetServerSideProps = async (context) => { offset, dateFrom, dateTo, + authorTypes, fallback: { - [swrKey]: searchResults + [swrKey]: fallbackData }, error } @@ -141,6 +189,7 @@ type Props = { offset: string | null; dateFrom: string | null; dateTo: string | null; + authorTypes: string | null; error: string | null; fallback: { [key: string]: { data: Interfaces.PublicationVersion[] } & Interfaces.SearchResultMeta }; }; @@ -150,6 +199,7 @@ const Publications: Types.NextPage = (props): React.ReactElement => { const searchInputRef = React.useRef(null); // params const [query, setQuery] = React.useState(props.query ? props.query : ''); + const [authorTypes, setAuthorTypes] = React.useState(props.authorTypes || ''); const [publicationTypes, setPublicationTypes] = React.useState(props.publicationTypes || ''); const [dateFrom, setDateFrom] = React.useState(props.dateFrom ? props.dateFrom : ''); const [dateTo, setDateTo] = React.useState(props.dateTo ? props.dateTo : ''); @@ -157,14 +207,17 @@ const Publications: Types.NextPage = (props): React.ReactElement => { const [limit, setLimit] = React.useState(props.limit ? parseInt(props.limit, 10) : 10); const [offset, setOffset] = React.useState(props.offset ? parseInt(props.offset, 10) : 0); - const dateFromFormatted = formatDateForAPI(dateFrom, 'from'); - const dateToFormatted = formatDateForAPI(dateTo, 'to'); + const params = constructQueryParams({ + query, + publicationTypes, + limit: limit.toString(), + offset: offset.toString(), + dateFrom, + dateTo, + authorTypes + }); - const swrKey = `/${props.searchType}?search=${encodeURIComponent(query || '')}&type=${ - publicationTypes || Config.values.publicationTypes.join(',') - }&limit=${limit || '10'}&offset=${offset || '0'}${dateFromFormatted ? `&dateFrom=${dateFromFormatted}` : ''}${ - dateToFormatted ? `&dateTo=${dateToFormatted}` : '' - }`; + const swrKey = `/${props.searchType}?${params}`; const { data: response, @@ -219,22 +272,27 @@ const Publications: Types.NextPage = (props): React.ReactElement => { setDate(newDate); }; - const collatePublicationTypes = async (e: React.ChangeEvent, value: string): Promise => { - if (e.target.name === 'select-all') { - await router.push( - { - query: { - ...router.query, - type: value - } - }, - undefined, - { shallow: true } - ); - - return setPublicationTypes(value); - } + const collateAuthorTypes = async (e: React.ChangeEvent, value: string): Promise => { + const current = authorTypes ? authorTypes.split(',') : []; + const uniqueSet = new Set(current); + e.target.checked ? uniqueSet.add(value) : uniqueSet.delete(value); + const uniqueArray = Array.from(uniqueSet).join(','); + + await router.push( + { + query: { + ...router.query, + authorType: uniqueArray + } + }, + undefined, + { shallow: true } + ); + setAuthorTypes(uniqueArray); + }; + + const collatePublicationTypes = async (e: React.ChangeEvent, value: string): Promise => { const current = publicationTypes ? publicationTypes.split(',') : []; const uniqueSet = new Set(current); e.target.checked ? uniqueSet.add(value) : uniqueSet.delete(value); @@ -276,6 +334,9 @@ const Publications: Types.NextPage = (props): React.ReactElement => { : limit + offset : null; + const checkBoxClasses = + 'h-4 w-4 rounded border-grey-300 text-teal-600 outline-none transition-colors duration-150 hover:cursor-pointer focus:ring-yellow-500 disabled:text-grey-300 hover:disabled:cursor-not-allowed'; + return ( <> @@ -374,69 +435,86 @@ const Publications: Types.NextPage = (props): React.ReactElement => {
+
+ {typeof response?.metadata?.total === 'number' + ? `${response.metadata.total} result${response.metadata.total !== 1 ? 's' : ''}` + : error && error.message + ? error.message + : ''} +
{props.error ? ( ) : ( - {error && } + {error && ( + + )} {!error && !response?.data?.length && !isValidating && ( = (props): React.ReactElement => {
+
+ {typeof data?.total === 'number' + ? `${data.total} results` + : error && error.message + ? error.message + : ''} +
{props.error ? ( ) : (