1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-09 13:47:13 +02:00

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).
This commit is contained in:
Thomas Heartman 2025-07-29 11:00:51 +02:00
parent 8554eee37a
commit e239014b24
No known key found for this signature in database
GPG Key ID: BD1F880DAED1EE78
4 changed files with 125 additions and 18 deletions

View File

@ -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(<ChangeRequestTimeline state={'Cancelled'} />);
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', () => {

View File

@ -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<ISuggestChangeTimelineProps> = ({
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;
}

View File

@ -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']);
});
});

View File

@ -0,0 +1,49 @@
import type { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
export const stepsFromTimestamps = (
timestamps: Partial<Record<ChangeRequestState, unknown>>,
currentState: Extract<ChangeRequestState, 'Cancelled' | 'Rejected'>,
): 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',
];