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} + +);