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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
|
|||
|
|
@ -122,7 +122,6 @@ export default function SkillTreeMultiSelect({
|
|||
searchQuery={query}
|
||||
onPickSkill={(id) => addId(id)}
|
||||
pickMode="multi"
|
||||
defaultCollapsed
|
||||
/>
|
||||
) : (
|
||||
<ul className="multiselect-combo__list" role="listbox">
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ export default function SkillTreeSelect({
|
|||
searchQuery={query}
|
||||
onPickSkill={pick}
|
||||
pickMode="single"
|
||||
defaultCollapsed={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user