Fähigkeitauswahl verbessert #41

Merged
Lars merged 6 commits from develop into main 2026-05-20 11:24:38 +02:00
6 changed files with 79 additions and 23 deletions
Showing only changes of commit 9020e5eb16 - Show all commits

View File

@ -5320,6 +5320,12 @@ html.modal-scroll-locked .app-main {
line-height: 1.35;
}
.skill-tree__group-label--skill-group {
font-weight: 500;
color: var(--text2);
font-size: 0.84rem;
}
.multi-assoc-block {
border: 1px solid var(--border);
border-radius: 8px;

View File

@ -122,7 +122,6 @@ export default function SkillTreeMultiSelect({
searchQuery={query}
onPickSkill={(id) => addId(id)}
pickMode="multi"
defaultCollapsed
/>
) : (
<ul className="multiselect-combo__list" role="listbox">

View File

@ -4,6 +4,7 @@ import {
allExpandableKeys,
buildSkillCatalogTree,
collectSkillLeavesFromTree,
defaultExpandedKeysForSkillTree,
filterSkillTreeByQuery,
} from '../utils/skillCatalogTree'
@ -54,7 +55,13 @@ function SkillTreeNodes({ nodes, depth, expanded, exclude, onToggle, onPickSkill
) : (
<span className="skill-tree__toggle-spacer" aria-hidden />
)}
<span className={`skill-tree__group-label skill-tree__group-label--${node.type}`}>{node.label}</span>
<span
className={`skill-tree__group-label skill-tree__group-label--${node.type}${
node.type === 'skillGroup' ? ' skill-tree__group-label--skill-group' : ''
}`}
>
{node.label}
</span>
</div>
{hasKids && isOpen ? (
<ul className="skill-tree__children" role={pickMode === 'multi' ? 'group' : 'tree'}>
@ -75,7 +82,7 @@ function SkillTreeNodes({ nodes, depth, expanded, exclude, onToggle, onPickSkill
}
/**
* Ausklappbare Baumliste für Fähigkeiten (Hauptgruppe Kategorie Fähigkeit).
* Ausklappbare Baumliste: Hauptgruppe Kategorie (beide standard offen) Fähigkeiten-Gruppe (standard zu).
*/
export default function SkillTreePickerPanel({
skills = [],
@ -84,8 +91,6 @@ export default function SkillTreePickerPanel({
onPickSkill,
pickMode = 'single',
className = '',
/** true: nur Hauptgruppen sichtbar, bis der Nutzer aufklappt (Filter) */
defaultCollapsed = true,
}) {
const exclude = useMemo(() => normExclude(excludeIds), [excludeIds])
const fullTree = useMemo(() => buildSkillCatalogTree(skills), [skills])
@ -98,16 +103,16 @@ export default function SkillTreePickerPanel({
useEffect(() => {
if (searchQuery.trim()) {
setExpanded(new Set(allExpandableKeys(displayTree)))
} else if (defaultCollapsed) {
setExpanded(new Set())
} else {
setExpanded(new Set(defaultExpandedKeysForSkillTree(displayTree)))
}
}, [searchQuery, displayTree, defaultCollapsed])
}, [searchQuery, displayTree])
useEffect(() => {
if (!defaultCollapsed) {
setExpanded(new Set(allExpandableKeys(fullTree)))
if (!searchQuery.trim()) {
setExpanded(new Set(defaultExpandedKeysForSkillTree(fullTree)))
}
}, [fullTree, defaultCollapsed])
}, [fullTree, searchQuery])
const onToggle = useCallback((key) => {
setExpanded((prev) => {

View File

@ -75,7 +75,6 @@ export default function SkillTreeSelect({
searchQuery={query}
onPickSkill={pick}
pickMode="single"
defaultCollapsed={false}
/>
</div>
</div>

View File

@ -80,18 +80,33 @@ export function buildSkillCatalogTree(skills) {
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
return a.label.localeCompare(b.label, 'de')
})
.map((cat) => ({
key: cat.key,
type: cat.type,
label: cat.label,
children: [...cat.skills].sort(bySortThenName).map((skill) => ({
.map((cat) => {
const skillLeaves = [...cat.skills].sort(bySortThenName).map((skill) => ({
key: `s-${skill.id}`,
type: 'skill',
label: (skill.name || '').trim() || `#${skill.id}`,
skillId: Number(skill.id),
skill,
})),
})),
}))
const n = skillLeaves.length
return {
key: cat.key,
type: cat.type,
label: cat.label,
children:
n > 0
? [
{
key: `${cat.key}-skills`,
type: 'skillGroup',
label: n === 1 ? '1 Fähigkeit' : `${n} Fähigkeiten`,
skillCount: n,
children: skillLeaves,
},
]
: [],
}
}),
}))
}
@ -117,7 +132,8 @@ export function collectSkillLeavesFromTree(tree, excludeIds) {
out.push({ id: n.skillId, label: n.label, pathLabel, skill: n.skill })
}
} else if (n.children?.length) {
walk(n.children, [...pathParts, n.label])
const nextPath = n.type === 'skillGroup' ? pathParts : [...pathParts, n.label]
walk(n.children, nextPath)
}
}
}
@ -137,6 +153,9 @@ export function filterSkillTreeByQuery(tree, query) {
if (n.type === 'skill') {
const hay = `${n.label} ${skillCatalogPathLabel(n.skill)}`.toLowerCase()
if (hay.includes(q)) result.push(n)
} else if (n.type === 'skillGroup') {
const kids = filterNodes(n.children || [])
if (kids.length) result.push({ ...n, children: kids })
} else {
const kids = filterNodes(n.children || [])
if (kids.length) result.push({ ...n, children: kids })
@ -147,7 +166,7 @@ export function filterSkillTreeByQuery(tree, query) {
return filterNodes(tree)
}
/** @param {ReturnType<typeof buildSkillCatalogTree>} tree */
/** Alle aufklappbaren Knoten (inkl. Fähigkeiten-Gruppen unter Kategorien). */
export function allExpandableKeys(tree) {
const keys = []
const walk = (nodes) => {
@ -161,3 +180,18 @@ export function allExpandableKeys(tree) {
walk(tree)
return keys
}
/** Standard: Hauptgruppe + Kategorie offen, Fähigkeiten-Liste zugeklappt. */
export function defaultExpandedKeysForSkillTree(tree) {
const keys = []
const walk = (nodes) => {
for (const n of nodes) {
if (n.type === 'main' || n.type === 'category') {
keys.push(n.key)
walk(n.children || [])
}
}
}
walk(tree)
return keys
}

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
buildSkillCatalogTree,
collectSkillLeavesFromTree,
defaultExpandedKeysForSkillTree,
filterSkillTreeByQuery,
skillCatalogPathLabel,
} from './skillCatalogTree.js'
@ -38,7 +39,17 @@ describe('skillCatalogTree', () => {
expect(tree[0].label).toBe('Karate')
expect(tree[0].children).toHaveLength(2)
expect(tree[0].children[0].label).toBe('Kata')
expect(tree[0].children[0].children[0].skillId).toBe(2)
const kataSkills = tree[0].children[0].children[0]
expect(kataSkills.type).toBe('skillGroup')
expect(kataSkills.children[0].skillId).toBe(2)
})
it('defaultExpandedKeysForSkillTree opens main and category only', () => {
const tree = buildSkillCatalogTree(sample)
const keys = defaultExpandedKeysForSkillTree(tree)
expect(keys).toContain('m-10')
expect(keys.some((k) => k.includes('c-21'))).toBe(true)
expect(keys.some((k) => k.endsWith('-skills'))).toBe(false)
})
it('skillCatalogPathLabel', () => {
@ -55,6 +66,8 @@ describe('skillCatalogTree', () => {
it('filterSkillTreeByQuery', () => {
const tree = buildSkillCatalogTree(sample)
const filtered = filterSkillTreeByQuery(tree, 'dachi')
expect(filtered[0].children.some((c) => c.children?.some((s) => s.skillId === 1))).toBe(true)
const kihon = filtered[0].children.find((c) => c.label === 'Kihon')
const group = kihon?.children?.[0]
expect(group?.children?.some((s) => s.skillId === 1)).toBe(true)
})
})