Skip to content

fix: added support for nested items in menu #1060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/nice-bottles-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ensembleui/react-kitchen-sink": patch
"@ensembleui/react-runtime": patch
---

Added support for nested menu items in menu
3 changes: 0 additions & 3 deletions apps/kitchen-sink/src/ensemble/screens/home.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ View:
console.log('>>> secret variable >>>', ensemble.secrets.dummyOauthSecret)
ensemble.storage.set('products', []);
ensemble.invokeAPI('getDummyProducts').then((res) => ensemble.storage.set('products', (res?.body?.users || []).map((i) => ({ ...i, name: i.firstName + ' ' + i.lastName }))));
const res = await ensemble.invokeAPI('getDummyNumbers')
await new Promise((resolve) => setTimeout(resolve, 5000))
return res
onComplete:
executeCode: |
console.log('API triggered', result)
Expand Down
21 changes: 17 additions & 4 deletions apps/kitchen-sink/src/ensemble/screens/menu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ ViewGroup:
page: layouts
- label: Actions
icon: CodeOutlined
page: actions
children:
- label: Actions example
icon: CodeOutlined
page: actions
- label: Test Actions
icon: WidgetsOutlined
page: test_actions
- label: Forms
icon: CodeOutlined
page: forms
Expand Down Expand Up @@ -74,9 +80,16 @@ ViewGroup:
- label: Hidden Page
icon: HelpOutlineOutlined
visible: false
- label: Test Actions
icon: WidgetsOutlined
page: test_actions
- label: Actions
icon: CodeOutlined
expanded: true
children:
- label: Actions example
icon: CodeOutlined
page: actions1
- label: Test Actions
icon: WidgetsOutlined
page: test_actions1
- customItem:
widget:
Button:
Expand Down
222 changes: 175 additions & 47 deletions packages/runtime/src/runtime/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ interface MenuItemProps {
hasNotifications?: boolean;
openNewTab?: boolean;
visible?: boolean;
expanded?: boolean;
children?: MenuItemProps[];
customItem?: {
widget?: { [key: string]: unknown };
selectedWidget?: { [key: string]: unknown };
Expand Down Expand Up @@ -165,12 +167,14 @@ export const EnsembleMenu: React.FC<{
const [isCollapsed, setIsCollapsed] = useState<boolean>(
type === EnsembleMenuModelType.Drawer,
);

const [defaultOpenKeys, setDefaultOpenKeys] = useState<string[]>([]);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const outletContext = {
isMenuCollapsed: isCollapsed,
setMenuCollapsed: setIsCollapsed,
};
const { id, items: rawItems, styles, header, footer, onCollapse } = menu;

// custom items may contain their own bindings to be evaluated in dynamic context
const itemInputs = rawItems?.map<MenuItemProps>((item) =>
omit(item, "customItem"),
Expand Down Expand Up @@ -204,14 +208,39 @@ export const EnsembleMenu: React.FC<{
}, [items]);

useEffect(() => {
const locationMatch = items?.find(
(item) =>
const openItems: string[] = [];

items?.forEach((item, index) => {
if (
item.page &&
`/${item.page.toLowerCase()}` === location.pathname.toLowerCase(),
);
if (locationMatch) {
setSelectedItem(locationMatch.page);
}
`/${item.page.toLowerCase()}` === location.pathname.toLowerCase()
) {
setSelectedItem(item.page);
}

if (item.children && item.children.length > 0) {
const hasActiveChild = item.children.some((childItem) => {
const isActive =
childItem.page &&
`/${childItem.page.toLowerCase()}` ===
location.pathname.toLowerCase();

if (isActive) {
setSelectedItem(childItem.page);
return true;
}

return false;
});

if (hasActiveChild || item.expanded) {
openItems.push(`submenu-${index}`);
}
}
});

setDefaultOpenKeys(openItems);
setIsInitialized(true);
}, [location.pathname, items]);

const handleClose = (): void => {
Expand All @@ -223,17 +252,24 @@ export const EnsembleMenu: React.FC<{
const onCollapseCallback = useCallback(() => {
return onCollapseAction?.callback();
}, [onCollapseAction?.callback]);

if (!isInitialized) {
return null;
}

return (
<div style={{ display: "flex", flexDirection: "row" }}>
{type === EnsembleMenuModelType.SideBar ? (
<SideBarMenu
defaultOpenKeys={defaultOpenKeys}
isCollapsed={isCollapsed}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
values={{ ...values, items }}
/>
) : (
<DrawerMenu
defaultOpenKeys={defaultOpenKeys}
handleClose={handleClose}
isOpen={!isCollapsed}
selectedItem={selectedItem}
Expand Down Expand Up @@ -261,9 +297,16 @@ export const SideBarMenu: React.FC<{
values?: MenuBaseProps<SideBarMenuStyles>;
isCollapsed: boolean;
selectedItem: string | undefined;
defaultOpenKeys: string[] | undefined;
setSelectedItem: (s: string) => void;
width?: string;
}> = ({ values, isCollapsed, selectedItem, setSelectedItem }) => {
}> = ({
values,
isCollapsed,
selectedItem,
defaultOpenKeys,
setSelectedItem,
}) => {
return (
<Col
id="ensemble-sidebar"
Expand All @@ -283,6 +326,7 @@ export const SideBarMenu: React.FC<{
>
{values?.header ? EnsembleRuntime.render([values.header]) : null}
<MenuItems
defaultOpenKeys={defaultOpenKeys}
isCollapsed={isCollapsed}
items={values?.items || []}
selectedItem={selectedItem}
Expand All @@ -295,12 +339,20 @@ export const SideBarMenu: React.FC<{
};

export const DrawerMenu: React.FC<{
defaultOpenKeys: string[] | undefined;
values?: MenuBaseProps<DrawerMenuStyles>;
handleClose: () => void;
isOpen: boolean;
selectedItem: string | undefined;
setSelectedItem: (s: string) => void;
}> = ({ values, handleClose, isOpen, selectedItem, setSelectedItem }) => {
}> = ({
defaultOpenKeys,
values,
handleClose,
isOpen,
selectedItem,
setSelectedItem,
}) => {
const validPosition = ["left", "right", "top", "bottom"];

return (
Expand Down Expand Up @@ -335,6 +387,7 @@ export const DrawerMenu: React.FC<{
>
{values?.header ? EnsembleRuntime.render([values.header]) : null}
<MenuItems
defaultOpenKeys={defaultOpenKeys}
items={values?.items || []}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
Expand All @@ -345,16 +398,71 @@ export const DrawerMenu: React.FC<{
);
};

const RenderMenuItem: React.FC<{
menuItem: MenuItemProps;
styles: MenuStyles;
selectedItem: string | undefined;
setSelectedItem: (s: string) => void;
itemIndex: number;
icon: ReactNode;
label: ReactNode;
}> = ({
menuItem,
styles,
selectedItem,
setSelectedItem,
itemIndex,
icon,
label,
}) => {
return (
<AntMenu.Item
data-testid={menuItem.id ?? menuItem.testId}
icon={icon}
key={menuItem.page || menuItem.url || `customItem${itemIndex}`}
onClick={(): void => {
if (!menuItem.openNewTab && menuItem.page) {
setSelectedItem(menuItem.page);
}
}}
style={{
color:
selectedItem === menuItem.page
? (styles.selectedColor as string) ?? "white"
: (styles.labelColor as string) ?? "grey",
display: menuItem.visible === false ? "none" : "flex",
justifyContent: "center",
borderRadius: 0,
alignItems: "center",
fontSize:
selectedItem === menuItem.page
? `${
parseInt(
`${styles.labelFontSize ? styles.labelFontSize : 1}` || "1",
) + 0.2
}rem`
: `${styles.labelFontSize ? styles.labelFontSize : 1}rem`,
height: "auto",
...(selectedItem === menuItem.page ? styles.onSelectStyles ?? {} : {}),
}}
>
<CustomLink item={menuItem}>{label}</CustomLink>
</AntMenu.Item>
);
};

const MenuItems: React.FC<{
items: MenuItemProps[];
styles: MenuStyles;
selectedItem: string | undefined;
defaultOpenKeys: string[] | undefined;
setSelectedItem: (s: string) => void;
isCollapsed?: boolean;
}> = ({
items,
styles,
selectedItem,
defaultOpenKeys,
setSelectedItem,
isCollapsed = false,
}) => {
Expand Down Expand Up @@ -408,7 +516,24 @@ const MenuItems: React.FC<{
},
}}
>
<style>
{`
.ant-menu-submenu-title{
color: inherit !important
}

.ant-menu-item {
padding-left: 24px !important;
}

.ant-menu-sub {
padding-left: 24px !important;
}
`}
</style>

<AntMenu
defaultOpenKeys={defaultOpenKeys?.length ? defaultOpenKeys : undefined}
mode="inline"
/* FIXME This is a hack so we can control our own selected styling. Ideally, this should use design tokens */
selectedKeys={[]}
Expand All @@ -422,43 +547,46 @@ const MenuItems: React.FC<{
borderRight: "none",
}}
>
{items.map((item, itemIndex) => (
<AntMenu.Item
data-testid={item.id ?? item.testId}
icon={getIcon(item)}
key={item.page || item.url || `customItem${itemIndex}`}
onClick={(): void => {
if (!item.openNewTab && item.page) {
setSelectedItem(item.page);
}
}}
style={{
color:
selectedItem === item.page
? (styles.selectedColor as string) ?? "white"
: (styles.labelColor as string) ?? "grey",
display: item.visible === false ? "none" : "flex",
justifyContent: "center",
borderRadius: 0,
alignItems: "center",
fontSize:
selectedItem === item.page
? `${
parseInt(
`${styles.labelFontSize ? styles.labelFontSize : 1}` ||
"1",
) + 0.2
}rem`
: `${styles.labelFontSize ? styles.labelFontSize : 1}rem`,
height: "auto",
...(selectedItem === item.page
? styles.onSelectStyles ?? {}
: {}),
}}
>
<CustomLink item={item}>{getLabel(item)}</CustomLink>
</AntMenu.Item>
))}
{items.map((item, itemIndex) =>
item.children ? (
<AntMenu.SubMenu
icon={getIcon(item)}
key={`submenu-${itemIndex}`}
style={{
color: styles.labelColor ?? "grey",
fontSize: "1rem",
height: "auto",
}}
title={getLabel(item)}
>
{item.children.map((childItem, childIndex) => (
<RenderMenuItem
icon={getIcon(childItem)}
itemIndex={childIndex}
key={
childItem.page || childItem.url || `customItem${childIndex}`
}
label={getLabel(childItem)}
menuItem={childItem}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
styles={styles}
/>
))}
</AntMenu.SubMenu>
) : (
<RenderMenuItem
icon={getIcon(item)}
itemIndex={itemIndex}
key={item.page || item.url || `customItem${itemIndex}`}
label={getLabel(item)}
menuItem={item}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
styles={styles}
/>
),
)}
</AntMenu>
</ConfigProvider>
);
Expand Down