Skip to content

Commit

Permalink
[BOW-47] Add DurationField instead of seconds number (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
markspolakovs authored Sep 4, 2023
1 parent 0b8c835 commit 34acbf3
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 26 deletions.
15 changes: 12 additions & 3 deletions server/app/shows/[show_id]/rundown/[rundown_id]/RundownItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import {
reorder,
} from "./itemsActions";
import { AddItemSchema, EditItemSchema, ItemTypeSchema } from "./schema";
import { Field, HiddenField, SelectField } from "@/components/FormFields";
import {
DurationField,
Field,
HiddenField,
SelectField,
} from "@/components/FormFields";
import { identity } from "lodash";
import {
useCallback,
Expand Down Expand Up @@ -101,7 +106,11 @@ function AddSegment(props: { rundown: CompleteRundown }) {
getOptionValue={identity}
filter={false}
/>
<Field name="durationSeconds" label="Duration (seconds)" />
<DurationField
name="durationSeconds"
label="Duration"
units="seconds"
/>
</Form>
</PopoverContent>
</Popover>
Expand Down Expand Up @@ -134,7 +143,7 @@ function EditItem(props: {
getOptionValue={identity}
filter={false}
/>
<Field name="durationSeconds" label="Duration (seconds)" />
<DurationField name="durationSeconds" label="Duration" units="seconds" />
</Form>
);
}
Expand Down
113 changes: 94 additions & 19 deletions server/components/FormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import {
} from "react-hook-form";
import { FieldPath } from "react-hook-form/dist/types/path";
import classNames from "classnames";
import { ForwardedRef, forwardRef, useEffect, useMemo, useState } from "react";
import {
ForwardedRef,
ReactNode,
forwardRef,
useEffect,
useMemo,
useState,
} from "react";
import Button from "@bowser/components/button";
import { identity } from "lodash";
import {
Expand All @@ -36,6 +43,26 @@ interface FieldBaseProps<
registerParams?: RegisterOptions<TFields, TFieldName>;
}

function FieldWrapper(props: {
label?: string;
error?: string;
children: ReactNode;
}) {
return (
<label className="block">
{props.label && (
<span className="font-bold text-gray-700">{props.label}</span>
)}
{props.error && (
<span className="block font-semibold text-danger">
{props.error ?? ""}
</span>
)}
{props.children}
</label>
);
}

/**
* A generic form field, tied into the validation system and with some basic styling.
* Pass the `as` prop to render a different element type (e.g. `as="textarea"`), otherwise it will be an input.
Expand Down Expand Up @@ -72,19 +99,16 @@ export const Field = forwardRef(function Field<
}}
/>
);
if (!props.label) {
if (!label) {
return field;
}
return (
<label className="block">
<span className="font-bold text-gray-700">{label}</span>
{ctx.formState.errors[props.name] && (
<span className="block font-semibold text-danger">
{(ctx.formState.errors[props.name]?.message as string) ?? ""}
</span>
)}
<FieldWrapper
label={label}
error={ctx.formState.errors[props.name]?.message as string | undefined}
>
{field}
</label>
</FieldWrapper>
);
});

Expand Down Expand Up @@ -117,19 +141,16 @@ export function DatePickerField(props: {
[controller.field.value],
);
return (
<label className="block">
<span className="font-bold text-gray-700">{props.label}</span>
{controller.fieldState.error && (
<span className="block font-semibold text-danger">
{(controller.fieldState.error?.message as string) ?? ""}
</span>
)}
<FieldWrapper
label={props.label}
error={controller.fieldState.error?.message}
>
<Popover>
<PopoverTrigger asChild>
<Button
color="ghost"
className={cn(
"flex justify-start items-center text-left font-normal block",
"justify-start items-center text-left font-normal block",
!v && "text-muted-foreground",
)}
>
Expand Down Expand Up @@ -163,7 +184,7 @@ export function DatePickerField(props: {
)}
</PopoverContent>
</Popover>
</label>
</FieldWrapper>
);
}

Expand Down Expand Up @@ -296,3 +317,57 @@ export function SelectField<TObj>(props: {
</Field>
);
}

/**
* A field for entering a duration in seconds, with a nice HH:MM:SS format.
* Note that milliseconds are not supported.
* Also note that the only supported value for the `units` prop is "seconds" - this is to allow for future expansion.
*/
export function DurationField(props: {
name: string;
label?: string;
units: "seconds";
}) {
const controller = useController({
name: props.name,
defaultValue: 0,
});
const valueFormatted = useMemo(() => {
const seconds = Math.floor(controller.field.value);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
let value = `${(minutes % 60).toString(10).padStart(2, "0")}:${(
seconds % 60
)
.toString(10)
.padStart(2, "0")}`;
if (hours > 0) {
value = `${hours.toString(10)}:${value}`;
}
return value;
}, [controller.field.value]);
return (
<FieldWrapper
label={props.label}
error={controller.fieldState.error?.message}
>
<Input
type="text"
{...controller.field}
value={valueFormatted}
onChange={(e) => {
let value = 0;
const parts = e.target.value.split(":");
for (let i = 0; i < parts.length; i++) {
if (parts[i].length > 0) {
value +=
parseInt(parts[i], 10) * Math.pow(60, parts.length - i - 1);
}
}
controller.field.onChange(value);
}}
placeholder="xx:xx"
/>
</FieldWrapper>
);
}
8 changes: 4 additions & 4 deletions server/e2e/showItems.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@ test("add rundown items + check runtime", async ({ showPage }) => {

await showPage.getByRole("button", { name: "Add Segment" }).click();
await showPage.getByLabel("Name").fill("Segment 1");
await showPage.getByLabel("Duration (seconds)").fill("60");
await showPage.getByLabel("Duration").fill("60");
const r1 = showPage.waitForRequest((r) => r.method() === "POST");
await showPage.getByRole("button", { name: "Create" }).click();
await r1;
await expect(showPage.getByLabel("Name")).toHaveValue("");

await showPage.getByLabel("Name").fill("Segment 2");
await showPage.getByLabel("Duration (seconds)").fill("60");
await showPage.getByLabel("Duration").fill("60");
const r2 = showPage.waitForRequest((r) => r.method() === "POST");
await showPage.getByRole("button", { name: "Create" }).click();
await r2;
Expand Down Expand Up @@ -197,8 +197,8 @@ test("media/assets for long rundowns", async ({ showPage }) => {
await showPage.getByRole("button", { name: "Add Segment" }).click();
for (let i = 0; i < 25; i++) {
await showPage.getByLabel("Name").fill("Segment " + i);
await showPage.getByLabel("Duration (seconds)").fill("60");
await showPage.getByLabel("Duration (seconds)").press("Enter");
await showPage.getByLabel("Duration").fill("60");
await showPage.getByLabel("Duration").press("Enter");
await expect(showPage.getByLabel("Name")).toHaveValue("");
}

Expand Down

0 comments on commit 34acbf3

Please sign in to comment.