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.
182 lines
No EOL
8.2 KiB
TypeScript
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; |