diff --git a/src/components/menu/__tests__/menu.test.js b/src/components/menu/__tests__/menu.test.js
index b417e76aa..36b6e7229 100644
--- a/src/components/menu/__tests__/menu.test.js
+++ b/src/components/menu/__tests__/menu.test.js
@@ -19,18 +19,20 @@ jest.mock("../../../models/member", () =>
}))
);
-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 menuDefs = {
+ global: [{ name: "directory", linkUrl: "directory" }],
+ summit: [{ name: "summit_dashboard", linkUrl: "dashboard" }]
+};
+
const renderMenu = (props = {}) =>
- render(
);
+ render(
+
+ );
describe("Menu", () => {
beforeEach(() => {
diff --git a/src/components/menu/__tests__/sub-menu-item.test.js b/src/components/menu/__tests__/sub-menu-item.test.js
new file mode 100644
index 000000000..dd6b01659
--- /dev/null
+++ b/src/components/menu/__tests__/sub-menu-item.test.js
@@ -0,0 +1,99 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import SubMenuItem from "../sub-menu-item";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+// folder > schedule > rooms is three levels deep to exercise the recursion
+const subItems = [
+ { name: "event_list", linkUrl: "events", accessRoute: "events" },
+ {
+ name: "schedule",
+ subItems: [
+ {
+ name: "schedule_settings",
+ linkUrl: "schedule/settings",
+ accessRoute: "schedule"
+ },
+ {
+ name: "rooms",
+ subItems: [
+ { name: "room_list", linkUrl: "rooms", accessRoute: "rooms" }
+ ]
+ }
+ ]
+ },
+ {
+ name: "track_chairs",
+ subItems: [
+ {
+ name: "track_chair_list",
+ linkUrl: "track-chairs",
+ accessRoute: "track-chairs"
+ }
+ ]
+ }
+];
+
+const ALL_ROUTES = ["events", "schedule", "rooms", "track-chairs"];
+
+const memberWith = (routes) => ({
+ hasAccess: (route) => routes.includes(route)
+});
+
+const renderSubMenu = (props = {}) =>
+ render(
+
+ );
+
+describe("SubMenuItem", () => {
+ test("hides the folder when none of its children are accessible", () => {
+ const { container } = renderSubMenu({ memberObj: memberWith([]) });
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ test("hides a nested group when none of its children are accessible", () => {
+ renderSubMenu({
+ memberObj: memberWith(["events", "schedule", "rooms"]),
+ currentPath: "/app/events"
+ });
+
+ expect(screen.getByText("menu.schedule")).toBeInTheDocument();
+ expect(screen.queryByText("menu.track_chairs")).not.toBeInTheDocument();
+ });
+
+ test("opens every group down to the current route and leaves siblings closed", () => {
+ renderSubMenu({ currentPath: "/app/rooms" });
+
+ expect(screen.getByText("menu.room_list")).toBeInTheDocument();
+ expect(screen.getByText("menu.track_chairs")).toBeInTheDocument();
+ expect(screen.queryByText("menu.track_chair_list")).not.toBeInTheDocument();
+ });
+
+ test("renders nested groups recursively and navigates from the deepest leaf", async () => {
+ const onItemClick = jest.fn();
+ renderSubMenu({ onItemClick });
+
+ expect(screen.queryByText("menu.room_list")).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByText("menu.summit_events"));
+ await userEvent.click(screen.getByText("menu.schedule"));
+ await userEvent.click(screen.getByText("menu.rooms"));
+ await userEvent.click(screen.getByText("menu.room_list"));
+
+ expect(onItemClick).toHaveBeenCalledWith(expect.anything(), "rooms");
+ });
+});
diff --git a/src/components/menu/expandable-item.js b/src/components/menu/expandable-item.js
index a9aace324..72d372f84 100644
--- a/src/components/menu/expandable-item.js
+++ b/src/components/menu/expandable-item.js
@@ -19,11 +19,15 @@ import List from "@mui/material/List";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
+const DEFAULT_PL = 2;
+const HEADER_PR = 2;
+
function ExpandableItem({
label,
children,
defaultOpen = true,
- isHeader = false
+ isHeader = false,
+ pl
}) {
const [open, setOpen] = useState(defaultOpen);
@@ -31,7 +35,11 @@ function ExpandableItem({
<>
setOpen((prev) => !prev)}
- sx={{ py: 1, ...(isHeader ? { px: 2 } : { pl: 2 }) }}
+ sx={{
+ py: 1,
+ pl: pl ?? DEFAULT_PL,
+ ...(isHeader && { pr: HEADER_PR })
+ }}
>
{
+const Menu = ({ currentSummit, member, history, onNavigate, menuDefs }) => {
const memberObj = new Member(member);
- const globalItems = getGlobalItems();
- const summitItems = currentSummit ? getSummitItems(currentSummit.id) : [];
const onMenuItemClick = (ev, url) => {
ev.preventDefault();
@@ -66,14 +63,14 @@ const Menu = ({ currentSummit, member, history, onNavigate }) => {
return (
<>
- {globalItems.map(drawMenuItem)}
+ {menuDefs.global.map(drawMenuItem)}
{!!currentSummit?.id && (
<>
- {summitItems.map(drawMenuItem)}
+ {menuDefs.summit.map(drawMenuItem)}
>
)}
diff --git a/src/components/menu/menu-definition.js b/src/components/menu/menu-definition.js
index b03098d26..3fb6aa62a 100644
--- a/src/components/menu/menu-definition.js
+++ b/src/components/menu/menu-definition.js
@@ -33,36 +33,48 @@ export const getGlobalItems = () => [
]
},
{
- name: "companies",
- linkUrl: "companies",
- accessRoute: "companies"
- },
- {
- name: "sponsors_inventory",
- accessRoute: "inventory",
+ name: "sponsors_services",
subItems: [
{
- name: "inventory",
- linkUrl: "inventory"
- },
- {
- name: "form_templates",
- linkUrl: "form-templates"
+ name: "commercial_offerings",
+ subItems: [
+ { name: "inventory", linkUrl: "inventory", accessRoute: "inventory" },
+ {
+ name: "form_templates",
+ linkUrl: "form-templates",
+ accessRoute: "inventory"
+ }
+ ]
},
{
name: "page_templates",
- linkUrl: "page-templates"
+ linkUrl: "page-templates",
+ accessRoute: "inventory"
},
{
name: "add_on_types",
- linkUrl: "add-on-types"
+ linkUrl: "add-on-types",
+ accessRoute: "inventory"
+ },
+ {
+ name: "sponsorship_types",
+ linkUrl: "sponsorship-types",
+ accessRoute: "sponsorship-types"
}
]
},
{
- name: "sponsorship_types",
- linkUrl: "sponsorship-types",
- accessRoute: "sponsorship-types"
+ name: "emails",
+ accessRoute: "emails",
+ subItems: [
+ { name: "email_templates", linkUrl: "emails/templates" },
+ { name: "email_logs", linkUrl: "emails/log" }
+ ]
+ },
+ {
+ name: "companies",
+ linkUrl: "companies",
+ accessRoute: "companies"
},
{
name: "tags",
@@ -75,14 +87,6 @@ export const getGlobalItems = () => [
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",
@@ -96,110 +100,234 @@ export const getGlobalItems = () => [
];
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",
+ name: "program_management",
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: "events",
+ subItems: [
+ {
+ name: "new_event",
+ linkUrl: `summits/${summitId}/events/new`,
+ accessRoute: "events"
+ },
+ {
+ name: "event_list",
+ linkUrl: `summits/${summitId}/events`,
+ accessRoute: "events"
+ },
+ {
+ name: "event_types",
+ linkUrl: `summits/${summitId}/event-types`,
+ accessRoute: "events"
+ },
+ {
+ name: "event_categories",
+ linkUrl: `summits/${summitId}/event-categories`,
+ accessRoute: "events"
+ },
+ {
+ name: "event_category_groups",
+ linkUrl: `summits/${summitId}/event-category-groups`,
+ accessRoute: "events"
+ }
+ ]
+ },
+ {
+ name: "track_chairs",
+ accessRoute: "track-chairs",
+ subItems: [
+ {
+ name: "track_chair_list",
+ linkUrl: `summits/${summitId}/track-chairs`,
+ accessRoute: "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: "summit_speakers",
+ subItems: [
+ {
+ name: "summit_speaker_list",
+ linkUrl: `summits/${summitId}/speakers`,
+ accessRoute: "speakers"
+ },
+ {
+ name: "submission_invitations",
+ linkUrl: `summits/${summitId}/submission-invitations`,
+ accessRoute: "speakers"
+ },
+ {
+ name: "speaker_attendance",
+ linkUrl: `summits/${summitId}/speaker-attendances`,
+ accessRoute: "speakers"
+ },
+ {
+ name: "featured_speakers",
+ linkUrl: `summits/${summitId}/featured-speakers`,
+ accessRoute: "speakers"
+ }
+ ]
+ },
+ {
+ name: "selection_plans",
+ linkUrl: `summits/${summitId}/selection-plans`,
+ accessRoute: "selection_plans"
+ },
+ {
+ name: "schedule",
+ linkUrl: `summits/${summitId}/events/schedule`,
+ accessRoute: "events"
},
{
name: "voteable_presentations",
- linkUrl: `summits/${summitId}/voteable-presentations`
+ linkUrl: `summits/${summitId}/voteable-presentations`,
+ accessRoute: "events"
},
{
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`
+ linkUrl: `summits/${summitId}/media-uploads`,
+ accessRoute: "events"
}
]
},
{
- name: "summit_speakers",
- accessRoute: "events",
+ name: "attendee_operations",
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: "attendees",
+ subItems: [
+ {
+ name: "attendee-list",
+ linkUrl: `summits/${summitId}/attendees`,
+ accessRoute: "attendees"
+ },
+ {
+ name: "badge_checkin",
+ linkUrl: `summits/${summitId}/attendees/checkin`,
+ accessRoute: "attendees"
+ }
+ ]
+ },
+ {
+ name: "badges",
+ subItems: [
+ {
+ name: "badge_feature_list",
+ linkUrl: `summits/${summitId}/badge-features`,
+ accessRoute: "badges"
+ },
+ {
+ name: "access_level_list",
+ linkUrl: `summits/${summitId}/access-levels`,
+ accessRoute: "badges"
+ },
+ {
+ name: "view_type_list",
+ linkUrl: `summits/${summitId}/view-types`,
+ accessRoute: "badges"
+ },
+ {
+ name: "badge_type_list",
+ linkUrl: `summits/${summitId}/badge-types`,
+ accessRoute: "badges"
+ },
+ {
+ name: "badge_settings",
+ linkUrl: `summits/${summitId}/badge-settings`,
+ accessRoute: "badges"
+ }
+ ]
}
]
},
{
- name: "track_chairs",
- accessRoute: "track-chairs",
+ name: "registration",
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: "purchase_orders",
+ subItems: [
+ {
+ name: "purchase_order_list",
+ linkUrl: `summits/${summitId}/purchase-orders`,
+ accessRoute: "purchase-orders"
+ },
+ {
+ name: "ticket_list",
+ linkUrl: `summits/${summitId}/tickets`,
+ accessRoute: "purchase-orders"
+ },
+ {
+ name: "order_extra_questions",
+ linkUrl: `summits/${summitId}/order-extra-questions`,
+ accessRoute: "purchase-orders"
+ },
+ {
+ name: "registration_stats",
+ linkUrl: `summits/${summitId}/registration-stats`,
+ accessRoute: "purchase-orders"
+ }
+ ]
+ },
+ {
+ name: "tickets",
+ subItems: [
+ {
+ name: "registration_invitation_list",
+ linkUrl: `summits/${summitId}/registration-invitations`,
+ accessRoute: "tickets"
+ },
+ {
+ name: "ticket_type_list",
+ linkUrl: `summits/${summitId}/ticket-types`,
+ accessRoute: "tickets"
+ },
+ {
+ name: "promocode_list",
+ linkUrl: `summits/${summitId}/promocodes`,
+ accessRoute: "tickets"
+ },
+ {
+ name: "tax_type_list",
+ linkUrl: `summits/${summitId}/tax-types`,
+ accessRoute: "tickets"
+ },
+ {
+ name: "refund_policy_list",
+ linkUrl: `summits/${summitId}/refund-policies`,
+ accessRoute: "tickets"
+ },
+ {
+ name: "payment_profiles_list",
+ linkUrl: `summits/${summitId}/payment-profiles`,
+ accessRoute: "tickets"
+ },
+ {
+ name: "registration_companies_list",
+ linkUrl: `summits/${summitId}/registration-companies`,
+ accessRoute: "tickets"
+ }
+ ]
}
]
},
@@ -260,144 +388,93 @@ export const getSummitItems = (summitId) => [
]
},
{
- name: "locations",
- linkUrl: `summits/${summitId}/locations`,
- accessRoute: "locations"
- },
- {
- name: "signage",
- linkUrl: `summits/${summitId}/signage`,
- accessRoute: "signage"
- },
- {
- name: "purchase_orders",
- accessRoute: "purchase-orders",
+ name: "venue_schedule",
subItems: [
{
- name: "purchase_order_list",
- linkUrl: `summits/${summitId}/purchase-orders`
+ name: "locations",
+ linkUrl: `summits/${summitId}/locations`,
+ accessRoute: "locations"
+ },
+ {
+ name: "signage",
+ linkUrl: `summits/${summitId}/signage`,
+ accessRoute: "signage"
},
- { name: "ticket_list", linkUrl: `summits/${summitId}/tickets` },
{
- name: "order_extra_questions",
- linkUrl: `summits/${summitId}/order-extra-questions`
+ name: "room_bookings",
+ linkUrl: `summits/${summitId}/room-bookings`,
+ accessRoute: "room-bookings",
+ exclusive: "room-bookings"
},
{
- name: "registration_stats",
- linkUrl: `summits/${summitId}/registration-stats`
+ name: "room_occupancy",
+ linkUrl: `summits/${summitId}/room-occupancy`,
+ accessRoute: "room-occupancy"
}
]
},
{
- name: "tickets",
- accessRoute: "tickets",
+ name: "communications",
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: "email_flow_events",
+ subItems: [
+ {
+ name: "email_flow_overrides",
+ linkUrl: `summits/${summitId}/email-flow-events`,
+ accessRoute: "email-flow-events"
+ },
+ {
+ name: "email_flow_settings",
+ linkUrl: `summits/${summitId}/email-flow-events-settings`,
+ accessRoute: "email-flow-events"
+ }
+ ]
+ },
+ {
+ name: "push_notifications",
+ linkUrl: `summits/${summitId}/push-notifications`,
+ accessRoute: "push-notifications"
}
]
},
{
- name: "badges",
- accessRoute: "badges",
+ name: "event_administration",
subItems: [
{
- name: "badge_feature_list",
- linkUrl: `summits/${summitId}/badge-features`
- },
- {
- name: "access_level_list",
- linkUrl: `summits/${summitId}/access-levels`
+ name: "settings",
+ subItems: [
+ {
+ name: "marketing",
+ linkUrl: `summits/${summitId}/marketing`,
+ accessRoute: "settings"
+ },
+ {
+ name: "schedule_settings",
+ linkUrl: `summits/${summitId}/schedule-settings`,
+ accessRoute: "settings"
+ }
+ ]
},
{
- name: "view_type_list",
- linkUrl: `summits/${summitId}/view-types`
+ name: "reports",
+ linkUrl: `summits/${summitId}/reports`,
+ accessRoute: "reports"
},
{
- name: "badge_type_list",
- linkUrl: `summits/${summitId}/badge-types`
+ name: "summitdocs",
+ linkUrl: `summits/${summitId}/summitdocs`,
+ accessRoute: "summitdocs"
},
{
- 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: "tag_groups",
+ linkUrl: `summits/${summitId}/tag-groups`,
+ accessRoute: "tag-groups"
},
{
- 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`
+ name: "audit_log",
+ linkUrl: `summits/${summitId}/audit-log`,
+ accessRoute: "audit-log"
}
]
}
diff --git a/src/components/menu/menu-item.js b/src/components/menu/menu-item.js
index 4b1098001..86dab76cc 100644
--- a/src/components/menu/menu-item.js
+++ b/src/components/menu/menu-item.js
@@ -17,14 +17,16 @@ import Exclusive from "openstack-uicore-foundation/lib/components/exclusive-wrap
import ListItemButton from "@mui/material/ListItemButton";
import Typography from "@mui/material/Typography";
-const MenuItem = ({ name, onClick, exclusive, nested, selected }) => {
+const NESTED_PL = 4;
+const TOP_LEVEL_PL = 2;
+
+const MenuItem = ({ name, onClick, exclusive, nested, selected, pl }) => {
const itemHtml = (