diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.test.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.test.tsx
new file mode 100644
index 000000000000..0871b4351e75
--- /dev/null
+++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.test.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { screen } from '@testing-library/react';
+import { render } from 'utils/testRenderer';
+import { ChangeRequestTimeline, determineColor } from './ChangeRequestTimeline';
+import { ChangeRequestState } from '../../changeRequest.types';
+
+test('cancelled timeline shows all states', () => {
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('In review')).toBeInTheDocument();
+ expect(screen.getByText('Approved')).toBeInTheDocument();
+ expect(screen.getByText('Applied')).toBeInTheDocument();
+});
+
+test('approved timeline shows all states', () => {
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('In review')).toBeInTheDocument();
+ expect(screen.getByText('Approved')).toBeInTheDocument();
+ expect(screen.getByText('Applied')).toBeInTheDocument();
+});
+
+test('applied timeline shows all states', () => {
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('In review')).toBeInTheDocument();
+ expect(screen.getByText('Approved')).toBeInTheDocument();
+ expect(screen.getByText('Applied')).toBeInTheDocument();
+});
+
+test('rejected timeline shows all states', () => {
+ render();
+
+ expect(screen.getByText('Draft')).toBeInTheDocument();
+ expect(screen.getByText('In review')).toBeInTheDocument();
+ expect(screen.getByText('Rejected')).toBeInTheDocument();
+ expect(screen.queryByText('Approved')).not.toBeInTheDocument();
+ expect(screen.queryByText('Applied')).not.toBeInTheDocument();
+});
+
+const irrelevantIndex = -99; // Using a number that's unlikely to be a valid index
+
+test('returns grey for Cancelled state regardless of displayed stage', () => {
+ const stages: ChangeRequestState[] = [
+ 'Draft',
+ 'In review',
+ 'Approved',
+ 'Applied',
+ 'Rejected',
+ ];
+ stages.forEach(stage => {
+ expect(
+ determineColor('Cancelled', irrelevantIndex, stage, irrelevantIndex)
+ ).toBe('grey');
+ });
+});
+
+test('returns error for Rejected stage in Rejected state', () => {
+ expect(
+ determineColor('Rejected', irrelevantIndex, 'Rejected', irrelevantIndex)
+ ).toBe('error');
+});
+
+test('returns success for stages other than Rejected in Rejected state', () => {
+ expect(
+ determineColor('Rejected', irrelevantIndex, 'Draft', irrelevantIndex)
+ ).toBe('success');
+ expect(
+ determineColor(
+ 'Rejected',
+ irrelevantIndex,
+ 'In review',
+ irrelevantIndex
+ )
+ ).toBe('success');
+});
+
+test('returns success for stages at or before activeIndex', () => {
+ expect(determineColor('In review', 1, 'Draft', 0)).toBe('success');
+ expect(determineColor('In review', 1, 'In review', 1)).toBe('success');
+});
+
+test('returns primary for stages right after activeIndex', () => {
+ expect(determineColor('In review', 1, 'Approved', 2)).toBe('primary');
+});
+
+test('returns grey for stages two or more steps after activeIndex', () => {
+ expect(determineColor('Draft', 0, 'Approved', 2)).toBe('grey');
+ expect(determineColor('Draft', 0, 'Applied', 3)).toBe('grey');
+});
diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx
index df4655a2d8f1..c90e6265133c 100644
--- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx
@@ -1,21 +1,16 @@
import { FC } from 'react';
-import { styled } from '@mui/material';
-import { Box, Paper } from '@mui/material';
+import { Box, Paper, styled } from '@mui/material';
import Timeline from '@mui/lab/Timeline';
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
import TimelineSeparator from '@mui/lab/TimelineSeparator';
import TimelineDot from '@mui/lab/TimelineDot';
import TimelineConnector from '@mui/lab/TimelineConnector';
import TimelineContent from '@mui/lab/TimelineContent';
-import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ChangeRequestState } from '../../changeRequest.types';
+
interface ISuggestChangeTimelineProps {
state: ChangeRequestState;
}
-interface ITimelineData {
- title: string;
- active: boolean;
-}
const StyledPaper = styled(Paper)(({ theme }) => ({
marginTop: theme.spacing(2),
@@ -34,96 +29,83 @@ const StyledTimeline = styled(Timeline)(() => ({
},
}));
+const steps: ChangeRequestState[] = [
+ 'Draft',
+ 'In review',
+ 'Approved',
+ 'Applied',
+];
+const rejectedSteps: ChangeRequestState[] = ['Draft', 'In review', 'Rejected'];
+
+export const determineColor = (
+ changeRequestState: ChangeRequestState,
+ changeRequestStateIndex: number,
+ displayStage: ChangeRequestState,
+ displayStageIndex: number
+) => {
+ if (changeRequestState === 'Cancelled') return 'grey';
+ if (changeRequestState === 'Rejected')
+ return displayStage === 'Rejected' ? 'error' : 'success';
+ if (
+ changeRequestStateIndex !== -1 &&
+ changeRequestStateIndex >= displayStageIndex
+ )
+ return 'success';
+ if (changeRequestStateIndex + 1 === displayStageIndex) return 'primary';
+ return 'grey';
+};
+
export const ChangeRequestTimeline: FC = ({
state,
}) => {
- const createTimeLineData = (state: ChangeRequestState): ITimelineData[] => {
- const steps: ChangeRequestState[] = [
- 'Draft',
- 'In review',
- 'Approved',
- 'Applied',
- ];
-
- return steps.map(step => ({
- title: step,
- active: step === state,
- }));
- };
-
- const renderTimeline = () => {
- const data = createTimeLineData(state);
- const index = data.findIndex(item => item.active);
- const activeIndex: number | null = index !== -1 ? index : null;
-
- if (state === 'Cancelled') {
- return createCancelledTimeline(data);
- }
-
- return createTimeline(data, activeIndex);
- };
+ const data = state === 'Rejected' ? rejectedSteps : steps;
+ const activeIndex = data.findIndex(item => item === state);
return (
- {renderTimeline()}
+
+ {data.map((title, index) => {
+ const color = determineColor(
+ state,
+ activeIndex,
+ title,
+ index
+ );
+ let timelineDotProps = {};
+
+ // Only add the outlined variant if it's the next step after the active one, but not for 'Draft' in 'Cancelled' state
+ if (
+ activeIndex + 1 === index &&
+ !(state === 'Cancelled' && title === 'Draft')
+ ) {
+ timelineDotProps = { variant: 'outlined' };
+ }
+
+ return createTimelineItem(
+ color,
+ title,
+ index < data.length - 1,
+ timelineDotProps
+ );
+ })}
+
);
};
-const createTimeline = (data: ITimelineData[], activeIndex: number | null) => {
- return data.map(({ title }, index) => {
- const shouldConnectToNextItem = index < data.length - 1;
-
- const connector = (
- }
- />
- );
-
- if (activeIndex !== null && activeIndex >= index) {
- return createTimelineItem('success', title, connector);
- }
-
- if (activeIndex !== null && activeIndex + 1 === index) {
- return createTimelineItem('primary', title, connector, {
- variant: 'outlined',
- });
- }
-
- return createTimelineItem('grey', title, connector);
- });
-};
-
-const createCancelledTimeline = (data: ITimelineData[]) => {
- return data.map(({ title }, index) => {
- const shouldConnectToNextItem = index < data.length - 1;
-
- const connector = (
- }
- />
- );
- return createTimelineItem('grey', title, connector);
- });
-};
-
const createTimelineItem = (
- color: 'primary' | 'success' | 'grey',
+ color: 'primary' | 'success' | 'grey' | 'error',
title: string,
- connector: JSX.Element,
- timelineDotProps: { [key: string]: string } = {}
-) => {
- return (
-
-
-
- {connector}
-
- {title}
-
- );
-};
+ shouldConnectToNextItem: boolean,
+ timelineDotProps: { [key: string]: string | undefined } = {}
+) => (
+
+
+
+ {shouldConnectToNextItem && }
+
+ {title}
+
+);