Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Has been cancelled
- Added CSS styles for skill group labels in the skill tree to improve visual hierarchy and readability. - Updated `SkillTreePickerPanel` and `SkillTreeMultiSelect` components to utilize the new default expansion logic, ensuring main and category nodes are open by default while skill groups remain collapsed. - Refactored state management in `SkillTreePickerPanel` to align with the new default expansion behavior. - Enhanced utility functions to support the new default expansion logic for skill trees.
155 lines
4.8 KiB
JavaScript
155 lines
4.8 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||
import { collectSkillLeavesFromTree, buildSkillCatalogTree } from '../utils/skillCatalogTree'
|
||
import SkillTreePickerPanel from './SkillTreePickerPanel'
|
||
|
||
function normId(id) {
|
||
return String(id)
|
||
}
|
||
|
||
/**
|
||
* Mehrfachauswahl Fähigkeiten mit hierarchischer Treeview („Alle“) und Pfad-Suche.
|
||
*/
|
||
export default function SkillTreeMultiSelect({
|
||
value = [],
|
||
onChange,
|
||
skills = [],
|
||
placeholder = 'Fähigkeit suchen …',
|
||
browseLabel = '▼ Katalog',
|
||
emptyHint = 'Keine Treffer',
|
||
className = '',
|
||
}) {
|
||
const [query, setQuery] = useState('')
|
||
const [open, setOpen] = useState(false)
|
||
const [browseTree, setBrowseTree] = useState(false)
|
||
const rootRef = useRef(null)
|
||
|
||
const tree = useMemo(() => buildSkillCatalogTree(skills), [skills])
|
||
const selectedSet = useMemo(() => new Set(value.map(normId)), [value])
|
||
|
||
const leaves = useMemo(() => collectSkillLeavesFromTree(tree, value), [tree, value])
|
||
|
||
const selectedLabels = useMemo(() => {
|
||
return value.map((id) => {
|
||
const leaf = leaves.find((l) => normId(l.id) === normId(id)) || leaves.find(() => false)
|
||
const fromSkills = skills.find((s) => normId(s.id) === normId(id))
|
||
return leaf?.pathLabel || fromSkills?.name || `#${id}`
|
||
})
|
||
}, [value, leaves, skills])
|
||
|
||
const addId = useCallback(
|
||
(id) => {
|
||
const sid = normId(id)
|
||
if (selectedSet.has(sid)) return
|
||
onChange([...value, id])
|
||
setQuery('')
|
||
setBrowseTree(false)
|
||
},
|
||
[value, onChange, selectedSet]
|
||
)
|
||
|
||
const removeAt = useCallback(
|
||
(idx) => {
|
||
onChange(value.filter((_, i) => i !== idx))
|
||
},
|
||
[value, onChange]
|
||
)
|
||
|
||
useEffect(() => {
|
||
const onDoc = (e) => {
|
||
if (!rootRef.current?.contains(e.target)) {
|
||
setOpen(false)
|
||
setBrowseTree(false)
|
||
}
|
||
}
|
||
document.addEventListener('mousedown', onDoc)
|
||
return () => document.removeEventListener('mousedown', onDoc)
|
||
}, [])
|
||
|
||
const showTree = browseTree || !query.trim()
|
||
|
||
return (
|
||
<div className={`multiselect-combo skill-tree-multiselect ${className}`.trim()} ref={rootRef}>
|
||
<div className="multiselect-combo__chips">
|
||
{value.map((id, idx) => (
|
||
<button
|
||
key={`${normId(id)}-${idx}`}
|
||
type="button"
|
||
className="multiselect-combo__chip"
|
||
onClick={() => removeAt(idx)}
|
||
title="Entfernen"
|
||
>
|
||
<span>{selectedLabels[idx]}</span>
|
||
<span className="multiselect-combo__chip-x" aria-hidden>
|
||
×
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="multiselect-combo__field">
|
||
<input
|
||
type="text"
|
||
className="form-input multiselect-combo__input"
|
||
placeholder={placeholder}
|
||
value={query}
|
||
onChange={(e) => {
|
||
setQuery(e.target.value)
|
||
setOpen(true)
|
||
setBrowseTree(false)
|
||
}}
|
||
onFocus={() => setOpen(true)}
|
||
autoComplete="off"
|
||
aria-expanded={open}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="btn multiselect-combo__browse"
|
||
title="Katalog als Baum"
|
||
onClick={() => {
|
||
setOpen(true)
|
||
setBrowseTree(true)
|
||
setQuery('')
|
||
}}
|
||
>
|
||
{browseLabel}
|
||
</button>
|
||
</div>
|
||
{open ? (
|
||
<div className="skill-tree-multiselect__panel">
|
||
{showTree ? (
|
||
<SkillTreePickerPanel
|
||
skills={skills}
|
||
excludeIds={value}
|
||
searchQuery={query}
|
||
onPickSkill={(id) => addId(id)}
|
||
pickMode="multi"
|
||
/>
|
||
) : (
|
||
<ul className="multiselect-combo__list" role="listbox">
|
||
{leaves.filter((l) => l.pathLabel.toLowerCase().includes(query.trim().toLowerCase())).length ===
|
||
0 ? (
|
||
<li className="multiselect-combo__empty">{emptyHint}</li>
|
||
) : (
|
||
leaves
|
||
.filter((l) => l.pathLabel.toLowerCase().includes(query.trim().toLowerCase()))
|
||
.map((l) => (
|
||
<li key={normId(l.id)}>
|
||
<button
|
||
type="button"
|
||
className="multiselect-combo__opt skill-tree__pick--path"
|
||
onMouseDown={(e) => e.preventDefault()}
|
||
onClick={() => addId(l.id)}
|
||
>
|
||
<span className="skill-tree__pick-name">{l.label}</span>
|
||
<span className="skill-tree__pick-path">{l.pathLabel}</span>
|
||
</button>
|
||
</li>
|
||
))
|
||
)}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
)
|
||
}
|