From 7c09e8ad46c44853331ef6f84f208d8e0e58595d Mon Sep 17 00:00:00 2001 From: James Nord Date: Thu, 5 Jun 2025 18:55:40 +0100 Subject: [PATCH 1/6] Distinguish between primary and secondary actions in the header This PR distinguishes between primary and secondary Actions for the header. Primary actions are always shown, and secondary actions are by default in the Hamburger menu. When the current page is the page for the secondary action (or a parent thereof) the action is put in the main header action list and not in the hamburger menu. --- .../hudson/model/ManageJenkinsAction.java | 5 ++ .../main/java/hudson/model/RootAction.java | 10 +++ .../model/navigation/SearchAction.java | 5 ++ .../jenkins/model/navigation/UserAction.java | 5 ++ core/src/main/java/jenkins/views/Header.java | 3 +- .../resources/lib/layout/header/actions.jelly | 88 +++++++------------ .../lib/layout/header/primaryAction.jelly | 72 +++++++++++++++ .../lib/layout/header/secondaryAction.jelly | 22 +++++ .../images/symbols/menu-hamburger.svg | 3 + 9 files changed, 156 insertions(+), 57 deletions(-) create mode 100644 core/src/main/resources/lib/layout/header/primaryAction.jelly create mode 100644 core/src/main/resources/lib/layout/header/secondaryAction.jelly create mode 100644 war/src/main/resources/images/symbols/menu-hamburger.svg diff --git a/core/src/main/java/hudson/model/ManageJenkinsAction.java b/core/src/main/java/hudson/model/ManageJenkinsAction.java index 959028933030..5c46ceff5319 100644 --- a/core/src/main/java/hudson/model/ManageJenkinsAction.java +++ b/core/src/main/java/hudson/model/ManageJenkinsAction.java @@ -68,6 +68,11 @@ public String getUrlName() { return "/manage"; } + @Override + public boolean isPrimaryAction() { + return true; + } + @Override public Object getStaplerFallback() { return Jenkins.get(); diff --git a/core/src/main/java/hudson/model/RootAction.java b/core/src/main/java/hudson/model/RootAction.java index d1279c47e81e..b13e69f160d7 100644 --- a/core/src/main/java/hudson/model/RootAction.java +++ b/core/src/main/java/hudson/model/RootAction.java @@ -50,4 +50,14 @@ public interface RootAction extends Action, ExtensionPoint { default @CheckForNull Badge getBadge() { return null; } + + /** + * Identifies if the action as a primary action. + * Primary actions may be handled differently in the UI (for existence by always showing on the header rather than in an actions dropdown). + * In almost all cases this should return {@code false} which is the default + * @return {@code true} iff this action should be considered primary. + */ + default boolean isPrimaryAction() { + return false; + } } diff --git a/core/src/main/java/jenkins/model/navigation/SearchAction.java b/core/src/main/java/jenkins/model/navigation/SearchAction.java index 36a56bda2f53..adb10d627280 100644 --- a/core/src/main/java/jenkins/model/navigation/SearchAction.java +++ b/core/src/main/java/jenkins/model/navigation/SearchAction.java @@ -47,4 +47,9 @@ public String getDisplayName() { public String getUrlName() { return null; } + + @Override + public boolean isPrimaryAction() { + return true; + } } diff --git a/core/src/main/java/jenkins/model/navigation/UserAction.java b/core/src/main/java/jenkins/model/navigation/UserAction.java index 5900e3d09708..c85829f6e5a4 100644 --- a/core/src/main/java/jenkins/model/navigation/UserAction.java +++ b/core/src/main/java/jenkins/model/navigation/UserAction.java @@ -80,6 +80,11 @@ public User getUser() { return User.current(); } + @Override + public boolean isPrimaryAction() { + return true; + } + @Restricted(NoExternalUse.class) public List getActions() { User current = User.current(); diff --git a/core/src/main/java/jenkins/views/Header.java b/core/src/main/java/jenkins/views/Header.java index 083f241340b9..d6ad84563447 100644 --- a/core/src/main/java/jenkins/views/Header.java +++ b/core/src/main/java/jenkins/views/Header.java @@ -64,7 +64,8 @@ public static Header get() { } /** - * @return a list of {@link Action} to show in the header, defaults to {@link hudson.model.RootAction} extensions + * @return a list of {@link Action} to show in the header. + * The default implemention returns an {@link Jenkins#getActions()} that should be displayed (ie have an icon). */ @Restricted(NoExternalUse.class) public List getActions() { diff --git a/core/src/main/resources/lib/layout/header/actions.jelly b/core/src/main/resources/lib/layout/header/actions.jelly index 874e0cce9476..4e30e5266dc7 100644 --- a/core/src/main/resources/lib/layout/header/actions.jelly +++ b/core/src/main/resources/lib/layout/header/actions.jelly @@ -3,68 +3,44 @@
- - - - - - + - - - - - + + + + - - - -
${action.displayName}
- -
${badge.tooltip}
-
-
+ + + + + +
- - - - ${interactive} - root-action-${action.class.simpleName} - ${h.getActionUrl(app.url, action)} - - [0, 10] + + + + + + + + - - - - ${interactive} - tooltip - ${interactive ? 'dropdown' : 'tooltip'} - mouseenter focus - ${interactive} - header-action - false - jenkins-button ${isCurrent ? '' : 'jenkins-button--tertiary'} - - ${action.displayName} - - - - - - - +
+ + + + + + + + + + + - - - -
- \ No newline at end of file + diff --git a/core/src/main/resources/lib/layout/header/primaryAction.jelly b/core/src/main/resources/lib/layout/header/primaryAction.jelly new file mode 100644 index 000000000000..a199bf00ee75 --- /dev/null +++ b/core/src/main/resources/lib/layout/header/primaryAction.jelly @@ -0,0 +1,72 @@ + + + + + to render primaryActions. + ]]> + + The action to render + + + true if the action is the action for the current page + + + + + + + + rendering primary action: ${action.class.name} + + + + + + + + + +
${action.displayName}
+ +
${badge.tooltip}
+
+
+
+ + + + + ${interactive} + root-action-${action.class.simpleName} + ${h.getActionUrl(app.url, action)} + + [0, 10] + + + + + ${interactive} + tooltip + ${interactive ? 'dropdown' : 'tooltip'} + mouseenter focus + ${interactive} + header-action + false + jenkins-button ${isCurrent ? '' : 'jenkins-button--tertiary'} + + ${action.displayName} + + + + + + + + + +
diff --git a/core/src/main/resources/lib/layout/header/secondaryAction.jelly b/core/src/main/resources/lib/layout/header/secondaryAction.jelly new file mode 100644 index 000000000000..cf9f72abd0c5 --- /dev/null +++ b/core/src/main/resources/lib/layout/header/secondaryAction.jelly @@ -0,0 +1,22 @@ + + + + + to render primaryActions. + ]]> + + The action to render + + + + + + + + + + diff --git a/war/src/main/resources/images/symbols/menu-hamburger.svg b/war/src/main/resources/images/symbols/menu-hamburger.svg new file mode 100644 index 000000000000..3ecffda8c5f1 --- /dev/null +++ b/war/src/main/resources/images/symbols/menu-hamburger.svg @@ -0,0 +1,3 @@ + + + From 8adbd0a34f3c6ccb32cedef9ae0a850dd4862354 Mon Sep 17 00:00:00 2001 From: James Nord Date: Tue, 17 Jun 2025 10:53:39 +0100 Subject: [PATCH 2/6] remove actions-overflow --- .../js/components/header/actions-overflow.js | 133 ------------------ .../js/components/header/actions-touch.js | 16 +++ src/main/js/components/header/index.js | 4 +- 3 files changed, 18 insertions(+), 135 deletions(-) delete mode 100644 src/main/js/components/header/actions-overflow.js create mode 100644 src/main/js/components/header/actions-touch.js diff --git a/src/main/js/components/header/actions-overflow.js b/src/main/js/components/header/actions-overflow.js deleted file mode 100644 index 62d84f5f7bd3..000000000000 --- a/src/main/js/components/header/actions-overflow.js +++ /dev/null @@ -1,133 +0,0 @@ -import Utils from "@/components/dropdowns/utils"; -import { createElementFromHtml } from "@/util/dom"; - -const OVERFLOW_ID = "jenkins-header-actions-overflow"; - -export default function computeActions() { - document - .querySelectorAll( - ".jenkins-header__actions .jenkins-button[data-type='header-action'].jenkins-hidden", - ) - .forEach((e) => { - e.classList.remove("jenkins-hidden"); - }); - - if (!actionsOverflows()) { - removeOverflowButton(); - return; - } - - const items = []; - const actions = Array.from( - document.querySelectorAll( - ".jenkins-header__actions .jenkins-button[data-type='header-action']", - ), - ).slice(1, -1); - - const overflowButton = generateOverflowButton(); - - while (actionsOverflows()) { - const item = actions.pop(); - - if (!item) { - break; - } - - items.unshift(item); - item.classList.add("jenkins-hidden"); - } - - Utils.generateDropdown( - overflowButton, - (instance) => { - const mappedItems = items.map((e) => { - let icon = e.querySelector("img"); - if (icon) { - icon = icon.src; - } - let iconXml = e.querySelector("svg"); - if (iconXml) { - icon = true; - iconXml = iconXml.outerHTML; - } - - const span = e.querySelector("[data-type='action-label']"); - let label = e.textContent; - if (span !== null) { - label = span.textContent; - } - - return { - type: "link", - icon: icon, - iconXml: iconXml, - label: label, - url: e.href, - }; - }); - - instance.setContent(Utils.generateDropdownItems(mappedItems)); - }, - true, - { - trigger: "mouseenter focus", - offset: [0, 10], - animation: "tooltip", - }, - ); - - // We want to disable the User action href on touch devices so that they can still activate the overflow menu - const link = document.querySelector("#root-action-UserAction"); - - if (link) { - const originalHref = link.getAttribute("href"); - const isTouchDevice = window.matchMedia("(hover: none)").matches; - - // HTMLUnit doesn't register itself as supporting hover, thus the href is removed when it shouldn't be - if (isTouchDevice && !window.isRunAsTest) { - link.removeAttribute("href"); - } else { - link.setAttribute("href", originalHref); - } - } -} - -function actionsOverflows() { - const actions = document.querySelector(".jenkins-header__actions"); - return actions.offsetWidth > Math.max(window.innerWidth / 4.5, 150); -} - -function generateOverflowButton() { - // If an overflow menu already exists let's use that - const overflowMenu = document.querySelector("#" + OVERFLOW_ID); - if (overflowMenu) { - return overflowMenu; - } - - // Generate an overflow menu to store actions - const element = - createElementFromHtml(``); - - const actionsContainer = document.querySelector(".jenkins-header__actions"); - - // Insert the new element before the last child - actionsContainer.insertBefore( - element, - actionsContainer.children[actionsContainer.children.length - 2], - ); - - return element; -} - -function removeOverflowButton() { - const overflowButton = document.querySelector("#" + OVERFLOW_ID); - - if (overflowButton) { - overflowButton.remove(); - } -} diff --git a/src/main/js/components/header/actions-touch.js b/src/main/js/components/header/actions-touch.js new file mode 100644 index 000000000000..d5f691ee6e30 --- /dev/null +++ b/src/main/js/components/header/actions-touch.js @@ -0,0 +1,16 @@ +export default function updateActionsForTouch() { + // We want to disable the User action href on touch devices so that they can still activate the overflow menu + const link = document.querySelector("#root-action-UserAction"); + + if (link) { + const originalHref = link.getAttribute("href"); + const isTouchDevice = window.matchMedia("(hover: none)").matches; + + // HTMLUnit doesn't register itself as supporting hover, thus the href is removed when it shouldn't be + if (isTouchDevice && !window.isRunAsTest) { + link.removeAttribute("href"); + } else { + link.setAttribute("href", originalHref); + } + } +} diff --git a/src/main/js/components/header/index.js b/src/main/js/components/header/index.js index 0d864ec32a3b..4df9be01e90d 100644 --- a/src/main/js/components/header/index.js +++ b/src/main/js/components/header/index.js @@ -1,9 +1,10 @@ -import computeActions from "@/components/header/actions-overflow"; +import updateActionsForTouch from "@/components/header/actions-touch"; import computeBreadcrumbs from "@/components/header/breadcrumbs-overflow"; function init() { // Recompute what actions and breadcrumbs should be visible when the viewport size is changed computeOverflow(); + updateActionsForTouch(); let lastWidth = window.innerWidth; window.addEventListener("resize", () => { if (window.innerWidth !== lastWidth) { @@ -59,7 +60,6 @@ function init() { } function computeOverflow() { - computeActions(); computeBreadcrumbs(); } From c701361ec7bc9aa58d7745de6291a01b3d7abbe4 Mon Sep 17 00:00:00 2001 From: James Nord Date: Thu, 19 Jun 2025 11:03:50 +0100 Subject: [PATCH 3/6] Fix Typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/src/main/java/jenkins/views/Header.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/jenkins/views/Header.java b/core/src/main/java/jenkins/views/Header.java index d6ad84563447..bbdfaeb85cfc 100644 --- a/core/src/main/java/jenkins/views/Header.java +++ b/core/src/main/java/jenkins/views/Header.java @@ -65,7 +65,7 @@ public static Header get() { /** * @return a list of {@link Action} to show in the header. - * The default implemention returns an {@link Jenkins#getActions()} that should be displayed (ie have an icon). + * The default implementation returns an {@link Jenkins#getActions()} that should be displayed (ie have an icon). */ @Restricted(NoExternalUse.class) public List getActions() { From 188baf00e23df5a15b5907a8ea1c81232833e5e5 Mon Sep 17 00:00:00 2001 From: James Nord Date: Thu, 19 Jun 2025 11:07:38 +0100 Subject: [PATCH 4/6] fix inverted badgeClass Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/src/main/resources/lib/layout/header/secondaryAction.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/lib/layout/header/secondaryAction.jelly b/core/src/main/resources/lib/layout/header/secondaryAction.jelly index cf9f72abd0c5..508fb3f79495 100644 --- a/core/src/main/resources/lib/layout/header/secondaryAction.jelly +++ b/core/src/main/resources/lib/layout/header/secondaryAction.jelly @@ -11,7 +11,7 @@ - + Date: Fri, 20 Jun 2025 11:33:08 +0100 Subject: [PATCH 5/6] Update Javadoc comment for isPrimaryAction Co-authored-by: Mark Waite --- core/src/main/java/hudson/model/RootAction.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/hudson/model/RootAction.java b/core/src/main/java/hudson/model/RootAction.java index b13e69f160d7..327cbe706ad1 100644 --- a/core/src/main/java/hudson/model/RootAction.java +++ b/core/src/main/java/hudson/model/RootAction.java @@ -52,10 +52,11 @@ public interface RootAction extends Action, ExtensionPoint { } /** - * Identifies if the action as a primary action. - * Primary actions may be handled differently in the UI (for existence by always showing on the header rather than in an actions dropdown). + * Identifies if the action is a primary action. + * Primary actions may be handled differently in the UI (for example, by always showing on the header rather than in an actions dropdown). * In almost all cases this should return {@code false} which is the default * @return {@code true} iff this action should be considered primary. + * @since TODO */ default boolean isPrimaryAction() { return false; From edec1345108d5c116641c001397be0ffeba23b2d Mon Sep 17 00:00:00 2001 From: James Nord Date: Fri, 20 Jun 2025 14:56:09 +0100 Subject: [PATCH 6/6] add ID to header "More Actions" button --- core/src/main/resources/lib/layout/header/actions.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/lib/layout/header/actions.jelly b/core/src/main/resources/lib/layout/header/actions.jelly index 4e30e5266dc7..a7a4ae939ec4 100644 --- a/core/src/main/resources/lib/layout/header/actions.jelly +++ b/core/src/main/resources/lib/layout/header/actions.jelly @@ -30,7 +30,7 @@ - +