From 456860702708483ba6057dd798fbe882d7662def Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Thu, 2 Oct 2025 16:35:25 +0200 Subject: [PATCH 1/8] feat: add ProfileInfo component and enhance AccountActionButton with account management options --- .../AppToolbar/AccountActionButton.tsx | 66 +++++++++-- .../src/components/AppToolbar/AppToolbar.tsx | 111 +++++------------- .../src/components/AppToolbar/ProfileInfo.tsx | 48 ++++++++ 3 files changed, 136 insertions(+), 89 deletions(-) create mode 100644 apps/frontend/src/components/AppToolbar/ProfileInfo.tsx diff --git a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx index 33694e99cf..31a156e83f 100644 --- a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx +++ b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx @@ -1,8 +1,9 @@ -import { ExitToApp } from '@mui/icons-material'; +import { ExitToApp, ManageAccounts } from '@mui/icons-material'; import AccountCircle from '@mui/icons-material/AccountCircle'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import SupervisedUserCircleIcon from '@mui/icons-material/SupervisedUserCircle'; import SwitchAccountOutlinedIcon from '@mui/icons-material/SwitchAccountOutlined'; +import { Chip, Divider } from '@mui/material'; import Badge from '@mui/material/Badge'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -12,26 +13,37 @@ import IconButton from '@mui/material/IconButton'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Tooltip from '@mui/material/Tooltip'; -import React, { useContext, useEffect, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import ImpersonateButton from 'components/common/ImpersonateButton'; import StyledDialog from 'components/common/StyledDialog'; import UOLoader from 'components/common/UOLoader'; +import { SettingsContext } from 'context/SettingsContextProvider'; import { UserContext } from 'context/UserContextProvider'; +import { SettingsId } from 'generated/sdk'; import { getUniqueArrayBy } from 'utils/helperFunctions'; +import ProfileInfo from './ProfileInfo'; import RoleSelection from './RoleSelection'; const AccountActionButton = () => { + const navigate = useNavigate(); const [isLoggingOut, setIsLoggingOut] = useState(false); const [show, setShow] = useState(false); - const { roles, handleLogout, impersonatingUserId } = useContext(UserContext); + const { user } = useContext(UserContext); + const settingsContext = useContext(SettingsContext); + const { roles, currentRole, handleLogout, impersonatingUserId } = + useContext(UserContext); const [anchorEl, setAnchorEl] = React.useState(null); - const [searchParams, setSearchParams] = useSearchParams(); - const hasMultipleRoles = getUniqueArrayBy(roles, 'id').length > 1; + const humanReadableActiveRole = useMemo( + () => + roles.find(({ shortCode }) => shortCode.toUpperCase() === currentRole) + ?.title ?? 'Unknown', + [roles, currentRole] + ); useEffect(() => { if (searchParams.get('selectRoles')) { @@ -46,6 +58,10 @@ const AccountActionButton = () => { const isUserImpersonated = typeof impersonatingUserId === 'number'; + const externalProfileLink = settingsContext.settingsMap.get( + SettingsId.PROFILE_PAGE_LINK + )?.settingsValue; + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -63,6 +79,15 @@ const AccountActionButton = () => { }); }; + const handleManageAccountClick = () => { + handleClose(); + if (externalProfileLink) { + window.open(externalProfileLink, '_blank', 'noopener,noreferrer'); + } else { + navigate(`/ProfilePage/${user.id}`); + } + }; + return ( <> { open={Boolean(anchorEl)} onClose={handleClose} > + + + + + + + Manage account + {hasMultipleRoles && ( { @@ -132,7 +165,26 @@ const AccountActionButton = () => { - Roles + + Roles + + )} {isUserImpersonated && ( diff --git a/apps/frontend/src/components/AppToolbar/AppToolbar.tsx b/apps/frontend/src/components/AppToolbar/AppToolbar.tsx index 7341a09470..b628274e9a 100644 --- a/apps/frontend/src/components/AppToolbar/AppToolbar.tsx +++ b/apps/frontend/src/components/AppToolbar/AppToolbar.tsx @@ -1,19 +1,17 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import MenuIcon from '@mui/icons-material/Menu'; +import { Box } from '@mui/material'; import AppBar from '@mui/material/AppBar'; -import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; -import MuiLink from '@mui/material/Link'; import { useTheme } from '@mui/material/styles'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import useMediaQuery from '@mui/material/useMediaQuery'; import PropTypes from 'prop-types'; -import React, { useContext, useMemo, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { SettingsContext } from 'context/SettingsContextProvider'; -import { UserContext } from 'context/UserContextProvider'; import { SettingsId } from 'generated/sdk'; import AccountActionButton from './AccountActionButton'; @@ -44,18 +42,6 @@ const AppToolbar = ({ open, handleDrawerOpen, header }: AppToolbarProps) => { setLogo('/images/' + logoFilename); }, [logoFilename]); - const { user, roles, currentRole } = useContext(UserContext); - const settingsContext = useContext(SettingsContext); - const humanReadableActiveRole = useMemo( - () => - roles.find(({ shortCode }) => shortCode.toUpperCase() === currentRole) - ?.title ?? 'Unknown', - [roles, currentRole] - ); - const externalProfileLink = settingsContext.settingsMap.get( - SettingsId.PROFILE_PAGE_LINK - )?.settingsValue; - return ( { }} > - - - - {(!isTabletOrMobile || !isPortraitMode) && logo && ( - - Institution logo - - )} - {(!isTabletOrMobile || !isPortraitMode) && ( - + - {location.pathname === '/' ? 'Dashboard' : header} - - )} - - Logged in as{' '} - {externalProfileLink ? ( - - {user.email} - - ) : ( - - {user.email} - + + + {(!isTabletOrMobile || !isPortraitMode) && logo && ( + + Institution logo + + )} + {(!isTabletOrMobile || !isPortraitMode) && ( + + {location.pathname === '/' ? 'Dashboard' : header} + )} - {roles.length > 1 && ` (${humanReadableActiveRole})`} diff --git a/apps/frontend/src/components/AppToolbar/ProfileInfo.tsx b/apps/frontend/src/components/AppToolbar/ProfileInfo.tsx new file mode 100644 index 0000000000..20e9aff603 --- /dev/null +++ b/apps/frontend/src/components/AppToolbar/ProfileInfo.tsx @@ -0,0 +1,48 @@ +// react functional component + +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import React, { useContext } from 'react'; + +import { UserContext } from 'context/UserContextProvider'; +import { getFullUserName } from 'utils/user'; + +const ProfileInfo = () => { + const { user } = useContext(UserContext); + + return ( + + + + + {getFullUserName(user)} + + + {user?.email.toLocaleLowerCase()} + + + + ); +}; + +export default ProfileInfo; From f61a11db37597711839c784e22a51b8bffabd35c Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Fri, 3 Oct 2025 11:01:19 +0200 Subject: [PATCH 2/8] fix: update personal information test to use new account management button --- apps/e2e/cypress/e2e/personalInformation.cy.ts | 4 +++- .../src/components/AppToolbar/AccountActionButton.tsx | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/e2e/cypress/e2e/personalInformation.cy.ts b/apps/e2e/cypress/e2e/personalInformation.cy.ts index 744b9be6f1..92f5e63d3b 100644 --- a/apps/e2e/cypress/e2e/personalInformation.cy.ts +++ b/apps/e2e/cypress/e2e/personalInformation.cy.ts @@ -55,7 +55,9 @@ context('Personal information tests', () => { cy.login('user1'); cy.visit('/'); - cy.get('[data-cy="active-user-profile"]').click(); + cy.get('[data-cy="profile-page-btn"]').click(); + + cy.get('[data-cy="manage-account-button"]').click(); cy.get("[name='firstname']").clear().type(newFirstName); diff --git a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx index 31a156e83f..f749967f7c 100644 --- a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx +++ b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx @@ -148,7 +148,11 @@ const AccountActionButton = () => { > - + From bc70ecacc5cf097c74ff384dc949fa6f90bf7a83 Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Sun, 12 Oct 2025 12:55:40 +0200 Subject: [PATCH 3/8] fix: update invite test to use data attribute for chip existence check --- apps/e2e/cypress/e2e/invites.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index 0fb0c3edf3..9e449a0534 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -101,7 +101,7 @@ context('Invites tests', () => { .should('be.enabled') .click(); - cy.get('.MuiChip-label').should('not.exist'); + cy.get('[data-cy="invites-chips"]').contains(email).should('not.exist'); cy.get('[data-cy="co-proposers"]').contains(lastName); }); From 8b575b4247b73df08bd750877e6ab69b2cb5600d Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Sun, 12 Oct 2025 15:10:43 +0200 Subject: [PATCH 4/8] fix: update invite test to check for non-existence of invites chips --- apps/e2e/cypress/e2e/invites.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index 1404f36ab9..56c2db79c0 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -101,8 +101,8 @@ context('Invites tests', () => { .should('be.enabled') .click(); - cy.get('[data-cy="invites-chips"]').contains(email).should('not.exist'); cy.get('[data-cy="co-proposers"]').contains(lastName); + cy.get('[data-cy="invites-chips"]').should('not.exist'); }); it('Should not be able to invite email already invited on proposal', function () { From 2b30860d7c30723ae7bba2075b9e9c46d6c4d9fe Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Wed, 15 Oct 2025 17:30:31 +0200 Subject: [PATCH 5/8] fix: optimised the element find fixing the issue --- apps/e2e/cypress/e2e/visits.cy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/visits.cy.ts b/apps/e2e/cypress/e2e/visits.cy.ts index 294c3975b9..d7a0ee58ec 100644 --- a/apps/e2e/cypress/e2e/visits.cy.ts +++ b/apps/e2e/cypress/e2e/visits.cy.ts @@ -273,7 +273,11 @@ context('visits tests', () => { cy.finishedLoading(); cy.get('[name=email]').type('david@teleworm.us{enter}'); cy.finishedLoading(); - cy.contains('Beckley').parent().find('[type=checkbox]').click(); + cy.get('[data-cy=co-proposers]') + .contains('Beckley') + .parent() + .find('[type=checkbox]') + .click(); cy.get('[data-cy=assign-selected-users]').click(); // specify team lead From 13e1c2911c6417ae8c831f2eccbe86f88c46f120 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Thu, 16 Oct 2025 17:39:56 +0200 Subject: [PATCH 6/8] fix: exp safety download button refresh fixed by using request polling --- ...eDefaultExperimentSafetyReviewTemplate.sql | 17 ++ .../db_seeds/0001_ProposalForScheduling.sql | 8 +- .../cypress/e2e/experimentSafetyReview.cy.ts | 79 ++---- .../ExperimentSafetyReviewSummary.tsx | 227 +++++++++++------- .../src/components/menu/MenuItems.tsx | 22 +- .../components/menu/SettingsMenuListItem.tsx | 28 +-- 6 files changed, 203 insertions(+), 178 deletions(-) create mode 100644 apps/backend/db_patches/0201_ActivateDefaultExperimentSafetyReviewTemplate.sql diff --git a/apps/backend/db_patches/0201_ActivateDefaultExperimentSafetyReviewTemplate.sql b/apps/backend/db_patches/0201_ActivateDefaultExperimentSafetyReviewTemplate.sql new file mode 100644 index 0000000000..cb3596d504 --- /dev/null +++ b/apps/backend/db_patches/0201_ActivateDefaultExperimentSafetyReviewTemplate.sql @@ -0,0 +1,17 @@ +DO +$$ +DECLARE + default_experiment_safety_review_template int; + experiment_safety_review_template_group_id text; +BEGIN + IF register_patch('ActivateDefaultExperimentSafetyReviewTemplate.sql', 'Yoganandan Pandiyan', 'Activate default Experiment Safety Review template', '2025-10-16') THEN + SELECT templates.template_id, templates.group_id + INTO default_experiment_safety_review_template, experiment_safety_review_template_group_id + FROM templates + WHERE name='default experiment safety review template'; + + INSERT INTO active_templates (template_id, group_id) VALUES (default_experiment_safety_review_template, experiment_safety_review_template_group_id); + END IF; +END; +$$ +LANGUAGE plpgsql; diff --git a/apps/backend/db_patches/db_seeds/0001_ProposalForScheduling.sql b/apps/backend/db_patches/db_seeds/0001_ProposalForScheduling.sql index b25ef98568..b3f37abec7 100644 --- a/apps/backend/db_patches/db_seeds/0001_ProposalForScheduling.sql +++ b/apps/backend/db_patches/db_seeds/0001_ProposalForScheduling.sql @@ -67,6 +67,7 @@ BEGIN , comment_for_user , notified , submitted + , management_decision_submitted ) VALUES ( @@ -83,7 +84,8 @@ BEGIN , NULL , NULL , true - , true + , true + , true ); INSERT INTO instrument_has_proposals(instrument_id, proposal_pk) VALUES (1, 1); @@ -107,6 +109,7 @@ BEGIN , comment_for_user , notified , submitted + , management_decision_submitted ) VALUES ( @@ -123,7 +126,8 @@ BEGIN , NULL , NULL , true - , true + , true + , true ); INSERT INTO instrument_has_proposals(instrument_id, proposal_pk) VALUES (2, 2); diff --git a/apps/e2e/cypress/e2e/experimentSafetyReview.cy.ts b/apps/e2e/cypress/e2e/experimentSafetyReview.cy.ts index 383f3014dd..1b37fc21c2 100644 --- a/apps/e2e/cypress/e2e/experimentSafetyReview.cy.ts +++ b/apps/e2e/cypress/e2e/experimentSafetyReview.cy.ts @@ -269,37 +269,6 @@ function createWorkflowForESR() { }); } -// Helper function to approve proposal -function approveProposal() { - cy.login('officer'); - cy.visit('/'); - cy.contains(TEST_CONSTANTS.UI_LABELS.PROPOSALS).click(); - cy.get('[data-cy=view-proposal]').first().click(); - cy.finishedLoading(); - cy.get('[role="dialog"]').contains(TEST_CONSTANTS.UI_LABELS.ADMIN).click(); - cy.get('[data-cy="proposal-final-status"]').should('exist'); - cy.get('[role="dialog"]').contains(TEST_CONSTANTS.UI_LABELS.ADMIN).click(); - cy.get('[data-cy="proposal-final-status"]').click(); - cy.get('li[data-cy="proposal-final-status-options"]') - .contains(TEST_CONSTANTS.FORM_VALUES.PROPOSAL_STATUS) - .click(); - cy.get( - `[data-cy="managementTimeAllocation-${initialDBData.instrument1.id}"] input` - ) - .clear() - .type(TEST_CONSTANTS.FORM_VALUES.MANAGEMENT_TIME); - cy.get('[data-cy="is-management-decision-submitted"]').click(); - cy.get('[data-cy="save-admin-decision"]').click(); - cy.notification({ variant: 'success', text: 'Saved' }); - cy.reload(); - cy.get('[data-cy="is-management-decision-submitted"] input').should( - 'have.value', - 'true' - ); - cy.closeModal(); - cy.contains(TEST_CONSTANTS.FORM_VALUES.PROPOSAL_STATUS); -} - // Helper function to submit ESF by user function submitESFByUser() { cy.login('user1'); @@ -490,9 +459,6 @@ context('Experiment Safety Review tests', () => { templateGroupId: TemplateGroupId.EXPERIMENT_SAFETY_REVIEW, templateId: initialDBData.experimentSafetyReviewTemplate.id, }); - - // Approve proposal for all tests - approveProposal(); }); describe('User Experiment Safety Form Submission', () => { @@ -679,13 +645,10 @@ context('Experiment Safety Review tests', () => { cy.get('[data-cy="button-submit-experiment-safety-review"]').click(); cy.contains(TEST_CONSTANTS.UI_LABELS.OK).click(); - // Refresh the page to get the updated status after workflow processing - cy.reload(); - - // Download button should now be enabled (IS approval enables download in IS workflow) - cy.contains(TEST_CONSTANTS.UI_LABELS.DOWNLOAD_SAFETY_DOCUMENT).should( - 'not.be.disabled' - ); + // Wait for the workflow processing to complete by checking the download button state + cy.contains(TEST_CONSTANTS.UI_LABELS.DOWNLOAD_SAFETY_DOCUMENT, { + timeout: 6000, + }).should('not.be.disabled'); }); it('Should change experiment status to ESF APPROVED after Instrument Scientist approval (IS workflow)', () => { @@ -744,9 +707,6 @@ context('Experiment Safety Review tests', () => { templateGroupId: TemplateGroupId.EXPERIMENT_SAFETY_REVIEW, templateId: initialDBData.experimentSafetyReviewTemplate.id, }); - - // Approve proposal for all tests - approveProposal(); }); describe('User Experiment Safety Form Submission', () => { @@ -812,13 +772,10 @@ context('Experiment Safety Review tests', () => { cy.get('[data-cy="button-submit-experiment-safety-review"]').click(); cy.contains('OK').click(); - // Refresh the page to get the updated status after workflow processing - cy.reload(); - - // Download button should now be enabled (ESR approval enables download in ESR workflow) - cy.contains('Download Safety Review Document').should( - 'not.be.disabled' - ); + // Wait for the workflow processing to complete by checking the download button state + cy.contains('Download Safety Review Document', { + timeout: 6000, + }).should('not.be.disabled'); }); it('Should change experiment status to ESF APPROVED after ESR approval (ESR workflow)', () => { @@ -869,11 +826,10 @@ context('Experiment Safety Review tests', () => { cy.get('[data-cy="button-submit-experiment-safety-review"]').click(); cy.contains('OK').click(); - // Refresh the page to get the updated status after workflow processing - cy.reload(); - - // Download button should remain disabled (IS approval doesn't enable download in ESR workflow) - cy.contains('Download Safety Review Document').should('be.disabled'); + // Wait for the workflow processing to complete, then check that download button remains disabled + cy.contains('Download Safety Review Document', { + timeout: 6000, + }).should('be.disabled'); // Status should remain the same (not changed to approved) cy.get('[data-cy="close-modal"]').click(); @@ -945,13 +901,10 @@ context('Experiment Safety Review tests', () => { cy.get('[data-cy="button-submit-experiment-safety-review"]').click(); cy.contains('OK').click(); - // Refresh the page to get the updated status after workflow processing - cy.reload(); - - // Now download should be enabled and status should change - cy.contains('Download Safety Review Document').should( - 'not.be.disabled' - ); + // Wait for the workflow processing to complete by checking the download button state + cy.contains('Download Safety Review Document', { + timeout: 6000, + }).should('not.be.disabled'); cy.get('[data-cy="close-modal"]').click(); cy.visit('/Experiments'); cy.finishedLoading(); diff --git a/apps/frontend/src/components/experimentSafetyReview/ExperimentSafetyReviewSummary.tsx b/apps/frontend/src/components/experimentSafetyReview/ExperimentSafetyReviewSummary.tsx index 4d891043ac..12489a6418 100644 --- a/apps/frontend/src/components/experimentSafetyReview/ExperimentSafetyReviewSummary.tsx +++ b/apps/frontend/src/components/experimentSafetyReview/ExperimentSafetyReviewSummary.tsx @@ -9,12 +9,14 @@ import TextField from '@mui/material/TextField'; import React, { useContext, useMemo, useState } from 'react'; import { NavigButton } from 'components/common/NavigButton'; +import UOLoader from 'components/common/UOLoader'; import NavigationFragment from 'components/questionary/NavigationFragment'; import { QuestionaryContext } from 'components/questionary/QuestionaryContext'; import { UserContext } from 'context/UserContextProvider'; import { ExperimentSafetyReviewerDecisionEnum, InstrumentScientistDecisionEnum, + Status, UserRole, } from 'generated/sdk'; import { useDownloadPDFExperimentSafety } from 'hooks/experiment/useDownloadPDFExperimentSafety'; @@ -38,6 +40,12 @@ function ExperimentSafetyReviewSummary({ QuestionaryContext ) as ExperimentSafetyReviewContextType; + const [ + updatedExperimentSafetyReviewState, + setUpdatedExperimentSafetyReviewState, + ] = useState(); + const [refetchingStatus, setRefetchingStatus] = useState(false); + // Initialize decision and comment based on current role and existing data const getInitialDecision = useMemo(() => { if (currentRole === UserRole.INSTRUMENT_SCIENTIST) { @@ -80,9 +88,6 @@ function ExperimentSafetyReviewSummary({ const [isFormLocked, setIsFormLocked] = useState( getInitialDecision !== '' ); - const [isDownloadEnabled, setIsDownloadEnabled] = useState( - state?.experimentSafety.status?.shortCode === 'ESF_APPROVED' - ); const downloadExperimentSafety = useDownloadPDFExperimentSafety(); @@ -94,6 +99,121 @@ function ExperimentSafetyReviewSummary({ throw new Error('Experiment safety review questionary not found'); } + const refreshExperimentSafetyStatus = async () => { + let callCount = 0; + const maxCalls = 3; + const intervalMs = 2000; + let isApproved = false; + + // Set loading state to true at the beginning + setRefetchingStatus(true); + + const makeApiCall = async () => { + try { + const { experimentSafety } = await api().getExperimentSafety({ + experimentSafetyPk: state.experimentSafety.experimentSafetyPk, + }); + if (!experimentSafety) { + throw new Error('Experiment safety review not found'); + } + if (experimentSafety.status) { + setUpdatedExperimentSafetyReviewState(experimentSafety.status); + // Check if status is ESF_APPROVED + if (experimentSafety.status.shortCode === 'ESF_APPROVED') { + isApproved = true; + } + } + } finally { + callCount++; + if (callCount >= maxCalls || isApproved) { + setRefetchingStatus(false); + } + } + }; + + // Start the first call after 3 seconds + setTimeout(() => { + makeApiCall(); + + // Set up interval for remaining calls + const intervalId = setInterval(() => { + if (callCount >= maxCalls || isApproved) { + clearInterval(intervalId); + + return; + } + makeApiCall(); + }, intervalMs); + }, intervalMs); + }; + + const handleSubmit = () => { + confirm( + async () => { + setIsSubmitting(true); + try { + if (currentRole === UserRole.INSTRUMENT_SCIENTIST) { + // Call the instrument scientist mutation + const { submitInstrumentScientistExperimentSafetyReview } = + await api({ + toastSuccessMessage: 'Safety review submitted successfully', + }).submitInstrumentScientistExperimentSafetyReview({ + experimentSafetyPk: state.experimentSafety.experimentSafetyPk, + decision: decision as InstrumentScientistDecisionEnum, + comment: comment, + }); + + dispatch({ + type: 'ITEM_WITH_QUESTIONARY_MODIFIED', + itemWithQuestionary: + submitInstrumentScientistExperimentSafetyReview, + }); + dispatch({ + type: 'ITEM_WITH_QUESTIONARY_SUBMITTED', + itemWithQuestionary: + submitInstrumentScientistExperimentSafetyReview, + }); + + refreshExperimentSafetyStatus(); + } else { + // For USER_OFFICER, EXPERIMENT_SAFETY_REVIEWER, and others + const { submitExperimentSafetyReviewerExperimentSafetyReview } = + await api({ + toastSuccessMessage: 'Safety review submitted successfully', + }).submitExperimentSafetyReviewerExperimentSafetyReview({ + experimentSafetyPk: state.experimentSafety.experimentSafetyPk, + decision: decision as ExperimentSafetyReviewerDecisionEnum, + comment: comment, + }); + + dispatch({ + type: 'ITEM_WITH_QUESTIONARY_MODIFIED', + itemWithQuestionary: + submitExperimentSafetyReviewerExperimentSafetyReview, + }); + dispatch({ + type: 'ITEM_WITH_QUESTIONARY_SUBMITTED', + itemWithQuestionary: + submitExperimentSafetyReviewerExperimentSafetyReview, + }); + + refreshExperimentSafetyStatus(); + // Update download enabled state based on the actual status + } + // Lock the form after successful submission + setIsFormLocked(true); + } finally { + setIsSubmitting(false); + } + }, + { + title: 'Please confirm', + description: + 'Are you sure want to submit the Experiment Safety Review?', + } + )(); + }; + return ( <> ) : ( { - confirm( - async () => { - setIsSubmitting(true); - try { - if (currentRole === UserRole.INSTRUMENT_SCIENTIST) { - // Call the instrument scientist mutation - const { - submitInstrumentScientistExperimentSafetyReview, - } = await api({ - toastSuccessMessage: - 'Safety review submitted successfully', - }).submitInstrumentScientistExperimentSafetyReview({ - experimentSafetyPk: - state.experimentSafety.experimentSafetyPk, - decision: decision as InstrumentScientistDecisionEnum, - comment: comment, - }); - - dispatch({ - type: 'ITEM_WITH_QUESTIONARY_MODIFIED', - itemWithQuestionary: - submitInstrumentScientistExperimentSafetyReview, - }); - dispatch({ - type: 'ITEM_WITH_QUESTIONARY_SUBMITTED', - itemWithQuestionary: - submitInstrumentScientistExperimentSafetyReview, - }); - - // Update download enabled state based on the actual status - setIsDownloadEnabled( - submitInstrumentScientistExperimentSafetyReview?.status - ?.shortCode === 'ESF_APPROVED' - ); - } else { - // For USER_OFFICER, EXPERIMENT_SAFETY_REVIEWER, and others - const { - submitExperimentSafetyReviewerExperimentSafetyReview, - } = await api({ - toastSuccessMessage: - 'Safety review submitted successfully', - }).submitExperimentSafetyReviewerExperimentSafetyReview({ - experimentSafetyPk: - state.experimentSafety.experimentSafetyPk, - decision: - decision as ExperimentSafetyReviewerDecisionEnum, - comment: comment, - }); - - dispatch({ - type: 'ITEM_WITH_QUESTIONARY_MODIFIED', - itemWithQuestionary: - submitExperimentSafetyReviewerExperimentSafetyReview, - }); - dispatch({ - type: 'ITEM_WITH_QUESTIONARY_SUBMITTED', - itemWithQuestionary: - submitExperimentSafetyReviewerExperimentSafetyReview, - }); - - // Update download enabled state based on the actual status - setIsDownloadEnabled( - submitExperimentSafetyReviewerExperimentSafetyReview - ?.status?.shortCode === 'ESF_APPROVED' - ); - } - // Lock the form after successful submission - setIsFormLocked(true); - } finally { - setIsSubmitting(false); - } - }, - { - title: 'Please confirm', - description: - 'Are you sure want to submit the Experiment Safety Review?', - } - )(); - }} + onClick={handleSubmit} isBusy={isSubmitting} disabled={!decision} data-cy="button-submit-experiment-safety-review" @@ -252,9 +293,25 @@ function ExperimentSafetyReviewSummary({ ) } color="secondary" - disabled={!isDownloadEnabled} + disabled={ + refetchingStatus || + (updatedExperimentSafetyReviewState + ? !( + updatedExperimentSafetyReviewState.shortCode === + 'ESF_APPROVED' + ) + : !(state?.experimentSafety.status?.shortCode === 'ESF_APPROVED')) + } + sx={{ minWidth: '250px' }} > - Download Safety Review Document + {refetchingStatus ? ( + <> + + Fetching Status... + + ) : ( + 'Download Safety Review Document' + )} diff --git a/apps/frontend/src/components/menu/MenuItems.tsx b/apps/frontend/src/components/menu/MenuItems.tsx index e60c5e4141..5cae256863 100644 --- a/apps/frontend/src/components/menu/MenuItems.tsx +++ b/apps/frontend/src/components/menu/MenuItems.tsx @@ -60,7 +60,9 @@ const MenuItems = ({ currentRole }: MenuItemsProps) => { const isUserManagementEnabled = context.featuresMap.get( FeatureId.USER_MANAGEMENT )?.isEnabled; - + const isExperimentSafetyEnabled = context.featuresMap.get( + FeatureId.EXPERIMENT_SAFETY_REVIEW + )?.isEnabled; const isTechniqueProposalsEnabled = useTechniqueProposalAccess([ UserRole.USER_OFFICER, UserRole.INSTRUMENT_SCIENTIST, @@ -215,6 +217,24 @@ const MenuItems = ({ currentRole }: MenuItemsProps) => { + {isExperimentSafetyEnabled && ( + + + + + + + + + )} + {isUserManagementEnabled && ( diff --git a/apps/frontend/src/components/menu/SettingsMenuListItem.tsx b/apps/frontend/src/components/menu/SettingsMenuListItem.tsx index e323a3cc23..cf16ee6df7 100644 --- a/apps/frontend/src/components/menu/SettingsMenuListItem.tsx +++ b/apps/frontend/src/components/menu/SettingsMenuListItem.tsx @@ -1,4 +1,3 @@ -import { Science } from '@mui/icons-material'; import DisplaySettingsIcon from '@mui/icons-material/DisplaySettings'; import ExpandLess from '@mui/icons-material/ExpandLess'; import ExpandMore from '@mui/icons-material/ExpandMore'; @@ -11,12 +10,10 @@ import { ListItemButton } from '@mui/material'; import Collapse from '@mui/material/Collapse'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; -import React, { useContext, useState } from 'react'; +import React, { useState } from 'react'; import { NavLink, useLocation } from 'react-router-dom'; import Tooltip from 'components/common/MenuTooltip'; -import { FeatureContext } from 'context/FeatureContextProvider'; -import { FeatureId } from 'generated/sdk'; import ProposalSettingsIcon from '../common/icons/ProposalSettingsIcon'; @@ -32,14 +29,9 @@ const menuMap = { const SettingsMenuListItem = () => { const location = useLocation(); - const featureContext = useContext(FeatureContext); const [isExpanded, setIsExpanded] = useState(false); - const isExperimentSafetyEnabled = featureContext.featuresMap.get( - FeatureId.EXPERIMENT_SAFETY_REVIEW - )?.isEnabled; - React.useEffect(() => { setIsExpanded(Object.values(menuMap).includes(location.pathname)); }, [location.pathname]); @@ -100,24 +92,6 @@ const SettingsMenuListItem = () => { - {isExperimentSafetyEnabled && ( - - - - - - - - - )} - From dc0e469527d9c2c0f6a536ddf7e0ed45bdc45ad9 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Tue, 4 Nov 2025 11:13:46 +0100 Subject: [PATCH 7/8] chore: automation of syncing open non dependabot PRs with develop branch --- .github/workflows/sync-prs-with-develop.yml | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/sync-prs-with-develop.yml diff --git a/.github/workflows/sync-prs-with-develop.yml b/.github/workflows/sync-prs-with-develop.yml new file mode 100644 index 0000000000..eda25a1f7c --- /dev/null +++ b/.github/workflows/sync-prs-with-develop.yml @@ -0,0 +1,40 @@ +name: Sync PRs with develop + +on: + push: + branches: + - develop1 + +jobs: + sync-prs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Rebase all open non-Dependabots PRs with develop branch as base + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get all open PRs targeting develop, excluding Dependabot + prs=$(gh pr list --state open --base develop1 --limit 100 --json number,author --jq '.[] | select(.author.is_bot != true) | .number') + + for pr in $prs; do + echo "Processing PR #$pr" + gh pr checkout $pr + git fetch origin develop1 + + # Attempt rebase + if git rebase origin/develop1; then + echo "PR #$pr rebased successfully. Pushing changes..." + git push --force-with-lease + else + echo "Conflict in PR #$pr. Rebase aborted." + git rebase --abort + # Optional: Notify author + gh pr comment $pr --body "⚠️ Automatic rebase failed due to conflicts. Please rebase manually." + fi + done From c95002d5d7adb3ec7ef996b144e1b58f38225fcf Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Tue, 4 Nov 2025 11:25:13 +0100 Subject: [PATCH 8/8] test --- apps/backend/db_patches/0001_CreateTopicReadinessState.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/db_patches/0001_CreateTopicReadinessState.sql b/apps/backend/db_patches/0001_CreateTopicReadinessState.sql index 216d0d566e..e0908c360c 100644 --- a/apps/backend/db_patches/0001_CreateTopicReadinessState.sql +++ b/apps/backend/db_patches/0001_CreateTopicReadinessState.sql @@ -20,6 +20,7 @@ BEGIN + END; END IF; END;