From f3d87c580e5de053c652c084422cdba6f1c0ba09 Mon Sep 17 00:00:00 2001 From: Gustavo Gama Date: Thu, 22 May 2025 03:08:11 -0300 Subject: [PATCH 1/5] feat: allow approval of previous versions of job specs Display the "Approve" button on all versions of a job spec in the "job proposal details" page that have the "cancelled" status. This allows users to approve older versions of a job spec. This PR must be deployed along with PR # from the core repo. [DX-534](https://smartcontract-it.atlassian.net/browse/DX-534) --- .changeset/flat-toys-roll.md | 5 ++ src/screens/JobProposal/SpecsView.test.tsx | 77 +++++++++++++++++++++- src/screens/JobProposal/SpecsView.tsx | 12 ++-- 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 .changeset/flat-toys-roll.md diff --git a/.changeset/flat-toys-roll.md b/.changeset/flat-toys-roll.md new file mode 100644 index 00000000..a733abf4 --- /dev/null +++ b/.changeset/flat-toys-roll.md @@ -0,0 +1,5 @@ +--- +'@smartcontractkit/operator-ui': patch +--- + +feat: allow approval of previous versions of job specs diff --git a/src/screens/JobProposal/SpecsView.test.tsx b/src/screens/JobProposal/SpecsView.test.tsx index 9ea34ece..edabcfa1 100644 --- a/src/screens/JobProposal/SpecsView.test.tsx +++ b/src/screens/JobProposal/SpecsView.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { render, screen, waitFor } from 'support/test-utils' +import { render, screen, waitFor, within } from 'support/test-utils' import userEvent from '@testing-library/user-event' import { @@ -260,4 +260,79 @@ describe('SpecsView', () => { expect(queryByText('Cancel')).not.toBeInTheDocument() }) }) + + describe('Approve button', () => { + let specs: ReadonlyArray + let proposal: JobProposalPayloadFields + + beforeEach(() => { + specs = [ + buildJobProposalSpec({ id: '105', version: 5, status: 'CANCELLED' }), + buildJobProposalSpec({ id: '104', version: 4, status: 'PENDING' }), + buildJobProposalSpec({ id: '103', version: 3, status: 'CANCELLED' }), + buildJobProposalSpec({ id: '102', version: 2, status: 'APPROVED' }), + buildJobProposalSpec({ id: '101', version: 1, status: 'REVOKED' }), + ] + }) + + it('is visible in all cancelled specs if proposal is not deleted or revoked', async () => { + proposal = buildJobProposal({ status: 'PENDING' }) + renderComponent(specs, proposal) + + const panels = screen.getAllByTestId('expansion-panel') + expect(panels).toHaveLength(5) + expect( + within(panels[0]).queryByRole('button', { + name: 'Approve', + hidden: false, + }), + ).toBeInTheDocument() + expect( + within(panels[1]).queryByRole('button', { + name: 'Approve', + hidden: true, + }), + ).not.toBeInTheDocument() + expect( + within(panels[2]).queryByRole('button', { + name: 'Approve', + hidden: true, + }), + ).toBeInTheDocument() + expect( + within(panels[3]).queryByRole('button', { + name: 'Approve', + hidden: true, + }), + ).not.toBeInTheDocument() + expect( + within(panels[4]).queryByRole('button', { + name: 'Approve', + hidden: true, + }), + ).not.toBeInTheDocument() + }) + + it('is not visible in any specs if proposal is deleted', async () => { + proposal = buildJobProposal({ status: 'DELETED' }) + renderComponent(specs, proposal) + + const panels = screen.getAllByTestId('expansion-panel') + expect(panels).toHaveLength(5) + expect( + screen.queryByRole('button', { name: 'Approve', hidden: false }), + ).not.toBeInTheDocument() + }) + + it('is not visible in any specs if proposal is revoked', async () => { + proposal = buildJobProposal({ status: 'REVOKED' }) + renderComponent(specs, proposal) + + const panels = screen.getAllByTestId('expansion-panel') + expect(panels).toHaveLength(5) + expect( + screen.queryByRole('button', { name: 'Approve', hidden: false }), + ).not.toBeInTheDocument() + }) + }) }) diff --git a/src/screens/JobProposal/SpecsView.tsx b/src/screens/JobProposal/SpecsView.tsx index 0e54d71e..f64885d0 100644 --- a/src/screens/JobProposal/SpecsView.tsx +++ b/src/screens/JobProposal/SpecsView.tsx @@ -192,11 +192,7 @@ export const SpecsView = withStyles(styles)( ) case 'CANCELLED': - if ( - latestSpec.id === specID && - proposal.status !== 'DELETED' && - proposal.status !== 'REVOKED' - ) { + if (proposal.status !== 'DELETED' && proposal.status !== 'REVOKED') { return ( @@ -295,6 +319,7 @@ export const SpecsView = withStyles(styles)( if (confirmationDialog) { switch (confirmationDialog.action) { case 'approve': + case 'approvePrevious': onApprove(confirmationDialog.id) break From 6d9785cd173b564630ab452651a85a4ba8abcfd3 Mon Sep 17 00:00:00 2001 From: gustavogama-cll <165679773+gustavogama-cll@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:33:51 -0300 Subject: [PATCH 4/5] Update src/screens/JobProposal/SpecsView.tsx fix spec id comparison Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/screens/JobProposal/SpecsView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/JobProposal/SpecsView.tsx b/src/screens/JobProposal/SpecsView.tsx index ccc06f0c..448a5d6e 100644 --- a/src/screens/JobProposal/SpecsView.tsx +++ b/src/screens/JobProposal/SpecsView.tsx @@ -228,7 +228,7 @@ export const SpecsView = withStyles(styles)( color="primary" onClick={() => openConfirmationDialog( - specID == latestSpec.id ? 'approve' : 'approvePrevious', + specID === latestSpec.id ? 'approve' : 'approvePrevious', specID, ) } From c63dca37fbbf3277fee26507098cc916794ed5ca Mon Sep 17 00:00:00 2001 From: gustavogama-cll <165679773+gustavogama-cll@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:34:42 -0300 Subject: [PATCH 5/5] simplify construction of approvableCancelledJobSpecs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/screens/JobProposal/SpecsView.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/screens/JobProposal/SpecsView.tsx b/src/screens/JobProposal/SpecsView.tsx index 448a5d6e..63ce586b 100644 --- a/src/screens/JobProposal/SpecsView.tsx +++ b/src/screens/JobProposal/SpecsView.tsx @@ -144,10 +144,9 @@ export const SpecsView = withStyles(styles)( }, [specs]) const approvableCancelledJobSpecs = sortedSpecs - .map((spec, idx) => ({ spec, idx })) - .filter((el) => el.spec.status === 'CANCELLED') + .filter((spec) => spec.status === 'CANCELLED') .slice(0, 2) - .map((el) => el.spec.id) + .map((spec) => spec.id) const renderActions = ( status: SpecStatus,