diff --git a/core/src/main/resources/hudson/model/View/newJob.jelly b/core/src/main/resources/hudson/model/View/newJob.jelly index 98c9d80960..23ff8a8d31 100644 --- a/core/src/main/resources/hudson/model/View/newJob.jelly +++ b/core/src/main/resources/hudson/model/View/newJob.jelly @@ -35,35 +35,48 @@ THE SOFTWARE. + + - ${%NewJob(it.newPronoun)} + + - + ${%ItemName.label} - » ${%ItemName.validation.required} - - » ${%ItemType.validation.required} + - - ${%ItemType.label} - - + + + ${%ItemType.label} + - - - ${%CopyOption.description} - - - ${%CopyOption.label} - - - + + + + + + + + + + ${%CopyOption.label} + + ${%CopyOption.description} + + + + + + + + + - + diff --git a/core/src/main/resources/hudson/model/View/newJob.properties b/core/src/main/resources/hudson/model/View/newJob.properties index c08fa1cdfe..e7b102424e 100644 --- a/core/src/main/resources/hudson/model/View/newJob.properties +++ b/core/src/main/resources/hudson/model/View/newJob.properties @@ -6,7 +6,7 @@ ItemType.label=Select an item type ItemName.validation.required=This field cannot be empty, please enter a valid name ItemType.validation.required=Please select an item type CopyOption.placeholder=Type to autocomplete -CopyOption.description=If you want to create a new item from other existing, you can use this option: -CopyOption.label=Copy from +CopyOption.description=Create a new item by copying the configuration of an existing one. +CopyOption.label=Duplicate an existing item CopyExisting=Copy existing {0} diff --git a/eslint.config.cjs b/eslint.config.cjs index 3ce24b9e19..bdb28f0630 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -77,6 +77,7 @@ module.exports = [ Utilities: "readonly", UTILITIES_VERSION: "writeable", YAHOO: "readonly", + updateValidationArea: "readonly", }, }, }, diff --git a/src/main/js/add-item.js b/src/main/js/add-item.js index b0acfa0ba4..56494a42d2 100644 --- a/src/main/js/add-item.js +++ b/src/main/js/add-item.js @@ -1,5 +1,7 @@ +import { getI18n } from "@/util/i18n"; import { createElementFromHtml } from "@/util/dom"; +const enableHeadings = false; const nameInput = document.querySelector(`#createItem input[name="name"]`); const copyFromInput = document.querySelector(`#createItem input[name="from"]`); const copyRadio = document.querySelector(`#createItem input[value="copy"]`); @@ -60,22 +62,25 @@ document.addEventListener("DOMContentLoaded", () => { } } - function activateValidationMessage(messageId, context, message) { - if (message !== undefined && message !== "") { - document.querySelector(context + " " + messageId).textContent = - "ยป " + message; + /** + * Shows or clears the validation message for the name input. + * + * Only updates the UI after the user has interacted with the input, which is + * indicated by `nameInput.dataset.dirty` being set. + */ + function activateValidationMessage(message) { + if (!nameInput.dataset.dirty) { + return; } - cleanValidationMessages(context); - document - .querySelector(messageId) - .classList.remove("input-message-disabled"); - refreshSubmitButtonState(); - } - function cleanValidationMessages(context) { - document - .querySelectorAll(context + " .input-validation-message") - .forEach((element) => element.classList.add("input-message-disabled")); + updateValidationArea( + document.querySelector(".validation-error-area"), + message !== undefined && message !== "" + ? `${message}` + : ``, + ); + + refreshSubmitButtonState(); } function refreshSubmitButtonState() { @@ -96,15 +101,9 @@ document.addEventListener("DOMContentLoaded", () => { } function cleanItemSelection() { - document - .querySelector('.categories li[role="radio"]') - .setAttribute("aria-checked", "false"); document .querySelector('#createItem input[type="radio"][name="mode"]') .removeAttribute("checked"); - document.querySelectorAll(".categories .active").forEach((item) => { - item.classList.remove("active"); - }); setFieldValidationStatus("items", false); } @@ -120,42 +119,37 @@ document.addEventListener("DOMContentLoaded", () => { // Draw functions function drawCategory(category) { - var $category = createElementFromHtml(""); - $category.setAttribute( - "id", - "j-add-item-type-" + cleanClassName(category.id), + const heading = createElementFromHtml( + "", ); - var $items = createElementFromHtml(``); - var $catHeader = createElementFromHtml(``); - var title = "" + category.name + ""; - var description = "" + category.description + ""; + const title = createElementFromHtml("" + category.name + ""); + const description = createElementFromHtml( + "" + category.description + "", + ); + heading.appendChild(title); + heading.appendChild(description); + const response = []; + + if (enableHeadings) { + response.push(heading); + } - // Add items category.items.forEach((elem) => { - $items.append(drawItem(elem)); + response.push(drawItem(elem)); }); - $catHeader.append(createElementFromHtml(title)); - $catHeader.append(createElementFromHtml(description)); - $category.append($catHeader); - $category.append($items); - - return $category; + return response; } function drawItem(elem) { - var item = document.createElement("li"); - item.tabIndex = 0; - item.className = cleanClassName(elem.class); - item.setAttribute("role", "radio"); - item.setAttribute("aria-checked", "false"); + var item = document.createElement("div"); + item.className = + cleanClassName(elem.class) + " jenkins-choice-list__item"; + + var label = item.appendChild(document.createElement("label")); var iconDiv = drawIcon(elem); - item.appendChild(iconDiv); - var labelContainer = document.createElement("div"); - item.appendChild(labelContainer); - - var label = labelContainer.appendChild(document.createElement("label")); + label.appendChild(iconDiv); var radio = label.appendChild(document.createElement("input")); radio.type = "radio"; @@ -163,36 +157,24 @@ document.addEventListener("DOMContentLoaded", () => { radio.value = elem.class; var displayName = label.appendChild(document.createElement("span")); - displayName.className = "label"; - + displayName.className = "jenkins-choice-list__item__label"; displayName.appendChild(document.createTextNode(elem.displayName)); - var desc = labelContainer.appendChild(document.createElement("div")); - desc.className = "desc"; + var desc = label.appendChild(document.createElement("div")); + desc.className = "jenkins-choice-list__item__description"; desc.innerHTML = checkForLink(elem.description); - function select(e) { - e.preventDefault(); + function select() { cleanCopyFromOption(); cleanItemSelection(); - - item.setAttribute("aria-checked", "true"); - radio.checked = true; - item.classList.add("active"); - setFieldValidationStatus("items", true); + if (getFieldValidationStatus("name")) { refreshSubmitButtonState(); } } - item.addEventListener("click", select); - item.addEventListener("keydown", function (evt) { - if (evt.code === "Space" || evt.code === "Enter") { - this.click(); - evt.stopPropagation(); - } - }); + radio.addEventListener("change", select); return item; } @@ -200,10 +182,10 @@ document.addEventListener("DOMContentLoaded", () => { function drawIcon(elem) { var iconDiv = document.createElement("div"); if (elem.iconXml) { - iconDiv.className = "icon"; + iconDiv.className = "jenkins-choice-list__item__icon"; iconDiv.innerHTML = elem.iconXml; } else if (elem.iconClassName && elem.iconQualifiedUrl) { - iconDiv.className = "icon"; + iconDiv.className = "jenkins-choice-list__item__icon"; var img1 = document.createElement("img"); img1.src = elem.iconQualifiedUrl; @@ -212,7 +194,7 @@ document.addEventListener("DOMContentLoaded", () => { // Example for Freestyle project // } else if (elem.iconFilePathPattern) { - iconDiv.className = "icon"; + iconDiv.className = "jenkins-choice-list__item__icon"; var iconFilePath = jRoot + "/" + elem.iconFilePathPattern.replace(":size", "48x48"); @@ -238,7 +220,7 @@ document.addEventListener("DOMContentLoaded", () => { spanFakeImgB.className = "b"; spanFakeImgB.innerText = b; iconDiv.appendChild(spanFakeImgB); - iconDiv.className = "default-icon"; + iconDiv.className = "jenkins-choice-list__item__icon"; // Example for MockFolder // Mo @@ -250,14 +232,11 @@ document.addEventListener("DOMContentLoaded", () => { document.querySelector("#add-item-panel").removeAttribute("style"); // Render all categories - var $categories = document.querySelector("div.categories"); + var $categories = document.querySelector(".categories"); data.categories.forEach((elem) => { - $categories.append(drawCategory(elem)); + drawCategory(elem).forEach((e) => $categories.append(e)); }); - // Focus - document.querySelector("#add-item-panel #name").focus(); - // Init NameField function nameFieldEvent() { if (!isItemNameEmpty()) { @@ -268,13 +247,11 @@ document.addEventListener("DOMContentLoaded", () => { response.text().then((data) => { var message = parseResponseFromCheckJobName(data); if (message !== "") { - activateValidationMessage( - "#itemname-invalid", - ".add-item-name", - message, - ); + activateValidationMessage(message); + setFieldValidationStatus("name", false); + refreshSubmitButtonState(); } else { - cleanValidationMessages(".add-item-name"); + activateValidationMessage(""); setFieldValidationStatus("name", true); refreshSubmitButtonState(); } @@ -283,14 +260,16 @@ document.addEventListener("DOMContentLoaded", () => { ); } else { setFieldValidationStatus("name", false); - cleanValidationMessages(".add-item-name"); - activateValidationMessage("#itemname-required", ".add-item-name"); + activateValidationMessage(getI18n("empty-name")); refreshSubmitButtonState(); } } nameInput.addEventListener("blur", nameFieldEvent); - nameInput.addEventListener("input", nameFieldEvent); + nameInput.addEventListener("input", () => { + nameInput.dataset.dirty = "true"; + nameFieldEvent(); + }); // Init CopyFromField function copyFromFieldEvent() { @@ -300,53 +279,23 @@ document.addEventListener("DOMContentLoaded", () => { cleanItemSelection(); copyRadio.setAttribute("checked", true); setFieldValidationStatus("from", true); - if (!getFieldValidationStatus("name")) { - activateValidationMessage("#itemname-required", ".add-item-name"); - setTimeout(function () { - var parentName = copyFromInput.value; - - fetch("job/" + parentName + "/api/json?tree=name").then( - (response) => { - response.json().then((data) => { - if (data.name === parentName) { - //if "name" is invalid, but "from" is a valid job, then switch focus to "name" - nameInput.focus(); - } - }); - }, - ); - }, 400); - } else { - refreshSubmitButtonState(); - } + refreshSubmitButtonState(); } } copyFromInput?.addEventListener("blur", copyFromFieldEvent); copyFromInput?.addEventListener("input", copyFromFieldEvent); - // Client-side validation - document - .querySelector("#createItem") - .addEventListener("submit", function (event) { - if (!getFormValidationStatus()) { - event.preventDefault(); - if (!getFieldValidationStatus("name")) { - activateValidationMessage("#itemname-required", ".add-item-name"); - nameInput.focus(); - } else { - if ( - !getFieldValidationStatus("items") && - !getFieldValidationStatus("from") - ) { - activateValidationMessage("#itemtype-required", ".add-item-name"); - nameInput.focus(); - } - } - } - }); + // Focus the Name input on load + document.querySelector("#add-item-panel #name").focus(); - // Disable the submit button + // Disable the Submit button on load refreshSubmitButtonState(); }); + + if (copyRadio !== null) { + copyRadio.addEventListener("change", () => { + copyFromInput.focus(); + }); + } }); diff --git a/src/main/js/add-item.scss b/src/main/js/add-item.scss index e033c98a5d..9a35ff9974 100644 --- a/src/main/js/add-item.scss +++ b/src/main/js/add-item.scss @@ -1,4 +1,19 @@ -/* - * Widget styles - */ -@use "widgets/add/addform"; +#add-item-panel { + max-width: 750px; + margin: auto; + + .jenkins-choice-list__item { + // Default icons' text placeholder don't appear unless z-index is set + .jenkins-choice-list__item__icon { + span { + z-index: 1; + } + } + + // Make 'Duplicate an existing item' input not full width + input[type="text"] { + width: 100%; + max-width: 400px; + } + } +} diff --git a/src/main/js/widgets/add/addform.scss b/src/main/js/widgets/add/addform.scss deleted file mode 100644 index 686e9a90ba..0000000000 --- a/src/main/js/widgets/add/addform.scss +++ /dev/null @@ -1,172 +0,0 @@ -@use "../../../scss/abstracts/mixins"; -#add-item-panel { - max-width: 850px; - margin: auto; - - form { - display: flex; - flex-direction: column; - gap: var(--section-padding); - - #bottom-sticker { - margin-top: calc(var(--section-padding) * -1); - } - } - - .item-copy { - position: relative; - - .add-item-copy { - position: relative; - - input[type="radio"] { - display: none; - } - - label { - line-height: 48px; - padding: 0 10px 0 0; - } - - input { - font-size: inherit; - } - } - } - - .add-item-name, - .add-item-copy { - .input-validation-message { - color: var(--error-color); - padding-top: 2px; - font-weight: var(--font-bold-weight); - } - - .input-message-disabled { - display: none; - } - } - - .icon { - display: flex; - align-items: center; - justify-content: center; - height: 2.5rem; - width: 2.5rem; - - img { - position: relative; - width: 100%; - height: 100%; - } - } - - .default-icon { - height: 40px; - width: 40px; - border: 1px solid var(--text-color); - border-radius: 50%; - text-align: center; - line-height: 36px; - font-weight: bold; - font-size: 125%; - color: var(--text-color); - opacity: 0.75; - - .b { - text-transform: lowercase; - } - } - - input { - width: 50%; - min-width: 300px; - } - - .categories { - .category { - .header { - padding: 0 20px; - - h2 { - margin: 0; - padding: 15px 0 0; - } - - p { - margin-top: 5px; - } - } - - .desc { - color: var(--text-color-secondary); - } - } - } - - .categories.flat { - .category { - border-bottom: none; - - .header { - display: none; - } - } - } -} - -.j-item-options li, -.j-item-options { - display: block; - margin: 0; - padding: 0; - list-style: none; - position: relative; -} - -.j-item-options { - display: flex; - flex-direction: column; - gap: 5px; - - li.active { - &::before { - background: var(--item-background--hover); - } - } - - li { - @include mixins.item($border: false); - - -webkit-touch-callout: none; - user-select: none; - padding: 0.75rem 1rem; - min-height: 68px; - display: grid; - grid-template-columns: auto 1fr; - gap: 0.25rem 1rem; - - .icon { - grid-row: span 2; - } - - label { - display: block; - font-size: var(--font-size-sm); - font-weight: var(--font-bold-weight); - color: var(--text-color); - cursor: pointer; - - input { - position: absolute; - top: 24px; - left: 25px; - display: none; - } - } - - .desc { - grid-column: 2; - } - } -} diff --git a/src/main/scss/components/_choice-list.scss b/src/main/scss/components/_choice-list.scss new file mode 100644 index 0000000000..bb8286a947 --- /dev/null +++ b/src/main/scss/components/_choice-list.scss @@ -0,0 +1,227 @@ +.jenkins-choice-list { + display: flex; + flex-direction: column; + padding: 0; + margin: 0; + border-radius: var(--form-input-border-radius); + border: var(--card-border-width) solid var(--card-border-color); + background: var(--card-background); + + &__heading { + font-size: 1rem; + padding: 0.875rem; + border-bottom: var(--card-border-width) solid var(--card-border-color); + + & > * { + margin: 0; + } + + h2 { + font-weight: var(--font-bold-weight); + font-size: var(--font-size-sm); + margin-bottom: 0.5rem; + } + + p { + color: var(--text-color-secondary); + } + } + + &__item { + -webkit-touch-callout: none; + position: relative; + user-select: none; + display: flex; + align-items: stretch; + flex-direction: column; + white-space: unset; + border-radius: 0; + cursor: pointer; + + &:not(:last-of-type) { + border-bottom: var(--card-border-width) solid var(--card-border-color); + } + + &::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + transition: var(--standard-transition); + background: transparent; + box-shadow: + 0 0 0 0.09375rem transparent, + 0 0 0 0.5rem transparent; + pointer-events: none; + } + + &:hover { + &::before { + background: color-mix( + in sRGB, + var(--text-color-secondary) 7.5%, + transparent + ); + } + } + + &:active, + &:has(input[type="radio"]:focus-visible) { + z-index: 10; + border-radius: calc(var(--form-input-border-radius) / 2); + + &::before { + background: color-mix( + in sRGB, + var(--text-color-secondary) 12.5%, + transparent + ) !important; + box-shadow: + 0 0 0 0.09375rem + color-mix( + in sRGB, + var(--text-color-secondary) 20%, + var(--card-background) + ), + 0 0 0 0.34375rem + color-mix( + in sRGB, + color-mix( + in sRGB, + var(--text-color-secondary) 15%, + var(--card-background) + ) + 65%, + transparent + ); + } + } + + &:hover, + &:active, + &:has(input[type="radio"]:focus-visible) { + .jenkins-choice-list__item__icon::before { + background: rgb(from var(--text-color-secondary) r g b / 0.1); + box-shadow: + inset var(--jenkins-border--subtle-shadow), + inset 0 0 0 var(--jenkins-border-width) + rgb(from var(--text-color-secondary) r g b / 0.1); + } + } + + &:has(input[type="radio"]:focus-visible) { + &::before { + box-shadow: 0 0 0 0.2rem var(--text-color) !important; + } + } + + &:has(input:checked) { + &::before { + background: color-mix( + in sRGB, + var(--text-color-secondary) 5%, + transparent + ); + } + + .jenkins-choice-list__item__icon { + color: var(--background); + + &::before { + box-shadow: + inset var(--jenkins-border--subtle-shadow), + inset 0 0 18px 18px var(--accent-color) !important; + } + } + + .jenkins-choice-list__item__conditional { + display: block; + } + } + + &:first-of-type { + border-top-left-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + border-top-right-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + } + + &:last-of-type { + border-bottom-left-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + border-bottom-right-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + } + + label { + display: grid; + gap: 0.25rem 0.875rem; + padding: 0.875rem; + grid-template-columns: auto 1fr; + cursor: pointer; + } + + label > input[type="radio"] { + position: absolute; + opacity: 0; + } + + &__icon { + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 2.25rem; + width: 2.25rem; + grid-row: span 2; + z-index: 10; + transition: var(--standard-transition); + + &::before { + content: ""; + position: absolute; + inset: 0; + border-radius: var(--form-input-border-radius); + transition: var(--standard-transition); + z-index: 0; + background: rgb(from var(--text-color-secondary) r g b / 0.05); + box-shadow: + inset 0 0 0 var(--jenkins-border-width) var(--jenkins-border-color), + inset 0 0 0 var(--jenkins-border-width) + rgb(from var(--text-color-secondary) r g b / 0.05); + } + + svg, + img { + position: relative; + width: 1.25rem; + height: 1.25rem; + z-index: 1; + } + } + + &__label { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-bold-weight); + color: var(--text-color); + } + + &__description { + color: var(--text-color-secondary); + grid-column: 2; + padding-right: 2rem; + } + + &__conditional { + display: none; + margin-left: 4rem; + margin-top: -0.25rem; + margin-bottom: 0.875rem; + } + } +} diff --git a/src/main/scss/components/_index.scss b/src/main/scss/components/_index.scss index 03bd4c03ce..1ee69a8e42 100644 --- a/src/main/scss/components/_index.scss +++ b/src/main/scss/components/_index.scss @@ -5,6 +5,7 @@ @use "breadcrumbs"; @use "buttons"; @use "cards"; +@use "choice-list"; @use "command-palette"; @use "content-blocks"; @use "description"; diff --git a/src/main/scss/form/_input.scss b/src/main/scss/form/_input.scss index fca6dc791e..822ae7d5db 100644 --- a/src/main/scss/form/_input.scss +++ b/src/main/scss/form/_input.scss @@ -7,6 +7,7 @@ width: 100%; min-height: 2.375rem; box-shadow: var(--form-input-glow); + caret-color: var(--focus-input-border); // Set height transition to 0s as vertical resizing has a delay/lag otherwise transition: diff --git a/src/main/scss/form/_layout.scss b/src/main/scss/form/_layout.scss index 8decaa5456..4a972a5e3d 100644 --- a/src/main/scss/form/_layout.scss +++ b/src/main/scss/form/_layout.scss @@ -50,6 +50,22 @@ &--medium { max-width: var(--form-item-max-width--medium); } + + &:has(> .validation-error-area .warning) { + --input-border: var(--warning-color); + --input-border-hover: var(--warning-color); + --focus-input-border: var(--warning-color); + --form-input-glow--focus: 0 0 0 0.25rem + color-mix(in sRGB, var(--warning-color) 20%, transparent); + } + + &:has(> .validation-error-area .error) { + --input-border: var(--error-color); + --input-border-hover: var(--error-color); + --focus-input-border: var(--error-color); + --form-input-glow--focus: 0 0 0 0.25rem + color-mix(in sRGB, var(--error-color) 20%, transparent); + } } .jenkins-form-label { diff --git a/test/src/test/java/hudson/model/MyViewTest.java b/test/src/test/java/hudson/model/MyViewTest.java index 890f7a32d6..0ee3c6d5d8 100644 --- a/test/src/test/java/hudson/model/MyViewTest.java +++ b/test/src/test/java/hudson/model/MyViewTest.java @@ -88,7 +88,7 @@ class MyViewTest { form.getInputByName("name").setValue("job"); form.getInputByName("name").blur(); // Select the item clicking on the first item type shown - HtmlElement itemType = newItemPage.getFirstByXPath("//div[@class='category']/ul/li"); + HtmlElement itemType = newItemPage.querySelector(".jenkins-choice-list div label"); itemType.click(); rule.submit(form); Item item = rule.jenkins.getItem("job"); diff --git a/test/src/test/java/hudson/model/ViewTest.java b/test/src/test/java/hudson/model/ViewTest.java index a08f3858ee..fefae640d0 100644 --- a/test/src/test/java/hudson/model/ViewTest.java +++ b/test/src/test/java/hudson/model/ViewTest.java @@ -844,7 +844,8 @@ class ViewTest { HtmlPage page = wc.goTo("view/all/newJob"); - Object result = page.executeJavaScript("Array.from(document.querySelectorAll('.label')).filter(el => el.innerText.indexOf('" + customizableTLID.customDisplayName + "') !== -1)[0].parentElement.parentElement").getJavaScriptResult(); + Object result = page.executeJavaScript("Array.from(document.querySelectorAll('.jenkins-choice-list__item__label'))" + + ".filter(el => el.innerText.indexOf('" + customizableTLID.customDisplayName + "') !== -1)[0].parentElement.parentElement").getJavaScriptResult(); assertThat(result, instanceOf(HTMLElement.class)); HTMLElement resultElement = (HTMLElement) result; assertThat(resultElement.getAttribute("onclick"), nullValue()); @@ -861,7 +862,7 @@ class ViewTest { HtmlPage page = wc.goTo("view/all/newJob"); - Object result = page.executeJavaScript("document.querySelector('.xss-dn .label').innerHTML").getJavaScriptResult(); + Object result = page.executeJavaScript("document.querySelector('.xss-dn .jenkins-choice-list__item__label').innerHTML").getJavaScriptResult(); assertThat(result, instanceOf(String.class)); String resultString = (String) result; assertThat(resultString, not(containsString("<"))); @@ -877,7 +878,7 @@ class ViewTest { HtmlPage page = wc.goTo("view/all/newJob"); - Object result = page.executeJavaScript("document.querySelector('.html-desc .desc strong')").getJavaScriptResult(); + Object result = page.executeJavaScript("document.querySelector('.html-desc .jenkins-choice-list__item__description strong')").getJavaScriptResult(); assertThat(result, instanceOf(HTMLElement.class)); assertThat(((HTMLElement) result).getTagName(), is("STRONG")); } @@ -894,12 +895,12 @@ class ViewTest { HtmlPage page = wc.goTo("view/all/newJob"); - Object resultIconChildrenCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon').children.length").getJavaScriptResult(); + Object resultIconChildrenCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .jenkins-choice-list__item__icon').children.length").getJavaScriptResult(); assertThat(resultIconChildrenCount, instanceOf(Integer.class)); int resultIconChildrenCountInt = (int) resultIconChildrenCount; assertEquals(1, resultIconChildrenCountInt); - Object resultImgAttributesCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon img').attributes.length").getJavaScriptResult(); + Object resultImgAttributesCount = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .jenkins-choice-list__item__icon img').attributes.length").getJavaScriptResult(); assertThat(resultImgAttributesCount, instanceOf(Integer.class)); int resultImgAttributesCountInt = (int) resultImgAttributesCount; assertEquals(1, resultImgAttributesCountInt); @@ -929,7 +930,7 @@ class ViewTest { HtmlPage page = wc.goTo("view/all/newJob"); - Object resultSrc = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon img').src").getJavaScriptResult(); + Object resultSrc = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .jenkins-choice-list__item__icon img').src").getJavaScriptResult(); assertThat(resultSrc, instanceOf(String.class)); String resultSrcString = (String) resultSrc; @@ -944,7 +945,7 @@ class ViewTest { HtmlPage page = wc.goTo("view/all/newJob"); - Object result = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .icon svg')").getJavaScriptResult(); + Object result = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .jenkins-choice-list__item__icon svg')").getJavaScriptResult(); assertThat(result, instanceOf(SVGElement.class)); SVGElement svg = (SVGElement) result; assertThat(svg.getClassName_js(), is("icon-xlg")); @@ -962,7 +963,7 @@ class ViewTest { HtmlPage page = wc.goTo("view/all/newJob"); - Object result = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .default-icon')").getJavaScriptResult(); + Object result = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .jenkins-choice-list__item__icon')").getJavaScriptResult(); assertThat(result, instanceOf(HTMLElement.class)); HTMLElement resultHtml = (HTMLElement) result; HTMLElement spanA = (HTMLElement) resultHtml.getFirstElementChild(); diff --git a/war/src/main/resources/images/symbols/duplicate.svg b/war/src/main/resources/images/symbols/duplicate.svg new file mode 100644 index 0000000000..14d519b91e --- /dev/null +++ b/war/src/main/resources/images/symbols/duplicate.svg @@ -0,0 +1 @@ + \ No newline at end of file
${%CopyOption.description}
" + category.description + "