diff --git a/.changeset/nice-bottles-bow.md b/.changeset/nice-bottles-bow.md new file mode 100644 index 000000000..339072661 --- /dev/null +++ b/.changeset/nice-bottles-bow.md @@ -0,0 +1,6 @@ +--- +"@ensembleui/react-kitchen-sink": patch +"@ensembleui/react-runtime": patch +--- + +Added support for nested menu items in menu diff --git a/apps/kitchen-sink/src/ensemble/screens/home.yaml b/apps/kitchen-sink/src/ensemble/screens/home.yaml index f0d4a9c8a..606322322 100644 --- a/apps/kitchen-sink/src/ensemble/screens/home.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/home.yaml @@ -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) diff --git a/apps/kitchen-sink/src/ensemble/screens/menu.yaml b/apps/kitchen-sink/src/ensemble/screens/menu.yaml index 1bdc42e50..ec4732416 100644 --- a/apps/kitchen-sink/src/ensemble/screens/menu.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/menu.yaml @@ -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 @@ -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: diff --git a/packages/runtime/src/runtime/menu.tsx b/packages/runtime/src/runtime/menu.tsx index d3348cb11..15a939056 100644 --- a/packages/runtime/src/runtime/menu.tsx +++ b/packages/runtime/src/runtime/menu.tsx @@ -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 }; @@ -165,12 +167,14 @@ export const EnsembleMenu: React.FC<{ const [isCollapsed, setIsCollapsed] = useState( type === EnsembleMenuModelType.Drawer, ); - + const [defaultOpenKeys, setDefaultOpenKeys] = useState([]); + const [isInitialized, setIsInitialized] = useState(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((item) => omit(item, "customItem"), @@ -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 => { @@ -223,10 +252,16 @@ export const EnsembleMenu: React.FC<{ const onCollapseCallback = useCallback(() => { return onCollapseAction?.callback(); }, [onCollapseAction?.callback]); + + if (!isInitialized) { + return null; + } + return (
{type === EnsembleMenuModelType.SideBar ? ( ) : ( ; isCollapsed: boolean; selectedItem: string | undefined; + defaultOpenKeys: string[] | undefined; setSelectedItem: (s: string) => void; width?: string; -}> = ({ values, isCollapsed, selectedItem, setSelectedItem }) => { +}> = ({ + values, + isCollapsed, + selectedItem, + defaultOpenKeys, + setSelectedItem, +}) => { return ( {values?.header ? EnsembleRuntime.render([values.header]) : null} ; 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 ( @@ -335,6 +387,7 @@ export const DrawerMenu: React.FC<{ > {values?.header ? EnsembleRuntime.render([values.header]) : null} void; + itemIndex: number; + icon: ReactNode; + label: ReactNode; +}> = ({ + menuItem, + styles, + selectedItem, + setSelectedItem, + itemIndex, + icon, + label, +}) => { + return ( + { + 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 ?? {} : {}), + }} + > + {label} + + ); +}; + 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, }) => { @@ -408,7 +516,24 @@ const MenuItems: React.FC<{ }, }} > + + - {items.map((item, itemIndex) => ( - { - 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 ?? {} - : {}), - }} - > - {getLabel(item)} - - ))} + {items.map((item, itemIndex) => + item.children ? ( + + {item.children.map((childItem, childIndex) => ( + + ))} + + ) : ( + + ), + )} );