Skip to content

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
50 changes: 50 additions & 0 deletions site/src/components/DurationField/DurationField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from "@storybook/react";
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 | undefined>(args.value);
return (
<DurationField
{...args}
value={value}
onChange={(value) => setValue(value)}
/>
);
},
};

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

export const Empty: Story = {
args: {
value: undefined,
},
};

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

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

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

function daysToMs(days: number): number {
return days * 24 * 60 * 60 * 1000;
}
118 changes: 118 additions & 0 deletions site/src/components/DurationField/DurationField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import TextField from "@mui/material/TextField";
import { useState, type FC } from "react";

type TimeUnit = "days" | "hours";

// Value should be in milliseconds or undefined. Undefined means no value.
type DurationValue = number | undefined;

type DurationFieldProps = {
label: string;
value: DurationValue;
onChange: (value: DurationValue) => void;
};

export const DurationField: FC<DurationFieldProps> = (props) => {
const { label, value, onChange } = props;
const [timeUnit, setTimeUnit] = useState<TimeUnit>(() => {
if (!value) {
return "hours";
}

return Number.isInteger(durationToDays(value)) ? "days" : "hours";
});

return (
<div
css={{
display: "flex",
gap: 8,
}}
>
<TextField
css={{ maxWidth: 160 }}
label={label}
value={
!value
? ""
: timeUnit === "hours"
? durationToHours(value)
: durationToDays(value)
}
onChange={(e) => {
if (e.target.value === "") {
onChange(undefined);
}

let value = parseInt(e.target.value);

if (Number.isNaN(value)) {
return;
}

// Avoid negative values
value = Math.abs(value);

onChange(
timeUnit === "hours"
? hoursToDuration(value)
: daysToDuration(value),
);
}}
inputProps={{
step: 1,
type: "number",
}}
/>
<Select
css={{ width: 120, "& .MuiSelect-icon": { padding: 2 } }}
value={timeUnit}
onChange={(e) => {
setTimeUnit(e.target.value as TimeUnit);
}}
inputProps={{ "aria-label": "Time unit" }}
IconComponent={KeyboardArrowDown}
>
<MenuItem
value="hours"
disabled={Boolean(value && !canConvertDurationToHours(value))}
>
Hours
</MenuItem>
<MenuItem
value="days"
disabled={Boolean(value && !canConvertDurationToDays(value))}
>
Days
</MenuItem>
</Select>
</div>
);
};

function durationToHours(duration: number): number {
return duration / 1000 / 60 / 60;
}

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

function durationToDays(duration: number): number {
return duration / 1000 / 60 / 60 / 24;
}

function daysToDuration(days: number): number {
return days * 24 * 60 * 60 * 1000;
}

function canConvertDurationToDays(duration: number): boolean {
return Number.isInteger(durationToDays(duration));
}

function canConvertDurationToHours(duration: number): boolean {
return Number.isInteger(durationToHours(duration));
}
Loading