rename and tags fields submit on enter, not just the buttons

The node rename input and the owner/tags editor's tags field sat in a div, so
pressing Enter did nothing and the check/apply button was the only way in.
Both are now real forms with the right button wrapped as type="submit" and
their manual Enter handling removed so there's no double-submit; Escape
handling stays since a form has no native equivalent for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 10:44:02 +00:00
co-authored by Claude Opus 5
parent 80f952a9a6
commit ad41bafd59
+15 -17
View File
@@ -103,6 +103,12 @@ const Ownership = ({ node, busy, onError }: OwnershipProps) => {
} }
}; };
const applyTags = (ev?: React.FormEvent) => {
ev?.preventDefault();
if (!tagsDirty) return;
void run(() => setTags.mutateAsync({ id: node.id, tags: nextTags }));
};
return ( return (
<div className="flex flex-col gap-2.5"> <div className="flex flex-col gap-2.5">
<div className="text-[10px] font-semibold uppercase tracking-wide text-zinc-500">Owner and tags</div> <div className="text-[10px] font-semibold uppercase tracking-wide text-zinc-500">Owner and tags</div>
@@ -148,13 +154,12 @@ const Ownership = ({ node, busy, onError }: OwnershipProps) => {
</p> </p>
)} )}
<div className="flex flex-wrap items-center gap-2"> <form onSubmit={applyTags} className="flex flex-wrap items-center gap-2">
<TagIcon className="h-3.5 w-3.5 shrink-0 text-zinc-500" /> <TagIcon className="h-3.5 w-3.5 shrink-0 text-zinc-500" />
<input <input
value={draftTags} value={draftTags}
onChange={(ev) => setDraftTags(ev.target.value)} onChange={(ev) => setDraftTags(ev.target.value)}
onKeyDown={(ev) => { onKeyDown={(ev) => {
if (ev.key === 'Enter' && tagsDirty) void run(() => setTags.mutateAsync({ id: node.id, tags: nextTags }));
if (ev.key === 'Escape') setDraftTags(node.tags.join(' ')); if (ev.key === 'Escape') setDraftTags(node.tags.join(' '));
}} }}
placeholder="tag:server tag:eu — space separated" placeholder="tag:server tag:eu — space separated"
@@ -164,19 +169,16 @@ const Ownership = ({ node, busy, onError }: OwnershipProps) => {
/> />
{tagsDirty && ( {tagsDirty && (
<> <>
<Button <Button type="submit" disabled={busy || pending}>
onClick={() => void run(() => setTags.mutateAsync({ id: node.id, tags: nextTags }))}
disabled={busy || pending}
>
<Check className="h-3.5 w-3.5" /> <Check className="h-3.5 w-3.5" />
Apply tags Apply tags
</Button> </Button>
<Button onClick={() => setDraftTags(node.tags.join(' '))} disabled={busy || pending}> <Button type="button" onClick={() => setDraftTags(node.tags.join(' '))} disabled={busy || pending}>
Revert Revert
</Button> </Button>
</> </>
)} )}
</div> </form>
<p className="text-[11px] leading-snug text-zinc-600"> <p className="text-[11px] leading-snug text-zinc-600">
Tags are what the access policy targets. A tag no rule mentions does nothing; removing one a rule depends on Tags are what the access policy targets. A tag no rule mentions does nothing; removing one a rule depends on
cuts the node off from it. The <span className="text-zinc-500">tag:</span> prefix is added for you. cuts the node off from it. The <span className="text-zinc-500">tag:</span> prefix is added for you.
@@ -204,7 +206,8 @@ const NodeCard = ({ node, onError }: NodeCardProps) => {
} }
}; };
const submitRename = async () => { const submitRename = async (ev?: React.FormEvent) => {
ev?.preventDefault();
const name = draftName.trim(); const name = draftName.trim();
setRenaming(false); setRenaming(false);
if (!name || name === node.name) return; if (!name || name === node.name) return;
@@ -218,29 +221,24 @@ const NodeCard = ({ node, onError }: NodeCardProps) => {
<Dot tone={node.online ? 'ok' : 'idle'} /> <Dot tone={node.online ? 'ok' : 'idle'} />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{renaming ? ( {renaming ? (
<div className="flex items-center gap-1.5"> <form onSubmit={submitRename} className="flex items-center gap-1.5">
<input <input
value={draftName} value={draftName}
onChange={(ev) => setDraftName(ev.target.value)} onChange={(ev) => setDraftName(ev.target.value)}
onKeyDown={(ev) => { onKeyDown={(ev) => {
if (ev.key === 'Enter') void submitRename();
if (ev.key === 'Escape') setRenaming(false); if (ev.key === 'Escape') setRenaming(false);
}} }}
autoFocus autoFocus
spellCheck={false} spellCheck={false}
className="min-w-0 flex-1 rounded-md border border-white/10 bg-black/40 px-2 py-1 text-sm text-zinc-100 outline-none focus:border-primary/50" className="min-w-0 flex-1 rounded-md border border-white/10 bg-black/40 px-2 py-1 text-sm text-zinc-100 outline-none focus:border-primary/50"
/> />
<button <button type="submit" className="cursor-pointer p-1 text-emerald-400">
type="button"
onClick={() => void submitRename()}
className="cursor-pointer p-1 text-emerald-400"
>
<Check className="h-3.5 w-3.5" /> <Check className="h-3.5 w-3.5" />
</button> </button>
<button type="button" onClick={() => setRenaming(false)} className="cursor-pointer p-1 text-zinc-500"> <button type="button" onClick={() => setRenaming(false)} className="cursor-pointer p-1 text-zinc-500">
<X className="h-3.5 w-3.5" /> <X className="h-3.5 w-3.5" />
</button> </button>
</div> </form>
) : ( ) : (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<span className="truncate text-sm font-medium text-zinc-100"> <span className="truncate text-sm font-medium text-zinc-100">