seahaven-ap/src/settings/SettingsFormShell.tsx
renovate[bot] a2cbcfb7e8
fix(deps): update dependency @mui/material to v9 (#53)
* fix(deps): update dependency @mui/material to v9

* fix(deps): couple @mui/icons-material v9 and migrate system props

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-08-25 20:26:42 +00:00

206 lines
4.9 KiB
TypeScript

import {
Alert,
Box,
Button,
CircularProgress,
MenuItem,
Snackbar,
Stack,
TextField,
Typography,
} from "@mui/material";
import { useState, type ReactNode } from "react";
export type SettingsFormField =
| {
name: string;
label: string;
type?: "text" | "email" | "number";
required?: boolean;
}
| {
name: string;
label: string;
type: "select";
required?: boolean;
options: Array<{ value: string; label: string }>;
};
type SettingsFormShellProps = {
title: string;
testId: string;
fields: SettingsFormField[];
initialValues: Record<string, string>;
formKey?: string;
isLoading?: boolean;
isError?: boolean;
errorMessage?: string;
notFound?: boolean;
onCancel: () => void;
onSave: (values: Record<string, string>) => Promise<void>;
extra?: ReactNode;
};
type SettingsFormFieldsProps = {
title: string;
testId: string;
fields: SettingsFormField[];
initialValues: Record<string, string>;
onCancel: () => void;
onSave: (values: Record<string, string>) => Promise<void>;
extra?: ReactNode;
};
function SettingsFormFields({
title,
testId,
fields,
initialValues,
onCancel,
onSave,
extra,
}: SettingsFormFieldsProps) {
const [values, setValues] = useState(initialValues);
const [saving, setSaving] = useState(false);
const [snackbar, setSnackbar] = useState<string | null>(null);
const [validationError, setValidationError] = useState<string | null>(null);
const handleSave = async () => {
for (const field of fields) {
if (field.required !== false && !values[field.name]?.trim()) {
setValidationError(`${field.label} is required.`);
return;
}
}
setValidationError(null);
setSaving(true);
try {
await onSave(values);
setSnackbar("Saved to mock store");
} catch (error) {
setValidationError(error instanceof Error ? error.message : "Save failed");
} finally {
setSaving(false);
}
};
return (
<Stack spacing={3} data-testid={testId}>
<Typography component="h1" variant="h1">
{title}
</Typography>
{validationError ? <Alert severity="error">{validationError}</Alert> : null}
<Stack
spacing={2}
sx={{
maxWidth: 480,
}}
>
{fields.map((field) => {
if (field.type === "select") {
return (
<TextField
key={field.name}
select
label={field.label}
value={values[field.name] ?? ""}
onChange={(event) =>
setValues((current) => ({ ...current, [field.name]: event.target.value }))
}
required={field.required !== false}
data-testid={`${testId}-field-${field.name}`}
>
{field.options.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
);
}
return (
<TextField
key={field.name}
label={field.label}
type={field.type ?? "text"}
value={values[field.name] ?? ""}
onChange={(event) =>
setValues((current) => ({ ...current, [field.name]: event.target.value }))
}
required={field.required !== false}
data-testid={`${testId}-field-${field.name}`}
/>
);
})}
{extra}
</Stack>
<Stack direction="row" spacing={1}>
<Button variant="contained" onClick={() => void handleSave()} disabled={saving}>
Save
</Button>
<Button variant="outlined" onClick={onCancel} disabled={saving}>
Cancel
</Button>
</Stack>
<Snackbar
open={Boolean(snackbar)}
autoHideDuration={2500}
onClose={() => setSnackbar(null)}
message={snackbar}
data-testid={`${testId}-snackbar`}
/>
</Stack>
);
}
export function SettingsFormShell({
title,
testId,
fields,
initialValues,
formKey = "new",
isLoading = false,
isError = false,
errorMessage,
notFound = false,
onCancel,
onSave,
extra,
}: SettingsFormShellProps) {
if (isLoading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", py: 6 }}>
<CircularProgress aria-label={`Loading ${title}`} />
</Box>
);
}
if (isError) {
return (
<Alert severity="error">{errorMessage ?? `Failed to load ${title.toLowerCase()}`}</Alert>
);
}
if (notFound) {
return <Alert severity="warning">Record not found.</Alert>;
}
return (
<SettingsFormFields
key={formKey}
title={title}
testId={testId}
fields={fields}
initialValues={initialValues}
onCancel={onCancel}
onSave={onSave}
extra={extra}
/>
);
}