Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:nested>
<f:repeatableHeteroProperty field="parameterDefinitions" hasHeader="true" addCaption="${%Add Parameter}"/>
<f:repeatableHeteroProperty field="parameterDefinitions" hasHeader="true" addCaption="${%Add Parameter}" enableTopButton="true"/>
</f:nested>
</j:jelly>
20 changes: 18 additions & 2 deletions core/src/main/resources/lib/form/hetero-list.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ THE SOFTWARE.
If true, insert new addition by default to their 'desired' location, which
is the order induced by the descriptors.
</st:attribute>
<st:attribute name="enableTopButton">
true if a new Add button, for inserting a new item, should be displayed above the existing items
Display of top button depends also on number of items. If there is no item, only one button is displayed. When at least one item is present, there are two
buttons displayed (only when enableTopButton is true). One above and one below.
Top button adds item on top Bottom button adds item on the bottom. When honorOrder is set the item is inserted at its 'desired' location.
@since TODO
</st:attribute>
<st:attribute name="capture">
Config fragments from descriptors are rendered lazily by default, which means
variables seen in the caller aren't visible to them. This attribute allows you
Expand Down Expand Up @@ -111,12 +118,21 @@ THE SOFTWARE.
</d:taglib>

<j:set var="targetType" value="${attrs.targetType?:it.class}"/>
<div class="jenkins-form-item hetero-list-container ${hasHeader?'with-drag-drop':''} ${attrs.oneEach?'one-each':''} ${attrs.honorOrder?'honor-order':''}">
<div class="jenkins-form-item hetero-list-container ${hasHeader?'with-drag-drop':''} ${attrs.oneEach?'one-each':''} ${attrs.honorOrder?'honor-order':''}"
enableTopButton="${attrs.enableTopButton}">
<j:if test="${!readOnlyMode and attrs.enableTopButton}">
<span>
<button type="button" class="jenkins-button hetero-list-add hetero-list-add-top ${empty(attrs.items)?'jenkins-hidden':''}"
menualign="${attrs.menuAlign}" suffix="${attrs.name}"><l:icon src="symbol-add"/>${attrs.addCaption?:'%Add'}
</button>
</span>
</j:if>

<!-- display existing items -->
<j:forEach var="i" items="${attrs.items}"><!-- TODO consider customizedFields: how to distinguish default items from user-configured items? -->
<j:set var="descriptor" value="${i.descriptor}" />
<j:set var="instance" value="${i}" />
<div name="${attrs.name}" class="repeated-chunk" descriptorId="${descriptor.id}">
<div name="${attrs.name}" class="repeated-chunk hetero-list-chunk" descriptorId="${descriptor.id}">
<local:body deleteCaption="${attrs.deleteCaption}" disableDragAndDrop="${attrs.disableDragAndDrop}">
<st:include from="${descriptor}" page="${descriptor.configPage}" optional="true" />
</local:body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ THE SOFTWARE.
If true, insert new addition by default to their 'desired' location, which
is the order induced by the descriptors.
</st:attribute>
<st:attribute name="enableTopButton">
true if a new Add button, for inserting a new item, should be displayed above the existing items
Display of top button depends also on number of items. If there is no item, only one button is displayed. When at least one item is present, there are two
buttons displayed (only when enableTopButton is true). One above and one below.
Top button adds item on top, Bottom button adds item on the bottom. When honorOrder is set the item is inserted at its 'desired' location.
@since TODO
</st:attribute>
<st:attribute name="capture">
Config fragments from descriptors are rendered lazily by default, which means
variables seen in the caller aren't visible to them. This attribute allows you
Expand All @@ -71,7 +78,7 @@ THE SOFTWARE.
addCaption="${attrs.addCaption}" deleteCaption="${attrs.deleteCaption}"
hasHeader="${attrs.hasHeader}" honorOrder="${attrs.honorOrder}"
menuAlign="${attrs.menuAlign}" oneEach="${attrs.oneEach}"
targetType="${attrs.targetType}"
targetType="${attrs.targetType}" enableTopButton="${attrs.enableTopButton}"
capture="${attrs.capture}"
/>
<!-- TODO: there should be a mechanism to pass-through map as parameters -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ THE SOFTWARE.
<f:hetero-list name="builder" hasHeader="true"
descriptors="${h.getBuilderDescriptors(it)}"
items="${it.builders}"
addCaption="${%Add build step}"/>
addCaption="${%Add build step}"
enableTopButton="true"/>
</f:section>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ THE SOFTWARE.
oneEach="true"
menuAlign="bl-tl"
honorOrder="true"
enableTopButton="true"
addCaption="${%Add post-build action}"/>
</f:section>
</j:jelly>
93 changes: 76 additions & 17 deletions src/main/js/components/dropdowns/hetero-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,56 @@ function convertInputsToButtons(e) {
});
}

function updateTopButton(container) {
if (container.getAttribute("enableTopButton") === "true") {
let children = Array.from(container.children).filter(function (n) {
return (
n.classList.contains("repeated-chunk") &&
!n.classList.contains("fade-out")
);
});
const topAddButton = container.querySelector(".hetero-list-add-top");
if (children.length === 0) {
topAddButton.classList.add("jenkins-hidden");
} else {
topAddButton.classList.remove("jenkins-hidden");
}
}
}

function generateButtons() {
behaviorShim.specify(
"BUTTON.repeatable-delete",
"repeatable",
2,
function (n) {
n.addEventListener("click", function () {
n = n.closest(".repeated-chunk");
if (n.classList.contains("hetero-list-chunk")) {
const container = n.closest(".hetero-list-container");
updateTopButton(container);
}
});
},
);

behaviorShim.specify(
"DIV.hetero-list-container",
"hetero-list-new",
-100,
function (e) {
if (isInsideRemovable(e)) {
function (c) {
if (isInsideRemovable(c)) {
return;
}

convertInputsToButtons(e);
let btn = Array.from(e.querySelectorAll("BUTTON.hetero-list-add")).pop();
convertInputsToButtons(c);
const enableTopButton = c.getAttribute("enableTopButton") === "true";
let btn = Array.from(c.querySelectorAll("BUTTON.hetero-list-add")).pop();
let topButton = enableTopButton
? Array.from(c.querySelectorAll("BUTTON.hetero-list-add-top")).shift()
: null;

let prototypes = e.lastElementChild;
let prototypes = c.lastElementChild;
while (!prototypes.classList.contains("prototypes")) {
prototypes = prototypes.previousElementSibling;
}
Expand All @@ -73,11 +109,11 @@ function generateButtons() {
});
}
prototypes.remove();
let withDragDrop = registerSortableDragDrop(e);
let withDragDrop = registerSortableDragDrop(c);

function insert(instance, template) {
function insert(instance, template, addOnTop) {
let nc = document.createElement("div");
nc.className = "repeated-chunk fade-in";
nc.className = "repeated-chunk hetero-list-chunk fade-in";
nc.setAttribute("name", template.name);
nc.setAttribute("descriptorId", template.descriptorId);
nc.innerHTML = template.html;
Expand Down Expand Up @@ -117,7 +153,7 @@ function generateButtons() {
return bestPos;
}

let current = Array.from(e.children).filter(function (e) {
let current = Array.from(c.children).filter(function (e) {
return e.matches("DIV.repeated-chunk");
});

Expand All @@ -140,17 +176,28 @@ function generateButtons() {
return insertionPoint;
}
}
let referenceNode = e.classList.contains("honor-order")
let honorOrder = c.classList.contains("honor-order");
let referenceNode = honorOrder
? findInsertionPoint()
: insertionPoint;
referenceNode.parentNode.insertBefore(nc, referenceNode);

if (addOnTop && !honorOrder && enableTopButton) {
let children = Array.from(c.children).filter(function (n) {
return n.classList.contains("repeated-chunk");
});
c.insertBefore(nc, children[0]);
} else {
referenceNode.parentNode.insertBefore(nc, referenceNode);
}

// Initialize drag & drop for this component
if (withDragDrop) {
registerSortableDragDrop(nc);
}
Behaviour.applySubtree(nc, true);
ensureVisible(nc);
nc.classList.remove("fade-in");
updateTopButton(c);
layoutUpdateCallback.call();
},
true,
Expand All @@ -159,31 +206,34 @@ function generateButtons() {

function has(id) {
return (
e.querySelector('DIV.repeated-chunk[descriptorId="' + id + '"]') !=
c.querySelector('DIV.repeated-chunk[descriptorId="' + id + '"]') !=
null
);
}

let oneEach = e.classList.contains("one-each");
let oneEach = c.classList.contains("one-each");

/**
* Disable the Add button if there are no more items to add
*/
function toggleButtonState() {
const templateCount = templates.length;
const selectedCount = Array.from(e.children).filter((e) =>
const selectedCount = Array.from(c.children).filter((e) =>
e.classList.contains("repeated-chunk"),
).length;

btn.disabled = oneEach && selectedCount >= templateCount;
if (topButton) {
topButton.disabled = oneEach && selectedCount >= templateCount;
}
}
const observer = new MutationObserver(() => {
toggleButtonState();
});
observer.observe(e, { childList: true });
observer.observe(c, { childList: true });
toggleButtonState();

generateDropDown(btn, (instance) => {
function expand(instance, addOnTop) {
let menuItems = [];
for (let i = 0; i < templates.length; i++) {
let n = templates[i];
Expand All @@ -194,7 +244,7 @@ function generateButtons() {
onClick: (event) => {
event.preventDefault();
event.stopPropagation();
insert(instance, n);
insert(instance, n, addOnTop);
},
type: type,
};
Expand All @@ -205,7 +255,16 @@ function generateButtons() {
menuContainer.appendChild(createFilter(menu));
menuContainer.appendChild(menu);
instance.setContent(menuContainer);
}

generateDropDown(btn, (instance) => {
expand(instance, false);
});
if (topButton) {
generateDropDown(topButton, (instance) => {
expand(instance, true);
});
}
},
);
}
Expand Down
1 change: 1 addition & 0 deletions src/main/js/util/symbols.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/main/scss/form/_reorderable-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ div.repeated-chunk {
}
}

.repeatable-add-top {
.repeatable-add-top,
.hetero-list-add-top {
margin-bottom: 1rem;
}

Expand Down
Loading