src/components/dives/dive-form-preset-list.tsx
Rama main · réplica de lectura del repositorio original.
"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<DiveFormPreset | null>(null);
const [draftName, setDraftName] = useState("");
const [deleting, setDeleting] = useState<DiveFormPreset | null>(null);
const nameInputRef = useRef<HTMLInputElement>(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 (
<div className="space-y-2">
{/* Built in, and so the one row with nothing to do to it. */}
<div className="flex h-9 items-center gap-2">
<span className="min-w-0 flex-1 text-sm">{ALL_FIELDS_PRESET_NAME}</span>
<span className="text-xs text-muted-foreground">
Built in, shows every field
</span>
</div>
{presets.isLoading ? (
<p className="text-sm text-muted-foreground">Loading presets...</p>
) : rows && rows.length > 0 ? (
<ul className="space-y-1">
{rows.map((preset) => {
const isEditing = editing?.uuid === preset.uuid;
return (
<li
key={preset.uuid}
className="flex flex-wrap items-center gap-2"
>
{isEditing ? (
// A `<form>` 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.
<form
className="flex min-w-0 flex-1 items-center gap-2"
onSubmit={dialogFormSubmit((event) => {
event?.preventDefault();
void handleRename();
})}
>
<Input
ref={nameInputRef}
value={draftName}
maxLength={255}
aria-label={`New name for "${preset.name}"`}
disabled={presets.isWorking}
onChange={(event) => 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"
/>
<IconTooltip
label={`Save the new name for "${preset.name}"`}
>
<Button
type="submit"
variant="ghost"
size="sm"
disabled={
presets.isWorking ||
draftName.trim().length === 0 ||
isAllFieldsPresetName(draftName)
}
>
<Check className="h-4 w-4" />
</Button>
</IconTooltip>
<IconTooltip label={`Stop renaming "${preset.name}"`}>
<Button
type="button"
variant="ghost"
size="sm"
disabled={presets.isWorking}
onClick={stopEditing}
>
<X className="h-4 w-4" />
</Button>
</IconTooltip>
</form>
) : (
<>
<span className="min-w-0 flex-1 text-sm">
{preset.name}
</span>
{presets.busyUuid === preset.uuid && (
<Loader2
className="h-3.5 w-3.5 animate-spin"
aria-hidden
/>
)}
<IconTooltip label={`Rename "${preset.name}"`}>
<Button
type="button"
variant="ghost"
size="sm"
disabled={presets.isWorking}
onClick={() => {
setDraftName(preset.name);
setEditing(preset);
}}
>
<Edit className="h-4 w-4" />
</Button>
</IconTooltip>
<IconTooltip label={`Delete "${preset.name}"`}>
<Button
type="button"
variant="ghost"
size="sm"
disabled={presets.isWorking}
onClick={() => setDeleting(preset)}
>
<Trash2 className="h-4 w-4" />
</Button>
</IconTooltip>
</>
)}
</li>
);
})}
</ul>
) : (
<p className="text-sm text-muted-foreground">
No presets yet. Save the fields as one from the Fields tab, or restore
the defaults.
</p>
)}
<div className="pt-1">
<Button
type="button"
variant="outline"
size="sm"
disabled={presets.isWorking}
onClick={() => void presets.restoreDefaults()}
>
Restore default presets
</Button>
</div>
<ConfirmDialog
open={deleting !== null}
onOpenChange={(next) => !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}
/>
</div>
);
}