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

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
<img width="381" height="241" alt="image"
src="https://github.com/user-attachments/assets/a6af70c5-ef09-4aeb-ae53-0e3ff4b25446"
/>


<img width="396" height="320" alt="image"
src="https://github.com/user-attachments/assets/699c2594-8c0c-44d9-bf50-22a8bdda3d00"
/>


Rejections 
<img width="397" height="244" alt="image"
src="https://github.com/user-attachments/assets/20887a23-e453-49ce-bc5c-738ba4180868"
/>


<img width="388" height="384" alt="image"
src="https://github.com/user-attachments/assets/e3c5842b-254d-47b8-a8f6-3721643c4223"
/>


New cancelled steps without timestamps:
<img width="387" height="309" alt="image"
src="https://github.com/user-attachments/assets/5fba979d-cb5b-4aba-b652-7c5ac89a3e37"
/>
This commit is contained in:
Thomas Heartman 2025-07-29 12:21:30 +02:00 committed by GitHub
parent 216e5411ca
commit 663f30c8db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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',
];