bug fixes

This commit is contained in:
Alex Holliday committed 2026-08-07 11:50:47 -07:00
1 parent d0c8da1653
commit 2765ebfc3b
15 files changed
+122 -104

No files matched your search

@@ -20,12 +20,14 @@ export const FormColorField = <T extends FieldValues>({
<Controller
name={name}
control={control}
render={({ field }) => (
render={({ field, fieldState }) => (
<ColorInput
format="hex"
value={field.value}
onChange={field.onChange}
fieldLabel={fieldLabel}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
@@ -1,43 +0,0 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import dayjs from "dayjs";
import { DatePicker } from "@/Components/inputs";
interface FormDatePickerFieldProps<T extends FieldValues> {
name: FieldPath<T>;
fieldLabel?: string;
format?: string;
onValueChange?: (value: string) => void; // e.g. () => trigger("startDate")
}
export const FormDatePickerField = <T extends FieldValues>({
name,
fieldLabel,
format = "YYYY-MM-DD",
onValueChange,
}: FormDatePickerFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<DatePicker
fieldLabel={fieldLabel}
value={field.value ? dayjs(field.value, format) : null}
onChange={(date) => {
const next = date ? date.format(format) : "";
field.onChange(next);
onValueChange?.(next);
}}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
);
};
@@ -0,0 +1,81 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import dayjs, { type Dayjs } from "dayjs";
import { DatePicker, TimePicker } from "@/Components/inputs";
export interface FormPickerFieldProps<T extends FieldValues> {
name: FieldPath<T>;
fieldLabel?: string;
format?: string;
onValueChange?: (value: string) => void; // e.g. () => trigger("startDate")
}
interface DayjsPickerProps {
fieldLabel?: string;
value: Dayjs | null;
onChange: (value: Dayjs | null) => void;
error?: boolean;
helperText?: string;
}
interface FormDayjsPickerFieldProps<
T extends FieldValues,
> extends FormPickerFieldProps<T> {
picker: React.ComponentType<DayjsPickerProps>;
format: string;
}
const FormDayjsPickerField = <T extends FieldValues>({
name,
fieldLabel,
format,
onValueChange,
picker: Picker,
}: FormDayjsPickerFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<Picker
fieldLabel={fieldLabel}
value={field.value ? dayjs(field.value, format) : null}
onChange={(value) => {
const next = value ? value.format(format) : "";
field.onChange(next);
onValueChange?.(next);
}}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
);
};
export const FormDatePickerField = <T extends FieldValues>({
format = "YYYY-MM-DD",
...rest
}: FormPickerFieldProps<T>) => (
<FormDayjsPickerField
{...rest}
format={format}
picker={DatePicker}
/>
);
export const FormTimePickerField = <T extends FieldValues>({
format = "HH:mm",
...rest
}: FormPickerFieldProps<T>) => (
<FormDayjsPickerField
{...rest}
format={format}
picker={TimePicker}
/>
);
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import {
Controller,
useFormContext,
@@ -25,6 +26,7 @@ interface FormMultiSelectFieldProps<T extends FieldValues, O extends MultiSelect
name: FieldPath<T>;
options: O[];
fieldLabel?: string;
placeholder?: string;
description?: string;
renderRow?: (option: O) => React.ReactNode;
renderOptionContent?: (option: O) => React.ReactNode;
@@ -35,6 +37,7 @@ export const FormMultiSelectField = <T extends FieldValues, O extends MultiSelec
name,
options,
fieldLabel,
placeholder,
description,
renderRow,
renderOptionContent,
@@ -43,6 +46,10 @@ export const FormMultiSelectField = <T extends FieldValues, O extends MultiSelec
const { control } = useFormContext<T>();
const theme = useTheme();
const { t } = useTranslation();
const optionsById = useMemo(
() => new Map<O["id"], O>(options.map((o) => [o.id, o])),
[options]
);
return (
<Controller
name={name}
@@ -50,15 +57,19 @@ export const FormMultiSelectField = <T extends FieldValues, O extends MultiSelec
render={({ field, fieldState }) => {
const selectedIds: O["id"][] = field.value ?? [];
const selected = selectedIds
.map((id) => options.find((o) => o.id === id))
.map((id) => optionsById.get(id))
.filter((o): o is O => o !== undefined);
// Drag indexes refer to the rendered (resolved) list, not the raw id
// array — ids missing from options are filtered out above, so reorder
// the rendered ids and re-append unresolved ids at the end.
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const reordered = Array.from(selectedIds);
const [removed] = reordered.splice(result.source.index, 1);
reordered.splice(result.destination.index, 0, removed);
field.onChange(reordered);
const reordered = selected.map((o) => o.id);
const [moved] = reordered.splice(result.source.index, 1);
reordered.splice(result.destination.index, 0, moved);
const unresolved = selectedIds.filter((id) => !optionsById.has(id));
field.onChange([...reordered, ...unresolved]);
};
const rowContent = (option: O) =>
@@ -74,7 +85,7 @@ export const FormMultiSelectField = <T extends FieldValues, O extends MultiSelec
onClick={() => {
field.onChange(selectedIds.filter((id) => id !== option.id));
}}
aria-label={t("common.buttons.remove")}
aria-label={t("common.buttons.removeItem", { item: option.name })}
>
<Trash2 size={16} />
</IconButton>
@@ -93,6 +104,7 @@ export const FormMultiSelectField = <T extends FieldValues, O extends MultiSelec
field.onChange(newValue.map((o) => o.id));
}}
fieldLabel={fieldLabel}
placeholder={placeholder}
renderOptionContent={renderOptionContent}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
@@ -9,9 +9,10 @@ import { TextField } from "@/Components/inputs";
interface FormTextFieldProps<T extends FieldValues> extends Omit<
React.ComponentProps<typeof TextField>,
"name" | "error"
"name" | "error" | "helperText"
> {
name: FieldPath<T>;
helperText?: string;
}
export const FormTextField = <T extends FieldValues>({
@@ -27,11 +28,11 @@ export const FormTextField = <T extends FieldValues>({
<TextField
{...field}
type="text"
{...rest}
value={field.value ?? ""}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? rest.helperText ?? ""}
fullWidth
{...rest}
/>
)}
/>
@@ -1,43 +0,0 @@
import {
Controller,
useFormContext,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import dayjs from "dayjs";
import { TimePicker } from "@/Components/inputs";
interface FormTimePickerFieldProps<T extends FieldValues> {
name: FieldPath<T>;
fieldLabel?: string;
format?: string;
onValueChange?: (value: string) => void; // e.g. () => trigger("startDate")
}
export const FormTimePickerField = <T extends FieldValues>({
name,
fieldLabel,
format = "HH:mm",
onValueChange,
}: FormTimePickerFieldProps<T>) => {
const { control } = useFormContext<T>();
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<TimePicker
fieldLabel={fieldLabel}
value={field.value ? dayjs(field.value, format) : null}
onChange={(time) => {
const next = time ? time.format(format) : "";
field.onChange(next);
onValueChange?.(next);
}}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
);
};