Skip to content
Closed
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
171 changes: 170 additions & 1 deletion src/main/js/components/header/breadcrumbs-overflow.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,140 @@
import Utils from "@/components/dropdowns/utils";
import { createElementFromHtml } from "@/util/dom";
import Path from "@/util/path";

/**
* Maps context menu items from the server response to dropdown item format.
* This follows the same pattern used in jumplists.js for consistency.
*/
function mapContextMenuItems(items) {
return items.map((item) => {
if (item.type === "HEADER") {
return { type: "HEADER", label: item.displayName };
}
if (item.type === "SEPARATOR") {
return { type: "SEPARATOR" };
}
return {
icon: item.icon,
iconXml: item.iconXml,
label: item.displayName,
url: item.url,
type: item.post || item.requiresConfirmation ? "button" : "link",
badge: item.badge,
onClick: () => {
if (item.post || item.requiresConfirmation) {
if (item.requiresConfirmation) {
// dialog, crumb, notificationBar are globals from Jenkins
dialog
.confirm(item.displayName, { message: item.message })
.then(() => {
const form = document.createElement("form");
form.setAttribute("method", item.post ? "POST" : "GET");
form.setAttribute("action", item.url);
if (item.post) {
crumb.appendToForm(form);
}
document.body.appendChild(form);
form.submit();
});
} else {
fetch(item.url, {
method: "post",
headers: crumb.wrap({}),
}).then((rsp) => {
if (rsp.ok) {
notificationBar.show(
item.displayName + ": Done.",
notificationBar.SUCCESS,
);
} else {
notificationBar.show(
item.displayName + ": Failed.",
notificationBar.ERROR,
);
}
});
}
}
},
subMenu: item.subMenu ? () => mapContextMenuItems(item.subMenu.items) : null,
};
});
}

/**
* Creates a dropdown content callback for a collapsed breadcrumb item.
* This fetches the context menu and children context menu on demand.
*/
function createContextMenuCallback(hasModel, hasChildren, href) {
return (instance) => {
const sections = {
model: null,
children: null,
};

const fetchSection = (urlSuffix) => {
return fetch(Path.combinePath(href, urlSuffix))
.then((response) => response.json())
.then((json) => Utils.generateDropdownItems(mapContextMenuItems(json.items)));
};

const promises = [];

if (hasModel === "true") {
promises.push(
fetchSection("contextMenu").then((section) => {
section.prepend(
createElementFromHtml(
`<p class="jenkins-dropdown__heading">Actions</p>`,
),
);
sections.model = section;
}),
);
}

if (hasChildren === "true") {
promises.push(
fetchSection("childrenContextMenu").then((section) => {
section.prepend(
createElementFromHtml(
`<p class="jenkins-dropdown__heading">Navigation</p>`,
),
);
sections.children = section;
}),
);
}

Promise.all(promises)
.then(() => {
const container = document.createElement("div");
container.className = "jenkins-dropdown__split-container";

if (sections.model && !sections.children) {
container.appendChild(sections.model);
} else if (!sections.model && sections.children) {
container.appendChild(sections.children);
} else if (sections.model && sections.children) {
// Merge both sections into one dropdown for proper a11y
const dropbox = sections.model;
Array.from(sections.children.children).forEach((item) => {
dropbox.appendChild(item);
});
container.appendChild(dropbox);
}

instance.setContent(container);
})
.catch((error) => {
console.log(`Breadcrumb context menu fetch failed: ${error}`);
})
.finally(() => {
instance.loaded = true;
});
};
}

export default function computeBreadcrumbs() {
document
Expand Down Expand Up @@ -53,7 +188,41 @@ export default function computeBreadcrumbs() {
};
});

instance.setContent(Utils.generateDropdownItems(mappedItems));
const content = Utils.generateDropdownItems(mappedItems);

// Attach context menu dropdowns to overflow items that had them originally
items.forEach((breadcrumbItem, index) => {
const dropdownIndicator =
breadcrumbItem.querySelector(".dropdown-indicator");
if (!dropdownIndicator) {
return;
}

const hasModel = dropdownIndicator.getAttribute("data-model");
const hasChildren = dropdownIndicator.getAttribute("data-children");
const dataHref = dropdownIndicator.getAttribute("data-href");

if ((hasModel === "true" || hasChildren === "true") && dataHref) {
const overflowMenuItem = content.children[index];
if (overflowMenuItem) {
// Attach nested dropdown using the same pattern as jumplists.js
Utils.generateDropdown(
overflowMenuItem,
createContextMenuCallback(hasModel, hasChildren, dataHref),
false,
{
trigger: "mouseenter",
placement: "right-start",
offset: [-8, 0],
animation: "tooltip",
touch: false,
},
);
}
}
});

instance.setContent(content);
},
true,
{
Expand Down
Loading