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 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; 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 839205e3d6..6267f56cbf 100644 --- a/apps/e2e/cypress/e2e/experimentSafetyReview.cy.ts +++ b/apps/e2e/cypress/e2e/experimentSafetyReview.cy.ts @@ -339,37 +339,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'); @@ -560,9 +529,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', () => { @@ -749,13 +715,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 not enable download button when Instrument Scientist rejects (IS workflow)', () => { @@ -865,9 +828,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', () => { @@ -933,13 +893,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 not enable download button when Experiment Safety Reviewer rejects (ESR workflow)', () => { @@ -1033,11 +990,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(); @@ -1109,13 +1065,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 && ( - - - - - - - - - )} -