Skip to content

Commit 1a7c570

Browse files
committed
fix: restore context menus for collapsed breadcrumbs (#16764)
When breadcrumbs overflow and are collapsed into the overflow menu, they were losing their context menu functionality. The overflow menu was only creating simple link items without preserving the dropdown indicators that provide access to contextMenu and childrenContextMenu. This fix detects breadcrumb items that have dropdown-indicator elements and attaches nested tippy dropdowns to the corresponding overflow menu items. The context menu data is fetched on hover, matching the behavior of non-collapsed breadcrumbs. The implementation: - Checks each collapsed breadcrumb item for .dropdown-indicator - Extracts data-model, data-children, and data-href attributes - Attaches a submenu dropdown using Utils.generateDropdown - Fetches and renders context menu items with proper action handling - Preserves all menu item types (HEADER, SEPARATOR, actions, navigation) Fixes #16764
1 parent 396e6e9 commit 1a7c570

File tree

1 file changed

+170
-1
lines changed

1 file changed

+170
-1
lines changed

src/main/js/components/header/breadcrumbs-overflow.js

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,140 @@
11
import Utils from "@/components/dropdowns/utils";
22
import { createElementFromHtml } from "@/util/dom";
3+
import Path from "@/util/path";
4+
5+
/**
6+
* Maps context menu items from the server response to dropdown item format.
7+
* This follows the same pattern used in jumplists.js for consistency.
8+
*/
9+
function mapContextMenuItems(items) {
10+
return items.map((item) => {
11+
if (item.type === "HEADER") {
12+
return { type: "HEADER", label: item.displayName };
13+
}
14+
if (item.type === "SEPARATOR") {
15+
return { type: "SEPARATOR" };
16+
}
17+
return {
18+
icon: item.icon,
19+
iconXml: item.iconXml,
20+
label: item.displayName,
21+
url: item.url,
22+
type: item.post || item.requiresConfirmation ? "button" : "link",
23+
badge: item.badge,
24+
onClick: () => {
25+
if (item.post || item.requiresConfirmation) {
26+
if (item.requiresConfirmation) {
27+
// dialog, crumb, notificationBar are globals from Jenkins
28+
dialog
29+
.confirm(item.displayName, { message: item.message })
30+
.then(() => {
31+
const form = document.createElement("form");
32+
form.setAttribute("method", item.post ? "POST" : "GET");
33+
form.setAttribute("action", item.url);
34+
if (item.post) {
35+
crumb.appendToForm(form);
36+
}
37+
document.body.appendChild(form);
38+
form.submit();
39+
});
40+
} else {
41+
fetch(item.url, {
42+
method: "post",
43+
headers: crumb.wrap({}),
44+
}).then((rsp) => {
45+
if (rsp.ok) {
46+
notificationBar.show(
47+
item.displayName + ": Done.",
48+
notificationBar.SUCCESS,
49+
);
50+
} else {
51+
notificationBar.show(
52+
item.displayName + ": Failed.",
53+
notificationBar.ERROR,
54+
);
55+
}
56+
});
57+
}
58+
}
59+
},
60+
subMenu: item.subMenu ? () => mapContextMenuItems(item.subMenu.items) : null,
61+
};
62+
});
63+
}
64+
65+
/**
66+
* Creates a dropdown content callback for a collapsed breadcrumb item.
67+
* This fetches the context menu and children context menu on demand.
68+
*/
69+
function createContextMenuCallback(hasModel, hasChildren, href) {
70+
return (instance) => {
71+
const sections = {
72+
model: null,
73+
children: null,
74+
};
75+
76+
const fetchSection = (urlSuffix) => {
77+
return fetch(Path.combinePath(href, urlSuffix))
78+
.then((response) => response.json())
79+
.then((json) => Utils.generateDropdownItems(mapContextMenuItems(json.items)));
80+
};
81+
82+
const promises = [];
83+
84+
if (hasModel === "true") {
85+
promises.push(
86+
fetchSection("contextMenu").then((section) => {
87+
section.prepend(
88+
createElementFromHtml(
89+
`<p class="jenkins-dropdown__heading">Actions</p>`,
90+
),
91+
);
92+
sections.model = section;
93+
}),
94+
);
95+
}
96+
97+
if (hasChildren === "true") {
98+
promises.push(
99+
fetchSection("childrenContextMenu").then((section) => {
100+
section.prepend(
101+
createElementFromHtml(
102+
`<p class="jenkins-dropdown__heading">Navigation</p>`,
103+
),
104+
);
105+
sections.children = section;
106+
}),
107+
);
108+
}
109+
110+
Promise.all(promises)
111+
.then(() => {
112+
const container = document.createElement("div");
113+
container.className = "jenkins-dropdown__split-container";
114+
115+
if (sections.model && !sections.children) {
116+
container.appendChild(sections.model);
117+
} else if (!sections.model && sections.children) {
118+
container.appendChild(sections.children);
119+
} else if (sections.model && sections.children) {
120+
// Merge both sections into one dropdown for proper a11y
121+
const dropbox = sections.model;
122+
Array.from(sections.children.children).forEach((item) => {
123+
dropbox.appendChild(item);
124+
});
125+
container.appendChild(dropbox);
126+
}
127+
128+
instance.setContent(container);
129+
})
130+
.catch((error) => {
131+
console.log(`Breadcrumb context menu fetch failed: ${error}`);
132+
})
133+
.finally(() => {
134+
instance.loaded = true;
135+
});
136+
};
137+
}
3138

4139
export default function computeBreadcrumbs() {
5140
document
@@ -53,7 +188,41 @@ export default function computeBreadcrumbs() {
53188
};
54189
});
55190

56-
instance.setContent(Utils.generateDropdownItems(mappedItems));
191+
const content = Utils.generateDropdownItems(mappedItems);
192+
193+
// Attach context menu dropdowns to overflow items that had them originally
194+
items.forEach((breadcrumbItem, index) => {
195+
const dropdownIndicator =
196+
breadcrumbItem.querySelector(".dropdown-indicator");
197+
if (!dropdownIndicator) {
198+
return;
199+
}
200+
201+
const hasModel = dropdownIndicator.getAttribute("data-model");
202+
const hasChildren = dropdownIndicator.getAttribute("data-children");
203+
const dataHref = dropdownIndicator.getAttribute("data-href");
204+
205+
if ((hasModel === "true" || hasChildren === "true") && dataHref) {
206+
const overflowMenuItem = content.children[index];
207+
if (overflowMenuItem) {
208+
// Attach nested dropdown using the same pattern as jumplists.js
209+
Utils.generateDropdown(
210+
overflowMenuItem,
211+
createContextMenuCallback(hasModel, hasChildren, dataHref),
212+
false,
213+
{
214+
trigger: "mouseenter",
215+
placement: "right-start",
216+
offset: [-8, 0],
217+
animation: "tooltip",
218+
touch: false,
219+
},
220+
);
221+
}
222+
}
223+
});
224+
225+
instance.setContent(content);
57226
},
58227
true,
59228
{

0 commit comments

Comments
 (0)