mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
feat: make milestone progression more clear (#10899)
This commit is contained in:
parent
96f7f2f1bf
commit
2458e5d5aa
@ -4,6 +4,7 @@ import { useMilestoneProgressionForm } from '../hooks/useMilestoneProgressionFor
|
||||
import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx';
|
||||
import type { ChangeMilestoneProgressionSchema } from 'openapi';
|
||||
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
||||
import { useMilestoneProgressionInfo } from '../hooks/useMilestoneProgressionInfo.ts';
|
||||
|
||||
const StyledFormContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -54,6 +55,13 @@ const StyledErrorMessage = styled('span')(({ theme }) => ({
|
||||
paddingLeft: theme.spacing(3.25),
|
||||
}));
|
||||
|
||||
const StyledInfoLine = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.typography.caption.fontSize,
|
||||
paddingLeft: theme.spacing(3.25),
|
||||
fontStyle: 'italic',
|
||||
}));
|
||||
|
||||
interface IMilestoneProgressionFormProps {
|
||||
sourceMilestoneId: string;
|
||||
targetMilestoneId: string;
|
||||
@ -81,6 +89,12 @@ export const MilestoneProgressionForm = ({
|
||||
status,
|
||||
);
|
||||
|
||||
const progressionInfo = useMilestoneProgressionInfo(
|
||||
form.getIntervalMinutes(),
|
||||
sourceMilestoneStartedAt,
|
||||
status,
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.validate()) {
|
||||
return;
|
||||
@ -103,14 +117,18 @@ export const MilestoneProgressionForm = ({
|
||||
<StyledFormContainer onKeyDown={handleKeyDown}>
|
||||
<StyledTopRow>
|
||||
<StyledIcon />
|
||||
<StyledLabel>Proceed to the next milestone after</StyledLabel>
|
||||
<StyledLabel>Proceed after</StyledLabel>
|
||||
<MilestoneProgressionTimeInput
|
||||
timeValue={form.timeValue}
|
||||
timeUnit={form.timeUnit}
|
||||
onTimeValueChange={form.handleTimeValueChange}
|
||||
onTimeUnitChange={form.handleTimeUnitChange}
|
||||
/>
|
||||
<StyledLabel>from milestone start</StyledLabel>
|
||||
</StyledTopRow>
|
||||
{progressionInfo && (
|
||||
<StyledInfoLine>{progressionInfo}</StyledInfoLine>
|
||||
)}
|
||||
{form.errors.time && (
|
||||
<StyledErrorMessage>{form.errors.time}</StyledErrorMessage>
|
||||
)}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
import type { ChangeMilestoneProgressionSchema } from 'openapi';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useMilestoneProgressionInfo } from '../hooks/useMilestoneProgressionInfo.ts';
|
||||
|
||||
const StyledFormWrapper = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'hasChanged',
|
||||
@ -97,6 +98,13 @@ const StyledErrorMessage = styled('span')(({ theme }) => ({
|
||||
paddingLeft: theme.spacing(3.25),
|
||||
}));
|
||||
|
||||
const StyledInfoLine = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.typography.caption.fontSize,
|
||||
paddingLeft: theme.spacing(3.25),
|
||||
fontStyle: 'italic',
|
||||
}));
|
||||
|
||||
interface IMilestoneTransitionDisplayProps {
|
||||
intervalMinutes: number;
|
||||
targetMilestoneId: string;
|
||||
@ -129,6 +137,7 @@ export const ReadonlyMilestoneTransitionDisplay = ({
|
||||
<span style={{ fontSize: 'inherit' }}>
|
||||
{initial.value} {initial.unit}
|
||||
</span>
|
||||
<StyledLabel status={status}>from milestone start</StyledLabel>
|
||||
</StyledContentGroup>
|
||||
</StyledDisplayContainer>
|
||||
);
|
||||
@ -159,6 +168,12 @@ export const MilestoneTransitionDisplay = ({
|
||||
const currentIntervalMinutes = form.getIntervalMinutes();
|
||||
const hasChanged = currentIntervalMinutes !== intervalMinutes;
|
||||
|
||||
const progressionInfo = useMilestoneProgressionInfo(
|
||||
currentIntervalMinutes,
|
||||
sourceMilestoneStartedAt ?? null,
|
||||
status,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newInitial = getTimeValueAndUnitFromMinutes(intervalMinutes);
|
||||
form.setTimeValue(newInitial.value);
|
||||
@ -214,15 +229,16 @@ export const MilestoneTransitionDisplay = ({
|
||||
<StyledDisplayContainer>
|
||||
<StyledContentGroup>
|
||||
<StyledIcon status={status} />
|
||||
<StyledLabel status={status}>
|
||||
Proceed to the next milestone after
|
||||
</StyledLabel>
|
||||
<StyledLabel status={status}>Proceed after</StyledLabel>
|
||||
<MilestoneProgressionTimeInput
|
||||
timeValue={form.timeValue}
|
||||
timeUnit={form.timeUnit}
|
||||
onTimeValueChange={form.handleTimeValueChange}
|
||||
onTimeUnitChange={form.handleTimeUnitChange}
|
||||
/>
|
||||
<StyledLabel status={status}>
|
||||
from milestone start
|
||||
</StyledLabel>
|
||||
</StyledContentGroup>
|
||||
{!hasChanged && (
|
||||
<StyledButtonGroup hasChanged={false}>
|
||||
@ -238,6 +254,9 @@ export const MilestoneTransitionDisplay = ({
|
||||
</StyledButtonGroup>
|
||||
)}
|
||||
</StyledDisplayContainer>
|
||||
{progressionInfo && (
|
||||
<StyledInfoLine>{progressionInfo}</StyledInfoLine>
|
||||
)}
|
||||
{form.errors.time && (
|
||||
<StyledErrorMessage>{form.errors.time}</StyledErrorMessage>
|
||||
)}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getMilestoneProgressionInfo } from './getMilestoneProgressionInfo.js';
|
||||
|
||||
describe('getMilestoneProgressionInfo', () => {
|
||||
const currentTime = new Date('2025-10-31T15:00:00.000Z');
|
||||
|
||||
it('returns immediate proceed message when elapsed >= interval', () => {
|
||||
const startedAt = '2025-10-31T14:00:00.000Z';
|
||||
const res = getMilestoneProgressionInfo(
|
||||
30,
|
||||
startedAt,
|
||||
'en-US',
|
||||
currentTime,
|
||||
);
|
||||
expect(res).toBeTruthy();
|
||||
expect(res as string).toMatch(/^Already .* in this milestone\.$/);
|
||||
});
|
||||
|
||||
it('returns proceed time and remaining message when elapsed < interval', () => {
|
||||
const startedAt = '2025-10-31T14:00:00.000Z';
|
||||
const res = getMilestoneProgressionInfo(
|
||||
120,
|
||||
startedAt,
|
||||
'en-US',
|
||||
currentTime,
|
||||
);
|
||||
expect(res).toBeTruthy();
|
||||
expect(res as string).toMatch(/^Will proceed at .* \(in .*\)\.$/);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import { addMinutes, differenceInMinutes, formatDistance } from 'date-fns';
|
||||
import { formatDateYMDHM } from 'utils/formatDate.ts';
|
||||
|
||||
export const getMilestoneProgressionInfo = (
|
||||
intervalMinutes: number,
|
||||
sourceMilestoneStartedAt: string | null | undefined,
|
||||
locale: string,
|
||||
currentTime: Date = new Date(),
|
||||
): string | null => {
|
||||
if (!sourceMilestoneStartedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startDate = new Date(sourceMilestoneStartedAt);
|
||||
const elapsedMinutes = differenceInMinutes(currentTime, startDate);
|
||||
const proceedDate = addMinutes(startDate, intervalMinutes);
|
||||
|
||||
if (elapsedMinutes >= intervalMinutes) {
|
||||
const elapsedTime = formatDistance(startDate, currentTime, {
|
||||
addSuffix: false,
|
||||
});
|
||||
return `Already ${elapsedTime} in this milestone.`;
|
||||
}
|
||||
|
||||
const proceedTime = formatDateYMDHM(proceedDate, locale);
|
||||
const remainingTime = formatDistance(proceedDate, currentTime, {
|
||||
addSuffix: false,
|
||||
});
|
||||
return `Will proceed at ${proceedTime} (in ${remainingTime}).`;
|
||||
};
|
||||
|
||||
export default getMilestoneProgressionInfo;
|
||||
@ -0,0 +1,20 @@
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { getMilestoneProgressionInfo } from './getMilestoneProgressionInfo.ts';
|
||||
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
||||
|
||||
export const useMilestoneProgressionInfo = (
|
||||
intervalMinutes: number,
|
||||
sourceMilestoneStartedAt?: string | null,
|
||||
status?: MilestoneStatus,
|
||||
) => {
|
||||
const { locationSettings } = useLocationSettings();
|
||||
if (!status || status.type !== 'active') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getMilestoneProgressionInfo(
|
||||
intervalMinutes,
|
||||
sourceMilestoneStartedAt,
|
||||
locationSettings.locale,
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user