Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit4af0f09

Browse files
fix(site): fix floating number on duration fields (#13209)
1 parentd8bb5a0 commit4af0f09

File tree

8 files changed

+343
-53
lines changed

8 files changed

+343
-53
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
importtype{Meta,StoryObj}from"@storybook/react";
2+
import{expect,within,userEvent}from"@storybook/test";
3+
import{useState}from"react";
4+
import{DurationField}from"./DurationField";
5+
6+
constmeta:Meta<typeofDurationField>={
7+
title:"components/DurationField",
8+
component:DurationField,
9+
args:{
10+
label:"Duration",
11+
},
12+
render:functionRenderComponent(args){
13+
const[value,setValue]=useState<number>(args.valueMs);
14+
return(
15+
<DurationField
16+
{...args}
17+
valueMs={value}
18+
onChange={(value)=>setValue(value)}
19+
/>
20+
);
21+
},
22+
};
23+
24+
exportdefaultmeta;
25+
typeStory=StoryObj<typeofDurationField>;
26+
27+
exportconstHours:Story={
28+
args:{
29+
valueMs:hoursToMs(16),
30+
},
31+
};
32+
33+
exportconstDays:Story={
34+
args:{
35+
valueMs:daysToMs(2),
36+
},
37+
};
38+
39+
exportconstTypeOnlyNumbers:Story={
40+
args:{
41+
valueMs:0,
42+
},
43+
play:async({ canvasElement})=>{
44+
constcanvas=within(canvasElement);
45+
constinput=canvas.getByLabelText("Duration");
46+
awaituserEvent.clear(input);
47+
awaituserEvent.type(input,"abcd_.?/48.0");
48+
awaitexpect(input).toHaveValue("480");
49+
},
50+
};
51+
52+
exportconstChangeUnit:Story={
53+
args:{
54+
valueMs:daysToMs(2),
55+
},
56+
play:async({ canvasElement})=>{
57+
constcanvas=within(canvasElement);
58+
constinput=canvas.getByLabelText("Duration");
59+
constunitDropdown=canvas.getByLabelText("Time unit");
60+
awaituserEvent.click(unitDropdown);
61+
consthoursOption=within(document.body).getByText("Hours");
62+
awaituserEvent.click(hoursOption);
63+
awaitexpect(input).toHaveValue("48");
64+
},
65+
};
66+
67+
exportconstCantConvertToDays:Story={
68+
args:{
69+
valueMs:hoursToMs(2),
70+
},
71+
play:async({ canvasElement})=>{
72+
constcanvas=within(canvasElement);
73+
constunitDropdown=canvas.getByLabelText("Time unit");
74+
awaituserEvent.click(unitDropdown);
75+
constdaysOption=within(document.body).getByText("Days");
76+
awaitexpect(daysOption).toHaveAttribute("aria-disabled","true");
77+
},
78+
};
79+
80+
functionhoursToMs(hours:number):number{
81+
returnhours*60*60*1000;
82+
}
83+
84+
functiondaysToMs(days:number):number{
85+
returndays*24*60*60*1000;
86+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
importKeyboardArrowDownfrom"@mui/icons-material/KeyboardArrowDown";
2+
importFormHelperTextfrom"@mui/material/FormHelperText";
3+
importMenuItemfrom"@mui/material/MenuItem";
4+
importSelectfrom"@mui/material/Select";
5+
importTextField,{typeTextFieldProps}from"@mui/material/TextField";
6+
import{typeFC,useEffect,useReducer}from"react";
7+
import{
8+
typeTimeUnit,
9+
durationInDays,
10+
durationInHours,
11+
suggestedTimeUnit,
12+
}from"utils/time";
13+
14+
typeDurationFieldProps=Omit<TextFieldProps,"value"|"onChange">&{
15+
valueMs:number;
16+
onChange:(value:number)=>void;
17+
};
18+
19+
typeState={
20+
unit:TimeUnit;
21+
// Handling empty values as strings in the input simplifies the process,
22+
// especially when a user clears the input field.
23+
durationFieldValue:string;
24+
};
25+
26+
typeAction=
27+
|{type:"SYNC_WITH_PARENT";parentValueMs:number}
28+
|{type:"CHANGE_DURATION_FIELD_VALUE";fieldValue:string}
29+
|{type:"CHANGE_TIME_UNIT";unit:TimeUnit};
30+
31+
constreducer=(state:State,action:Action):State=>{
32+
switch(action.type){
33+
case"SYNC_WITH_PARENT":{
34+
returninitState(action.parentValueMs);
35+
}
36+
case"CHANGE_DURATION_FIELD_VALUE":{
37+
return{
38+
...state,
39+
durationFieldValue:action.fieldValue,
40+
};
41+
}
42+
case"CHANGE_TIME_UNIT":{
43+
constcurrentDurationMs=durationInMs(
44+
state.durationFieldValue,
45+
state.unit,
46+
);
47+
48+
if(
49+
action.unit==="days"&&
50+
!canConvertDurationToDays(currentDurationMs)
51+
){
52+
returnstate;
53+
}
54+
55+
return{
56+
unit:action.unit,
57+
durationFieldValue:
58+
action.unit==="hours"
59+
?durationInHours(currentDurationMs).toString()
60+
:durationInDays(currentDurationMs).toString(),
61+
};
62+
}
63+
default:{
64+
returnstate;
65+
}
66+
}
67+
};
68+
69+
exportconstDurationField:FC<DurationFieldProps>=(props)=>{
70+
const{
71+
valueMs:parentValueMs,
72+
onChange,
73+
helperText,
74+
...textFieldProps
75+
}=props;
76+
const[state,dispatch]=useReducer(reducer,initState(parentValueMs));
77+
constcurrentDurationMs=durationInMs(state.durationFieldValue,state.unit);
78+
79+
useEffect(()=>{
80+
if(parentValueMs!==currentDurationMs){
81+
dispatch({type:"SYNC_WITH_PARENT", parentValueMs});
82+
}
83+
},[currentDurationMs,parentValueMs]);
84+
85+
return(
86+
<div>
87+
<div
88+
css={{
89+
display:"flex",
90+
gap:8,
91+
}}
92+
>
93+
<TextField
94+
{...textFieldProps}
95+
fullWidth
96+
value={state.durationFieldValue}
97+
onChange={(e)=>{
98+
constdurationFieldValue=intMask(e.currentTarget.value);
99+
100+
dispatch({
101+
type:"CHANGE_DURATION_FIELD_VALUE",
102+
fieldValue:durationFieldValue,
103+
});
104+
105+
constnewDurationInMs=durationInMs(
106+
durationFieldValue,
107+
state.unit,
108+
);
109+
if(newDurationInMs!==parentValueMs){
110+
onChange(newDurationInMs);
111+
}
112+
}}
113+
inputProps={{
114+
step:1,
115+
}}
116+
/>
117+
<Select
118+
disabled={props.disabled}
119+
css={{width:120,"& .MuiSelect-icon":{padding:2}}}
120+
value={state.unit}
121+
onChange={(e)=>{
122+
constunit=e.target.valueasTimeUnit;
123+
dispatch({
124+
type:"CHANGE_TIME_UNIT",
125+
unit,
126+
});
127+
}}
128+
inputProps={{"aria-label":"Time unit"}}
129+
IconComponent={KeyboardArrowDown}
130+
>
131+
<MenuItemvalue="hours">Hours</MenuItem>
132+
<MenuItem
133+
value="days"
134+
disabled={!canConvertDurationToDays(currentDurationMs)}
135+
>
136+
Days
137+
</MenuItem>
138+
</Select>
139+
</div>
140+
141+
{helperText&&(
142+
<FormHelperTexterror={props.error}>{helperText}</FormHelperText>
143+
)}
144+
</div>
145+
);
146+
};
147+
148+
functioninitState(value:number):State{
149+
constunit=suggestedTimeUnit(value);
150+
constdurationFieldValue=
151+
unit==="hours"
152+
?durationInHours(value).toString()
153+
:durationInDays(value).toString();
154+
155+
return{
156+
unit,
157+
durationFieldValue,
158+
};
159+
}
160+
161+
functionintMask(value:string):string{
162+
returnvalue.replace(/\D/g,"");
163+
}
164+
165+
functiondurationInMs(durationFieldValue:string,unit:TimeUnit):number{
166+
constdurationInMs=parseInt(durationFieldValue,10);
167+
168+
if(Number.isNaN(durationInMs)){
169+
return0;
170+
}
171+
172+
returnunit==="hours"
173+
?hoursToDuration(durationInMs)
174+
:daysToDuration(durationInMs);
175+
}
176+
177+
functionhoursToDuration(hours:number):number{
178+
returnhours*60*60*1000;
179+
}
180+
181+
functiondaysToDuration(days:number):number{
182+
returndays*24*hoursToDuration(1);
183+
}
184+
185+
functioncanConvertDurationToDays(duration:number):boolean{
186+
returnNumber.isInteger(durationInDays(duration));
187+
}

‎site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import{humanDuration}from"utils/time";
2+
13
consthours=(h:number)=>(h===1 ?"hour" :"hours");
2-
constdays=(d:number)=>(d===1 ?"day" :"days");
34

45
exportconstDefaultTTLHelperText=(props:{ttl?:number})=>{
56
const{ ttl=0}=props;
@@ -60,7 +61,7 @@ export const FailureTTLHelperText = (props: { ttl?: number }) => {
6061

6162
return(
6263
<span>
63-
Coder will attempt to stop failed workspaces after{ttl}{days(ttl)}.
64+
Coder will attempt to stop failed workspaces after{humanDuration(ttl)}.
6465
</span>
6566
);
6667
};
@@ -79,8 +80,8 @@ export const DormancyTTLHelperText = (props: { ttl?: number }) => {
7980

8081
return(
8182
<span>
82-
Coder will mark workspaces as dormant after{ttl}{days(ttl)} without user
83-
connections.
83+
Coder will mark workspaces as dormant after{humanDuration(ttl)} without
84+
userconnections.
8485
</span>
8586
);
8687
};
@@ -99,8 +100,8 @@ export const DormancyAutoDeletionTTLHelperText = (props: { ttl?: number }) => {
99100

100101
return(
101102
<span>
102-
Coder will automatically delete dormant workspaces after{ttl}{days(ttl)}
103-
.
103+
Coder will automatically delete dormant workspaces after{" "}
104+
{humanDuration(ttl)}.
104105
</span>
105106
);
106107
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp