Shared UI kit
forms.jsx 166 lines
Files · 11
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166import React, { useState } from "react";
import { Button, Field, Modal } from "./ui.jsx";
export function FormActions({ busy, onCancel, label = "Save" }) {
return (
<div className="ui-form-actions">
<Button onClick={onCancel} disabled={busy}>
Cancel
</Button>
<Button type="submit" variant="primary" disabled={busy}>
{busy ? "Saving…" : label}
</Button>
</div>
);
}
export function RecordForm({
title,
record,
fields,
onSave,
onClose,
busy: externalBusy,
validate,
error,
prepare,
wide = false,
}) {
const [form, setForm] = useState(() =>
Object.fromEntries(
fields.map((field) => [
field.name,
record?.[field.name] ?? field.defaultValue ?? "",
]),
),
);
const [validation, setValidation] = useState("");
const [saving, setSaving] = useState(false);
const busy = externalBusy || saving;
function input(field) {
const common = {
id: `field-${field.name}`,
required: field.required,
disabled: busy || field.disabled,
value: form[field.name],
onChange: (event) =>
setForm({ ...form, [field.name]: event.target.value }),
};
return (
<Field
key={field.name}
id={common.id}
label={field.label}
hint={field.hint}
wide={field.wide}
>
{field.options ? (
<select {...common}>
{field.options.map((option) => (
<option
key={option.value ?? option}
value={option.value ?? option}
>
{option.label ?? option}
</option>
))}
</select>
) : field.multiline ? (
<textarea
{...common}
rows={field.rows}
placeholder={field.placeholder}
/>
) : (
<input
{...common}
type={field.type || "text"}
autoFocus={field === fields[0]}
placeholder={field.placeholder}
min={field.min}
max={field.max}
step={field.step}
inputMode={field.inputMode}
/>
)}
</Field>
);
}
const main = fields.filter((field) => !field.advanced),
advanced = fields.filter((field) => field.advanced);
return (
<Modal title={title} onClose={onClose} busy={busy} wide={wide}>
<form
onSubmit={async (event) => {
event.preventDefault();
setValidation("");
setSaving(true);
try {
let next = Object.fromEntries(
Object.entries(form).map(([key, value]) => [
key,
typeof value === "string" &&
["name", "title", "url", "folder"].includes(key)
? value.trim()
: value,
]),
);
if (prepare) next = prepare(next);
const problem = validate?.(next);
if (problem) setValidation(problem);
else await onSave(next);
} catch (err) {
setValidation(err.message || String(err));
} finally {
setSaving(false);
}
}}
>
<div className={wide ? "ui-form-grid" : ""}>{main.map(input)}</div>
{advanced.length > 0 && (
<details className="ui-advanced">
<summary>More details</summary>
<div className="ui-form-grid">{advanced.map(input)}</div>
</details>
)}
{(validation || error) && (
<p className="ui-error-text" role="alert">
{validation || error}
</p>
)}
<FormActions busy={busy} onCancel={onClose} />
</form>
</Modal>
);
}
export function ConfirmDelete({
title = "Delete item?",
children,
onConfirm,
onClose,
busy,
error,
}) {
return (
<Modal
title={title}
onClose={busy ? undefined : onClose}
actions={
<>
<Button disabled={busy} onClick={onClose}>
Cancel
</Button>
<Button variant="danger" disabled={busy} onClick={onConfirm}>
{busy ? "Deleting…" : "Delete"}
</Button>
</>
}
>
<p className="ui-muted">{children}</p>
{error && (
<p className="ui-error-text" role="alert">
{error}
</p>
)}
</Modal>
);
}