Skip to content
Permalink
Browse files

[JENKINS-43507] Add support for grouping traits into sections into th…

…e <scm:traits> tag
  • Loading branch information...
stephenc committed May 5, 2017
1 parent 1a76314 commit fd9652e16f383d06ebe16186eddf467192103a13
@@ -102,12 +102,6 @@
<version>1.6</version>
<scope>test</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>org.jenkins-ci.plugins</groupId>-->
<!--<artifactId>git</artifactId>-->
<!--<version>2.5.3</version> &lt;!&ndash; 2.4.x does not work since it lacks https://github.com/jenkinsci/git-plugin/pull/402 &ndash;&gt;-->
<!--<scope>test</scope>-->
<!--</dependency>-->
</dependencies>

<build>
@@ -0,0 +1,132 @@
/*
* The MIT License
*
* Copyright (c) 2017, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.scm.impl.form;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import jenkins.scm.api.FormTagLib;

/**
* An {@link ArrayList} that also has an associated name for use with the {@link FormTagLib#traits()} tag.
*
* @param <E> the type of element.
*/
public class NamedArrayList<E> extends ArrayList<E> {
/**
* The associated name.
*/
@NonNull
private final String name;

/**
* Copy constructor.
*
* @param name the name.
* @param c the collection whose elements are to be placed into this list
*/
public NamedArrayList(@NonNull String name, Collection<? extends E> c) {
super(c);
this.name = name;
}

/**
* Copy constructor.
*
* @param name the name.
* @param elements the initial elements.
*/
public NamedArrayList(@NonNull String name, E... elements) {
super(Arrays.asList(elements));
this.name = name;
}

/**
* Gets the associate name.
*
* @return the associate name.
*/
@NonNull
public final String getName() {
return name;
}

/**
* Helper method that creates a new {@link NamedArrayList} by selecting matching elements from a source list
* and appends the new {@link NamedArrayList} to a list of {@link NamedArrayList}.
*
* @param source the list of candidate elements.
* @param name the name.
* @param selector the (optional) selection criteria (if {@code null} then all candidate elements
* will be added)
* @param removeSelectedFromSource if {@code true} then the matching elements will be removed from the source.
* @param destination the {@link List} of {@link NamedArrayList} to add to (empty selections will
* not be added)
* @param <E> the type of element.
*/
@SuppressWarnings("unchecked")
public static <E> void select(@NonNull List<? extends E> source, @NonNull String name,
@CheckForNull Predicate<? super E> selector, boolean removeSelectedFromSource,
@NonNull List<NamedArrayList<? extends E>> destination) {
NamedArrayList<E> selection = new NamedArrayList<E>(name);
if (selector == null) {
selection.addAll(source);
if (removeSelectedFromSource) {
source.clear();
}
} else {
for (Iterator<? extends E> iterator = source.iterator(); iterator.hasNext(); ) {
E candidate = iterator.next();
if (selector.test(candidate)) {
selection.add(candidate);
if (removeSelectedFromSource) {
iterator.remove();
}
}
}
}
if (!selection.isEmpty()) {
destination.add(selection);
}
}

/**
* Represents a predicate (boolean-valued function) of one argument.
*
* @param <T> the type of the input to the predicate
*/
// TODO replace with java.util.function.Predicate once baseline is Java 8
//@FunctionalInterface
//@Restricted(NoExternalUse.class)
//@RestrictedSince("...")
//@Deprecated
public interface Predicate<T> { // extends java.util.function.Predicate<T>
boolean test(T t);
}
}
@@ -29,7 +29,9 @@ THE SOFTWARE.
<st:documentation>
Data-bound only version of &lt;f:hetero-list&gt; specialized for traits.
<st:attribute name="field" use="required">
Used for the data binding.
Used for the data binding. Note: if the descriptor implements a getXxxDescriptorLists that returns a List
of jenkins.scm.impl.form.NamedArrayList this will enable the sectioned listing of traits otherwise this
will fall back to looking for a getXxxDescriptors and finally inferrence of the descriptors.
</st:attribute>
<st:attribute name="default">
default configuration to use (if descriptor does not have a getFieldDefaults() method).
@@ -82,46 +84,87 @@ THE SOFTWARE.
</d:taglib>

<st:adjunct includes="lib.form.hetero-list.hetero-list"/>
<st:adjunct includes="jenkins.scm.api.form.traits.traits"/>

<j:set var="targetType" value="${attrs.targetType?:it.class}"/>
<j:set var="descriptors"
value="${descriptor[attrs.field+'Descriptors'] ?: descriptor.getPropertyType(instance,attrs.field).getApplicableItemDescriptors()}"/>
<j:set var="descriptorLists" value="${descriptor[attrs.field+'DescriptorLists']}"/>
<j:if test="${descriptorLists==null}">
<!-- if you don't implement a descriptor.getXxxDescriptorLists then fall back to standard and no sections -->
<j:new var="descriptorLists" className="java.util.ArrayList"/>
<j:invoke on="${descriptorLists}" method="add">
<j:arg value="${descriptor[attrs.field+'Descriptors'] ?: descriptor.getPropertyType(instance,attrs.field).getApplicableItemDescriptors()}"/>
</j:invoke>
</j:if>
<j:set var="renderSections" value="${descriptorLists.size() &gt; 1}"/>
<div class="hetero-list-container with-drag-drop one-each honor-order">
<!-- display existing items -->
<j:forEach var="i"
items="${instance != null ? instance[attrs.field] : (attrs.default ?: descriptor[attrs.field+'Defaults'])}">
<j:set var="descriptor" value="${i.descriptor}"/>
<j:set var="instance" value="${i}"/>
<div name="${attrs.field}" class="repeated-chunk" descriptorId="${descriptor.id}">
<local:body deleteCaption="${attrs.deleteCaption}">
<st:include from="${descriptor}" page="${descriptor.configPage}" optional="true"/>
</local:body>
</div>
<j:set var="instances"
value="${instance != null ? instance[attrs.field] : (attrs.default ?: descriptor[attrs.field+'Defaults'])}"/>
<j:forEach var="descriptors" items="${descriptorLists}" varStatus="descriptorsLoop">
<j:if test="${renderSections}">
<!-- hijack descriptorId to provide permanent placeholders for the section headers
descriptorId is not used in form submission, only to determine sections already present (one-each CSS style)
and to determine the placing of sections with the sequence (honor-order CSS style)
thus we get the section headers in the menu (but disabled) and the entries are added to their respective
sections
-->
<div name="${attrs.field}" class="repeated-chunk trait-section"
descriptorId="!fake@section/[${descriptorsLoop.index}]">
<span class="trait-section-header">
<span>${descriptors.getName()}</span>
</span>
</div>
</j:if>
<j:forEach var="i" items="${instances}">
<j:if test="${descriptors.contains(i.descriptor)}">
<j:set var="descriptor" value="${i.descriptor}"/>
<j:set var="instance" value="${i}"/>
<div name="${attrs.field}" class="repeated-chunk" descriptorId="${descriptor.id}">
<local:body deleteCaption="${attrs.deleteCaption}">
<st:include from="${descriptor}" page="${descriptor.configPage}" optional="true"/>
</local:body>
</div>
</j:if>
</j:forEach>
</j:forEach>

<div class="repeatable-insertion-point"/>

<div class="prototypes to-be-removed">
<!-- render one prototype for each type -->
<j:set var="instance" value="${null}"/>
<j:set var="descriptors" value="${h.filterDescriptors(it,descriptors)}"/>
<j:forEach var="descriptor" items="${descriptors}" varStatus="loop">
<div name="${attrs.field}" title="${descriptor.displayName}" tooltip="${descriptor.tooltip}"
descriptorId="${descriptor.id}">
<j:set var="capture" value="${attrs.capture?:''}"/>
<local:body deleteCaption="${attrs.deleteCaption}">
<l:renderOnDemand tag="tr" clazz="config-page" capture="descriptor,it,instance,${capture}">
<l:ajax>
<st:include from="${descriptor}" page="${descriptor.configPage}" optional="true"/>
</l:ajax>
</l:renderOnDemand>
</local:body>
</div>
<j:forEach var="descriptors" items="${descriptorLists}" varStatus="descriptorsLoop">
<!-- render one prototype for each type -->
<j:set var="instance" value="${null}"/>
<j:set var="filteredDescriptors" value="${h.filterDescriptors(it,descriptors)}"/>
<j:if test="${!filteredDescriptors.isEmpty()}">
<j:if test="${renderSections}">
<div name="${attrs.field}" title="&#8212; ${descriptors.getName()} &#8212;"
descriptorId="!fake@section/[${descriptorsLoop.index}]">
<span class="trait-section-header">
<span>${descriptors.getName()}</span>
</span>
</div>
</j:if>
<j:forEach var="descriptor" items="${filteredDescriptors}" varStatus="loop">
<div name="${attrs.field}" title="${descriptor.displayName}"
tooltip="${descriptor.tooltip?:'Why does nobody use me'}"
descriptorId="${descriptor.id}">
<j:set var="capture" value="${attrs.capture?:''}"/>
<local:body deleteCaption="${attrs.deleteCaption}">
<l:renderOnDemand tag="tr" clazz="config-page" capture="descriptor,it,instance,${capture}">
<l:ajax>
<st:include from="${descriptor}" page="${descriptor.configPage}" optional="true"/>
</l:ajax>
</l:renderOnDemand>
</local:body>
</div>
</j:forEach>
</j:if>
</j:forEach>
</div>

<div>
<input type="button" value="${attrs.addCaption?:'%Add'}" class="hetero-list-add" menualign="${attrs.menuAlign}"
<input type="button" value="${attrs.addCaption?:'%Add'}" class="hetero-list-add trait-add"
menualign="${attrs.menuAlign}"
suffix="${attrs.field}"/>
</div>
</div>
@@ -0,0 +1,39 @@
DIV.repeated-chunk.trait-section {
display: block;
text-align: center;
margin: 0;
padding: 0.1em;
}

SPAN.trait-section-header {
position: relative;
overflow: hidden;
font-style: italic;
}

SPAN.trait-section-header SPAN {
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
position: relative;
padding: 0 1em;
}

SPAN.trait-section-header SPAN:before, SPAN.trait-section-header SPAN:after {
content: '';
display: block;
width: 10em;
position: absolute;
top: 0.5em;
border-top: 1px dotted rgba(0, 0, 0, 0.5);
}

SPAN.trait-section-header SPAN:before {
right: 100%;
}

SPAN.trait-section-header SPAN:after {
left: 100%;
}
@@ -172,14 +172,14 @@ public ListBoxModel doFillControllerIdItems() {
return result;
}

public List<SCMTraitDescriptor<?>> getTraitDescriptors() {
public List<SCMTraitDescriptor<?>> getTraitsDescriptors() {
List<SCMTraitDescriptor<?>> descriptors = new ArrayList<SCMTraitDescriptor<?>>();
descriptors.addAll(SCMNavigatorTrait._for(MockSCMNavigatorContext.class, MockSCMSourceBuilder.class));
descriptors.addAll(SCMSourceTrait._for(MockSCMSourceContext.class, MockSCMBuilder.class));
return descriptors;
}

public List<SCMTrait<?>> getTraitDefaults() {
public List<SCMTrait<?>> getTraitsDefaults() {
return Collections.<SCMTrait<?>>singletonList(new MockSCMDiscoverBranches());
}
}
@@ -352,11 +352,11 @@ public ListBoxModel doFillRepositoryItems(@QueryParameter String controllerId) t
};
}

public List<SCMSourceTraitDescriptor> getTraitDescriptors() {
public List<SCMSourceTraitDescriptor> getTraitsDescriptors() {
return SCMSourceTrait._for(this, MockSCMSourceContext.class, MockSCMBuilder.class);
}

public List<SCMSourceTrait> getTraitDefaults() {
public List<SCMSourceTrait> getTraitsDefaults() {
return Collections.<SCMSourceTrait>singletonList(new MockSCMDiscoverBranches());
}
}

0 comments on commit fd9652e

Please sign in to comment.
You can’t perform that action at this time.