feat: ObjektSelector – Neu-Button entfernt, unbekannte Objekte direkt eintragen
Kein separater „+ Neu"-Button mehr. Im Dropdown erscheint stattdessen „+ ‚[Name]' hinzufügen", sobald der eingegebene Text nicht bekannt ist. Enter-Taste: bei einem Treffer auswählen, bei keinem Treffer neu anlegen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,6 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
|||||||
const [all, setAll] = useState<ObjektOption[]>([]);
|
const [all, setAll] = useState<ObjektOption[]>([]);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const [newName, setNewName] = useState('');
|
|
||||||
const [showNewInput, setShowNewInput] = useState(false);
|
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,29 +37,44 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
|||||||
? available.filter((o) => o.Name.toLowerCase().startsWith(search.toLowerCase()))
|
? available.filter((o) => o.Name.toLowerCase().startsWith(search.toLowerCase()))
|
||||||
: available;
|
: available;
|
||||||
|
|
||||||
|
const searchTrimmed = search.trim();
|
||||||
|
const alreadySelected = searchTrimmed !== '' && selectedNames.has(searchTrimmed.toLowerCase());
|
||||||
|
const exactAvailableMatch = available.find((o) => o.Name.toLowerCase() === searchTrimmed.toLowerCase());
|
||||||
|
const showAddNew = searchTrimmed !== '' && !alreadySelected && !exactAvailableMatch;
|
||||||
|
|
||||||
function add(obj: ObjektOption) {
|
function add(obj: ObjektOption) {
|
||||||
onChange([...selected, { ID: obj.ID, Name: obj.Name }]);
|
onChange([...selected, { ID: obj.ID, Name: obj.Name }]);
|
||||||
setSearch('');
|
setSearch('');
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNew() {
|
function addNew(name: string) {
|
||||||
const name = newName.trim();
|
const trimmed = name.trim();
|
||||||
if (!name || selectedNames.has(name.toLowerCase())) return;
|
if (!trimmed || selectedNames.has(trimmed.toLowerCase())) return;
|
||||||
const existing = all.find((o) => o.Name.toLowerCase() === name.toLowerCase());
|
const existing = all.find((o) => o.Name.toLowerCase() === trimmed.toLowerCase());
|
||||||
if (existing) {
|
if (existing) {
|
||||||
onChange([...selected, { ID: existing.ID, Name: existing.Name }]);
|
onChange([...selected, { ID: existing.ID, Name: existing.Name }]);
|
||||||
} else {
|
} else {
|
||||||
onChange([...selected, { ID: null, Name: name }]);
|
onChange([...selected, { ID: null, Name: trimmed }]);
|
||||||
}
|
}
|
||||||
setNewName('');
|
setSearch('');
|
||||||
setShowNewInput(false);
|
setDropdownOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(name: string) {
|
function remove(name: string) {
|
||||||
onChange(selected.filter((o) => o.Name !== name));
|
onChange(selected.filter((o) => o.Name !== name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (e.key !== 'Enter') return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (filtered.length === 1 && !showAddNew) {
|
||||||
|
add(filtered[0]);
|
||||||
|
} else if (filtered.length === 0 && searchTrimmed) {
|
||||||
|
addNew(searchTrimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -83,18 +96,17 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div ref={wrapperRef} className="relative">
|
||||||
{available.length > 0 && (
|
|
||||||
<div ref={wrapperRef} className="relative flex-1">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => { setSearch(e.target.value); setDropdownOpen(true); }}
|
onChange={(e) => { setSearch(e.target.value); setDropdownOpen(true); }}
|
||||||
onFocus={() => setDropdownOpen(true)}
|
onFocus={() => setDropdownOpen(true)}
|
||||||
placeholder="Objekt suchen..."
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Objekt suchen oder neu eingeben..."
|
||||||
className="w-full px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none"
|
className="w-full px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
{dropdownOpen && filtered.length > 0 && (
|
{dropdownOpen && (filtered.length > 0 || showAddNew) && (
|
||||||
<div className="absolute z-50 left-0 right-0 mt-1 bg-white border-2 border-gray-400 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
<div className="absolute z-50 left-0 right-0 mt-1 bg-white border-2 border-gray-400 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||||
{filtered.map((o) => (
|
{filtered.map((o) => (
|
||||||
<button
|
<button
|
||||||
@@ -106,46 +118,18 @@ export default function ObjektSelector({ selected, onChange }: Props) {
|
|||||||
{o.Name}
|
{o.Name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
{showAddNew && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addNew(searchTrimmed)}
|
||||||
|
className="w-full text-left px-4 py-2 text-sm text-blue-700 hover:bg-blue-50 active:bg-blue-100 border-t border-gray-200 font-medium"
|
||||||
|
>
|
||||||
|
+ „{searchTrimmed}" hinzufügen
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowNewInput((v) => !v)}
|
|
||||||
className="px-4 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
+ Neu
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showNewInput && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newName}
|
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addNew(); } }}
|
|
||||||
placeholder="Objektname eingeben"
|
|
||||||
className="flex-1 px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm focus:border-blue-500 focus:outline-none"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={addNew}
|
|
||||||
className="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700"
|
|
||||||
>
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setShowNewInput(false); setNewName(''); }}
|
|
||||||
className="px-4 py-2 bg-gray-200 text-gray-700 text-sm rounded-lg hover:bg-gray-300"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user