1
0
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:
Jaanus Sellin 2025-10-31 15:53:05 +02:00 committed by GitHub
parent 96f7f2f1bf
commit 2458e5d5aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 123 additions and 4 deletions

View File

@ -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>
)}

View File

@ -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>
)}

View File

@ -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 .*\)\.$/);
});
});

View File

@ -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;

View File

@ -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,
);
};