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>
  );
}