66 lines
2.6 KiB
TypeScript
66 lines
2.6 KiB
TypeScript
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from '@mui/material'
|
|
import { useState, type FormEvent, type ReactNode } from 'react'
|
|
import { ApiError, type Validation } from './api.ts'
|
|
import { useValidation } from './useValidation.ts'
|
|
|
|
/**
|
|
* A dialog that edits one row. The server validates the value as it changes (validatePath); onSave writes it and
|
|
* the dialog closes when it succeeds. fields renders the inputs from the value, a setter and the problems by field;
|
|
* below shows extra information from the validation, such as subnet placements.
|
|
*/
|
|
export default function RowDialog<T, V extends Validation = Validation>(props: {
|
|
title: string
|
|
initial: T
|
|
validatePath: string
|
|
saveLabel: string
|
|
onSave: (value: T) => Promise<void>
|
|
onCancel: () => void
|
|
fields: (value: T, set: (next: T) => void, problems: Record<string, string>) => ReactNode
|
|
below?: (result: V | null) => ReactNode
|
|
}) {
|
|
const [value, setValue] = useState<T>(props.initial)
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const validation = useValidation<V>(props.validatePath, value)
|
|
const problems = validation.result?.problems ?? {}
|
|
const message = validation.result?.message ?? ''
|
|
const blocked = validation.pending || Object.keys(problems).length > 0 || message !== ''
|
|
|
|
async function submit(event: FormEvent) {
|
|
event.preventDefault()
|
|
if (blocked) {
|
|
return
|
|
}
|
|
setSaving(true)
|
|
setError(null)
|
|
try {
|
|
await props.onSave(value)
|
|
} catch (caught) {
|
|
const failure = caught as ApiError
|
|
setError([failure.message, failure.detail].filter(Boolean).join('\n'))
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open onClose={(_event, reason) => reason !== 'backdropClick' && props.onCancel()} maxWidth="sm" fullWidth slotProps={{ paper: { component: 'form', onSubmit: submit } }}>
|
|
<DialogTitle>{props.title}</DialogTitle>
|
|
<DialogContent>
|
|
<Stack sx={{ gap: 2, pt: 1 }}>
|
|
{error && <Alert severity="error" sx={{ whiteSpace: 'pre-line' }}>{error}</Alert>}
|
|
{validation.error && <Alert severity="error">{validation.error}</Alert>}
|
|
{message && <Alert severity="error" sx={{ whiteSpace: 'pre-line' }}>{message}</Alert>}
|
|
{props.fields(value, setValue, problems)}
|
|
{props.below?.(validation.result)}
|
|
</Stack>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={props.onCancel}>Cancel</Button>
|
|
<Button type="submit" variant="contained" disabled={saving || blocked}>
|
|
{props.saveLabel}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
)
|
|
}
|