import clsx from 'clsx'; import { ChangeEvent, ComponentType, KeyboardEvent, useEffect, useRef, useState } from 'react'; import { SquarePen, Trash2 } from 'lucide-react'; import { Button, Styles, useClickOutside, useFlag } from '@vertesia/ui/core'; const VIEW_BOX = "block text-sm sm:leading-6 rounded-md border-0 py-1.5 px-4" const VIEW_BOX_HOVER = `${VIEW_BOX} hover:shadow-xs hover:ring-1 hover:ring-inset hover:ring-ring` const EDIT_BOX = `${VIEW_BOX} shadow-xs ring-1 ring-inset ring-ring` export interface DataViewerProps { value: T | undefined; placeholder?: string } export interface DataEditorProps { value: T | undefined; onChange: (value: any, autoSave?: boolean) => void onSave?: () => void onCancel?: () => void } interface EditableProps { value: T; viewer: ComponentType>; editor: ComponentType>; isEditing?: boolean; placeholder?: string; onChange: (value: T) => boolean; onDelete?: () => void; outlineOnHover?: boolean; editOnClick?: boolean; skipClickOutside?: (e: MouseEvent) => boolean; readonly?: boolean; /** * An optional validation function that returns an error message if the value is invalid. * * @returns An error message or undefined if the value is valid. */ onValidate?: (value: T) => string | undefined; } export function Editable({ value, onChange, onDelete, outlineOnHover = false, editOnClick = true, placeholder, viewer, editor, skipClickOutside, isEditing = false, readonly = false, onValidate, }: EditableProps) { const { on, off, isOn } = useFlag(isEditing); const [validationError, setValidationError] = useState(); const _onChange = (value?: any) => { if (onValidate) { const err = onValidate(value); if (err) { setValidationError(err); return; // don't save } else { setValidationError(undefined); } } if (onChange(value)) { off(); } }; const _skipClickOutside = (e: MouseEvent) => { if (skipClickOutside) { return skipClickOutside(e); } return false; }; return (
{ isOn && !readonly ? : } { validationError &&
{validationError}
}
) } interface DataViewProps { value: T; viewer: ComponentType>; onEdit: () => void; outlineOnHover?: boolean editOnClick?: boolean placeholder?: string onDelete?: () => void readonly?: boolean, } function DataView({ viewer: Viewer, value, onEdit, editOnClick, outlineOnHover, placeholder, onDelete, readonly }: DataViewProps) { const onClick = () => { editOnClick && onEdit(); }; const onKeyUp = (e: KeyboardEvent) => { if (e.key === "Enter") { onEdit(); } }; const btnStyle = 'invisible group-hover:visible'; return (
{ !readonly && onDelete && } { !readonly ? : null }
); } interface DataEditProps { value: T; editor: ComponentType>; onSave: (value: T) => void; onCancel: () => void; skipClickOutside?: (e: MouseEvent) => boolean; } function DataEdit({ editor: Editor, value, onSave, onCancel, skipClickOutside }: DataEditProps) { const [actualValue, setActualValue] = useState(value); const latestValue = useRef(value); const [debounceTimer, setDebounceTimer] = useState(null); const handleSave = () => { onSave(latestValue.current); }; const ref = useClickOutside(handleSave, skipClickOutside); const _onChange = (value: any, autoSave = false) => { setActualValue(value); latestValue.current = value; if (autoSave) { if (debounceTimer) { clearTimeout(debounceTimer); } setDebounceTimer(setTimeout(() => { onSave(value); }, 500)) } }; return (
e.stopPropagation()}>
); } export function TextDataViewer({ value, placeholder }: DataViewerProps) { if (!value) { return ( {placeholder || 'Missing value'} ); } else { return ( {value == null ? '' : value.toString()} ); } } export function TextDataEditor({ value, onChange, onCancel, onSave }: DataEditorProps) { const ref = useRef(null); useEffect(() => { ref.current?.focus(); }, []); const onKeyUp = (e: KeyboardEvent) => { switch (e.key) { case "Enter": onSave?.(); break; case "Escape": onCancel?.(); break; } }; const _onChange = (e: ChangeEvent) => { onChange(e.target.value); }; return ( ); } interface EditableTextProps extends Omit, 'viewer' | 'editor'> { viewer?: ComponentType>; editor?: ComponentType>; } export function EditableText(props: EditableTextProps) { if (!props.viewer) { props.viewer = TextDataViewer; } if (!props.editor) { props.editor = TextDataEditor; } return ( } /> ); }