Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions core/src/main/java/hudson/model/ManageJenkinsAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public String getUrlName() {
return "/manage";
}

@Override
public boolean isPrimaryAction() {
return true;
}

@Override
public Object getStaplerFallback() {
return Jenkins.get();
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/hudson/model/RootAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
5 changes: 5 additions & 0 deletions core/src/main/java/jenkins/model/navigation/SearchAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,9 @@ public String getDisplayName() {
public String getUrlName() {
return null;
}

@Override
public boolean isPrimaryAction() {
return true;
}
}
5 changes: 5 additions & 0 deletions core/src/main/java/jenkins/model/navigation/UserAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public User getUser() {
return User.current();
}

@Override
public boolean isPrimaryAction() {
return true;
}

@Restricted(NoExternalUse.class)
public List<Action> getActions() {
User current = User.current();
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/jenkins/views/Header.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Action> getActions() {
Expand Down
88 changes: 32 additions & 56 deletions core/src/main/resources/lib/layout/header/actions.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,44 @@
<div class="jenkins-header__actions">
<st:include page="suffix" optional="true" />

<j:forEach var="action" items="${it.actions}">
<j:set var="isCurrent" value="${h.hyperlinkMatchesCurrentPage(action.urlName)}" />

<j:set var="jumplist">
<st:include it="${action}" page="jumplist.jelly" optional="true" />
</j:set>
<j:set var="allActions" value="${it.actions}"/>
Copy link
Member Author

@jtnord jtnord Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh this all seems a bit wrong.

We have a generic taglib, but this rendering is specific to the JenkinsHeader

Open to better suggestions.


<j:if test="${jumplist.length() == 0}">
<j:set var="tooltip">
<st:include it="${action}" page="tooltip.jelly" optional="true" />
</j:set>
</j:if>
<!-- first render all primary actions (except the user login )-->
<j:forEach var="action" items="${allActions}">
<j:set var="isCurrent" value="${h.hyperlinkMatchesCurrentPage(action.urlName)}" />
<j:set var="isPrimary" value="${action.isPrimaryAction()}"/>

<j:set var="badge" value="${action.badge}" />
<j:if test="${jumplist.length() == 0 and tooltip.length() == 0}">
<j:set var="tooltip">
<div style="text-align: center;">${action.displayName}</div>
<j:if test="${badge != null}">
<div style="text-align: center; color: var(--text-color-secondary)">${badge.tooltip}</div>
</j:if>
</j:set>
<j:if test="${isCurrent or isPrimary}">
<!-- do not show the user avatar -->
<j:if test="${action.class.name != 'jenkins.model.navigation.UserAction'}">
<h:primaryAction action="${action}" isCurrent="${isCurrent}"/>
</j:if>
</j:if>
</j:forEach>

<j:set var="interactive" value="${jumplist.length() gt 0}" />
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
<x:element name="${action.urlName == null ? 'button' : 'a'}">
<x:attribute name="data-dropdown">${interactive}</x:attribute>
<x:attribute name="id">root-action-${action.class.simpleName}</x:attribute>
<x:attribute name="href">${h.getActionUrl(app.url, action)}</x:attribute>
<j:if test="${interactive}">
<x:attribute name="data-tippy-offset">[0, 10]</x:attribute>
<!-- we only want the hamburger menu if there is something to display -->
<!-- render all non primary non current actions -->
<j:set var="hamburgerEntries">
<j:forEach var="action" items="${allActions}">
<j:set var="isCurrent" value="${h.hyperlinkMatchesCurrentPage(action.urlName)}" />
<j:set var="isPrimary" value="${action.isPrimaryAction()}"/>
<j:if test="${!isCurrent and !isPrimary}">
<h:secondaryAction action="${action}"/>
</j:if>
<j:if test="${!interactive}">
<x:attribute name="data-html-tooltip" escapeText="false"><j:out value="${tooltip}" /></x:attribute>
</j:if>
<x:attribute name="data-tooltip-interactive">${interactive}</x:attribute>
<x:attribute name="data-tippy-animation">tooltip</x:attribute>
<x:attribute name="data-tippy-theme">${interactive ? 'dropdown' : 'tooltip'}</x:attribute>
<x:attribute name="data-tippy-trigger">mouseenter focus</x:attribute>
<x:attribute name="data-tippy-touch">${interactive}</x:attribute>
<x:attribute name="data-type">header-action</x:attribute>
<x:attribute name="draggable">false</x:attribute>
<x:attribute name="class">jenkins-button ${isCurrent ? '' : 'jenkins-button--tertiary'}</x:attribute>
<l:icon src="${icon}" class="${action.class.name == 'jenkins.model.navigation.UserAction' ? 'jenkins-avatar' : ''}"/>
<span class="jenkins-visually-hidden" data-type="action-label">${action.displayName}</span>
<j:if test="${badge != null}">
<span class="jenkins-badge jenkins-!-${badge.severity}-color" />
</j:if>
</x:element>

<j:if test="${interactive}">
<template>
<div class="jenkins-dropdown">
<j:out value="${jumplist}" />
</div>
</template>
</j:forEach>
</j:set>
<j:if test="${hamburgerEntries.length() gt 0}">
<l:overflowButton icon="symbol-menu-hamburger">
<j:out value="${hamburgerEntries}" />
</l:overflowButton>
</j:if>

<!-- finally the user avatar or login button (if required) -->
<j:forEach var="action" items="${allActions}">
<j:if test="${action.class.name == 'jenkins.model.navigation.UserAction'}">
<h:primaryAction action="${action}" isCurrent="${h.hyperlinkMatchesCurrentPage(action.urlName)}"/>
</j:if>

<j:set var="jumplist" />
<j:set var="tooltip" />
</j:forEach>

<h:login/>
</div>
</j:jelly>
</j:jelly>
72 changes: 72 additions & 0 deletions core/src/main/resources/lib/layout/header/primaryAction.jelly
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:h="/lib/layout/header" xmlns:l="/lib/layout" xmlns:st="jelly:stapler" xmlns:x="jelly:xml">


<st:documentation> <![CDATA[
Used inside <h:actions> to render primaryActions.
]]>
<st:attribute name="action" use="required">
The action to render
</st:attribute>
<st:attribute name="isCurrent" use="required">
true if the action is the action for the current page
</st:attribute>

</st:documentation>

<j:set var="jumplist">
<st:include it="${action}" page="jumplist.jelly" optional="true" />
</j:set>
<x:comment>rendering primary action: ${action.class.name}</x:comment>
<j:if test="${jumplist.length() == 0}">
<j:set var="tooltip">
<st:include it="${action}" page="tooltip.jelly" optional="true" />
</j:set>
</j:if>

<j:set var="badge" value="${action.badge}" />
<j:if test="${jumplist.length() == 0 and tooltip.length() == 0}">
<j:set var="tooltip">
<div style="text-align: center;">${action.displayName}</div>
<j:if test="${badge != null}">
<div style="text-align: center; color: var(--text-color-secondary)">${badge.tooltip}</div>
</j:if>
</j:set>
</j:if>

<j:set var="interactive" value="${jumplist.length() gt 0}" />
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
<x:element name="${action.urlName == null ? 'button' : 'a'}">
<x:attribute name="data-dropdown">${interactive}</x:attribute>
<x:attribute name="id">root-action-${action.class.simpleName}</x:attribute>
Copy link
Member Author

@jtnord jtnord Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: existing code, simpleName is liable to collisions.

<x:attribute name="href">${h.getActionUrl(app.url, action)}</x:attribute>
<j:if test="${interactive}">
<x:attribute name="data-tippy-offset">[0, 10]</x:attribute>
</j:if>
<j:if test="${!interactive}">
<x:attribute name="data-html-tooltip" escapeText="false"><j:out value="${tooltip}" /></x:attribute>
</j:if>
<x:attribute name="data-tooltip-interactive">${interactive}</x:attribute>
<x:attribute name="data-tippy-animation">tooltip</x:attribute>
<x:attribute name="data-tippy-theme">${interactive ? 'dropdown' : 'tooltip'}</x:attribute>
<x:attribute name="data-tippy-trigger">mouseenter focus</x:attribute>
<x:attribute name="data-tippy-touch">${interactive}</x:attribute>
<x:attribute name="data-type">header-action</x:attribute>
<x:attribute name="draggable">false</x:attribute>
<x:attribute name="class">jenkins-button ${isCurrent ? '' : 'jenkins-button--tertiary'}</x:attribute>
<l:icon src="${icon}" class="${action.class.name == 'jenkins.model.navigation.UserAction' ? 'jenkins-avatar' : ''}"/>
<span class="jenkins-visually-hidden" data-type="action-label">${action.displayName}</span>
<j:if test="${badge != null}">
<span class="jenkins-badge jenkins-!-${badge.severity}-color" />
</j:if>
</x:element>

<j:if test="${interactive}">
<template>
<div class="jenkins-dropdown">
<j:out value="${jumplist}" />
</div>
</template>
</j:if>

</j:jelly>
22 changes: 22 additions & 0 deletions core/src/main/resources/lib/layout/header/secondaryAction.jelly
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:h="/lib/layout/header" xmlns:l="/lib/layout" xmlns:dd="/lib/layout/dropdowns" xmlns:st="jelly:stapler" xmlns:x="jelly:xml">


<st:documentation> <![CDATA[
Used inside <h:actions> to render primaryActions.
]]>
<st:attribute name="action" use="required">
The action to render
</st:attribute>
</st:documentation>

<j:set var="badge" value="${action.badge}"/>
<j:set var="badgeClazz" value="${badge != null ? '' : 'jenkins-badge jenkins-!-${badge.severity}-color'}"/>

<dd:item icon="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"
text="${action.displayName}"
href="${h.getActionUrl(app.url, action)}"
clazz="${badgeClazz}">
</dd:item>

</j:jelly>
133 changes: 0 additions & 133 deletions src/main/js/components/header/actions-overflow.js
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to actions-touch.js and all the collapsing removed

This file was deleted.

Loading
Loading