enhanced generic navigation tree
This commit is contained in:
parent
2a1cd2ef64
commit
4231727544
11 changed files with 193 additions and 161 deletions
|
|
@ -8,26 +8,24 @@
|
||||||
* Backend liefert Blocks-Struktur mit Static und Dynamic Blocks.
|
* Backend liefert Blocks-Struktur mit Static und Dynamic Blocks.
|
||||||
* UI mappt uiComponent zu Icons via pageRegistry.
|
* UI mappt uiComponent zu Icons via pageRegistry.
|
||||||
*
|
*
|
||||||
* FLAT STRUCTURE (kompakte Darstellung):
|
* TREE STRUCTURE (alles collapsible):
|
||||||
* - SYSTEM (static block)
|
* ▼ Meine Sicht
|
||||||
* - Mandant 1
|
* - Übersicht, Einstellungen, Prompts, Dateien, Verbindungen, Billing
|
||||||
|
* ─────────────
|
||||||
|
* ▼ Mandant 1
|
||||||
* - 🎯 Instanz 1 (Feature-Icon + Instanz-Name)
|
* - 🎯 Instanz 1 (Feature-Icon + Instanz-Name)
|
||||||
* - 💼 Instanz 2 (Feature-Icon + Instanz-Name)
|
* - 💼 Instanz 2 (Feature-Icon + Instanz-Name)
|
||||||
* - BASISDATEN (static block)
|
* ─────────────
|
||||||
* - ADMINISTRATION (static block)
|
* ▶ Administration
|
||||||
*
|
* - Users, Mandates, Roles, ...
|
||||||
* Jede Instanz zeigt das Icon des zugehörigen Features.
|
|
||||||
* Keine Gruppierung nach Features - direkt Instanzen unter Mandant.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useNavigation } from '../../hooks/useNavigation';
|
import { useNavigation } from '../../hooks/useNavigation';
|
||||||
import type {
|
import type {
|
||||||
StaticBlock,
|
|
||||||
DynamicBlock,
|
DynamicBlock,
|
||||||
NavigationItem,
|
NavigationItem,
|
||||||
NavigationMandate,
|
NavigationMandate,
|
||||||
MandateFeature,
|
|
||||||
FeatureInstance,
|
FeatureInstance,
|
||||||
FeatureView
|
FeatureView
|
||||||
} from '../../hooks/useNavigation';
|
} from '../../hooks/useNavigation';
|
||||||
|
|
@ -53,13 +51,20 @@ function navigationItemToTreeNode(item: NavigationItem): TreeNodeItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a StaticBlock to TreeItem (section)
|
* Convert a list of NavigationItems into a collapsible TreeNodeItem container.
|
||||||
|
* Used for grouping static items under "Meine Sicht" and "Administration".
|
||||||
*/
|
*/
|
||||||
function staticBlockToTreeItem(block: StaticBlock): TreeItem {
|
function _staticItemsToTreeNode(
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
items: NavigationItem[],
|
||||||
|
defaultExpanded: boolean = true,
|
||||||
|
): TreeNodeItem {
|
||||||
return {
|
return {
|
||||||
type: 'section',
|
id,
|
||||||
title: block.title,
|
label,
|
||||||
children: block.items.map(navigationItemToTreeNode),
|
children: items.map(navigationItemToTreeNode),
|
||||||
|
defaultExpanded,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,38 +172,47 @@ export const MandateNavigation: React.FC = () => {
|
||||||
const { blocks, loading } = useNavigation('de');
|
const { blocks, loading } = useNavigation('de');
|
||||||
|
|
||||||
// Build navigation items from blocks
|
// Build navigation items from blocks
|
||||||
|
// Groups static items into collapsible containers:
|
||||||
|
// - "Meine Sicht": all non-admin static items (Übersicht, Einstellungen, Prompts, etc.)
|
||||||
|
// - "Administration": all admin static items
|
||||||
|
// - Dynamic block (mandates) renders between them
|
||||||
const navigationItems: TreeItem[] = useMemo(() => {
|
const navigationItems: TreeItem[] = useMemo(() => {
|
||||||
const items: TreeItem[] = [];
|
const items: TreeItem[] = [];
|
||||||
|
|
||||||
// Process blocks in order (already sorted by backend)
|
// Collect static items by category
|
||||||
|
const meineSichtItems: NavigationItem[] = [];
|
||||||
|
let adminItems: NavigationItem[] = [];
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.type === 'static') {
|
if (block.type === 'static') {
|
||||||
// Static block: system, workflows, basedata, migrate, admin
|
if (block.id === 'admin') {
|
||||||
if (block.items.length > 0) {
|
adminItems = [...block.items];
|
||||||
// Add separator before admin block
|
} else if (block.items.length > 0) {
|
||||||
if (block.id === 'admin') {
|
meineSichtItems.push(...block.items);
|
||||||
items.push({ type: 'separator' });
|
|
||||||
}
|
|
||||||
items.push(staticBlockToTreeItem(block));
|
|
||||||
}
|
}
|
||||||
} else if (block.type === 'dynamic') {
|
|
||||||
// Dynamic block: features/mandates
|
|
||||||
// Add separator before dynamic block
|
|
||||||
items.push({ type: 'separator' });
|
|
||||||
|
|
||||||
const mandateNodes = dynamicBlockToTreeNodes(block);
|
|
||||||
if (mandateNodes.length > 0) {
|
|
||||||
items.push(...mandateNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add separator after dynamic block (before next static blocks)
|
|
||||||
items.push({ type: 'separator' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove trailing separator if present
|
// "Meine Sicht" - collapsible container for user-facing pages
|
||||||
while (items.length > 0 && (items[items.length - 1] as TreeItem & { type?: string }).type === 'separator') {
|
if (meineSichtItems.length > 0) {
|
||||||
items.pop();
|
items.push(_staticItemsToTreeNode('meine-sicht', 'Meine Sicht', meineSichtItems, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic block: mandates with feature instances
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.type === 'dynamic') {
|
||||||
|
const mandateNodes = dynamicBlockToTreeNodes(block);
|
||||||
|
if (mandateNodes.length > 0) {
|
||||||
|
if (items.length > 0) items.push({ type: 'separator' });
|
||||||
|
items.push(...mandateNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Administration" - collapsible container for admin pages
|
||||||
|
if (adminItems.length > 0) {
|
||||||
|
if (items.length > 0) items.push({ type: 'separator' });
|
||||||
|
items.push(_staticItemsToTreeNode('administration', 'Administration', adminItems, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* TreeNavigation Styles
|
* TreeNavigation Styles
|
||||||
*
|
*
|
||||||
* Flexible hierarchical navigation with support for:
|
* Modern minimal tree navigation (Notion/Linear style):
|
||||||
* - Dynamic sublevels
|
* - CSS-only disclosure triangle with smooth rotation
|
||||||
* - Sections and separators
|
* - No guide lines — clean indentation only
|
||||||
* - Various visual states (active, disabled, hover)
|
* - Depth-aware sizing via data-depth attribute
|
||||||
|
* - Hover-reveal toggle for subtle UX
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.treeNavigation {
|
.treeNavigation {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 1px;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,8 +21,8 @@
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--border-color, #e0e0e0);
|
background: var(--border-color, #e2e8f0);
|
||||||
margin: 0.75rem 0.5rem;
|
margin: 0.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
@ -29,7 +30,7 @@
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
||||||
.treeSection {
|
.treeSection {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
|
|
@ -40,14 +41,14 @@
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
color: var(--text-tertiary, #888);
|
color: var(--text-tertiary, #94a3b8);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionContent {
|
.sectionContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
@ -62,9 +63,9 @@
|
||||||
.treeNode {
|
.treeNode {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.375rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.375rem 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -72,9 +73,11 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--text-secondary, #666);
|
color: var(--text-secondary, #64748b);
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
transition: all 0.15s ease;
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeNode:hover {
|
.treeNode:hover {
|
||||||
|
|
@ -82,12 +85,18 @@
|
||||||
color: var(--text-primary, #1a1a1a);
|
color: var(--text-primary, #1a1a1a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Leaf node active — strong pill highlight */
|
||||||
.treeNode.active {
|
.treeNode.active {
|
||||||
background: var(--primary-light, #e0e7ff);
|
background: var(--primary-light, #e0e7ff);
|
||||||
color: var(--primary-color, #2563eb);
|
color: var(--primary-color, #2563eb);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Group/parent active — subtle text color only, no background */
|
||||||
|
.treeNode.activeGroup {
|
||||||
|
color: var(--primary-color, #2563eb);
|
||||||
|
}
|
||||||
|
|
||||||
.treeNode.disabled {
|
.treeNode.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
@ -95,105 +104,109 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
/* LEVEL-SPECIFIC STYLES */
|
/* DEPTH-SPECIFIC STYLES (via data-depth) */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
||||||
/* Root level (level 0) */
|
.treeNode[data-depth="0"] {
|
||||||
.levelRoot {
|
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #1a1a1a);
|
color: var(--text-primary, #1a1a1a);
|
||||||
padding: 0.625rem 0.75rem;
|
padding: 0.5rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.levelRoot .nodeLabel {
|
.treeNode[data-depth="1"] {
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Level 1 */
|
|
||||||
.levelOne {
|
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-secondary, #666);
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Level 2 */
|
.treeNode[data-depth="2"],
|
||||||
.levelTwo {
|
.treeNode[data-depth="3"],
|
||||||
|
.treeNode[data-depth="4"],
|
||||||
|
.treeNode[data-depth="5"] {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 400;
|
||||||
color: var(--text-secondary, #666);
|
|
||||||
padding: 0.375rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Level 3 */
|
|
||||||
.levelThree {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary, #666);
|
|
||||||
padding: 0.375rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Deep levels (4+) */
|
|
||||||
.levelDeep {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: var(--text-tertiary, #888);
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
/* NODE CHILDREN (INDENTATION) */
|
/* NODE CHILDREN (INDENTATION ONLY) */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
||||||
.treeNodeChildren {
|
.treeNodeChildren {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.75rem;
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.5rem;
|
||||||
border-left: 2px solid var(--border-color, #e0e0e0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Active parent highlights the border */
|
/* ============================================ */
|
||||||
.treeNodeContainer:has(> .treeNode.active) > .treeNodeChildren {
|
/* TOGGLE (CSS-only disclosure triangle) */
|
||||||
|
/* ============================================ */
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.125rem;
|
||||||
|
height: 1.125rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The triangle — pure CSS, no icon needed */
|
||||||
|
.toggle::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 4.5px solid var(--text-tertiary, #94a3b8);
|
||||||
|
border-top: 3.5px solid transparent;
|
||||||
|
border-bottom: 3.5px solid transparent;
|
||||||
|
transition: transform 0.2s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rotate triangle when expanded */
|
||||||
|
.toggleExpanded::after {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover feedback */
|
||||||
|
.toggle:hover {
|
||||||
|
background: var(--hover-bg, rgba(0, 0, 0, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle:hover::after {
|
||||||
|
border-left-color: var(--text-primary, #1a1a1a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active node toggle */
|
||||||
|
.treeNode.active .toggle::after,
|
||||||
|
.treeNode.activeGroup .toggle::after {
|
||||||
border-left-color: var(--primary-color, #2563eb);
|
border-left-color: var(--primary-color, #2563eb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Also highlight if any descendant is active */
|
/* Spacer for leaf nodes (keeps alignment with toggle nodes) */
|
||||||
.treeNodeContainer:has(.treeNode.active) > .treeNodeChildren {
|
.toggleSpacer {
|
||||||
border-left-color: var(--primary-light, #93c5fd);
|
width: 1.125rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
/* NODE ELEMENTS */
|
/* NODE ELEMENTS */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
||||||
.chevron {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
font-size: 0.625rem;
|
|
||||||
color: var(--text-tertiary, #888);
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: background 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron:hover {
|
|
||||||
background: var(--hover-bg, rgba(0, 0, 0, 0.06));
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevronSpacer {
|
|
||||||
width: 1rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeIcon {
|
.nodeIcon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 1rem;
|
font-size: 0.875rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeNode.active .nodeIcon,
|
||||||
|
.treeNode.activeGroup .nodeIcon {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodeLabel {
|
.nodeLabel {
|
||||||
|
|
@ -208,7 +221,7 @@
|
||||||
padding: 0.0625rem 0.375rem;
|
padding: 0.0625rem 0.375rem;
|
||||||
background: var(--surface-color, #f0f0f0);
|
background: var(--surface-color, #f0f0f0);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
color: var(--text-tertiary, #888);
|
color: var(--text-tertiary, #94a3b8);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.025em;
|
letter-spacing: 0.025em;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
@ -262,41 +275,45 @@
|
||||||
color: var(--primary-light, #93c5fd);
|
color: var(--primary-light, #93c5fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .levelRoot {
|
:global(.dark-theme) .treeNode.activeGroup {
|
||||||
|
color: var(--primary-light, #93c5fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark-theme) .treeNode[data-depth="0"] {
|
||||||
color: var(--text-primary-dark, #fff);
|
color: var(--text-primary-dark, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .levelOne,
|
:global(.dark-theme) .toggle::after {
|
||||||
:global(.dark-theme) .levelTwo,
|
border-left-color: var(--text-tertiary-dark, #555);
|
||||||
:global(.dark-theme) .levelThree {
|
|
||||||
color: var(--text-secondary-dark, #aaa);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .levelDeep {
|
:global(.dark-theme) .toggle:hover {
|
||||||
color: var(--text-tertiary-dark, #888);
|
background: var(--hover-bg-dark, rgba(255, 255, 255, 0.08));
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .treeNodeChildren {
|
:global(.dark-theme) .toggle:hover::after {
|
||||||
border-left-color: var(--border-dark, #444);
|
border-left-color: var(--text-primary-dark, #ddd);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .treeNodeContainer:has(.treeNode.active) > .treeNodeChildren {
|
:global(.dark-theme) .treeNode.active .toggle::after,
|
||||||
|
:global(.dark-theme) .treeNode.activeGroup .toggle::after {
|
||||||
border-left-color: var(--primary-light, #93c5fd);
|
border-left-color: var(--primary-light, #93c5fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark-theme) .nodeIcon {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark-theme) .treeNode.active .nodeIcon,
|
||||||
|
:global(.dark-theme) .treeNode.activeGroup .nodeIcon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .nodeBadge {
|
:global(.dark-theme) .nodeBadge {
|
||||||
background: var(--surface-dark, #2a2a2a);
|
background: var(--surface-dark, #2a2a2a);
|
||||||
color: var(--text-tertiary-dark, #888);
|
color: var(--text-tertiary-dark, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark-theme) .chevron {
|
|
||||||
color: var(--text-tertiary-dark, #666);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark-theme) .chevron:hover {
|
|
||||||
background: var(--hover-bg-dark, rgba(255, 255, 255, 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark-theme) .treeNode.active .nodeBadge {
|
:global(.dark-theme) .treeNode.active .nodeBadge {
|
||||||
background: var(--primary-color, #2563eb);
|
background: var(--primary-color, #2563eb);
|
||||||
color: white;
|
color: white;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
|
|
||||||
import React, { useState, useEffect, ReactNode } from 'react';
|
import React, { useState, useEffect, ReactNode } from 'react';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
|
|
||||||
import styles from './TreeNavigation.module.css';
|
import styles from './TreeNavigation.module.css';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -155,8 +154,11 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
}
|
}
|
||||||
}, [currentPath, autoExpandActive, node]);
|
}, [currentPath, autoExpandActive, node]);
|
||||||
|
|
||||||
// Check if this exact node is active
|
// Check if this node is active (exact match or ancestor of active path)
|
||||||
const isActive = node.path ? currentPath === node.path || currentPath.startsWith(node.path + '/') : false;
|
const isActive = node.path ? currentPath === node.path || currentPath.startsWith(node.path + '/') : false;
|
||||||
|
// Differentiate: leaf active (strong highlight) vs group active (subtle text only)
|
||||||
|
const isLeafActive = isActive && !hasChildren;
|
||||||
|
const isGroupActive = isActive && !!hasChildren;
|
||||||
|
|
||||||
// Handle click
|
// Handle click
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
|
@ -186,34 +188,24 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle chevron click separately
|
// Handle toggle click separately (expand/collapse)
|
||||||
const handleChevronClick = (e: React.MouseEvent) => {
|
const handleToggleClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsExpanded(!isExpanded);
|
setIsExpanded(!isExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get level-specific styles
|
|
||||||
const getLevelClass = () => {
|
|
||||||
switch (level) {
|
|
||||||
case 0: return styles.levelRoot;
|
|
||||||
case 1: return styles.levelOne;
|
|
||||||
case 2: return styles.levelTwo;
|
|
||||||
case 3: return styles.levelThree;
|
|
||||||
default: return styles.levelDeep;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render the node content
|
// Render the node content
|
||||||
const nodeContent = (
|
const nodeContent = (
|
||||||
<>
|
<>
|
||||||
{isExpandable && (
|
{isExpandable && (
|
||||||
<span className={styles.chevron} onClick={handleChevronClick}>
|
<span
|
||||||
{isExpanded ? <FaChevronDown /> : <FaChevronRight />}
|
className={`${styles.toggle} ${isExpanded ? styles.toggleExpanded : ''}`}
|
||||||
</span>
|
onClick={handleToggleClick}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!isExpandable && hasChildren === false && (
|
{!isExpandable && hasChildren === false && (
|
||||||
<span className={styles.chevronSpacer} />
|
<span className={styles.toggleSpacer} />
|
||||||
)}
|
)}
|
||||||
{node.icon && <span className={styles.nodeIcon}>{node.icon}</span>}
|
{node.icon && <span className={styles.nodeIcon}>{node.icon}</span>}
|
||||||
<span className={styles.nodeLabel} title={node.label}>{node.label}</span>
|
<span className={styles.nodeLabel} title={node.label}>{node.label}</span>
|
||||||
|
|
@ -228,7 +220,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine if we should render as NavLink or button
|
// Determine if we should render as NavLink or button
|
||||||
const nodeClasses = `${styles.treeNode} ${getLevelClass()} ${isActive ? styles.active : ''} ${node.disabled ? styles.disabled : ''} ${node.className || ''}`;
|
const nodeClasses = `${styles.treeNode} ${isLeafActive ? styles.active : ''} ${isGroupActive ? styles.activeGroup : ''} ${node.disabled ? styles.disabled : ''} ${node.className || ''}`;
|
||||||
|
|
||||||
const nodeElement = node.path ? (
|
const nodeElement = node.path ? (
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|
@ -236,6 +228,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
className={nodeClasses}
|
className={nodeClasses}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
data-id={node.dataId}
|
data-id={node.dataId}
|
||||||
|
data-depth={level}
|
||||||
>
|
>
|
||||||
{nodeContent}
|
{nodeContent}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
@ -246,6 +239,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={node.disabled}
|
disabled={node.disabled}
|
||||||
data-id={node.dataId}
|
data-id={node.dataId}
|
||||||
|
data-depth={level}
|
||||||
>
|
>
|
||||||
{nodeContent}
|
{nodeContent}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export interface Role {
|
||||||
export interface Mandate {
|
export interface Mandate {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | { [key: string]: string };
|
name: string | { [key: string]: string };
|
||||||
|
label?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
isSystem?: boolean;
|
isSystem?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { FeatureInstanceWizard } from './FeatureInstanceWizard';
|
||||||
import { InstanceHierarchyView } from './InstanceHierarchyView';
|
import { InstanceHierarchyView } from './InstanceHierarchyView';
|
||||||
|
|
||||||
function getMandateName(mandate: Mandate): string {
|
function getMandateName(mandate: Mandate): string {
|
||||||
|
if (mandate.label) return mandate.label;
|
||||||
if (typeof mandate.name === 'object') {
|
if (typeof mandate.name === 'object') {
|
||||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
|
|
||||||
// Get mandate name
|
// Get mandate name
|
||||||
const getMandateName = (mandate: Mandate) => {
|
const getMandateName = (mandate: Mandate) => {
|
||||||
|
if (mandate.label) return mandate.label;
|
||||||
if (typeof mandate.name === 'object') {
|
if (typeof mandate.name === 'object') {
|
||||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
allOptions.push({
|
allOptions.push({
|
||||||
mandateId: mandate.id,
|
mandateId: mandate.id,
|
||||||
instanceId: inst.id,
|
instanceId: inst.id,
|
||||||
mandateName: typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id),
|
mandateName: mandate.label || (typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id)),
|
||||||
instanceLabel: inst.label || inst.id,
|
instanceLabel: inst.label || inst.id,
|
||||||
featureCode: inst.featureCode,
|
featureCode: inst.featureCode,
|
||||||
combinedKey: `${mandate.id}:${inst.id}`,
|
combinedKey: `${mandate.id}:${inst.id}`,
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
||||||
|
|
||||||
// Get mandate name
|
// Get mandate name
|
||||||
const getMandateName = (mandate: Mandate) => {
|
const getMandateName = (mandate: Mandate) => {
|
||||||
|
if (mandate.label) return mandate.label;
|
||||||
if (typeof mandate.name === 'object') {
|
if (typeof mandate.name === 'object') {
|
||||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
||||||
|
|
||||||
// Get mandate name
|
// Get mandate name
|
||||||
const getMandateName = (mandate: Mandate) => {
|
const getMandateName = (mandate: Mandate) => {
|
||||||
|
if (mandate.label) return mandate.label;
|
||||||
if (typeof mandate.name === 'object') {
|
if (typeof mandate.name === 'object') {
|
||||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
||||||
|
|
||||||
// Get mandate name
|
// Get mandate name
|
||||||
const getMandateName = (mandate: Mandate) => {
|
const getMandateName = (mandate: Mandate) => {
|
||||||
|
if (mandate.label) return mandate.label;
|
||||||
if (typeof mandate.name === 'object') {
|
if (typeof mandate.name === 'object') {
|
||||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,8 @@ export interface MandateFeature {
|
||||||
*/
|
*/
|
||||||
export interface Mandate {
|
export interface Mandate {
|
||||||
id: string; // mandateId
|
id: string; // mandateId
|
||||||
name: string; // Anzeige-Name
|
name: string; // Technischer Identifier
|
||||||
|
label?: string; // Anzeige-Label (fuer FK-Referenzen und UI)
|
||||||
code?: string; // Optionaler Code
|
code?: string; // Optionaler Code
|
||||||
features: MandateFeature[];
|
features: MandateFeature[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue