shinkan-jinkendo/frontend/src/components/SkillTreePickerPanel.jsx
Lars ab612a5335
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
Update Skill Tree Picker and Catalog Functions for Clarity
- 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.
2026-05-20 11:21:18 +02:00

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