Skip to content

Commit 58d6d98

Browse files
feat: add the ability to execute status actions when the proposal status is updated manually (#1441)
2 parents c2d9e51 + a0e0c32 commit 58d6d98

12 files changed

Lines changed: 458 additions & 153 deletions

File tree

apps/backend/src/mutations/ProposalMutations.ts

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -541,51 +541,60 @@ export default class ProposalMutations {
541541
agent: UserWithRole | null,
542542
args: ChangeProposalsStatusInput
543543
): Promise<Proposals | Rejection> {
544-
const { workflowStatusId: statusId, proposalPks } = args;
544+
const {
545+
workflowStatusId: statusId,
546+
proposalPks,
547+
statusActionsWorkflowConnectionId,
548+
} = args;
545549

546550
const result = await this.proposalDataSource.changeProposalsWorkflowStatus(
547551
statusId,
548552
proposalPks.map((proposalPk) => proposalPk)
549553
);
550554

551555
if (result.proposals.length === proposalPks.length) {
552-
const fullProposals = await Promise.all(
553-
proposalPks.map(async (proposalPk) => {
554-
const fullProposal = result.proposals.find(
555-
(updatedProposal) => updatedProposal.primaryKey === proposalPk
556-
);
557-
558-
if (!fullProposal) {
559-
return null;
560-
}
561-
562-
const proposalWorkflow =
563-
await this.callDataSource.getProposalWorkflowByCall(
564-
fullProposal.callId
556+
// Only run status actions if a specific workflow connection was provided
557+
if (statusActionsWorkflowConnectionId) {
558+
const fullProposals = await Promise.all(
559+
proposalPks.map(async (proposalPk) => {
560+
const fullProposal = result.proposals.find(
561+
(updatedProposal) => updatedProposal.primaryKey === proposalPk
565562
);
566563

567-
if (!proposalWorkflow) {
568-
return rejection(
569-
`No propsal workflow found for the specific proposal call with id: ${fullProposal.callId}`,
570-
{
571-
agent,
572-
args,
573-
}
574-
);
575-
}
564+
if (!fullProposal) {
565+
return null;
566+
}
576567

577-
return {
578-
...fullProposal,
579-
};
580-
})
581-
);
568+
const proposalWorkflow =
569+
await this.callDataSource.getProposalWorkflowByCall(
570+
fullProposal.callId
571+
);
572+
573+
if (!proposalWorkflow) {
574+
return rejection(
575+
`No propsal workflow found for the specific proposal call with id: ${fullProposal.callId}`,
576+
{
577+
agent,
578+
args,
579+
}
580+
);
581+
}
582582

583-
const statusEngineReadyProposals = fullProposals.filter(
584-
(item): item is WorkflowEngineProposalType => !!item
585-
);
583+
return {
584+
...fullProposal,
585+
prevStatusId: fullProposal.workflowStatusId,
586+
workflowStatusConnectionId: statusActionsWorkflowConnectionId,
587+
};
588+
})
589+
);
586590

587-
// NOTE: After proposal status change we need to run the status engine and execute the actions on the selected status.
588-
proposalStatusActionEngine(statusEngineReadyProposals);
591+
const statusEngineReadyProposals = fullProposals.filter(
592+
(item): item is WorkflowEngineProposalType => !!item
593+
);
594+
595+
// NOTE: After proposal status change we need to run the status engine and execute the actions on the selected status.
596+
proposalStatusActionEngine(statusEngineReadyProposals);
597+
}
589598
} else {
590599
rejection('Could not change statuses to all of the selected proposals', {
591600
result,

apps/backend/src/queries/StatusActionQueries.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default class StatusActionQueries {
2828
public emailTemplateDataSource: EmailTemplateDataSource
2929
) {}
3030

31-
@Authorized([Roles.USER_OFFICER])
31+
@Authorized([Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST])
3232
async getStatusAction(agent: UserWithRole | null, actionId: number) {
3333
const statusAction = await this.dataSource.getStatusAction(actionId);
3434

@@ -42,15 +42,15 @@ export default class StatusActionQueries {
4242
return statusActions;
4343
}
4444

45-
@Authorized([Roles.USER_OFFICER])
45+
@Authorized([Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST])
4646
async getConnectionStatusActions(
4747
agent: UserWithRole | null,
4848
{ workflowConnectionId }: { workflowConnectionId: number }
4949
) {
5050
return this.dataSource.getConnectionStatusActions(workflowConnectionId);
5151
}
5252

53-
@Authorized([Roles.USER_OFFICER])
53+
@Authorized([Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST])
5454
async getStatusActionConfig(
5555
agent: UserWithRole | null,
5656
statusAction: StatusAction

apps/backend/src/queries/WorkflowQueries.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default class WorkflowQueries {
1717
public emailService: MailService
1818
) {}
1919

20-
@Authorized([Roles.USER_OFFICER])
20+
@Authorized([Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST])
2121
async getWorkflow(
2222
agent: UserWithRole | null,
2323
id: number,

apps/backend/src/resolvers/mutations/ChangeProposalsStatusMutation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export class ChangeProposalsStatusInput {
1818

1919
@Field(() => [Int])
2020
public proposalPks: number[];
21+
22+
/**
23+
* The workflow connection whose status actions (emails, RabbitMQ messages,
24+
* PDF generation) should run after the status change. Omit to change the
25+
* proposal status WITHOUT running any status actions.
26+
*/
27+
@Field(() => Int, { nullable: true })
28+
public statusActionsWorkflowConnectionId?: number;
2129
}
2230

2331
@Resolver()

apps/e2e/cypress/e2e/proposals.cy.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { faker } from '@faker-js/faker';
22
import {
33
AllocationTimeUnits,
44
DataType,
5+
EmailStatusActionRecipients,
56
FeatureId,
67
ProposalEndStatus,
78
SettingsId,
9+
StatusActionType,
810
TemplateCategoryId,
911
TemplateGroupId,
1012
WorkflowType,
@@ -143,6 +145,7 @@ context('Proposal tests', () => {
143145
cy.addStatusToWorkflow({
144146
statusId: initialDBData.proposalStatuses.feasibilityReview.id,
145147
workflowId: result.createWorkflow.id,
148+
posY: 200,
146149
});
147150
createdWorkflowId = result.createWorkflow.id;
148151
}
@@ -896,6 +899,79 @@ context('Proposal tests', () => {
896899
);
897900
});
898901

902+
it('User officer should be able to opt-in to run status actions when changing status', () => {
903+
const statusActionConfig = {
904+
recipientsWithEmailTemplate: [
905+
{
906+
recipient: {
907+
name: EmailStatusActionRecipients.PI,
908+
description: '',
909+
},
910+
emailTemplate: {
911+
id: initialDBData.emailTemplates.template1.id,
912+
name: initialDBData.emailTemplates.template1.name,
913+
},
914+
combineEmails: true,
915+
},
916+
],
917+
};
918+
919+
// Add a status with a connection from DRAFT and attach a status action
920+
cy.addStatusToWorkflow({
921+
statusId: initialDBData.proposalStatuses.feasibilityReview.id,
922+
workflowId: initialDBData.workflows.defaultWorkflow.id,
923+
prevId:
924+
initialDBData.workflows.defaultWorkflow.workflowStatuses.draft.id,
925+
posX: 0,
926+
posY: 200,
927+
}).then((result) => {
928+
cy.addConnectionStatusActions({
929+
actions: [
930+
{
931+
actionId: 1,
932+
actionType: StatusActionType.EMAIL,
933+
config: JSON.stringify(statusActionConfig),
934+
},
935+
],
936+
connectionId: result.createWorkflowConnection.id,
937+
workflowId: initialDBData.workflows.defaultWorkflow.id,
938+
});
939+
});
940+
941+
cy.login('officer');
942+
cy.visit('/');
943+
944+
cy.contains(newProposalTitle).parent().find('[type="checkbox"]').check();
945+
946+
cy.get('[data-cy="change-proposal-status"]').click();
947+
948+
cy.finishedLoading();
949+
950+
cy.get('[role="presentation"] .MuiDialogContent-root').as('dialog');
951+
952+
// Select the status that has a connection with actions
953+
cy.get('@dialog').find('#selectedWorkflowStatusId-input').click();
954+
cy.get('[role="listbox"]')
955+
.contains(initialDBData.proposalStatuses.feasibilityReview.name)
956+
.click();
957+
958+
// The run status actions checkbox should appear and be unchecked
959+
cy.get('[data-cy="run-status-actions-checkbox"] input')
960+
.should('exist')
961+
.should('not.be.checked');
962+
963+
// Check it to opt-in to running status actions
964+
cy.get('[data-cy="run-status-actions-checkbox"] input').check();
965+
966+
// Should be able to submit the status change
967+
cy.get('[data-cy="submit-proposal-status-change"]').click();
968+
969+
cy.notification({
970+
variant: 'success',
971+
text: 'status changed successfully',
972+
});
973+
});
974+
899975
it('Should be able to delete proposal', () => {
900976
cy.login('user1', initialDBData.roles.user);
901977
cy.visit('/');

apps/e2e/cypress/e2e/settings.cy.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,7 +1596,7 @@ context('Settings tests', () => {
15961596
cy.get('[data-cy^="status_ESF_IS_REVIEW"]').should('exist');
15971597
});
15981598

1599-
it('User Officer should be able to select events that are triggering change to ESF workflow status', () => {
1599+
it.only('User Officer should be able to select events that are triggering change to ESF workflow status', () => {
16001600
cy.login('officer');
16011601
cy.visit('/');
16021602

@@ -1625,7 +1625,9 @@ context('Settings tests', () => {
16251625

16261626
cy.reload();
16271627

1628-
cy.get(`[aria-label="Edge from AWAITING_ESF to ESF_IS_REVIEW"]`).click();
1628+
cy.get(`[aria-label="Edge from AWAITING_ESF to ESF_IS_REVIEW"]`).click({
1629+
force: true,
1630+
});
16291631

16301632
cy.get('[data-cy="status-events-and-actions-modal"]').should('exist');
16311633
cy.contains(Event.EXPERIMENT_ESF_SUBMITTED).click();
@@ -1642,7 +1644,9 @@ context('Settings tests', () => {
16421644
.contains(Event.EXPERIMENT_ESF_SUBMITTED)
16431645
.should('exist');
16441646

1645-
cy.get(`[aria-label="Edge from AWAITING_ESF to ESF_IS_REVIEW"]`).click();
1647+
cy.get(`[aria-label="Edge from AWAITING_ESF to ESF_IS_REVIEW"]`).click({
1648+
force: true,
1649+
});
16461650

16471651
cy.get('[data-cy="status-events-and-actions-modal"]').should('exist');
16481652
cy.contains(Event.EXPERIMENT_ESF_APPROVED_BY_IS).click();

0 commit comments

Comments
 (0)