ui-nyla/src/components/Sidebar/SidebarSubmenu.tsx
ValueOn AG b2c38e75bf Fixed UI issues:
Data: roles & rules: fixed id and pydantic handling ui+gateway
Table generic: Fixed column width changing with persistent user width not overwritten by default in the session
Table generic: fixed active item in navbar working with subpaths
Table generic: fixed sort and pagination with modified routes
Table generic: enhanced logic to select page directly and bar on top of the table
Table generic: Always load data with pagination, fixed page counter (not updated), enhanced pagination to access pages directly
Table generic: Filter function enhanced directly as dropdown in column headers, sorting enhanced to have cascading sorting
Table generic: Added multilingual to AttributeType and handle it in the form renderer, removed explicit field definitions from team-members.ts and prompts.ts to use dynamic generation from backend attributes strictly
Table generic: Inline editting of checkboxes and boolean values
Table generig: Only generic logic, no explicit logic (e.g. id's)
Table generic: Removed all specific parts like action icons. To e a parameter by calling instance.
2026-01-13 20:01:27 +01:00

182 lines
No EOL
8.2 KiB
TypeScript

import styles from './SidebarStyles/SidebarSubmenu.module.css';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import React, { useRef, useEffect, useState } from 'react';
import { SidebarSubmenuProps, SidebarSubmenuItemData } from './sidebarTypes';
// Separate component for submenu item to properly use hooks
interface SubmenuItemProps {
subitem: SidebarSubmenuItemData;
isActive: boolean;
}
const SubmenuItem: React.FC<SubmenuItemProps> = ({ subitem, isActive }) => {
const textRef = useRef<HTMLSpanElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isOverflowing, setIsOverflowing] = useState(false);
useEffect(() => {
const checkOverflow = () => {
if (textRef.current && containerRef.current) {
const textWidth = textRef.current.scrollWidth;
const containerWidth = containerRef.current.clientWidth;
setIsOverflowing(textWidth > containerWidth);
}
};
checkOverflow();
// Also check on window resize
window.addEventListener('resize', checkOverflow);
return () => window.removeEventListener('resize', checkOverflow);
}, [subitem.name]);
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
return (
<li className={isActive ? styles.active : ''}>
<Link
to={subitem.link || '#'}
title={subitem.name}
className={isActive ? styles.activeLink : ''}
>
<div
ref={containerRef}
className={styles.textContainer}
>
<motion.span
ref={textRef}
style={{
display: 'block',
whiteSpace: 'nowrap',
}}
initial={{ x: 0 }}
animate={{ x: 0 }}
{...(isOverflowing && {
whileHover: {
x: -(textRef.current?.scrollWidth || 0) + (containerRef.current?.clientWidth || 153),
transition: {
duration: 2,
ease: "linear"
}
}
})}
>
<div style={{ display: 'flex', alignItems: 'center', paddingRight: '10px' }}>
{SubIcon && <SubIcon className={styles.submenuIcon} />}
<span style={{ marginLeft: SubIcon ? '8px' : '0' }}>
{subitem.name}
</span>
</div>
</motion.span>
</div>
</Link>
</li>
);
};
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen, isMinimized = false }) => {
const location = useLocation();
// Check if a submenu item is active
const isSubmenuItemActive = (itemPath?: string) => {
if (!itemPath) return false;
const currentPath = location.pathname;
// Exact match or prefix match at path segment boundary
if (currentPath === itemPath) return true;
if (currentPath.startsWith(itemPath)) {
const nextChar = currentPath[itemPath.length];
if (nextChar === '/' || nextChar === undefined) return true;
}
return false;
};
if (!item.submenu) return null;
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{
height: "auto",
opacity: 1,
transition: {
height: { duration: 0.3, ease: "easeInOut", delay: 0 },
opacity: { duration: 0.25, ease: "easeInOut", delay: 0.3 }
}
}}
exit={{
height: 0,
opacity: 0,
transition: {
opacity: { duration: 0.25, ease: "easeInOut", delay: 0 },
height: { duration: 0.3, ease: "easeInOut", delay: 0.25 }
}
}}
style={{ overflow: "hidden" }}
className={`${styles.submenu} ${isMinimized ? styles.minimized : ''}`}
>
{isMinimized ? (
// Horizontal layout for minimized sidebar
<motion.div
className={styles.submenuHorizontalContainer}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.3, duration: 0.25 } }}
exit={{ opacity: 0, transition: { duration: 0.25, delay: 0 } }}
>
<ul className={styles.submenuHorizontalList}>
{item.submenu.map(subitem => {
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
const isActive = isSubmenuItemActive(subitem.link);
return (
<li key={subitem.id} className={`${styles.submenuHorizontalItem} ${isActive ? styles.active : ''}`}>
<Link
to={subitem.link || '#'}
title={subitem.name}
className={`${styles.submenuHorizontalLink} ${isActive ? styles.activeLink : ''}`}
>
{SubIcon && (
<SubIcon
className={styles.submenuHorizontalIcon}
style={{
width: '16px',
height: '16px',
color: isActive ? 'white' : '#181818',
display: 'block'
}}
/>
)}
</Link>
</li>
);
})}
</ul>
</motion.div>
) : (
// Vertical layout for expanded sidebar
<motion.div
className={styles.submenuLineContainer}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.3, duration: 0.25 } }}
exit={{ opacity: 0, transition: { duration: 0.25, delay: 0 } }}
>
<ul className={styles.submenuList}>
{item.submenu.map(subitem => (
<SubmenuItem
key={subitem.id}
subitem={subitem}
isActive={isSubmenuItemActive(subitem.link)}
/>
))}
</ul>
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>
);
};
export default SidebarSubmenu;