diff --git a/packages/datagateway-common/src/api/dataPublications.test.tsx b/packages/datagateway-common/src/api/dataPublications.test.tsx index 660515e45..d3d5e08c5 100644 --- a/packages/datagateway-common/src/api/dataPublications.test.tsx +++ b/packages/datagateway-common/src/api/dataPublications.test.tsx @@ -9,6 +9,7 @@ import { useDataPublicationsInfinite, useDataPublicationsPaginated, useDataPublication, + useDataPublications, } from './dataPublications'; jest.mock('../handleICATError'); @@ -276,7 +277,7 @@ describe('data publications api functions', () => { describe('useDataPublication', () => { it('sends axios request to fetch a single data publication and returns successful response', async () => { (axios.get as jest.Mock).mockResolvedValue({ - data: mockData[0], + data: mockData, }); const { result, waitFor } = renderHook(() => useDataPublication(1), { @@ -292,18 +293,26 @@ describe('data publications api functions', () => { id: { eq: 1 }, }) ); - params.append('include', JSON.stringify('users')); params.append( 'include', - JSON.stringify({ - content: { - dataCollectionInvestigations: { - investigation: { - investigationInstruments: 'instrument', + JSON.stringify([ + { + content: { + dataCollectionInvestigations: { + investigation: [ + 'datasets', + { + datasets: 'type', + investigationInstruments: 'instrument', + }, + ], }, }, }, - }) + 'users', + 'facility', + 'dates', + ]) ); expect(axios.get).toHaveBeenCalledWith( @@ -335,20 +344,105 @@ describe('data publications api functions', () => { id: { eq: 1 }, }) ); - params.append('include', JSON.stringify('users')); params.append( 'include', - JSON.stringify({ - content: { - dataCollectionInvestigations: { - investigation: { - investigationInstruments: 'instrument', + JSON.stringify([ + { + content: { + dataCollectionInvestigations: { + investigation: [ + 'datasets', + { + datasets: 'type', + investigationInstruments: 'instrument', + }, + ], }, }, }, + 'users', + 'facility', + 'dates', + ]) + ); + + expect(axios.get).toHaveBeenCalledWith( + 'https://example.com/api/datapublications', + expect.objectContaining({ + params, + }) + ); + expect((axios.get as jest.Mock).mock.calls[0][1].params.toString()).toBe( + params.toString() + ); + expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + }); + }); + + describe('useDataPublications', () => { + it('sends axios request to fetch a data publications with specified filters and returns successful response', async () => { + (axios.get as jest.Mock).mockResolvedValue({ + data: mockData, + }); + + params.append('order', JSON.stringify('id asc')); + params.append( + 'where', + JSON.stringify({ + name: { eq: 'test' }, }) ); + const { result, waitFor } = renderHook( + () => + useDataPublications([ + { + filterType: 'where', + filterValue: JSON.stringify({ name: { eq: 'test' } }), + }, + ]), + { + wrapper: createReactQueryWrapper(history), + } + ); + + await waitFor(() => result.current.isSuccess); + + expect(axios.get).toHaveBeenCalledWith( + 'https://example.com/api/datapublications', + expect.objectContaining({ + params, + }) + ); + expect((axios.get as jest.Mock).mock.calls[0][1].params.toString()).toBe( + params.toString() + ); + expect(result.current.data).toEqual(mockData); + }); + + it('sends axios request to fetch a single data publication and calls handleICATError on failure', async () => { + (axios.get as jest.Mock).mockRejectedValue({ + message: 'Test error', + }); + + params.append('order', JSON.stringify('id asc')); + params.append('include', '"type"'); + + const { result, waitFor } = renderHook( + () => + useDataPublications([ + { + filterType: 'include', + filterValue: '"type"', + }, + ]), + { + wrapper: createReactQueryWrapper(history), + } + ); + + await waitFor(() => result.current.isError); + expect(axios.get).toHaveBeenCalledWith( 'https://example.com/api/datapublications', expect.objectContaining({ diff --git a/packages/datagateway-common/src/api/dataPublications.tsx b/packages/datagateway-common/src/api/dataPublications.tsx index 43226735a..3d2ba5f0b 100644 --- a/packages/datagateway-common/src/api/dataPublications.tsx +++ b/packages/datagateway-common/src/api/dataPublications.tsx @@ -9,6 +9,7 @@ import { useQuery, UseInfiniteQueryResult, useInfiniteQuery, + UseQueryOptions, } from 'react-query'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; @@ -144,16 +145,17 @@ export const useDataPublicationsInfinite = ( }; export const useDataPublication = ( - dataPublicationId: number -): UseQueryResult => { - const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); - - return useQuery< + dataPublicationId: number, + queryOptions?: UseQueryOptions< DataPublication[], AxiosError, - DataPublication[], + DataPublication, [string, number] - >( + > +): UseQueryResult => { + const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); + + return useQuery( ['dataPublication', dataPublicationId], () => { return fetchDataPublications(apiUrl, { sort: {}, filters: {} }, [ @@ -165,22 +167,57 @@ export const useDataPublication = ( }, { filterType: 'include', - filterValue: JSON.stringify('users'), - }, - { - filterType: 'include', - filterValue: JSON.stringify({ - content: { - dataCollectionInvestigations: { - investigation: { - investigationInstruments: 'instrument', + filterValue: JSON.stringify([ + { + content: { + dataCollectionInvestigations: { + investigation: [ + 'datasets', + { + datasets: 'type', + investigationInstruments: 'instrument', + }, + ], }, }, }, - }), + 'users', + 'facility', + 'dates', + ]), }, ]); }, + { + onError: (error) => { + handleICATError(error); + }, + retry: retryICATErrors, + select: (data) => data[0], + ...queryOptions, + } + ); +}; + +export const useDataPublications = ( + additionalFilters: AdditionalFilters +): UseQueryResult => { + const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); + + return useQuery< + DataPublication[], + AxiosError, + DataPublication[], + [string, AdditionalFilters?] + >( + ['dataPublication', additionalFilters], + (params) => { + return fetchDataPublications( + apiUrl, + { sort: {}, filters: {} }, + additionalFilters + ); + }, { onError: (error) => { handleICATError(error); diff --git a/packages/datagateway-common/src/app.types.tsx b/packages/datagateway-common/src/app.types.tsx index 4fdfe2cb0..13b0a70a3 100644 --- a/packages/datagateway-common/src/app.types.tsx +++ b/packages/datagateway-common/src/app.types.tsx @@ -153,8 +153,8 @@ export interface DataCollectionDataset { export interface DataCollectionInvestigation { id: number; - dataCollection: DataCollection; - investigation: Investigation; + dataCollection?: DataCollection; + investigation?: Investigation; } export interface DataCollection { @@ -171,16 +171,21 @@ export interface DataPublicationUser { fullName: string; } +export interface DataPublicationType { + id: number; + name: string; +} + export interface DataPublication { id: number; pid: string; title: string; - modTime: string; - createTime: string; + facility?: Facility; description?: string; publicationDate?: string; users?: DataPublicationUser[]; content?: DataCollection; + type?: DataPublicationType; } interface InstrumentScientist { diff --git a/packages/datagateway-common/src/detailsPanels/isis/__snapshots__/investigationDetailsPanel.component.test.tsx.snap b/packages/datagateway-common/src/detailsPanels/isis/__snapshots__/investigationDetailsPanel.component.test.tsx.snap index b7a0ee2bf..5d59a3455 100644 --- a/packages/datagateway-common/src/detailsPanels/isis/__snapshots__/investigationDetailsPanel.component.test.tsx.snap +++ b/packages/datagateway-common/src/detailsPanels/isis/__snapshots__/investigationDetailsPanel.component.test.tsx.snap @@ -143,9 +143,9 @@ exports[`Investigation details panel component should check if multiple publicat - Data Publication Pid + Data Publication Study Pid

@@ -388,9 +388,9 @@ exports[`Investigation details panel component should check if multiple samples - Data Publication Pid + Data Publication Study Pid

@@ -822,9 +822,9 @@ exports[`Investigation details panel component should render correctly 1`] = ` - Data Publication Pid + Data Publication Study Pid

@@ -1057,9 +1057,9 @@ exports[`Investigation details panel component should render user, sample and pu - Data Publication Pid + Data Publication Study Pid

diff --git a/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.test.tsx b/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.test.tsx index f1b47de56..9cd2bc9eb 100644 --- a/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.test.tsx +++ b/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.test.tsx @@ -1,6 +1,5 @@ import { render, RenderResult, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { UserEvent } from '@testing-library/user-event/dist/types/setup'; import axios from 'axios'; import * as React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; @@ -40,7 +39,7 @@ function renderComponent({ describe('Investigation details panel component', () => { let rowData: Investigation; - let user: UserEvent; + let user: ReturnType; beforeEach(() => { user = userEvent.setup(); @@ -80,6 +79,36 @@ describe('Investigation details panel component', () => { modTime: '2019-06-10', createTime: '2019-06-11', title: 'Data Publication', + type: { + id: 15, + name: 'investigation', + }, + }, + ], + }, + }, + { + id: 2, + investigation: { + id: 1, + title: 'Test 1', + name: 'Test 1', + visitId: '1', + }, + dataCollection: { + id: 13, + dataPublications: [ + { + id: 14, + pid: 'Data Publication Study Pid', + description: 'Data Publication description', + modTime: '2019-06-10', + createTime: '2019-06-11', + title: 'Data Publication Study', + type: { + id: 16, + name: 'study', + }, }, ], }, @@ -322,6 +351,16 @@ describe('Investigation details panel component', () => { expect(link).toHaveTextContent('doi 1'); expect(link).toHaveAttribute('href', 'https://doi.org/doi 1'); + + const link2 = await screen.findByRole('link', { + name: /Data Publication Study Pid/, + }); + + expect(link2).toHaveTextContent('Data Publication Study Pid'); + expect(link2).toHaveAttribute( + 'href', + 'https://doi.org/Data Publication Study Pid' + ); }); it('should gracefully handles dataCollectionInvestigations without dataPublications and InvestigationUsers without Users', () => { diff --git a/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.tsx b/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.tsx index 526fc145e..4a12ea725 100644 --- a/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.tsx +++ b/packages/datagateway-common/src/detailsPanels/isis/investigationDetailsPanel.component.tsx @@ -94,6 +94,11 @@ const InvestigationDetailsPanel = ( } }, [data, selectedTab, changeTab]); + const studyDataPublication = + investigationData.dataCollectionInvestigations?.filter( + (dci) => dci.dataCollection?.dataPublications?.[0]?.type?.name === 'study' + )?.[0]?.dataCollection?.dataPublications?.[0]; + return (
- {/* TODO: when datapublications are created for studies, need to pick the study datapublication */} - {investigationData.dataCollectionInvestigations?.[0]?.dataCollection - ?.dataPublications && - investigationData.dataCollectionInvestigations[0].dataCollection.dataPublications.map( - (dataPublication) => { - if (dataPublication) { - return ( - - - {t('investigations.details.pid')} - - - - {dataPublication.pid} - - - - ); - } else { - return null; - } - } - )} + {studyDataPublication && ( + + + {t('investigations.details.pid')} + + + + {studyDataPublication.pid} + + + + )} {t('investigations.details.doi')} diff --git a/packages/datagateway-dataview/cypress/e2e/card/isis/dataPublications.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/isis/dataPublications.cy.ts deleted file mode 100644 index 918aa94dc..000000000 --- a/packages/datagateway-dataview/cypress/e2e/card/isis/dataPublications.cy.ts +++ /dev/null @@ -1,114 +0,0 @@ -describe('ISIS - Data Publication Cards', () => { - beforeEach(() => { - cy.intercept('**/datapublications/count*').as('getDataPublicationsCount'); - cy.intercept('**/datapublications?order*').as('getDataPublicationsOrder'); - cy.login(); - cy.visit( - '/browseDataPublications/instrument/8/dataPublication?view=card' - ).wait(['@getDataPublicationsCount', '@getDataPublicationsOrder'], { - timeout: 10000, - }); - }); - - it('should load correctly', () => { - cy.title().should('equal', 'DataGateway DataView'); - cy.get('#datagateway-dataview').should('be.visible'); - - cy.get('[data-testid="card"]') - .first() - .get('[data-testid="landing-datapublication-card-pid-link"]') - .first() - .then(($pid) => { - const pid = $pid.text(); - - const url = `https://doi.org/${pid}`; - - cy.get('[data-testid="card"]') - .first() - .get('[data-testid="landing-datapublication-card-pid-link"]') - .first() - .should('have.attr', 'href', url); - }); - - //Default sort - cy.contains('[role="button"]', 'desc').should('exist'); - cy.get('.MuiTableSortLabel-iconDirectionDesc').should('be.visible'); - }); - - it('should be able to click a datapublication to see its landing page', () => { - cy.get('[data-testid="card"]') - .first() - .contains('Church') - .click({ force: true }); - cy.location('pathname').should( - 'eq', - '/browseDataPublications/instrument/8/dataPublication/51' - ); - }); - - it('should be able to sort by one field or multiple', () => { - //Revert the default sort - cy.contains('[role="button"]', 'Publication Date') - .as('dateSortButton') - .click(); - cy.wait('@getDataPublicationsOrder', { timeout: 10000 }); - - // ascending - cy.get('@dateSortButton').click(); - cy.wait('@getDataPublicationsOrder', { timeout: 10000 }); - cy.contains('[role="button"]', 'asc').should('exist'); - cy.contains('[role="button"]', 'desc').should('not.exist'); - cy.get('[data-testid="card"]').first().contains('Article'); - - // descending - cy.get('@dateSortButton').click(); - cy.contains('[role="button"]', 'asc').should('not.exist'); - cy.contains('[role="button"]', 'desc').should('exist'); - cy.get('[data-testid="card"]').first().contains('Church'); - - // no order - cy.get('@dateSortButton').click(); - cy.contains('[role="button"]', 'asc').should('not.exist'); - cy.contains('[role="button"]', 'desc').should('not.exist'); - cy.get('[data-testid="card"]').first().contains('Article'); - - // multiple fields (shift click) - cy.contains('[role="button"]', 'Title').click(); - cy.get('@dateSortButton').click({ shiftKey: true }); - cy.wait('@getDataPublicationsOrder', { timeout: 10000 }); - - cy.contains('[aria-label="Sort by TITLE"]', 'asc').should('exist'); - cy.contains('[aria-label="Sort by PUBLICATION DATE"]', 'asc').should( - 'exist' - ); - cy.contains('[role="button"]', 'desc').should('not.exist'); - cy.get('[data-testid="card"]').first().contains('Article'); - - // should replace current sort if clicked without shift - cy.get('@dateSortButton').click(); - cy.wait('@getDataPublicationsOrder', { timeout: 10000 }); - - cy.contains('[aria-label="Sort by PUBLICATION DATE"]', 'desc').should( - 'exist' - ); - cy.contains('[aria-label="Sort by TITLE"]', 'asc').should('not.exist'); - - cy.get('[data-testid="card"]').first().contains('Church'); - }); - - it('should be able to filter by multiple fields', () => { - cy.get('[data-testid="advanced-filters-link"]').click(); - - cy.get('[aria-label="Filter by DOI"]').first().type('0'); - cy.wait(['@getDataPublicationsCount', '@getDataPublicationsOrder'], { - timeout: 10000, - }); - cy.get('[data-testid="card"]').first().contains('Consider'); - - cy.get('[aria-label="Filter by Title"]').first().type('sub'); - cy.wait(['@getDataPublicationsCount', '@getDataPublicationsOrder'], { - timeout: 10000, - }); - cy.get('[data-testid="card"]').first().contains('Article'); - }); -}); diff --git a/packages/datagateway-dataview/cypress/e2e/card/isis/investigationDataPublications.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/isis/investigationDataPublications.cy.ts new file mode 100644 index 000000000..f5d74ec12 --- /dev/null +++ b/packages/datagateway-dataview/cypress/e2e/card/isis/investigationDataPublications.cy.ts @@ -0,0 +1,173 @@ +describe('ISIS - Study Data Publication Cards', () => { + beforeEach(() => { + cy.intercept( + /\/datapublications\/count\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22investigation%22%7D%7D.*/, + (req) => { + // delete type = investigation requirement + const [url, search] = req.url.split('?'); + const params = new URLSearchParams(search); + // params.delete with value is still a new standard, so use workaround for now until browser compat catches up + // params.delete('where', '{"type.name":{"eq":"investigation"}}'); + const removeValue = ( + params: URLSearchParams, + key: string, + valueToRemove: string + ): URLSearchParams => { + const values = params.getAll(key); + if (values.length) { + params.delete(key); + for (const value of values) { + if (value !== valueToRemove) { + params.append(key, value); + } + } + } + return params; + }; + removeValue(params, 'where', '{"type.name":{"eq":"investigation"}}'); + req.url = `${url}?${params.toString()}`; + + // our count may have 1 extra at times, but it shouldn't matter too much... + req.continue(); + } + ).as('getDataPublicationsCount'); + cy.intercept( + /\/datapublications\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22investigation%22%7D%7D.*/, + (req) => { + // delete type = investigation requirement + const [url, search] = req.url.split('?'); + const params = new URLSearchParams(search); + // params.delete with value is still a new standard, so use workaround for now until browser compat catches up + // params.delete('where', '{"type.name":{"eq":"investigation"}}'); + const removeValue = ( + params: URLSearchParams, + key: string, + valueToRemove: string + ): URLSearchParams => { + const values = params.getAll(key); + if (values.length) { + params.delete(key); + for (const value of values) { + if (value !== valueToRemove) { + params.append(key, value); + } + } + } + return params; + }; + removeValue(params, 'where', '{"type.name":{"eq":"investigation"}}'); + req.url = `${url}?${params.toString()}`; + + req.continue((res) => { + // remove the "study" datapublication i.e. the one whose ID is in the URL + const i = res.body.findIndex((dp) => dp.id === 57); + if (i !== -1) res.body.splice(i, 1); + }); + } + ).as('getDataPublications'); + cy.login(); + cy.visit( + '/browseDataPublications/instrument/13/dataPublication/57/investigation?view=card' + ).wait(['@getDataPublicationsCount', '@getDataPublications'], { + timeout: 10000, + }); + }); + + it('should load correctly', () => { + cy.title().should('equal', 'DataGateway DataView'); + cy.get('#datagateway-dataview').should('be.visible'); + + cy.get('[data-testid="card"]') + .first() + .get('[data-testid="landing-datapublication-card-pid-link"]') + .first() + .then(($pid) => { + const pid = $pid.text(); + + const url = `https://doi.org/${pid}`; + + cy.get('[data-testid="card"]') + .first() + .get('[data-testid="landing-datapublication-card-pid-link"]') + .first() + .should('have.attr', 'href', url); + }); + + //Default sort + cy.contains('[role="button"]', 'desc').should('exist'); + cy.get('.MuiTableSortLabel-iconDirectionDesc').should('be.visible'); + }); + + it('should be able to click a datapublication to see its landing page', () => { + cy.get('[data-testid="card"]') + .first() + .contains('Leg') + .click({ force: true }); + cy.location('pathname').should( + 'eq', + '/browseDataPublications/instrument/13/dataPublication/57/investigation/56' + ); + }); + + it('should be able to sort by one field or multiple', () => { + //Revert the default sort + cy.contains('[role="button"]', 'Publication Date') + .as('dateSortButton') + .click(); + + // ascending + cy.get('@dateSortButton').click(); + cy.contains('[role="button"]', 'asc').should('exist'); + cy.contains('[role="button"]', 'desc').should('not.exist'); + cy.get('[data-testid="card"]').first().contains('Because'); + + // descending + cy.get('@dateSortButton').click(); + cy.contains('[role="button"]', 'asc').should('not.exist'); + cy.contains('[role="button"]', 'desc').should('exist'); + cy.get('[data-testid="card"]').first().contains('Leg'); + + // no order + cy.get('@dateSortButton').click(); + cy.contains('[role="button"]', 'asc').should('not.exist'); + cy.contains('[role="button"]', 'desc').should('not.exist'); + cy.get('[data-testid="card"]').first().contains('Because'); + + // multiple fields (shift click) + cy.contains('[role="button"]', 'Title').click(); + cy.get('@dateSortButton').click({ shiftKey: true }); + + cy.contains('[aria-label="Sort by TITLE"]', 'asc').should('exist'); + cy.contains('[aria-label="Sort by PUBLICATION DATE"]', 'asc').should( + 'exist' + ); + cy.contains('[role="button"]', 'desc').should('not.exist'); + cy.get('[data-testid="card"]').first().contains('Because'); + + // should replace current sort if clicked without shift + cy.get('@dateSortButton').click(); + + cy.contains('[aria-label="Sort by PUBLICATION DATE"]', 'desc').should( + 'exist' + ); + cy.contains('[aria-label="Sort by TITLE"]', 'asc').should('not.exist'); + + cy.get('[data-testid="card"]').first().contains('Leg'); + }); + + it('should be able to filter by multiple fields', () => { + cy.get('[data-testid="advanced-filters-link"]').click(); + + cy.get('[aria-label="Filter by DOI"]').first().type('0'); + cy.wait(['@getDataPublicationsCount', '@getDataPublications'], { + timeout: 10000, + }); + cy.get('[data-testid="card"]').first().contains('Leg'); + + cy.get('[aria-label="Filter by Title"]').first().type('fin'); + cy.wait(['@getDataPublicationsCount', '@getDataPublications'], { + timeout: 10000, + }); + cy.get('[data-testid="card"]').first().contains('Because'); + }); +}); diff --git a/packages/datagateway-dataview/cypress/e2e/card/isis/investigations.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/isis/investigations.cy.ts index 0c6cd802c..5299287f7 100644 --- a/packages/datagateway-dataview/cypress/e2e/card/isis/investigations.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/card/isis/investigations.cy.ts @@ -1,7 +1,17 @@ describe('ISIS - Investigations Cards', () => { beforeEach(() => { cy.intercept('**/investigations/count*').as('getInvestigationsCount'); - cy.intercept('**/investigations?order*').as('getInvestigationsOrder'); + cy.intercept('**/investigations?order*', (req) => { + req.continue((res) => { + // add type study to related data publication to emulate ISIS like data + if ( + res.body?.[0]?.dataCollectionInvestigations?.[0]?.dataCollection + ?.dataPublications?.[0] + ) + res.body[0].dataCollectionInvestigations[0].dataCollection.dataPublications[0].type = + { id: 1, name: 'study' }; + }); + }).as('getInvestigationsOrder'); cy.login(); cy.visit( '/browse/instrument/13/facilityCycle/12/investigation?view=card' diff --git a/packages/datagateway-dataview/cypress/e2e/card/isis/studyDataPublications.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/isis/studyDataPublications.cy.ts new file mode 100644 index 000000000..6d0427203 --- /dev/null +++ b/packages/datagateway-dataview/cypress/e2e/card/isis/studyDataPublications.cy.ts @@ -0,0 +1,162 @@ +describe('ISIS - Study Data Publication Cards', () => { + beforeEach(() => { + cy.intercept( + /\/datapublications\/count\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22study%22%7D%7D.*/, + (req) => { + // delete type = study requirement + const [url, search] = req.url.split('?'); + const params = new URLSearchParams(search); + // params.delete with value is still a new standard, so use workaround for now until browser compat catches up + // params.delete('where', '{"type.name":{"eq":"study"}}'); + const removeValue = ( + params: URLSearchParams, + key: string, + valueToRemove: string + ): URLSearchParams => { + const values = params.getAll(key); + if (values.length) { + params.delete(key); + for (const value of values) { + if (value !== valueToRemove) { + params.append(key, value); + } + } + } + return params; + }; + removeValue(params, 'where', '{"type.name":{"eq":"study"}}'); + req.url = `${url}?${params.toString()}`; + + req.continue(); + } + ).as('getDataPublicationsCount'); + cy.intercept( + /\/datapublications\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22study%22%7D%7D.*/, + (req) => { + // delete type = study requirement + const [url, search] = req.url.split('?'); + const params = new URLSearchParams(search); + // params.delete with value is still a new standard, so use workaround for now until browser compat catches up + // params.delete('where', '{"type.name":{"eq":"study"}}'); + const removeValue = ( + params: URLSearchParams, + key: string, + valueToRemove: string + ): URLSearchParams => { + const values = params.getAll(key); + if (values.length) { + params.delete(key); + for (const value of values) { + if (value !== valueToRemove) { + params.append(key, value); + } + } + } + return params; + }; + removeValue(params, 'where', '{"type.name":{"eq":"study"}}'); + req.url = `${url}?${params.toString()}`; + + req.continue(); + } + ).as('getDataPublications'); + cy.login(); + cy.visit( + '/browseDataPublications/instrument/8/dataPublication?view=card' + ).wait(['@getDataPublicationsCount', '@getDataPublications'], { + timeout: 10000, + }); + }); + + it('should load correctly', () => { + cy.title().should('equal', 'DataGateway DataView'); + cy.get('#datagateway-dataview').should('be.visible'); + + cy.get('[data-testid="card"]') + .first() + .get('[data-testid="landing-datapublication-card-pid-link"]') + .first() + .then(($pid) => { + const pid = $pid.text(); + + const url = `https://doi.org/${pid}`; + + cy.get('[data-testid="card"]') + .first() + .get('[data-testid="landing-datapublication-card-pid-link"]') + .first() + .should('have.attr', 'href', url); + }); + + //Default sort + cy.contains('[role="button"]', 'desc').should('exist'); + cy.get('.MuiTableSortLabel-iconDirectionDesc').should('be.visible'); + }); + + it('should be able to click a datapublication to see its landing page', () => { + cy.get('[data-testid="card"]') + .first() + .contains('Daughter') + .click({ force: true }); + cy.location('pathname').should( + 'eq', + '/browseDataPublications/instrument/8/dataPublication/50' + ); + }); + + it('should be able to sort by one field or multiple', () => { + //Revert the default sort + cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); + + // ascending + cy.contains('[role="button"]', 'DOI').as('doiSortButton').click(); + cy.contains('[role="button"]', 'asc').should('exist'); + cy.contains('[role="button"]', 'desc').should('not.exist'); + cy.get('[data-testid="card"]').first().contains('Article'); + + // descending + cy.get('@doiSortButton').click(); + cy.contains('[role="button"]', 'asc').should('not.exist'); + cy.contains('[role="button"]', 'desc').should('exist'); + cy.get('[data-testid="card"]').first().contains('Daughter'); + + // no order + cy.get('@doiSortButton').click(); + cy.contains('[role="button"]', 'asc').should('not.exist'); + cy.contains('[role="button"]', 'desc').should('not.exist'); + cy.get('[data-testid="card"]').first().contains('Article'); + + // multiple fields (shift click) + cy.get('@titleSortButton').click(); + cy.get('@doiSortButton').click({ shiftKey: true }); + + cy.contains('[aria-label="Sort by TITLE"]', 'asc').should('exist'); + cy.contains('[aria-label="Sort by DOI"]', 'asc').should('exist'); + cy.contains('[role="button"]', 'desc').should('not.exist'); + cy.get('[data-testid="card"]').first().contains('Article'); + + // should replace current sort if clicked without shift + cy.get('@doiSortButton').click(); + + cy.contains('[aria-label="Sort by DOI"]', 'desc').should('exist'); + cy.contains('[aria-label="Sort by TITLE"]', 'asc').should('not.exist'); + + cy.get('[data-testid="card"]').first().contains('Daughter'); + }); + + it('should be able to filter by multiple fields', () => { + cy.get('[data-testid="advanced-filters-link"]').click(); + + cy.get('[aria-label="Filter by DOI"]').first().type('5'); + + cy.contains('Results: 2').should('exist'); + + cy.get('[data-testid="card"]').first().contains('Consider'); + + cy.get('[aria-label="Filter by Title"]').first().type('sub'); + + cy.contains('Results: 1').should('exist'); + + cy.get('[data-testid="card"]').first().contains('Article'); + }); +}); diff --git a/packages/datagateway-dataview/cypress/e2e/datafilePreview.cy.ts b/packages/datagateway-dataview/cypress/e2e/datafilePreview.cy.ts index a70879fd3..7a8757e2e 100644 --- a/packages/datagateway-dataview/cypress/e2e/datafilePreview.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/datafilePreview.cy.ts @@ -2,6 +2,10 @@ import { join } from 'path'; describe('Datafile preview', () => { beforeEach(() => { + cy.intercept('GET', '**/datafiles/findone*', { + statusCode: 200, + fixture: 'datafile.json', + }); cy.intercept('GET', '**/datafiles*', { statusCode: 200, fixture: 'datafile.json', @@ -18,7 +22,7 @@ describe('Datafile preview', () => { }); cy.login(); cy.visit( - '/browse/instrument/1/facilityCycle/19/investigation/19/dataset/139/datafile/3484', + '/browse/instrument/1/facilityCycle/19/investigation/19/dataset/79/datafile/3484', { onBeforeLoad(win: Cypress.AUTWindow) { cy.spy(win.navigator.clipboard, 'writeText').as('copy'); @@ -84,7 +88,7 @@ describe('Datafile preview', () => { cy.contains('Copy link').click(); cy.get('@copy').should( 'be.calledOnceWithExactly', - 'http://127.0.0.1:3000/browse/instrument/1/facilityCycle/19/investigation/19/dataset/139/datafile/3484' + 'http://127.0.0.1:3000/browse/instrument/1/facilityCycle/19/investigation/19/dataset/79/datafile/3484' ); // should show a successful after copy is successful cy.contains('Link copied to clipboard').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/landing/isis/investigation.cy.ts b/packages/datagateway-dataview/cypress/e2e/landing/isis/investigation.cy.ts index 6ac5c88ca..7f47ee3e5 100644 --- a/packages/datagateway-dataview/cypress/e2e/landing/isis/investigation.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/landing/isis/investigation.cy.ts @@ -1,5 +1,16 @@ describe('ISIS - Investigation Landing', () => { beforeEach(() => { + cy.intercept('**/investigations?order*', (req) => { + req.continue((res) => { + // add type study to related data publication to emulate ISIS like data + if ( + res.body?.[0]?.dataCollectionInvestigations?.[0]?.dataCollection + ?.dataPublications?.[0] + ) + res.body[0].dataCollectionInvestigations[0].dataCollection.dataPublications[0].type = + { id: 1, name: 'study' }; + }); + }); cy.login(); cy.visit('/browse/instrument/13/facilityCycle/12/investigation/31'); }); @@ -86,6 +97,10 @@ describe('ISIS - Investigation Landing', () => { modTime: '2019-06-10', createTime: '2019-06-11', title: 'Data Publication', + type: { + id: 1, + name: 'study', + }, }, ], }, diff --git a/packages/datagateway-dataview/cypress/e2e/landing/isis/investigationDataPublication.cy.ts b/packages/datagateway-dataview/cypress/e2e/landing/isis/investigationDataPublication.cy.ts new file mode 100644 index 000000000..4a3863b55 --- /dev/null +++ b/packages/datagateway-dataview/cypress/e2e/landing/isis/investigationDataPublication.cy.ts @@ -0,0 +1,151 @@ +describe('ISIS - Investigation Data Publication Landing', () => { + beforeEach(() => { + cy.login(); + cy.visit( + '/browseDataPublications/instrument/13/dataPublication/57/investigation/46' + ); + }); + + it('should load correctly', () => { + cy.title().should('equal', 'DataGateway DataView'); + cy.get('#datagateway-dataview').should('be.visible'); + cy.get('#investigation-details-panel') + .contains('Because fine have business') + .should('be.visible'); + cy.get('#investigation-details-panel') + .contains('a', '0-686-22941-X') + .should('have.attr', 'href', 'https://doi.org/0-686-22941-X'); + cy.get('#investigation-details-panel') + .contains('INSTRUMENT 13') + .should('be.visible'); + + cy.get('[aria-label="landing-investigation-part-label"').should( + 'have.length', + 2 + ); + }); + + it('should be able to click tab to see datasets', () => { + cy.get('#investigation-datasets-tab').first().click({ force: true }); + cy.location('pathname').should( + 'eq', + '/browseDataPublications/instrument/13/dataPublication/57/investigation/46/dataset' + ); + cy.contains('Results: 2').should('be.visible'); + }); + + it('should be able to click a specific dataset', () => { + cy.get('[aria-label="landing-investigation-part-label"') + .children() + .first() + .click({ force: true }); + cy.location('pathname').should( + 'eq', + '/browseDataPublications/instrument/13/dataPublication/57/investigation/46/dataset/15' + ); + }); + + it('should load correctly when investigation missing', () => { + cy.intercept('**/datapublications?order*', (req) => { + req.continue((res) => { + if ( + res.body?.[0]?.content?.dataCollectionInvestigations?.[0] + ?.investigation + ) + delete res.body[0].content.dataCollectionInvestigations[0] + .investigation; + }); + }); + + cy.get('#investigation-details-panel') + .contains('Because fine have business') + .should('be.visible'); + cy.get('#investigation-details-panel') + .contains('a', '0-686-22941-X') + .should('have.attr', 'href', 'https://doi.org/0-686-22941-X'); + cy.get('#investigation-details-panel') + .contains('INSTRUMENT 13') + .should('not.exist'); + + cy.get('[aria-label="landing-investigation-part-label"').should( + 'not.exist' + ); + }); + + it('should disable the hover tool tip by pressing escape', () => { + cy.intercept('**/datapublications?*', [ + { + id: 36, + pid: '10.5286/ISIS.E.RB1810842', + }, + ]); + cy.get('[data-testid="isis-investigations-landing-parent-doi-link"]') + .first() + .trigger('mouseover'); + cy.get('[role="tooltip"]').should('exist'); + + cy.get('body').type('{esc}'); + + cy.get('[data-testid="isis-investigation-landing-doi-link"]') + .first() + .get('[role="tooltip"]') + .should('not.exist'); + }); + + it('should be able to use the citation formatter', () => { + cy.intercept('**/datapublications?*', [ + { + id: 101224979, + pid: '10.5286/ISIS.E.RB1810842', + }, + ]); + cy.intercept('**/text/x-bibliography/10.5286/ISIS.E.RB1810842?*', [ + '@misc{dr sabrina gaertner_mr vincent deguin_dr pierre ghesquiere_dr claire...}', + ]); + + cy.contains('10.5286/ISIS.E.RB1810842').should('be.visible'); + cy.get('[data-testid="citation-formatter-citation"]').contains( + 'STFC ISIS Neutron and Muon Source, https://doi.org/10.5286/ISIS.E.RB1810842' + ); + + cy.get('#citation-formatter').click(); + cy.get('[role="listbox"]') + .find('[role="option"]') + .should('have.length.gte', 2); + + cy.get('[role="option"][data-value="bibtex"]').click(); + cy.get('[data-testid="citation-formatter-citation"]').contains( + '@misc{dr sabrina gaertner_mr vincent deguin_dr pierre ghesquiere_dr claire' + ); + cy.get('#citation-formatter-error-message').should('not.exist'); + }); + + it('citation formatter should give an error when there is a problem', () => { + cy.intercept('**/datapublications?*', [ + { + id: 101224979, + pid: 'invaliddoi', + }, + ]); + cy.intercept('**/text/x-bibliography/invaliddoi?*', { + statusCode: 503, + }); + + cy.contains('invaliddoi').should('be.visible'); + + //Default citation + cy.get('[data-testid="citation-formatter-citation"]').contains( + 'STFC ISIS Neutron and Muon Source, https://doi.org/invaliddoi' + ); + + cy.get('#citation-formatter').click(); + cy.get('[role="listbox"]') + .find('[role="option"]') + .should('have.length.gte', 2); + + cy.get('[role="option"][data-value="chicago-author-date"]').click(); + cy.get('#citation-formatter-error-message', { timeout: 10000 }).should( + 'exist' + ); + }); +}); diff --git a/packages/datagateway-dataview/cypress/e2e/landing/isis/dataPublication.cy.ts b/packages/datagateway-dataview/cypress/e2e/landing/isis/studyDataPublication.cy.ts similarity index 58% rename from packages/datagateway-dataview/cypress/e2e/landing/isis/dataPublication.cy.ts rename to packages/datagateway-dataview/cypress/e2e/landing/isis/studyDataPublication.cy.ts index 9e58009c4..944f1d5d8 100644 --- a/packages/datagateway-dataview/cypress/e2e/landing/isis/dataPublication.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/landing/isis/studyDataPublication.cy.ts @@ -1,29 +1,60 @@ -describe('ISIS - Data Publication Landing', () => { +describe('ISIS - Study Data Publication Landing', () => { beforeEach(() => { + cy.intercept( + /\/datapublications\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22investigation%22%7D%7D.*/, + (req) => { + // delete type = investigation requirement + const [url, search] = req.url.split('?'); + const params = new URLSearchParams(search); + // params.delete with value is still a new standard, so use workaround for now until browser compat catches up + // params.delete('where', '{"type.name":{"eq":"investigation"}}'); + const removeValue = ( + params: URLSearchParams, + key: string, + valueToRemove: string + ): URLSearchParams => { + const values = params.getAll(key); + if (values.length) { + params.delete(key); + for (const value of values) { + if (value !== valueToRemove) { + params.append(key, value); + } + } + } + return params; + }; + removeValue(params, 'where', '{"type.name":{"eq":"investigation"}}'); + req.url = `${url}?${params.toString()}`; + + req.continue((res) => { + // remove the "study" datapublication i.e. the one whose ID is in the URL + const i = res.body.findIndex((dp) => dp.id === 57); + if (i !== -1) res.body.splice(i, 1); + }); + } + ).as('getDataPublications'); + cy.login(); - cy.visit('/browseDataPublications/instrument/1/dataPublication/36'); + cy.visit('/browseDataPublications/instrument/13/dataPublication/57'); }); it('should load correctly', () => { cy.title().should('equal', 'DataGateway DataView'); cy.get('#datagateway-dataview').should('be.visible'); - cy.contains('Yourself good together red across.').should('be.visible'); - cy.contains('a', '0-7602-7584-X').should( - 'have.attr', - 'href', - 'https://doi.org/0-7602-7584-X' + cy.contains('Dream he television').should('be.visible'); + cy.get('[data-testid="isis-dataPublication-landing"]') + .contains('Because fine have business') + .should('be.visible'); + + cy.get('[data-testid="isis-dataPublication-landing"]') + .contains('a', '1-64379-596-1') + .should('have.attr', 'href', 'https://doi.org/1-64379-596-1'); + + cy.get('[data-testid="landing-datapublication-part-label"').should( + 'have.length', + 2 ); - cy.get('[data-testid="landing-dataPublication-pid-link"]') - .first() - .then(($pid) => { - const pid = $pid.text(); - - const url = `https://doi.org/${pid}`; - - cy.get('[data-testid="landing-dataPublication-pid-link"]') - .first() - .should('have.attr', 'href', url); - }); }); it('should be able to click tab to see investigations', () => { @@ -32,7 +63,7 @@ describe('ISIS - Data Publication Landing', () => { .click({ force: true }); cy.location('pathname').should( 'eq', - '/browseDataPublications/instrument/1/dataPublication/36/investigation' + '/browseDataPublications/instrument/13/dataPublication/57/investigation' ); }); @@ -43,20 +74,20 @@ describe('ISIS - Data Publication Landing', () => { .click({ force: true }); cy.location('pathname').should( 'eq', - '/browseDataPublications/instrument/1/dataPublication/36/investigation/38' + '/browseDataPublications/instrument/13/dataPublication/57/investigation/46' ); }); it('should load correctly when investigation missing', () => { - cy.intercept('**/datapublications?*', [ - { - id: 101224979, - pid: '10.5286/ISIS.E.RB1810842', - }, - ]); - cy.visit('/browseDataPublications/instrument/1/dataPublication/36'); - cy.get('#datagateway-dataview').should('be.visible'); - cy.contains('10.5286/ISIS.E.RB1810842').should('be.visible'); + cy.intercept( + /\/datapublications\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22investigation%22%7D%7D.*/, + [] + ); + + cy.contains('1-64379-596-1').should('be.visible'); + cy.get('[data-testid="landing-datapublication-part-label"').should( + 'not.exist' + ); }); it('should disable the hover tool tip by pressing escape', () => { diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/investigationDataPublications.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/investigationDataPublications.cy.ts new file mode 100644 index 000000000..41b4cce8b --- /dev/null +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/investigationDataPublications.cy.ts @@ -0,0 +1,192 @@ +describe('ISIS - Investigation Data Publication Table', () => { + beforeEach(() => { + cy.intercept('**/datapublications/count*').as('getDataPublicationsCount'); + cy.intercept( + /\/datapublications\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22investigation%22%7D%7D.*/, + (req) => { + // delete type = investigation requirement + const [url, search] = req.url.split('?'); + const params = new URLSearchParams(search); + // params.delete with value is still a new standard, so use workaround for now until browser compat catches up + // params.delete('where', '{"type.name":{"eq":"investigation"}}'); + const removeValue = ( + params: URLSearchParams, + key: string, + valueToRemove: string + ): URLSearchParams => { + const values = params.getAll(key); + if (values.length) { + params.delete(key); + for (const value of values) { + if (value !== valueToRemove) { + params.append(key, value); + } + } + } + return params; + }; + removeValue(params, 'where', '{"type.name":{"eq":"investigation"}}'); + req.url = `${url}?${params.toString()}`; + + req.continue((res) => { + // remove the "study" datapublication i.e. the one whose ID is in the URL + const i = res.body.findIndex((dp) => dp.id === 57); + if (i !== -1) res.body.splice(i, 1); + }); + } + ).as('getDataPublications'); + cy.login(); + cy.visit( + '/browseDataPublications/instrument/13/dataPublication/57/investigation' + ).wait(['@getDataPublicationsCount', '@getDataPublications'], { + timeout: 10000, + }); + }); + + it('should load correctly', () => { + cy.title().should('equal', 'DataGateway DataView'); + cy.get('#datagateway-dataview').should('be.visible'); + + //Default sort + cy.get('[aria-sort="descending"]').should('exist'); + cy.get('.MuiTableSortLabel-iconDirectionDesc').should('be.visible'); + }); + + it('should be able to click a data publication to see its landing page', () => { + cy.get('[role="gridcell"] a').first().click({ force: true }); + cy.location('pathname').should( + 'eq', + '/browseDataPublications/instrument/13/dataPublication/57/investigation/56' + ); + }); + + it('should have the correct url for the DOI link', () => { + cy.get('[data-testid="isis-datapublication-table-doi-link"]') + .first() + .then(($doi) => { + const doi = $doi.text(); + + const url = `https://doi.org/${doi}`; + + cy.get('[data-testid="isis-datapublication-table-doi-link"]') + .first() + .should('have.attr', 'href', url); + }); + }); + + // Not enough data in datapublications to load. + it.skip('should be able to scroll down and load more rows', () => { + cy.get('[aria-rowcount="50"]').should('exist'); + cy.get('[aria-label="grid"]').scrollTo('bottom'); + cy.get('[aria-rowcount="75"]').should('exist'); + }); + + it('should be able to sort by all sort directions on single and multiple columns', () => { + //Revert the default sort + cy.contains('[role="button"]', 'Publication Date') + .as('dateSortButton') + .click(); + + // ascending order + cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); + + cy.get('[aria-sort="ascending"]').should('exist'); + cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); + cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( + 'Because fine have business' + ); + + // descending order + cy.get('@titleSortButton').click(); + + cy.get('[aria-sort="descending"]').should('exist'); + cy.get('.MuiTableSortLabel-iconDirectionDesc').should( + 'not.have.css', + 'opacity', + '0' + ); + cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( + 'Leg can time eat' + ); + + // no order + cy.get('@titleSortButton').click(); + + cy.get('[aria-sort="ascending"]').should('not.exist'); + cy.get('[aria-sort="descending"]').should('not.exist'); + cy.get('.MuiTableSortLabel-iconDirectionAsc').should('not.exist'); + + cy.get('[data-testid="SortIcon"]').should('have.length', 3); + cy.get('[data-testid="ArrowUpwardIcon"]').should('not.exist'); + + cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( + 'Because fine have business' + ); + + // multiple columns (shift click) + cy.get('@dateSortButton').click(); + cy.get('@dateSortButton').click({ shiftKey: true }); + cy.get('@titleSortButton').click({ shiftKey: true }); + + cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( + 'Leg can time eat' + ); + + // should replace previous sort when clicked without shift + cy.get('@dateSortButton').click(); + cy.get('[aria-sort="ascending"]').should('have.length', 1); + cy.get('[aria-rowindex="1"] [aria-colindex="3"]').contains('2014-12-02'); + }); + + it('should change icons when sorting on a column', () => { + // clear default sort + cy.contains('[role="button"]', 'Publication Date').click(); + + cy.get('[data-testid="SortIcon"]').should('have.length', 3); + + // check icon when clicking on a column + cy.contains('[role="button"]', 'DOI').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); + cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); + + // check icon when clicking on a column again + cy.contains('[role="button"]', 'DOI').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); + cy.get('.MuiTableSortLabel-iconDirectionAsc').should('not.exist'); + + // check icon when hovering over a column + cy.contains('[role="button"]', 'Title').trigger('mouseover'); + cy.get('[data-testid="ArrowUpwardIcon"]').should('have.length', 1); + cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); + + // check icons when shift is held + cy.get('.App').trigger('keydown', { key: 'Shift' }); + cy.get('[data-testid="AddIcon"]').should('have.length', 1); + }); + + it('should be able to filter with both text & date filters on multiple columns', () => { + // test date filter + cy.get('input[id="Publication Date filter to"]').type('2016-01-01'); + + cy.get('[aria-rowcount="1"]').should('exist'); + cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( + 'Because fine have business' + ); + + cy.get('input[id="Publication Date filter from"]').type('2015-01-01'); + + cy.get('[aria-rowcount="0"]').should('exist'); + + cy.get('input[id="Publication Date filter from"]').type( + '{ctrl}a{backspace}' + ); + cy.get('input[id="Publication Date filter from"]').type('2014-01-01'); + + cy.get('[aria-rowcount="1"]').should('exist'); + + // test text filter + cy.get('[aria-label="Filter by Title"]').first().type('xy'); + + cy.get('[aria-rowcount="0"]').should('exist'); + }); +}); diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/investigations.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/investigations.cy.ts index 1ba9ec19c..fd63bc511 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/investigations.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/investigations.cy.ts @@ -1,7 +1,18 @@ describe('ISIS - Investigations Table', () => { beforeEach(() => { cy.intercept('**/investigations/count*').as('getInvestigationsCount'); - cy.intercept('**/investigations?order*').as('getInvestigationsOrder'); + cy.intercept('**/investigations?order*', (req) => { + req.continue((res) => { + // add type study to related data publication to emulate ISIS like data + if ( + res.body?.[0]?.dataCollectionInvestigations?.[0]?.dataCollection + ?.dataPublications?.[0] + ) + res.body[0].dataCollectionInvestigations[0].dataCollection.dataPublications[0].type = + { id: 1, name: 'study' }; + }); + }).as('getInvestigationsOrder'); + cy.login(); cy.visit('/browse/instrument/13/facilityCycle/12/investigation').wait( ['@getInvestigationsCount', '@getInvestigationsOrder'], diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/myData.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/myData.cy.ts index e4b2e6923..68624dbeb 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/myData.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/myData.cy.ts @@ -7,6 +7,17 @@ describe('ISIS - MyData Table', () => { describe('Logged in tests', () => { beforeEach(() => { cy.intercept('**/investigations/count?*').as('getInvestigationCount'); + cy.intercept('**/investigations?order*', (req) => { + req.continue((res) => { + // add type study to related data publication to emulate ISIS like data + if ( + res.body?.[0]?.dataCollectionInvestigations?.[0]?.dataCollection + ?.dataPublications?.[0] + ) + res.body[0].dataCollectionInvestigations[0].dataCollection.dataPublications[0].type = + { id: 1, name: 'study' }; + }); + }); cy.login( { username: 'root', diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/dataPublications.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/studyDataPublications.cy.ts similarity index 69% rename from packages/datagateway-dataview/cypress/e2e/table/isis/dataPublications.cy.ts rename to packages/datagateway-dataview/cypress/e2e/table/isis/studyDataPublications.cy.ts index 0b0fee3ec..3dd208c23 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/dataPublications.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/studyDataPublications.cy.ts @@ -1,10 +1,42 @@ -describe('ISIS - Data Publication Table', () => { +describe('ISIS - Study Data Publication Table', () => { beforeEach(() => { cy.intercept('**/datapublications/count*').as('getDataPublicationsCount'); - cy.intercept('**/datapublications?order*').as('getDataPublicationsOrder'); + cy.intercept( + /\/datapublications\?.*where=%7B%22type\.name%22%3A%7B%22eq%22%3A%22study%22%7D%7D.*/, + (req) => { + console.log('INTERCEPTING!!!!!'); + // delete type = study requirement + const [url, search] = req.url.split('?'); + const params = new URLSearchParams(search); + // params.delete with value is still a new standard, so use workaround for now until browser compat catches up + // params.delete('where', '{"type.name":{"eq":"study"}}'); + const removeValue = ( + params: URLSearchParams, + key: string, + valueToRemove: string + ): URLSearchParams => { + const values = params.getAll(key); + if (values.length) { + params.delete(key); + for (const value of values) { + if (value !== valueToRemove) { + params.append(key, value); + } + } + } + return params; + }; + removeValue(params, 'where', '{"type.name":{"eq":"study"}}'); + req.url = `${url}?${params.toString()}`; + console.log('req.url', req.url); + + req.continue(); + } + ).as('getDataPublications'); + cy.login(); cy.visit('/browseDataPublications/instrument/8/dataPublication').wait( - ['@getDataPublicationsCount', '@getDataPublicationsOrder'], + ['@getDataPublicationsCount', '@getDataPublications'], { timeout: 10000 } ); }); @@ -22,7 +54,7 @@ describe('ISIS - Data Publication Table', () => { cy.get('[role="gridcell"] a').first().click({ force: true }); cy.location('pathname').should( 'eq', - '/browseDataPublications/instrument/8/dataPublication/51' + '/browseDataPublications/instrument/8/dataPublication/50' ); }); @@ -49,12 +81,10 @@ describe('ISIS - Data Publication Table', () => { it('should be able to sort by all sort directions on single and multiple columns', () => { //Revert the default sort - cy.contains('[role="button"]', 'Publication Date') - .as('dateSortButton') - .click(); + cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); // ascending order - cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); + cy.contains('[role="button"]', 'DOI').as('doiSortButton').click(); cy.get('[aria-sort="ascending"]').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); @@ -63,7 +93,7 @@ describe('ISIS - Data Publication Table', () => { ); // descending order - cy.get('@titleSortButton').click(); + cy.get('@doiSortButton').click(); cy.get('[aria-sort="descending"]').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionDesc').should( @@ -76,13 +106,13 @@ describe('ISIS - Data Publication Table', () => { ); // no order - cy.get('@titleSortButton').click(); + cy.get('@doiSortButton').click(); cy.get('[aria-sort="ascending"]').should('not.exist'); cy.get('[aria-sort="descending"]').should('not.exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('not.exist'); - cy.get('[data-testid="SortIcon"]').should('have.length', 3); + cy.get('[data-testid="SortIcon"]').should('have.length', 2); cy.get('[data-testid="ArrowUpwardIcon"]').should('not.exist'); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( @@ -90,25 +120,25 @@ describe('ISIS - Data Publication Table', () => { ); // multiple columns (shift click) - cy.get('@dateSortButton').click(); - cy.get('@dateSortButton').click({ shiftKey: true }); + cy.get('@titleSortButton').click(); cy.get('@titleSortButton').click({ shiftKey: true }); + cy.get('@doiSortButton').click({ shiftKey: true }); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( - 'Church child time Congress' + 'Daughter experience discussion' ); // should replace previous sort when clicked without shift - cy.contains('[role="button"]', 'Publication Date').click(); + cy.get('@titleSortButton').click(); cy.get('[aria-sort="ascending"]').should('have.length', 1); - cy.get('[aria-rowindex="1"] [aria-colindex="3"]').contains('2010-02-24'); + cy.get('[aria-rowindex="1"] [aria-colindex="2"]').contains('0-356-85165-6'); }); it('should change icons when sorting on a column', () => { // clear default sort - cy.contains('[role="button"]', 'Publication Date').click(); + cy.contains('[role="button"]', 'Title').click(); - cy.get('[data-testid="SortIcon"]').should('have.length', 3); + cy.get('[data-testid="SortIcon"]').should('have.length', 2); // check icon when clicking on a column cy.contains('[role="button"]', 'DOI').click(); @@ -120,14 +150,14 @@ describe('ISIS - Data Publication Table', () => { cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('not.exist'); + // check icons when shift is held + cy.get('.App').trigger('keydown', { key: 'Shift' }); + cy.get('[data-testid="AddIcon"]').should('have.length', 1); + // check icon when hovering over a column cy.contains('[role="button"]', 'Title').trigger('mouseover'); cy.get('[data-testid="ArrowUpwardIcon"]').should('have.length', 1); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); - - // check icons when shift is held - cy.get('.App').trigger('keydown', { key: 'Shift' }); - cy.get('[data-testid="AddIcon"]').should('have.length', 1); }); it('should be able to filter with both text & date filters on multiple columns', () => { @@ -137,19 +167,7 @@ describe('ISIS - Data Publication Table', () => { cy.get('[aria-rowcount="3"]').should('exist'); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( - 'Church child time Congress' - ); - - // test date filter - cy.get('input[id="Publication Date filter to"]').type('2016-01-01'); - - cy.get('[aria-rowcount="2"]').should('exist'); - cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( - 'Consider author watch' + 'Daughter experience discussion' ); - - cy.get('input[id="Publication Date filter from"]').type('2014-01-01'); - - cy.get('[aria-rowcount="1"]').should('exist'); }); }); diff --git a/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json b/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json index 8b48c10eb..6c9366bb4 100644 --- a/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json +++ b/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json @@ -153,5 +153,24 @@ "order": 6 } ], + "breadcrumbs": [ + { + "matchEntity": "proposal", + "replaceEntity": "investigation", + "replaceEntityField": "title", + "replaceEntityQueryField": "name" + }, + { + "matchEntity": "investigation", + "replaceEntityField": "visitId", + "parentEntity": "proposal" + }, + { + "matchEntity": "investigation", + "replaceEntity": "dataPublication", + "replaceEntityField": "title", + "parentEntity": "dataPublication" + } + ], "pluginHost": "http://localhost:3000" } diff --git a/packages/datagateway-dataview/public/res/default.json b/packages/datagateway-dataview/public/res/default.json index 6f40ef68a..b97b1f7e5 100644 --- a/packages/datagateway-dataview/public/res/default.json +++ b/packages/datagateway-dataview/public/res/default.json @@ -20,6 +20,7 @@ "instrument_other": "Instruments", "facilityCycle_other": "Facility Cycles", "dataPublication_other": "Experiments", + "experiment_other": "Investigations", "proposal_other": "Proposals", "investigation_other": "Investigations", "dataset_other": "Datasets", @@ -329,13 +330,10 @@ "email": "isisdata@stfc.ac.uk", "logo": "https://data.isis.stfc.ac.uk/doi/ISIS/images/dsLogo.png" }, - "distribution": { - "format": "RAW/Nexus", - "content_url": "https://data.isis.stfc.ac.uk" - }, "branding": { "title": "ISIS Neutron and Muon Source", "body": "This is a page describing data taken during an experiment at the ISIS Neutron and Muon Source. Information about the ISIS Neutron and Muon Source can be found at https://www.isis.stfc.ac.uk." - } + }, + "license": "https://www.isis.stfc.ac.uk/Pages/Data-Policy.aspx" } } diff --git a/packages/datagateway-dataview/server/e2e-settings.json b/packages/datagateway-dataview/server/e2e-settings.json index 62e07f93d..395b7be54 100644 --- a/packages/datagateway-dataview/server/e2e-settings.json +++ b/packages/datagateway-dataview/server/e2e-settings.json @@ -9,16 +9,25 @@ "idsUrl": "https://localhost:8181/ids", "apiUrl": "http://localhost:5000", "downloadApiUrl": "https://localhost:8181/topcat", - "breadcrumbs": { - "proposal": { + "breadcrumbs": [ + { + "matchEntity": "proposal", "replaceEntity": "investigation", - "replaceEntityField": "title" + "replaceEntityField": "title", + "replaceEntityQueryField": "name" }, - "investigation": { + { + "matchEntity": "investigation", "replaceEntityField": "visitId", "parentEntity": "proposal" + }, + { + "matchEntity": "investigation", + "replaceEntity": "dataPublication", + "replaceEntityField": "title", + "parentEntity": "dataPublication" } - }, + ], "helpSteps": [ { "target": "#plugin-link--browse-investigation", diff --git a/packages/datagateway-dataview/src/index.test.tsx b/packages/datagateway-dataview/src/index.test.tsx index 86e3210d0..9069caa31 100644 --- a/packages/datagateway-dataview/src/index.test.tsx +++ b/packages/datagateway-dataview/src/index.test.tsx @@ -25,11 +25,12 @@ describe('index - fetchSettings', () => { features: {}, idsUrl: 'ids', apiUrl: 'api', - breadcrumbs: { - test: { + breadcrumbs: [ + { + matchEntity: 'test', replaceEntityField: 'title', }, - }, + ], downloadApiUrl: 'download-api', selectAllSetting: false, routes: [ @@ -78,11 +79,12 @@ describe('index - fetchSettings', () => { features: {}, idsUrl: 'ids', apiUrl: 'api', - breadcrumbs: { - test: { + breadcrumbs: [ + { + matchEntity: 'test', replaceEntityField: 'title', }, - }, + ], downloadApiUrl: 'download-api', selectAllSetting: false, routes: [ diff --git a/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx b/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx index 3d71d0e49..29c2a5394 100644 --- a/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx +++ b/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx @@ -23,17 +23,29 @@ jest.mock('loglevel'); const genericRoutes = { investigations: '/browse/investigation', datasets: '/browse/investigation/1/dataset', - datafiles: '/browse/investigation/1/dataset/1/datafile', + datafiles: '/browse/investigation/1/dataset/2/datafile', }; // The ISIS routes to test. const ISISRoutes = { instruments: '/browse/instrument', facilityCycles: '/browse/instrument/1/facilityCycle', - investigations: '/browse/instrument/1/facilityCycle/1/investigation', - datasets: '/browse/instrument/1/facilityCycle/1/investigation/1/dataset', + investigations: '/browse/instrument/1/facilityCycle/2/investigation', + datasets: '/browse/instrument/1/facilityCycle/2/investigation/3/dataset', datafiles: - '/browse/instrument/1/facilityCycle/1/investigation/1/dataset/1/datafile', + '/browse/instrument/1/facilityCycle/2/investigation/3/dataset/4/datafile', +}; + +// The ISIS experiments +const ISISExperimentsRoutes = { + instruments: '/browseDataPublications/instrument', + studyDataPublications: '/browseDataPublications/instrument/1/dataPublication', + investigationDataPublications: + '/browseDataPublications/instrument/1/dataPublication/2/investigation', + datasets: + '/browseDataPublications/instrument/1/dataPublication/2/investigation/3/dataset', + datafiles: + '/browseDataPublications/instrument/1/dataPublication/2/investigation/3/dataset/4/datafile', }; // The DLS routes to test. @@ -42,7 +54,7 @@ const DLSRoutes = { investigations: '/browse/proposal/INVESTIGATION 1/investigation', datasets: '/browse/proposal/INVESTIGATION 1/investigation/1/dataset', datafiles: - '/browse/proposal/INVESTIGATION 1/investigation/1/dataset/1/datafile', + '/browse/proposal/INVESTIGATION 1/investigation/1/dataset/2/datafile', }; describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { @@ -74,16 +86,27 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { ...dgDataViewInitialState, // Set up the breadcrumb settings. - breadcrumbSettings: { - proposal: { + breadcrumbSettings: [ + // DLS settings + { + matchEntity: 'proposal', replaceEntity: 'investigation', replaceEntityField: 'title', + replaceEntityQueryField: 'name', }, - investigation: { + { + matchEntity: 'investigation', replaceEntityField: 'visitId', parentEntity: 'proposal', }, - }, + // ISIS settings + { + matchEntity: 'investigation', + replaceEntity: 'dataPublication', + replaceEntityField: 'title', + parentEntity: 'dataPublication', + }, + ], }, dgcommon: dGCommonInitialState, @@ -95,449 +118,675 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { }) ); - (axios.get as jest.Mock).mockImplementation(() => - Promise.resolve({ + (axios.get as jest.Mock).mockImplementation((url) => { + const potentialId = parseInt(url.split('/').at(-1)); + let id = 1; + if (!Number.isNaN(potentialId)) { + id = potentialId; + } + return Promise.resolve({ data: { - id: 1, - name: 'Name 1', - title: 'Title 1', - visitId: '1', + id: id, + name: `Name ${id}`, + title: `Title ${id}`, + visitId: `${id}`, }, - }) - ); + }); + }); }); afterEach(() => { (axios.get as jest.Mock).mockClear(); }); - it('generic route renders correctly at the base route and does not request', async () => { - // Set up test state pathname. - history.replace(createLocation(genericRoutes['investigations'])); + describe('Generic', () => { + it('generic route renders correctly at the base route and does not request', async () => { + // Set up test state pathname. + history.replace(createLocation(genericRoutes['investigations'])); - // Set up store with test state and mount the breadcrumb. - renderComponent(state); + // Set up store with test state and mount the breadcrumb. + renderComponent(state); - // Expect the axios.get to not have been made. - expect(axios.get).not.toBeCalled(); + // Expect the axios.get to not have been made. + expect(axios.get).not.toBeCalled(); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - expect(screen.getByTestId('Breadcrumb-base')).toHaveTextContent( - 'breadcrumbs.investigation' - ); - }); - - it('generic route renders correctly at the dataset level and requests the investigation entity', async () => { - // Set up test state pathname. - history.replace( - createLocation({ - pathname: genericRoutes['datasets'], - search: '?view=card', - }) - ); - - // Set up store with test state and mount the breadcrumb. - renderComponent(state); + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + expect(screen.getByTestId('Breadcrumb-base')).toHaveTextContent( + 'breadcrumbs.investigation' + ); + }); - // Expect the axios.get to have been called once to get the investigation. - expect(axios.get).toBeCalledTimes(1); - expect(axios.get).toHaveBeenCalledWith('/investigations/1', { - headers: { - Authorization: 'Bearer null', - }, + it('generic route renders correctly at the dataset level and requests the investigation entity', async () => { + // Set up test state pathname. + history.replace( + createLocation({ + pathname: genericRoutes['datasets'], + search: '?view=card', + }) + ); + + // Set up store with test state and mount the breadcrumb. + renderComponent(state); + + // Expect the axios.get to have been called once to get the investigation. + expect(axios.get).toBeCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith('/investigations/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute( + 'href', + '/browse/investigation?view=card' + ); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.investigation'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.dataset' + ) + ).toBeInTheDocument(); }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute( - 'href', - '/browse/investigation?view=card' - ); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.investigation'); + it('generic route renders correctly at the datafile level and requests the investigation & dataset entities', async () => { + // Set up test state pathname. + history.replace(createLocation(genericRoutes['datafiles'])); - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); + // Set up store with test state and mount the breadcrumb. + renderComponent(state); - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.dataset' - ) - ).toBeInTheDocument(); + // Expect the axios.get to have been called twice; first to get the investigation + // and second to get the dataset. + expect(axios.get).toBeCalledTimes(2); + expect(axios.get).toHaveBeenNthCalledWith(1, '/investigations/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(2, '/datasets/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/investigation'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.investigation'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); + expect(within(breadcrumbs[1]).getByText('Name 2')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.datafile' + ) + ).toBeInTheDocument(); + }); }); - it('generic route renders correctly at the datafile level and requests the investigation & dataset entities', async () => { - // Set up test state pathname. - history.replace(createLocation(genericRoutes['datafiles'])); + describe('DLS', () => { + it('DLS route renders correctly at the base level and does not request', async () => { + // Set up test state pathname. + history.replace(createLocation(DLSRoutes['proposals'])); - // Set up store with test state and mount the breadcrumb. - renderComponent(state); + // Set up store with test state and mount the breadcrumb. + renderComponent(state); - // Expect the axios.get to have been called twice; first to get the investigation - // and second to get the dataset. - expect(axios.get).toBeCalledTimes(2); - expect(axios.get).toHaveBeenNthCalledWith(1, '/investigations/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - expect(axios.get).toHaveBeenNthCalledWith(2, '/datasets/1', { - headers: { - Authorization: 'Bearer null', - }, + // Expect the axios.get to not have been called. + expect(axios.get).not.toBeCalled(); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/investigation'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.investigation'); + it('DLS route renders correctly at the investigation level and requests the proposal entity', async () => { + // Set up test state pathname. + history.replace(createLocation(DLSRoutes['investigations'])); + + // Set up store with test state and mount the breadcrumb. + renderComponent(state); + + // Expect the axios.get to have been called twice. + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith( + '/investigations/findone?where=' + + JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), + { + headers: { + Authorization: 'Bearer null', + }, + } + ); - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); - expect(within(breadcrumbs[1]).getByText('Name 1')).toBeInTheDocument(); + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/proposal'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); + }); - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.datafile' - ) - ).toBeInTheDocument(); + it('DLS route renders correctly at the dataset level and requests the proposal & investigation entities', async () => { + // Set up test state pathname. + history.replace(createLocation(DLSRoutes['datasets'])); + + // Set up store with test state and mount the breadcrumb. + renderComponent(state); + + // Expect the axios.get to have been called twice. + expect(axios.get).toHaveBeenCalledTimes(2); + expect(axios.get).toHaveBeenNthCalledWith( + 1, + '/investigations/findone?where=' + + JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), + { + headers: { + Authorization: 'Bearer null', + }, + } + ); + expect(axios.get).toHaveBeenNthCalledWith(2, '/investigations/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/proposal'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browse/proposal/INVESTIGATION 1/investigation' + ); + expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); + expect(within(breadcrumbs[1]).getByText('1')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.dataset' + ) + ).toBeInTheDocument(); + }); + + it('DLS route renders correctly at the datafile level and requests the proposal, investigation and dataset entities', async () => { + // Set up test state pathname. + history.replace(createLocation(DLSRoutes['datafiles'])); + + // Set up store with test state and mount the breadcrumb. + renderComponent(state); + + // Expect the axios.get to have been called three times. + expect(axios.get).toHaveBeenCalledTimes(3); + expect(axios.get).toHaveBeenNthCalledWith( + 1, + '/investigations/findone?where=' + + JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), + { + headers: { + Authorization: 'Bearer null', + }, + } + ); + expect(axios.get).toHaveBeenNthCalledWith(2, '/investigations/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(3, '/datasets/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/proposal'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browse/proposal/INVESTIGATION 1/investigation' + ); + expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); + expect(breadcrumbs[1]).toHaveAttribute( + 'href', + '/browse/proposal/INVESTIGATION 1/investigation/1/dataset' + ); + expect(within(breadcrumbs[1]).getByText('1')).toBeInTheDocument(); + expect(within(breadcrumbs[2]).getByText('Name 2')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.datafile' + ) + ).toBeInTheDocument(); + }); }); - it('DLS route renders correctly at the base level and does not request', async () => { - // Set up test state pathname. - history.replace(createLocation(DLSRoutes['proposals'])); + describe('ISIS', () => { + it('ISIS route renders correctly at the base level and does not request', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISRoutes['instruments'])); - // Set up store with test state and mount the breadcrumb. - renderComponent(state); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['investigation', 'dataset']); - // Expect the axios.get to not have been called. - expect(axios.get).not.toBeCalled(); + // Expect the axios.get not to have been called + expect(axios.get).not.toHaveBeenCalled(); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); - }); + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + expect(screen.getByTestId('Breadcrumb-base')).toHaveTextContent( + 'breadcrumbs.instrument' + ); + }); - it('DLS route renders correctly at the investigation level and requests the proposal entity', async () => { - // Set up test state pathname. - history.replace(createLocation(DLSRoutes['investigations'])); + it('ISIS route renders correctly at the facility cycle level and requests the instrument entity', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISRoutes['facilityCycles'])); - // Set up store with test state and mount the breadcrumb. - renderComponent(state); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['investigation', 'dataset']); - // Expect the axios.get to have been called twice. - expect(axios.get).toHaveBeenCalledTimes(1); - expect(axios.get).toHaveBeenCalledWith( - '/investigations/findone?where=' + - JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), - { + // Expect the axios.get to have been called three times. + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith('/instruments/1', { headers: { Authorization: 'Bearer null', }, - } - ); + }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/proposal'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); - }); + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); - it('DLS route renders correctly at the dataset level and requests the proposal & investigation entities', async () => { - // Set up test state pathname. - history.replace(createLocation(DLSRoutes['datasets'])); + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); - // Set up store with test state and mount the breadcrumb. - renderComponent(state); + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.facilityCycle' + ) + ).toBeInTheDocument(); + }); - // Expect the axios.get to have been called twice. - expect(axios.get).toHaveBeenCalledTimes(2); - expect(axios.get).toHaveBeenNthCalledWith( - 1, - '/investigations/findone?where=' + - JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), - { + it('ISIS route renders correctly at the investigation level and requests the instrument and facility cycle entities', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISRoutes['investigations'])); + + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['investigation', 'dataset']); + + // Expect the axios.get to have been called three times. + expect(axios.get).toHaveBeenCalledTimes(2); + expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { headers: { Authorization: 'Bearer null', }, - } - ); - expect(axios.get).toHaveBeenNthCalledWith(2, '/investigations/1', { - headers: { - Authorization: 'Bearer null', - }, + }); + expect(axios.get).toHaveBeenNthCalledWith(2, '/facilitycycles/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browse/instrument/1/facilityCycle' + ); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); + expect(within(breadcrumbs[1]).getByText('Name 2')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.investigation' + ) + ).toBeInTheDocument(); }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/proposal'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); + it('ISIS route renders correctly at the dataset level and requests the instrument, facility cycle and investigation entities', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISRoutes['datasets'])); - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(breadcrumbs[0]).toHaveAttribute( - 'href', - '/browse/proposal/INVESTIGATION 1/investigation' - ); - expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); - expect(within(breadcrumbs[1]).getByText('1')).toBeInTheDocument(); - - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.dataset' - ) - ).toBeInTheDocument(); - }); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['investigation', 'dataset']); - it('DLS route renders correctly at the datafile level and requests the proposal, investigation and dataset entities', async () => { - // Set up test state pathname. - history.replace(createLocation(DLSRoutes['datafiles'])); - - // Set up store with test state and mount the breadcrumb. - renderComponent(state); - - // Expect the axios.get to have been called three times. - expect(axios.get).toHaveBeenCalledTimes(3); - expect(axios.get).toHaveBeenNthCalledWith( - 1, - '/investigations/findone?where=' + - JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), - { + // Expect the axios.get to have been called three times. + expect(axios.get).toHaveBeenCalledTimes(3); + expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { headers: { Authorization: 'Bearer null', }, - } - ); - expect(axios.get).toHaveBeenNthCalledWith(2, '/investigations/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - expect(axios.get).toHaveBeenNthCalledWith(3, '/datasets/1', { - headers: { - Authorization: 'Bearer null', - }, + }); + expect(axios.get).toHaveBeenNthCalledWith(2, '/facilitycycles/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(3, '/investigations/3', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browse/instrument/1/facilityCycle' + ); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); + expect(breadcrumbs[1]).toHaveAttribute( + 'href', + '/browse/instrument/1/facilityCycle/2/investigation' + ); + expect(within(breadcrumbs[1]).getByText('Name 2')).toBeInTheDocument(); + expect(breadcrumbs[2]).toHaveAttribute( + 'href', + '/browse/instrument/1/facilityCycle/2/investigation/3' + ); + expect(within(breadcrumbs[2]).getByText('Title 3')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.dataset' + ) + ).toBeInTheDocument(); }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/proposal'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.proposal'); - - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(breadcrumbs[0]).toHaveAttribute( - 'href', - '/browse/proposal/INVESTIGATION 1/investigation' - ); - expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); - expect(breadcrumbs[1]).toHaveAttribute( - 'href', - '/browse/proposal/INVESTIGATION 1/investigation/1/dataset' - ); - expect(within(breadcrumbs[1]).getByText('1')).toBeInTheDocument(); - expect(within(breadcrumbs[2]).getByText('Name 1')).toBeInTheDocument(); - - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.datafile' - ) - ).toBeInTheDocument(); - }); - - it('ISIS route renders correctly at the base level and does not request', async () => { - // Set up test state pathname. - history.replace(createLocation(ISISRoutes['instruments'])); - - // Set up store with test state and mount the breadcrumb. - renderComponent(state, ['investigation', 'dataset']); - - // Expect the axios.get not to have been called - expect(axios.get).not.toHaveBeenCalled(); + it('ISIS route renders correctly at the datafile level and requests the instrument, facility cycle, investigation and dataset entities', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISRoutes['datafiles'])); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - expect(screen.getByTestId('Breadcrumb-base')).toHaveTextContent( - 'breadcrumbs.instrument' - ); - }); - - it('ISIS route renders correctly at the facility cycle level and requests the instrument entity', async () => { - // Set up test state pathname. - history.replace(createLocation(ISISRoutes['facilityCycles'])); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['investigation', 'dataset']); - // Set up store with test state and mount the breadcrumb. - renderComponent(state, ['investigation', 'dataset']); - - // Expect the axios.get to have been called three times. - expect(axios.get).toHaveBeenCalledTimes(1); - expect(axios.get).toHaveBeenCalledWith('/instruments/1', { - headers: { - Authorization: 'Bearer null', - }, + // Expect the axios.get to have been called three times. + expect(axios.get).toHaveBeenCalledTimes(4); + expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(2, '/facilitycycles/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(3, '/investigations/3', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(4, '/datasets/4', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browse/instrument/1/facilityCycle' + ); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); + expect(breadcrumbs[1]).toHaveAttribute( + 'href', + '/browse/instrument/1/facilityCycle/2/investigation' + ); + expect(within(breadcrumbs[1]).getByText('Name 2')).toBeInTheDocument(); + expect(breadcrumbs[2]).toHaveAttribute( + 'href', + '/browse/instrument/1/facilityCycle/2/investigation/3' + ); + expect(within(breadcrumbs[2]).getByText('Title 3')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.datafile' + ) + ).toBeInTheDocument(); }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); - - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); - - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.facilityCycle' - ) - ).toBeInTheDocument(); - }); + it('ISIS experiments route renders correctly at the base level and does not request', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISExperimentsRoutes['instruments'])); - it('ISIS route renders correctly at the investigation level and requests the instrument and facility cycle entities', async () => { - // Set up test state pathname. - history.replace(createLocation(ISISRoutes['investigations'])); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['investigation', 'dataset']); - // Set up store with test state and mount the breadcrumb. - renderComponent(state, ['investigation', 'dataset']); + // Expect the axios.get not to have been called + expect(axios.get).not.toHaveBeenCalled(); - // Expect the axios.get to have been called three times. - expect(axios.get).toHaveBeenCalledTimes(2); - expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { - headers: { - Authorization: 'Bearer null', - }, + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + expect(screen.getByTestId('Breadcrumb-base')).toHaveTextContent( + 'breadcrumbs.instrument' + ); }); - expect(axios.get).toHaveBeenNthCalledWith(2, '/facilitycycles/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(breadcrumbs[0]).toHaveAttribute( - 'href', - '/browse/instrument/1/facilityCycle' - ); - expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); - expect(within(breadcrumbs[1]).getByText('Name 1')).toBeInTheDocument(); + it('ISIS experiments route renders correctly at the study datapublications level and requests the instrument entity', async () => { + // Set up test state pathname. + history.replace( + createLocation(ISISExperimentsRoutes['studyDataPublications']) + ); - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.investigation' - ) - ).toBeInTheDocument(); - }); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['dataPublication', 'investigation', 'dataset']); - it('ISIS route renders correctly at the dataset level and requests the instrument, facility cycle and investigation entities', async () => { - // Set up test state pathname. - history.replace(createLocation(ISISRoutes['datasets'])); - - // Set up store with test state and mount the breadcrumb. - renderComponent(state, ['investigation', 'dataset']); - - // Expect the axios.get to have been called three times. - expect(axios.get).toHaveBeenCalledTimes(3); - expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - expect(axios.get).toHaveBeenNthCalledWith(2, '/facilitycycles/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - expect(axios.get).toHaveBeenNthCalledWith(3, '/investigations/1', { - headers: { - Authorization: 'Bearer null', - }, + // Expect the axios.get to have been called 1 time. + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith('/instruments/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute( + 'href', + '/browseDataPublications/instrument' + ); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.dataPublication' + ) + ).toBeInTheDocument(); }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + it('ISIS experiments route renders correctly at the data publication investigation level and requests the instrument and datapublication entities', async () => { + // Set up test state pathname. + history.replace( + createLocation(ISISExperimentsRoutes['investigationDataPublications']) + ); - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(breadcrumbs[0]).toHaveAttribute( - 'href', - '/browse/instrument/1/facilityCycle' - ); - expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); - expect(breadcrumbs[1]).toHaveAttribute( - 'href', - '/browse/instrument/1/facilityCycle/1/investigation' - ); - expect(within(breadcrumbs[1]).getByText('Name 1')).toBeInTheDocument(); - expect(breadcrumbs[2]).toHaveAttribute( - 'href', - '/browse/instrument/1/facilityCycle/1/investigation/1' - ); - expect(within(breadcrumbs[2]).getByText('Title 1')).toBeInTheDocument(); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['dataPublication', 'investigation', 'dataset']); - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.dataset' - ) - ).toBeInTheDocument(); - }); + // Expect the axios.get to have been called 2 times. + expect(axios.get).toHaveBeenCalledTimes(2); + expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(2, '/datapublications/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute( + 'href', + '/browseDataPublications/instrument' + ); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication' + ); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); + expect(within(breadcrumbs[1]).getByText('Title 2')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.investigation' + ) + ).toBeInTheDocument(); + }); - it('ISIS route renders correctly at the datafile level and requests the instrument, facility cycle, investigation and dataset entities', async () => { - // Set up test state pathname. - history.replace(createLocation(ISISRoutes['datafiles'])); + it('ISIS experiments route renders correctly at the dataset level and requests the instrument, data publication and investigation entities', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISExperimentsRoutes['datasets'])); - // Set up store with test state and mount the breadcrumb. - renderComponent(state, ['investigation', 'dataset']); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['dataPublication', 'investigation', 'dataset']); - // Expect the axios.get to have been called three times. - expect(axios.get).toHaveBeenCalledTimes(4); - expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - expect(axios.get).toHaveBeenNthCalledWith(2, '/facilitycycles/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - expect(axios.get).toHaveBeenNthCalledWith(3, '/investigations/1', { - headers: { - Authorization: 'Bearer null', - }, - }); - expect(axios.get).toHaveBeenNthCalledWith(4, '/datasets/1', { - headers: { - Authorization: 'Bearer null', - }, + // Expect the axios.get to have been called three times. + expect(axios.get).toHaveBeenCalledTimes(3); + expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(2, '/datapublications/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(3, '/datapublications/3', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute( + 'href', + '/browseDataPublications/instrument' + ); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication' + ); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); + expect(breadcrumbs[1]).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/2' + ); + expect(within(breadcrumbs[1]).getByText('Title 2')).toBeInTheDocument(); + expect(breadcrumbs[2]).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/2/investigation/3' + ); + expect(within(breadcrumbs[2]).getByText('Title 3')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.dataset' + ) + ).toBeInTheDocument(); }); - expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); - const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); - expect(baseBreadcrumb).toHaveAttribute('href', '/browse/instrument'); - expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + it('ISIS experiments route renders correctly at the datafile level and requests the instrument, data publication, investigation and dataset entities', async () => { + // Set up test state pathname. + history.replace(createLocation(ISISExperimentsRoutes['datafiles'])); - const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); - expect(breadcrumbs[0]).toHaveAttribute( - 'href', - '/browse/instrument/1/facilityCycle' - ); - expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); - expect(breadcrumbs[1]).toHaveAttribute( - 'href', - '/browse/instrument/1/facilityCycle/1/investigation' - ); - expect(within(breadcrumbs[1]).getByText('Name 1')).toBeInTheDocument(); - expect(breadcrumbs[2]).toHaveAttribute( - 'href', - '/browse/instrument/1/facilityCycle/1/investigation/1' - ); - expect(within(breadcrumbs[2]).getByText('Title 1')).toBeInTheDocument(); + // Set up store with test state and mount the breadcrumb. + renderComponent(state, ['dataPublication', 'investigation', 'dataset']); - expect( - within(screen.getByTestId('Breadcrumb-last')).getByText( - 'breadcrumbs.datafile' - ) - ).toBeInTheDocument(); + // Expect the axios.get to have been called four times. + expect(axios.get).toHaveBeenCalledTimes(4); + expect(axios.get).toHaveBeenNthCalledWith(1, '/instruments/1', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(2, '/datapublications/2', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(3, '/datapublications/3', { + headers: { + Authorization: 'Bearer null', + }, + }); + expect(axios.get).toHaveBeenNthCalledWith(4, '/datasets/4', { + headers: { + Authorization: 'Bearer null', + }, + }); + + expect(await screen.findByText('breadcrumbs.home')).toBeInTheDocument(); + const baseBreadcrumb = screen.getByTestId('Breadcrumb-base'); + expect(baseBreadcrumb).toHaveAttribute( + 'href', + '/browseDataPublications/instrument' + ); + expect(baseBreadcrumb).toHaveTextContent('breadcrumbs.instrument'); + + const breadcrumbs = screen.getAllByTestId(/^Breadcrumb-hierarchy-\d+$/); + expect(breadcrumbs[0]).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication' + ); + expect(within(breadcrumbs[0]).getByText('Name 1')).toBeInTheDocument(); + expect(breadcrumbs[1]).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/2' + ); + expect(within(breadcrumbs[1]).getByText('Title 2')).toBeInTheDocument(); + expect(breadcrumbs[2]).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/2/investigation/3' + ); + expect(within(breadcrumbs[2]).getByText('Title 3')).toBeInTheDocument(); + + expect( + within(screen.getByTestId('Breadcrumb-last')).getByText( + 'breadcrumbs.datafile' + ) + ).toBeInTheDocument(); + }); }); }); diff --git a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx index a8629a115..59ea7d58d 100644 --- a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx +++ b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx @@ -107,36 +107,42 @@ const useEntityInformation = ( let apiEntity = entity; // If the entity is a investigation or a data publication, we can't use name field as the default - // TODO: what do we want to show for a datapublication breadcrumb? let requestEntityField = entity === 'investigation' ? 'title' : entity === 'dataPublication' - ? 'pid' + ? 'title' : 'name'; + // this is the field we use to lookup the relevant entity in ICAT - it's usually ID + // but for DLS proposals this will be name + let requestQueryField = 'id'; + // Use breadcrumb settings in state to customise API call for entities. - if ( - Object.entries(breadcrumbSettings).length !== 0 && - entity in breadcrumbSettings - ) { - const entitySettings = breadcrumbSettings[entity]; - - // Check for a parent entity. - if ( - !entitySettings.parentEntity || - (entitySettings.parentEntity && - currentPathnames.includes(entitySettings.parentEntity)) - ) { - // Get the defined replace entity field. - requestEntityField = entitySettings.replaceEntityField; - - // Get the replace entity, if one has been defined. - if (entitySettings.replaceEntity) { - apiEntity = entitySettings.replaceEntity; + breadcrumbSettings + .filter( + (breadcrumbSetting) => breadcrumbSetting.matchEntity === entity + ) + .forEach((entitySettings) => { + // Check for a parent entity. + if ( + !entitySettings.parentEntity || + (entitySettings.parentEntity && + currentPathnames.includes(entitySettings.parentEntity)) + ) { + // Get the defined replace entity field. + requestEntityField = entitySettings.replaceEntityField; + + // Get the replace entity, if one has been defined. + if (entitySettings.replaceEntity) { + apiEntity = entitySettings.replaceEntity; + } + + if (entitySettings.replaceEntityQueryField) { + requestQueryField = entitySettings.replaceEntityQueryField; + } } - } - } + }); // Create the entity url to request the name, this is pluralised to get the API endpoint. let requestEntityUrl: string; @@ -156,7 +162,7 @@ const useEntityInformation = ( requestEntityUrl = pluralisedApiEntity.toLowerCase() + '/findone?where=' + - JSON.stringify({ name: { eq: entityId } }); + JSON.stringify({ [requestQueryField]: { eq: entityId } }); } queryConfigs.push({ diff --git a/packages/datagateway-dataview/src/page/idCheckFunctions.test.ts b/packages/datagateway-dataview/src/page/idCheckFunctions.test.ts index 07672be8b..75f2cb441 100644 --- a/packages/datagateway-dataview/src/page/idCheckFunctions.test.ts +++ b/packages/datagateway-dataview/src/page/idCheckFunctions.test.ts @@ -4,8 +4,8 @@ import { checkInstrumentAndFacilityCycleId, saveApiUrlMiddleware, checkInstrumentId, - checkDataPublicationId, - checkDatafileId, + checkStudyDataPublicationId, + checkDatasetId, } from './idCheckFunctions'; import axios from 'axios'; import { handleICATError, ConfigureURLsType } from 'datagateway-common'; @@ -74,7 +74,7 @@ describe('ID check functions', () => { expect.assertions(2); (axios.get as jest.Mock).mockImplementation(() => Promise.resolve({ - data: { id: 2, name: 'Test dataset', investigation: { id: 1 } }, + data: { id: 2, name: 'Test dataset' }, }) ); @@ -281,16 +281,16 @@ describe('ID check functions', () => { }); }); - describe('checkDataPublicationId', () => { - it('returns true on valid datapublication + investigation pair', async () => { + describe('checkStudyDataPublicationId', () => { + it('returns true on valid study datapublication + investigation data publication pair', async () => { expect.assertions(3); (axios.get as jest.Mock).mockImplementation(() => Promise.resolve({ - data: [{ id: 3, name: 'Test investigation' }], + data: [{ id: 3, title: 'Test DataPublication' }], }) ); - const result = await checkDataPublicationId(2, 3); + const result = await checkStudyDataPublicationId(2, 3); expect(result).toBe(true); const params = new URLSearchParams(); params.append( @@ -302,13 +302,14 @@ describe('ID check functions', () => { params.append( 'where', JSON.stringify({ - 'dataCollectionInvestigations.dataCollection.dataPublications.id': { - eq: 2, - }, + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: 2, + }, }) ); expect(axios.get).toHaveBeenCalledWith( - '/investigations/', + '/datapublications', expect.objectContaining({ params, }) @@ -317,7 +318,7 @@ describe('ID check functions', () => { params.toString() ); }); - it('returns false on invalid data publication + investigation pair', async () => { + it('returns false on invalid study datapublication + investigation data publication pair', async () => { expect.assertions(1); (axios.get as jest.Mock).mockImplementation(() => Promise.resolve({ @@ -325,7 +326,7 @@ describe('ID check functions', () => { }) ); - const result = await checkDataPublicationId(2, 3); + const result = await checkStudyDataPublicationId(2, 3); expect(result).toBe(false); }); it('returns false on HTTP error', async () => { @@ -336,70 +337,66 @@ describe('ID check functions', () => { }) ); - const result = await checkDataPublicationId(2, 3); + const result = await checkStudyDataPublicationId(2, 3); expect(result).toBe(false); expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error message', }); }); }); -}); - -describe('checkDatafileId', () => { - it('should return true when the given investigation + dataset + datafile id matches', async () => { - (axios.get as jest.Mock).mockResolvedValue({ - data: [ - { - id: 123, - dataset: { - id: 234, - investigation: { - id: 456, - }, - }, - }, - ], - }); - expect(await checkDatafileId(456, 234, 123)).toBe(true); - }); + describe('checkDatasetId', () => { + it('returns true on valid dataset + datafile pair', async () => { + expect.assertions(2); + (axios.get as jest.Mock).mockImplementation(() => + Promise.resolve({ + data: { id: 2, name: 'Test datafile' }, + }) + ); - it('should return false when the given investigation + dataset + datafile id does not match', async () => { - // datafile id and dataset id matches but not investigation - (axios.get as jest.Mock).mockResolvedValue({ - data: [ - { - id: 123, - dataset: { - id: 234, - investigation: { - id: 456, - }, + const result = await checkDatasetId(1, 2); + expect(result).toBe(true); + const params = new URLSearchParams(); + params.append( + 'where', + JSON.stringify({ + id: { + eq: 2, }, - }, - ], + }) + ); + params.append('where', JSON.stringify({ 'dataset.id': { eq: 1 } })); + expect(axios.get).toHaveBeenCalledWith('/datafiles/findone', { + params, + headers: { Authorization: 'Bearer null' }, + }); }); + it('returns false on invalid dataset + datafile pair', async () => { + expect.assertions(2); + (axios.get as jest.Mock).mockImplementation(() => + Promise.reject({ + response: { status: 404 }, + isAxiosError: true, + }) + ); - // dataset & datafile matches but not investigation - expect(await checkDatafileId(100, 234, 123)).toBe(false); - // investigation & datafile matches but not dataset - expect(await checkDatafileId(456, 199, 123)).toBe(false); - // only datafile matches - expect(await checkDatafileId(199, 200, 123)).toBe(false); - // no match at all - expect(await checkDatafileId(1, 2, 0)).toBe(false); - }); + const result = await checkDatasetId(1, 2); + expect(result).toBe(false); + expect(handleICATError).not.toHaveBeenCalled(); + }); + it('returns false on HTTP error', async () => { + expect.assertions(2); + (axios.get as jest.Mock).mockImplementation(() => + Promise.reject({ + message: 'Test error message', + }) + ); - it('should return false when an http error is encountered', async () => { - (axios.get as jest.Mock).mockImplementation(() => - Promise.reject({ + const result = await checkDatasetId(1, 2); + expect(result).toBe(false); + expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error message', - }) - ); - - expect(await checkDatafileId(456, 234, 123)).toBe(false); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', + }); }); }); }); diff --git a/packages/datagateway-dataview/src/page/idCheckFunctions.ts b/packages/datagateway-dataview/src/page/idCheckFunctions.ts index 3bfd5c0af..23b5a605b 100644 --- a/packages/datagateway-dataview/src/page/idCheckFunctions.ts +++ b/packages/datagateway-dataview/src/page/idCheckFunctions.ts @@ -2,8 +2,6 @@ import axios, { AxiosResponse } from 'axios'; import { handleICATError, Investigation, - Dataset, - Datafile, ConfigureURLsType, readSciGatewayToken, } from 'datagateway-common'; @@ -106,27 +104,28 @@ export const checkInstrumentAndFacilityCycleId = memoize( (...args) => JSON.stringify(args) ); -const unmemoizedCheckDataPublicationId = ( - dataPublicationId: number, - investigationId: number +const unmemoizedCheckStudyDataPublicationId = ( + studyDataPublicationId: number, + investigationDataPublicationId: number ): Promise => { const params = new URLSearchParams(); params.append( 'where', JSON.stringify({ - id: { eq: investigationId }, + id: { eq: investigationDataPublicationId }, }) ); params.append( 'where', JSON.stringify({ - 'dataCollectionInvestigations.dataCollection.dataPublications.id': { - eq: dataPublicationId, - }, + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: studyDataPublicationId, + }, }) ); return axios - .get(`${apiUrl}/investigations/`, { + .get(`${apiUrl}/datapublications`, { params, headers: { Authorization: `Bearer ${readSciGatewayToken().sessionId}`, @@ -141,8 +140,8 @@ const unmemoizedCheckDataPublicationId = ( }); }; -export const checkDataPublicationId = memoize( - unmemoizedCheckDataPublicationId, +export const checkStudyDataPublicationId = memoize( + unmemoizedCheckStudyDataPublicationId, (...args) => JSON.stringify(args) ); @@ -211,35 +210,40 @@ export const checkProposalName = memoize( (...args) => JSON.stringify(args) ); -export const unmemoizedCheckDatafileId = ( - investigationId: Investigation['id'], - datasetId: Dataset['id'], - datafileId: Datafile['id'] -): Promise => - axios - .get(`${apiUrl}/datafiles`, { - params: { - include: JSON.stringify(['dataset', 'dataset.investigation']), - where: JSON.stringify({ id: { eq: datafileId } }), +const unmemoizedCheckDatasetId = ( + datasetId: number, + datafileId: number +): Promise => { + const params = new URLSearchParams(); + params.append( + 'where', + JSON.stringify({ + id: { + eq: datafileId, }, + }) + ); + params.append('where', JSON.stringify({ 'dataset.id': { eq: datasetId } })); + return axios + .get(`${apiUrl}/datafiles/findone`, { + params, headers: { Authorization: `Bearer ${readSciGatewayToken().sessionId}`, }, }) - .then((response: AxiosResponse) => { - if (response.data.length <= 0) return false; - const datafile = response.data[0]; - return ( - datafile.id === datafileId && - datafile.dataset?.id === datasetId && - datafile.dataset?.investigation?.id === investigationId - ); + .then(() => { + return true; }) .catch((error) => { + // 404 is valid response from API saying the investigation id is invalid + if (axios.isAxiosError(error) && error.response?.status === 404) + return false; + // handle other API errors handleICATError(error); return false; }); +}; -export const checkDatafileId = memoize(unmemoizedCheckDatafileId, (...args) => +export const checkDatasetId = memoize(unmemoizedCheckDatasetId, (...args) => JSON.stringify(args) ); diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.tsx index 163e606c8..af6cbc8bb 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.tsx @@ -124,10 +124,12 @@ export const paths = { root: '/browseDataPublications', toggle: { isisInstrument: '/browseDataPublications/instrument', + isisStudyDataPublication: + '/browseDataPublications/instrument/:instrumentId/dataPublication', + isisInvestigationDataPublication: + '/browseDataPublications/instrument/:instrumentId/dataPublication/:studyDataPublicationId/investigation', isisDataPublication: '/browseDataPublications/instrument/:instrumentId/dataPublication', - isisInvestigation: - '/browseDataPublications/instrument/:instrumentId/dataPublication/:dataPublicationId/investigation', isisDataset: '/browseDataPublications/instrument/:instrumentId/dataPublication/:dataPublicationId/investigation/:investigationId/dataset', }, @@ -148,6 +150,8 @@ export const paths = { preview: { isisDatafilePreview: '/browse/instrument/:instrumentId/facilityCycle/:facilityCycleId/investigation/:investigationId/dataset/:datasetId/datafile/:datafileId', + isisDataPublicationDatafilePreview: + '/browseDataPublications/instrument/:instrumentId/dataPublication/:dataPublicationId/investigation/:investigationId/dataset/:datasetId/datafile/:datafileId', }, }; diff --git a/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx b/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx index 5fddd51e2..d9e975f49 100644 --- a/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx +++ b/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx @@ -7,14 +7,15 @@ import { Router } from 'react-router-dom'; import PageRouting from './pageRouting.component'; import { Provider } from 'react-redux'; import { initialState as dgDataViewInitialState } from '../state/reducers/dgdataview.reducer'; -import { dGCommonInitialState } from 'datagateway-common'; +import { DataPublication, dGCommonInitialState } from 'datagateway-common'; import { - checkDataPublicationId, + checkDatasetId, checkInstrumentAndFacilityCycleId, checkInstrumentId, checkInvestigationId, checkProposalName, + checkStudyDataPublicationId, } from './idCheckFunctions'; import { findColumnHeaderByName, flushPromises } from '../setupTests'; import { act } from 'react-dom/test-utils'; @@ -46,6 +47,8 @@ const ISISRoutes = { investigation: '/browse/instrument/1/facilityCycle/1/investigation/1', dataset: '/browse/instrument/1/facilityCycle/1/investigation/1/dataset/1', }, + datafilePreview: + '/browse/instrument/1/facilityCycle/1/investigation/1/dataset/1/datafile/1', }; // The ISIS DataPublications routes to test. @@ -65,6 +68,8 @@ const ISISDataPublicationsRoutes = { dataset: '/browseDataPublications/instrument/1/dataPublication/1/investigation/1/dataset/1', }, + datafilePreview: + '/browseDataPublications/instrument/1/dataPublication/1/investigation/1/dataset/1/datafile/1', }; // The DLS routes to test. @@ -110,6 +115,31 @@ describe('PageTable', () => { (axios.get as jest.Mock).mockImplementation((url: string) => { if (url.includes('count')) { return Promise.resolve({ data: 0 }); + } else if (url.includes('datapublications')) { + // this is so that routes can convert from data pub id -> investigation id + return Promise.resolve({ + data: [ + { + id: 1, + pid: 'pid.1', + title: 'test', + content: { + id: 2, + dataCollectionInvestigations: [ + { + id: 3, + investigation: { + id: 4, + title: 'Test', + name: 'test', + visitId: '1', + }, + }, + ], + }, + } satisfies DataPublication, + ], + }); } else { return Promise.resolve({ data: [] }); } @@ -120,7 +150,7 @@ describe('PageTable', () => { (checkInstrumentId as jest.Mock).mockImplementation(() => Promise.resolve(true) ); - (checkDataPublicationId as jest.Mock).mockImplementation(() => + (checkStudyDataPublicationId as jest.Mock).mockImplementation(() => Promise.resolve(true) ); (checkInvestigationId as jest.Mock).mockImplementation(() => @@ -129,6 +159,9 @@ describe('PageTable', () => { (checkProposalName as jest.Mock).mockImplementation(() => Promise.resolve(true) ); + (checkDatasetId as jest.Mock).mockImplementation(() => + Promise.resolve(true) + ); }); afterEach(() => { @@ -726,6 +759,48 @@ describe('PageTable', () => { expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); + + it('renders DatafilePreviewer for ISIS datafiles previewer route', async () => { + history.push(ISISRoutes.datafilePreview); + + render( + , + { wrapper: Wrapper } + ); + + expect( + await screen.findByText('datafiles.preview.invalid_datafile') + ).toBeInTheDocument(); + }); + + it('does not render DatafilePreviewer for incorrect ISIS datafiles previewer route', async () => { + (checkInstrumentAndFacilityCycleId as jest.Mock).mockImplementation(() => + Promise.resolve(false) + ); + (checkInvestigationId as jest.Mock).mockImplementation(() => + Promise.resolve(false) + ); + (checkDatasetId as jest.Mock).mockImplementation(() => + Promise.resolve(false) + ); + + history.push(ISISRoutes.datafilePreview); + + render( + , + { wrapper: Wrapper } + ); + + expect(await screen.findByText('loading.oops')).toBeInTheDocument(); + }); }); describe('ISIS Data Publication Hierarchy', () => { @@ -784,9 +859,6 @@ describe('PageTable', () => { expect( await findColumnHeaderByName('datapublications.pid') ).toBeInTheDocument(); - expect( - await findColumnHeaderByName('datapublications.publication_date') - ).toBeInTheDocument(); }); it('renders ISISDataPublicationsCardView for ISIS dataPublications route in Data Publication Hierarchy', async () => { @@ -855,25 +927,13 @@ describe('PageTable', () => { ); expect( - await findColumnHeaderByName('investigations.title') - ).toBeInTheDocument(); - expect( - await findColumnHeaderByName('investigations.name') - ).toBeInTheDocument(); - expect( - await findColumnHeaderByName('investigations.doi') - ).toBeInTheDocument(); - expect( - await findColumnHeaderByName('investigations.size') - ).toBeInTheDocument(); - expect( - await findColumnHeaderByName('investigations.principal_investigators') + await findColumnHeaderByName('datapublications.title') ).toBeInTheDocument(); expect( - await findColumnHeaderByName('investigations.start_date') + await findColumnHeaderByName('datapublications.pid') ).toBeInTheDocument(); expect( - await findColumnHeaderByName('investigations.end_date') + await findColumnHeaderByName('datapublications.publication_date') ).toBeInTheDocument(); }); @@ -890,7 +950,7 @@ describe('PageTable', () => { ); expect( - await screen.findByTestId('isis-investigations-card-view') + await screen.findByTestId('isis-dataPublications-card-view') ).toBeInTheDocument(); }); @@ -920,7 +980,7 @@ describe('PageTable', () => { (checkInstrumentId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); - (checkDataPublicationId as jest.Mock).mockImplementation(() => + (checkStudyDataPublicationId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); @@ -959,7 +1019,7 @@ describe('PageTable', () => { (checkInstrumentId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); - (checkDataPublicationId as jest.Mock).mockImplementation(() => + (checkStudyDataPublicationId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); @@ -998,7 +1058,7 @@ describe('PageTable', () => { (checkInstrumentId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); - (checkDataPublicationId as jest.Mock).mockImplementation(() => + (checkStudyDataPublicationId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); @@ -1037,7 +1097,7 @@ describe('PageTable', () => { (checkInstrumentId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); - (checkDataPublicationId as jest.Mock).mockImplementation(() => + (checkStudyDataPublicationId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); @@ -1085,7 +1145,7 @@ describe('PageTable', () => { (checkInstrumentId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); - (checkDataPublicationId as jest.Mock).mockImplementation(() => + (checkStudyDataPublicationId as jest.Mock).mockImplementation(() => Promise.resolve(false) ); (checkInvestigationId as jest.Mock).mockImplementation(() => @@ -1105,6 +1165,51 @@ describe('PageTable', () => { expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); + + it('renders DatafilePreviewer for ISIS datafile preview route in Data Publication Hierarchy', async () => { + history.push(ISISDataPublicationsRoutes.datafilePreview); + + render( + , + { wrapper: Wrapper } + ); + + expect( + await screen.findByText('datafiles.preview.invalid_datafile') + ).toBeInTheDocument(); + }); + + it('does not render DatafilePreviewer for incorrect ISIS datafile preview route in Data Publication Hierarchy', async () => { + (checkInstrumentId as jest.Mock).mockImplementation(() => + Promise.resolve(false) + ); + (checkStudyDataPublicationId as jest.Mock).mockImplementation(() => + Promise.resolve(false) + ); + (checkInvestigationId as jest.Mock).mockImplementation(() => + Promise.resolve(false) + ); + (checkDatasetId as jest.Mock).mockImplementation(() => + Promise.resolve(false) + ); + + history.push(ISISDataPublicationsRoutes.datafilePreview); + + render( + , + { wrapper: Wrapper } + ); + + expect(await screen.findByText('loading.oops')).toBeInTheDocument(); + }); }); describe('DLS', () => { diff --git a/packages/datagateway-dataview/src/page/pageRouting.component.tsx b/packages/datagateway-dataview/src/page/pageRouting.component.tsx index d4895d650..fec453efc 100644 --- a/packages/datagateway-dataview/src/page/pageRouting.component.tsx +++ b/packages/datagateway-dataview/src/page/pageRouting.component.tsx @@ -50,12 +50,12 @@ import { checkInvestigationId, checkInstrumentAndFacilityCycleId, checkInstrumentId, - checkDataPublicationId, - checkDatafileId, + checkStudyDataPublicationId, + checkDatasetId, } from './idCheckFunctions'; import { paths } from './pageContainer.component'; -import type { ViewsType } from 'datagateway-common'; +import { useDataPublication, ViewsType } from 'datagateway-common'; const SafeDatafileTable = React.memo( (props: { @@ -78,164 +78,258 @@ const SafeDatafileTable = React.memo( ); SafeDatafileTable.displayName = 'SafeDatafileTable'; -const SafeISISDatafilesTable = React.memo( - (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - datasetId: string; - dataPublication: boolean; - }): React.ReactElement => { - const SafeISISDatafilesTable = props.dataPublication - ? withIdCheck( - Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - parseInt(props.investigationId), - parseInt(props.datasetId) - ), - ]).then((values) => !values.includes(false)) - )(ISISDatafilesTable) - : withIdCheck( - Promise.all([ +const SafeISISDatafilesTable = (props: { + instrumentId: string; + instrumentChildId: string; + investigationId: string; + datasetId: string; + dataPublication: boolean; +}): React.ReactElement => { + const { data, isLoading } = useDataPublication( + parseInt(props.investigationId), + { + enabled: props.dataPublication, + } + ); + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const SafeISISDatafilesTable = React.useMemo( + () => + props.dataPublication + ? withIdCheck( + Promise.all([ + checkInstrumentId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId) + ), + checkStudyDataPublicationId( + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + checkInvestigationId( + dataPublicationInvestigationId ?? -1, + parseInt(props.datasetId) + ), + ...(isLoading ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + )(ISISDatafilesTable) + : withIdCheck( + Promise.all([ + checkInstrumentAndFacilityCycleId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + checkInvestigationId( + parseInt(props.investigationId), + parseInt(props.datasetId) + ), + ]).then((values) => !values.includes(false)) + )(ISISDatafilesTable), + [ + dataPublicationInvestigationId, + isLoading, + props.dataPublication, + props.datasetId, + props.instrumentChildId, + props.instrumentId, + props.investigationId, + ] + ); + + return ( + + ); +}; + +const SafeISISDatasetLanding = (props: { + instrumentId: string; + instrumentChildId: string; + investigationId: string; + datasetId: string; + dataPublication: boolean; +}): React.ReactElement => { + const { data, isLoading } = useDataPublication( + parseInt(props.investigationId), + { + enabled: props.dataPublication, + } + ); + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const SafeISISDatasetLanding = React.useMemo( + () => + props.dataPublication + ? withIdCheck( + Promise.all([ + checkInstrumentId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId) + ), + checkStudyDataPublicationId( + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + checkInvestigationId( + dataPublicationInvestigationId ?? -1, + parseInt(props.datasetId) + ), + ...(isLoading ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + )(ISISDatasetLanding) + : withIdCheck( + Promise.all([ + checkInstrumentAndFacilityCycleId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + checkInvestigationId( + parseInt(props.investigationId), + parseInt(props.datasetId) + ), + ]).then((values) => !values.includes(false)) + )(ISISDatasetLanding), + [ + dataPublicationInvestigationId, + isLoading, + props.dataPublication, + props.datasetId, + props.instrumentChildId, + props.instrumentId, + props.investigationId, + ] + ); + + return ; +}; + +const SafeISISDatasetsTable = (props: { + instrumentId: string; + instrumentChildId: string; + investigationId: string; + dataPublication: boolean; +}): React.ReactElement => { + const { data, isLoading } = useDataPublication( + parseInt(props.investigationId), + { + enabled: props.dataPublication, + } + ); + + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const SafeISISDatasetsTable = React.useMemo( + () => + props.dataPublication + ? withIdCheck( + Promise.all([ + checkInstrumentId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId) + ), + checkStudyDataPublicationId( + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + ...(isLoading ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + )(ISISDatasetsTable) + : withIdCheck( checkInstrumentAndFacilityCycleId( parseInt(props.instrumentId), parseInt(props.instrumentChildId), parseInt(props.investigationId) - ), - checkInvestigationId( - parseInt(props.investigationId), - parseInt(props.datasetId) - ), - ]).then((values) => !values.includes(false)) - )(ISISDatafilesTable); - - return ( - - ); - } -); -SafeISISDatafilesTable.displayName = 'SafeISISDatafilesTable'; - -const SafeISISDatasetLanding = React.memo( - (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - datasetId: string; - dataPublication: boolean; - }): React.ReactElement => { - const SafeISISDatasetLanding = props.dataPublication - ? withIdCheck( - Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - parseInt(props.investigationId), - parseInt(props.datasetId) - ), - ]).then((values) => !values.includes(false)) - )(ISISDatasetLanding) - : withIdCheck( - Promise.all([ + ) + )(ISISDatasetsTable), + [ + isLoading, + props.dataPublication, + props.instrumentChildId, + props.instrumentId, + props.investigationId, + ] + ); + + return ( + + ); +}; + +const SafeISISDatasetsCardView = (props: { + instrumentId: string; + instrumentChildId: string; + investigationId: string; + dataPublication: boolean; +}): React.ReactElement => { + const { data, isLoading } = useDataPublication( + parseInt(props.investigationId), + { + enabled: props.dataPublication, + } + ); + + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const SafeISISDatasetsCardView = React.useMemo( + () => + props.dataPublication + ? withIdCheck( + Promise.all([ + checkInstrumentId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId) + ), + checkStudyDataPublicationId( + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + ...(isLoading ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + )(ISISDatasetsCardView) + : withIdCheck( checkInstrumentAndFacilityCycleId( parseInt(props.instrumentId), parseInt(props.instrumentChildId), parseInt(props.investigationId) - ), - checkInvestigationId( - parseInt(props.investigationId), - parseInt(props.datasetId) - ), - ]).then((values) => !values.includes(false)) - )(ISISDatasetLanding); - - return ; - } -); -SafeISISDatasetLanding.displayName = 'SafeISISDatasetLanding'; - -const SafeISISDatasetsTable = React.memo( - (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - dataPublication: boolean; - }): React.ReactElement => { - const SafeISISDatasetsTable = props.dataPublication - ? withIdCheck( - Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - ]).then((values) => !values.includes(false)) - )(ISISDatasetsTable) - : withIdCheck( - checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ) - )(ISISDatasetsTable); - - return ; - } -); -SafeISISDatasetsTable.displayName = 'SafeISISDatasetsTable'; - -const SafeISISDatasetsCardView = React.memo( - (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - dataPublication: boolean; - }): React.ReactElement => { - const SafeISISDatasetsCardView = props.dataPublication - ? withIdCheck( - Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - ]).then((values) => !values.includes(false)) - )(ISISDatasetsCardView) - : withIdCheck( - checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ) - )(ISISDatasetsCardView); - - return ; - } -); -SafeISISDatasetsCardView.displayName = 'SafeISISDatasetsCardView'; + ) + )(ISISDatasetsCardView), + [ + isLoading, + props.dataPublication, + props.instrumentChildId, + props.instrumentId, + props.investigationId, + ] + ); + + return ( + + ); +}; const SafeISISInvestigationLanding = React.memo( (props: { @@ -251,7 +345,7 @@ const SafeISISInvestigationLanding = React.memo( parseInt(props.instrumentId), parseInt(props.instrumentChildId) ), - checkDataPublicationId( + checkStudyDataPublicationId( parseInt(props.instrumentChildId), parseInt(props.investigationId) ), @@ -265,7 +359,13 @@ const SafeISISInvestigationLanding = React.memo( ) )(ISISInvestigationLanding); - return ; + return ( + + ); } ); SafeISISInvestigationLanding.displayName = 'SafeISISInvestigationLanding'; @@ -351,33 +451,79 @@ const SafeDLSDatasetsCardView = React.memo( ); SafeDLSDatasetsCardView.displayName = 'SafeDLSDatasetsCardView'; -const SafeDatafilePreviewer = React.memo( - (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - datasetId: string; - datafileId: string; - }): React.ReactElement => { - const SafeDatafilePreviewer = withIdCheck( - Promise.all([ - checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkDatafileId( - parseInt(props.investigationId), - parseInt(props.datasetId), - parseInt(props.datafileId) - ), - ]).then((checks) => checks.every((passes) => passes)) - )(DatafilePreviewer); - - return ; - } -); -SafeDatafilePreviewer.displayName = 'SafeDatafilePreviewer'; +const SafeDatafilePreviewer = (props: { + dataPublication: boolean; + instrumentId: string; + instrumentChildId: string; + investigationId: string; + datasetId: string; + datafileId: string; +}): React.ReactElement => { + const { data, isLoading } = useDataPublication( + parseInt(props.investigationId), + { + enabled: props.dataPublication, + } + ); + + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const SafeDatafilePreviewer = React.useMemo( + () => + props.dataPublication + ? withIdCheck( + Promise.all([ + checkInstrumentId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId) + ), + checkStudyDataPublicationId( + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + checkInvestigationId( + dataPublicationInvestigationId ?? -1, + parseInt(props.datasetId) + ), + checkDatasetId( + parseInt(props.datasetId), + parseInt(props.datafileId) + ), + ...(isLoading ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + )(DatafilePreviewer) + : withIdCheck( + Promise.all([ + checkInstrumentAndFacilityCycleId( + parseInt(props.instrumentId), + parseInt(props.instrumentChildId), + parseInt(props.investigationId) + ), + checkInvestigationId( + parseInt(props.investigationId), + parseInt(props.datasetId) + ), + checkDatasetId( + parseInt(props.datasetId), + parseInt(props.datafileId) + ), + ]).then((checks) => checks.every((passes) => passes)) + )(DatafilePreviewer), + [ + dataPublicationInvestigationId, + isLoading, + props.dataPublication, + props.datafileId, + props.datasetId, + props.instrumentChildId, + props.instrumentId, + props.investigationId, + ] + ); + + return ; +}; interface PageRoutingProps { view: ViewsType; @@ -493,7 +639,7 @@ class PageRouting extends React.PureComponent { /> this.props.view === 'card' ? ( { /> this.props.view === 'card' ? ( - ) : ( - ) } @@ -594,6 +742,20 @@ class PageRouting extends React.PureComponent { /> )} /> + ( + + )} + /> {/* ISIS routes */} { render={({ match }) => this.props.view === 'card' ? ( ) : ( ) } @@ -705,6 +865,7 @@ class PageRouting extends React.PureComponent { path={paths.preview.isisDatafilePreview} render={({ match }) => ( { }); it('given JSON loadBreadcrumbSettings returns a ConfigureBreadcrumbSettingsType with ConfigureBreadcrumbSettingsPayload', () => { - const action = loadBreadcrumbSettings({ - test: { + const action = loadBreadcrumbSettings([ + { + matchEntity: 'test', replaceEntity: 'testEntity', replaceEntityField: 'testField', }, - }); + ]); expect(action.type).toEqual(ConfigureBreadcrumbSettingsType); expect(action.payload).toEqual({ - settings: { - test: { + settings: [ + { + matchEntity: 'test', replaceEntity: 'testEntity', replaceEntityField: 'testField', }, - }, + ], }); }); @@ -95,11 +97,12 @@ describe('Actions', () => { features: {}, idsUrl: 'ids', apiUrl: 'api', - breadcrumbs: { - test: { + breadcrumbs: [ + { + matchEntity: 'test', replaceEntityField: 'title', }, - }, + ], downloadApiUrl: 'download-api', selectAllSetting: false, routes: [ @@ -127,11 +130,12 @@ describe('Actions', () => { }) ); expect(actions).toContainEqual( - loadBreadcrumbSettings({ - test: { + loadBreadcrumbSettings([ + { + matchEntity: 'test', replaceEntityField: 'title', }, - }) + ]) ); expect(actions).toContainEqual(settingsLoaded()); expect(actions).toContainEqual(loadSelectAllSetting(false)); diff --git a/packages/datagateway-dataview/src/state/actions/actions.types.tsx b/packages/datagateway-dataview/src/state/actions/actions.types.tsx index 4e25d3eaa..b3b04ddf1 100644 --- a/packages/datagateway-dataview/src/state/actions/actions.types.tsx +++ b/packages/datagateway-dataview/src/state/actions/actions.types.tsx @@ -19,7 +19,7 @@ export interface FeatureSwitchesPayload { export interface FeatureSwitches {} export interface ConfigureBreadcrumbSettingsPayload { - settings: BreadcrumbSettings; + settings: BreadcrumbSettings[]; } export interface ConfigureSelectAllSettingPayload { @@ -35,9 +35,9 @@ export interface ConfigureFacilityImageSettingPayload { } export interface BreadcrumbSettings { - [matchEntity: string]: { - replaceEntityField: string; - replaceEntity?: string; - parentEntity?: string; - }; + matchEntity: string; + replaceEntityField: string; + replaceEntityQueryField?: string; + replaceEntity?: string; + parentEntity?: string; } diff --git a/packages/datagateway-dataview/src/state/actions/index.tsx b/packages/datagateway-dataview/src/state/actions/index.tsx index fa8bb63b9..0e93bee90 100644 --- a/packages/datagateway-dataview/src/state/actions/index.tsx +++ b/packages/datagateway-dataview/src/state/actions/index.tsx @@ -32,7 +32,7 @@ export const loadFeatureSwitches = ( }); export const loadBreadcrumbSettings = ( - breadcrumbSettings: BreadcrumbSettings + breadcrumbSettings: BreadcrumbSettings[] ): ActionType => ({ type: ConfigureBreadcrumbSettingsType, payload: { diff --git a/packages/datagateway-dataview/src/state/app.types.tsx b/packages/datagateway-dataview/src/state/app.types.tsx index 55b9611de..8fcfaed39 100644 --- a/packages/datagateway-dataview/src/state/app.types.tsx +++ b/packages/datagateway-dataview/src/state/app.types.tsx @@ -10,7 +10,7 @@ import type { export interface DGDataViewState { facilityImageURL: string; features: FeatureSwitches; - breadcrumbSettings: BreadcrumbSettings; + breadcrumbSettings: BreadcrumbSettings[]; settingsLoaded: boolean; selectAllSetting: boolean; pluginHost: string; diff --git a/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.test.tsx b/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.test.tsx index bbad966e7..3189f26cc 100644 --- a/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.test.tsx +++ b/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.test.tsx @@ -46,22 +46,24 @@ describe('dgdataview reducer', () => { }); it('should set breadcrumb settings property when configure breadcrumb settings action is sent', () => { - expect(state.dgdataview.breadcrumbSettings).toEqual({}); + expect(state.dgdataview.breadcrumbSettings).toEqual([]); const updatedState = DGDataViewReducer( state, - loadBreadcrumbSettings({ - test: { + loadBreadcrumbSettings([ + { + matchEntity: 'test', replaceEntityField: 'title', }, - }) + ]) ); - expect(updatedState.breadcrumbSettings).toEqual({ - test: { + expect(updatedState.breadcrumbSettings).toEqual([ + { + matchEntity: 'test', replaceEntityField: 'title', }, - }); + ]); }); it('should set selectAllSetting when configuring action is sent', () => { diff --git a/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx b/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx index 14c3eed80..d7d3099b0 100644 --- a/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx +++ b/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx @@ -20,7 +20,7 @@ import { export const initialState: DGDataViewState = { features: {}, - breadcrumbSettings: {}, + breadcrumbSettings: [], settingsLoaded: false, selectAllSetting: true, pluginHost: '', diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx index 77d48a1f7..c262dada8 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx @@ -1,7 +1,7 @@ import { type DataPublication, dGCommonInitialState } from 'datagateway-common'; import * as React from 'react'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../../../state/app.types'; @@ -21,6 +21,7 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; +import { paths } from '../../../page/pageContainer.component'; describe('ISIS Data Publication - Card View', () => { let mockStore; @@ -28,16 +29,30 @@ describe('ISIS Data Publication - Card View', () => { let cardData: DataPublication[]; let history: History; - const renderComponent = (): RenderResult => - render( + const renderComponent = (studyDataPublicationId?: string): RenderResult => { + if (studyDataPublicationId) + history.replace( + generatePath( + paths.dataPublications.toggle.isisInvestigationDataPublication, + { + instrumentId: 1, + studyDataPublicationId, + } + ) + ); + return render( - + ); + }; beforeEach(() => { cardData = [ @@ -67,7 +82,13 @@ describe('ISIS Data Publication - Card View', () => { }, }, ]; - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.dataPublications.toggle.isisStudyDataPublication, { + instrumentId: 1, + }), + ], + }); mockStore = configureStore([thunk]); state = JSON.parse( @@ -101,122 +122,167 @@ describe('ISIS Data Publication - Card View', () => { jest.clearAllMocks(); }); - it('renders correctly', async () => { - renderComponent(); + describe('Study Data Publication', () => { + it('renders correctly', async () => { + renderComponent(); + + const cards = await screen.findAllByTestId( + 'isis-dataPublications-card-view' + ); + expect(cards).toHaveLength(1); + + const card = cards[0]; + // card id should be rendered as link to data publication + expect( + within(card).getByRole('link', { name: 'Test 1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/14' + ); + expect(within(card).getByLabelText('card-description')).toHaveTextContent( + 'Data Publication Description' + ); + expect(within(card).getByRole('link', { name: 'doi' })).toHaveAttribute( + 'href', + 'https://doi.org/doi' + ); + }); - const cards = await screen.findAllByTestId( - 'isis-dataPublications-card-view' - ); - expect(cards).toHaveLength(1); + it('uses default sort', () => { + renderComponent(); + expect(history.length).toBe(1); + expect(history.location.search).toBe( + `?sort=${encodeURIComponent('{"title":"desc"}')}` + ); + + // check that the data request is sent only once after mounting + const datafilesCalls = (axios.get as jest.Mock).mock.calls.filter( + (call) => call[0] === '/datapublications' + ); + expect(datafilesCalls).toHaveLength(1); + }); - const card = cards[0]; - // card id should be rendered as link to data publication - expect(within(card).getByRole('link', { name: 'Test 1' })).toHaveAttribute( - 'href', - '/browseDataPublications/instrument/1/dataPublication/14' - ); - expect(within(card).getByLabelText('card-description')).toHaveTextContent( - 'Data Publication Description' - ); - expect(within(card).getByRole('link', { name: 'doi' })).toHaveAttribute( - 'href', - 'https://doi.org/doi' - ); - expect(within(card).getByText('2001-01-01')).toBeInTheDocument(); - }); + it('updates filter query params on text filter', async () => { + jest.useFakeTimers(); + const user = userEvent.setup({ + advanceTimers: jest.advanceTimersByTime, + }); - it('uses default sort', () => { - renderComponent(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( - `?sort=${encodeURIComponent('{"publicationDate":"desc"}')}` - ); + renderComponent(); - // check that the data request is sent only once after mounting - const datafilesCalls = (axios.get as jest.Mock).mock.calls.filter( - (call) => call[0] === '/datapublications' - ); - expect(datafilesCalls).toHaveLength(1); - }); + // click on button to show advanced filters + await user.click( + await screen.findByRole('button', { name: 'advanced_filters.show' }) + ); - it('updates filter query params on text filter', async () => { - jest.useFakeTimers(); - const user = userEvent.setup({ - advanceTimers: jest.advanceTimersByTime, - }); + const filter = await screen.findByRole('textbox', { + name: 'Filter by datapublications.title', + hidden: true, + }); - renderComponent(); + await user.type(filter, 'Test'); - // click on button to show advanced filters - await user.click( - await screen.findByRole('button', { name: 'advanced_filters.show' }) - ); + expect(history.location.search).toBe( + `?filters=${encodeURIComponent( + '{"title":{"value":"Test","type":"include"}}' + )}` + ); - const filter = await screen.findByRole('textbox', { - name: 'Filter by datapublications.title', - hidden: true, - }); + await user.clear(filter); - await user.type(filter, 'Test'); + expect(history.location.search).toBe('?'); - expect(history.location.search).toBe( - `?filters=${encodeURIComponent( - '{"title":{"value":"Test","type":"include"}}' - )}` - ); - - await user.clear(filter); + jest.useRealTimers(); + }); - expect(history.location.search).toBe('?'); + it('updates sort query params on sort', async () => { + const user = userEvent.setup(); - jest.useRealTimers(); - }); + renderComponent(); - it('updates filter query params on date filter', async () => { - const user = userEvent.setup(); - applyDatePickerWorkaround(); + await user.click( + await screen.findByRole('button', { + name: 'Sort by DATAPUBLICATIONS.PID', + }) + ); - renderComponent(); + expect(history.location.search).toBe( + `?sort=${encodeURIComponent('{"pid":"asc"}')}` + ); + }); + }); - // open advanced filter - await user.click( - await screen.findByRole('button', { name: 'advanced_filters.show' }) - ); + describe('Investigation Data Publication', () => { + it('renders correctly', async () => { + renderComponent('2'); + + const cards = await screen.findAllByTestId( + 'isis-dataPublications-card-view' + ); + expect(cards).toHaveLength(1); + + const card = cards[0]; + // card id should be rendered as link to data publication + expect( + within(card).getByRole('link', { name: 'Test 1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/2/investigation/14' + ); + expect(within(card).getByLabelText('card-description')).toHaveTextContent( + 'Data Publication Description' + ); + expect(within(card).getByRole('link', { name: 'doi' })).toHaveAttribute( + 'href', + 'https://doi.org/doi' + ); + expect(within(card).getByText('2001-01-01')).toBeInTheDocument(); + }); - const filterInput = screen.getByRole('textbox', { - name: 'datapublications.publication_date filter to', + it('uses default sort', () => { + renderComponent('2'); + expect(history.length).toBe(1); + expect(history.location.search).toBe( + `?sort=${encodeURIComponent('{"publicationDate":"desc"}')}` + ); + + // check that the data request is sent only once after mounting + const datafilesCalls = (axios.get as jest.Mock).mock.calls.filter( + (call) => call[0] === '/datapublications' + ); + expect(datafilesCalls).toHaveLength(1); }); - await user.type(filterInput, '2019-08-06'); - expect(history.location.search).toBe( - `?filters=${encodeURIComponent( - '{"publicationDate":{"endDate":"2019-08-06"}}' - )}` - ); + it('updates filter query params on date filter', async () => { + const user = userEvent.setup(); + applyDatePickerWorkaround(); - // await user.clear(filterInput); - await user.click(filterInput); - await user.keyboard('{Control}a{/Control}'); - await user.keyboard('{Delete}'); + renderComponent('2'); - expect(history.location.search).toBe('?'); + // open advanced filter + await user.click( + await screen.findByRole('button', { name: 'advanced_filters.show' }) + ); - cleanupDatePickerWorkaround(); - }); + const filterInput = screen.getByRole('textbox', { + name: 'datapublications.publication_date filter to', + }); - it('updates sort query params on sort', async () => { - const user = userEvent.setup(); + await user.type(filterInput, '2019-08-06'); + expect(history.location.search).toBe( + `?filters=${encodeURIComponent( + '{"publicationDate":{"endDate":"2019-08-06"}}' + )}` + ); - renderComponent(); + // await user.clear(filterInput); + await user.click(filterInput); + await user.keyboard('{Control}a{/Control}'); + await user.keyboard('{Delete}'); - await user.click( - await screen.findByRole('button', { - name: 'Sort by DATAPUBLICATIONS.TITLE', - }) - ); + expect(history.location.search).toBe('?'); - expect(history.location.search).toBe( - `?sort=${encodeURIComponent('{"title":"asc"}')}` - ); + cleanupDatePickerWorkaround(); + }); }); }); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx index 4d97aa2d5..3024fefbd 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx @@ -21,12 +21,13 @@ import { Link as MuiLink } from '@mui/material'; interface ISISDataPublicationsCVProps { instrumentId: string; + studyDataPublicationId?: string; } const ISISDataPublicationsCardView = ( props: ISISDataPublicationsCVProps ): React.ReactElement => { - const { instrumentId } = props; + const { instrumentId, studyDataPublicationId } = props; const [t] = useTranslation(); const location = useLocation(); @@ -62,6 +63,36 @@ const ISISDataPublicationsCardView = ( }, }), }, + ...(studyDataPublicationId + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: studyDataPublicationId, + }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'investigation' }, + }), + }, + ] + : [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'study' }, + }), + }, + { + filterType: 'distinct', + filterValue: JSON.stringify(['id', 'title', 'pid']), + }, + ]), ]); const { isLoading: dataLoading, data } = useDataPublicationsPaginated( @@ -75,26 +106,54 @@ const ISISDataPublicationsCardView = ( }, }), }, + ...(studyDataPublicationId + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: studyDataPublicationId, + }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'investigation' }, + }), + }, + ] + : [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'study' }, + }), + }, + { + filterType: 'distinct', + filterValue: JSON.stringify(['id', 'title', 'pid']), + }, + ]), ], isMounted ); - const title = React.useMemo(() => { - const pathRoot = 'browseDataPublications'; - const instrumentChild = 'dataPublication'; - + const title: CardViewDetails = React.useMemo(() => { return { label: t('datapublications.title'), dataKey: 'title', content: (dataPublication: DataPublication) => tableLink( - `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${dataPublication.id}`, + `${location.pathname}/${dataPublication.id}`, dataPublication.title, view ), filterComponent: textFilter, + defaultSort: studyDataPublicationId ? undefined : 'desc', }; - }, [t, textFilter, instrumentId, view]); + }, [t, textFilter, studyDataPublicationId, location.pathname, view]); const description: CardViewDetails = React.useMemo( () => ({ @@ -125,17 +184,21 @@ const ISISDataPublicationsCardView = ( dataKey: 'pid', filterComponent: textFilter, }, - { - icon: CalendarToday, - label: t('datapublications.publication_date'), - dataKey: 'publicationDate', - content: (dataPublication: DataPublication) => - dataPublication.publicationDate?.slice(0, 10) ?? '', - filterComponent: dateFilter, - defaultSort: 'desc', - }, + ...(studyDataPublicationId + ? ([ + { + icon: CalendarToday, + label: t('datapublications.publication_date'), + dataKey: 'publicationDate', + content: (dataPublication: DataPublication) => + dataPublication.publicationDate?.slice(0, 10) ?? '', + filterComponent: dateFilter, + defaultSort: 'desc', + }, + ] as CardViewDetails[]) + : []), ], - [dateFilter, t, textFilter] + [dateFilter, studyDataPublicationId, t, textFilter] ); return ( diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx index 755d7f7eb..cdcc2f787 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx @@ -6,7 +6,7 @@ import { } from 'datagateway-common'; import * as React from 'react'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../../../state/app.types'; @@ -21,6 +21,7 @@ import { import { render, type RenderResult, screen } from '@testing-library/react'; import type { UserEvent } from '@testing-library/user-event/setup/setup'; import userEvent from '@testing-library/user-event'; +import { paths } from '../../../page/pageContainer.component'; jest.mock('datagateway-common', () => { const originalModule = jest.requireActual('datagateway-common'); @@ -45,12 +46,7 @@ describe('ISIS Datasets - Card View', () => { - + @@ -66,7 +62,15 @@ describe('ISIS Datasets - Card View', () => { createTime: '2019-07-23', }, ]; - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.toggle.isisDataset, { + instrumentId: '1', + investigationId: '1', + facilityCycleId: '1', + }), + ], + }); user = userEvent.setup(); mockStore = configureStore([thunk]); @@ -103,20 +107,16 @@ describe('ISIS Datasets - Card View', () => { }); it('correct link used for dataPublication hierarchy', async () => { - render( - - - - - - - + history.replace( + generatePath(paths.dataPublications.toggle.isisDataset, { + instrumentId: '1', + investigationId: '1', + dataPublicationId: '1', + }) ); + + renderComponent(); + expect(await screen.findByRole('link', { name: 'Test 1' })).toHaveAttribute( 'href', '/browseDataPublications/instrument/1/dataPublication/1/investigation/1/dataset/1' diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx index fbec76bbc..8e0dad841 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx @@ -33,17 +33,13 @@ const ActionButtonsContainer = styled('div')(({ theme }) => ({ })); interface ISISDatasetCardViewProps { - instrumentId: string; - instrumentChildId: string; investigationId: string; - dataPublication: boolean; } const ISISDatasetsCardView = ( props: ISISDatasetCardViewProps ): React.ReactElement => { - const { instrumentId, instrumentChildId, investigationId, dataPublication } = - props; + const { investigationId } = props; const [t] = useTranslation(); const location = useLocation(); @@ -89,10 +85,6 @@ const ISISDatasetsCardView = ( isMounted ); - const pathRoot = dataPublication ? 'browseDataPublications' : 'browse'; - const instrumentChild = dataPublication ? 'dataPublication' : 'facilityCycle'; - const urlPrefix = `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${instrumentChildId}/investigation/${investigationId}/dataset`; - const title: CardViewDetails = React.useMemo( () => ({ // Provide label for filter component. @@ -100,10 +92,10 @@ const ISISDatasetsCardView = ( // Provide both the dataKey (for tooltip) and content to render. dataKey: 'name', content: (dataset: Dataset) => - tableLink(`${urlPrefix}/${dataset.id}`, dataset.name, view), + tableLink(`${location.pathname}/${dataset.id}`, dataset.name, view), filterComponent: textFilter, }), - [t, textFilter, urlPrefix, view] + [t, textFilter, location.pathname, view] ); const description: CardViewDetails = React.useMemo( @@ -172,13 +164,13 @@ const ISISDatasetsCardView = ( rowData={dataset} viewDatafiles={(id: number) => { const url = view - ? `${urlPrefix}/${id}/datafile?view=${view}` - : `${urlPrefix}/${id}/datafile`; + ? `${location.pathname}/${id}/datafile?view=${view}` + : `${location.pathname}/${id}/datafile`; push(url); }} /> ), - [push, urlPrefix, view] + [push, location.pathname, view] ); return ( diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx index b35b0f14c..4bf3b500c 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx @@ -1,7 +1,7 @@ import { dGCommonInitialState, type Investigation } from 'datagateway-common'; import * as React from 'react'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../../../state/app.types'; @@ -23,6 +23,7 @@ import { import type { UserEvent } from '@testing-library/user-event/setup/setup'; import userEvent from '@testing-library/user-event'; import axios, { type AxiosResponse } from 'axios'; +import { paths } from '../../../page/pageContainer.component'; describe('ISIS Investigations - Card View', () => { let mockStore; @@ -32,7 +33,7 @@ describe('ISIS Investigations - Card View', () => { let replaceSpy: jest.SpyInstance; let user: UserEvent; - const renderComponent = (dataPublication = false): RenderResult => + const renderComponent = (): RenderResult => render( @@ -43,11 +44,7 @@ describe('ISIS Investigations - Card View', () => { }) } > - + @@ -67,12 +64,24 @@ describe('ISIS Investigations - Card View', () => { dataCollectionInvestigations: [ { id: 1, - investigation: { - id: 1, - title: 'Test 1', - name: 'Test 1', - visitId: '1', + dataCollection: { + id: 14, + dataPublications: [ + { + id: 15, + pid: 'Investigation.Data.Publication.Pid', + description: 'Investigation Data Publication description', + title: 'Investigation Data Publication', + type: { + id: 16, + name: 'investigation', + }, + }, + ], }, + }, + { + id: 1, dataCollection: { id: 11, dataPublications: [ @@ -80,9 +89,11 @@ describe('ISIS Investigations - Card View', () => { id: 12, pid: 'Data.Publication.Pid', description: 'Data Publication description', - modTime: '2019-06-10', - createTime: '2019-06-11', title: 'Data Publication', + type: { + id: 13, + name: 'study', + }, }, ], }, @@ -102,7 +113,14 @@ describe('ISIS Investigations - Card View', () => { ], }, ]; - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.toggle.isisInvestigation, { + instrumentId: '1', + facilityCycleId: '1', + }), + ], + }); replaceSpy = jest.spyOn(history, 'replace'); user = userEvent.setup(); @@ -232,16 +250,6 @@ describe('ISIS Investigations - Card View', () => { ); }); - it('correct link used for studyHierarchy', async () => { - renderComponent(true); - expect( - await screen.findByRole('link', { name: 'Test title 1' }) - ).toHaveAttribute( - 'href', - '/browseDataPublications/instrument/1/dataPublication/1/investigation/1' - ); - }); - it('updates filter query params on text filter', async () => { renderComponent(); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx index e54517fff..f73770e4a 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx @@ -43,14 +43,13 @@ const ActionButtonsContainer = styled('div')(({ theme }) => ({ interface ISISInvestigationsCardViewProps { instrumentId: string; - instrumentChildId: string; - dataPublication: boolean; + facilityCycleId: string; } const ISISInvestigationsCardView = ( props: ISISInvestigationsCardViewProps ): React.ReactElement => { - const { instrumentId, instrumentChildId, dataPublication } = props; + const { instrumentId, facilityCycleId } = props; const [t] = useTranslation(); const location = useLocation(); @@ -81,10 +80,8 @@ const ISISInvestigationsCardView = ( { filterType: 'where', filterValue: JSON.stringify({ - [dataPublication - ? 'dataCollectionInvestigations.dataCollection.dataPublications.id' - : 'investigationFacilityCycles.facilityCycle.id']: { - eq: parseInt(instrumentChildId), + 'investigationFacilityCycles.facilityCycle.id': { + eq: parseInt(facilityCycleId), }, }), }, @@ -111,7 +108,7 @@ const ISISInvestigationsCardView = ( }, { dataCollectionInvestigations: { - dataCollection: 'dataPublications', + dataCollection: { dataPublications: 'type' }, }, }, { @@ -124,24 +121,20 @@ const ISISInvestigationsCardView = ( isMounted ); - const pathRoot = dataPublication ? 'browseDataPublications' : 'browse'; - const instrumentChild = dataPublication ? 'dataPublication' : 'facilityCycle'; - const urlPrefix = `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${instrumentChildId}/investigation`; - const title: CardViewDetails = React.useMemo( () => ({ label: t('investigations.title'), dataKey: 'title', content: (investigation: Investigation) => tableLink( - `${urlPrefix}/${investigation.id}`, + `${location.pathname}/${investigation.id}`, investigation.title, view, 'isis-investigations-card-title' ), filterComponent: textFilter, }), - [t, textFilter, urlPrefix, view] + [location.pathname, t, textFilter, view] ); const description: CardViewDetails = React.useMemo( @@ -162,18 +155,17 @@ const ISISInvestigationsCardView = ( filterComponent: textFilter, }, { - // TODO: this was previously the Study DOI - currently there are no datapublication - // representations of Studies, only of Investigations themselves - // should this be showing the study DOI or the investigation DOI anyway? content: function doiFormat(entity: Investigation) { - if ( - entity?.dataCollectionInvestigations?.[0]?.dataCollection - ?.dataPublications?.[0] - ) { + const studyDataPublication = + entity.dataCollectionInvestigations?.filter( + (dci) => + dci.dataCollection?.dataPublications?.[0]?.type?.name === + 'study' + )?.[0]?.dataCollection?.dataPublications?.[0]; + if (studyDataPublication) { return externalSiteLink( - `https://doi.org/${entity.dataCollectionInvestigations?.[0]?.dataCollection?.dataPublications?.[0].pid}`, - entity.dataCollectionInvestigations?.[0]?.dataCollection - ?.dataPublications?.[0].pid, + `https://doi.org/${studyDataPublication.pid}`, + studyDataPublication.pid, 'isis-investigations-card-doi-link' ); } else { @@ -258,13 +250,13 @@ const ISISInvestigationsCardView = ( rowData={investigation} viewDatasets={(id: number) => { const url = view - ? `${urlPrefix}/${id}/dataset?view=${view}` - : `${urlPrefix}/${id}/dataset`; + ? `${location.pathname}/${id}/dataset?view=${view}` + : `${location.pathname}/${id}/dataset`; push(url); }} /> ), - [push, urlPrefix, view] + [location.pathname, push, view] ); return ( diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx index 665e4daaa..940869691 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx @@ -2,16 +2,22 @@ import * as React from 'react'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import configureStore from 'redux-mock-store'; import { StateType } from '../../../state/app.types'; -import { dGCommonInitialState, useDataPublication } from 'datagateway-common'; +import { + DataPublication, + dGCommonInitialState, + useDataPublication, + useDataPublications, +} from 'datagateway-common'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { createMemoryHistory, History } from 'history'; import { QueryClient, QueryClientProvider } from 'react-query'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import { render, type RenderResult, screen } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event/setup/setup'; import userEvent from '@testing-library/user-event'; import ISISDataPublicationLanding from './isisDataPublicationLanding.component'; +import { paths } from '../../../page/pageContainer.component'; jest.mock('datagateway-common', () => { const originalModule = jest.requireActual('datagateway-common'); @@ -20,6 +26,7 @@ jest.mock('datagateway-common', () => { __esModule: true, ...originalModule, useDataPublication: jest.fn(), + useDataPublications: jest.fn(), }; }); @@ -34,10 +41,7 @@ describe('ISIS Data Publication Landing page', () => { - + @@ -60,7 +64,7 @@ describe('ISIS Data Publication Landing page', () => { fullName: 'Jesse Smith', }, { - id: 4, + id: 6, contributorType: 'experimenter', fullName: '', }, @@ -70,10 +74,17 @@ describe('ISIS Data Publication Landing page', () => { { id: 1, instrument: { - id: 3, + id: 4, name: 'LARMOR', }, }, + { + id: 2, + instrument: { + id: 5, + name: 'ALF', + }, + }, ]; const investigation = { @@ -82,30 +93,36 @@ describe('ISIS Data Publication Landing page', () => { name: 'Name 1', summary: 'foo bar', visitId: '1', - doi: 'doi 1', - size: 1, - investigationInstruments: investigationInstrument, + doi: 'investigation doi 1.1', + fileSize: 1, + investigationInstruments: [investigationInstrument[0]], startDate: '2019-06-10', endDate: '2019-06-11', + releaseDate: '2019-06-12', }; - const initialData = [ - { - id: 7, - pid: 'doi 1', - description: 'foo bar', - users: users, - content: { - dataCollectionInvestigations: [ - { - investigation: investigation, - }, - ], - }, - startDate: '2019-06-10', - endDate: '2019-06-11', - }, - ]; + const investigation2 = { + id: 2, + title: 'Title 2', + name: 'Name 2', + summary: 'foo bar', + visitId: '2', + doi: 'investigation doi 1.2', + fileSize: 2, + investigationInstruments: [investigationInstrument[1]], + startDate: '2019-06-10', + endDate: '2019-06-11', + }; + + // dummy data to stop TS from complaining, gets overwritten in beforeEach + let initialStudyDataPublicationData: DataPublication = { + id: 1, + pid: '1', + title: '1', + modTime: '1', + createTime: '1', + }; + let initialInvestigationDataPublicationsData: DataPublication[] = []; beforeEach(() => { state = JSON.parse( @@ -115,11 +132,71 @@ describe('ISIS Data Publication Landing page', () => { }) ); - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath( + paths.dataPublications.landing.isisDataPublicationLanding, + { + instrumentId: '4', + dataPublicationId: '5', + } + ), + ], + }); user = userEvent.setup(); + initialStudyDataPublicationData = { + id: 5, + pid: 'doi 1', + users: users, + publicationDate: '2019-06-10', + title: 'Study title', + modTime: '2019-06-10', + createTime: '2019-06-11', + }; + initialInvestigationDataPublicationsData = [ + { + id: 8, + pid: 'investigation doi 1.1', + title: 'Title 1', + description: 'foo bar', + content: { + id: 21, + dataCollectionInvestigations: [ + { + id: 19, + investigation: investigation, + }, + ], + }, + modTime: '2019-06-10', + createTime: '2019-06-11', + publicationDate: '2019-06-11', + }, + { + id: 9, + pid: 'investigation doi 1.2', + title: 'Title 2', + content: { + id: 22, + dataCollectionInvestigations: [ + { + id: 20, + investigation: investigation2, + }, + ], + }, + modTime: '2019-06-10', + createTime: '2019-06-11', + }, + ]; + (useDataPublication as jest.Mock).mockReturnValue({ - data: initialData, + data: initialStudyDataPublicationData, + }); + + (useDataPublications as jest.Mock).mockReturnValue({ + data: initialInvestigationDataPublicationsData, }); }); @@ -143,7 +220,7 @@ describe('ISIS Data Publication Landing page', () => { }); it('in cards view', async () => { - history.replace('/?view=card'); + history.replace({ search: '?view=card' }); renderComponent(); await user.click( @@ -159,9 +236,22 @@ describe('ISIS Data Publication Landing page', () => { }); }); - it('users displayed correctly', async () => { + it('renders correctly', async () => { renderComponent(); + // displays doi + link correctly + expect(await screen.findByRole('link', { name: 'doi 1' })).toHaveAttribute( + 'href', + 'https://doi.org/doi 1' + ); + expect( + screen.getByText('datapublications.publication_date:') + ).toBeInTheDocument(); + expect(screen.getByText('2019-06-10')).toBeInTheDocument(); + expect(screen.getByText('Title 1')).toBeInTheDocument(); + expect(screen.getByText('foo bar')).toBeInTheDocument(); + + // renders users correctly expect( await screen.findByTestId('landing-dataPublication-user-0') ).toHaveTextContent('Principal Investigator: John Smith'); @@ -171,14 +261,114 @@ describe('ISIS Data Publication Landing page', () => { expect( await screen.findByTestId('landing-dataPublication-user-2') ).toHaveTextContent('Experimenter: Jesse Smith'); + + // renders parts (investigations) correctly + expect( + screen.getByRole('link', { name: 'Part DOI: investigation doi 1.1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/4/dataPublication/5/investigation/8' + ); + expect( + screen.getByText( + (_, element) => + element.textContent === 'investigations.instrument:LARMOR' + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + (_, element) => + element.textContent === 'investigations.release_date:2019-06-11' + ) + ).toBeInTheDocument(); + + expect( + screen.getByRole('link', { name: 'Part DOI: investigation doi 1.2' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/4/dataPublication/5/investigation/9' + ); + expect( + screen.getByText( + (_, element) => element.textContent === 'investigations.instrument:ALF' + ) + ).toBeInTheDocument(); + + // actions for investigations should be visible + expect( + document.getElementById('add-to-cart-btn-investigation-1') + ).toBeInTheDocument(); + expect(document.getElementById('download-btn-1')).toBeInTheDocument(); + expect( + document.getElementById('add-to-cart-btn-investigation-2') + ).toBeInTheDocument(); + expect(document.getElementById('download-btn-2')).toBeInTheDocument(); }); - it('displays DOI and renders the expected link', async () => { + it('renders correctly when info is missing', async () => { + // missing description + initialInvestigationDataPublicationsData[0].description = undefined; + initialStudyDataPublicationData.users = []; + // missing investigations - i.e. investigations not public yet + if ( + initialInvestigationDataPublicationsData[0].content + ?.dataCollectionInvestigations?.[0] + ) + initialInvestigationDataPublicationsData[0].content.dataCollectionInvestigations[0].investigation = + undefined; + if ( + initialInvestigationDataPublicationsData[1].content + ?.dataCollectionInvestigations?.[0] + ) + initialInvestigationDataPublicationsData[1].content.dataCollectionInvestigations[0].investigation = + undefined; + renderComponent(); - expect(await screen.findByRole('link', { name: 'doi 1' })).toHaveAttribute( + expect(screen.getByText('Description not provided')).toBeInTheDocument(); + + expect(screen.queryByText('investigations.details.users.label')).toBeNull(); + + // renders parts (investigations) correctly + expect( + screen.getByRole('link', { name: 'Part DOI: investigation doi 1.1' }) + ).toHaveAttribute( 'href', - 'https://doi.org/doi 1' + '/browseDataPublications/instrument/4/dataPublication/5/investigation/8' ); + + expect( + screen.getByRole('link', { name: 'Part DOI: investigation doi 1.2' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/4/dataPublication/5/investigation/9' + ); + + // actions for investigations should not be visible + expect( + document.getElementById('add-to-cart-btn-investigation-1') + ).not.toBeInTheDocument(); + expect(document.getElementById('download-btn-1')).not.toBeInTheDocument(); + expect( + document.getElementById('add-to-cart-btn-investigation-2') + ).not.toBeInTheDocument(); + expect(document.getElementById('download-btn-2')).not.toBeInTheDocument(); + }); + + it('renders structured data correctly', async () => { + renderComponent(); + + expect( + await screen.findByTestId('landing-dataPublication-user-0') + ).toBeInTheDocument(); + + expect(document.getElementById('dataPublication-5')).toMatchInlineSnapshot(` + + `); }); }); diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx index d431e78a0..158ad8e64 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx @@ -12,7 +12,6 @@ import { import { Assessment, CalendarToday, - Fingerprint, Public, Storage, } from '@mui/icons-material'; @@ -21,11 +20,12 @@ import { useDataPublication, ArrowTooltip, getTooltipText, - Investigation, tableLink, AddToCartButton, ViewsType, parseSearchToQuery, + useDataPublications, + DownloadButton, } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -75,11 +75,10 @@ export interface FormattedUser { interface LandingPageProps { dataPublicationId: string; - instrumentId: string; } interface LinkedInvestigationProps { - investigation: Investigation; + investigation: DataPublication; urlPrefix: string; view: ViewsType; } @@ -93,18 +92,15 @@ const LinkedInvestigation = ( const shortInvestigationInfo = [ { - content: (entity: Investigation) => entity.name, - label: t('investigations.name'), - icon: , - }, - { - content: (entity: Investigation) => - entity.investigationInstruments?.[0]?.instrument?.name, + content: (entity: DataPublication) => + entity.content?.dataCollectionInvestigations?.[0]?.investigation + ?.investigationInstruments?.[0]?.instrument?.name, label: t('investigations.instrument'), icon: , }, { - content: (entity: Investigation) => entity.releaseDate?.slice(0, 10), + content: (entity: DataPublication) => + entity.publicationDate?.slice(0, 10), label: t('investigations.release_date'), icon: , }, @@ -119,7 +115,7 @@ const LinkedInvestigation = ( > {tableLink( `${props.urlPrefix}/investigation/${investigation.id}`, - `${t('investigations.visit_id')}: ${investigation.visitId}`, + `${'Part DOI'}: ${investigation.pid}`, props.view )} @@ -134,13 +130,37 @@ const LinkedInvestigation = ( ))} - - - + {investigation.content?.dataCollectionInvestigations?.[0]?.investigation + ?.id && ( + + + + + )}
); }; @@ -155,22 +175,53 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { ); const [value, setValue] = React.useState<'details'>('details'); - const { instrumentId, dataPublicationId } = props; + const { dataPublicationId } = props; - const pathRoot = 'browseDataPublications'; - const instrumentChild = 'dataPublication'; - const urlPrefix = `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${dataPublicationId}`; + const { data: studyDataPublication } = useDataPublication( + parseInt(dataPublicationId) + ); - const { data } = useDataPublication(parseInt(dataPublicationId)); + const { data: investigationDataPublications } = useDataPublications([ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: dataPublicationId, + }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { + eq: 'investigation', + }, + }), + }, + { + filterType: 'include', + filterValue: JSON.stringify({ + content: { + dataCollectionInvestigations: { + investigation: { + investigationInstruments: 'instrument', + }, + }, + }, + }), + }, + ]); - const pid = data?.[0]?.pid; - const title = data?.[0]?.title; + const pid = studyDataPublication?.pid; + const title = investigationDataPublications?.[0]?.title; const description = React.useMemo( () => - data?.[0]?.description && data?.[0]?.description !== 'null' - ? data[0]?.description + investigationDataPublications?.[0]?.description && + investigationDataPublications?.[0]?.description !== 'null' + ? investigationDataPublications?.[0]?.description : 'Description not provided', - [data] + [investigationDataPublications] ); const formattedUsers = React.useMemo(() => { @@ -178,40 +229,38 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { const contacts: FormattedUser[] = []; const experimenters: FormattedUser[] = []; - if (data?.[0]?.users) { - const dataPublicationUsers = data[0]?.users; - dataPublicationUsers.forEach((user) => { - // Only keep users where we have their fullName - const fullname = user.fullName; - if (fullname) { - switch (user.contributorType) { - case 'principal_experimenter': - principals.push({ - fullName: fullname, - contributorType: 'Principal Investigator', - }); - break; - case 'local_contact': - contacts.push({ - fullName: fullname, - contributorType: 'Local Contact', - }); - break; - default: - experimenters.push({ - fullName: fullname, - contributorType: 'Experimenter', - }); - } + studyDataPublication?.users?.forEach((user) => { + // Only keep users where we have their fullName + const fullname = user.fullName; + if (fullname) { + switch (user.contributorType) { + case 'principal_experimenter': + principals.push({ + fullName: fullname, + contributorType: 'Principal Investigator', + }); + break; + case 'local_contact': + contacts.push({ + fullName: fullname, + contributorType: 'Local Contact', + }); + break; + default: + experimenters.push({ + fullName: fullname, + contributorType: 'Experimenter', + }); } - }); - } + } + }); + // Ensure PIs are listed first, and sort within roles for consistency principals.sort((a, b) => a.fullName.localeCompare(b.fullName)); contacts.sort((a, b) => a.fullName.localeCompare(b.fullName)); experimenters.sort((a, b) => a.fullName.localeCompare(b.fullName)); return principals.concat(contacts, experimenters); - }, [data]); + }, [studyDataPublication]); React.useEffect(() => { const scriptId = `dataPublication-${dataPublicationId}`; @@ -246,18 +295,14 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { url: t('doi_constants.publisher.url'), }, }, + creator: formattedUsers.map((user) => { + return { '@type': 'Person', name: user.fullName }; + }), includedInDataCatalog: { '@type': 'DataCatalog', url: t('doi_constants.distribution.content_url'), }, - distribution: [ - { - '@type': 'DataDownload', - encodingFormat: t('doi_constants.distribution.format'), - // TODO format contentUrl with and actual download link if possible - contentUrl: t('doi_constants.distribution.content_url'), - }, - ], + license: t('doi_constants.distribution.license'), }); return () => { @@ -266,7 +311,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { currentScript.remove(); } }; - }, [t, title, pid, dataPublicationId, description]); + }, [t, title, pid, dataPublicationId, description, formattedUsers]); const shortInfo = [ { @@ -333,8 +378,8 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { onClick={() => push( view - ? `${urlPrefix}/investigation?view=${view}` - : `${urlPrefix}/investigation` + ? `${location.pathname}/investigation?view=${view}` + : `${location.pathname}/investigation` ) } /> @@ -384,7 +429,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { doi={pid} formattedUsers={formattedUsers} title={title} - startDate={data?.[0]?.createTime} + startDate={studyDataPublication?.publicationDate} /> @@ -393,8 +438,8 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { {shortInfo.map( (field, i) => - data?.[0] && - field.content(data[0] as DataPublication) && ( + studyDataPublication && + field.content(studyDataPublication as DataPublication) && ( {field.icon} @@ -402,31 +447,25 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { - {field.content(data[0] as DataPublication)} + {field.content(studyDataPublication as DataPublication)} ) )} {/* Parts */} - {data?.map((dataPublication, i) => ( + {investigationDataPublications?.map((dataPublication, i) => ( - {dataPublication?.content?.dataCollectionInvestigations?.[0] - ?.investigation && ( - - )} + ))} diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx index f8b950451..a5dccc6ba 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx @@ -12,10 +12,11 @@ import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { createMemoryHistory, History } from 'history'; import { QueryClient, QueryClientProvider } from 'react-query'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import { render, type RenderResult, screen } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event/setup/setup'; import userEvent from '@testing-library/user-event'; +import { paths } from '../../../page/pageContainer.component'; jest.mock('datagateway-common', () => { const originalModule = jest.requireActual('datagateway-common'); @@ -34,18 +35,12 @@ describe('ISIS Dataset Landing page', () => { let history: History; let user: UserEvent; - const renderComponent = (dataPublication = false): RenderResult => + const renderComponent = (): RenderResult => render( - + @@ -75,7 +70,16 @@ describe('ISIS Dataset Landing page', () => { dgcommon: dGCommonInitialState, }) ); - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.landing.isisDatasetLanding, { + instrumentId: '4', + investigationId: '1', + facilityCycleId: '5', + datasetId: '87', + }), + ], + }); user = userEvent.setup(); (useDatasetDetails as jest.Mock).mockReturnValue({ @@ -101,7 +105,7 @@ describe('ISIS Dataset Landing page', () => { }); it('for facility cycle hierarchy and cards view', async () => { - history.replace('/?view=card'); + history.replace({ search: '?view=card' }); renderComponent(); await user.click( @@ -115,7 +119,15 @@ describe('ISIS Dataset Landing page', () => { }); it('for data publication hierarchy and normal view', async () => { - renderComponent(true); + history.replace( + generatePath(paths.dataPublications.landing.isisDatasetLanding, { + instrumentId: '4', + investigationId: '1', + dataPublicationId: '5', + datasetId: '87', + }) + ); + renderComponent(); await user.click( await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) @@ -127,8 +139,19 @@ describe('ISIS Dataset Landing page', () => { }); it('for data publication hierarchy and cards view', async () => { - history.replace('/?view=card'); - renderComponent(true); + history.replace({ + pathname: generatePath( + paths.dataPublications.landing.isisDatasetLanding, + { + instrumentId: '4', + investigationId: '1', + dataPublicationId: '5', + datasetId: '87', + } + ), + search: '?view=card', + }); + renderComponent(); await user.click( await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx index 6e5b93425..4bf307fdc 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx @@ -60,11 +60,7 @@ const ActionButtonsContainer = styled('div')(({ theme }) => ({ })); interface LandingPageProps { - instrumentId: string; - instrumentChildId: string; - investigationId: string; datasetId: string; - dataPublication: boolean; } const LandingPage = (props: LandingPageProps): React.ReactElement => { @@ -76,17 +72,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { [location.search] ); const [value, setValue] = React.useState<'details'>('details'); - const { - instrumentId, - instrumentChildId, - investigationId, - datasetId, - dataPublication, - } = props; - - const pathRoot = dataPublication ? 'browseDataPublications' : 'browse'; - const instrumentChild = dataPublication ? 'dataPublication' : 'facilityCycle'; - const urlPrefix = `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${instrumentChildId}/investigation/${investigationId}/dataset/${datasetId}`; + const { datasetId } = props; const { data } = useDatasetDetails(parseInt(datasetId)); @@ -158,8 +144,8 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { onClick={() => push( view - ? `${urlPrefix}/datafile?view=${view}` - : `${urlPrefix}/datafile` + ? `${location.pathname}/datafile?view=${view}` + : `${location.pathname}/datafile` ) } /> 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 261fbaef3..333126516 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 @@ -3,19 +3,26 @@ import ISISInvestigationLanding from './isisInvestigationLanding.component'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import configureStore from 'redux-mock-store'; import { StateType } from '../../../state/app.types'; -import { dGCommonInitialState, useInvestigation } from 'datagateway-common'; +import { + DataPublication, + dGCommonInitialState, + Investigation, + useDataPublication, + useDataPublications, + useInvestigation, +} from 'datagateway-common'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { createMemoryHistory, History } from 'history'; import { QueryClient, QueryClientProvider } from 'react-query'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import { render, - type RenderResult, screen, within, + type RenderResult, } from '@testing-library/react'; -import { UserEvent } from '@testing-library/user-event/setup/setup'; +import { paths } from '../../../page/pageContainer.component'; import userEvent from '@testing-library/user-event'; jest.mock('datagateway-common', () => { @@ -25,7 +32,8 @@ jest.mock('datagateway-common', () => { __esModule: true, ...originalModule, useInvestigation: jest.fn(), - useInvestigationSizes: jest.fn(), + useDataPublication: jest.fn(), + useDataPublications: jest.fn(), }; }); @@ -33,7 +41,7 @@ describe('ISIS Investigation Landing page', () => { const mockStore = configureStore([thunk]); let state: StateType; let history: History; - let user: UserEvent; + let user: ReturnType; const renderComponent = (dataPublication = false): RenderResult => render( @@ -41,8 +49,6 @@ describe('ISIS Investigation Landing page', () => { @@ -51,73 +57,9 @@ describe('ISIS Investigation Landing page', () => { ); - const initialData = [ - { - id: 1, - title: 'Test title 1', - name: 'Test 1', - fileSize: 1, - fileCount: 1, - summary: 'foo bar', - visitId: 'visit id 1', - doi: 'doi 1', - size: 1, - facility: { - name: 'LILS', - }, - investigationInstruments: [ - { - id: 1, - instrument: { - id: 3, - name: 'LARMOR', - }, - investigation: { - id: 1, - }, - }, - ], - dataCollectionInvestigations: [ - { - id: 1, - investigation: { - id: 1, - title: 'Test title 1', - name: 'Test 1', - visitId: 'visit id 1', - }, - dataCollection: { - id: 11, - dataPublications: [ - { - id: 7, - pid: 'Data Publication Pid', - description: 'Data Publication description', - modTime: '2019-06-10', - createTime: '2019-06-11', - title: 'Data Publication', - }, - ], - }, - }, - ], - startDate: '2019-06-10', - endDate: '2019-06-11', - datasets: [ - { - id: 1, - name: 'dataset 1', - doi: 'dataset doi', - }, - ], - }, - ]; const investigationUser = [ { id: 1, - investigation: { - id: 1, - }, role: 'principal_experimenter', user: { id: 1, @@ -127,9 +69,6 @@ describe('ISIS Investigation Landing page', () => { }, { id: 2, - investigation: { - id: 1, - }, role: 'local_contact', user: { id: 2, @@ -139,9 +78,6 @@ describe('ISIS Investigation Landing page', () => { }, { id: 3, - investigation: { - id: 1, - }, role: 'experimenter', user: { id: 3, @@ -151,9 +87,6 @@ describe('ISIS Investigation Landing page', () => { }, { id: 4, - investigation: { - id: 1, - }, role: 'experimenter', user: { id: 4, @@ -177,8 +110,41 @@ describe('ISIS Investigation Landing page', () => { fullReference: 'Journal, Author, Date, DOI', }, ]; - const noSamples: never[] = []; - const noPublication: never[] = []; + + let initialInvestigationData: Investigation[] = []; + + const users = [ + { + id: 1, + contributorType: 'principal_experimenter', + fullName: 'John Smith', + }, + { + id: 2, + contributorType: 'local_contact', + fullName: 'Jane Smith', + }, + { + id: 3, + contributorType: 'experimenter', + fullName: 'Jesse Smith', + }, + { + id: 6, + contributorType: 'experimenter', + fullName: '', + }, + ]; + + // dummy data to stop TS from complaining, gets overwritten in beforeEach + let initialDataPublicationData: DataPublication = { + id: 1, + pid: '1', + title: '1', + modTime: '1', + createTime: '1', + }; + let initialStudyDataPublicationData: DataPublication[] = []; beforeEach(() => { state = JSON.parse( @@ -187,11 +153,121 @@ describe('ISIS Investigation Landing page', () => { dgcommon: dGCommonInitialState, }) ); - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.landing.isisInvestigationLanding, { + instrumentId: '4', + facilityCycleId: '5', + investigationId: '1', + }), + ], + }); user = userEvent.setup(); + initialInvestigationData = [ + { + id: 1, + title: 'Test title 1', + name: 'Test 1', + fileSize: 1, + fileCount: 1, + summary: 'foo bar', + visitId: 'visit id 1', + doi: 'doi 1', + size: 1, + facility: { + id: 17, + name: 'LILS', + }, + investigationInstruments: [ + { + id: 1, + instrument: { + id: 3, + name: 'LARMOR', + }, + }, + ], + dataCollectionInvestigations: [ + { + id: 1, + dataCollection: { + id: 11, + dataPublications: [ + { + id: 7, + pid: 'Data Publication Pid', + description: 'Data Publication description', + modTime: '2019-06-10', + createTime: '2019-06-11', + title: 'Data Publication', + type: { id: 12, name: 'study' }, + }, + ], + }, + }, + ], + startDate: '2019-06-10', + endDate: '2019-06-11', + datasets: [ + { + id: 1, + name: 'dataset 1', + doi: 'dataset doi', + modTime: '2019-06-10', + createTime: '2019-06-10', + }, + ], + investigationUsers: investigationUser, + publications: publication, + samples: sample, + }, + ]; + + initialStudyDataPublicationData = [ + { + id: 5, + pid: '10.1234/ISIS.E.RB123456', + title: 'Title 1', + publicationDate: '2019-06-10', + modTime: '2019-06-10', + createTime: '2019-06-10', + }, + ]; + initialDataPublicationData = { + id: 8, + pid: '10.1234/ISIS.E.RB123456-1', + title: 'Title 1.1', + description: 'foo bar', + publicationDate: '2019-06-10', + content: { + id: 10, + dataCollectionInvestigations: [ + { + id: 9, + investigation: initialInvestigationData[0], + }, + ], + }, + users, + facility: { + id: 17, + name: 'LILS', + }, + modTime: '2019-06-10', + createTime: '2019-06-10', + }; + (useInvestigation as jest.Mock).mockReturnValue({ - data: initialData, + data: initialInvestigationData, + }); + + (useDataPublication as jest.Mock).mockReturnValue({ + data: initialDataPublicationData, + }); + + (useDataPublications as jest.Mock).mockReturnValue({ + data: initialStudyDataPublicationData, }); }); @@ -199,7 +275,7 @@ describe('ISIS Investigation Landing page', () => { jest.clearAllMocks(); }); - it('renders landing for investigation correctly', () => { + it('renders correctly for facility cycle hierarchy', () => { renderComponent(); // branding should be visible @@ -221,27 +297,27 @@ describe('ISIS Investigation Landing page', () => { screen.getByText('doi_constants.publisher.name') ).toBeInTheDocument(); - // investigation samples should be hidden (initial data does not have samples) + // investigation samples should displayed correctly expect( - screen.queryByText('investigations.details.samples.label') - ).toBeNull(); - expect( - screen.queryByText('investigations.details.samples.no_samples') - ).toBeNull(); + screen.getByText('investigations.details.samples.label') + ).toBeInTheDocument(); expect( screen.queryAllByLabelText(/landing-investigation-sample-\d+$/) - ).toHaveLength(0); - - // publication section should be hidden (initial data does not have publications) + ).toHaveLength(1); expect( - screen.queryByText('investigations.details.publications.label') - ).toBeNull(); + screen.getByLabelText('landing-investigation-sample-0') + ).toHaveTextContent('Sample'); + + // publications should be displayed correctly expect( - screen.queryByText('investigations.details.publications.no_publications') - ).toBeNull(); + screen.getByText('investigations.details.publications.label') + ).toBeInTheDocument(); expect( screen.queryAllByLabelText(/landing-investigation-reference-\d+$/) - ).toHaveLength(0); + ).toHaveLength(1); + expect( + screen.getByLabelText('landing-investigation-reference-0') + ).toHaveTextContent('Journal, Author, Date, DOI'); // short format information should be visible expect(screen.getByText('investigations.visit_id:')).toBeInTheDocument(); @@ -291,126 +367,237 @@ describe('ISIS Investigation Landing page', () => { name: 'buttons.add_to_cart', }) ).toBeInTheDocument(); + expect( + within(actionButtonContainer).getByRole('button', { + name: 'buttons.download', + }) + ).toBeInTheDocument(); + + // datasets should be visible + + expect( + screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + ).toHaveAttribute( + 'href', + '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1' + ); + expect(screen.getByText('datasets.doi:')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'dataset doi' })).toHaveAttribute( + 'href', + 'https://doi.org/dataset doi' + ); + + // actions for datasets should be visible + const actionContainer = screen.getByTestId( + 'investigation-landing-dataset-0-action-container' + ); + expect(actionContainer).toBeInTheDocument(); + expect( + within(actionContainer).getByRole('button', { + name: 'buttons.add_to_cart', + }) + ).toBeInTheDocument(); }); - describe('renders datasets for the investigation correctly', () => { - it('for facility cycle hierarchy and normal view', () => { - renderComponent(); + it('renders correctly for facility cycle hierarchy when no description, users, samples or publications', async () => { + initialInvestigationData[0].summary = undefined; + initialInvestigationData[0].publications = []; + initialInvestigationData[0].samples = []; + initialInvestigationData[0].investigationUsers = undefined; - expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) - ).toHaveAttribute( - 'href', - '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1' - ); - expect(screen.getByText('datasets.doi:')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'dataset doi' })).toHaveAttribute( - 'href', - 'https://doi.org/dataset doi' - ); + renderComponent(); - // actions for datasets should be visible - const actionContainer = screen.getByTestId( - 'investigation-landing-dataset-0-action-container' - ); - expect(actionContainer).toBeInTheDocument(); - expect( - within(actionContainer).getByRole('button', { - name: 'buttons.add_to_cart', - }) - ).toBeInTheDocument(); - }); + expect(screen.getByText('Test title 1')).toBeInTheDocument(); + expect(screen.getByText('Description not provided')).toBeInTheDocument(); - it('for facility cycle hierarchy and card view', () => { - history.replace('/?view=card'); + // no investigation samples, so show no samples message + expect( + screen.getByText('investigations.details.samples.no_samples') + ).toBeInTheDocument(); - renderComponent(); + // no investigation publications, so show no publications message + expect( + screen.getByText('investigations.details.publications.no_publications') + ); - expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) - ).toHaveAttribute( - 'href', - '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1?view=card' - ); - expect(screen.getByText('datasets.doi:')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'dataset doi' })).toHaveAttribute( - 'href', - 'https://doi.org/dataset doi' - ); + // no users, so should hide users section + expect(screen.queryByText('investigations.details.users.label')).toBeNull(); + }); - // actions for datasets should be visible - const actionContainer = screen.getByTestId( - 'investigation-landing-dataset-0-action-container' - ); - expect(actionContainer).toBeInTheDocument(); - expect( - within(actionContainer).getByRole('button', { - name: 'buttons.add_to_cart', - }) - ).toBeInTheDocument(); - }); + it('renders correctly for data publication hierarchy', () => { + history.replace( + generatePath(paths.dataPublications.landing.isisInvestigationLanding, { + instrumentId: '4', + dataPublicationId: '5', + investigationId: '1', + }) + ); + renderComponent(true); - it('for data publication hierarchy and normal view', () => { - renderComponent(true); + // branding should be visible + expect(screen.getByRole('img', { name: 'STFC Logo' })).toBeInTheDocument(); + expect( + screen.getByText('doi_constants.branding.title') + ).toBeInTheDocument(); + expect(screen.getByText('doi_constants.branding.body')).toBeInTheDocument(); - expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) - ).toHaveAttribute( - 'href', - '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1' - ); - expect(screen.getByText('datasets.doi:')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'dataset doi' })).toHaveAttribute( - 'href', - 'https://doi.org/dataset doi' - ); + // investigation details should be visible + expect(screen.getByText('Title 1.1')).toBeInTheDocument(); + expect(screen.getByText('foo bar')).toBeInTheDocument(); - // actions for datasets should be visible - const actionContainer = screen.getByTestId( - 'investigation-landing-dataset-0-action-container' - ); - expect(actionContainer).toBeInTheDocument(); - expect( - within(actionContainer).getByRole('button', { - name: 'buttons.add_to_cart', - }) - ).toBeInTheDocument(); - }); + // publisher section should be visible + expect( + screen.getByText('datapublications.details.publisher') + ).toBeInTheDocument(); + expect( + screen.getByText('doi_constants.publisher.name') + ).toBeInTheDocument(); - it('for data publication hierarchy and card view', () => { - history.push('/?view=card'); + // investigation samples should be hidden + expect( + screen.queryByText('investigations.details.samples.label') + ).toBeNull(); + expect( + screen.queryByText('investigations.details.samples.no_samples') + ).toBeNull(); + expect( + screen.queryAllByLabelText(/landing-investigation-sample-\d+$/) + ).toHaveLength(0); - renderComponent(true); + // publication section should be hidden + expect( + screen.queryByText('investigations.details.publications.label') + ).toBeNull(); + expect( + screen.queryByText('investigations.details.publications.no_publications') + ).toBeNull(); + expect( + screen.queryAllByLabelText(/landing-investigation-reference-\d+$/) + ).toHaveLength(0); - expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) - ).toHaveAttribute( - 'href', - '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1?view=card' - ); - expect(screen.getByText('datasets.doi:')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'dataset doi' })).toHaveAttribute( - 'href', - 'https://doi.org/dataset doi' - ); + // short format information should be visible + expect(screen.getByText('investigations.visit_id:')).toBeInTheDocument(); + expect(screen.getByText('visit id 1')).toBeInTheDocument(); + expect(screen.getByText('investigations.doi:')).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: '10.1234/ISIS.E.RB123456-1' }) + ).toHaveAttribute('href', 'https://doi.org/10.1234/ISIS.E.RB123456-1'); + expect(screen.getByText('investigations.parent_doi:')).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: '10.1234/ISIS.E.RB123456' }) + ).toHaveAttribute('href', 'https://doi.org/10.1234/ISIS.E.RB123456'); + expect(screen.getByText('investigations.name:')).toBeInTheDocument(); + expect(screen.getByText('Title 1')).toBeInTheDocument(); + expect( + screen.getByText('investigations.details.facility:') + ).toBeInTheDocument(); + expect(screen.getByText('LILS')).toBeInTheDocument(); + expect(screen.getByText('investigations.instrument:')).toBeInTheDocument(); + expect(screen.getByText('LARMOR')).toBeInTheDocument(); + expect( + screen.getByText('datapublications.details.format:') + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: 'doi_constants.distribution.format' }) + ).toHaveAttribute( + 'href', + 'https://www.isis.stfc.ac.uk/Pages/ISIS-Raw-File-Format.aspx' + ); + expect( + screen.getByText('investigations.release_date:') + ).toBeInTheDocument(); + expect(screen.getByText('2019-06-10')).toBeInTheDocument(); - // actions for datasets should be visible - const actionContainer = screen.getByTestId( - 'investigation-landing-dataset-0-action-container' - ); - expect(actionContainer).toBeInTheDocument(); - expect( - within(actionContainer).getByRole('button', { - name: 'buttons.add_to_cart', - }) - ).toBeInTheDocument(); - }); + const actionButtonContainer = screen.getByTestId( + 'investigation-landing-action-container' + ); + + // actions should be visible + expect(actionButtonContainer).toBeInTheDocument(); + expect( + within(actionButtonContainer).getByRole('button', { + name: 'buttons.add_to_cart', + }) + ).toBeInTheDocument(); + expect( + within(actionButtonContainer).getByRole('button', { + name: 'buttons.download', + }) + ).toBeInTheDocument(); + + // datasets should be visible + expect( + screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1' + ); + + // actions for datasets should be visible + const actionContainer = screen.getByTestId( + 'investigation-landing-dataset-0-action-container' + ); + expect(actionContainer).toBeInTheDocument(); + expect( + within(actionContainer).getByRole('button', { + name: 'buttons.add_to_cart', + }) + ).toBeInTheDocument(); + }); + + it('renders correctly for data publication hierarchy when no investigations or description', () => { + initialDataPublicationData.description = undefined; + if (initialDataPublicationData.content?.dataCollectionInvestigations?.[0]) + initialDataPublicationData.content.dataCollectionInvestigations[0].investigation = + undefined; + + history.replace( + generatePath(paths.dataPublications.landing.isisInvestigationLanding, { + instrumentId: '4', + dataPublicationId: '5', + investigationId: '1', + }) + ); + renderComponent(true); + + expect(screen.getByText('Description not provided')).toBeInTheDocument(); + + expect( + screen.getByText( + (_, element) => element.textContent === 'investigations.visit_id:1' + ) + ).toBeInTheDocument(); + + // should not display missing info + + expect( + screen.queryByText('investigations.instrument') + ).not.toBeInTheDocument(); + + expect( + screen.queryByRole('tab', { name: 'investigation-datasets-tab' }) + ).not.toBeInTheDocument(); + + expect( + screen.queryByTestId('investigation-landing-action-container') + ).not.toBeInTheDocument(); + + expect( + screen.queryByLabelText('landing-investigation-part-label') + ).not.toBeInTheDocument(); }); describe('links to the correct url in the datafiles tab', () => { it('for facility cycle hierarchy and normal view', async () => { renderComponent(); + expect( + screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + ).toHaveAttribute( + 'href', + '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1' + ); + await user.click( await screen.findByRole('tab', { name: 'investigations.details.datasets', @@ -423,9 +610,16 @@ describe('ISIS Investigation Landing page', () => { }); it('for facility cycle hierarchy and cards view', async () => { - history.replace('/?view=card'); + history.replace({ search: '?view=card' }); renderComponent(); + expect( + screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + ).toHaveAttribute( + 'href', + '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1?view=card' + ); + await user.click( await screen.findByRole('tab', { name: 'investigations.details.datasets', @@ -439,9 +633,22 @@ describe('ISIS Investigation Landing page', () => { }); it('for data publication hierarchy and normal view', async () => { - history.replace('/?view=card'); + history.replace( + generatePath(paths.dataPublications.landing.isisInvestigationLanding, { + instrumentId: '4', + dataPublicationId: '5', + investigationId: '1', + }) + ); renderComponent(true); + expect( + screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1' + ); + await user.click( await screen.findByRole('tab', { name: 'investigations.details.datasets', @@ -454,9 +661,26 @@ describe('ISIS Investigation Landing page', () => { }); it('for data publication hierarchy and cards view', async () => { - history.replace('/?view=card'); + history.replace({ + pathname: generatePath( + paths.dataPublications.landing.isisInvestigationLanding, + { + instrumentId: '4', + dataPublicationId: '5', + investigationId: '1', + } + ), + search: '?view=card', + }); renderComponent(true); + expect( + screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1?view=card' + ); + await user.click( await screen.findByRole('tab', { name: 'investigations.details.datasets', @@ -469,119 +693,4 @@ describe('ISIS Investigation Landing page', () => { expect(history.location.search).toBe('?view=card'); }); }); - - it('users displayed correctly', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], investigationUsers: investigationUser }], - }); - renderComponent(); - - expect( - await screen.findByLabelText('landing-investigation-user-0') - ).toHaveTextContent('Principal Investigator: John Smith'); - expect( - await screen.findByLabelText('landing-investigation-user-1') - ).toHaveTextContent('Local Contact: Jane Smith'); - expect( - await screen.findByLabelText('landing-investigation-user-2') - ).toHaveTextContent('Experimenter: Jesse Smith'); - }); - - it('renders text "No samples" when no data is present', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], samples: noSamples }], - }); - renderComponent(); - - expect( - await screen.findByText('investigations.details.samples.no_samples') - ).toBeInTheDocument(); - }); - - it('renders text "No publications" when no data is present', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], publications: noPublication }], - }); - renderComponent(); - - expect( - await screen.findByText( - 'investigations.details.publications.no_publications' - ) - ).toBeInTheDocument(); - }); - - it('publications displayed correctly', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], publications: publication }], - }); - renderComponent(); - - expect( - await screen.findByText('Journal, Author, Date, DOI') - ).toBeInTheDocument(); - }); - - it('samples displayed correctly', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], samples: sample }], - }); - 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 }], - }); - renderComponent(); - - expect( - await screen.findByText( - '2019: Test title 1, doi_constants.publisher.name, https://doi.org/doi 1' - ) - ).toBeInTheDocument(); - }); - - it('displays citation correctly with one user', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], investigationUsers: [investigationUser[0]] }], - }); - renderComponent(); - - expect( - await screen.findByText( - 'John Smith; 2019: Test title 1, doi_constants.publisher.name, https://doi.org/doi 1' - ) - ).toBeInTheDocument(); - }); - - it('displays citation correctly with multiple users', async () => { - (useInvestigation as jest.Mock).mockReturnValue({ - data: [{ ...initialData[0], investigationUsers: investigationUser }], - }); - renderComponent(); - - expect( - await screen.findByText( - 'John Smith et al; 2019: Test title 1, doi_constants.publisher.name, https://doi.org/doi 1' - ) - ).toBeInTheDocument(); - }); - - it('displays DOI and renders the expected Link ', async () => { - renderComponent(); - expect(await screen.findByRole('link', { name: 'doi 1' })).toHaveAttribute( - 'href', - 'https://doi.org/doi 1' - ); - }); - - it('displays Experiment DOI (PID) and renders the expected Link ', async () => { - renderComponent(); - expect( - await screen.findByRole('link', { name: 'Data Publication Pid' }) - ).toHaveAttribute('href', 'https://doi.org/Data Publication Pid'); - }); }); 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 a3b69dfb4..06c67814f 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx @@ -20,7 +20,6 @@ import { import { Dataset, Investigation, - InvestigationUser, parseSearchToQuery, Publication, Sample, @@ -32,6 +31,9 @@ import { getTooltipText, formatBytes, externalSiteLink, + useDataPublication, + DataPublication, + useDataPublications, } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -76,32 +78,56 @@ const ActionButtonsContainer = styled('div')(({ theme }) => ({ interface FormattedUser { role?: string; + contributorType?: string; fullName: string; } interface LandingPageProps { - instrumentId: string; - instrumentChildId: string; investigationId: string; dataPublication: boolean; } -const LandingPage = (props: LandingPageProps): React.ReactElement => { - const [t] = useTranslation(); - const { push } = useHistory(); - const location = useLocation(); - const { view } = React.useMemo( - () => parseSearchToQuery(location.search), - [location.search] - ); - const [value, setValue] = React.useState<'details'>('details'); - const { instrumentId, instrumentChildId, investigationId, dataPublication } = - props; +const InvestigationDataPublicationLandingPage = ( + props: LandingPageProps +): React.ReactElement => { + const { investigationId } = props; + + const { data } = useDataPublication(parseInt(investigationId)); + + const { data: studyDataPublications } = useDataPublications([ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: investigationId, + }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { + eq: 'study', + }, + }), + }, + ]); + + const studyDataPublication = studyDataPublications?.[0]; - const pathRoot = dataPublication ? 'browseDataPublications' : 'browse'; - const instrumentChild = dataPublication ? 'dataPublication' : 'facilityCycle'; - const urlPrefix = `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${instrumentChildId}/investigation/${investigationId}`; + return ( + + ); +}; +const InvestigationLandingPage = ( + props: LandingPageProps +): React.ReactElement => { + const { investigationId } = props; const { data } = useInvestigation(parseInt(investigationId), [ { filterType: 'include', @@ -113,7 +139,9 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { 'publications', 'datasets', { - dataCollectionInvestigations: { dataCollection: 'dataPublications' }, + dataCollectionInvestigations: { + dataCollection: { dataPublications: 'type' }, + }, }, { investigationInstruments: 'instrument', @@ -122,19 +150,51 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { }, ]); - const title = React.useMemo(() => data?.[0]?.title, [data]); - const doi = React.useMemo(() => data?.[0]?.doi, [data]); + return ; +}; + +interface CommonLandingPageProps { + data?: DataPublication | Investigation[]; + studyDataPublication?: DataPublication; +} + +const CommonLandingPage = ( + props: CommonLandingPageProps +): React.ReactElement => { + const [t] = useTranslation(); + const { push } = useHistory(); + const location = useLocation(); + const { view } = React.useMemo( + () => parseSearchToQuery(location.search), + [location.search] + ); + const [value, setValue] = React.useState<'details'>('details'); + const { data, studyDataPublication } = props; + + const title = React.useMemo( + () => (Array.isArray(data) ? data?.[0]?.title : data?.title), + [data] + ); + const doi = React.useMemo( + () => (Array.isArray(data) ? data?.[0]?.doi : data?.pid), + [data] + ); const formattedUsers = React.useMemo(() => { const principals: FormattedUser[] = []; const contacts: FormattedUser[] = []; const experimenters: FormattedUser[] = []; - if (data?.[0]?.investigationUsers) { - const investigationUsers = data?.[0] - .investigationUsers as InvestigationUser[]; - investigationUsers.forEach((user) => { + const users = Array.isArray(data) + ? data?.[0]?.investigationUsers + : data?.users; + if (users) { + users.forEach((u) => { + let user: { role?: string; fullName?: string } = {}; + if ('user' in u) user = { fullName: u.user?.fullName, role: u.role }; + if ('contributorType' in u) + user = { fullName: u.fullName, role: u.contributorType }; // Only keep users where we have their fullName - const fullname = user.user?.fullName; + const fullname = user.fullName; if (fullname) { switch (user.role) { case 'principal_experimenter': @@ -160,7 +220,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { }, [data]); const formattedPublications = React.useMemo(() => { - if (data?.[0]?.publications) { + if (Array.isArray(data) && data?.[0]?.publications) { return (data[0].publications as Publication[]).map( (publication) => publication.fullReference ); @@ -168,114 +228,191 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { }, [data]); const formattedSamples = React.useMemo(() => { - if (data?.[0]?.samples) { + if (Array.isArray(data) && data?.[0]?.samples) { return (data[0].samples as Sample[]).map((sample) => sample.name); } }, [data]); - const shortInfo = [ - { - content: (entity: Investigation) => entity.visitId, - label: t('investigations.visit_id'), - icon: , - }, - { - content: function doiFormat(entity: Investigation) { - return ( - entity?.doi && - externalSiteLink( - `https://doi.org/${entity.doi}`, - entity.doi, - 'isis-investigation-landing-doi-link' - ) - ); - }, - label: t('investigations.doi'), - icon: , - }, - // TODO: when datapublications are created for studies, need to pick the study datapublication - { - content: function parentDoiFormat(entity: Investigation) { - return ( - entity.dataCollectionInvestigations?.[0]?.dataCollection - ?.dataPublications?.[0] && - externalSiteLink( - `https://doi.org/${entity.dataCollectionInvestigations?.[0]?.dataCollection?.dataPublications?.[0].pid}`, - entity.dataCollectionInvestigations?.[0]?.dataCollection - ?.dataPublications?.[0].pid, - 'isis-investigations-landing-parent-doi-link' - ) - ); - }, - label: t('investigations.parent_doi'), - icon: , - }, - { - content: (entity: Investigation) => entity.name, - label: t('investigations.name'), - icon: , - }, - { - content: (entity: Investigation) => { - return formatBytes(entity.fileSize); - }, - label: t('investigations.size'), - icon: , - }, - { - content: (entity: Investigation) => entity.facility?.name, - label: t('investigations.details.facility'), - icon: , - }, - { - content: (entity: Investigation) => - entity.investigationInstruments?.[0]?.instrument?.name, - label: t('investigations.instrument'), - icon: , - }, - { - content: function distributionFormat(entity: Investigation) { - return externalSiteLink( - 'https://www.isis.stfc.ac.uk/Pages/ISIS-Raw-File-Format.aspx', - t('doi_constants.distribution.format') - ); - }, - label: t('datapublications.details.format'), - icon: , - }, - { - content: (entity: Investigation) => entity.releaseDate?.slice(0, 10), - label: t('investigations.release_date'), - icon: , - }, - { - content: (entity: Investigation) => entity.startDate?.slice(0, 10), - label: t('investigations.start_date'), - icon: , - }, - { - content: (entity: Investigation) => entity.endDate?.slice(0, 10), - label: t('investigations.end_date'), - icon: , - }, - ]; + const shortInfo = Array.isArray(data) + ? [ + { + content: () => data?.[0]?.visitId, + label: t('investigations.visit_id'), + icon: , + }, + { + content: function doiFormat() { + return ( + data?.[0]?.doi && + externalSiteLink( + `https://doi.org/${data[0].doi}`, + data[0].doi, + 'isis-investigation-landing-doi-link' + ) + ); + }, + label: t('investigations.doi'), + icon: , + }, + { + content: function parentDoiFormat() { + const studyDataPublication = + data?.[0]?.dataCollectionInvestigations?.filter( + (dci) => + dci.dataCollection?.dataPublications?.[0]?.type?.name === + 'study' + )?.[0]?.dataCollection?.dataPublications?.[0]; + return ( + studyDataPublication && + externalSiteLink( + `https://doi.org/${studyDataPublication.pid}`, + studyDataPublication.pid, + 'isis-investigations-landing-parent-doi-link' + ) + ); + }, + label: t('investigations.parent_doi'), + icon: , + }, + { + content: () => data?.[0]?.name, + label: t('investigations.name'), + icon: , + }, + { + content: () => { + return formatBytes(data?.[0]?.fileSize); + }, + label: t('investigations.size'), + icon: , + }, + { + content: () => data?.[0]?.facility?.name, + label: t('investigations.details.facility'), + icon: , + }, + { + content: () => + data?.[0]?.investigationInstruments?.[0]?.instrument?.name, + label: t('investigations.instrument'), + icon: , + }, + { + content: function distributionFormat() { + return externalSiteLink( + 'https://www.isis.stfc.ac.uk/Pages/ISIS-Raw-File-Format.aspx', + t('doi_constants.distribution.format') + ); + }, + label: t('datapublications.details.format'), + icon: , + }, + { + content: () => data?.[0]?.releaseDate?.slice(0, 10), + label: t('investigations.release_date'), + icon: , + }, + { + content: () => data?.[0]?.startDate?.slice(0, 10), + label: t('investigations.start_date'), + icon: , + }, + { + content: () => data?.[0]?.endDate?.slice(0, 10), + label: t('investigations.end_date'), + icon: , + }, + ] + : [ + { + content: function doiFormat() { + return ( + data?.pid && + externalSiteLink( + `https://doi.org/${data.pid}`, + data.pid, + 'isis-investigation-landing-doi-link' + ) + ); + }, + label: t('investigations.doi'), + icon: , + }, + { + content: function doiFormat() { + return ( + studyDataPublication && + studyDataPublication?.pid && + externalSiteLink( + `https://doi.org/${studyDataPublication.pid}`, + studyDataPublication.pid, + 'isis-investigations-landing-parent-doi-link' + ) + ); + }, + label: t('investigations.parent_doi'), + icon: , + }, + { + content: () => studyDataPublication && studyDataPublication.title, + label: t('investigations.name'), + icon: , + }, + { + content: () => + data?.content?.dataCollectionInvestigations?.[0].investigation + ?.visitId ?? + (data?.pid.includes('-') && data.pid.split('-')[1]), + label: t('investigations.visit_id'), + icon: , + }, + { + content: () => data?.facility?.name, + label: t('investigations.details.facility'), + icon: , + }, + { + content: () => + data?.content?.dataCollectionInvestigations?.[0]?.investigation + ?.investigationInstruments?.[0]?.instrument?.name, + label: t('investigations.instrument'), + icon: , + }, + { + content: function distributionFormat() { + return externalSiteLink( + 'https://www.isis.stfc.ac.uk/Pages/ISIS-Raw-File-Format.aspx', + t('doi_constants.distribution.format') + ); + }, + label: t('datapublications.details.format'), + icon: , + }, + { + content: () => data?.publicationDate?.slice(0, 10), + label: t('investigations.release_date'), + icon: , + }, + ]; - const shortDatasetInfo = [ - { - content: function doiFormat(entity: Dataset) { - return ( - entity?.doi && - externalSiteLink( - `https://doi.org/${entity.doi}`, - entity.doi, - 'landing-study-doi-link' - ) - ); - }, - label: t('datasets.doi'), - icon: , - }, - ]; + const shortDatasetInfo = Array.isArray(data) + ? [ + { + content: function doiFormat(entity: Dataset) { + return ( + entity?.doi && + externalSiteLink( + `https://doi.org/${entity.doi}`, + entity.doi, + 'landing-study-doi-link' + ) + ); + }, + label: t('datasets.doi'), + icon: , + }, + ] + : []; return ( { label={t('investigations.details.label')} value="details" /> - - push( - view - ? `${urlPrefix}/dataset?view=${view}` - : `${urlPrefix}/dataset` - ) - } - /> + {typeof data !== 'undefined' && + (Array.isArray(data) || + data?.content?.dataCollectionInvestigations?.[0] + ?.investigation) && ( + + push( + view + ? `${location.pathname}/dataset?view=${view}` + : `${location.pathname}/dataset` + ) + } + /> + )} @@ -319,12 +461,16 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { {/* Long format information */} - {data?.[0]?.title} + {Array.isArray(data) ? data?.[0]?.title : data?.title} - {data?.[0]?.summary && data[0].summary !== 'null' - ? data[0].summary + {Array.isArray(data) + ? data?.[0]?.summary && data[0].summary !== 'null' + ? data[0].summary + : 'Description not provided' + : data?.description && data.description !== 'null' + ? data.description : 'Description not provided'} {formattedUsers.length > 0 && ( @@ -359,7 +505,11 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { doi={doi} formattedUsers={formattedUsers} title={title} - startDate={data?.[0]?.startDate} + startDate={ + Array.isArray(data) + ? data?.[0]?.startDate + : data?.publicationDate + } /> {formattedSamples && ( @@ -415,35 +565,68 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { {shortInfo.map( (field, i) => - data?.[0] && - field.content(data[0] as Investigation) && ( + field.content() && ( {field.icon} {field.label}: - - - {field.content(data[0] as Investigation)} - + + {field.content()} ) )} {/* Actions */} - - - + {(Array.isArray(data) || + data?.content?.dataCollectionInvestigations?.[0]?.investigation + ?.id) && ( + + + + + )} {/* Parts */} - {(data?.[0] as Investigation)?.datasets?.map((dataset, i) => ( + {(Array.isArray(data) + ? data?.[0] + : data?.content?.dataCollectionInvestigations?.[0]?.investigation + )?.datasets?.map((dataset, i) => ( { aria-label="landing-investigation-part-label" > {tableLink( - `${urlPrefix}/dataset/${dataset.id}`, + `${location.pathname}/dataset/${dataset.id}`, `${t('datasets.dataset')}: ${dataset.name}`, view )} @@ -501,4 +684,12 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { ); }; +const LandingPage = (props: LandingPageProps): React.ReactElement => { + if (props.dataPublication) { + return ; + } else { + return ; + } +}; + export default LandingPage; diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx index cf47cedfe..6306d488e 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx @@ -7,7 +7,7 @@ import configureStore from 'redux-mock-store'; import { QueryClient, QueryClientProvider } from 'react-query'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import { createMemoryHistory, type History } from 'history'; import { parse } from 'date-fns'; import { @@ -28,6 +28,7 @@ import { UserEvent } from '@testing-library/user-event/setup/setup'; import userEvent from '@testing-library/user-event'; import ISISDataPublicationsTable from './isisDataPublicationsTable.component'; import axios, { AxiosResponse } from 'axios'; +import { paths } from '../../../page/pageContainer.component'; jest .useFakeTimers('modern') @@ -40,13 +41,26 @@ describe('ISIS Data Publication table component', () => { let history: History; let user: UserEvent; - const renderComponent = (): RenderResult => { + const renderComponent = (studyDataPublicationId?: string): RenderResult => { + if (studyDataPublicationId) + history.replace( + generatePath( + paths.dataPublications.toggle.isisInvestigationDataPublication, + { + instrumentId: 1, + studyDataPublicationId, + } + ) + ); const store = mockStore(state); return render( - + @@ -81,7 +95,13 @@ describe('ISIS Data Publication table component', () => { }, }, ]; - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.dataPublications.toggle.isisStudyDataPublication, { + instrumentId: 1, + }), + ], + }); user = userEvent.setup({ delay: null, }); @@ -119,153 +139,222 @@ describe('ISIS Data Publication table component', () => { jest.clearAllMocks(); }); - it('renders correctly', async () => { - renderComponent(); - - const rows = await screen.findAllByRole('row'); - //should have 1 row in the table - expect(rows).toHaveLength(1); - - expect( - await findColumnHeaderByName('datapublications.title') - ).toBeInTheDocument(); - expect( - await findColumnHeaderByName('datapublications.pid') - ).toBeInTheDocument(); - expect( - await findColumnHeaderByName('datapublications.publication_date') - ).toBeInTheDocument(); - - const row = await findRowAt(0); - - // check that every cell contains the correct values - expect( - within( - findCellInRow(row, { - columnIndex: await findColumnIndexByName('datapublications.title'), - }) - ).getByText('Test 1') - ).toBeInTheDocument(); - expect( - within( - findCellInRow(row, { - columnIndex: await findColumnIndexByName('datapublications.pid'), - }) - ).getByText('doi') - ).toBeInTheDocument(); - }); + describe('Study Data Publication', () => { + it('renders correctly', async () => { + renderComponent(); + + const rows = await screen.findAllByRole('row'); + //should have 1 row in the table + expect(rows).toHaveLength(1); + + expect( + await findColumnHeaderByName('datapublications.title') + ).toBeInTheDocument(); + expect( + await findColumnHeaderByName('datapublications.pid') + ).toBeInTheDocument(); + + const row = await findRowAt(0); + + // check that every cell contains the correct values + expect( + within( + findCellInRow(row, { + columnIndex: await findColumnIndexByName('datapublications.title'), + }) + ).getByText('Test 1') + ).toBeInTheDocument(); + expect( + within( + findCellInRow(row, { + columnIndex: await findColumnIndexByName('datapublications.pid'), + }) + ).getByText('doi') + ).toBeInTheDocument(); + }); - it('updates filter query params on text filter', async () => { - renderComponent(); + it('updates filter query params on text filter', async () => { + renderComponent(); - const filterInput = await screen.findByRole('textbox', { - name: 'Filter by datapublications.title', - hidden: true, - }); + const filterInput = await screen.findByRole('textbox', { + name: 'Filter by datapublications.title', + hidden: true, + }); - await user.type(filterInput, 'test'); + await user.type(filterInput, 'test'); - // user.type inputs the given string character by character to simulate user typing - // each keystroke of user.type creates a new entry in the history stack - // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( - `?filters=${encodeURIComponent( - '{"title":{"value":"test","type":"include"}}' - )}` - ); + // user.type inputs the given string character by character to simulate user typing + // each keystroke of user.type creates a new entry in the history stack + // so the initial entry + 4 characters in "test" = 5 entries + expect(history.length).toBe(5); + expect(history.location.search).toBe( + `?filters=${encodeURIComponent( + '{"title":{"value":"test","type":"include"}}' + )}` + ); - await user.clear(filterInput); + await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); - }); + expect(history.length).toBe(6); + expect(history.location.search).toBe('?'); + }); - it('updates filter query params on date filter', async () => { - applyDatePickerWorkaround(); + it('uses default sort', () => { + renderComponent(); + expect(history.length).toBe(1); + expect(history.location.search).toBe( + `?sort=${encodeURIComponent('{"title":"desc"}')}` + ); + + // check that the data request is sent only once after mounting + const datafilesCalls = (axios.get as jest.Mock).mock.calls.filter( + (call) => call[0] === '/datapublications' + ); + expect(datafilesCalls).toHaveLength(1); + }); + + it('updates sort query params on sort', async () => { + renderComponent(); - renderComponent(); + await user.click( + await screen.findByRole('button', { name: 'datapublications.pid' }) + ); - const filterInput = await screen.findByRole('textbox', { - name: 'datapublications.publication_date filter to', + expect(history.length).toBe(2); + expect(history.location.search).toBe( + `?sort=${encodeURIComponent('{"pid":"asc"}')}` + ); }); - await user.type(filterInput, '2019-08-06'); + it('renders data publication name as a link', async () => { + renderComponent(); - expect(history.length).toBe(2); - expect(history.location.search).toBe( - `?filters=${encodeURIComponent( - '{"publicationDate":{"endDate":"2019-08-06"}}' - )}` - ); + const dataPublicationIdColIndex = await findColumnIndexByName( + 'datapublications.title' + ); + const row = await findRowAt(0); + const dataPublicationIdCell = findCellInRow(row, { + columnIndex: dataPublicationIdColIndex, + }); - // await user.clear(filterInput); - await user.click(filterInput); - await user.keyboard('{Control}a{/Control}'); - await user.keyboard('{Delete}'); + expect( + within(dataPublicationIdCell).getByRole('link', { name: 'Test 1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/1' + ); + }); + + it('displays Experiment DOI (PID) and renders the expected Link ', async () => { + renderComponent(); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + const pidColIndex = await findColumnIndexByName('datapublications.pid'); + const row = await findRowAt(0); + const pidCell = findCellInRow(row, { columnIndex: pidColIndex }); - cleanupDatePickerWorkaround(); + expect( + within(pidCell).getByRole('link', { name: 'doi' }) + ).toHaveAttribute('href', 'https://doi.org/doi'); + }); }); - it('uses default sort', () => { - renderComponent(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( - `?sort=${encodeURIComponent('{"publicationDate":"desc"}')}` - ); + describe('Investigation Data Publication', () => { + it('renders correctly', async () => { + renderComponent('2'); + + const rows = await screen.findAllByRole('row'); + //should have 1 row in the table + expect(rows).toHaveLength(1); + + expect( + await findColumnHeaderByName('datapublications.title') + ).toBeInTheDocument(); + expect( + await findColumnHeaderByName('datapublications.pid') + ).toBeInTheDocument(); + expect( + await findColumnHeaderByName('datapublications.publication_date') + ).toBeInTheDocument(); + + const row = await findRowAt(0); + + // check that every cell contains the correct values + expect( + within( + findCellInRow(row, { + columnIndex: await findColumnIndexByName('datapublications.title'), + }) + ).getByText('Test 1') + ).toBeInTheDocument(); + expect( + within( + findCellInRow(row, { + columnIndex: await findColumnIndexByName('datapublications.pid'), + }) + ).getByText('doi') + ).toBeInTheDocument(); + }); - // check that the data request is sent only once after mounting - const datafilesCalls = (axios.get as jest.Mock).mock.calls.filter( - (call) => call[0] === '/datapublications' - ); - expect(datafilesCalls).toHaveLength(1); - }); + it('updates filter query params on date filter', async () => { + applyDatePickerWorkaround(); - it('updates sort query params on sort', async () => { - renderComponent(); + renderComponent('2'); - await user.click( - await screen.findByRole('button', { name: 'datapublications.title' }) - ); + const filterInput = await screen.findByRole('textbox', { + name: 'datapublications.publication_date filter to', + }); - expect(history.length).toBe(2); - expect(history.location.search).toBe( - `?sort=${encodeURIComponent('{"title":"asc"}')}` - ); - }); + await user.type(filterInput, '2019-08-06'); - it('renders data publication name as a link', async () => { - renderComponent(); + expect(history.length).toBe(2); + expect(history.location.search).toBe( + `?filters=${encodeURIComponent( + '{"publicationDate":{"endDate":"2019-08-06"}}' + )}` + ); - const dataPublicationIdColIndex = await findColumnIndexByName( - 'datapublications.title' - ); - const row = await findRowAt(0); - const dataPublicationIdCell = findCellInRow(row, { - columnIndex: dataPublicationIdColIndex, + // await user.clear(filterInput); + await user.click(filterInput); + await user.keyboard('{Control}a{/Control}'); + await user.keyboard('{Delete}'); + + expect(history.length).toBe(3); + expect(history.location.search).toBe('?'); + + cleanupDatePickerWorkaround(); }); - expect( - within(dataPublicationIdCell).getByRole('link', { name: 'Test 1' }) - ).toHaveAttribute( - 'href', - '/browseDataPublications/instrument/1/dataPublication/1' - ); - }); + it('uses default sort', () => { + renderComponent('2'); + expect(history.length).toBe(1); + expect(history.location.search).toBe( + `?sort=${encodeURIComponent('{"publicationDate":"desc"}')}` + ); + + // check that the data request is sent only once after mounting + const datafilesCalls = (axios.get as jest.Mock).mock.calls.filter( + (call) => call[0] === '/datapublications' + ); + expect(datafilesCalls).toHaveLength(1); + }); - it('displays Experiment DOI (PID) and renders the expected Link ', async () => { - renderComponent(); + it('renders data publication name as a link', async () => { + renderComponent('2'); - const pidColIndex = await findColumnIndexByName('datapublications.pid'); - const row = await findRowAt(0); - const pidCell = findCellInRow(row, { columnIndex: pidColIndex }); + const dataPublicationIdColIndex = await findColumnIndexByName( + 'datapublications.title' + ); + const row = await findRowAt(0); + const dataPublicationIdCell = findCellInRow(row, { + columnIndex: dataPublicationIdColIndex, + }); - expect(within(pidCell).getByRole('link', { name: 'doi' })).toHaveAttribute( - 'href', - 'https://doi.org/doi' - ); + expect( + within(dataPublicationIdCell).getByRole('link', { name: 'Test 1' }) + ).toHaveAttribute( + 'href', + '/browseDataPublications/instrument/1/dataPublication/2/investigation/1' + ); + }); }); }); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx index 5a5104c4b..1f82260ea 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx @@ -20,12 +20,13 @@ import { useLocation } from 'react-router-dom'; interface ISISDataPublicationsTableProps { instrumentId: string; + studyDataPublicationId?: string; } const ISISDataPublicationsTable = ( props: ISISDataPublicationsTableProps ): React.ReactElement => { - const { instrumentId } = props; + const { instrumentId, studyDataPublicationId } = props; const location = useLocation(); const [t] = useTranslation(); @@ -45,6 +46,36 @@ const ISISDataPublicationsTable = ( }, }), }, + ...(studyDataPublicationId + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: studyDataPublicationId, + }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'investigation' }, + }), + }, + ] + : [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'study' }, + }), + }, + { + filterType: 'distinct', + filterValue: JSON.stringify(['id', 'title', 'pid']), + }, + ]), ]); // isMounted is used to disable queries when the component isn't fully mounted. @@ -66,6 +97,36 @@ const ISISDataPublicationsTable = ( }, }), }, + ...(studyDataPublicationId + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'content.dataCollectionInvestigations.investigation.dataCollectionInvestigations.dataCollection.dataPublications.id': + { + eq: studyDataPublicationId, + }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'investigation' }, + }), + }, + ] + : [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'study' }, + }), + }, + { + filterType: 'distinct', + filterValue: JSON.stringify(['id', 'title', 'pid']), + }, + ]), ], isMounted ); @@ -93,8 +154,6 @@ const ISISDataPublicationsTable = ( ); const columns: ColumnType[] = React.useMemo(() => { - const pathRoot = 'browseDataPublications'; - const instrumentChild = 'dataPublication'; return [ { icon: Fingerprint, @@ -102,12 +161,14 @@ const ISISDataPublicationsTable = ( dataKey: 'title', cellContentRenderer: (cellProps: TableCellProps) => tableLink( - `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${cellProps.rowData.id}`, + `${location.pathname}/${cellProps.rowData.id}`, cellProps.rowData.title, view, 'isis-datapublication-table-id' ), + filterComponent: textFilter, + defaultSort: studyDataPublicationId ? undefined : 'desc', }, { icon: Public, @@ -125,20 +186,31 @@ const ISISDataPublicationsTable = ( }, filterComponent: textFilter, }, - { - icon: CalendarToday, - label: t('datapublications.publication_date'), - dataKey: 'publicationDate', - cellContentRenderer: (cellProps: TableCellProps) => - (cellProps.rowData as DataPublication).publicationDate?.slice( - 0, - 10 - ) ?? '', - filterComponent: dateFilter, - defaultSort: 'desc', - }, + ...(studyDataPublicationId + ? ([ + { + icon: CalendarToday, + label: t('datapublications.publication_date'), + dataKey: 'publicationDate', + cellContentRenderer: (cellProps: TableCellProps) => + (cellProps.rowData as DataPublication).publicationDate?.slice( + 0, + 10 + ) ?? '', + filterComponent: dateFilter, + defaultSort: 'desc', + }, + ] as ColumnType[]) + : []), ]; - }, [t, textFilter, dateFilter, instrumentId, view]); + }, [ + t, + textFilter, + studyDataPublicationId, + dateFilter, + location.pathname, + view, + ]); return ( { const originalModule = jest.requireActual('datagateway-common'); @@ -44,7 +44,6 @@ jest.mock('datagateway-common', () => { useCart: jest.fn(), useAddToCart: jest.fn(), useRemoveFromCart: jest.fn(), - useDatasetSizes: jest.fn(), }; }); @@ -61,12 +60,7 @@ describe('ISIS Dataset table component', () => { - + @@ -83,7 +77,15 @@ describe('ISIS Dataset table component', () => { createTime: '2019-07-23', }, ]; - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.toggle.isisDataset, { + instrumentId: '1', + investigationId: '3', + facilityCycleId: '2', + }), + ], + }); user = userEvent.setup(); mockStore = configureStore([thunk]); @@ -117,7 +119,6 @@ describe('ISIS Dataset table component', () => { mutate: jest.fn(), isLoading: false, }); - (useDatasetSizes as jest.Mock).mockReturnValue({ data: 1 }); }); afterEach(() => { @@ -320,21 +321,14 @@ describe('ISIS Dataset table component', () => { }); it('renders dataset name as a link in data publication hierarchy', () => { - const store = mockStore(state); - render( - - - - - - - + history.replace( + generatePath(paths.dataPublications.toggle.isisDataset, { + instrumentId: '1', + investigationId: '3', + dataPublicationId: '2', + }) ); + renderComponent(); expect(screen.getByText('Test 1')).toMatchSnapshot(); }); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx index 33a409361..498dae595 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx @@ -29,21 +29,13 @@ import { useSelector } from 'react-redux'; import { StateType } from '../../../state/app.types'; interface ISISDatasetsTableProps { - instrumentId: string; - instrumentChildId: string; investigationId: string; - dataPublication: boolean; } const ISISDatasetsTable = ( props: ISISDatasetsTableProps ): React.ReactElement => { - const { investigationId, instrumentChildId, instrumentId, dataPublication } = - props; - - const pathRoot = dataPublication ? 'browseDataPublications' : 'browse'; - const instrumentChild = dataPublication ? 'dataPublication' : 'facilityCycle'; - const urlPrefix = `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${instrumentChildId}/investigation/${investigationId}/dataset`; + const { investigationId } = props; const [t] = useTranslation(); @@ -145,7 +137,7 @@ const ISISDatasetsTable = ( dataKey: 'name', cellContentRenderer: (cellProps: TableCellProps) => tableLink( - `${urlPrefix}/${cellProps.rowData.id}`, + `${location.pathname}/${cellProps.rowData.id}`, cellProps.rowData.name, view ), @@ -173,7 +165,7 @@ const ISISDatasetsTable = ( filterComponent: dateFilter, }, ], - [t, textFilter, dateFilter, urlPrefix, view] + [t, textFilter, dateFilter, view, location.pathname] ); const selectedRows = React.useMemo( @@ -195,10 +187,12 @@ const ISISDatasetsTable = ( push(`${urlPrefix}/${id}/datafile`)} + viewDatafiles={(id: number) => + push(`${location.pathname}/${id}/datafile`) + } /> ), - [push, urlPrefix] + [location.pathname, push] ); return ( diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx index 435b6106e..9c5d75a09 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx @@ -10,7 +10,7 @@ import { } from 'datagateway-common'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; -import { Router } from 'react-router-dom'; +import { generatePath, Router } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from 'react-query'; import { createMemoryHistory, type History } from 'history'; import { @@ -32,6 +32,7 @@ import { import type { UserEvent } from '@testing-library/user-event/setup/setup'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; +import { paths } from '../../../page/pageContainer.component'; describe('ISIS Investigations table component', () => { let mockStore; @@ -43,17 +44,13 @@ describe('ISIS Investigations table component', () => { let cartItems: DownloadCartItem[]; let holder: HTMLElement; - const renderComponent = (dataPublication = false): RenderResult => { + const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + @@ -88,12 +85,24 @@ describe('ISIS Investigations table component', () => { dataCollectionInvestigations: [ { id: 1, - investigation: { - id: 1, - title: 'Test 1', - name: 'Test 1', - visitId: '1', + dataCollection: { + id: 14, + dataPublications: [ + { + id: 15, + pid: 'Investigation Data Publication Pid', + description: 'Investigation Data Publication description', + title: 'Investigation Data Publication', + type: { + id: 16, + name: 'investigation', + }, + }, + ], }, + }, + { + id: 1, dataCollection: { id: 11, dataPublications: [ @@ -101,9 +110,11 @@ describe('ISIS Investigations table component', () => { id: 12, pid: 'Data Publication Pid', description: 'Data Publication description', - modTime: '2019-06-10', - createTime: '2019-06-11', title: 'Data Publication', + type: { + id: 13, + name: 'study', + }, }, ], }, @@ -113,7 +124,14 @@ describe('ISIS Investigations table component', () => { endDate: '2019-06-11', }, ]; - history = createMemoryHistory(); + history = createMemoryHistory({ + initialEntries: [ + generatePath(paths.toggle.isisInvestigation, { + instrumentId: '4', + facilityCycleId: '5', + }), + ], + }); replaceSpy = jest.spyOn(history, 'replace'); user = userEvent.setup(); @@ -477,17 +495,6 @@ describe('ISIS Investigations table component', () => { ).toHaveAttribute('href', 'https://doi.org/Data Publication Pid'); }); - it('renders title and DOI as links in Data Publication Hierarchy', async () => { - renderComponent(true); - expect(await screen.findByRole('link', { name: 'Test 1' })).toHaveAttribute( - 'href', - '/browseDataPublications/instrument/4/dataPublication/5/investigation/1' - ); - expect( - await screen.findByRole('link', { name: 'Data Publication Pid' }) - ).toHaveAttribute('href', 'https://doi.org/Data Publication Pid'); - }); - it('displays the correct user as the PI ', async () => { renderComponent(); @@ -518,7 +525,7 @@ describe('ISIS Investigations table component', () => { }); }); - it('gracefully handles missing Study from Study Investigation object and missing User from investigationUsers object', async () => { + it('gracefully handles missing data collection from data collection Investigations object and missing User from investigationUsers object', async () => { rowData = [ { ...rowData[0], diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx index 53688f5b5..327f46472 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx @@ -38,14 +38,13 @@ import { StateType } from '../../../state/app.types'; interface ISISInvestigationsTableProps { instrumentId: string; - instrumentChildId: string; - dataPublication: boolean; + facilityCycleId: string; } const ISISInvestigationsTable = ( props: ISISInvestigationsTableProps ): React.ReactElement => { - const { instrumentId, instrumentChildId, dataPublication } = props; + const { instrumentId, facilityCycleId } = props; const selectAllSetting = useSelector( (state: StateType) => state.dgdataview.selectAllSetting ); @@ -70,10 +69,8 @@ const ISISInvestigationsTable = ( { filterType: 'where', filterValue: JSON.stringify({ - [dataPublication - ? 'dataCollectionInvestigations.dataCollection.dataPublications.id' - : 'investigationFacilityCycles.facilityCycle.id']: { - eq: parseInt(instrumentChildId), + 'investigationFacilityCycles.facilityCycle.id': { + eq: parseInt(facilityCycleId), }, }), }, @@ -101,7 +98,7 @@ const ISISInvestigationsTable = ( }, { dataCollectionInvestigations: { - dataCollection: 'dataPublications', + dataCollection: { dataPublications: 'type' }, }, }, { @@ -161,19 +158,17 @@ const ISISInvestigationsTable = ( [fetchNextPage] ); - const pathRoot = dataPublication ? 'browseDataPublications' : 'browse'; - const instrumentChild = dataPublication ? 'dataPublication' : 'facilityCycle'; - const urlPrefix = `/${pathRoot}/instrument/${instrumentId}/${instrumentChild}/${instrumentChildId}/investigation`; - const detailsPanel = React.useCallback( ({ rowData, detailsPanelResize }) => ( push(`${urlPrefix}/${id}/dataset`)} + viewDatasets={(id: number) => + push(`${location.pathname}/${id}/dataset`) + } /> ), - [push, urlPrefix] + [push, location.pathname] ); const columns: ColumnType[] = React.useMemo( @@ -185,7 +180,7 @@ const ISISInvestigationsTable = ( cellContentRenderer: (cellProps: TableCellProps) => { const investigationData = cellProps.rowData as Investigation; return tableLink( - `${urlPrefix}/${investigationData.id}`, + `${location.pathname}/${investigationData.id}`, investigationData.title, view, 'isis-investigations-table-title' @@ -204,19 +199,18 @@ const ISISInvestigationsTable = ( label: t('investigations.doi'), dataKey: 'dataCollectionInvestigations.dataCollection.dataPublications.pid', - // TODO: this was previously the Study DOI - currently there are no datapublication - // representations of Studies, only of Investigations themselves - // should this be showing the study DOI or the investigation DOI anyway? cellContentRenderer: (cellProps: TableCellProps) => { const investigationData = cellProps.rowData as Investigation; - if ( - investigationData?.dataCollectionInvestigations?.[0]?.dataCollection - ?.dataPublications?.[0] - ) { + const studyDataPublication = + investigationData.dataCollectionInvestigations?.filter( + (dci) => + dci.dataCollection?.dataPublications?.[0]?.type?.name === + 'study' + )?.[0]?.dataCollection?.dataPublications?.[0]; + if (studyDataPublication) { return externalSiteLink( - `https://doi.org/${investigationData.dataCollectionInvestigations?.[0]?.dataCollection?.dataPublications?.[0].pid}`, - investigationData.dataCollectionInvestigations?.[0] - ?.dataCollection?.dataPublications?.[0].pid, + `https://doi.org/${studyDataPublication.pid}`, + studyDataPublication.pid, 'isis-investigations-table-doi-link' ); } else { @@ -267,7 +261,14 @@ const ISISInvestigationsTable = ( filterComponent: dateFilter, }, ], - [t, textFilter, principalExperimenterFilter, dateFilter, urlPrefix, view] + [ + t, + textFilter, + principalExperimenterFilter, + dateFilter, + location.pathname, + view, + ] ); return (