From e239014b248d12a4d8fb08dbc3e400f98ec5225b Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 29 Jul 2025 11:00:51 +0200 Subject: [PATCH] feat: Timeline for cancelled CRs Shows the cancelled state in the timeline for cancelled CRs. Also, if timestamps are present, it will dynamically show the steps we have times for for rejected and cancelled CRs. CRs can be cancelled after: - In review - Approved CRs can be rejected after: - In review - Approved - 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). --- .../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', +];