diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx
index 09a613f6ce..4412d7df95 100644
--- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx
@@ -349,7 +349,10 @@ export const ChangeRequestOverview: FC = () => {
-
+
{
describe('changeRequestScheduleProps', () => {
test('returns correct props for a pending schedule', () => {
+ const date = new Date();
const schedule = {
- scheduledAt: new Date().toISOString(),
+ scheduledAt: date.toISOString(),
status: 'pending' as const,
};
- const time = 'some time string';
-
- const { title, subtitle, color, reason } = getScheduleProps(
- schedule,
- time,
- );
+ const { title, subtitle, color, reason } = getScheduleProps(schedule);
expect(title).toBe('Scheduled');
- expect(subtitle).toBe(`for ${time}`);
expect(color).toBe('warning');
expect(reason).toBeNull();
+
+ render(subtitle);
+ screen.getByText('for');
+ const timeElement = screen.getByRole('time');
+ const datetime = timeElement.getAttribute('datetime');
+ expect(new Date(datetime || 1)).toEqual(date);
});
test('returns correct props for a failed schedule', () => {
+ const date = new Date();
const schedule = {
- scheduledAt: new Date().toISOString(),
+ scheduledAt: date.toISOString(),
status: 'failed' as const,
reason: 'reason',
failureReason: 'failure reason',
};
- const time = 'some time string';
-
- const { title, subtitle, color, reason } = getScheduleProps(
- schedule,
- time,
- );
+ const { title, subtitle, color, reason } = getScheduleProps(schedule);
expect(title).toBe('Schedule failed');
- expect(subtitle).toBe(`at ${time}`);
expect(color).toBe('error');
expect(reason).toBeTruthy();
+
+ render(subtitle);
+ screen.getByText('at');
+ const timeElement = screen.getByRole('time');
+ const datetime = timeElement.getAttribute('datetime');
+ expect(new Date(datetime || 1)).toEqual(date);
});
test('returns correct props for a suspended schedule', () => {
+ const date = new Date();
const schedule = {
- scheduledAt: new Date().toISOString(),
+ scheduledAt: date.toISOString(),
status: 'suspended' as const,
reason: 'reason',
};
- const time = 'some time string';
-
- const { title, subtitle, color, reason } = getScheduleProps(
- schedule,
- time,
- );
+ const { title, subtitle, color, reason } = getScheduleProps(schedule);
expect(title).toBe('Schedule suspended');
- expect(subtitle).toBe(`was ${time}`);
expect(color).toBe('grey');
expect(reason).toBeTruthy();
+
+ render(subtitle);
+ screen.getByText('was');
+ const timeElement = screen.getByRole('time');
+ const datetime = timeElement.getAttribute('datetime');
+ expect(new Date(datetime || 1)).toEqual(date);
});
});
diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx
index 58a8fce25d..efc25510a7 100644
--- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx
@@ -1,7 +1,7 @@
-import type { FC } from 'react';
+import type { FC, ReactNode } from 'react';
import { Box, Paper, styled, Typography } from '@mui/material';
import Timeline from '@mui/lab/Timeline';
-import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
+import MuiTimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
import TimelineSeparator from '@mui/lab/TimelineSeparator';
import TimelineDot from '@mui/lab/TimelineDot';
import TimelineConnector from '@mui/lab/TimelineConnector';
@@ -9,16 +9,16 @@ import TimelineContent from '@mui/lab/TimelineContent';
import type {
ChangeRequestSchedule,
ChangeRequestState,
+ ChangeRequestType,
} from '../../changeRequest.types';
import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip.tsx';
import ErrorIcon from '@mui/icons-material/Error';
-import {
- type ILocationSettings,
- useLocationSettings,
-} from 'hooks/useLocationSettings';
-import { formatDateYMDHMS } from 'utils/formatDate';
+import { useLocationSettings } from 'hooks/useLocationSettings';
+import { formatDateYMDHM } from 'utils/formatDate.ts';
-export type ISuggestChangeTimelineProps =
+export type ISuggestChangeTimelineProps = {
+ timestamps?: ChangeRequestType['stateTransitionTimestamps']; // todo: update with flag `timestampsInChangeRequestTimeline`
+} & (
| {
state: Exclude;
schedule?: undefined;
@@ -26,7 +26,13 @@ export type ISuggestChangeTimelineProps =
| {
state: 'Scheduled';
schedule: ChangeRequestSchedule;
- };
+ }
+);
+
+const StyledTimelineContent = styled(TimelineContent)(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+}));
const StyledPaper = styled(Paper)(({ theme }) => ({
marginTop: theme.spacing(2),
@@ -89,6 +95,7 @@ export const determineColor = (
export const ChangeRequestTimeline: FC = ({
state,
schedule,
+ timestamps,
}) => {
let data: ChangeRequestState[];
switch (state) {
@@ -102,17 +109,24 @@ export const ChangeRequestTimeline: FC = ({
data = steps;
}
const activeIndex = data.findIndex((item) => item === state);
- const { locationSettings } = useLocationSettings();
return (
{data.map((title, index) => {
+ const timestampComponent =
+ index <= activeIndex && timestamps?.[title] ? (
+
+ ) : undefined;
+
if (schedule && title === 'Scheduled') {
- return createTimelineScheduleItem(
- schedule,
- locationSettings,
+ return (
+
);
}
@@ -132,11 +146,17 @@ export const ChangeRequestTimeline: FC = ({
timelineDotProps = { variant: 'outlined' };
}
- return createTimelineItem(
- color,
- title,
- index < data.length - 1,
- timelineDotProps,
+ return (
+
);
})}
@@ -145,30 +165,61 @@ export const ChangeRequestTimeline: FC = ({
);
};
-const createTimelineItem = (
- color: 'primary' | 'success' | 'grey' | 'error' | 'warning',
- title: string,
- shouldConnectToNextItem: boolean,
- timelineDotProps: { [key: string]: string | undefined } = {},
-) => (
-
-
-
- {shouldConnectToNextItem && }
-
- {title}
-
-);
+const Time = styled(({ dateTime, ...props }: { dateTime: string }) => {
+ const { locationSettings } = useLocationSettings();
+ const displayTime = formatDateYMDHM(
+ new Date(dateTime || ''),
+ locationSettings.locale,
+ );
+ return (
+
+ );
+})(({ theme }) => ({
+ color: theme.palette.text.secondary,
+ fontSize: theme.typography.body2.fontSize,
+}));
+
+const TimelineItem = ({
+ color,
+ title,
+ shouldConnectToNextItem,
+ timestamp,
+ timelineDotProps = {},
+}: {
+ color: 'primary' | 'success' | 'grey' | 'error' | 'warning';
+ title: string;
+ shouldConnectToNextItem: boolean;
+ timestamp?: ReactNode;
+ timelineDotProps: { [key: string]: string | undefined };
+}) => {
+ return (
+
+
+
+ {shouldConnectToNextItem && }
+
+
+ {title}
+ {timestamp}
+
+
+ );
+};
+
+export const getScheduleProps = (schedule: ChangeRequestSchedule) => {
+ const Subtitle = ({ prefix }: { prefix: string }) => (
+ <>
+ {prefix}
+ >
+ );
-export const getScheduleProps = (
- schedule: ChangeRequestSchedule,
- formattedTime: string,
-) => {
switch (schedule.status) {
case 'suspended':
return {
title: 'Schedule suspended',
- subtitle: `was ${formattedTime}`,
+ subtitle: ,
color: 'grey' as const,
reason: (
@@ -179,7 +230,7 @@ export const getScheduleProps = (
case 'failed':
return {
title: 'Schedule failed',
- subtitle: `at ${formattedTime}`,
+ subtitle: ,
color: 'error' as const,
reason: (
,
color: 'warning' as const,
reason: null,
};
}
};
-const createTimelineScheduleItem = (
- schedule: ChangeRequestSchedule,
- locationSettings: ILocationSettings,
-) => {
- const time = formatDateYMDHMS(
- new Date(schedule.scheduledAt),
- locationSettings?.locale,
- );
-
- const { title, subtitle, color, reason } = getScheduleProps(schedule, time);
+const TimelineScheduleItem = ({
+ schedule,
+ timestamp,
+}: {
+ schedule: ChangeRequestSchedule;
+ timestamp: ReactNode;
+}) => {
+ const { title, subtitle, color, reason } = getScheduleProps(schedule);
return (
-
+
-
+
{title}
+ {timestamp}
- {`(${subtitle})`}
+
+ ({subtitle})
+
{reason}
-
-
+
+
);
};
diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts
index 3b11075b60..9f2727b714 100644
--- a/frontend/src/component/changeRequest/changeRequest.types.ts
+++ b/frontend/src/component/changeRequest/changeRequest.types.ts
@@ -19,6 +19,7 @@ type BaseChangeRequest = {
rejections: IChangeRequestApproval[];
comments: IChangeRequestComment[];
conflict?: string;
+ stateTransitionTimestamps?: Partial>; // todo(timestampsInChangeRequestTimeline): make sure this matches the model and what we return from the API
};
export type UnscheduledChangeRequest = BaseChangeRequest & {