refactor maintenance create page
This commit is contained in:
1 parent
52c80b9a39
commit
0eff052258
3 files changed
+177
-269
No files matched your search
@@ -3,6 +3,7 @@ import type { MobileTimePickerProps } from "@mui/x-date-pickers/MobileTimePicker
|
||||
import type { Dayjs } from "dayjs";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
@@ -10,11 +11,15 @@ import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
interface TimePickerComponentProps extends Omit<MobileTimePickerProps<Dayjs>, "label"> {
|
||||
fieldLabel?: string;
|
||||
required?: boolean;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
export const TimePickerComponent = ({
|
||||
fieldLabel,
|
||||
required,
|
||||
error,
|
||||
helperText,
|
||||
...props
|
||||
}: TimePickerComponentProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -33,12 +38,12 @@ export const TimePickerComponent = ({
|
||||
px: theme.spacing(5),
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.divider,
|
||||
borderColor: error ? theme.palette.error.main : theme.palette.divider,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
|
||||
{
|
||||
borderColor: theme.palette.divider,
|
||||
borderColor: error ? theme.palette.error.main : theme.palette.divider,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -48,14 +53,18 @@ export const TimePickerComponent = ({
|
||||
</LocalizationProvider>
|
||||
);
|
||||
|
||||
if (fieldLabel) {
|
||||
return (
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||
{picker}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return picker;
|
||||
return (
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
{fieldLabel && <FieldLabel required={required}>{fieldLabel}</FieldLabel>}
|
||||
{picker}
|
||||
{helperText && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={error ? "error" : "text.secondary"}
|
||||
>
|
||||
{helperText}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
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,18 +1,8 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { logger } from "@/Utils/logger";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
||||
import {
|
||||
TextField,
|
||||
Select,
|
||||
DatePicker,
|
||||
TimePicker,
|
||||
Button,
|
||||
Autocomplete,
|
||||
} from "@/Components/inputs";
|
||||
import { Button } from "@/Components/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,10 +15,15 @@ import { useGet, usePost, usePatch } from "@/Hooks/UseApi";
|
||||
import { mutate } from "swr";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { useEffect } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
|
||||
import { Trash2 } from "lucide-react";
|
||||
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 { FormMultiSelectField } from "@/Components/inputs/forms/FormMultiSelectField";
|
||||
import { FormNumberField } from "@/Components/inputs/forms/FormNumberField";
|
||||
|
||||
const CreateMaintenanceWindowPage = () => {
|
||||
const theme = useTheme();
|
||||
@@ -57,7 +52,7 @@ const CreateMaintenanceWindowPage = () => {
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
const { control, handleSubmit, trigger } = form;
|
||||
const { handleSubmit, trigger } = form;
|
||||
useEffect(() => {
|
||||
if (existingMaintenanceWindow) {
|
||||
form.reset(defaults);
|
||||
@@ -114,255 +109,116 @@ const CreateMaintenanceWindowPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
component={"form"}
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.general.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.general.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
defaultValue={defaults.name}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.maintenanceWindow.form.general.option.name.label")}
|
||||
placeholder={t(
|
||||
"pages.maintenanceWindow.form.general.option.name.placeholder"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="repeat"
|
||||
control={control}
|
||||
defaultValue={defaults.repeat}
|
||||
render={({ field, fieldState }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.general.option.repeat.label"
|
||||
)}
|
||||
error={!!fieldState.error}
|
||||
onChange={field.onChange}
|
||||
>
|
||||
{repeatOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
>
|
||||
<Typography>{option.name}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.startDate.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.startDate.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
defaultValue={defaults.startDate}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startDate.option.startDate.label"
|
||||
)}
|
||||
value={field.value ? dayjs(field.value, "YYYY-MM-DD") : null}
|
||||
onChange={(date) => {
|
||||
field.onChange(date ? date.format("YYYY-MM-DD") : "");
|
||||
trigger("startDate");
|
||||
}}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.startTime.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.startTime.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<Controller
|
||||
name="startTime"
|
||||
control={control}
|
||||
defaultValue={defaults.startTime}
|
||||
render={({ field }) => (
|
||||
<TimePicker
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startTime.option.startTime.label"
|
||||
)}
|
||||
value={field.value ? dayjs(field.value, "HH:mm") : null}
|
||||
onChange={(time) => {
|
||||
field.onChange(time ? time.format("HH:mm") : "");
|
||||
trigger("startDate");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
<Controller
|
||||
name="duration"
|
||||
control={control}
|
||||
defaultValue={defaults.duration}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="number"
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startTime.option.duration.label"
|
||||
)}
|
||||
value={field.value === 0 ? "" : field.value}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
field.onChange(val === "" ? 0 : Number(val));
|
||||
}}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ width: 120 }}
|
||||
/>
|
||||
<FormProvider {...form}>
|
||||
<BasePage
|
||||
component={"form"}
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.general.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.general.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<FormTextField
|
||||
name="name"
|
||||
fieldLabel={t("pages.maintenanceWindow.form.general.option.name.label")}
|
||||
placeholder={t(
|
||||
"pages.maintenanceWindow.form.general.option.name.placeholder"
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="durationUnit"
|
||||
control={control}
|
||||
defaultValue={defaults.durationUnit}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
fieldLabel=" "
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
sx={{ minWidth: 120 }}
|
||||
>
|
||||
{durationUnitOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
>
|
||||
<Typography>{option.name}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<FormSelectField
|
||||
name="repeat"
|
||||
fieldLabel={t("pages.maintenanceWindow.form.general.option.repeat.label")}
|
||||
options={repeatOptions.map((option) => ({
|
||||
value: option.id,
|
||||
label: option.name,
|
||||
}))}
|
||||
defaultValue={defaults.repeat}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.startTime.monitors.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.startTime.monitors.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="monitors"
|
||||
control={control}
|
||||
defaultValue={defaults.monitors}
|
||||
render={({ field, fieldState }) => {
|
||||
const monitorsList = monitors ?? [];
|
||||
const selectedMonitors = field.value
|
||||
.map((id: string) => monitorsList.find((m) => m.id === id))
|
||||
.filter((m): m is Monitor => m !== undefined);
|
||||
|
||||
return (
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={monitorsList}
|
||||
getOptionLabel={(option: Monitor) => option.name}
|
||||
value={selectedMonitors}
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue.map((m: Monitor) => m.id));
|
||||
}}
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label"
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder={
|
||||
selectedMonitors.length === 0
|
||||
? t(
|
||||
"pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label"
|
||||
)
|
||||
: ""
|
||||
}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{selectedMonitors.length > 0 && (
|
||||
<Stack>
|
||||
{selectedMonitors.map((monitor) => (
|
||||
<Stack
|
||||
key={monitor.id}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
padding={theme.spacing(LAYOUT.XS)}
|
||||
marginTop={theme.spacing(SPACING.LG)}
|
||||
borderRadius={1}
|
||||
sx={{
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
field.onChange(
|
||||
field.value.filter((id: string) => id !== monitor.id)
|
||||
);
|
||||
}}
|
||||
aria-label="Remove monitor"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.startDate.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.startDate.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<FormDatePickerField
|
||||
name="startDate"
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startDate.option.startDate.label"
|
||||
)}
|
||||
onValueChange={() => trigger("startDate")}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.startTime.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.startTime.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
<FormTimePickerField
|
||||
name="startTime"
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startTime.option.startTime.label"
|
||||
)}
|
||||
onValueChange={() => trigger("startDate")}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
<FormNumberField
|
||||
name="duration"
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startTime.option.duration.label"
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
spacing={theme.spacing(SPACING.LG)}
|
||||
>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ width: 120 }}
|
||||
/>
|
||||
<FormSelectField
|
||||
name="durationUnit"
|
||||
fieldLabel={" "}
|
||||
defaultValue={defaults.durationUnit}
|
||||
options={durationUnitOptions.map((option) => ({
|
||||
value: option.id,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.startTime.monitors.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.startTime.monitors.description")}
|
||||
rightContent={
|
||||
<FormMultiSelectField
|
||||
name="monitors"
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label"
|
||||
)}
|
||||
options={monitors ?? []}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
spacing={theme.spacing(SPACING.LG)}
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user