"use client"; import { useEffect, useRef, useState } from "react"; import { Check, Edit, Loader2, Trash2, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { IconTooltip } from "@/components/ui/tooltip"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { Input } from "@/components/ui/input"; import { dialogFormSubmit } from "@/lib/dialog-form"; import { ALL_FIELDS_PRESET_NAME, isAllFieldsPresetName, type DiveFormPreset, } from "@/lib/api/dive-form-presets"; import type { DiveFormPresets } from "@/hooks/useDiveFormPresets"; /** * The Presets tab: what the account has saved, and the two things that can be done to * one without changing the fields on the form. * * **Neither applying nor saving lives here.** The menu applies a preset in one click * from the page's top row, and the Fields tab's "Save as" writes the current fields into a new * preset or over an existing one - so a row is left with renaming and deleting, which * is all that is genuinely about the preset rather than about the form. * * **Nor does the mark for which one is current.** The menu carries it, where it is the * answer to what the diver is about to pick from; renaming and deleting a preset are * the same acts whether or not its set happens to equal what is on the form, so a check * here only invited the reading that this row is somehow protected. * * **Renaming happens in the row.** The pencil turns the name into a field and itself * into a tick, so the new name is typed where the old one was rather than in a prompt * below a list the diver then has to find their row in again. Delete becomes Cancel for * as long as that lasts: a delete button beside a half-typed rename is one mis-click * from destroying the row being edited, and without it there is no pointer way out of * the edit at all. * * The buttons are icons in an `IconTooltip` naming the preset, which is the shape * `RowActions` already gives a table row's actions. */ export function DiveFormPresetList({ presets }: { presets: DiveFormPresets }) { const [editing, setEditing] = useState(null); const [draftName, setDraftName] = useState(""); const [deleting, setDeleting] = useState(null); const nameInputRef = useRef(null); useEffect(() => { if (editing) nameInputRef.current?.focus(); }, [editing]); const stopEditing = () => { setEditing(null); setDraftName(""); }; const handleRename = async () => { const name = draftName.trim(); if (!name || !editing || isAllFieldsPresetName(name)) return; // An unchanged name is not a rename. Saving it anyway spends a PATCH to store // what is already stored, and reads as an edit in the account's history. if (name !== editing.name) await presets.renamePreset(editing, name); stopEditing(); }; const handleDelete = async () => { if (!deleting) return; await presets.deletePreset(deleting); setDeleting(null); }; const rows = presets.presets; return (
{/* Built in, and so the one row with nothing to do to it. */}
{ALL_FIELDS_PRESET_NAME} Built in, shows every field
{presets.isLoading ? (

Loading presets...

) : rows && rows.length > 0 ? (
    {rows.map((preset) => { const isEditing = editing?.uuid === preset.uuid; return (
  • {isEditing ? ( // A `
    ` so Enter saves. `dialogFormSubmit` stops the submit // event here rather than letting React bubble it into the dive // form's own `onSubmit` - see "A dialog's submit event bubbles into // the form that opened it" in DECISIONS.md. { event?.preventDefault(); void handleRename(); })} > setDraftName(event.target.value)} // Escape closes the dialog; the cross beside the field // cancels only the rename. className="h-8 min-w-0 flex-1" />
    ) : ( <> {preset.name} {presets.busyUuid === preset.uuid && ( )} )}
  • ); })}
) : (

No presets yet. Save the fields as one from the Fields tab, or restore the defaults.

)}
!next && setDeleting(null)} title="Delete this preset?" description={ deleting ? `"${deleting.name}" will be removed. The fields on this form stay as they are.` : undefined } confirmText="Delete" isLoading={presets.isWorking} onConfirm={handleDelete} />
); }