All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m14s
- Modified comments in `SkillTreePickerPanel` and `skillCatalogTree.js` to accurately reflect the default expansion behavior, specifying that only main groups are open by default. - Refactored `defaultExpandedKeysForSkillTree` to simplify the logic, ensuring only main groups are returned as expanded. - Adjusted tests in `skillCatalogTree.test.js` to validate the updated behavior, confirming that only main groups are opened in the skill tree.
164 lines
4.9 KiB
JavaScript
164 lines
4.9 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
import {
|
|
allExpandableKeys,
|
|
buildSkillCatalogTree,
|
|
collectSkillLeavesFromTree,
|
|
defaultExpandedKeysForSkillTree,
|
|
filterSkillTreeByQuery,
|
|
} from '../utils/skillCatalogTree'
|
|
|
|
function normExclude(excludeIds) {
|
|
return new Set(
|
|
(excludeIds instanceof Set ? [...excludeIds] : excludeIds || [])
|
|
.map((x) => Number(x))
|
|
.filter((n) => Number.isFinite(n))
|
|
)
|
|
}
|
|
|
|
function SkillTreeNodes({ nodes, depth, expanded, exclude, onToggle, onPickSkill, pickMode }) {
|
|
if (!nodes?.length) {
|
|
return <li className="skill-tree__empty">Keine Fähigkeiten</li>
|
|
}
|
|
|
|
return nodes.map((node) => {
|
|
if (node.type === 'skill') {
|
|
if (exclude.has(node.skillId)) return null
|
|
return (
|
|
<li key={node.key} className="skill-tree__leaf" style={{ paddingLeft: `${8 + depth * 14}px` }}>
|
|
<button
|
|
type="button"
|
|
className="skill-tree__pick"
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
onClick={() => onPickSkill(node.skillId, node.skill)}
|
|
>
|
|
{node.label}
|
|
</button>
|
|
</li>
|
|
)
|
|
}
|
|
|
|
const hasKids = node.children?.length > 0
|
|
const isOpen = expanded.has(node.key)
|
|
return (
|
|
<li key={node.key} className="skill-tree__branch">
|
|
<div className="skill-tree__branch-head" style={{ paddingLeft: `${4 + depth * 14}px` }}>
|
|
{hasKids ? (
|
|
<button
|
|
type="button"
|
|
className="skill-tree__toggle"
|
|
aria-expanded={isOpen}
|
|
onClick={() => onToggle(node.key)}
|
|
>
|
|
{isOpen ? <ChevronDown size={16} aria-hidden /> : <ChevronRight size={16} aria-hidden />}
|
|
</button>
|
|
) : (
|
|
<span className="skill-tree__toggle-spacer" aria-hidden />
|
|
)}
|
|
<span className={`skill-tree__group-label skill-tree__group-label--${node.type}`}>
|
|
{node.label}
|
|
</span>
|
|
</div>
|
|
{hasKids && isOpen ? (
|
|
<ul className="skill-tree__children" role={pickMode === 'multi' ? 'group' : 'tree'}>
|
|
<SkillTreeNodes
|
|
nodes={node.children}
|
|
depth={depth + 1}
|
|
expanded={expanded}
|
|
exclude={exclude}
|
|
onToggle={onToggle}
|
|
onPickSkill={onPickSkill}
|
|
pickMode={pickMode}
|
|
/>
|
|
</ul>
|
|
) : null}
|
|
</li>
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Ausklappbare Baumliste: Hauptgruppe → Kategorie → Fähigkeit (nur Hauptgruppe standard offen).
|
|
*/
|
|
export default function SkillTreePickerPanel({
|
|
skills = [],
|
|
excludeIds,
|
|
searchQuery = '',
|
|
onPickSkill,
|
|
pickMode = 'single',
|
|
className = '',
|
|
}) {
|
|
const exclude = useMemo(() => normExclude(excludeIds), [excludeIds])
|
|
const fullTree = useMemo(() => buildSkillCatalogTree(skills), [skills])
|
|
const displayTree = useMemo(
|
|
() => filterSkillTreeByQuery(fullTree, searchQuery),
|
|
[fullTree, searchQuery]
|
|
)
|
|
const [expanded, setExpanded] = useState(() => new Set())
|
|
|
|
useEffect(() => {
|
|
if (searchQuery.trim()) {
|
|
setExpanded(new Set(allExpandableKeys(displayTree)))
|
|
} else {
|
|
setExpanded(new Set(defaultExpandedKeysForSkillTree(displayTree)))
|
|
}
|
|
}, [searchQuery, displayTree])
|
|
|
|
useEffect(() => {
|
|
if (!searchQuery.trim()) {
|
|
setExpanded(new Set(defaultExpandedKeysForSkillTree(fullTree)))
|
|
}
|
|
}, [fullTree, searchQuery])
|
|
|
|
const onToggle = useCallback((key) => {
|
|
setExpanded((prev) => {
|
|
const next = new Set(prev)
|
|
if (next.has(key)) next.delete(key)
|
|
else next.add(key)
|
|
return next
|
|
})
|
|
}, [])
|
|
|
|
const flatSearchHits = useMemo(() => {
|
|
const q = searchQuery.trim()
|
|
if (!q) return []
|
|
return collectSkillLeavesFromTree(fullTree, exclude).filter((leaf) =>
|
|
leaf.pathLabel.toLowerCase().includes(q.toLowerCase())
|
|
)
|
|
}, [fullTree, exclude, searchQuery])
|
|
|
|
if (searchQuery.trim() && flatSearchHits.length > 0) {
|
|
return (
|
|
<ul className={`skill-tree skill-tree--flat-hits ${className}`.trim()} role="listbox">
|
|
{flatSearchHits.map((leaf) => (
|
|
<li key={leaf.id}>
|
|
<button
|
|
type="button"
|
|
className="skill-tree__pick skill-tree__pick--path"
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
onClick={() => onPickSkill(leaf.id, leaf.skill)}
|
|
>
|
|
<span className="skill-tree__pick-name">{leaf.label}</span>
|
|
<span className="skill-tree__pick-path">{leaf.pathLabel}</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<ul className={`skill-tree ${className}`.trim()} role="tree">
|
|
<SkillTreeNodes
|
|
nodes={displayTree}
|
|
depth={0}
|
|
expanded={expanded}
|
|
exclude={exclude}
|
|
onToggle={onToggle}
|
|
onPickSkill={onPickSkill}
|
|
pickMode={pickMode}
|
|
/>
|
|
</ul>
|
|
)
|
|
}
|