Files

71 lines
3.1 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. body gives the value posted to
* validatePath, such as a whole document with the row put in, and select the problems by field and the message of
* the row taken from the answer; without them the row itself is posted and the answer is its validation.
*/
export default function RowDialog<T, V = Validation>(props: {
title: string
initial: T
validatePath: string
body?: (value: T) => unknown
select?: (result: V) => Validation
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, props.body ? props.body(value) : value)
const selected = validation.result === null ? null : props.select ? props.select(validation.result) : validation.result as Validation
const problems = selected?.problems ?? {}
const message = selected?.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>
)
}