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

@@ -6,6 +6,7 @@ import Stack from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { FieldLabel } from "./FieldLabel";
import { ChevronDown } from "lucide-react";
import { useTranslation } from "react-i18next";
type AutoCompleteInputProps = Omit<
AutocompleteProps<any, boolean, boolean, boolean>,
@@ -31,12 +32,13 @@ export const AutoCompleteInput = ({
...props
}: AutoCompleteInputProps) => {
const theme = useTheme();
const { t } = useTranslation();
const multiple = props.multiple;
const { renderOption: customRenderOption, ...autocompleteProps } = props;
const defaultRenderInput = (params: any) => (
<TextField
{...params}
placeholder={placeholder ?? "Type to search"}
placeholder={placeholder ?? t("common.form.autocomplete.placeholder")}
error={error}
helperText={helperText}
/>
@@ -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}
/>
)}
/>
);
};
-1
View File
@@ -62,7 +62,6 @@ const LoginPage = () => {
>
<FormTextField
name="email"
type="email"
fieldLabel={t("pages.auth.common.form.option.email.label")}
placeholder={t("pages.auth.common.form.option.email.placeholder")}
/>
-1
View File
@@ -41,7 +41,6 @@ const ForgotPasswordPage = () => {
>
<FormTextField
name="email"
type="email"
fieldLabel={t("pages.auth.common.form.option.email.label")}
placeholder={t("pages.auth.common.form.option.email.placeholder")}
/>
-1
View File
@@ -127,7 +127,6 @@ const RegisterPage = () => {
/>
<FormTextField
name="email"
type="email"
disabled={!!token}
fieldLabel={t("common.form.email.option.email.label")}
placeholder={t("common.form.email.option.email.placeholder")}
+1 -1
View File
@@ -572,7 +572,7 @@ const CreateMonitorPage = () => {
)}
{/* Alert Thresholds - only for hardware type */}
{generalSettingsConfig.showSecret && showStep(1) && (
{watchedType === "hardware" && showStep(1) && (
<ConfigBox
title={t("pages.createMonitor.form.thresholds.title")}
subtitle={t("pages.createMonitor.form.thresholds.description")}
@@ -20,8 +20,10 @@ import { useEffect } from "react";
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
import { FormTextField } from "@/Components/inputs/forms/FormTextField";
import { FormSelectField } from "@/Components/inputs/forms/FormSelectField";
import { FormDatePickerField } from "@/Components/inputs/forms/FormDatePickerField";
import { FormTimePickerField } from "@/Components/inputs/forms/FormTimePickerField";
import {
FormDatePickerField,
FormTimePickerField,
} from "@/Components/inputs/forms/FormDateTimePickerField";
import { FormMultiSelectField } from "@/Components/inputs/forms/FormMultiSelectField";
import { FormNumberField } from "@/Components/inputs/forms/FormNumberField";
+2 -1
View File
@@ -41,6 +41,7 @@ import { CHECK_TTL_SENTINEL } from "@/Types/Check";
import { FormTextField } from "@/Components/inputs/forms/FormTextField";
import { FormSliderField } from "@/Components/inputs/forms/FormSliderField";
import { FormSwitchField } from "@/Components/inputs/forms/FormSwitchField";
import { FormNumberField } from "@/Components/inputs/forms/FormNumberField";
interface Timezone {
id: string;
@@ -596,7 +597,7 @@ export const SettingsPage = () => {
/>
{/* Email Port */}
<FormTextField
<FormNumberField
name="systemEmailPort"
fieldLabel={t("pages.settings.form.email.option.port.label")}
placeholder={t("pages.settings.form.email.option.port.placeholder")}
@@ -311,6 +311,9 @@ const CreateStatusPage = () => {
sortable
name="monitors"
fieldLabel={t("pages.statusPages.form.monitors.option.monitors.label")}
placeholder={t(
"pages.statusPages.form.monitors.option.monitors.placeholder"
)}
options={monitors}
renderRow={(monitor) => (
<Typography flexGrow={1}>
+4 -1
View File
@@ -61,7 +61,7 @@
"importFromJSON": "Import from JSON",
"clearFilters": "Clear filters",
"removeUser": "Remove user",
"remove": "Remove"
"removeItem": "Remove {{item}}"
},
"charts": {
"labels": {
@@ -105,6 +105,9 @@
"responseTime": "Response time"
},
"form": {
"autocomplete": {
"placeholder": "Type to search"
},
"role": {
"option": {
"role": {