shinkan-jinkendo/frontend/src/components/SkillTreeMultiSelect.jsx
Lars 9020e5eb16
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
Enhance Skill Tree Components with Improved Group Labeling and Default Expansion
- 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.
2026-05-20 11:13:34 +02:00

155 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}