mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-10-26 19:15:09 +00:00
[JENKINS-75683] re-introduce actions for Jenkins with custom action.jelly (#10678)
This commit is contained in:
8 files changed
+377
-121
No files matched your search
@@ -55,6 +55,7 @@ import hudson.model.PaneStatusProperties;
|
||||
import hudson.model.ParameterDefinition;
|
||||
import hudson.model.ParameterDefinition.ParameterDescriptor;
|
||||
import hudson.model.PasswordParameterDefinition;
|
||||
import hudson.model.RootAction;
|
||||
import hudson.model.Run;
|
||||
import hudson.model.Slave;
|
||||
import hudson.model.TimeZoneProperty;
|
||||
@@ -2097,6 +2098,46 @@ public class Functions {
|
||||
return url.endsWith(href);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given {@code Action} is a {@link RootAction#isPrimaryAction() primary} {code RootAction}, or a parent of the current page, return {@code true}.
|
||||
* Used in {@code actions.jelly} to decide if the action should shown in the main header or the hamburger.
|
||||
*/
|
||||
@Restricted(NoExternalUse.class)
|
||||
public static boolean showInPrimaryHeader(Action action) {
|
||||
// regular Actions can be injected into Jenkins via a factory.
|
||||
if (action instanceof RootAction ra) {
|
||||
if (ra.isPrimaryAction()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
String path = Stapler.getCurrentRequest2().getPathInfo();
|
||||
if (path == null || path.equals("/")) {
|
||||
// we are in the root page so there is nothing current
|
||||
return false;
|
||||
}
|
||||
|
||||
String actionPath = action.getUrlName();
|
||||
if (actionPath == null) {
|
||||
// we are not a primary action and can not ever be current when we have no URL
|
||||
return false;
|
||||
}
|
||||
|
||||
// RootActions are not expected to start with a `/` but some do and some do, and not all Actions will be RootActions
|
||||
// but path will always start with a "/"
|
||||
if (!actionPath.startsWith("/")) {
|
||||
actionPath = '/' + actionPath;
|
||||
}
|
||||
|
||||
// the action /foo should not match /foobar but should match /foo/bar
|
||||
if (!actionPath.endsWith("/")) {
|
||||
actionPath = actionPath + '/';
|
||||
}
|
||||
if (!path.endsWith("/")) {
|
||||
path = path + '/';
|
||||
}
|
||||
return path.startsWith(actionPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated From JEXL expressions ({@code ${…}}) in {@code *.jelly} files
|
||||
* you can use {@code [obj]} syntax to construct an {@code Object[]}
|
||||
|
||||
@@ -3,8 +3,10 @@ package jenkins.views;
|
||||
import hudson.ExtensionComponent;
|
||||
import hudson.ExtensionList;
|
||||
import hudson.ExtensionPoint;
|
||||
import hudson.Functions;
|
||||
import hudson.model.Action;
|
||||
import hudson.model.RootAction;
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -82,10 +84,24 @@ public abstract class Header implements ExtensionPoint {
|
||||
return Jenkins.get()
|
||||
.getActions()
|
||||
.stream()
|
||||
.filter(e -> e.getIconFileName() != null || (e instanceof IconSpec is && is.getIconClassName() != null))
|
||||
.filter(e -> e.getIconFileName() != null || (e instanceof IconSpec is && is.getIconClassName() != null) || hasLegacyView(e))
|
||||
.sorted(Comparator.comparingDouble(
|
||||
a -> rootActionsOrdinal.getOrDefault(a.getClass().getName(), Double.MAX_VALUE)
|
||||
).reversed())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Jenkins will show actions with a custom action.jelly even if its getIconFileName returned {@code null}.
|
||||
* @param action the action to check if it has a custom view.
|
||||
* @return {@code true} iff the action has an {@code action.jelly}
|
||||
*/
|
||||
private static boolean hasLegacyView(Action action) {
|
||||
try {
|
||||
return Functions.hasView(action, "action");
|
||||
} catch (IOException ignored) {
|
||||
// can not load the view so ignore for the header
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<!-- 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="isPrimary" value="${h.showInPrimaryHeader(action)}"/>
|
||||
|
||||
<j:if test="${isCurrent or isPrimary}">
|
||||
<!-- do not show the user avatar -->
|
||||
@@ -23,7 +23,7 @@
|
||||
<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:set var="isPrimary" value="${h.showInPrimaryHeader(action)}"/>
|
||||
<j:if test="${!isCurrent and !isPrimary}">
|
||||
<h:secondaryAction action="${action}"/>
|
||||
</j:if>
|
||||
|
||||
@@ -14,59 +14,73 @@
|
||||
|
||||
</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:choose>
|
||||
<j:when test="${h.hasView(action, 'action')}">
|
||||
<j:scope>
|
||||
<!-- Root actions expect to be in the context of Jenkins (as it) -->
|
||||
<j:set var="it" value="${app}"/>
|
||||
<j:set var="isInPrimaryHeader" value="true"/>
|
||||
<st:include page="action.jelly" from="${action}" optional="true"/>
|
||||
</j:scope>
|
||||
</j:when>
|
||||
<j:otherwise>
|
||||
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
|
||||
<j:if test="${icon != null}">
|
||||
<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: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}" />
|
||||
<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>
|
||||
</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: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>
|
||||
<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:otherwise>
|
||||
</j:choose>
|
||||
|
||||
</j:jelly>
|
||||
@@ -10,13 +10,27 @@
|
||||
</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:choose>
|
||||
<j:when test="${h.hasView(action, 'action')}">
|
||||
<j:scope>
|
||||
<!-- Root actions expect to be in the context of Jenkins (as it) -->
|
||||
<j:set var="it" value="${app}"/>
|
||||
<j:set var="isInSecondaryHeader" value="true"/>
|
||||
<st:include page="action.jelly" from="${action}" optional="true"/>
|
||||
</j:scope>
|
||||
</j:when>
|
||||
<j:otherwise>
|
||||
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
|
||||
<j:if test="${icon != null}">
|
||||
<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:if>
|
||||
</j:otherwise>
|
||||
</j:choose>
|
||||
</j:jelly>
|
||||
@@ -23,7 +23,7 @@ THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<?jelly escape-by-default='true'?>
|
||||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout">
|
||||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:x="jelly:xml" xmlns:dd="/lib/layout/dropdowns">
|
||||
<st:documentation> <![CDATA[
|
||||
This tag inside <l:tasks> tag renders the left navigation bar of Jenkins.
|
||||
Each <task> tag gets an icon and link.
|
||||
@@ -133,9 +133,11 @@ THE SOFTWARE.
|
||||
</j:when>
|
||||
<j:set var="match" value="${isCurrent or hasMatchingChild}" />
|
||||
|
||||
<!--
|
||||
<j:if test="${attrs.permission != null}">
|
||||
<j:set var="hasPermission" value="${h.hasPermission(it, attrs.permission)}" />
|
||||
</j:if>
|
||||
-->
|
||||
|
||||
<!--
|
||||
The icon attribute can be an icon class spec, or can be the url of an icon image.
|
||||
@@ -147,6 +149,116 @@ THE SOFTWARE.
|
||||
icon class spec from the icon metadata, otherwise we create an icon element instance using
|
||||
icon attribute value as an icon "src" (Vs class spec).
|
||||
-->
|
||||
|
||||
<j:when test="${isInPrimaryHeader}">
|
||||
<j:scope>
|
||||
|
||||
<!-- we are rendering inside the modern header this is for compatibility, so we do not need to worry about jumplists which was introduced in the new header -->
|
||||
<j:if test="${enabled != false}"> <!-- no rendering at all for disabled tasks in the header -->
|
||||
|
||||
<!-- there was no custom action and we have no icon so this is intended to be hidden -->
|
||||
<j:if test="${icon != null}">
|
||||
|
||||
<j:set var="isCurrent" value="${h.hyperlinkMatchesCurrentPage(action.urlName)}" />
|
||||
|
||||
<j:set var="jumplist">
|
||||
<j:if test="${match}">
|
||||
<j:scope>
|
||||
<j:set var="isInPrimaryHeader" value="false" />
|
||||
<d:invokeBody />
|
||||
</j:scope>
|
||||
</j:if>
|
||||
</j:set>
|
||||
|
||||
<j:if test="${jumplist.length() == 0}">
|
||||
<j:set var="tooltip">
|
||||
<div style="text-align: center;">${title}</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}" />
|
||||
|
||||
<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>
|
||||
<j:if test="${requiresConfirmation}">
|
||||
<x:attribute name="href">#</x:attribute>
|
||||
<x:attribute name="data-url">${href}</x:attribute>
|
||||
<x:attribute name="data-post">${post}</x:attribute>
|
||||
<x:attribute name="data-message">${confirmationMessage}</x:attribute>
|
||||
<x:attribute name="data-title">${title}</x:attribute>
|
||||
<x:attribute name="data-destructive">${destructive}</x:attribute>
|
||||
</j:if>
|
||||
<j:if test="${!requiresConfirmation}">
|
||||
<x:attribute name="href">${href}</x:attribute>
|
||||
</j:if>
|
||||
<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'} ${requiresConfirmation ? 'confirmation-link' : ''}</x:attribute>
|
||||
<l:icon src="${icon}" class="''"/>
|
||||
<span class="jenkins-visually-hidden" data-type="action-label">${title}</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">
|
||||
<!-- add the action title back into the dropdown -->
|
||||
<dd:header text="${title}"/>
|
||||
<!-- TODO we are missing some styling the icon and text have no space inbetween -->
|
||||
<dd:custom>
|
||||
<div class="tasks">
|
||||
<j:out value="${jumplist}" />
|
||||
</div>
|
||||
</dd:custom>
|
||||
</div>
|
||||
</template>
|
||||
</j:if>
|
||||
</j:if>
|
||||
</j:if>
|
||||
</j:scope>
|
||||
</j:when>
|
||||
<j:when test="${isInSecondaryHeader}">
|
||||
<!-- no extra menus...just some small stuff.... -->
|
||||
<j:choose>
|
||||
<j:when test="${requiresConfirmation}">
|
||||
<dd:custom>
|
||||
<l:confirmationLink class="jenkins-dropdown__item" href="${href}" post="${post}" message="${confirmationMessage ?: ''}" title="${title}">
|
||||
<div class="jenkins-dropdown__item__icon">
|
||||
<l:icon src="${icon}" />
|
||||
</div>
|
||||
${title}
|
||||
<l:badge badge="${attrs.badge}" class="jenkins-dropdown__item__badge"/>
|
||||
</l:confirmationLink>
|
||||
</dd:custom>
|
||||
</j:when>
|
||||
<j:otherwise>
|
||||
<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="${href}"
|
||||
clazz="${badgeClazz}">
|
||||
</dd:item>
|
||||
</j:otherwise>
|
||||
</j:choose>
|
||||
</j:when>
|
||||
<!-- regular sidebar not the new header -->
|
||||
<j:when test="${enabled==false}">
|
||||
<div class="task disabled">
|
||||
<span class="task-link-wrapper">
|
||||
|
||||
@@ -31,12 +31,14 @@ import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import hudson.console.ConsoleAnnotatorFactory.RootAction;
|
||||
import hudson.model.Action;
|
||||
import hudson.model.Computer;
|
||||
import hudson.model.Item;
|
||||
@@ -58,6 +60,9 @@ import java.util.regex.Pattern;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.kohsuke.stapler.Ancestor;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
@@ -309,6 +314,13 @@ class FunctionsTest {
|
||||
return action;
|
||||
}
|
||||
|
||||
private static RootAction createMockRootAction(String uri, boolean primary) {
|
||||
RootAction action = mock(RootAction.class);
|
||||
when(action.getUrlName()).thenReturn(uri);
|
||||
when(action.isPrimaryAction()).thenReturn(primary);
|
||||
return action;
|
||||
}
|
||||
|
||||
private static StaplerRequest2 createMockRequest(String contextPath) {
|
||||
StaplerRequest2 req = mock(StaplerRequest2.class);
|
||||
when(req.getContextPath()).thenReturn(contextPath);
|
||||
@@ -741,4 +753,47 @@ class FunctionsTest {
|
||||
assertEquals("http://acme.com/icon.svg", Functions.guessIcon("http://acme.com/icon.svg", "/jenkins"));
|
||||
assertEquals("https://acme.com/icon.svg", Functions.guessIcon("https://acme.com/icon.svg", "/jenkins"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"/myAction", "/myAction/", "/myAction/subPath"})
|
||||
void showInPrimaryHeader_shouldReturnTrueForCurrentAction(String path) {
|
||||
StaplerRequest2 req = mock(StaplerRequest2.class);
|
||||
when(req.getPathInfo()).thenReturn(path);
|
||||
|
||||
Action action = createMockAction("myAction");
|
||||
try (MockedStatic<Stapler> mocked = mockStatic(Stapler.class)) {
|
||||
mocked.when(Stapler::getCurrentRequest2).thenReturn(req);
|
||||
assertTrue(Functions.showInPrimaryHeader(action));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
@ValueSource(strings = {"/myAction", "/myAction/", "/myAction/subPath"})
|
||||
void showInPrimaryHeader_shouldReturnTrueForPrimaryActions(String path) {
|
||||
StaplerRequest2 req = mock(StaplerRequest2.class);
|
||||
when(req.getPathInfo()).thenReturn(path);
|
||||
|
||||
RootAction action = createMockRootAction("anything", true);
|
||||
|
||||
try (MockedStatic<Stapler> mocked = mockStatic(Stapler.class)) {
|
||||
mocked.when(Stapler::getCurrentRequest2).thenReturn(req);
|
||||
assertTrue(Functions.showInPrimaryHeader(action));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
@ValueSource(strings = {"/myAction", "/myAction/", "/myAction/subPath"})
|
||||
void showInPrimaryHeader_shouldReturnFalseForNonPrimaryNonCurrentAction(String path) {
|
||||
StaplerRequest2 req = mock(StaplerRequest2.class);
|
||||
when(req.getPathInfo()).thenReturn(path);
|
||||
|
||||
Action action = createMockRootAction("myAct", false);
|
||||
try (MockedStatic<Stapler> mocked = mockStatic(Stapler.class)) {
|
||||
mocked.when(Stapler::getCurrentRequest2).thenReturn(req);
|
||||
assertFalse(Functions.showInPrimaryHeader(action));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
$background-outset: 0.7rem;
|
||||
|
||||
#tasks,
|
||||
.tasks,
|
||||
.subtasks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -44,68 +45,71 @@ $background-outset: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
#tasks .task {
|
||||
margin: 0 calc($background-outset * -1);
|
||||
}
|
||||
#tasks,
|
||||
.tasks {
|
||||
.task {
|
||||
margin: 0 calc($background-outset * -1);
|
||||
|
||||
#tasks .task .task-link {
|
||||
@include mixins.item;
|
||||
.task-link {
|
||||
@include mixins.item;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0.5rem $background-outset;
|
||||
gap: 0.65rem;
|
||||
width: 100%;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
transition: opacity var(--standard-transition);
|
||||
|
||||
.task-icon-link {
|
||||
display: inline-flex;
|
||||
|
||||
svg,
|
||||
img {
|
||||
width: 1.375rem !important;
|
||||
height: 1.375rem !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0.5rem $background-outset;
|
||||
gap: 0.65rem;
|
||||
width: 100%;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
transition: opacity var(--standard-transition);
|
||||
|
||||
* {
|
||||
transition: stroke-width var(--standard-transition);
|
||||
.task-icon-link {
|
||||
display: inline-flex;
|
||||
|
||||
svg,
|
||||
img {
|
||||
width: 1.375rem !important;
|
||||
height: 1.375rem !important;
|
||||
color: var(--text-color);
|
||||
|
||||
* {
|
||||
transition: stroke-width var(--standard-transition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-icon-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.task-link-text {
|
||||
display: contents;
|
||||
word-break: word-break;
|
||||
}
|
||||
|
||||
&--active {
|
||||
font-weight: 450;
|
||||
cursor: default;
|
||||
|
||||
svg * {
|
||||
stroke-width: 35px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: var(--item-background--active) !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:hover, &:active, &:focus, &--active) {
|
||||
&::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-icon-badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.task-link-text {
|
||||
display: contents;
|
||||
word-break: word-break;
|
||||
}
|
||||
|
||||
&--active {
|
||||
font-weight: 450;
|
||||
cursor: default;
|
||||
|
||||
svg * {
|
||||
stroke-width: 35px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: var(--item-background--active) !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:hover, &:active, &:focus, &--active) {
|
||||
&::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user