diff --git a/src/components/menu/__tests__/menu.test.js b/src/components/menu/__tests__/menu.test.js new file mode 100644 index 000000000..129e01e4c --- /dev/null +++ b/src/components/menu/__tests__/menu.test.js @@ -0,0 +1,63 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import Menu from "../index"; + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("react-router-dom", () => ({ + withRouter: (component) => component +})); + +jest.mock("../../../models/member", () => jest.fn().mockImplementation(() => ({ + hasAccess: () => true + }))); + +jest.mock("../menu-definition", () => ({ + getGlobalItems: () => [{ name: "directory", linkUrl: "directory" }], + getSummitItems: () => [{ name: "summit_dashboard", linkUrl: "dashboard" }] +})); + +const mockHistory = { + push: jest.fn(), + location: { pathname: "/app/directory" } +}; + +const renderMenu = (props = {}) => + render(); + +describe("Menu", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("renders the general section", () => { + renderMenu(); + expect(screen.getByText("menu.general")).toBeInTheDocument(); + }); + + test("does not render summit section when currentSummit is not provided", () => { + renderMenu(); + expect(screen.queryByText("Test Summit")).not.toBeInTheDocument(); + }); + + test("renders summit section when currentSummit has a valid id", () => { + renderMenu({ currentSummit: { id: 1, name: "Test Summit" } }); + expect(screen.getByText("Test Summit")).toBeInTheDocument(); + }); + + test("does not render summit section when currentSummit.id is 0", () => { + renderMenu({ currentSummit: { id: 0, name: "Test Summit" } }); + expect(screen.queryByText("Test Summit")).not.toBeInTheDocument(); + }); + + test("navigates when a menu item is clicked", () => { + renderMenu(); + const link = screen.getByText("menu.directory"); + fireEvent.click(link); + expect(mockHistory.push).toHaveBeenCalledWith("/app/directory"); + }); +}); diff --git a/src/components/menu/expandable-item.js b/src/components/menu/expandable-item.js new file mode 100644 index 000000000..a9aace324 --- /dev/null +++ b/src/components/menu/expandable-item.js @@ -0,0 +1,54 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import React, { useState } from "react"; +import ListItemButton from "@mui/material/ListItemButton"; +import Typography from "@mui/material/Typography"; +import Collapse from "@mui/material/Collapse"; +import List from "@mui/material/List"; +import ExpandLess from "@mui/icons-material/ExpandLess"; +import ExpandMore from "@mui/icons-material/ExpandMore"; + +function ExpandableItem({ + label, + children, + defaultOpen = true, + isHeader = false +}) { + const [open, setOpen] = useState(defaultOpen); + + return ( + <> + setOpen((prev) => !prev)} + sx={{ py: 1, ...(isHeader ? { px: 2 } : { pl: 2 }) }} + > + + {label} + + {open ? : } + + + {children} + + + ); +} + +export default ExpandableItem; diff --git a/src/components/menu/index.js b/src/components/menu/index.js index 44c0aaf64..2ad078c55 100644 --- a/src/components/menu/index.js +++ b/src/components/menu/index.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 OpenStack Foundation + * Copyright 2026 OpenStack Foundation * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -11,545 +11,114 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import T from "i18n-react/dist/i18n-react"; import { withRouter } from "react-router-dom"; +import Box from "@mui/material/Box"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import MenuIcon from "@mui/icons-material/Menu"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import SubMenuItem from "./sub-menu-item"; import MenuItem from "./menu-item"; +import ExpandableItem from "./expandable-item"; import Member from "../../models/member"; +import { getGlobalItems, getSummitItems } from "./menu-definition"; import styles from "./menu.module.less"; -const getGlobalItems = () => [ - { - name: "directory", - iconClass: "fa-fw fa-list-ul", - linkUrl: "directory" - }, - { - name: "speakers", - iconClass: "fa-users", - accessRoute: "speakers", - childs: [ - { - name: "speaker_list", - linkUrl: "speakers", - accessRoute: "speaker-list" - }, - { - name: "merge_speakers", - linkUrl: "speakers/merge", - accessRoute: "speakers-merge" - } - ] - }, - { - name: "companies", - iconClass: "fa fa-copyright", - linkUrl: "companies", - accessRoute: "companies" - }, - { - name: "sponsors_inventory", - iconClass: "fa-users", - accessRoute: "inventory", - childs: [ - { - name: "inventory", - linkUrl: "inventory" - }, - { - name: "form_templates", - linkUrl: "form-templates" - }, - { - name: "page_templates", - linkUrl: "page-templates" - } - ] - }, - { - name: "sponsorship_types", - iconClass: "fa fa-handshake-o", - linkUrl: "sponsorship-types", - accessRoute: "sponsorship-types" - }, - { - name: "tags", - iconClass: "fa fa-tag", - linkUrl: "tags", - accessRoute: "tags" - }, - { - name: "sponsored_projects", - iconClass: "fa fa-cubes", - linkUrl: "sponsored-projects", - accessRoute: "sponsored-projects", - exclusive: "sponsored-projects" - }, - { - name: "emails", - iconClass: "fa-envelope-o", - accessRoute: "emails", - childs: [ - { name: "email_templates", linkUrl: "emails/templates" }, - { name: "email_logs", linkUrl: "emails/log" } - ] - }, - { - name: "admin_access", - iconClass: "fa-arrow-circle-o-right", - linkUrl: "admin-access", - accessRoute: "admin-access" - }, - { - name: "media_file_types", - iconClass: "fa-file-text-o", - linkUrl: "media-file-types", - accessRoute: "admin-access" - } -]; - -const getSummitItems = (summitId) => [ - { - name: "audit_log", - iconClass: "fa-tasks", - linkUrl: `summits/${summitId}/audit-log`, - accessRoute: "audit-log" - }, - { - name: "dashboard", - iconClass: "fa-dashboard", - linkUrl: `summits/${summitId}/dashboard`, - accessRoute: "general" - }, - { - name: "selection_plans", - iconClass: "fa-bars", - linkUrl: `summits/${summitId}/selection-plans`, - accessRoute: "selection_plans" - }, - { - name: "events", - iconClass: "fa-calendar", - accessRoute: "events", - childs: [ - { name: "new_event", linkUrl: `summits/${summitId}/events/new` }, - { name: "event_list", linkUrl: `summits/${summitId}/events` }, - { name: "schedule", linkUrl: `summits/${summitId}/events/schedule` }, - { name: "event_types", linkUrl: `summits/${summitId}/event-types` }, - { - name: "event_categories", - linkUrl: `summits/${summitId}/event-categories` - }, - { - name: "event_category_groups", - linkUrl: `summits/${summitId}/event-category-groups` - }, - { - name: "voteable_presentations", - linkUrl: `summits/${summitId}/voteable-presentations` - }, - { - name: "media_uploads", - linkUrl: `summits/${summitId}/media-uploads` - } - ] - }, - { - name: "attendees", - iconClass: "fa-users", - accessRoute: "attendees", - childs: [ - { - name: "attendee-list", - linkUrl: `summits/${summitId}/attendees`, - accessRoute: "attendees" - }, - { - name: "badge_checkin", - linkUrl: `summits/${summitId}/attendees/checkin` - } - ] - }, - { - name: "summit_speakers", - iconClass: "fa-users", - accessRoute: "events", - childs: [ - { - name: "submission_invitations", - iconClass: "fa-ticket", - linkUrl: `summits/${summitId}/submission-invitations`, - accessRoute: "speakers" - }, - { - name: "speakers", - iconClass: "fa-users", - linkUrl: `summits/${summitId}/speakers`, - accessRoute: "speakers" - }, - { - name: "speaker_attendance", - iconClass: "fa-users", - linkUrl: `summits/${summitId}/speaker-attendances`, - accessRoute: "speakers" - }, - { - name: "featured_speakers", - iconClass: "fa-star", - linkUrl: `summits/${summitId}/featured-speakers`, - accessRoute: "speakers" - } - ] - }, - { - name: "track_chairs", - iconClass: "fa-user-circle-o", - accessRoute: "track-chairs", - childs: [ - { - name: "track_chair_list", - linkUrl: `summits/${summitId}/track-chairs` - }, - { - name: "progress_flags", - linkUrl: `summits/${summitId}/track-chairs/progress-flags`, - accessRoute: "progress-flags" - }, - { - name: "track_timeframes", - linkUrl: `summits/${summitId}/track-chairs/track-timeframes`, - accessRoute: "track-timeframes" - }, - { - name: "track_chair_team_lists", - linkUrl: `summits/${summitId}/track-chairs/team-lists`, - accessRoute: "team-lists" - } - ] - }, - { - name: "sponsors", - iconClass: "fa-handshake-o", - accessRoute: "sponsors", - childs: [ - { - name: "sponsor_list", - linkUrl: `summits/${summitId}/sponsors`, - accessRoute: "sponsors" - }, - { - name: "sponsor_forms", - linkUrl: `summits/${summitId}/sponsors/forms`, - accessRoute: "admin-sponsors" - }, - { - name: "sponsor_pages", - linkUrl: `summits/${summitId}/sponsors/pages`, - accessRoute: "admin-sponsors" - }, - { - name: "sponsorship_list", - linkUrl: `summits/${summitId}/sponsorships`, - accessRoute: "admin-sponsors" - }, - { - name: "sponsor_users", - linkUrl: `summits/${summitId}/sponsors/users`, - accessRoute: "admin-sponsors" - }, - { - name: "sponsors_promocodes", - linkUrl: `summits/${summitId}/sponsors/promocodes`, - accessRoute: "admin-sponsors" - }, - { - name: "sponsor_settings", - linkUrl: `summits/${summitId}/sponsors/settings`, - accessRoute: "admin-sponsors" - }, - { - name: "badge_scans", - linkUrl: `summits/${summitId}/badge-scans`, - accessRoute: "badge-scans" - } - ] - }, - { - name: "locations", - iconClass: "fa-map-marker", - linkUrl: `summits/${summitId}/locations`, - accessRoute: "locations" - }, - { - name: "signage", - iconClass: "fa-map-signs", - linkUrl: `summits/${summitId}/signage`, - accessRoute: "signage" - }, - { - name: "purchase_orders", - iconClass: "fa-money", - accessRoute: "purchase-orders", - childs: [ - { - name: "purchase_order_list", - linkUrl: `summits/${summitId}/purchase-orders` - }, - { name: "ticket_list", linkUrl: `summits/${summitId}/tickets` }, - { - name: "order_extra_questions", - linkUrl: `summits/${summitId}/order-extra-questions` - }, - { - name: "registration_stats", - linkUrl: `summits/${summitId}/registration-stats` - } - ] - }, - { - name: "tickets", - iconClass: "fa-ticket", - accessRoute: "tickets", - childs: [ - { - name: "registration_invitation_list", - linkUrl: `summits/${summitId}/registration-invitations` - }, - { - name: "ticket_type_list", - linkUrl: `summits/${summitId}/ticket-types` - }, - { - name: "promocode_list", - linkUrl: `summits/${summitId}/promocodes` - }, - { name: "tax_type_list", linkUrl: `summits/${summitId}/tax-types` }, - { - name: "refund_policy_list", - linkUrl: `summits/${summitId}/refund-policies` - }, - { - name: "payment_profiles_list", - linkUrl: `summits/${summitId}/payment-profiles` - }, - { - name: "registration_companies_list", - linkUrl: `summits/${summitId}/registration-companies` - } - ] - }, - { - name: "badges", - iconClass: "fa-id-card-o", - accessRoute: "badges", - childs: [ - { - name: "badge_feature_list", - linkUrl: `summits/${summitId}/badge-features` - }, - { - name: "access_level_list", - linkUrl: `summits/${summitId}/access-levels` - }, - { - name: "view_type_list", - linkUrl: `summits/${summitId}/view-types` - }, - { - name: "badge_type_list", - linkUrl: `summits/${summitId}/badge-types` - }, - { - name: "badge_settings", - linkUrl: `summits/${summitId}/badge-settings` - } - ] - }, - { - name: "room_bookings", - iconClass: "fa-bookmark", - linkUrl: `summits/${summitId}/room-bookings`, - accessRoute: "room-bookings", - exclusive: "room-bookings" - }, - { - name: "push_notifications", - iconClass: "fa-paper-plane", - linkUrl: `summits/${summitId}/push-notifications`, - accessRoute: "push-notifications" - }, - { - name: "room_occupancy", - iconClass: "fa-male", - linkUrl: `summits/${summitId}/room-occupancy`, - accessRoute: "room-occupancy" - }, - { - name: "tag_groups", - iconClass: "fa-tags", - linkUrl: `summits/${summitId}/tag-groups`, - accessRoute: "tag-groups" - }, - { - name: "reports", - iconClass: "fa-list-ol", - linkUrl: `summits/${summitId}/reports`, - accessRoute: "reports" - }, - { - name: "summitdocs", - iconClass: "fa-file-text", - linkUrl: `summits/${summitId}/summitdocs`, - accessRoute: "summitdocs" - }, - { - name: "email_flow_events", - iconClass: "fa-envelope-o", - accessRoute: "email-flow-events", - childs: [ - { - name: "email_flow_overrides", - linkUrl: `summits/${summitId}/email-flow-events` - }, - { - name: "email_flow_settings", - linkUrl: `summits/${summitId}/email-flow-events-settings` - } - ] - }, - { - name: "settings", - iconClass: "fa-cog", - accessRoute: "settings", - childs: [ - { name: "marketing", linkUrl: `summits/${summitId}/marketing` }, - { - name: "schedule_settings", - linkUrl: `summits/${summitId}/schedule-settings` - } - ] - } -]; - const Menu = ({ currentSummit, member, history }) => { const [menuOpen, setMenuOpen] = useState(false); - const [subMenuOpen, setSubMenuOpen] = useState([]); const memberObj = new Member(member); - const canEditSummit = memberObj.canEditSummit(); const globalItems = getGlobalItems(); - const summitItems = getSummitItems(currentSummit.id); - - const closeMenu = () => { - setMenuOpen(false); - }; + const summitItems = currentSummit ? getSummitItems(currentSummit.id) : []; - const toggleSubMenu = (ev, submenu) => { - ev.preventDefault(); - let newSubMenuOpen = []; - - if (subMenuOpen.includes(submenu)) { - newSubMenuOpen = subMenuOpen.filter((sm) => sm !== submenu); - } else { - newSubMenuOpen = [...subMenuOpen, submenu]; - } - - setSubMenuOpen(newSubMenuOpen); - setMenuOpen(true); - }; + const closeMenu = () => setMenuOpen(false); const onMenuItemClick = (ev, url) => { ev.preventDefault(); - setMenuOpen(false); + closeMenu(); history.push(`/app/${url}`); }; + const currentPath = history.location.pathname; + const drawMenuItem = (item) => { const hasAccess = !item.accessRoute || memberObj.hasAccess(item.accessRoute); if (!hasAccess) return null; - if (item.childs) { + if (item.subItems) { return ( toggleSubMenu(e, item.name)} onItemClick={onMenuItemClick} + currentPath={currentPath} /> ); } return ( onMenuItemClick(e, item.linkUrl)} /> ); }; - useEffect(() => { - document.getElementById("page-wrap")?.addEventListener("click", closeMenu); - document - .getElementById("page-header") - ?.addEventListener("click", closeMenu); - - return () => { - document - .getElementById("page-wrap") - .removeEventListener("click", closeMenu); - document - .getElementById("page-header") - .removeEventListener("click", closeMenu); - }; - }, []); - return ( -
-
- -
-
setMenuOpen(true)} - onMouseLeave={() => setMenuOpen(false)} + <> + {menuOpen && ( + + )} + -
- -
- -
-
{T.translate("menu.general")}
- {globalItems.map(drawMenuItem)} - - {currentSummit?.id && ( -
- {currentSummit.name} - {canEditSummit && ( - - )} -
- )} - {currentSummit?.id && summitItems.map(drawMenuItem)} -
-
-
+ + (menuOpen ? closeMenu() : setMenuOpen(true))} + > + + + + setMenuOpen(true)} + onMouseLeave={() => setMenuOpen(false)} + > + + + + + + + {globalItems.map(drawMenuItem)} + + + {!!currentSummit?.id && ( + <> + + + {summitItems.map(drawMenuItem)} + + + )} + + + + ); }; diff --git a/src/components/menu/menu-definition.js b/src/components/menu/menu-definition.js new file mode 100644 index 000000000..d853d6d58 --- /dev/null +++ b/src/components/menu/menu-definition.js @@ -0,0 +1,390 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +export const getGlobalItems = () => [ + { + name: "directory", + linkUrl: "directory" + }, + { + name: "speakers", + accessRoute: "speakers", + subItems: [ + { + name: "speaker_list", + linkUrl: "speakers", + accessRoute: "speaker-list" + }, + { + name: "merge_speakers", + linkUrl: "speakers/merge", + accessRoute: "speakers-merge" + } + ] + }, + { + name: "companies", + linkUrl: "companies", + accessRoute: "companies" + }, + { + name: "sponsors_inventory", + accessRoute: "inventory", + subItems: [ + { + name: "inventory", + linkUrl: "inventory" + }, + { + name: "form_templates", + linkUrl: "form-templates" + }, + { + name: "page_templates", + linkUrl: "page-templates" + } + ] + }, + { + name: "sponsorship_types", + linkUrl: "sponsorship-types", + accessRoute: "sponsorship-types" + }, + { + name: "tags", + linkUrl: "tags", + accessRoute: "tags" + }, + { + name: "sponsored_projects", + linkUrl: "sponsored-projects", + accessRoute: "sponsored-projects", + exclusive: "sponsored-projects" + }, + { + name: "emails", + accessRoute: "emails", + subItems: [ + { name: "email_templates", linkUrl: "emails/templates" }, + { name: "email_logs", linkUrl: "emails/log" } + ] + }, + { + name: "admin_access", + linkUrl: "admin-access", + accessRoute: "admin-access" + }, + { + name: "media_file_types", + linkUrl: "media-file-types", + accessRoute: "admin-access" + } +]; + +export const getSummitItems = (summitId) => [ + { + name: "audit_log", + linkUrl: `summits/${summitId}/audit-log`, + accessRoute: "audit-log" + }, + { + name: "dashboard", + linkUrl: `summits/${summitId}/dashboard`, + accessRoute: "general" + }, + { + name: "selection_plans", + linkUrl: `summits/${summitId}/selection-plans`, + accessRoute: "selection_plans" + }, + { + name: "events", + accessRoute: "events", + subItems: [ + { name: "new_event", linkUrl: `summits/${summitId}/events/new` }, + { name: "event_list", linkUrl: `summits/${summitId}/events` }, + { name: "schedule", linkUrl: `summits/${summitId}/events/schedule` }, + { name: "event_types", linkUrl: `summits/${summitId}/event-types` }, + { + name: "event_categories", + linkUrl: `summits/${summitId}/event-categories` + }, + { + name: "event_category_groups", + linkUrl: `summits/${summitId}/event-category-groups` + }, + { + name: "voteable_presentations", + linkUrl: `summits/${summitId}/voteable-presentations` + }, + { + name: "media_uploads", + linkUrl: `summits/${summitId}/media-uploads` + } + ] + }, + { + name: "attendees", + accessRoute: "attendees", + subItems: [ + { + name: "attendee-list", + linkUrl: `summits/${summitId}/attendees`, + accessRoute: "attendees" + }, + { + name: "badge_checkin", + linkUrl: `summits/${summitId}/attendees/checkin` + } + ] + }, + { + name: "summit_speakers", + accessRoute: "events", + subItems: [ + { + name: "submission_invitations", + linkUrl: `summits/${summitId}/submission-invitations`, + accessRoute: "speakers" + }, + { + name: "speakers", + linkUrl: `summits/${summitId}/speakers`, + accessRoute: "speakers" + }, + { + name: "speaker_attendance", + linkUrl: `summits/${summitId}/speaker-attendances`, + accessRoute: "speakers" + }, + { + name: "featured_speakers", + linkUrl: `summits/${summitId}/featured-speakers`, + accessRoute: "speakers" + } + ] + }, + { + name: "track_chairs", + accessRoute: "track-chairs", + subItems: [ + { + name: "track_chair_list", + linkUrl: `summits/${summitId}/track-chairs` + }, + { + name: "progress_flags", + linkUrl: `summits/${summitId}/track-chairs/progress-flags`, + accessRoute: "progress-flags" + }, + { + name: "track_timeframes", + linkUrl: `summits/${summitId}/track-chairs/track-timeframes`, + accessRoute: "track-timeframes" + }, + { + name: "track_chair_team_lists", + linkUrl: `summits/${summitId}/track-chairs/team-lists`, + accessRoute: "team-lists" + } + ] + }, + { + name: "sponsors", + accessRoute: "sponsors", + subItems: [ + { + name: "sponsor_list", + linkUrl: `summits/${summitId}/sponsors`, + accessRoute: "sponsors" + }, + { + name: "sponsor_forms", + linkUrl: `summits/${summitId}/sponsors/forms`, + accessRoute: "admin-sponsors" + }, + { + name: "sponsor_pages", + linkUrl: `summits/${summitId}/sponsors/pages`, + accessRoute: "admin-sponsors" + }, + { + name: "sponsorship_list", + linkUrl: `summits/${summitId}/sponsorships`, + accessRoute: "admin-sponsors" + }, + { + name: "sponsor_users", + linkUrl: `summits/${summitId}/sponsors/users`, + accessRoute: "admin-sponsors" + }, + { + name: "sponsors_promocodes", + linkUrl: `summits/${summitId}/sponsors/promocodes`, + accessRoute: "admin-sponsors" + }, + { + name: "sponsor_settings", + linkUrl: `summits/${summitId}/sponsors/settings`, + accessRoute: "admin-sponsors" + }, + { + name: "badge_scans", + linkUrl: `summits/${summitId}/badge-scans`, + accessRoute: "badge-scans" + } + ] + }, + { + name: "locations", + linkUrl: `summits/${summitId}/locations`, + accessRoute: "locations" + }, + { + name: "signage", + linkUrl: `summits/${summitId}/signage`, + accessRoute: "signage" + }, + { + name: "purchase_orders", + accessRoute: "purchase-orders", + subItems: [ + { + name: "purchase_order_list", + linkUrl: `summits/${summitId}/purchase-orders` + }, + { name: "ticket_list", linkUrl: `summits/${summitId}/tickets` }, + { + name: "order_extra_questions", + linkUrl: `summits/${summitId}/order-extra-questions` + }, + { + name: "registration_stats", + linkUrl: `summits/${summitId}/registration-stats` + } + ] + }, + { + name: "tickets", + accessRoute: "tickets", + subItems: [ + { + name: "registration_invitation_list", + linkUrl: `summits/${summitId}/registration-invitations` + }, + { + name: "ticket_type_list", + linkUrl: `summits/${summitId}/ticket-types` + }, + { + name: "promocode_list", + linkUrl: `summits/${summitId}/promocodes` + }, + { name: "tax_type_list", linkUrl: `summits/${summitId}/tax-types` }, + { + name: "refund_policy_list", + linkUrl: `summits/${summitId}/refund-policies` + }, + { + name: "payment_profiles_list", + linkUrl: `summits/${summitId}/payment-profiles` + }, + { + name: "registration_companies_list", + linkUrl: `summits/${summitId}/registration-companies` + } + ] + }, + { + name: "badges", + accessRoute: "badges", + subItems: [ + { + name: "badge_feature_list", + linkUrl: `summits/${summitId}/badge-features` + }, + { + name: "access_level_list", + linkUrl: `summits/${summitId}/access-levels` + }, + { + name: "view_type_list", + linkUrl: `summits/${summitId}/view-types` + }, + { + name: "badge_type_list", + linkUrl: `summits/${summitId}/badge-types` + }, + { + name: "badge_settings", + linkUrl: `summits/${summitId}/badge-settings` + } + ] + }, + { + name: "room_bookings", + linkUrl: `summits/${summitId}/room-bookings`, + accessRoute: "room-bookings", + exclusive: "room-bookings" + }, + { + name: "push_notifications", + linkUrl: `summits/${summitId}/push-notifications`, + accessRoute: "push-notifications" + }, + { + name: "room_occupancy", + linkUrl: `summits/${summitId}/room-occupancy`, + accessRoute: "room-occupancy" + }, + { + name: "tag_groups", + linkUrl: `summits/${summitId}/tag-groups`, + accessRoute: "tag-groups" + }, + { + name: "reports", + linkUrl: `summits/${summitId}/reports`, + accessRoute: "reports" + }, + { + name: "summitdocs", + linkUrl: `summits/${summitId}/summitdocs`, + accessRoute: "summitdocs" + }, + { + name: "email_flow_events", + accessRoute: "email-flow-events", + subItems: [ + { + name: "email_flow_overrides", + linkUrl: `summits/${summitId}/email-flow-events` + }, + { + name: "email_flow_settings", + linkUrl: `summits/${summitId}/email-flow-events-settings` + } + ] + }, + { + name: "settings", + accessRoute: "settings", + subItems: [ + { name: "marketing", linkUrl: `summits/${summitId}/marketing` }, + { + name: "schedule_settings", + linkUrl: `summits/${summitId}/schedule-settings` + } + ] + } +]; diff --git a/src/components/menu/menu-item.js b/src/components/menu/menu-item.js index da00398e7..4b1098001 100644 --- a/src/components/menu/menu-item.js +++ b/src/components/menu/menu-item.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 OpenStack Foundation + * Copyright 2026 OpenStack Foundation * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,20 +14,26 @@ import React from "react"; import T from "i18n-react/dist/i18n-react"; import Exclusive from "openstack-uicore-foundation/lib/components/exclusive-wrapper"; -import styles from "./menu.module.less"; +import ListItemButton from "@mui/material/ListItemButton"; +import Typography from "@mui/material/Typography"; -const MenuItem = ({ name, iconClass, onClick, exclusive }) => { - const itemHtml = [ - { + const itemHtml = ( + - - {T.translate(`menu.${name}`)} - - ]; + + {T.translate(`menu.${name}`)} + + + ); if (exclusive) { return {itemHtml}; diff --git a/src/components/menu/menu.module.less b/src/components/menu/menu.module.less index 4d2c3660a..5664cc2d9 100644 --- a/src/components/menu/menu.module.less +++ b/src/components/menu/menu.module.less @@ -15,7 +15,7 @@ } .menuWrapper { - width: 220px; + width: 260px; } .menuItemsWrapper { @@ -59,44 +59,16 @@ } .menuWrapper { - background-color: rgba(0, 0, 0, 0.9); + background-color: #ffffff; + border-right: 1px solid #e0e0e0; cursor: pointer; transition: width 200ms linear; height: 100%; - .separator { - border-bottom: 1px solid #b8b7ad; - color: #b8b7ad; - padding: 15px 0 5px 10px; - margin-bottom: 10px; - font-size: 1.2em; - - button { - background: none; - border: none; - } - } - .expandButton { justify-content: center; align-items: center; height: 100%; - - :global(.fa) { - font-size: 2em; - color: white; - } - } - - .editSummitBtn { - font-size: 15px; - margin-left: 5px; - cursor: pointer; - - &:hover { - color: #dfded8; - font-weight: bold; - } } .menuItemsWrapper { @@ -104,46 +76,10 @@ transition: visibility 200ms linear 200ms; overflow-y: auto; height: 100%; - - .menuItem { - display: flex; - margin: 8px 0 0 10px; - - :hover { - font-weight: 600; - } - - :global(.fa) { - padding-top: 3px; - margin-right: 10px; - } - } - - .submenu { - .menuItem { - margin-left: 20px; - - :global(.fa) { - margin-right: 5px; - } - } - } - - a { - text-decoration: none; - color: white; - font-size: 1em; - } } } .burgerButton { padding: 5px 0 0 5px; - - button { - border: none; - background: none; - font-size: 2.5em; - } } } diff --git a/src/components/menu/sub-menu-item.js b/src/components/menu/sub-menu-item.js index 0665fdc29..04085eec2 100644 --- a/src/components/menu/sub-menu-item.js +++ b/src/components/menu/sub-menu-item.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 OpenStack Foundation + * Copyright 2026 OpenStack Foundation * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,42 +14,37 @@ import React from "react"; import T from "i18n-react/dist/i18n-react"; import MenuItem from "./menu-item"; -import styles from "./menu.module.less"; +import ExpandableItem from "./expandable-item"; -function SubMenuItem({ - name, - iconClass, - isOpen, - onClick, - onItemClick, - childs, - memberObj -}) { - const _childs = childs.filter( +function SubMenuItem({ name, onItemClick, subItems, memberObj, currentPath }) { + const _subItems = subItems.filter( (item) => !item.hasOwnProperty("accessRoute") || memberObj.hasAccess(item.accessRoute) ); + if (_subItems.length === 0) return null; + + const isChildActive = _subItems.some( + (ch) => currentPath === `/app/${ch.linkUrl}` + ); + return ( -
- - - {T.translate(`menu.${name}`)} - - {isOpen && ( -
- {_childs.map((ch) => ( - onItemClick(e, ch.linkUrl)} - /> - ))} -
- )} -
+ + {_subItems.map((item) => ( + onItemClick(e, item.linkUrl)} + /> + ))} + ); }