diff --git a/resources/js/components/InlineEditInput.tsx b/resources/js/components/InlineEditInput.tsx index 793c129..f008441 100644 --- a/resources/js/components/InlineEditInput.tsx +++ b/resources/js/components/InlineEditInput.tsx @@ -1,26 +1,34 @@ import { useEffect, useRef, useState } from 'react'; -interface InlineEditInputProps { +interface BaseProps { + className?: string; + disabled?: boolean; +} + +interface NumberProps extends BaseProps { + type?: 'number'; value: number; onSave: (value: number) => Promise; formatDisplay?: (value: number) => string; min?: number; step?: string; - className?: string; - disabled?: boolean; } +interface TextProps extends BaseProps { + type: 'text'; + value: string; + onSave: (value: string) => Promise; + formatDisplay?: (value: string) => string; +} + +type InlineEditInputProps = NumberProps | TextProps; + type Status = 'idle' | 'editing' | 'saving' | 'success' | 'error'; -export default function InlineEditInput({ - value, - onSave, - formatDisplay = (v) => String(v), - min, - step, - className = '', - disabled = false, -}: InlineEditInputProps) { +export default function InlineEditInput(props: InlineEditInputProps) { + const { className = '', disabled = false } = props; + const isText = props.type === 'text'; + const [status, setStatus] = useState('idle'); const [editValue, setEditValue] = useState(''); const inputRef = useRef(null); @@ -35,7 +43,7 @@ export default function InlineEditInput({ const startEditing = () => { if (disabled) return; - setEditValue(String(value)); + setEditValue(String(props.value)); setStatus('editing'); }; @@ -46,20 +54,30 @@ export default function InlineEditInput({ const save = async () => { if (savingRef.current) return; - const parsed = Number(editValue); - if (isNaN(parsed)) { - setStatus('idle'); - return; - } - if (parsed === value) { - setStatus('idle'); - return; + let parsedValue: string | number; + if (isText) { + const trimmed = editValue.trim(); + if (trimmed === '' || trimmed === props.value) { + setStatus('idle'); + return; + } + parsedValue = trimmed; + } else { + const parsed = Number(editValue); + if (isNaN(parsed) || parsed === props.value) { + setStatus('idle'); + return; + } + parsedValue = parsed; } savingRef.current = true; setStatus('saving'); try { - await onSave(parsed); + // Type assertion needed: TS can't correlate isText with the union branch + await (props.onSave as (v: typeof parsedValue) => Promise)( + parsedValue, + ); setStatus('success'); setTimeout(() => setStatus('idle'), 1500); } catch { @@ -79,19 +97,23 @@ export default function InlineEditInput({ } }; + const displayValue = isText + ? (props.formatDisplay ?? String)(props.value) + : (props.formatDisplay ?? ((v: number) => String(v)))(props.value); + if (status === 'editing' || status === 'saving') { return ( setEditValue(e.target.value)} onBlur={save} onKeyDown={handleKeyDown} - min={min} - step={step} + min={props.type !== 'text' ? props.min : undefined} + step={props.type !== 'text' ? props.step : undefined} disabled={status === 'saving'} - className={`w-24 rounded border border-blue-300 bg-white px-2 py-0.5 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-blue-500 ${className}`} + className={`rounded border border-blue-300 bg-white px-2 py-0.5 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-blue-500 ${isText ? 'w-48' : 'w-24'} ${className}`} /> ); } @@ -105,7 +127,7 @@ export default function InlineEditInput({ : 'cursor-pointer rounded px-1 py-0.5 hover:bg-blue-50 hover:text-blue-700' } ${className}`} > - {formatDisplay(value)} + {displayValue} {status === 'success' && } {status === 'error' && }