diff --git a/apps/admin/src/components/ui/EditableField.tsx b/apps/admin/src/components/ui/EditableField.tsx index 00447a4..9c97007 100644 --- a/apps/admin/src/components/ui/EditableField.tsx +++ b/apps/admin/src/components/ui/EditableField.tsx @@ -30,6 +30,13 @@ export interface StringEditableFieldProps extends Omit< input?: FieldInputConfig; } +function multilineSaveShortcutLabel(): string { + if (!("navigator" in globalThis)) { + return "Ctrl+Enter"; + } + return /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? "⌘↵" : "Ctrl+Enter"; +} + function EditableFieldInner({ value, formatValue, @@ -49,8 +56,11 @@ function EditableFieldInner({ isUnchanged, }: EditableFieldProps): ReactElement { const [editing, setEditing] = useState(false); + const [savedValue, setSavedValue] = useState(undefined); const [draft, setDraft] = useState(() => formatValue(value)); const [error, setError] = useState(null); + const multilineSaveShortcut = multilineSaveShortcutLabel(); + const displayValue = savedValue ?? value; function valueIsEmpty(nextValue: T): boolean { if (isEmpty) { @@ -66,21 +76,25 @@ function EditableFieldInner({ return formatValue(nextValue); } + useEffect(() => { + setSavedValue(undefined); + }, [value]); + useEffect(() => { if (!editing) { - setDraft(formatValue(value)); + setDraft(formatValue(displayValue)); setError(null); } - }, [value, editing, formatValue]); + }, [displayValue, editing, formatValue]); function startEdit(): void { - setDraft(formatValue(value)); + setDraft(formatValue(displayValue)); setError(null); setEditing(true); } function cancelEdit(): void { - setDraft(formatValue(value)); + setDraft(formatValue(displayValue)); setError(null); setEditing(false); } @@ -88,8 +102,8 @@ function EditableFieldInner({ async function handleSave(): Promise { const nextDraft = trimOnCommit ? draft.trim() : draft; const changed = isUnchanged - ? !isUnchanged(nextDraft, value) - : nextDraft !== formatValue(value); + ? !isUnchanged(nextDraft, displayValue) + : nextDraft !== formatValue(displayValue); if (!changed) { setEditing(false); @@ -97,14 +111,17 @@ function EditableFieldInner({ return; } + const parsed = parseDraft(nextDraft); + try { - await onSave(parseDraft(nextDraft)); + await onSave(parsed); + setSavedValue(parsed); setEditing(false); setError(null); } catch (err) { setError(formatCaughtError(err)); if (revertOnError) { - setDraft(formatValue(value)); + setDraft(formatValue(displayValue)); } } } @@ -133,6 +150,11 @@ function EditableFieldInner({ } if (editing) { + const saveShortcut = + input.kind === "textarea" || input.kind === "json" + ? multilineSaveShortcut + : "↵"; + return (
({ onCancel={cancelEdit} commitOnChange={input.kind === "select"} /> + {input.kind !== "select" ? ( +
+ + {saveShortcut} save + + Esc cancel +
+ ) : null} {error ?

{error}

: null}
); @@ -159,7 +189,7 @@ function EditableFieldInner({ align === "start" ? "items-start" : "items-center", )} > -
{renderValue(value)}
+
{renderValue(displayValue)}