From 13bc1a15f97fc2c931d27f240080e3c8a1bfd134 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Tue, 14 Jul 2026 12:04:06 +0100 Subject: [PATCH 1/8] store role tags in user context --- .../src/datasources/stfc/StfcUserDataSource.ts | 13 ++++++++++++- apps/backend/src/models/Role.ts | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts index 1cbb3cc100..3402bd5b27 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -1,5 +1,6 @@ import { logger } from '@user-office-software/duo-logger'; +import { createUOWSClient } from './UOWSClient'; import { BasicPersonDetailsDTO } from '../../../generated/models/BasicPersonDetailsDTO'; import { PermissionUserGroupDTO } from '../../../generated/models/PermissionUserGroupDTO'; import { RoleDTO } from '../../../generated/models/RoleDTO'; @@ -12,11 +13,12 @@ import { UpdateUserByIdArgs } from '../../resolvers/mutations/UpdateUserMutation import { UsersArgs } from '../../resolvers/queries/UsersQuery'; import { Cache } from '../../utils/Cache'; import { PaginationSortDirection } from '../../utils/pagination'; +import RoleDataSource from '../postgres/RoleDataSource'; import PostgresUserDataSource from '../postgres/UserDataSource'; import { UserDataSource } from '../UserDataSource'; -import { createUOWSClient } from './UOWSClient'; const postgresUserDataSource = new PostgresUserDataSource(); +const roleDataSource = new RoleDataSource(); const UOWSClient = createUOWSClient(); @@ -454,6 +456,7 @@ export class StfcUserDataSource implements UserDataSource { return stfcRawRolesRequest!; } + async getUserRoles(id: number): Promise { const cachedRoles = this.uopRolesCache.get(String(id)); if (cachedRoles) { @@ -477,6 +480,8 @@ export class StfcUserDataSource implements UserDataSource { return []; } + userRole.tags = await roleDataSource.getTagsByRoleId(userRole.id); + if (!stfcRoles || stfcRoles.length == 0) { return [userRole]; } @@ -490,6 +495,12 @@ export class StfcUserDataSource implements UserDataSource { const uniqueRoles: Role[] = [...new Set(combinedUserRoles)]; + for (let i = 0; i < uniqueRoles.length; i++) { + uniqueRoles[i].tags = await roleDataSource.getTagsByRoleId( + uniqueRoles[i].id + ); + } + uniqueRoles.sort((a, b) => a.id - b.id); const userRoles = [userRole, ...uniqueRoles]; diff --git a/apps/backend/src/models/Role.ts b/apps/backend/src/models/Role.ts index 651a693800..409d1589f0 100644 --- a/apps/backend/src/models/Role.ts +++ b/apps/backend/src/models/Role.ts @@ -1,3 +1,5 @@ +import { Tag } from './Tag'; + export enum Roles { USER = 'user', USER_OFFICER = 'user_officer', @@ -23,6 +25,7 @@ type RoleBase = { title: string; description: string; isRootRole: boolean; + tags?: Tag[]; }; export type Role = From ffb2d514a639275b57397cd11e34e7aafa3f0114 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Tue, 14 Jul 2026 12:44:21 +0100 Subject: [PATCH 2/8] dynamic translation for technique proposal export --- apps/backend/i18next.ts | 17 +++++++ apps/backend/package-lock.json | 46 ++++++++++++++++++- apps/backend/package.json | 3 ++ apps/backend/src/auth/UserAuthorization.ts | 11 +++++ apps/backend/src/middlewares/factory/xlsx.ts | 24 ++++++---- apps/frontend/src/components/AppRoutes.tsx | 3 +- .../call/AssignInstrumentsToCall.tsx | 3 +- .../call/AssignedInstrumentsTable.tsx | 3 +- .../src/components/call/CallsTable.tsx | 3 +- .../FapMeetingInstrumentsTable.tsx | 3 +- .../AssignProposalsToInstruments.tsx | 3 +- .../instrument/AssignedScientistsTable.tsx | 3 +- .../instrument/CreateUpdateInstrument.tsx | 3 +- .../components/instrument/InstrumentTable.tsx | 3 +- .../src/components/menu/MenuItems.tsx | 3 +- .../proposal/ChangeProposalStatus.tsx | 3 +- .../proposal/ProposalTableOfficer.tsx | 3 +- .../AssignInstrumentsToTechniques.tsx | 3 +- .../technique/AssignedScientistsTable.tsx | 3 +- .../technique/CreateUpdateTechnique.tsx | 3 +- .../components/technique/TechniqueTable.tsx | 3 +- .../TechniqueProposalTable.tsx | 14 ++++-- .../template/CreateUpdateEmailTemplate.tsx | 3 +- package-lock.json | 15 ++++++ package.json | 1 + 25 files changed, 133 insertions(+), 49 deletions(-) create mode 100644 apps/backend/i18next.ts diff --git a/apps/backend/i18next.ts b/apps/backend/i18next.ts new file mode 100644 index 0000000000..e20d373bb6 --- /dev/null +++ b/apps/backend/i18next.ts @@ -0,0 +1,17 @@ +import path from 'node:path'; + +import i18next from 'i18next'; +import Backend from 'i18next-fs-backend'; + +i18next.use(Backend).init({ + initAsync: false, + lng: 'override', + fallbackLng: 'override', + backend: { + loadPath: path.join(__dirname, '/locales/{{lng}}/translation.json'), + }, +}); + +i18next.languages = ['override']; + +export default i18next; diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index a78ed249a1..946d2de7eb 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -46,6 +46,8 @@ "graphql": "^16.13.2", "graphql-request": "^7.4.0", "graphql-tag": "^2.12.6", + "i18next": "^26.3.4", + "i18next-fs-backend": "^2.6.6", "jsonpath": "^1.2.1", "jsonwebtoken": "^9.0.1", "knex": "^3.1.0", @@ -78,6 +80,7 @@ "@types/email-templates": "^8.0.4", "@types/express": "^4.17.13", "@types/express-jwt": "^6.0.4", + "@types/i18n": "^0.13.12", "@types/jest": "^29.5.6", "@types/jsonpath": "^0.2.0", "@types/jsonwebtoken": "^9.0.2", @@ -7178,6 +7181,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/i18n": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/@types/i18n/-/i18n-0.13.12.tgz", + "integrity": "sha512-iAd2QjKh+0ToBXocmCS3m38GskiaGzmSV1MTQz2GaOraqSqBiLf46J7u3EGINl+st+Uk4lO3OL7QyIjTJlrWIg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.5", "dev": true, @@ -11485,6 +11495,40 @@ "version": "2.1.3", "license": "MIT" }, + "node_modules/i18next": { + "version": "26.3.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz", + "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.6.tgz", + "integrity": "sha512-mYGu6Nt8RIp3X/U8Y+Gej1wo5xmYWmGKLqBGMCC2OCAou5rW5epeHgHmVcw20mJs9Z9+DAPHIxQPNCgFyPRMeg==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -16810,7 +16854,7 @@ }, "node_modules/typescript": { "version": "5.4.5", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/apps/backend/package.json b/apps/backend/package.json index 8f131bfc3f..ee5f138e68 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -69,6 +69,8 @@ "graphql": "^16.13.2", "graphql-request": "^7.4.0", "graphql-tag": "^2.12.6", + "i18next": "^26.3.4", + "i18next-fs-backend": "^2.6.6", "jsonpath": "^1.2.1", "jsonwebtoken": "^9.0.1", "knex": "^3.1.0", @@ -101,6 +103,7 @@ "@types/email-templates": "^8.0.4", "@types/express": "^4.17.13", "@types/express-jwt": "^6.0.4", + "@types/i18n": "^0.13.12", "@types/jest": "^29.5.6", "@types/jsonpath": "^0.2.0", "@types/jsonwebtoken": "^9.0.2", diff --git a/apps/backend/src/auth/UserAuthorization.ts b/apps/backend/src/auth/UserAuthorization.ts index 4a8f55e703..3983a4f28e 100644 --- a/apps/backend/src/auth/UserAuthorization.ts +++ b/apps/backend/src/auth/UserAuthorization.ts @@ -7,6 +7,7 @@ import { Tokens } from '../config/Tokens'; import { FapDataSource } from '../datasources/FapDataSource'; import { InternalReviewDataSource } from '../datasources/InternalReviewDataSource'; import { ProposalDataSource } from '../datasources/ProposalDataSource'; +import { RoleDataSource } from '../datasources/RoleDataSource'; import { UserDataSource } from '../datasources/UserDataSource'; import { VisitDataSource } from '../datasources/VisitDataSource'; import { Institution } from '../models/Institution'; @@ -36,6 +37,10 @@ export abstract class UserAuthorization { Tokens.AdminDataSource ); + protected roleDataSource: RoleDataSource = container.resolve( + Tokens.RoleDataSource + ); + protected getUniqueId(user: ValidUserInfo) { return user.sub; } @@ -197,6 +202,12 @@ export abstract class UserAuthorization { return readableUsers.includes(id); } + async getCurrentRoleTags(agent: UserWithRole | null) { + return agent?.currentRole?.id == null + ? [] + : this.roleDataSource.getTagsByRoleId(agent.currentRole.id); + } + abstract externalTokenLogin( token: string, redirectUri: string, diff --git a/apps/backend/src/middlewares/factory/xlsx.ts b/apps/backend/src/middlewares/factory/xlsx.ts index 64c9861f89..09dd0e80d3 100644 --- a/apps/backend/src/middlewares/factory/xlsx.ts +++ b/apps/backend/src/middlewares/factory/xlsx.ts @@ -1,6 +1,7 @@ import express from 'express'; import { container } from 'tsyringe'; +import i18next from '../../../i18next'; //needed? import { UserAuthorization } from '../../auth/UserAuthorization'; import { Tokens } from '../../config/Tokens'; import callFactoryService, { @@ -165,13 +166,26 @@ router.get(`/${XLSXType.CALL_FAP}/:call_id`, async (req, res, next) => { }); router.get(`/${XLSXType.TECHNIQUE}/:proposal_pks`, async (req, res, next) => { + const userAuthorization = container.resolve( + Tokens.UserAuthorization + ); + + const userWithRole = { + ...res.locals.agent, + }; + + const roleTags = await userAuthorization.getCurrentRoleTags(userWithRole); + const translationForFirstRoleTag = !roleTags.length + ? '' + : roleTags[0].name + '.'; + const techniqueProposalDataColumns = [ 'Proposal ID', 'Title', 'Principal Investigator', 'PI Email', 'Date Submitted', - 'Technique', + i18next.t(`${translationForFirstRoleTag}Technique`), 'Instrument', 'Status', ]; @@ -181,19 +195,11 @@ router.get(`/${XLSXType.TECHNIQUE}/:proposal_pks`, async (req, res, next) => { throw new Error('Not authorized'); } - const userWithRole = { - ...res.locals.agent, - }; - const proposalPks: number[] = req.params.proposal_pks .split(',') .map((n: string) => parseInt(n)) .filter((id: number) => !isNaN(id)); - const userAuthorization = container.resolve( - Tokens.UserAuthorization - ); - if ( !userAuthorization.isUserOfficer(userWithRole) && !userAuthorization.isInstrumentScientist(userWithRole) diff --git a/apps/frontend/src/components/AppRoutes.tsx b/apps/frontend/src/components/AppRoutes.tsx index d0a7bc5cfa..0b099da93c 100644 --- a/apps/frontend/src/components/AppRoutes.tsx +++ b/apps/frontend/src/components/AppRoutes.tsx @@ -1,9 +1,8 @@ +import i18n from 'i18n'; import React, { lazy, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; -import i18n from 'i18n'; - import { FeatureContext } from 'context/FeatureContextProvider'; import { UserContext } from 'context/UserContextProvider'; import { FeatureId, UserRole, WorkflowType } from 'generated/sdk'; diff --git a/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx b/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx index 2df2d4d2ec..3650158cfd 100644 --- a/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx +++ b/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx @@ -1,11 +1,10 @@ import { Autocomplete, TextField, Typography } from '@mui/material'; import Button from '@mui/material/Button'; +import i18n from 'i18n'; import PropTypes from 'prop-types'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import { ActionButtonContainer } from 'components/common/ActionButtonContainer'; import MaterialTable from 'components/common/DenseMaterialTable'; import { UserContext } from 'context/UserContextProvider'; diff --git a/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx b/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx index fb844baf7b..0340ef207f 100644 --- a/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx +++ b/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx @@ -1,12 +1,11 @@ import { Column, EditComponentProps } from '@material-table/core'; import { Autocomplete, Box } from '@mui/material'; import TextField from '@mui/material/TextField'; +import i18n from 'i18n'; import PropTypes from 'prop-types'; import React, { ChangeEvent, useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import MaterialTable from 'components/common/DenseMaterialTable'; import { UserContext } from 'context/UserContextProvider'; import { diff --git a/apps/frontend/src/components/call/CallsTable.tsx b/apps/frontend/src/components/call/CallsTable.tsx index 7d81c65dfb..6cb80ed0b8 100644 --- a/apps/frontend/src/components/call/CallsTable.tsx +++ b/apps/frontend/src/components/call/CallsTable.tsx @@ -10,12 +10,11 @@ import FormGroup from '@mui/material/FormGroup'; import Grid from '@mui/material/Grid'; import Switch from '@mui/material/Switch'; import Typography from '@mui/material/Typography'; +import i18n from 'i18n'; import React, { useCallback, ReactElement, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; -import i18n from 'i18n'; - import ScienceIcon from 'components/common/icons/ScienceIcon'; import StyledDialog from 'components/common/StyledDialog'; import SuperMaterialTable from 'components/common/SuperMaterialTable'; diff --git a/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx b/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx index 4983cf4410..62297e2a47 100644 --- a/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx +++ b/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx @@ -11,12 +11,11 @@ import { DialogTitle, Typography, } from '@mui/material'; +import i18n from 'i18n'; import { useSnackbar } from 'notistack'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import MaterialTable from 'components/common/DenseMaterialTable'; import FapInstrumentProposalsTable from 'components/fap/MeetingComponents/FapInstrumentProposalsTable'; import { Call, InstrumentWithAvailabilityTime, UserRole } from 'generated/sdk'; diff --git a/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx b/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx index 48163b38ff..4aeb616cd8 100644 --- a/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx +++ b/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx @@ -4,11 +4,10 @@ import Container from '@mui/material/Container'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import { Form, Formik } from 'formik'; +import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import { InstrumentMinimalFragment } from 'generated/sdk'; import { useInstrumentsMinimalData } from 'hooks/instrument/useInstrumentsMinimalData'; diff --git a/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx b/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx index c2a3724708..a7e24c4ce6 100644 --- a/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx +++ b/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx @@ -1,10 +1,9 @@ import MaterialTable from '@material-table/core'; import Box from '@mui/material/Box'; +import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import { Instrument, BasicUserDetails, UserRole } from 'generated/sdk'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; import { tableIcons } from 'utils/materialIcons'; diff --git a/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx b/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx index fbe7d428f6..14f15cc1a9 100644 --- a/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx +++ b/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx @@ -10,11 +10,10 @@ import { updateInstrumentValidationSchema, } from '@user-office-software/duo-validation/lib/Instrument'; import { Field, Form, Formik, FormikProps } from 'formik'; +import i18n from 'i18n'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import TextField from 'components/common/FormikUITextField'; import UOLoader from 'components/common/UOLoader'; diff --git a/apps/frontend/src/components/instrument/InstrumentTable.tsx b/apps/frontend/src/components/instrument/InstrumentTable.tsx index c0cc6b2f1f..412fe23e24 100644 --- a/apps/frontend/src/components/instrument/InstrumentTable.tsx +++ b/apps/frontend/src/components/instrument/InstrumentTable.tsx @@ -1,11 +1,10 @@ import { Column } from '@material-table/core'; import AssignmentInd from '@mui/icons-material/AssignmentInd'; import { Typography } from '@mui/material'; +import i18n from 'i18n'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import SuperMaterialTable from 'components/common/SuperMaterialTable'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; import { useInstrumentsData } from 'hooks/instrument/useInstrumentsData'; diff --git a/apps/frontend/src/components/menu/MenuItems.tsx b/apps/frontend/src/components/menu/MenuItems.tsx index cb23a1af1a..6350de98e9 100644 --- a/apps/frontend/src/components/menu/MenuItems.tsx +++ b/apps/frontend/src/components/menu/MenuItems.tsx @@ -11,12 +11,11 @@ import People from '@mui/icons-material/People'; import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; +import i18n from 'i18n'; import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; -import i18n from 'i18n'; - import TagIcon from 'components/common/icons/TagIcon'; import Tooltip from 'components/common/MenuTooltip'; import { FeatureContext } from 'context/FeatureContextProvider'; diff --git a/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx b/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx index bf96c388ec..c9c495c69a 100644 --- a/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx +++ b/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx @@ -10,12 +10,11 @@ import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; import Typography from '@mui/material/Typography'; import { Form, Formik } from 'formik'; +import i18n from 'i18n'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; -import i18n from 'i18n'; - import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import WorkflowView from 'components/settings/workflow/WorkflowView'; import { GetWorkflowQuery, WorkflowStatus, WorkflowType } from 'generated/sdk'; diff --git a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx index 4b9eea5135..5cc92374c5 100644 --- a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx +++ b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx @@ -25,14 +25,13 @@ import { ResourceId, getTranslation, } from '@user-office-software/duo-localisation'; +import i18n from 'i18n'; import { TFunction } from 'i18next'; import React, { useContext, useEffect, useRef, useState } from 'react'; import isEqual from 'react-fast-compare'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; -import i18n from 'i18n'; - import CopyToClipboard from 'components/common/CopyToClipboard'; import MaterialTable from 'components/common/DenseMaterialTable'; import ListStatusIcon from 'components/common/icons/ListStatusIcon'; diff --git a/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx b/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx index 3e6eceb476..90f9a7b79f 100644 --- a/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx +++ b/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx @@ -4,11 +4,10 @@ import Container from '@mui/material/Container'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import { Form, Formik } from 'formik'; +import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import { InstrumentFragment } from 'generated/sdk'; import { useInstruments } from 'hooks/instrument/useInstruments'; diff --git a/apps/frontend/src/components/technique/AssignedScientistsTable.tsx b/apps/frontend/src/components/technique/AssignedScientistsTable.tsx index 94c40dd473..d33db52bc7 100644 --- a/apps/frontend/src/components/technique/AssignedScientistsTable.tsx +++ b/apps/frontend/src/components/technique/AssignedScientistsTable.tsx @@ -1,10 +1,9 @@ import MaterialTable from '@material-table/core'; import { Box } from '@mui/material'; +import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import { BasicUserDetails, TechniqueFragment, UserRole } from 'generated/sdk'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; import { tableIcons } from 'utils/materialIcons'; diff --git a/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx b/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx index ab37674827..2690aee9fc 100755 --- a/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx +++ b/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx @@ -5,11 +5,10 @@ import { updateTechniqueValidationSchema, } from '@user-office-software/duo-validation/lib/Technique'; import { Field, Form, Formik } from 'formik'; +import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import TextField from 'components/common/FormikUITextField'; import UOLoader from 'components/common/UOLoader'; import { TechniqueFragment } from 'generated/sdk'; diff --git a/apps/frontend/src/components/technique/TechniqueTable.tsx b/apps/frontend/src/components/technique/TechniqueTable.tsx index 0c02f7e287..cff6fa7377 100755 --- a/apps/frontend/src/components/technique/TechniqueTable.tsx +++ b/apps/frontend/src/components/technique/TechniqueTable.tsx @@ -1,11 +1,10 @@ import { Column } from '@material-table/core'; import { AssignmentInd } from '@mui/icons-material'; import { Dialog, DialogContent, Typography } from '@mui/material'; +import i18n from 'i18n'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import ScienceIcon from 'components/common/icons/ScienceIcon'; import SimpleTabs from 'components/common/SimpleTabs'; import SuperMaterialTable from 'components/common/SuperMaterialTable'; diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index 00ad4f2abc..e0d2699b96 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -18,6 +18,7 @@ import { getTranslation, ResourceId, } from '@user-office-software/duo-localisation'; +import i18n from 'i18n'; import { t, TFunction } from 'i18next'; import React, { useCallback, @@ -29,8 +30,6 @@ import React, { } from 'react'; import { useSearchParams } from 'react-router-dom'; -import i18n from 'i18n'; - import { parseInstrumentQuery } from 'components/common/proposalFilters/InstrumentFilter'; import UOLoader from 'components/common/UOLoader'; import ProposalReviewContent, { @@ -74,7 +73,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { tableRef.current?.onQueryChange({}); }, []); const [searchParams, setSearchParams] = useSearchParams({}); - const { currentRole } = useContext(UserContext); + const { currentRole, roles, currentRoleId } = useContext(UserContext); const [tableData, setTableData] = useState([]); const { @@ -190,7 +189,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { const excludedStatusIds = proposalStatuses .filter((status) => !techPropStatusCodes.includes(status.id as StatusCode)) .map((status) => status.id); - console.log({ proposalStatusId }); + const [proposalFilter, setProposalFilter] = useState({ callId, instrumentFilter: { @@ -214,6 +213,11 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { excludeProposalStatusIds: excludedStatusIds, }); + const currentRoleTags = roles.find((r) => currentRoleId === r.id)!.tags; + const currentRoleFirstTagName = currentRoleTags?.length + ? currentRoleTags[0].name + : ''; + const lastProcessedCallId = useRef(null); useEffect(() => { if (callId !== lastProcessedCallId.current) { @@ -694,7 +698,7 @@ const TechniqueProposalTable = ({ confirm }: { confirm: WithConfirmType }) => { const techniquesColumns = () => [ { - title: 'Technique', + title: i18n.t(`${currentRoleFirstTagName}.Technique`), field: 'technique.name', sorting: false, render: (rowData: ProposalViewData) => diff --git a/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx b/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx index 67a0df6f75..81b5a2f0fa 100644 --- a/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx +++ b/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx @@ -5,11 +5,10 @@ import { updateEmailTemplateValidationSchema, } from '@user-office-software/duo-validation'; import { Field, Form, Formik } from 'formik'; +import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import i18n from 'i18n'; - import CheckboxWithLabel from 'components/common/FormikUICheckboxWithLabel'; import TextField from 'components/common/FormikUITextField'; import UOLoader from 'components/common/UOLoader'; diff --git a/package-lock.json b/package-lock.json index 79aa12258a..cb339dfc71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "concurrently": "^10.0.0", + "i18next-http-backend": "^4.0.0", "wait-on": "^9.0.1" }, "devDependencies": { @@ -485,6 +486,15 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next-http-backend": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.0.tgz", + "integrity": "sha512-EgSjO3Q1G6f2Q5oy7u9mmxuesE0oSfzAD97NFBjC8EmkK4guBSYLljM0Fng3DarMWIIkU70jfo4+mUzmyVISTA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/joi": { "version": "18.2.1", "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", @@ -1260,6 +1270,11 @@ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true }, + "i18next-http-backend": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.0.tgz", + "integrity": "sha512-EgSjO3Q1G6f2Q5oy7u9mmxuesE0oSfzAD97NFBjC8EmkK4guBSYLljM0Fng3DarMWIIkU70jfo4+mUzmyVISTA==" + }, "joi": { "version": "18.2.1", "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", diff --git a/package.json b/package.json index 5dfd6ca90d..af76b2e086 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ ], "dependencies": { "concurrently": "^10.0.0", + "i18next-http-backend": "^4.0.0", "wait-on": "^9.0.1" }, "devDependencies": { From 22d3fb35df3ee3dd860a62f5a20ccfe2bc4ece8a Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Tue, 14 Jul 2026 14:53:03 +0100 Subject: [PATCH 3/8] adds technique proposal filter translation --- .../common/proposalFilters/TechniqueFilter.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/common/proposalFilters/TechniqueFilter.tsx b/apps/frontend/src/components/common/proposalFilters/TechniqueFilter.tsx index 94cfecfa9d..74071207bb 100644 --- a/apps/frontend/src/components/common/proposalFilters/TechniqueFilter.tsx +++ b/apps/frontend/src/components/common/proposalFilters/TechniqueFilter.tsx @@ -5,10 +5,11 @@ import InputLabel from '@mui/material/InputLabel'; import ListSubheader from '@mui/material/ListSubheader'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; -import React, { Dispatch } from 'react'; +import React, { useContext, Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; +import { UserContext } from 'context/UserContextProvider'; import { TechniqueMinimalFragment, TechniqueFilterInput } from 'generated/sdk'; export enum TechniqueFilterEnum { @@ -35,6 +36,7 @@ const TechniqueFilter = ({ shouldShowMultiple, showMultiTechniqueProposals, }: TechniqueFilterProps) => { + const { roles, currentRoleId } = useContext(UserContext); const [, setSearchParams] = useSearchParams(); const { t } = useTranslation(); @@ -42,11 +44,16 @@ const TechniqueFilter = ({ return null; } + const currentRoleTags = roles.find((r) => currentRoleId === r.id)!.tags; + const currentRoleFirstTagName = currentRoleTags?.length + ? currentRoleTags[0].name + : ''; + return ( <> - {t('Technique')} + {t(`${currentRoleFirstTagName}.Technique`)} {isLoading ? ( Loading... From 64afebebb819cb4a3970c3ad1fc6953acde628fa Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Wed, 15 Jul 2026 13:54:14 +0100 Subject: [PATCH 4/8] uses env var for backend translation file path --- apps/backend/i18next.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/backend/i18next.ts b/apps/backend/i18next.ts index e20d373bb6..b15c28aba4 100644 --- a/apps/backend/i18next.ts +++ b/apps/backend/i18next.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import path from 'path'; import i18next from 'i18next'; import Backend from 'i18next-fs-backend'; @@ -8,7 +8,10 @@ i18next.use(Backend).init({ lng: 'override', fallbackLng: 'override', backend: { - loadPath: path.join(__dirname, '/locales/{{lng}}/translation.json'), + loadPath: path.resolve( + process.env.TRANSLATION_PATH || '', + '{{lng}}/translation.json' + ), }, }); From b2e93e39c19e3136039929fb2493fa84ffc1aa26 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Thu, 16 Jul 2026 10:12:54 +0100 Subject: [PATCH 5/8] adds new env var to test config --- .github/workflows/test-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 4c80664336..78a3c1f712 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -496,6 +496,7 @@ jobs: FBS_EMAIL: FacilitiesBusinessSystem@stfc.ac.uk PROFILE_PAGE_LINK: http://localhost:9003/auth/ManageDetails.aspx EXTERNAL_AUTH_HOMEPAGE_URL: http://localhost:9003/auth/Menu.aspx + TRANSLATION_PATH: /config/locales/ run: | REPO_DIR_NAME=$(basename $GITHUB_WORKSPACE) From 0a23565dd82c9182333732751704c316670a614e Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Thu, 16 Jul 2026 10:49:28 +0100 Subject: [PATCH 6/8] removes comment --- apps/backend/src/middlewares/factory/xlsx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/middlewares/factory/xlsx.ts b/apps/backend/src/middlewares/factory/xlsx.ts index 09dd0e80d3..da38baf3c5 100644 --- a/apps/backend/src/middlewares/factory/xlsx.ts +++ b/apps/backend/src/middlewares/factory/xlsx.ts @@ -1,7 +1,7 @@ import express from 'express'; import { container } from 'tsyringe'; -import i18next from '../../../i18next'; //needed? +import i18next from '../../../i18next'; import { UserAuthorization } from '../../auth/UserAuthorization'; import { Tokens } from '../../config/Tokens'; import callFactoryService, { From 1265b7a3f0bf04bad7eb8f4938d6c87f4c716e51 Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Fri, 17 Jul 2026 10:14:04 +0100 Subject: [PATCH 7/8] fixes backend unit tests --- .../stfc/StfcUserDataSource.spec.ts | 30 ++++++++++++------- apps/frontend/src/components/AppRoutes.tsx | 3 +- .../call/AssignInstrumentsToCall.tsx | 3 +- .../call/AssignedInstrumentsTable.tsx | 3 +- .../src/components/call/CallsTable.tsx | 3 +- .../FapMeetingInstrumentsTable.tsx | 3 +- .../AssignProposalsToInstruments.tsx | 3 +- .../instrument/AssignedScientistsTable.tsx | 3 +- .../instrument/CreateUpdateInstrument.tsx | 3 +- .../components/instrument/InstrumentTable.tsx | 3 +- .../src/components/menu/MenuItems.tsx | 3 +- .../proposal/ChangeProposalStatus.tsx | 3 +- .../proposal/ProposalTableOfficer.tsx | 3 +- .../AssignInstrumentsToTechniques.tsx | 3 +- .../technique/AssignedScientistsTable.tsx | 3 +- .../technique/CreateUpdateTechnique.tsx | 3 +- .../components/technique/TechniqueTable.tsx | 3 +- .../TechniqueProposalTable.tsx | 3 +- .../template/CreateUpdateEmailTemplate.tsx | 3 +- 19 files changed, 56 insertions(+), 28 deletions(-) diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts index 9bbb77a4ca..484ffe02b9 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts @@ -187,6 +187,7 @@ describe('Role tests', () => { title: 'User', description: '', isRootRole: true, + tags: [], }) ); }); @@ -198,16 +199,25 @@ describe('Role tests', () => { userdataSource.getUserRoles(dummyUserNumber) ).resolves.toEqual( expect.arrayContaining([ - createRole(1, Roles.USER, 'User', '', { note: '' }, true), - createRole(2, Roles.USER_OFFICER, 'User Officer', '', {}, true), - createRole( - 3, - Roles.INSTRUMENT_SCIENTIST, - 'Instrument Scientist', - '', - {}, - true - ), + { + ...createRole(1, Roles.USER, 'User', '', { note: '' }, true), + tags: [], + }, + { + ...createRole(2, Roles.USER_OFFICER, 'User Officer', '', {}, true), + tags: [], + }, + { + ...createRole( + 3, + Roles.INSTRUMENT_SCIENTIST, + 'Instrument Scientist', + '', + {}, + true + ), + tags: [], + }, ]) ); }); diff --git a/apps/frontend/src/components/AppRoutes.tsx b/apps/frontend/src/components/AppRoutes.tsx index 0b099da93c..d0a7bc5cfa 100644 --- a/apps/frontend/src/components/AppRoutes.tsx +++ b/apps/frontend/src/components/AppRoutes.tsx @@ -1,8 +1,9 @@ -import i18n from 'i18n'; import React, { lazy, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; +import i18n from 'i18n'; + import { FeatureContext } from 'context/FeatureContextProvider'; import { UserContext } from 'context/UserContextProvider'; import { FeatureId, UserRole, WorkflowType } from 'generated/sdk'; diff --git a/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx b/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx index 3650158cfd..2df2d4d2ec 100644 --- a/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx +++ b/apps/frontend/src/components/call/AssignInstrumentsToCall.tsx @@ -1,10 +1,11 @@ import { Autocomplete, TextField, Typography } from '@mui/material'; import Button from '@mui/material/Button'; -import i18n from 'i18n'; import PropTypes from 'prop-types'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import { ActionButtonContainer } from 'components/common/ActionButtonContainer'; import MaterialTable from 'components/common/DenseMaterialTable'; import { UserContext } from 'context/UserContextProvider'; diff --git a/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx b/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx index 0340ef207f..fb844baf7b 100644 --- a/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx +++ b/apps/frontend/src/components/call/AssignedInstrumentsTable.tsx @@ -1,11 +1,12 @@ import { Column, EditComponentProps } from '@material-table/core'; import { Autocomplete, Box } from '@mui/material'; import TextField from '@mui/material/TextField'; -import i18n from 'i18n'; import PropTypes from 'prop-types'; import React, { ChangeEvent, useContext } from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import MaterialTable from 'components/common/DenseMaterialTable'; import { UserContext } from 'context/UserContextProvider'; import { diff --git a/apps/frontend/src/components/call/CallsTable.tsx b/apps/frontend/src/components/call/CallsTable.tsx index 6cb80ed0b8..7d81c65dfb 100644 --- a/apps/frontend/src/components/call/CallsTable.tsx +++ b/apps/frontend/src/components/call/CallsTable.tsx @@ -10,11 +10,12 @@ import FormGroup from '@mui/material/FormGroup'; import Grid from '@mui/material/Grid'; import Switch from '@mui/material/Switch'; import Typography from '@mui/material/Typography'; -import i18n from 'i18n'; import React, { useCallback, ReactElement, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; +import i18n from 'i18n'; + import ScienceIcon from 'components/common/icons/ScienceIcon'; import StyledDialog from 'components/common/StyledDialog'; import SuperMaterialTable from 'components/common/SuperMaterialTable'; diff --git a/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx b/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx index 62297e2a47..4983cf4410 100644 --- a/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx +++ b/apps/frontend/src/components/fap/MeetingComponents/FapMeetingInstrumentsTable.tsx @@ -11,11 +11,12 @@ import { DialogTitle, Typography, } from '@mui/material'; -import i18n from 'i18n'; import { useSnackbar } from 'notistack'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import MaterialTable from 'components/common/DenseMaterialTable'; import FapInstrumentProposalsTable from 'components/fap/MeetingComponents/FapInstrumentProposalsTable'; import { Call, InstrumentWithAvailabilityTime, UserRole } from 'generated/sdk'; diff --git a/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx b/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx index 4aeb616cd8..48163b38ff 100644 --- a/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx +++ b/apps/frontend/src/components/instrument/AssignProposalsToInstruments.tsx @@ -4,10 +4,11 @@ import Container from '@mui/material/Container'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import { Form, Formik } from 'formik'; -import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import { InstrumentMinimalFragment } from 'generated/sdk'; import { useInstrumentsMinimalData } from 'hooks/instrument/useInstrumentsMinimalData'; diff --git a/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx b/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx index a7e24c4ce6..c2a3724708 100644 --- a/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx +++ b/apps/frontend/src/components/instrument/AssignedScientistsTable.tsx @@ -1,9 +1,10 @@ import MaterialTable from '@material-table/core'; import Box from '@mui/material/Box'; -import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import { Instrument, BasicUserDetails, UserRole } from 'generated/sdk'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; import { tableIcons } from 'utils/materialIcons'; diff --git a/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx b/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx index 14f15cc1a9..fbe7d428f6 100644 --- a/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx +++ b/apps/frontend/src/components/instrument/CreateUpdateInstrument.tsx @@ -10,10 +10,11 @@ import { updateInstrumentValidationSchema, } from '@user-office-software/duo-validation/lib/Instrument'; import { Field, Form, Formik, FormikProps } from 'formik'; -import i18n from 'i18n'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import TextField from 'components/common/FormikUITextField'; import UOLoader from 'components/common/UOLoader'; diff --git a/apps/frontend/src/components/instrument/InstrumentTable.tsx b/apps/frontend/src/components/instrument/InstrumentTable.tsx index 412fe23e24..c0cc6b2f1f 100644 --- a/apps/frontend/src/components/instrument/InstrumentTable.tsx +++ b/apps/frontend/src/components/instrument/InstrumentTable.tsx @@ -1,10 +1,11 @@ import { Column } from '@material-table/core'; import AssignmentInd from '@mui/icons-material/AssignmentInd'; import { Typography } from '@mui/material'; -import i18n from 'i18n'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import SuperMaterialTable from 'components/common/SuperMaterialTable'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; import { useInstrumentsData } from 'hooks/instrument/useInstrumentsData'; diff --git a/apps/frontend/src/components/menu/MenuItems.tsx b/apps/frontend/src/components/menu/MenuItems.tsx index 6350de98e9..cb23a1af1a 100644 --- a/apps/frontend/src/components/menu/MenuItems.tsx +++ b/apps/frontend/src/components/menu/MenuItems.tsx @@ -11,11 +11,12 @@ import People from '@mui/icons-material/People'; import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; -import i18n from 'i18n'; import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; +import i18n from 'i18n'; + import TagIcon from 'components/common/icons/TagIcon'; import Tooltip from 'components/common/MenuTooltip'; import { FeatureContext } from 'context/FeatureContextProvider'; diff --git a/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx b/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx index c9c495c69a..bf96c388ec 100644 --- a/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx +++ b/apps/frontend/src/components/proposal/ChangeProposalStatus.tsx @@ -10,11 +10,12 @@ import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; import Typography from '@mui/material/Typography'; import { Form, Formik } from 'formik'; -import i18n from 'i18n'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; +import i18n from 'i18n'; + import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import WorkflowView from 'components/settings/workflow/WorkflowView'; import { GetWorkflowQuery, WorkflowStatus, WorkflowType } from 'generated/sdk'; diff --git a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx index 5cc92374c5..4b9eea5135 100644 --- a/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx +++ b/apps/frontend/src/components/proposal/ProposalTableOfficer.tsx @@ -25,13 +25,14 @@ import { ResourceId, getTranslation, } from '@user-office-software/duo-localisation'; -import i18n from 'i18n'; import { TFunction } from 'i18next'; import React, { useContext, useEffect, useRef, useState } from 'react'; import isEqual from 'react-fast-compare'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; +import i18n from 'i18n'; + import CopyToClipboard from 'components/common/CopyToClipboard'; import MaterialTable from 'components/common/DenseMaterialTable'; import ListStatusIcon from 'components/common/icons/ListStatusIcon'; diff --git a/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx b/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx index 90f9a7b79f..3e6eceb476 100644 --- a/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx +++ b/apps/frontend/src/components/technique/AssignInstrumentsToTechniques.tsx @@ -4,10 +4,11 @@ import Container from '@mui/material/Container'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import { Form, Formik } from 'formik'; -import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import FormikUIAutocomplete from 'components/common/FormikUIAutocomplete'; import { InstrumentFragment } from 'generated/sdk'; import { useInstruments } from 'hooks/instrument/useInstruments'; diff --git a/apps/frontend/src/components/technique/AssignedScientistsTable.tsx b/apps/frontend/src/components/technique/AssignedScientistsTable.tsx index d33db52bc7..94c40dd473 100644 --- a/apps/frontend/src/components/technique/AssignedScientistsTable.tsx +++ b/apps/frontend/src/components/technique/AssignedScientistsTable.tsx @@ -1,9 +1,10 @@ import MaterialTable from '@material-table/core'; import { Box } from '@mui/material'; -import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import { BasicUserDetails, TechniqueFragment, UserRole } from 'generated/sdk'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; import { tableIcons } from 'utils/materialIcons'; diff --git a/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx b/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx index 2690aee9fc..ab37674827 100755 --- a/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx +++ b/apps/frontend/src/components/technique/CreateUpdateTechnique.tsx @@ -5,10 +5,11 @@ import { updateTechniqueValidationSchema, } from '@user-office-software/duo-validation/lib/Technique'; import { Field, Form, Formik } from 'formik'; -import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import TextField from 'components/common/FormikUITextField'; import UOLoader from 'components/common/UOLoader'; import { TechniqueFragment } from 'generated/sdk'; diff --git a/apps/frontend/src/components/technique/TechniqueTable.tsx b/apps/frontend/src/components/technique/TechniqueTable.tsx index cff6fa7377..0c02f7e287 100755 --- a/apps/frontend/src/components/technique/TechniqueTable.tsx +++ b/apps/frontend/src/components/technique/TechniqueTable.tsx @@ -1,10 +1,11 @@ import { Column } from '@material-table/core'; import { AssignmentInd } from '@mui/icons-material'; import { Dialog, DialogContent, Typography } from '@mui/material'; -import i18n from 'i18n'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import ScienceIcon from 'components/common/icons/ScienceIcon'; import SimpleTabs from 'components/common/SimpleTabs'; import SuperMaterialTable from 'components/common/SuperMaterialTable'; diff --git a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx index e0d2699b96..843b1b740d 100644 --- a/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx +++ b/apps/frontend/src/components/techniqueProposal/TechniqueProposalTable.tsx @@ -18,7 +18,6 @@ import { getTranslation, ResourceId, } from '@user-office-software/duo-localisation'; -import i18n from 'i18n'; import { t, TFunction } from 'i18next'; import React, { useCallback, @@ -30,6 +29,8 @@ import React, { } from 'react'; import { useSearchParams } from 'react-router-dom'; +import i18n from 'i18n'; + import { parseInstrumentQuery } from 'components/common/proposalFilters/InstrumentFilter'; import UOLoader from 'components/common/UOLoader'; import ProposalReviewContent, { diff --git a/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx b/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx index 81b5a2f0fa..67a0df6f75 100644 --- a/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx +++ b/apps/frontend/src/components/template/CreateUpdateEmailTemplate.tsx @@ -5,10 +5,11 @@ import { updateEmailTemplateValidationSchema, } from '@user-office-software/duo-validation'; import { Field, Form, Formik } from 'formik'; -import i18n from 'i18n'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import i18n from 'i18n'; + import CheckboxWithLabel from 'components/common/FormikUICheckboxWithLabel'; import TextField from 'components/common/FormikUITextField'; import UOLoader from 'components/common/UOLoader'; From eecc7157e3c9a06bced0506b66760b81adc0432a Mon Sep 17 00:00:00 2001 From: Scott Hurley Date: Fri, 17 Jul 2026 13:20:54 +0100 Subject: [PATCH 8/8] adds roledatasource mock to stfcuserdatasource unit tests --- .../src/datasources/stfc/StfcUserDataSource.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts index 484ffe02b9..5f76383a94 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.spec.ts @@ -1,6 +1,7 @@ import { StfcUserDataSource } from './StfcUserDataSource'; import { Roles, createRole } from '../../models/Role'; import { dummyUser } from '../mockups/UserDataSource'; +import RoleDataSource from '../postgres/RoleDataSource'; import PostgresUserDataSource from '../postgres/UserDataSource'; jest.mock('../postgres/UserDataSource.ts'); @@ -164,6 +165,12 @@ describe('Role tests', () => { ); mockGetUserRoles.mockImplementation(() => Promise.resolve([])); + const mockGetTagsByRoleId = jest.spyOn( + RoleDataSource.prototype, + 'getTagsByRoleId' + ); + mockGetTagsByRoleId.mockImplementation(() => Promise.resolve([])); + const mockEnsureDummyUserExists = jest.spyOn( StfcUserDataSource.prototype, 'ensureDummyUserExists'