mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +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:
parent
216e5411ca
commit
663f30c8db
@ -7,13 +7,13 @@ import {
|
|||||||
} from './ChangeRequestTimeline.tsx';
|
} from './ChangeRequestTimeline.tsx';
|
||||||
import type { ChangeRequestState } from '../../changeRequest.types';
|
import type { ChangeRequestState } from '../../changeRequest.types';
|
||||||
|
|
||||||
test('cancelled timeline shows all states', () => {
|
test('cancelled timeline shows expected states', () => {
|
||||||
render(<ChangeRequestTimeline state={'Cancelled'} />);
|
render(<ChangeRequestTimeline state={'Cancelled'} />);
|
||||||
|
|
||||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||||
expect(screen.getByText('In review')).toBeInTheDocument();
|
expect(screen.getByText('In review')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Approved')).toBeInTheDocument();
|
expect(screen.getByText('Approved')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Applied')).toBeInTheDocument();
|
expect(screen.getByText('Cancelled')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('approved timeline shows all states', () => {
|
test('approved timeline shows all states', () => {
|
||||||
|
@ -15,6 +15,13 @@ import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip.tsx';
|
|||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { formatDateYMDHM } from 'utils/formatDate.ts';
|
import { formatDateYMDHM } from 'utils/formatDate.ts';
|
||||||
|
import {
|
||||||
|
stepsFromTimestamps,
|
||||||
|
rejectedSteps,
|
||||||
|
scheduledSteps,
|
||||||
|
cancelledSteps,
|
||||||
|
steps,
|
||||||
|
} from './change-request-timeline-steps.ts';
|
||||||
|
|
||||||
export type ISuggestChangeTimelineProps = {
|
export type ISuggestChangeTimelineProps = {
|
||||||
timestamps?: ChangeRequestType['stateTransitionTimestamps']; // todo: update with flag `timestampsInChangeRequestTimeline`
|
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 = (
|
export const determineColor = (
|
||||||
changeRequestState: ChangeRequestState,
|
changeRequestState: ChangeRequestState,
|
||||||
changeRequestStateIndex: number,
|
changeRequestStateIndex: number,
|
||||||
@ -101,11 +93,22 @@ export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
|
|||||||
let data: ChangeRequestState[];
|
let data: ChangeRequestState[];
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'Rejected':
|
case 'Rejected':
|
||||||
data = rejectedSteps;
|
if (timestamps) {
|
||||||
|
data = stepsFromTimestamps(timestamps, 'Rejected');
|
||||||
|
} else {
|
||||||
|
data = rejectedSteps;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'Scheduled':
|
case 'Scheduled':
|
||||||
data = scheduledSteps;
|
data = scheduledSteps;
|
||||||
break;
|
break;
|
||||||
|
case 'Cancelled':
|
||||||
|
if (timestamps) {
|
||||||
|
data = stepsFromTimestamps(timestamps, 'Cancelled');
|
||||||
|
} else {
|
||||||
|
data = cancelledSteps;
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
data = steps;
|
data = steps;
|
||||||
}
|
}
|
||||||
|
@ -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']);
|
||||||
|
});
|
||||||
|
});
|
@ -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',
|
||||||
|
];
|
Loading…
Reference in New Issue
Block a user