From 3c2954c05c5102af37a06155306a74e74c584cd5 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 17 Jun 2026 14:46:01 +0100 Subject: [PATCH 1/3] Add instrument landing pages #1779 - make branding component generic via a config option to specify the logo --- packages/datagateway-common/src/app.types.tsx | 3 + ...datagateway-dataview-settings.example.json | 8 + .../public/res/default.json | 12 +- .../src/page/pageContainer.component.tsx | 5 + packages/datagateway-dataview/src/settings.ts | 1 + .../src/state/actions/actions.types.tsx | 6 + .../src/state/actions/index.tsx | 15 ++ .../src/state/app.types.tsx | 1 + .../src/state/reducers/dgdataview.reducer.tsx | 13 + .../views/landing/StyledDOILink.component.tsx | 32 +++ .../src/views/landing/branding.component.tsx | 68 ++++++ .../landing/dls/dlsBranding.component.tsx | 69 ------ .../dlsDataPublicationLanding.component.tsx | 40 +-- ...ationRelatedIdentifiersPanel.component.tsx | 3 +- ...sDataPublicationVersionPanel.component.tsx | 3 +- .../landing/instrumentLanding.component.tsx | 227 ++++++++++++++++++ .../landing/isis/isisBranding.component.tsx | 77 ------ .../isisDataPublicationLanding.component.tsx | 8 +- .../isis/isisDatasetLanding.component.tsx | 4 +- .../isisInvestigationLanding.component.tsx | 8 +- 20 files changed, 411 insertions(+), 192 deletions(-) create mode 100644 packages/datagateway-dataview/src/views/landing/StyledDOILink.component.tsx create mode 100644 packages/datagateway-dataview/src/views/landing/branding.component.tsx delete mode 100644 packages/datagateway-dataview/src/views/landing/dls/dlsBranding.component.tsx create mode 100644 packages/datagateway-dataview/src/views/landing/instrumentLanding.component.tsx delete mode 100644 packages/datagateway-dataview/src/views/landing/isis/isisBranding.component.tsx diff --git a/packages/datagateway-common/src/app.types.tsx b/packages/datagateway-common/src/app.types.tsx index 8026e3fd6..a92fe46ee 100644 --- a/packages/datagateway-common/src/app.types.tsx +++ b/packages/datagateway-common/src/app.types.tsx @@ -112,6 +112,9 @@ export interface Instrument { url?: string; instrumentScientists?: InstrumentScientist[]; facility?: Facility; + pid?: string; + startDate?: string; + endDate?: string; } export interface InvestigationUser { diff --git a/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json b/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json index af91b0a29..7a48ce47e 100644 --- a/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json +++ b/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json @@ -1,6 +1,7 @@ { "facilityName": "Generic", "facilityImageURL": "", + "landingPageLogo": "STFC", "features": { "disableAnonDownload": false, "disableSelectAll": false @@ -133,6 +134,13 @@ "order": 0, "hideFromMenu": true }, + { + "section": "Browse", + "link": "/instrument", + "displayName": "Instrument Landing Pages", + "order": 0, + "hideFromMenu": true + }, { "section": "Browse", "link": "/browse/investigation", diff --git a/packages/datagateway-dataview/public/res/default.json b/packages/datagateway-dataview/public/res/default.json index a5e4ff113..4b048dc86 100644 --- a/packages/datagateway-dataview/public/res/default.json +++ b/packages/datagateway-dataview/public/res/default.json @@ -32,6 +32,13 @@ "description": "Description", "type": "Type", "url": "URL", + "pid": "DOI", + "start_date": "Start Date", + "end_date": "End Date", + "owner": "Owner", + "owner_value": "Science and Technology Facilities Council (https://ror.org/057g20z61)", + "manufacturer": "Manufacturer", + "manufacturer_value": "ISIS Neutron and Muon Source (https://ror.org/01t8fg661)", "details": { "tabs_label": "Instrument Details", "label": "Instrument Details", @@ -381,6 +388,7 @@ "warning_message_session_token": "Please remember that your selection will be lost after 2 hours of inactivity on this site" }, "doi_constants": { + "no_description": "Description not provided", "keywords": [ "STFC > ISIS", "STFC > ISIS > SYNCHROTRON", @@ -396,7 +404,9 @@ }, "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." + "body_data": "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.", + "body_instrument": "This is a page describing an instrument 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.", + "logo_alt_text": "ISIS Logo" }, "license": { "url": "https://creativecommons.org/licenses/by/4.0/", diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.tsx index e2e1a1252..f1e0f2f1a 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.tsx @@ -39,6 +39,7 @@ import { } from 'react-router-dom'; import { StateType } from '../state/app.types'; import DOITypeSelector from '../views/doiTypeSelector.component'; +import InstrumentLandingPage from '../views/landing/instrumentLanding.component'; import RoleSelector from '../views/roleSelector.component'; import PageBreadcrumbs from './breadcrumbs.component'; import PageRouting from './pageRouting.component'; @@ -89,6 +90,7 @@ export const paths = { homepage: '/datagateway', root: '/browse', doiRedirect: '/doi-redirect/:facilityName/:entityName/:entityId', + instrumentLandingPage: '/instrument/:instrumentId', genericRedirect: '/redirect/:facilityName/:entityName/:entityField/:fieldValue', myData: { @@ -813,6 +815,9 @@ const PageContainer: React.FC = () => { + + + {/* Load the standard dataview pageContainer */} diff --git a/packages/datagateway-dataview/src/settings.ts b/packages/datagateway-dataview/src/settings.ts index 6ea4e4d6d..aeb7adc49 100644 --- a/packages/datagateway-dataview/src/settings.ts +++ b/packages/datagateway-dataview/src/settings.ts @@ -4,6 +4,7 @@ import { BreadcrumbSettings } from './state/actions/actions.types'; export type DataviewSettings = DataviewSearchCommonSettings & DOISettings & { facilityImageURL?: string; + landingPageLogo?: 'STFC' | 'DLS' | string; breadcrumbs?: BreadcrumbSettings[]; PIRole?: string; localContactRole?: string; diff --git a/packages/datagateway-dataview/src/state/actions/actions.types.tsx b/packages/datagateway-dataview/src/state/actions/actions.types.tsx index d54a7a28e..e121509c4 100644 --- a/packages/datagateway-dataview/src/state/actions/actions.types.tsx +++ b/packages/datagateway-dataview/src/state/actions/actions.types.tsx @@ -5,6 +5,8 @@ export const ConfigurePluginHostSettingType = 'datagateway_dataview:configure_plugin_host'; export const ConfigureFacilityImageSettingType = 'datagateway_dataview:configure_facility_image'; +export const ConfigureLandingPageLogoSettingType = + 'datagateway_dataview:configure_landing_page_logo'; export const ConfigurePIRoleSettingType = 'datagateway_dataview:configure_pi_role'; export const ConfigureLocalContactRoleSettingType = @@ -23,6 +25,10 @@ export interface ConfigureFacilityImageSettingPayload { settings: string; } +export interface ConfigureLandingPageLogoSettingPayload { + settings: 'STFC' | 'DLS' | string; +} + export interface ConfigurePIRoleSettingPayload { settings: string; } diff --git a/packages/datagateway-dataview/src/state/actions/index.tsx b/packages/datagateway-dataview/src/state/actions/index.tsx index 9c3595e47..eafce43d6 100644 --- a/packages/datagateway-dataview/src/state/actions/index.tsx +++ b/packages/datagateway-dataview/src/state/actions/index.tsx @@ -14,6 +14,8 @@ import { ConfigureBreadcrumbSettingsType, ConfigureFacilityImageSettingPayload, ConfigureFacilityImageSettingType, + ConfigureLandingPageLogoSettingPayload, + ConfigureLandingPageLogoSettingType, ConfigureLocalContactRoleSettingPayload, ConfigureLocalContactRoleSettingType, ConfigurePIRoleSettingPayload, @@ -54,6 +56,15 @@ export const loadFacilityImageSetting = ( }, }); +export const loadLandingPageLogoSetting = ( + landingPageLogoSetting: string +): ActionType => ({ + type: ConfigureLandingPageLogoSettingType, + payload: { + settings: landingPageLogoSetting, + }, +}); + export const loadPIRoleSetting = ( PIRoleSetting: string | undefined ): ActionType => ({ @@ -117,6 +128,10 @@ export const configureApp = (): ThunkResult> => { dispatch(loadFacilityImageSetting(settingsResult['facilityImageURL'])); } + if (settingsResult?.['landingPageLogo'] !== undefined) { + dispatch(loadLandingPageLogoSetting(settingsResult['landingPageLogo'])); + } + dispatch(loadPIRoleSetting(settingsResult['PIRole'])); dispatch(loadLocalContactRoleSetting(settingsResult['localContactRole'])); diff --git a/packages/datagateway-dataview/src/state/app.types.tsx b/packages/datagateway-dataview/src/state/app.types.tsx index 1c214ccc5..8343deb1c 100644 --- a/packages/datagateway-dataview/src/state/app.types.tsx +++ b/packages/datagateway-dataview/src/state/app.types.tsx @@ -6,6 +6,7 @@ import type { BreadcrumbSettings } from './actions/actions.types'; export interface DGDataViewState { facilityImageURL: string; + landingPageLogo?: 'STFC' | 'DLS' | string; breadcrumbSettings: BreadcrumbSettings[]; settingsLoaded: boolean; pluginHost: string; diff --git a/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx b/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx index 4475d81e2..02673b3ca 100644 --- a/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx +++ b/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.tsx @@ -8,6 +8,8 @@ import { ConfigureBreadcrumbSettingsType, ConfigureFacilityImageSettingPayload, ConfigureFacilityImageSettingType, + ConfigureLandingPageLogoSettingPayload, + ConfigureLandingPageLogoSettingType, ConfigureLocalContactRoleSettingPayload, ConfigureLocalContactRoleSettingType, ConfigurePIRoleSettingPayload, @@ -67,6 +69,16 @@ export function handleConfigureFacilityImageSetting( }; } +export function handleConfigureLandingPageLogoSetting( + state: DGDataViewState, + payload: ConfigureLandingPageLogoSettingPayload +): DGDataViewState { + return { + ...state, + landingPageLogo: payload.settings, + }; +} + export function handleConfigurePIRoleSetting( state: DGDataViewState, payload: ConfigurePIRoleSettingPayload @@ -92,6 +104,7 @@ const DGDataViewReducer = createReducer(initialState, { [ConfigureBreadcrumbSettingsType]: handleConfigureBreadcrumbSettings, [ConfigurePluginHostSettingType]: handleConfigurePluginHostSetting, [ConfigureFacilityImageSettingType]: handleConfigureFacilityImageSetting, + [ConfigureLandingPageLogoSettingType]: handleConfigureLandingPageLogoSetting, [ConfigurePIRoleSettingType]: handleConfigurePIRoleSetting, [ConfigureLocalContactRoleSettingType]: handleConfigureLocalContactRoleSetting, diff --git a/packages/datagateway-dataview/src/views/landing/StyledDOILink.component.tsx b/packages/datagateway-dataview/src/views/landing/StyledDOILink.component.tsx new file mode 100644 index 000000000..2c384a109 --- /dev/null +++ b/packages/datagateway-dataview/src/views/landing/StyledDOILink.component.tsx @@ -0,0 +1,32 @@ +import { styled } from '@mui/material'; + +const StyledDOILink = styled('a')({ + display: 'inline-flex', + backgroundColor: '#000', + color: '#fff', + textDecoration: 'none', + paddingLeft: '5px', + borderRadius: '5px', + overflow: 'hidden', +}); + +const StyledDOISpan = styled('span')({ + backgroundColor: '#09c', + padding: '0 5px', + marginLeft: '5px', + '&:hover': { + backgroundColor: '#006a8d', + }, +}); + +const StyledDOI: React.FC<{ + doi: string; + doiHandleUrl: string; + testId?: string; +}> = ({ doi, doiHandleUrl, testId }) => ( + + DOI {doi} + +); + +export default StyledDOI; diff --git a/packages/datagateway-dataview/src/views/landing/branding.component.tsx b/packages/datagateway-dataview/src/views/landing/branding.component.tsx new file mode 100644 index 000000000..de359772b --- /dev/null +++ b/packages/datagateway-dataview/src/views/landing/branding.component.tsx @@ -0,0 +1,68 @@ +import { Grid, Link, Paper, styled, Typography } from '@mui/material'; +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { StateType } from '../../state/app.types'; +// TODO: when vite 6, explore no-inline w/ pluginHost vs inline as we have to inline in vite 5 +import STFCLogoWhite from 'datagateway-common/src/images/stfc-logo-white-text.png'; +// TODO: when vite 6, explore no-inline w/ pluginHost vs inline as we have to inline in vite 5 +import DLSLogo from 'datagateway-common/src/images/DLS-logo-white-text.png'; +import { useSelector } from 'react-redux'; + +const StyledPaper = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.primary.light, + margin: theme.spacing(-1.5), + padding: theme.spacing(1.5), + paddingBottom: theme.spacing(3), +})); + +const Branding = (props: { + landingPageType: 'data' | 'instrument'; +}): React.ReactElement => { + const [t] = useTranslation(); + const landingPageLogo = useSelector( + (state: StateType) => state.dgdataview.landingPageLogo + ); + + return ( + + + {landingPageLogo && ( + + {t('doi_constants.branding.logo_alt_text')} + + )} + +
+ + {t('doi_constants.branding.title')} + + + }} + /> + +
+
+
+
+ ); +}; + +export default Branding; diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsBranding.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsBranding.component.tsx deleted file mode 100644 index 1910b141d..000000000 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsBranding.component.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Grid, Paper, styled, Typography } from '@mui/material'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -// TODO: when vite 6, explore no-inline w/ pluginHost vs inline as we have to inline in vite 5 -import DLSLogo from 'datagateway-common/src/images/DLS-logo-white-text.png'; - -const StyledPaper = styled(Paper)(({ theme }) => ({ - backgroundColor: theme.palette.primary.light, - margin: theme.spacing(-1.5), - padding: theme.spacing(1.5), - paddingBottom: theme.spacing(3), -})); - -const StyledTypography = styled(Typography)(({ theme }) => ({ - // link color received from parent app theme object - // this requires casting the theme to any so that we can explicitly - // access properties we know to exist in the received object - color: theme.palette.primary.contrastText, - '& a': { - '&:link': { - color: '#FFCA98', - }, - '&:visited': { - color: '#FFCA98', - }, - '&:active': { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - color: (theme as any).colours?.red, - }, - }, -})); - -const Branding = (): React.ReactElement => { - const [t] = useTranslation(); - - return ( - - - - Diamond Logo - - -
- - {t('doi_constants.branding.title')} - - -
-
-
-
- ); -}; - -export default Branding; diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx index 3da66c177..82944f4c6 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import CitationFormatter from '../../citationFormatter.component'; -import Branding from './dlsBranding.component'; +import Branding from '../branding.component'; import DLSDataPublicationContentTable from './dlsDataPublicationContentTable.component'; import DLSDataPublicationRelatedIdentifiersPanel from './dlsDataPublicationRelatedIdentifiersPanel.component'; import DLSDataPublicationVersionPanel, { @@ -41,6 +41,7 @@ import DLSDataPublicationVersionPanel, { import ORCIDIdLogo from 'datagateway-common/src/images/ORCID-iD_icon_unauth_vector.svg'; import { useSelector } from 'react-redux'; import { StateType } from '../../../state/app.types'; +import StyledDOILink from '../StyledDOILink.component'; const Subheading = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), @@ -57,35 +58,8 @@ const ShortInfoValue = styled(Typography)({ textOverflow: 'ellipsis', }); -const StyledDOILink = styled('a')({ - display: 'inline-flex', - backgroundColor: '#000', - color: '#fff', - textDecoration: 'none', - paddingLeft: '5px', - borderRadius: '5px', - overflow: 'hidden', -}); - -const StyledDOISpan = styled('span')({ - backgroundColor: '#09c', - padding: '0 5px', - marginLeft: '5px', - '&:hover': { - backgroundColor: '#006a8d', - }, -}); - -export const StyledDOI: React.FC<{ doi: string; doiHandleUrl: string }> = ({ - doi, - doiHandleUrl, -}) => ( - - DOI {doi} - +const StyledDOI = (props: React.ComponentProps) => ( + ); export const ORCIDLink: React.FC<{ orcidId: string }> = ({ orcidId }) => ( @@ -216,8 +190,8 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { () => data?.description && data?.description !== 'null' ? data?.description - : 'Description not provided', - [data] + : t('doi_constants.no_description'), + [data?.description, t] ); const latestVersionPid = data?.relatedItems @@ -553,7 +527,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { ) : ( - + ) : relatedIdentifierType === DOIIdentifierType.URL ? ( {relatedIdentifier} diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationVersionPanel.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationVersionPanel.component.tsx index b94dd7816..426be6d25 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationVersionPanel.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationVersionPanel.component.tsx @@ -13,7 +13,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { StyledDOI } from './dlsDataPublicationLanding.component'; +import StyledDOI from '../StyledDOILink.component'; type DLSDataPublicationVersionPanelProps = { dataPublicationId: string; @@ -68,6 +68,7 @@ const DLSDataPublicationVersionPanel: React.FC< diff --git a/packages/datagateway-dataview/src/views/landing/instrumentLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/instrumentLanding.component.tsx new file mode 100644 index 000000000..c183c779c --- /dev/null +++ b/packages/datagateway-dataview/src/views/landing/instrumentLanding.component.tsx @@ -0,0 +1,227 @@ +import { + CircularProgress, + Divider, + Grid, + Link, + Paper, + Typography, + styled, +} from '@mui/material'; +import { Instrument, useInstrumentDetails } from 'datagateway-common'; +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { StateType } from '../../state/app.types'; +import StyledDOI from './StyledDOILink.component'; +import Branding from './branding.component'; + +const Subheading = styled(Typography)(({ theme }) => ({ + marginTop: theme.spacing(1), +})); + +const ShortInfoLabel = styled(Typography)({ + fontWeight: 'bold', + overflow: 'hidden', + textOverflow: 'ellipsis', +}); + +const ShortInfoValue = styled(Typography)({ + overflow: 'hidden', + textOverflow: 'ellipsis', +}); + +interface LandingPageProps { + instrumentId: string; +} + +const BaseInstrumentLandingPage = ( + props: LandingPageProps +): React.ReactElement => { + const [t] = useTranslation(); + const doiHandleUrl = useSelector( + (state: StateType) => state.dgcommon.urls.doiHandleUrl + ); + + const { instrumentId } = props; + + const { data, isLoading } = useInstrumentDetails(parseInt(instrumentId)); + + const pid = data?.pid; + const name = data?.fullName ?? data?.name; + const description = data?.description ?? t('doi_constants.no_description'); + + const formattedUsers = React.useMemo(() => { + const users: { fullName: string; contributorType: string }[] = []; + + data?.instrumentScientists?.forEach((instrumentScientist) => { + // Only keep users where we have their fullName + const fullname = instrumentScientist.user?.fullName; + if (fullname) { + users.push({ + fullName: fullname, + contributorType: 'Instrument Scientist', // we don't actually use this + }); + } + }); + + return users; + }, [data?.instrumentScientists]); + + const shortInfo = [ + { + content: function instrumentPidFormat(entity: Instrument) { + return ( + entity?.pid && ( + + ) + ); + }, + label: t('instruments.pid'), + }, + { + content: (instrument: Instrument) => instrument?.type, + label: t('instruments.type'), + }, + { + content: (_instrument: Instrument) => ( + }} + /> + ), + label: t('instruments.owner'), + }, + { + content: (_instrument: Instrument) => ( + }} + /> + ), + label: t('instruments.manufacturer'), + }, + { + content: (instrument: Instrument) => + instrument?.startDate?.slice(0, 10) ?? '', + label: t('instruments.start_date'), + }, + { + content: (instrument: Instrument) => + instrument?.endDate?.slice(0, 10) ?? '', + label: t('instruments.end_date'), + }, + ]; + + React.useEffect(() => { + const scriptId = `instrument-${instrumentId}`; + + if (!document.getElementById(scriptId) && pid) { + const newMetaTag = document.createElement('meta'); + newMetaTag.id = scriptId; + newMetaTag.name = 'DC.identifier'; + newMetaTag.content = `doi:${pid}`; + const head = document.getElementsByTagName('head')[0]; + head.appendChild(newMetaTag); + } + + return () => { + const currentMetaTag = document.getElementById(scriptId); + if (currentMetaTag) { + currentMetaTag.remove(); + } + }; + }, [instrumentId, pid]); + + return ( + + + + + + {isLoading ? ( + + + + ) : ( + + {/* Long format information */} + + + {name} + + + {description} + + + {formattedUsers.length > 0 && ( +
+ + {t('instruments.details.instrument_scientists.label')} + + {formattedUsers.map((user, i) => ( + + {user.fullName} + + ))} +
+ )} +
+ + + {/* Short format information */} + {shortInfo.some((field) => data && field.content(data)) && ( + + {shortInfo.map( + (field, i) => + data && + field.content(data) && ( + + + {field.label} + + + {field.content(data)} + + + ) + )} + + )} +
+ )} +
+
+ ); +}; + +const InstrumentLandingPage = () => { + const { instrumentId = '' } = useParams<{ instrumentId: string }>(); + + return ; +}; + +export default InstrumentLandingPage; diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisBranding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisBranding.component.tsx deleted file mode 100644 index 5fe9a722c..000000000 --- a/packages/datagateway-dataview/src/views/landing/isis/isisBranding.component.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Grid, Paper, styled, Typography } from '@mui/material'; -// TODO: when vite 6, explore no-inline w/ pluginHost vs inline as we have to inline in vite 5 -import STFCLogoWhite from 'datagateway-common/src/images/stfc-logo-white-text.png'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; -import { StateType } from '../../../state/app.types'; - -const StyledPaper = styled(Paper)(({ theme }) => ({ - backgroundColor: theme.palette.primary.light, - margin: theme.spacing(-1.5), - padding: theme.spacing(1.5), - paddingBottom: theme.spacing(3), -})); - -const StyledTypography = styled(Typography)(({ theme }) => ({ - // link color received from parent app theme object - // this requires casting the theme to any so that we can explicitly - // access properties we know to exist in the received object - color: theme.palette.primary.contrastText, - '& a': { - '&:link': { - color: '#FFCA98', - }, - '&:visited': { - color: '#FFCA98', - }, - '&:active': { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - color: (theme as any).colours?.red, - }, - }, -})); - -const Branding = (): React.ReactElement => { - const [t] = useTranslation(); - - return ( - - - - STFC Logo - - -
- - {t('doi_constants.branding.title')} - - -
-
-
-
- ); -}; - -const mapStateToProps = (state: StateType): { pluginHost: string } => { - return { - pluginHost: state.dgdataview.pluginHost, - }; -}; - -export default connect(mapStateToProps)(Branding); 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 8ef47bf9e..4ebab1fb6 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx @@ -30,7 +30,7 @@ import { useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; import { StateType } from '../../../state/app.types'; import CitationFormatter from '../../citationFormatter.component'; -import Branding from './isisBranding.component'; +import Branding from '../branding.component'; const Subheading = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), @@ -226,8 +226,8 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { investigationDataPublications?.[0]?.description && investigationDataPublications?.[0]?.description !== 'null' ? investigationDataPublications?.[0]?.description - : 'Description not provided', - [investigationDataPublications] + : t('doi_constants.no_description'), + [investigationDataPublications, t] ); const formattedUsers = React.useMemo(() => { @@ -371,7 +371,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { > - + 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 d9e3672ca..40e585e9a 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx @@ -27,7 +27,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; -import Branding from './isisBranding.component'; +import Branding from '../branding.component'; const Subheading = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), @@ -130,7 +130,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { - + 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 f8bcd1411..c65d1f26d 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx @@ -38,7 +38,7 @@ import { useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; import { StateType } from '../../../state/app.types'; import CitationFormatter from '../../citationFormatter.component'; -import Branding from './isisBranding.component'; +import Branding from '../branding.component'; const Subheading = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), @@ -401,7 +401,7 @@ const CommonLandingPage = ( > - + @@ -448,10 +448,10 @@ const CommonLandingPage = ( {isInvestigation ? data.summary && data.summary !== 'null' ? data.summary - : 'Description not provided' + : t('doi_constants.no_description') : data?.description && data.description !== 'null' ? data.description - : 'Description not provided'} + : t('doi_constants.no_description')} {formattedUsers.length > 0 && (
From 33e691f1a4b9f6edfef0d40d5ee419899e225fe0 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 17 Jun 2026 14:47:17 +0100 Subject: [PATCH 2/3] #1779 add unit tests for instrument landing page and adjust other landing page tests to account for branding changes --- .../datagateway-dataview/src/main.test.tsx | 1 + .../src/page/pageRouting.component.test.tsx | 2 +- .../reducers/dgdataview.reducer.test.tsx | 12 ++ ...sDataPublicationLanding.component.test.tsx | 15 +- .../instrumentLanding.component.test.tsx | 175 ++++++++++++++++++ ...sDataPublicationLanding.component.test.tsx | 7 +- .../isisDatasetLanding.component.test.tsx | 5 +- ...sisInvestigationLanding.component.test.tsx | 23 ++- 8 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 packages/datagateway-dataview/src/views/landing/instrumentLanding.component.test.tsx diff --git a/packages/datagateway-dataview/src/main.test.tsx b/packages/datagateway-dataview/src/main.test.tsx index 5494928cd..16df976ab 100644 --- a/packages/datagateway-dataview/src/main.test.tsx +++ b/packages/datagateway-dataview/src/main.test.tsx @@ -22,6 +22,7 @@ describe('index - fetchSettings', () => { const settingsResult = { facilityName: 'Generic', facilityImageURL: 'test-image.jpg', + landingPageLogo: 'DLS', features: {}, idsUrl: 'ids', apiUrl: 'api', diff --git a/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx b/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx index ed076930c..8b839fccd 100644 --- a/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx +++ b/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx @@ -562,7 +562,7 @@ describe('PageTable', () => { ); expect( - await screen.findByLabelText('branding-title') + await screen.findByText('doi_constants.branding.title') ).toBeInTheDocument(); }); 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 f64abd87c..8f5437cd7 100644 --- a/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.test.tsx +++ b/packages/datagateway-dataview/src/state/reducers/dgdataview.reducer.test.tsx @@ -1,6 +1,7 @@ import { loadBreadcrumbSettings, loadFacilityImageSetting, + loadLandingPageLogoSetting, loadLocalContactRoleSetting, loadPIRoleSetting, loadPluginHostSetting, @@ -75,6 +76,17 @@ describe('dgdataview reducer', () => { expect(updatedState.facilityImageURL).toEqual('test-image.jpg'); }); + it('should set landingPageLogo when configuring action is sent', () => { + expect(state.landingPageLogo).toEqual(undefined); + + const updatedState = DGDataViewReducer( + state, + loadLandingPageLogoSetting('DLS') + ); + + expect(updatedState.landingPageLogo).toEqual('DLS'); + }); + it('should set loadPIRoleSetting when configuring action is sent', () => { expect(state.PIRole).toEqual('PI'); diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx index a8aebe840..f874c638f 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx @@ -281,6 +281,7 @@ describe('DLS Data Publication Landing page', () => { dgcommon: { ...dGCommonInitialState, accessMethods: {} }, }) ); + state.dgdataview.landingPageLogo = 'DLS'; history = createMemoryHistory({ initialEntries: [ @@ -344,6 +345,16 @@ describe('DLS Data Publication Landing page', () => { it('renders correctly', async () => { renderComponent(); + // renders branding correctly + expect( + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) + ).toHaveAttribute( + 'src', + expect.stringMatching(/(.*)DLS-logo-white-text\.png/) + ); + // displays doi + link correctly expect( await screen.findByText('datapublications.concept datapublications.pid') @@ -427,7 +438,9 @@ describe('DLS Data Publication Landing page', () => { await screen.findByRole('link', { name: 'DOI doi 1' }) ).toHaveAttribute('href', 'https://doi.org/doi 1'); - expect(screen.getByText('Description not provided')).toBeInTheDocument(); + expect( + screen.getByText('doi_constants.no_description') + ).toBeInTheDocument(); expect(screen.queryByText('datapublications.details.users')).toBeNull(); expect( diff --git a/packages/datagateway-dataview/src/views/landing/instrumentLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/instrumentLanding.component.test.tsx new file mode 100644 index 000000000..e11610978 --- /dev/null +++ b/packages/datagateway-dataview/src/views/landing/instrumentLanding.component.test.tsx @@ -0,0 +1,175 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, screen, type RenderResult } from '@testing-library/react'; +import axios, { AxiosResponse } from 'axios'; +import { Instrument, dGCommonInitialState } from 'datagateway-common'; +import { Provider } from 'react-redux'; +import { BrowserRouter, Route, generatePath } from 'react-router-dom'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { paths } from '../../page/pageContainer.component'; +import { StateType } from '../../state/app.types'; +import { initialState as dgDataViewInitialState } from '../../state/reducers/dgdataview.reducer'; +import InstrumentLandingPage from './instrumentLanding.component'; + +describe('Instrument landing page', () => { + const mockStore = configureStore([thunk]); + let state: StateType; + + const renderComponent = (): RenderResult => + render( + + + + + + + + + + ); + + let initialData: Instrument; + + beforeEach(() => { + state = JSON.parse( + JSON.stringify({ + dgdataview: dgDataViewInitialState, + dgcommon: { ...dGCommonInitialState, accessMethods: {} }, + }) + ); + state.dgdataview.landingPageLogo = '/test/image.png'; + + window.history.replaceState( + {}, + '', + generatePath(paths.instrumentLandingPage, { + instrumentId: '1', + }) + ); + + initialData = { + id: 1, + name: 'b1', + fullName: 'Beamline 1', + instrumentScientists: [ + { id: 1, user: { id: 1, name: 'a', fullName: 'Alice' } }, + { id: 2, user: { id: 2, name: 'b', fullName: 'Bob' } }, + { id: 2, user: { id: 2, name: 'c' } }, + ], + pid: 'pid1', + description: 'Beamline description', + type: 'Type1', + startDate: '2026-06-16', + endDate: '2026-06-17', + }; + + axios.get = vi + .fn() + .mockImplementation((url: string): Promise> => { + if (/\/instruments$/.test(url)) { + return Promise.resolve({ + data: [initialData], + }); + } else { + return Promise.reject(`Endpoint not mocked: ${url}`); + } + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders correctly', async () => { + renderComponent(); + + // renders branding correctly + expect( + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) + ).toHaveAttribute('src', '/test/image.png'); + + // displays doi + link correctly + expect(await screen.findByText('instruments.pid')).toBeInTheDocument(); + expect( + await screen.findByRole('link', { name: 'DOI pid1' }) + ).toHaveAttribute('href', 'https://doi.org/pid1'); + + // full name and description rendered + expect(screen.getByText('Beamline 1')).toBeInTheDocument(); + expect(screen.getByText('Beamline description')).toBeInTheDocument(); + + // start and end date rendered + expect(screen.getByText('2026-06-16')).toBeInTheDocument(); + expect(screen.getByText('2026-06-17')).toBeInTheDocument(); + + expect(screen.getByText('instruments.owner_value')).toBeInTheDocument(); + expect( + screen.getByText('instruments.manufacturer_value') + ).toBeInTheDocument(); + + expect( + await screen.findByTestId('landing-instrument-user-0') + ).toHaveTextContent('Alice'); + expect( + await screen.findByTestId('landing-instrument-user-1') + ).toHaveTextContent('Bob'); + expect( + screen.queryByTestId('landing-instrument-user-2') + ).not.toBeInTheDocument(); + }); + + it('renders correctly if info is missing', async () => { + initialData.description = undefined; + initialData.instrumentScientists = undefined; + initialData.startDate = undefined; + initialData.endDate = undefined; + initialData.fullName = undefined; + initialData.type = undefined; + renderComponent(); + + // displays doi + link correctly + expect( + await screen.findByRole('link', { name: 'DOI pid1' }) + ).toHaveAttribute('href', 'https://doi.org/pid1'); + + // expect name to be rendered as fallback if fullName not provided + expect(screen.getByText('b1')).toBeInTheDocument(); + + expect( + screen.getByText('doi_constants.no_description') + ).toBeInTheDocument(); + + expect( + screen.queryByText('instruments.details.instrument_scientists.label') + ).toBeNull(); + expect(screen.queryByText('instruments.type')).toBeNull(); + expect(screen.queryByText('instruments.start_date')).toBeNull(); + expect(screen.queryByText('instruments.end_date')).toBeNull(); + }); + + it('renders correctly whilst loading', async () => { + renderComponent(); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect( + screen.queryByText('instruments.manufacturer') + ).not.toBeInTheDocument(); + }); + + it('renders meta tag correctly', async () => { + renderComponent(); + + expect(await screen.findByText('Beamline 1')).toBeInTheDocument(); + + // eslint-disable-next-line testing-library/no-node-access + expect(document.getElementById('instrument-1')).toMatchInlineSnapshot(` + + `); + }); +}); 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 f82281d5e..4d7f55583 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 @@ -128,6 +128,7 @@ describe('ISIS Data Publication Landing page', () => { }) ); state.dgdataview.pluginHost = '/test/'; + state.dgdataview.landingPageLogo = 'STFC'; history = createMemoryHistory({ initialEntries: [ @@ -232,7 +233,9 @@ describe('ISIS Data Publication Landing page', () => { // renders branding correctly expect( - await screen.findByRole('img', { name: 'STFC Logo' }) + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) ).toHaveAttribute( 'src', expect.stringMatching(/(.*)stfc-logo-white-text\.png/) @@ -332,7 +335,7 @@ describe('ISIS Data Publication Landing page', () => { renderComponent(); expect( - await screen.findByText('Description not provided') + await screen.findByText('doi_constants.no_description') ).toBeInTheDocument(); expect(screen.queryByText('investigations.details.users.label')).toBeNull(); 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 3411c4848..1daa90164 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 @@ -69,6 +69,7 @@ describe('ISIS Dataset Landing page', () => { }) ); state.dgdataview.pluginHost = '/test/'; + state.dgdataview.landingPageLogo = 'STFC'; history = createMemoryHistory({ initialEntries: [ generatePath(paths.landing.isisDatasetLanding, { @@ -172,7 +173,9 @@ describe('ISIS Dataset Landing page', () => { // renders branding correctly expect( - await screen.findByRole('img', { name: 'STFC Logo' }) + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) ).toHaveAttribute( 'src', expect.stringMatching(/(.*)stfc-logo-white-text\.png/) 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 cc3c02984..af2b1ad8d 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 @@ -157,6 +157,7 @@ describe('ISIS Investigation Landing page', () => { }) ); state.dgdataview.pluginHost = '/test/'; + state.dgdataview.landingPageLogo = 'STFC'; history = createMemoryHistory({ initialEntries: [ generatePath(paths.landing.isisInvestigationLanding, { @@ -276,10 +277,14 @@ describe('ISIS Investigation Landing page', () => { // branding should be visible expect( - await screen.findByRole('img', { name: 'STFC Logo' }) + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) ).toBeInTheDocument(); expect( - await screen.findByRole('img', { name: 'STFC Logo' }) + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) ).toHaveAttribute( 'src', expect.stringMatching(/\/(.*)stfc-logo-white-text\.png/) @@ -403,7 +408,9 @@ describe('ISIS Investigation Landing page', () => { renderComponent(); expect(await screen.findByText('Test title 1')).toBeInTheDocument(); - expect(screen.getByText('Description not provided')).toBeInTheDocument(); + expect( + screen.getByText('doi_constants.no_description') + ).toBeInTheDocument(); // no investigation samples, so show no samples message expect( @@ -431,10 +438,14 @@ describe('ISIS Investigation Landing page', () => { // branding should be visible expect( - await screen.findByRole('img', { name: 'STFC Logo' }) + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) ).toBeInTheDocument(); expect( - await screen.findByRole('img', { name: 'STFC Logo' }) + await screen.findByRole('img', { + name: 'doi_constants.branding.logo_alt_text', + }) ).toHaveAttribute( 'src', expect.stringMatching(/(.*)stfc-logo-white-text\.png/) @@ -555,7 +566,7 @@ describe('ISIS Investigation Landing page', () => { renderComponent(true); expect( - await screen.findByText('Description not provided') + await screen.findByText('doi_constants.no_description') ).toBeInTheDocument(); expect( From 306a9cf76d91d41d037261069387d50df4afa354 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 19 Jun 2026 13:57:50 +0100 Subject: [PATCH 3/3] #1779 - add e2e test for instrument landing page --- .../cypress/e2e/landing/instrument.cy.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/datagateway-dataview/cypress/e2e/landing/instrument.cy.ts diff --git a/packages/datagateway-dataview/cypress/e2e/landing/instrument.cy.ts b/packages/datagateway-dataview/cypress/e2e/landing/instrument.cy.ts new file mode 100644 index 000000000..2bb8d9bf6 --- /dev/null +++ b/packages/datagateway-dataview/cypress/e2e/landing/instrument.cy.ts @@ -0,0 +1,24 @@ +describe('Instrument Landing', () => { + beforeEach(() => { + cy.login(); + cy.visit('/instrument/1'); + }); + + it('should load correctly', () => { + cy.title().should('equal', 'DataGateway DataView'); + cy.get('#datagateway-dataview').should('be.visible'); + + // title + cy.contains('Stop prove field onto think suffer measure.').should( + 'be.visible' + ); + // description + cy.contains('Suggest shake effort many last prepare small.').should( + 'be.visible' + ); + // instrument scientist + cy.contains('Kathryn Fox').should('be.visible'); + + // TODO: no doi, start date or end date fields yet, and type is just a number so hard to test via text + }); +});