Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reject timeline state #4517

Merged
merged 3 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(<ChangeRequestTimeline state={'Cancelled'} />);

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

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

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

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');
});
Original file line number Diff line number Diff line change
@@ -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),
Expand All @@ -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<ISuggestChangeTimelineProps> = ({
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 (
<StyledPaper elevation={0}>
<StyledBox>
<StyledTimeline>{renderTimeline()}</StyledTimeline>
<StyledTimeline>
{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
);
})}
</StyledTimeline>
</StyledBox>
</StyledPaper>
);
};

const createTimeline = (data: ITimelineData[], activeIndex: number | null) => {
return data.map(({ title }, index) => {
const shouldConnectToNextItem = index < data.length - 1;

const connector = (
<ConditionallyRender
condition={shouldConnectToNextItem}
show={<TimelineConnector />}
/>
);

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 = (
<ConditionallyRender
condition={shouldConnectToNextItem}
show={<TimelineConnector />}
/>
);
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 (
<TimelineItem key={title}>
<TimelineSeparator>
<TimelineDot color={color} {...timelineDotProps} />
{connector}
</TimelineSeparator>
<TimelineContent>{title}</TimelineContent>
</TimelineItem>
);
};
shouldConnectToNextItem: boolean,
timelineDotProps: { [key: string]: string | undefined } = {}
) => (
<TimelineItem key={title}>
<TimelineSeparator>
<TimelineDot color={color} {...timelineDotProps} />
{shouldConnectToNextItem && <TimelineConnector />}
</TimelineSeparator>
<TimelineContent>{title}</TimelineContent>
</TimelineItem>
);