diff --git a/frontend/src/app.css b/frontend/src/app.css
index 29877e8..2900609 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -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;
diff --git a/frontend/src/components/SkillTreeMultiSelect.jsx b/frontend/src/components/SkillTreeMultiSelect.jsx
index d6d6358..98dc4c0 100644
--- a/frontend/src/components/SkillTreeMultiSelect.jsx
+++ b/frontend/src/components/SkillTreeMultiSelect.jsx
@@ -122,7 +122,6 @@ export default function SkillTreeMultiSelect({
searchQuery={query}
onPickSkill={(id) => addId(id)}
pickMode="multi"
- defaultCollapsed
/>
) : (
diff --git a/frontend/src/components/SkillTreePickerPanel.jsx b/frontend/src/components/SkillTreePickerPanel.jsx
index 3b55257..d6647fb 100644
--- a/frontend/src/components/SkillTreePickerPanel.jsx
+++ b/frontend/src/components/SkillTreePickerPanel.jsx
@@ -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
) : (
)}
- {node.label}
+
+ {node.label}
+
{hasKids && isOpen ? (
@@ -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) => {
diff --git a/frontend/src/components/SkillTreeSelect.jsx b/frontend/src/components/SkillTreeSelect.jsx
index 728e93b..9ef6d1c 100644
--- a/frontend/src/components/SkillTreeSelect.jsx
+++ b/frontend/src/components/SkillTreeSelect.jsx
@@ -75,7 +75,6 @@ export default function SkillTreeSelect({
searchQuery={query}
onPickSkill={pick}
pickMode="single"
- defaultCollapsed={false}
/>
diff --git a/frontend/src/utils/skillCatalogTree.js b/frontend/src/utils/skillCatalogTree.js
index e4578c6..89e4664 100644
--- a/frontend/src/utils/skillCatalogTree.js
+++ b/frontend/src/utils/skillCatalogTree.js
@@ -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} 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
+}
diff --git a/frontend/src/utils/skillCatalogTree.test.js b/frontend/src/utils/skillCatalogTree.test.js
index 6ddc8ff..c6477d7 100644
--- a/frontend/src/utils/skillCatalogTree.test.js
+++ b/frontend/src/utils/skillCatalogTree.test.js
@@ -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)
})
})