Skip to content
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

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

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.
Jump to
Jump to file
Failed to load files.
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)}
/>
);
},
BrunoQuaresma marked this conversation as resolved.
Show resolved Hide resolved
};

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),
},
};
BrunoQuaresma marked this conversation as resolved.
Show resolved Hide resolved

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";
});
BrunoQuaresma marked this conversation as resolved.
Show resolved Hide resolved

return (
<div
css={{
display: "flex",
gap: 8,
}}
>
<TextField
css={{ maxWidth: 160 }}
label={label}
value={
!value
? ""
: timeUnit === "hours"
? durationToHours(value)
: durationToDays(value)
}
BrunoQuaresma marked this conversation as resolved.
Show resolved Hide resolved
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))}
BrunoQuaresma marked this conversation as resolved.
Show resolved Hide resolved
>
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));
}
BrunoQuaresma marked this conversation as resolved.
Show resolved Hide resolved