From 663f30c8db73612bc7aa6b604fad1f2255046eb5 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 29 Jul 2025 12:21:30 +0200 Subject: [PATCH] feat: Timeline for cancelled CRs (#10421) Shows the cancelled state in the timeline for cancelled CRs. Extracts steps into a separate file. Also, if timestamps are present, it will dynamically show the steps we have times for for rejected and cancelled CRs. If we do not have timestamps, it'll use the old behavior for displaying rejected steps, but does add a new one for cancelled steps, which includes 'Draft', 'In review', 'Approved', 'Cancelled'. CRs can be cancelled after: - In review - Approved CRs can be rejected after: - In review - Schedule For other states, use the existing steps. Cancelled and Rejected are both terminal states, so there's no future steps to show. Regardless of what we have timestamps for, always show 'Draft', 'In review' and the final state (Rejected | Cancelled). ## Gallery Cancellations image image Rejections image image New cancelled steps without timestamps: image --- .../ChangeRequestTimeline.test.tsx | 4 +- .../ChangeRequestTimeline.tsx | 35 ++++++------ .../change-request-timeline-steps.test.ts | 55 +++++++++++++++++++ .../change-request-timeline-steps.ts | 49 +++++++++++++++++ 4 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.test.ts create mode 100644 frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.ts diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.test.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.test.tsx index 63a6045000..00c10eebfa 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.test.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.test.tsx @@ -7,13 +7,13 @@ import { } from './ChangeRequestTimeline.tsx'; import type { ChangeRequestState } from '../../changeRequest.types'; -test('cancelled timeline shows all states', () => { +test('cancelled timeline shows expected states', () => { render(); expect(screen.getByText('Draft')).toBeInTheDocument(); expect(screen.getByText('In review')).toBeInTheDocument(); expect(screen.getByText('Approved')).toBeInTheDocument(); - expect(screen.getByText('Applied')).toBeInTheDocument(); + expect(screen.getByText('Cancelled')).toBeInTheDocument(); }); test('approved timeline shows all states', () => { diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx index 9f7eac308b..b8758a2edc 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx @@ -15,6 +15,13 @@ import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip.tsx'; import ErrorIcon from '@mui/icons-material/Error'; import { useLocationSettings } from 'hooks/useLocationSettings'; import { formatDateYMDHM } from 'utils/formatDate.ts'; +import { + stepsFromTimestamps, + rejectedSteps, + scheduledSteps, + cancelledSteps, + steps, +} from './change-request-timeline-steps.ts'; export type ISuggestChangeTimelineProps = { timestamps?: ChangeRequestType['stateTransitionTimestamps']; // todo: update with flag `timestampsInChangeRequestTimeline` @@ -58,21 +65,6 @@ const StyledTimeline = styled(Timeline)(() => ({ }, })); -const steps: ChangeRequestState[] = [ - 'Draft', - 'In review', - 'Approved', - 'Applied', -]; -const rejectedSteps: ChangeRequestState[] = ['Draft', 'In review', 'Rejected']; -const scheduledSteps: ChangeRequestState[] = [ - 'Draft', - 'In review', - 'Approved', - 'Scheduled', - 'Applied', -]; - export const determineColor = ( changeRequestState: ChangeRequestState, changeRequestStateIndex: number, @@ -101,11 +93,22 @@ export const ChangeRequestTimeline: FC = ({ let data: ChangeRequestState[]; switch (state) { case 'Rejected': - data = rejectedSteps; + if (timestamps) { + data = stepsFromTimestamps(timestamps, 'Rejected'); + } else { + data = rejectedSteps; + } break; case 'Scheduled': data = scheduledSteps; break; + case 'Cancelled': + if (timestamps) { + data = stepsFromTimestamps(timestamps, 'Cancelled'); + } else { + data = cancelledSteps; + } + break; default: data = steps; } diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.test.ts b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.test.ts new file mode 100644 index 0000000000..c196599a3b --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.test.ts @@ -0,0 +1,55 @@ +import { stepsFromTimestamps } from './change-request-timeline-steps.ts'; + +describe('stepsFromTimestamps', () => { + test.each(['Rejected', 'Cancelled'])( + "always includes 'Draft' and 'In review' and the current stage (%s)", + (state) => { + expect( + stepsFromTimestamps({}, state as 'Rejected' | 'Cancelled'), + ).toStrictEqual(['Draft', 'In review', state]); + }, + ); + + test('Includes all steps in the timestamps object', () => { + expect( + stepsFromTimestamps( + { + Draft: {}, + 'In review': {}, + Approved: {}, + Rejected: {}, + }, + 'Rejected', + ), + ).toStrictEqual(['Draft', 'In review', 'Approved', 'Rejected']); + + expect( + stepsFromTimestamps( + { + Draft: {}, + Approved: {}, + Scheduled: {}, + }, + 'Rejected', + ), + ).toStrictEqual([ + 'Draft', + 'In review', + 'Approved', + 'Scheduled', + 'Rejected', + ]); + + // The implementation is naïve, so even if a CR must be approved to be + // scheduled, if the timestamps object does not contain the 'Approved' + // step, it will not be included. + expect( + stepsFromTimestamps( + { + Scheduled: {}, + }, + 'Rejected', + ), + ).toStrictEqual(['Draft', 'In review', 'Scheduled', 'Rejected']); + }); +}); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.ts b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.ts new file mode 100644 index 0000000000..8a6c3785f6 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/change-request-timeline-steps.ts @@ -0,0 +1,49 @@ +import type { ChangeRequestState } from 'component/changeRequest/changeRequest.types'; + +export const stepsFromTimestamps = ( + timestamps: Partial>, + currentState: Extract, +): ChangeRequestState[] => { + const optionalSteps: ChangeRequestState[] = [ + 'Approved', + 'Applied', + 'Cancelled', + 'Scheduled', + 'Rejected', + ]; + + return [ + 'Draft', + 'In review', + ...optionalSteps.filter( + (step) => timestamps.hasOwnProperty(step) || step === currentState, + ), + ]; +}; + +export const steps: ChangeRequestState[] = [ + 'Draft', + 'In review', + 'Approved', + 'Applied', +]; + +export const rejectedSteps: ChangeRequestState[] = [ + 'Draft', + 'In review', + 'Rejected', +]; +export const cancelledSteps: ChangeRequestState[] = [ + 'Draft', + 'In review', + 'Approved', + 'Cancelled', +]; + +export const scheduledSteps: ChangeRequestState[] = [ + 'Draft', + 'In review', + 'Approved', + 'Scheduled', + 'Applied', +];