Skip to content

Commit 82047bb

Browse files
committed
update on menu UI
1 parent d511944 commit 82047bb

File tree

5 files changed

+179
-140
lines changed

5 files changed

+179
-140
lines changed

frontend/app/src/components/ui/dropdown-menu.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ export const DropdownMenuAccordionTrigger = forwardRef<
122122
ref={ref}
123123
onSelect={(e) => {
124124
e.preventDefault();
125-
e.stopPropagation();
126125
}}
127126
asChild
128127
>
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import { ButtonProps, ButtonWithTooltip } from "@/components/buttons/button-primitive";
22
import { classNames } from "@/utils/common";
33
import { Icon } from "@iconify-icon/react";
4+
import { forwardRef } from "react";
45

56
export interface CollapsedButton extends ButtonProps {
67
tooltipContent: string;
78
icon: string;
89
}
910

10-
export const CollapsedButton = ({ className, icon, ...props }: CollapsedButton) => {
11-
return (
12-
<ButtonWithTooltip
13-
variant="ghost"
14-
size="square"
15-
side="right"
16-
tooltipEnabled
17-
className={classNames("w-10 h-10", className)}
18-
{...props}
19-
>
20-
<Icon icon={icon} className="text-xl" />
21-
</ButtonWithTooltip>
22-
);
23-
};
11+
export const CollapsedButton = forwardRef<HTMLButtonElement, CollapsedButton>(
12+
({ className, icon, ...props }, ref) => {
13+
return (
14+
<ButtonWithTooltip
15+
ref={ref}
16+
variant="ghost"
17+
size="square"
18+
side="right"
19+
tooltipEnabled
20+
className={classNames("w-10 h-10 p-2", className)}
21+
{...props}
22+
>
23+
<Icon icon={icon} className="text-base" />
24+
</ButtonWithTooltip>
25+
);
26+
}
27+
);

frontend/app/src/screens/layout/menu-navigation/components/menu-section-internal.tsx

Lines changed: 68 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -9,86 +9,94 @@ import {
99
} from "@/components/ui/dropdown-menu";
1010
import { CollapsedButton } from "@/screens/layout/menu-navigation/components/collapsed-button";
1111
import { menuNavigationItemStyle } from "@/screens/layout/menu-navigation/styles";
12-
import { MenuItem } from "@/screens/layout/menu-navigation/types";
12+
import type { MenuItem } from "@/screens/layout/menu-navigation/types";
1313
import { classNames } from "@/utils/common";
1414
import { constructPath } from "@/utils/fetch";
1515
import { Icon } from "@iconify-icon/react";
16+
import React from "react";
1617
import { Link } from "react-router-dom";
1718

1819
export interface MenuSectionInternalProps {
1920
items: MenuItem[];
2021
isCollapsed?: boolean;
2122
}
2223

24+
const RecursiveDropdownMenuItem: React.FC<{ item: MenuItem }> = ({ item }) => {
25+
if (!item.children?.length) {
26+
return (
27+
<DropdownMenuItem key={item.identifier} asChild>
28+
<Link to={constructPath(item.path)}>{item.label}</Link>
29+
</DropdownMenuItem>
30+
);
31+
}
32+
33+
return (
34+
<DropdownMenuSub key={item.identifier}>
35+
<DropdownMenuSubTrigger>{item.label}</DropdownMenuSubTrigger>
36+
<DropdownMenuSubContent>
37+
{item.children.map((childItem) => (
38+
<RecursiveDropdownMenuItem key={childItem.identifier} item={childItem} />
39+
))}
40+
</DropdownMenuSubContent>
41+
</DropdownMenuSub>
42+
);
43+
};
44+
45+
const CollapsedMenuItemLink: React.FC<{ item: MenuItem }> = ({ item }) => (
46+
<Link to={constructPath(item.path)} key={item.identifier}>
47+
<CollapsedButton icon={item.icon} tooltipContent={item.label} />
48+
</Link>
49+
);
50+
51+
const ExpandedMenuItemLink: React.FC<{ item: MenuItem }> = ({ item }) => (
52+
<Link to={constructPath(item.path)} className={menuNavigationItemStyle} key={item.identifier}>
53+
<Icon icon={item.icon} className="min-w-4" />
54+
<span className="text-sm truncate">{item.label}</span>
55+
</Link>
56+
);
57+
58+
const DropdownMenuTriggerButton: React.FC<{ item: MenuItem; isCollapsed: boolean }> = ({
59+
item,
60+
isCollapsed,
61+
}) => (
62+
<DropdownMenuTrigger
63+
className={classNames(menuNavigationItemStyle, isCollapsed && "p-0")}
64+
asChild={isCollapsed}
65+
>
66+
{isCollapsed ? (
67+
<CollapsedButton tooltipContent={item.label} icon={item.icon} />
68+
) : (
69+
<>
70+
<Icon icon={item.icon} className="min-w-4" />
71+
<span className="text-sm truncate">{item.label}</span>
72+
<Icon
73+
icon="mdi:dots-vertical"
74+
className="m-1 ml-auto opacity-0 group-hover:opacity-100 group-focus:opacity-100 group-data-[state=open]:opacity-100"
75+
/>
76+
</>
77+
)}
78+
</DropdownMenuTrigger>
79+
);
80+
2381
export function MenuSectionInternal({ items, isCollapsed }: MenuSectionInternalProps) {
2482
return (
2583
<div className="flex flex-col mb-auto">
2684
{items.map((item) => {
27-
if (!item.children || item.children.length === 0) {
28-
if (isCollapsed) {
29-
return (
30-
<Link to={constructPath(item.path)}>
31-
<CollapsedButton
32-
icon={item.icon}
33-
tooltipContent={item.label}
34-
key={item.identifier}
35-
/>
36-
</Link>
37-
);
38-
}
39-
return (
40-
<Link to={constructPath(item.path)} className={menuNavigationItemStyle}>
41-
<Icon icon={item.icon} className="m-1 min-w-4" />
42-
<span className="text-sm truncate">{item.label}</span>
43-
</Link>
85+
if (!item.children?.length) {
86+
return isCollapsed ? (
87+
<CollapsedMenuItemLink key={item.identifier} item={item} />
88+
) : (
89+
<ExpandedMenuItemLink key={item.identifier} item={item} />
4490
);
4591
}
4692

4793
return (
4894
<DropdownMenu key={item.identifier}>
49-
<DropdownMenuTrigger
50-
className={classNames(menuNavigationItemStyle, isCollapsed && "p-0")}
51-
>
52-
{isCollapsed ? (
53-
<CollapsedButton tooltipContent={item.label} icon={item.icon} className="p-0" />
54-
) : (
55-
<>
56-
<Icon icon={item.icon} className="text-lg min-w-4" />
57-
<span className="text-sm truncate">{item.label}</span>
58-
<Icon
59-
icon="mdi:dots-vertical"
60-
className="m-1 ml-auto opacity-0 group-hover:opacity-100 group-focus:opacity-100 group-data-[state=open]:opacity-100"
61-
/>
62-
</>
63-
)}
64-
</DropdownMenuTrigger>
65-
95+
<DropdownMenuTriggerButton item={item} isCollapsed={!!isCollapsed} />
6696
<DropdownMenuContent side="left" align="start" className="min-w-[200px]">
67-
{item.children.map((child) => {
68-
if (!child.children || child.children.length === 0) {
69-
return (
70-
<DropdownMenuItem key={child.identifier} asChild>
71-
<Link to={constructPath(child.path)}>{child.label}</Link>
72-
</DropdownMenuItem>
73-
);
74-
}
75-
76-
return (
77-
<DropdownMenuSub key={child.identifier}>
78-
<DropdownMenuSubTrigger>{item.label}</DropdownMenuSubTrigger>
79-
80-
<DropdownMenuSubContent>
81-
{child.children.map((grandchild) => {
82-
return (
83-
<DropdownMenuItem key={grandchild.identifier} asChild>
84-
<Link to={constructPath(grandchild.path)}>{grandchild.label}</Link>
85-
</DropdownMenuItem>
86-
);
87-
})}
88-
</DropdownMenuSubContent>
89-
</DropdownMenuSub>
90-
);
91-
})}
97+
{item.children.map((childItem) => (
98+
<RecursiveDropdownMenuItem key={childItem.identifier} item={childItem} />
99+
))}
92100
</DropdownMenuContent>
93101
</DropdownMenu>
94102
);

frontend/app/src/screens/layout/menu-navigation/components/menu-section-object.tsx

Lines changed: 92 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,80 +14,108 @@ import { MenuItem } from "@/screens/layout/menu-navigation/types";
1414
import { classNames } from "@/utils/common";
1515
import { constructPath } from "@/utils/fetch";
1616
import { Icon } from "@iconify-icon/react";
17+
import React from "react";
1718
import { Link } from "react-router-dom";
1819

1920
export interface MenuSectionObjectsProps {
2021
items: MenuItem[];
2122
isCollapsed?: boolean;
2223
}
2324

24-
export function MenuSectionObject({ isCollapsed, items }: MenuSectionObjectsProps) {
25+
const MenuItemIcon: React.FC<{ item: MenuItem }> = ({ item }) => {
26+
if (item.icon) {
27+
return <Icon icon={item.icon} className="text-md m-1 min-h-4 min-w-4" />;
28+
}
29+
return <ObjectAvatar name={item.label} />;
30+
};
31+
32+
const RenderMenuItem: React.FC<{
33+
item: MenuItem;
34+
isCollapsed?: boolean;
35+
level?: number;
36+
}> = ({ item, isCollapsed, level = 0 }) => {
37+
const commonProps = {
38+
key: item.identifier,
39+
className: menuNavigationItemStyle,
40+
style: { marginLeft: level * 20 },
41+
};
42+
43+
if (!item.children?.length) {
44+
return (
45+
<DropdownMenuItem {...commonProps}>
46+
<Link to={constructPath(item.path)}>
47+
<Icon icon={item.icon} className="w-5 shrink-0 inline-flex justify-center items-center" />
48+
{item.label}
49+
</Link>
50+
</DropdownMenuItem>
51+
);
52+
}
53+
2554
return (
26-
<div className="flex flex-col w-full overflow-auto">
27-
{items.map((item) => {
28-
if (!item.children || item.children.length === 0) {
29-
return (
30-
<Link to={constructPath(item.path)} className={menuNavigationItemStyle}>
31-
{item.icon ? (
32-
<Icon icon={item.icon} className="text-md m-1 min-h-4 min-w-4" />
33-
) : (
34-
<ObjectAvatar name={item.label} />
35-
)}
36-
<span className={classNames("text-sm", isCollapsed && "hidden")}>{item.label}</span>
37-
</Link>
38-
);
39-
}
55+
<DropdownMenuAccordion value={item.identifier}>
56+
<DropdownMenuAccordionTrigger {...commonProps}>
57+
{item.path ? <Link to={constructPath(item.path)}>{item.label}</Link> : item.label}
58+
</DropdownMenuAccordionTrigger>
59+
<DropdownMenuAccordionContent>
60+
{item.children.map((child) => (
61+
<RenderMenuItem
62+
key={child.identifier}
63+
item={child}
64+
isCollapsed={isCollapsed}
65+
level={level + 1}
66+
/>
67+
))}
68+
</DropdownMenuAccordionContent>
69+
</DropdownMenuAccordion>
70+
);
71+
};
4072

41-
return (
42-
<DropdownMenu key={item.identifier}>
43-
<Tooltip enabled={isCollapsed} content={item.label} side="right">
44-
<DropdownMenuTrigger className={menuNavigationItemStyle}>
45-
{item.icon ? (
46-
<Icon icon={item.icon} className="text-md m-1 min-h-4 min-w-4" />
47-
) : (
48-
<ObjectAvatar name={item.label} />
49-
)}
50-
<span className={classNames("text-sm", isCollapsed && "hidden")}>{item.label}</span>
51-
</DropdownMenuTrigger>
52-
</Tooltip>
73+
const TopLevelMenuItem: React.FC<{
74+
item: MenuItem;
75+
isCollapsed?: boolean;
76+
}> = ({ item, isCollapsed }) => {
77+
if (!item.children?.length) {
78+
return (
79+
<Link key={item.identifier} to={constructPath(item.path)} className={menuNavigationItemStyle}>
80+
<MenuItemIcon item={item} />
81+
<span className={classNames("text-sm", isCollapsed && "hidden")}>{item.label}</span>
82+
</Link>
83+
);
84+
}
5385

54-
<DropdownMenuContent
55-
side="left"
56-
align="start"
57-
sideOffset={12}
58-
className="h-[calc(100vh-57px)] mt-[57px] min-w-[224px] px-4 py-5 bg-white border rounded-r-lg rounded-l-none shadow-none relative -top-px overflow-auto data-[side=right]:slide-in-from-left-[100px]"
59-
>
60-
<h3 className="text-xl font-medium text-neutral-800 mb-5">{item.label}</h3>
61-
{item.children.map((child) => {
62-
if (!child.children || child.children.length === 0) {
63-
return (
64-
<DropdownMenuItem key={child.identifier} className="px-3" asChild>
65-
<Link to={constructPath(child.path)}>
66-
<Icon icon={child.icon} className="w-5" />
67-
{child.label}
68-
</Link>
69-
</DropdownMenuItem>
70-
);
71-
}
86+
return (
87+
<DropdownMenu key={item.identifier}>
88+
<DropdownMenuTrigger className={classNames(menuNavigationItemStyle, isCollapsed && "p-2")}>
89+
<Tooltip enabled={isCollapsed} content={item.label} side="right">
90+
<span className="flex">
91+
<MenuItemIcon item={item} />
92+
</span>
93+
</Tooltip>
7294

73-
return (
74-
<DropdownMenuAccordion key={child.identifier} value={child.identifier}>
75-
<DropdownMenuAccordionTrigger className={menuNavigationItemStyle}>
76-
{child.label}
77-
</DropdownMenuAccordionTrigger>
95+
<span className={classNames("text-sm truncate", isCollapsed && "hidden")}>
96+
{item.label}
97+
</span>
98+
</DropdownMenuTrigger>
7899

79-
<DropdownMenuAccordionContent>
80-
<DropdownMenuItem key={child.identifier} className="pl-10" asChild>
81-
<Link to={constructPath(child.path)}>{child.label}</Link>
82-
</DropdownMenuItem>
83-
</DropdownMenuAccordionContent>
84-
</DropdownMenuAccordion>
85-
);
86-
})}
87-
</DropdownMenuContent>
88-
</DropdownMenu>
89-
);
90-
})}
91-
</div>
100+
<DropdownMenuContent
101+
side="left"
102+
align="start"
103+
sideOffset={isCollapsed ? 6 : 12}
104+
className="h-[calc(100vh-57px)] mt-[57px] min-w-[224px] px-4 py-5 bg-white border rounded-r-lg rounded-l-none shadow-none relative -top-px overflow-auto data-[side=right]:slide-in-from-left-[100px]"
105+
>
106+
<h3 className="text-xl font-medium text-neutral-800 mb-5">{item.label}</h3>
107+
{item.children.map((child) => (
108+
<RenderMenuItem key={child.identifier} item={child} isCollapsed={isCollapsed} />
109+
))}
110+
</DropdownMenuContent>
111+
</DropdownMenu>
92112
);
93-
}
113+
};
114+
115+
export const MenuSectionObject: React.FC<MenuSectionObjectsProps> = ({ isCollapsed, items }) => (
116+
<div className="flex flex-col w-full overflow-auto">
117+
{items.map((item) => (
118+
<TopLevelMenuItem key={item.identifier} item={item} isCollapsed={isCollapsed} />
119+
))}
120+
</div>
121+
);

frontend/app/src/state/atoms/schema.atom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const menuFlatAtom = atom((get) => {
3030
const menuItems: MenuItem[] = [];
3131

3232
const flattenMenuItems = (menuItem: MenuItem) => {
33-
if (menuItem.path !== "" && menuItem.children?.length === 0) menuItems.push(menuItem);
33+
if (menuItem.path !== "") menuItems.push(menuItem);
3434

3535
if (menuItem.children && menuItem.children.length > 0) {
3636
menuItem.children.forEach(flattenMenuItems);

0 commit comments

Comments
 (0)