Skip to content

Commit 39e85a0

Browse files
authored
[JENKINS-75683] re-introduce actions for Jenkins with custom action.jelly (#10678)
2 parents 855e213 + 4db689d commit 39e85a0

File tree

8 files changed

+372
-116
lines changed

8 files changed

+372
-116
lines changed

core/src/main/java/hudson/Functions.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import hudson.model.ParameterDefinition;
5656
import hudson.model.ParameterDefinition.ParameterDescriptor;
5757
import hudson.model.PasswordParameterDefinition;
58+
import hudson.model.RootAction;
5859
import hudson.model.Run;
5960
import hudson.model.Slave;
6061
import hudson.model.TimeZoneProperty;
@@ -2097,6 +2098,46 @@ public boolean hyperlinkMatchesCurrentPage(String href) {
20972098
return url.endsWith(href);
20982099
}
20992100

2101+
/**
2102+
* If the given {@code Action} is a {@link RootAction#isPrimaryAction() primary} {code RootAction}, or a parent of the current page, return {@code true}.
2103+
* Used in {@code actions.jelly} to decide if the action should shown in the main header or the hamburger.
2104+
*/
2105+
@Restricted(NoExternalUse.class)
2106+
public static boolean showInPrimaryHeader(Action action) {
2107+
// regular Actions can be injected into Jenkins via a factory.
2108+
if (action instanceof RootAction ra) {
2109+
if (ra.isPrimaryAction()) {
2110+
return true;
2111+
}
2112+
}
2113+
String path = Stapler.getCurrentRequest2().getPathInfo();
2114+
if (path == null || path.equals("/")) {
2115+
// we are in the root page so there is nothing current
2116+
return false;
2117+
}
2118+
2119+
String actionPath = action.getUrlName();
2120+
if (actionPath == null) {
2121+
// we are not a primary action and can not ever be current when we have no URL
2122+
return false;
2123+
}
2124+
2125+
// RootActions are not expected to start with a `/` but some do and some do, and not all Actions will be RootActions
2126+
// but path will always start with a "/"
2127+
if (!actionPath.startsWith("/")) {
2128+
actionPath = '/' + actionPath;
2129+
}
2130+
2131+
// the action /foo should not match /foobar but should match /foo/bar
2132+
if (!actionPath.endsWith("/")) {
2133+
actionPath = actionPath + '/';
2134+
}
2135+
if (!path.endsWith("/")) {
2136+
path = path + '/';
2137+
}
2138+
return path.startsWith(actionPath);
2139+
}
2140+
21002141
/**
21012142
* @deprecated From JEXL expressions ({@code ${…}}) in {@code *.jelly} files
21022143
* you can use {@code [obj]} syntax to construct an {@code Object[]}

core/src/main/java/jenkins/views/Header.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import hudson.ExtensionComponent;
44
import hudson.ExtensionList;
55
import hudson.ExtensionPoint;
6+
import hudson.Functions;
67
import hudson.model.Action;
78
import hudson.model.RootAction;
9+
import java.io.IOException;
810
import java.util.Comparator;
911
import java.util.List;
1012
import java.util.Map;
@@ -82,10 +84,24 @@ public List<Action> getActions() {
8284
return Jenkins.get()
8385
.getActions()
8486
.stream()
85-
.filter(e -> e.getIconFileName() != null || (e instanceof IconSpec is && is.getIconClassName() != null))
87+
.filter(e -> e.getIconFileName() != null || (e instanceof IconSpec is && is.getIconClassName() != null) || hasLegacyView(e))
8688
.sorted(Comparator.comparingDouble(
8789
a -> rootActionsOrdinal.getOrDefault(a.getClass().getName(), Double.MAX_VALUE)
8890
).reversed())
8991
.toList();
9092
}
93+
94+
/**
95+
* Jenkins will show actions with a custom action.jelly even if its getIconFileName returned {@code null}.
96+
* @param action the action to check if it has a custom view.
97+
* @return {@code true} iff the action has an {@code action.jelly}
98+
*/
99+
private static boolean hasLegacyView(Action action) {
100+
try {
101+
return Functions.hasView(action, "action");
102+
} catch (IOException ignored) {
103+
// can not load the view so ignore for the header
104+
return false;
105+
}
106+
}
91107
}

core/src/main/resources/lib/layout/header/actions.jelly

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<!-- first render all primary actions (except the user login )-->
99
<j:forEach var="action" items="${allActions}">
1010
<j:set var="isCurrent" value="${h.hyperlinkMatchesCurrentPage(action.urlName)}" />
11-
<j:set var="isPrimary" value="${action.isPrimaryAction()}"/>
11+
<j:set var="isPrimary" value="${h.showInPrimaryHeader(action)}"/>
1212

1313
<j:if test="${isCurrent or isPrimary}">
1414
<!-- do not show the user avatar -->
@@ -23,7 +23,7 @@
2323
<j:set var="hamburgerEntries">
2424
<j:forEach var="action" items="${allActions}">
2525
<j:set var="isCurrent" value="${h.hyperlinkMatchesCurrentPage(action.urlName)}" />
26-
<j:set var="isPrimary" value="${action.isPrimaryAction()}"/>
26+
<j:set var="isPrimary" value="${h.showInPrimaryHeader(action)}"/>
2727
<j:if test="${!isCurrent and !isPrimary}">
2828
<h:secondaryAction action="${action}"/>
2929
</j:if>

core/src/main/resources/lib/layout/header/primaryAction.jelly

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,59 +14,73 @@
1414

1515
</st:documentation>
1616

17-
<j:set var="jumplist">
18-
<st:include it="${action}" page="jumplist.jelly" optional="true" />
19-
</j:set>
20-
<x:comment>rendering primary action: ${action.class.name}</x:comment>
21-
<j:if test="${jumplist.length() == 0}">
22-
<j:set var="tooltip">
23-
<st:include it="${action}" page="tooltip.jelly" optional="true" />
24-
</j:set>
25-
</j:if>
17+
<j:choose>
18+
<j:when test="${h.hasView(action, 'action')}">
19+
<j:scope>
20+
<!-- Root actions expect to be in the context of Jenkins (as it) -->
21+
<j:set var="it" value="${app}"/>
22+
<j:set var="isInPrimaryHeader" value="true"/>
23+
<st:include page="action.jelly" from="${action}" optional="true"/>
24+
</j:scope>
25+
</j:when>
26+
<j:otherwise>
27+
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
28+
<j:if test="${icon != null}">
29+
<j:set var="jumplist">
30+
<st:include it="${action}" page="jumplist.jelly" optional="true" />
31+
</j:set>
32+
<x:comment>rendering primary action: ${action.class.name}</x:comment>
33+
<j:if test="${jumplist.length() == 0}">
34+
<j:set var="tooltip">
35+
<st:include it="${action}" page="tooltip.jelly" optional="true" />
36+
</j:set>
37+
</j:if>
2638

27-
<j:set var="badge" value="${action.badge}" />
28-
<j:if test="${jumplist.length() == 0 and tooltip.length() == 0}">
29-
<j:set var="tooltip">
30-
<div style="text-align: center;">${action.displayName}</div>
31-
<j:if test="${badge != null}">
32-
<div style="text-align: center; color: var(--text-color-secondary)">${badge.tooltip}</div>
33-
</j:if>
34-
</j:set>
35-
</j:if>
39+
<j:set var="badge" value="${action.badge}" />
40+
<j:if test="${jumplist.length() == 0 and tooltip.length() == 0}">
41+
<j:set var="tooltip">
42+
<div style="text-align: center;">${action.displayName}</div>
43+
<j:if test="${badge != null}">
44+
<div style="text-align: center; color: var(--text-color-secondary)">${badge.tooltip}</div>
45+
</j:if>
46+
</j:set>
47+
</j:if>
3648

37-
<j:set var="interactive" value="${jumplist.length() gt 0}" />
38-
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
39-
<x:element name="${action.urlName == null ? 'button' : 'a'}">
40-
<x:attribute name="data-dropdown">${interactive}</x:attribute>
41-
<x:attribute name="id">root-action-${action.class.simpleName}</x:attribute>
42-
<x:attribute name="href">${h.getActionUrl(app.url, action)}</x:attribute>
43-
<j:if test="${interactive}">
44-
<x:attribute name="data-tippy-offset">[0, 10]</x:attribute>
45-
</j:if>
46-
<j:if test="${!interactive}">
47-
<x:attribute name="data-html-tooltip" escapeText="false"><j:out value="${tooltip}" /></x:attribute>
48-
</j:if>
49-
<x:attribute name="data-tooltip-interactive">${interactive}</x:attribute>
50-
<x:attribute name="data-tippy-animation">tooltip</x:attribute>
51-
<x:attribute name="data-tippy-theme">${interactive ? 'dropdown' : 'tooltip'}</x:attribute>
52-
<x:attribute name="data-tippy-trigger">mouseenter focus</x:attribute>
53-
<x:attribute name="data-tippy-touch">${interactive}</x:attribute>
54-
<x:attribute name="data-type">header-action</x:attribute>
55-
<x:attribute name="draggable">false</x:attribute>
56-
<x:attribute name="class">jenkins-button ${isCurrent ? '' : 'jenkins-button--tertiary'}</x:attribute>
57-
<l:icon src="${icon}" class="${action.class.name == 'jenkins.model.navigation.UserAction' ? 'jenkins-avatar' : ''}"/>
58-
<span class="jenkins-visually-hidden" data-type="action-label">${action.displayName}</span>
59-
<j:if test="${badge != null}">
60-
<span class="jenkins-badge jenkins-!-${badge.severity}-color" />
61-
</j:if>
62-
</x:element>
49+
<j:set var="interactive" value="${jumplist.length() gt 0}" />
50+
<x:element name="${action.urlName == null ? 'button' : 'a'}">
51+
<x:attribute name="data-dropdown">${interactive}</x:attribute>
52+
<x:attribute name="id">root-action-${action.class.simpleName}</x:attribute>
53+
<x:attribute name="href">${h.getActionUrl(app.url, action)}</x:attribute>
54+
<j:if test="${interactive}">
55+
<x:attribute name="data-tippy-offset">[0, 10]</x:attribute>
56+
</j:if>
57+
<j:if test="${!interactive}">
58+
<x:attribute name="data-html-tooltip" escapeText="false"><j:out value="${tooltip}" /></x:attribute>
59+
</j:if>
60+
<x:attribute name="data-tooltip-interactive">${interactive}</x:attribute>
61+
<x:attribute name="data-tippy-animation">tooltip</x:attribute>
62+
<x:attribute name="data-tippy-theme">${interactive ? 'dropdown' : 'tooltip'}</x:attribute>
63+
<x:attribute name="data-tippy-trigger">mouseenter focus</x:attribute>
64+
<x:attribute name="data-tippy-touch">${interactive}</x:attribute>
65+
<x:attribute name="data-type">header-action</x:attribute>
66+
<x:attribute name="draggable">false</x:attribute>
67+
<x:attribute name="class">jenkins-button ${isCurrent ? '' : 'jenkins-button--tertiary'}</x:attribute>
68+
<l:icon src="${icon}" class="${action.class.name == 'jenkins.model.navigation.UserAction' ? 'jenkins-avatar' : ''}"/>
69+
<span class="jenkins-visually-hidden" data-type="action-label">${action.displayName}</span>
70+
<j:if test="${badge != null}">
71+
<span class="jenkins-badge jenkins-!-${badge.severity}-color" />
72+
</j:if>
73+
</x:element>
6374

64-
<j:if test="${interactive}">
65-
<template>
66-
<div class="jenkins-dropdown">
67-
<j:out value="${jumplist}" />
68-
</div>
69-
</template>
70-
</j:if>
75+
<j:if test="${interactive}">
76+
<template>
77+
<div class="jenkins-dropdown">
78+
<j:out value="${jumplist}" />
79+
</div>
80+
</template>
81+
</j:if>
82+
</j:if>
83+
</j:otherwise>
84+
</j:choose>
7185

7286
</j:jelly>

core/src/main/resources/lib/layout/header/secondaryAction.jelly

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,27 @@
1010
</st:attribute>
1111
</st:documentation>
1212

13-
<j:set var="badge" value="${action.badge}"/>
14-
<j:set var="badgeClazz" value="${badge != null ? 'jenkins-badge jenkins-!-${badge.severity}-color' : ''}"/>
15-
16-
<dd:item icon="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"
17-
text="${action.displayName}"
18-
href="${h.getActionUrl(app.url, action)}"
19-
clazz="${badgeClazz}">
20-
</dd:item>
13+
<j:choose>
14+
<j:when test="${h.hasView(action, 'action')}">
15+
<j:scope>
16+
<!-- Root actions expect to be in the context of Jenkins (as it) -->
17+
<j:set var="it" value="${app}"/>
18+
<j:set var="isInSecondaryHeader" value="true"/>
19+
<st:include page="action.jelly" from="${action}" optional="true"/>
20+
</j:scope>
21+
</j:when>
22+
<j:otherwise>
23+
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
24+
<j:if test="${icon != null}">
25+
<j:set var="badge" value="${action.badge}"/>
26+
<j:set var="badgeClazz" value="${badge != null ? 'jenkins-badge jenkins-!-${badge.severity}-color' : ''}"/>
2127

28+
<dd:item icon="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"
29+
text="${action.displayName}"
30+
href="${h.getActionUrl(app.url, action)}"
31+
clazz="${badgeClazz}">
32+
</dd:item>
33+
</j:if>
34+
</j:otherwise>
35+
</j:choose>
2236
</j:jelly>

0 commit comments

Comments
 (0)