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

fix(site): fix floating number on duration fields#13209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
BrunoQuaresma merged 18 commits intomainfrombq/fix-float-input
May 17, 2024
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
18 commits
Select commitHold shift + click to select a range
6d359fd
Add DurationField component
BrunoQuaresmaMay 8, 2024
5ac0602
Add empty story
BrunoQuaresmaMay 8, 2024
95e4f44
Avoid negative values
BrunoQuaresmaMay 8, 2024
4a48102
Use duration field time_til_dormant_ms
BrunoQuaresmaMay 9, 2024
09ddb8f
Fix parent updates
BrunoQuaresmaMay 9, 2024
3427160
Fix helper text
BrunoQuaresmaMay 9, 2024
eef159c
Support errors
BrunoQuaresmaMay 9, 2024
b3042c6
Use valueMs to make the value is in miliseconds
BrunoQuaresmaMay 10, 2024
e6101cf
Replace useState by useReducer
BrunoQuaresmaMay 10, 2024
c82fc8f
Add 10 to base int
BrunoQuaresmaMay 10, 2024
1b77488
Merge branch 'main' of https://github.com/coder/coder into bq/fix-flo…
BrunoQuaresmaMay 13, 2024
9036465
Use a number mask
BrunoQuaresmaMay 13, 2024
cac3a89
Make number input full width
BrunoQuaresmaMay 13, 2024
d89ec94
Add duration field to auto deletion
BrunoQuaresmaMay 13, 2024
0a93c9f
Apply duration field to failure clean up
BrunoQuaresmaMay 13, 2024
5a35e1a
Merge branch 'main' of https://github.com/coder/coder into bq/fix-flo…
BrunoQuaresmaMay 17, 2024
45a5eb5
Add extra tests to storybook
BrunoQuaresmaMay 17, 2024
a6ff426
Fix tests
BrunoQuaresmaMay 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletionssite/src/components/DurationField/DurationField.stories.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, within, userEvent } from "@storybook/test";
import { useState } from "react";
import { DurationField } from "./DurationField";

const meta: Meta<typeof DurationField> = {
title: "components/DurationField",
component: DurationField,
args: {
label: "Duration",
},
render: function RenderComponent(args) {
const [value, setValue] = useState<number>(args.valueMs);
return (
<DurationField
{...args}
valueMs={value}
onChange={(value) => setValue(value)}
/>
);
},
};

export default meta;
type Story = StoryObj<typeof DurationField>;

export const Hours: Story = {
args: {
valueMs: hoursToMs(16),
},
};

export const Days: Story = {
args: {
valueMs: daysToMs(2),
},
};

export const TypeOnlyNumbers: Story = {
args: {
valueMs: 0,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Duration");
await userEvent.clear(input);
await userEvent.type(input, "abcd_.?/48.0");
await expect(input).toHaveValue("480");
},
};

export const ChangeUnit: Story = {
args: {
valueMs: daysToMs(2),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Duration");
const unitDropdown = canvas.getByLabelText("Time unit");
await userEvent.click(unitDropdown);
const hoursOption = within(document.body).getByText("Hours");
await userEvent.click(hoursOption);
await expect(input).toHaveValue("48");
},
};

export const CantConvertToDays: Story = {
args: {
valueMs: hoursToMs(2),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const unitDropdown = canvas.getByLabelText("Time unit");
await userEvent.click(unitDropdown);
const daysOption = within(document.body).getByText("Days");
await expect(daysOption).toHaveAttribute("aria-disabled", "true");
},
};

function hoursToMs(hours: number): number {
return hours * 60 * 60 * 1000;
}

function daysToMs(days: number): number {
return days * 24 * 60 * 60 * 1000;
}
187 changes: 187 additions & 0 deletionssite/src/components/DurationField/DurationField.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown";
import FormHelperText from "@mui/material/FormHelperText";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import TextField, { type TextFieldProps } from "@mui/material/TextField";
import { type FC, useEffect, useReducer } from "react";
import {
type TimeUnit,
durationInDays,
durationInHours,
suggestedTimeUnit,
} from "utils/time";

type DurationFieldProps = Omit<TextFieldProps, "value" | "onChange"> & {
valueMs: number;
onChange: (value: number) => void;
};

type State = {
unit: TimeUnit;
// Handling empty values as strings in the input simplifies the process,
// especially when a user clears the input field.
durationFieldValue: string;
};

type Action =
| { type: "SYNC_WITH_PARENT"; parentValueMs: number }
| { type: "CHANGE_DURATION_FIELD_VALUE"; fieldValue: string }
| { type: "CHANGE_TIME_UNIT"; unit: TimeUnit };

const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SYNC_WITH_PARENT": {
return initState(action.parentValueMs);
}
case "CHANGE_DURATION_FIELD_VALUE": {
return {
...state,
durationFieldValue: action.fieldValue,
};
}
case "CHANGE_TIME_UNIT": {
const currentDurationMs = durationInMs(
state.durationFieldValue,
state.unit,
);

if (
action.unit === "days" &&
!canConvertDurationToDays(currentDurationMs)
) {
return state;
}
Comment on lines +48 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Just making sure: this is just a precaution/double-bookkeeping, right? Even though the UI has thedisabled check to prevent the days unit from being selected at certain points, the reducer is also enforcing that the state update can't go through, in case the UI is set up wrong?

Copy link
CollaboratorAuthor

@BrunoQuaresmaBrunoQuaresmaMay 17, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yes, I think it can be useful to have it in the reducer + disabled attribute. Wdyt?


return {
unit: action.unit,
durationFieldValue:
action.unit === "hours"
? durationInHours(currentDurationMs).toString()
: durationInDays(currentDurationMs).toString(),
};
}
default: {
return state;
}
}
};

export const DurationField: FC<DurationFieldProps> = (props) => {
const {
valueMs: parentValueMs,
onChange,
helperText,
...textFieldProps
} = props;
const [state, dispatch] = useReducer(reducer, initState(parentValueMs));
const currentDurationMs = durationInMs(state.durationFieldValue, state.unit);

useEffect(() => {
if (parentValueMs !== currentDurationMs) {
dispatch({ type: "SYNC_WITH_PARENT", parentValueMs });
}
}, [currentDurationMs, parentValueMs]);

return (
<div>
<div
css={{
display: "flex",
gap: 8,
}}
>
<TextField
{...textFieldProps}
fullWidth
value={state.durationFieldValue}
onChange={(e) => {
const durationFieldValue = intMask(e.currentTarget.value);

dispatch({
type: "CHANGE_DURATION_FIELD_VALUE",
fieldValue: durationFieldValue,
});

const newDurationInMs = durationInMs(
durationFieldValue,
state.unit,
);
if (newDurationInMs !== parentValueMs) {
onChange(newDurationInMs);
}
}}
inputProps={{
step: 1,
}}
/>
<Select
disabled={props.disabled}
css={{ width: 120, "& .MuiSelect-icon": { padding: 2 } }}
value={state.unit}
onChange={(e) => {
const unit = e.target.value as TimeUnit;
dispatch({
type: "CHANGE_TIME_UNIT",
unit,
});
}}
inputProps={{ "aria-label": "Time unit" }}
IconComponent={KeyboardArrowDown}
>
<MenuItem value="hours">Hours</MenuItem>
<MenuItem
value="days"
disabled={!canConvertDurationToDays(currentDurationMs)}
>
Days
</MenuItem>
</Select>
</div>

{helperText && (
<FormHelperText error={props.error}>{helperText}</FormHelperText>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Was there a reason whyprops.error wasn't destructured from the props at the top of the component?

Copy link
CollaboratorAuthor

@BrunoQuaresmaBrunoQuaresmaMay 17, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Because it is used by the TextField props as well.

)}
</div>
);
};

function initState(value: number): State {
const unit = suggestedTimeUnit(value);
const durationFieldValue =
unit === "hours"
? durationInHours(value).toString()
: durationInDays(value).toString();

return {
unit,
durationFieldValue,
};
}

function intMask(value: string): string {
return value.replace(/\D/g, "");
}

function durationInMs(durationFieldValue: string, unit: TimeUnit): number {
const durationInMs = parseInt(durationFieldValue, 10);

if (Number.isNaN(durationInMs)) {
return 0;
}

return unit === "hours"
? hoursToDuration(durationInMs)
: daysToDuration(durationInMs);
}

function hoursToDuration(hours: number): number {
return hours * 60 * 60 * 1000;
}

function daysToDuration(days: number): number {
return days * 24 * hoursToDuration(1);
}

function canConvertDurationToDays(duration: number): boolean {
return Number.isInteger(durationInDays(duration));
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { humanDuration } from "utils/time";

const hours = (h: number) => (h === 1 ? "hour" : "hours");
const days = (d: number) => (d === 1 ? "day" : "days");

export const DefaultTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
Expand DownExpand Up@@ -60,7 +61,7 @@ export const FailureTTLHelperText = (props: { ttl?: number }) => {

return (
<span>
Coder will attempt to stop failed workspaces after {ttl} {days(ttl)}.
Coder will attempt to stop failed workspaces after {humanDuration(ttl)}.
</span>
);
};
Expand All@@ -79,8 +80,8 @@ export const DormancyTTLHelperText = (props: { ttl?: number }) => {

return (
<span>
Coder will mark workspaces as dormant after {ttl} {days(ttl)} without user
connections.
Coder will mark workspaces as dormant after {humanDuration(ttl)} without
userconnections.
</span>
);
};
Expand All@@ -99,8 +100,8 @@ export const DormancyAutoDeletionTTLHelperText = (props: { ttl?: number }) => {

return (
<span>
Coder will automatically delete dormant workspaces after {ttl} {days(ttl)}
.
Coder will automatically delete dormant workspaces after{" "}
{humanDuration(ttl)}.
</span>
);
};
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp