mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02:00
feat: reject timeline state (#4517)
This commit is contained in:
parent
f561cfac4d
commit
8a7f488147
@ -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');
|
||||||
|
});
|
@ -1,21 +1,16 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { styled } from '@mui/material';
|
import { Box, Paper, styled } from '@mui/material';
|
||||||
import { Box, Paper } from '@mui/material';
|
|
||||||
import Timeline from '@mui/lab/Timeline';
|
import Timeline from '@mui/lab/Timeline';
|
||||||
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
|
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
|
||||||
import TimelineSeparator from '@mui/lab/TimelineSeparator';
|
import TimelineSeparator from '@mui/lab/TimelineSeparator';
|
||||||
import TimelineDot from '@mui/lab/TimelineDot';
|
import TimelineDot from '@mui/lab/TimelineDot';
|
||||||
import TimelineConnector from '@mui/lab/TimelineConnector';
|
import TimelineConnector from '@mui/lab/TimelineConnector';
|
||||||
import TimelineContent from '@mui/lab/TimelineContent';
|
import TimelineContent from '@mui/lab/TimelineContent';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { ChangeRequestState } from '../../changeRequest.types';
|
import { ChangeRequestState } from '../../changeRequest.types';
|
||||||
|
|
||||||
interface ISuggestChangeTimelineProps {
|
interface ISuggestChangeTimelineProps {
|
||||||
state: ChangeRequestState;
|
state: ChangeRequestState;
|
||||||
}
|
}
|
||||||
interface ITimelineData {
|
|
||||||
title: string;
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(2),
|
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<ISuggestChangeTimelineProps> = ({
|
export const ChangeRequestTimeline: FC<ISuggestChangeTimelineProps> = ({
|
||||||
state,
|
state,
|
||||||
}) => {
|
}) => {
|
||||||
const createTimeLineData = (state: ChangeRequestState): ITimelineData[] => {
|
const data = state === 'Rejected' ? rejectedSteps : steps;
|
||||||
const steps: ChangeRequestState[] = [
|
const activeIndex = data.findIndex(item => item === state);
|
||||||
'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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledPaper elevation={0}>
|
<StyledPaper elevation={0}>
|
||||||
<StyledBox>
|
<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>
|
</StyledBox>
|
||||||
</StyledPaper>
|
</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 = (
|
const createTimelineItem = (
|
||||||
color: 'primary' | 'success' | 'grey',
|
color: 'primary' | 'success' | 'grey' | 'error',
|
||||||
title: string,
|
title: string,
|
||||||
connector: JSX.Element,
|
shouldConnectToNextItem: boolean,
|
||||||
timelineDotProps: { [key: string]: string } = {}
|
timelineDotProps: { [key: string]: string | undefined } = {}
|
||||||
) => {
|
) => (
|
||||||
return (
|
<TimelineItem key={title}>
|
||||||
<TimelineItem key={title}>
|
<TimelineSeparator>
|
||||||
<TimelineSeparator>
|
<TimelineDot color={color} {...timelineDotProps} />
|
||||||
<TimelineDot color={color} {...timelineDotProps} />
|
{shouldConnectToNextItem && <TimelineConnector />}
|
||||||
{connector}
|
</TimelineSeparator>
|
||||||
</TimelineSeparator>
|
<TimelineContent>{title}</TimelineContent>
|
||||||
<TimelineContent>{title}</TimelineContent>
|
</TimelineItem>
|
||||||
</TimelineItem>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user