import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import { Alert, AlertColor, Button, IconButton, List, ListItem, ListItemText, Stack, TextField, } from '@mui/material'; import { useMemo, useState } from 'react'; import { ActionOutcome, applyOutcome, FeedbackState } from '../utils/editableEntityUtils'; export type { ActionOutcome, FeedbackState } from '../utils/editableEntityUtils'; export interface EditableEntity { id: string; name: string; secondaryText?: string; } interface EditableEntityListProps { nameLabel: string; addButtonLabel?: string; entityLabel?: string; addPlaceholder?: string; entities: EditableEntity[]; validateName?: (name: string, entityId?: string | null) => boolean; onAdd: (name: string) => Promise; onUpdate: (id: string, name: string) => Promise; onDelete: (id: string, name: string) => Promise; } export const EditableEntityList = ({ nameLabel, addButtonLabel = 'Add', entityLabel = 'Item', addPlaceholder, entities, validateName, onAdd, onUpdate, onDelete, }: EditableEntityListProps) => { const [newName, setNewName] = useState(''); const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(''); const [feedback, setFeedback] = useState(null); const isNameValid = useMemo(() => { return (value: string, entityId: string | null) => { const trimmed = value.trim(); if (!trimmed) return false; if (validateName) return validateName(trimmed, entityId); return true; }; }, [validateName]); const handleAdd = async () => { const trimmed = newName.trim(); if (!isNameValid(newName, null)) return; try { const success = applyOutcome(setFeedback, await onAdd(trimmed), `${entityLabel} added.`); if (success) { setNewName(''); } } catch (error) { setFeedback({ text: `Unable to add ${entityLabel.toLowerCase()}.`, severity: 'error' }); } }; const startEditing = (id: string, currentName: string) => { setEditingId(id); setEditName(currentName); setFeedback(null); }; const cancelEditing = () => { setEditingId(null); setEditName(''); }; const handleSave = async () => { if (!editingId) return; const trimmed = editName.trim(); if (!isNameValid(editName, editingId)) return; try { const success = applyOutcome(setFeedback, await onUpdate(editingId, trimmed), `${entityLabel} updated.`); if (success) { cancelEditing(); } } catch (error) { setFeedback({ text: `Unable to update ${entityLabel.toLowerCase()}.`, severity: 'error' }); } }; const handleDelete = async (id: string, name: string) => { try { const success = applyOutcome(setFeedback, await onDelete(id, name), `${entityLabel} deleted.`); if (success && editingId === id) { cancelEditing(); } } catch (error) { setFeedback({ text: `Unable to delete ${entityLabel.toLowerCase()}.`, severity: 'error' }); } }; const canAdd = isNameValid(newName, null); const canSave = editingId ? isNameValid(editName, editingId) : false; return ( {feedback ? {feedback.text} : null} setNewName(event.target.value)} /> {entities.map((entity) => ( {editingId === entity.id ? ( setEditName(event.target.value)} /> ) : ( startEditing(entity.id, entity.name)} aria-label={`Edit ${entity.name}`} size="small" > handleDelete(entity.id, entity.name)} aria-label={`Delete ${entity.name}`} size="small" > )} ))} ); }; export default EditableEntityList;