Enhance Skill Tree Components with Improved Group Labeling and Default Expansion
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
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
- 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.
This commit is contained in:
parent
46feb4c867
commit
9020e5eb16
|
|
@ -5320,6 +5320,12 @@ html.modal-scroll-locked .app-main {
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skill-tree__group-label--skill-group {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text2);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
.multi-assoc-block {
|
.multi-assoc-block {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ export default function SkillTreeMultiSelect({
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
onPickSkill={(id) => addId(id)}
|
onPickSkill={(id) => addId(id)}
|
||||||
pickMode="multi"
|
pickMode="multi"
|
||||||
defaultCollapsed
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ul className="multiselect-combo__list" role="listbox">
|
<ul className="multiselect-combo__list" role="listbox">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
allExpandableKeys,
|
allExpandableKeys,
|
||||||
buildSkillCatalogTree,
|
buildSkillCatalogTree,
|
||||||
collectSkillLeavesFromTree,
|
collectSkillLeavesFromTree,
|
||||||
|
defaultExpandedKeysForSkillTree,
|
||||||
filterSkillTreeByQuery,
|
filterSkillTreeByQuery,
|
||||||
} from '../utils/skillCatalogTree'
|
} 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__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>
|
</div>
|
||||||
{hasKids && isOpen ? (
|
{hasKids && isOpen ? (
|
||||||
<ul className="skill-tree__children" role={pickMode === 'multi' ? 'group' : 'tree'}>
|
<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({
|
export default function SkillTreePickerPanel({
|
||||||
skills = [],
|
skills = [],
|
||||||
|
|
@ -84,8 +91,6 @@ export default function SkillTreePickerPanel({
|
||||||
onPickSkill,
|
onPickSkill,
|
||||||
pickMode = 'single',
|
pickMode = 'single',
|
||||||
className = '',
|
className = '',
|
||||||
/** true: nur Hauptgruppen sichtbar, bis der Nutzer aufklappt (Filter) */
|
|
||||||
defaultCollapsed = true,
|
|
||||||
}) {
|
}) {
|
||||||
const exclude = useMemo(() => normExclude(excludeIds), [excludeIds])
|
const exclude = useMemo(() => normExclude(excludeIds), [excludeIds])
|
||||||
const fullTree = useMemo(() => buildSkillCatalogTree(skills), [skills])
|
const fullTree = useMemo(() => buildSkillCatalogTree(skills), [skills])
|
||||||
|
|
@ -98,16 +103,16 @@ export default function SkillTreePickerPanel({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
setExpanded(new Set(allExpandableKeys(displayTree)))
|
setExpanded(new Set(allExpandableKeys(displayTree)))
|
||||||
} else if (defaultCollapsed) {
|
} else {
|
||||||
setExpanded(new Set())
|
setExpanded(new Set(defaultExpandedKeysForSkillTree(displayTree)))
|
||||||
}
|
}
|
||||||
}, [searchQuery, displayTree, defaultCollapsed])
|
}, [searchQuery, displayTree])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!defaultCollapsed) {
|
if (!searchQuery.trim()) {
|
||||||
setExpanded(new Set(allExpandableKeys(fullTree)))
|
setExpanded(new Set(defaultExpandedKeysForSkillTree(fullTree)))
|
||||||
}
|
}
|
||||||
}, [fullTree, defaultCollapsed])
|
}, [fullTree, searchQuery])
|
||||||
|
|
||||||
const onToggle = useCallback((key) => {
|
const onToggle = useCallback((key) => {
|
||||||
setExpanded((prev) => {
|
setExpanded((prev) => {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ export default function SkillTreeSelect({
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
onPickSkill={pick}
|
onPickSkill={pick}
|
||||||
pickMode="single"
|
pickMode="single"
|
||||||
defaultCollapsed={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -80,18 +80,33 @@ export function buildSkillCatalogTree(skills) {
|
||||||
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
|
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
|
||||||
return a.label.localeCompare(b.label, 'de')
|
return a.label.localeCompare(b.label, 'de')
|
||||||
})
|
})
|
||||||
.map((cat) => ({
|
.map((cat) => {
|
||||||
key: cat.key,
|
const skillLeaves = [...cat.skills].sort(bySortThenName).map((skill) => ({
|
||||||
type: cat.type,
|
|
||||||
label: cat.label,
|
|
||||||
children: [...cat.skills].sort(bySortThenName).map((skill) => ({
|
|
||||||
key: `s-${skill.id}`,
|
key: `s-${skill.id}`,
|
||||||
type: 'skill',
|
type: 'skill',
|
||||||
label: (skill.name || '').trim() || `#${skill.id}`,
|
label: (skill.name || '').trim() || `#${skill.id}`,
|
||||||
skillId: Number(skill.id),
|
skillId: Number(skill.id),
|
||||||
skill,
|
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 })
|
out.push({ id: n.skillId, label: n.label, pathLabel, skill: n.skill })
|
||||||
}
|
}
|
||||||
} else if (n.children?.length) {
|
} 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') {
|
if (n.type === 'skill') {
|
||||||
const hay = `${n.label} ${skillCatalogPathLabel(n.skill)}`.toLowerCase()
|
const hay = `${n.label} ${skillCatalogPathLabel(n.skill)}`.toLowerCase()
|
||||||
if (hay.includes(q)) result.push(n)
|
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 {
|
} else {
|
||||||
const kids = filterNodes(n.children || [])
|
const kids = filterNodes(n.children || [])
|
||||||
if (kids.length) result.push({ ...n, children: kids })
|
if (kids.length) result.push({ ...n, children: kids })
|
||||||
|
|
@ -147,7 +166,7 @@ export function filterSkillTreeByQuery(tree, query) {
|
||||||
return filterNodes(tree)
|
return filterNodes(tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {ReturnType<typeof buildSkillCatalogTree>} tree */
|
/** Alle aufklappbaren Knoten (inkl. Fähigkeiten-Gruppen unter Kategorien). */
|
||||||
export function allExpandableKeys(tree) {
|
export function allExpandableKeys(tree) {
|
||||||
const keys = []
|
const keys = []
|
||||||
const walk = (nodes) => {
|
const walk = (nodes) => {
|
||||||
|
|
@ -161,3 +180,18 @@ export function allExpandableKeys(tree) {
|
||||||
walk(tree)
|
walk(tree)
|
||||||
return keys
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
buildSkillCatalogTree,
|
buildSkillCatalogTree,
|
||||||
collectSkillLeavesFromTree,
|
collectSkillLeavesFromTree,
|
||||||
|
defaultExpandedKeysForSkillTree,
|
||||||
filterSkillTreeByQuery,
|
filterSkillTreeByQuery,
|
||||||
skillCatalogPathLabel,
|
skillCatalogPathLabel,
|
||||||
} from './skillCatalogTree.js'
|
} from './skillCatalogTree.js'
|
||||||
|
|
@ -38,7 +39,17 @@ describe('skillCatalogTree', () => {
|
||||||
expect(tree[0].label).toBe('Karate')
|
expect(tree[0].label).toBe('Karate')
|
||||||
expect(tree[0].children).toHaveLength(2)
|
expect(tree[0].children).toHaveLength(2)
|
||||||
expect(tree[0].children[0].label).toBe('Kata')
|
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', () => {
|
it('skillCatalogPathLabel', () => {
|
||||||
|
|
@ -55,6 +66,8 @@ describe('skillCatalogTree', () => {
|
||||||
it('filterSkillTreeByQuery', () => {
|
it('filterSkillTreeByQuery', () => {
|
||||||
const tree = buildSkillCatalogTree(sample)
|
const tree = buildSkillCatalogTree(sample)
|
||||||
const filtered = filterSkillTreeByQuery(tree, 'dachi')
|
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user