场景描述
在jira页面中的某一个位置新增一个按钮,点击按钮可以弹出对话框,对话框中的输入框可以模糊搜索用户
实现方案
通过 UI Fragments 功能中的 web-item 的高级功能实现
官方示例链接:Web Item
实现示例
一、通过 REST Endpoints 功能返回一个包含js与css样式的html代码
放入以下代码
| Code Block | ||||||
|---|---|---|---|---|---|---|
| ||||||
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
@BaseScript CustomEndpointDelegate delegate
showDialog { MultivaluedMap queryParams ->
// get a reference to the current page...
// def page = getPage(queryParams)
def dialog = """<section role="dialog" id="sr-dialog" class="aui-layer aui-dialog2 aui-dialog2-medium"
aria-hidden="true" data-aui-remove-on-hide="true">
<header class="aui-dialog2-header">
<h2 class="aui-dialog2-header-main">Some dialog</h2>
<a class="aui-dialog2-header-close">
<span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>
</a>
</header>
<div class="aui-dialog2-content">
<div class="field-group">
<div class="search-select-container" style="position: relative; margin-bottom: 16px;">
<label style="width: 100px;">assignee</label>
<input type="text" class="search-input aui-field-text" placeholder="Search users..." />
<div class="aui-list search-results" style="position: absolute;
top: 100%;
left: 57px;
right: 0;
z-index: 1000;
border: 1px solid #dfe1e6;
border-top: none;
border-radius: 0 0 4px 4px;
background: white;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: none;" id="searchResults"></div>
</div>
</div>
</div>
<footer class="aui-dialog2-footer">
<div class="aui-dialog2-footer-actions">
<button id="dialog-close-button" class="aui-button aui-button-link">Close</button>
</div>
<div class="aui-dialog2-footer-hint">Some hint here if you like</div>
</footer>
<style>
.search-result-item,.search-result-item-no-options{
padding: 6px 4px;
}
.search-result-item-no-options{
color: #999;
user-select: none;
}
.search-result-item:hover {
background: #f7f7f7;
}
</style>
<script>
(function (global) {
'use strict';
var AUI = global.AUI || {};
AUI.SearchSelect = AUI.SearchSelect || {};
// 防抖函数(避免频繁请求)
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* 初始化搜索选择框(每次弹窗加载后调用)
* @param {HTMLElement} container - 包含 .search-input 和 .search-results 的容器
*/
AUI.SearchSelect.init = function (container) {
if (!container) return;
const input = container.querySelector('.search-input');
const resultsContainer = container.querySelector('.search-results');
if (!input || !resultsContainer) {
console.warn('Search elements not found in container');
return;
}
// 防抖搜索(300ms 延迟)
const debouncedSearch = debounce((query) => {
if (!query.trim()) {
resultsContainer.innerHTML = '';
resultsContainer.style.display = 'none';
return;
}
// 每次输入都请求接口,传入 q 参数
fetch("/rest/api/2/user/picker?query=" + encodeURIComponent(query.trim())+"&showAvatar=true")
.then(response => {
if (!response.ok) throw new Error('Network error');
return response.json();
})
.then(users => {
renderResults(resultsContainer, users);
})
.catch(err => {
console.error('Search failed:', err);
resultsContainer.innerHTML = '<div class="aui-list-item search-result-item-no-options">Search failed</div>';
resultsContainer.style.display = 'block';
});
}, 300);
// 绑定输入事件
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// 点击结果项
// resultsContainer.addEventListener('click', (e) => {
// e.stopPropagation()
// if (e.target.classList.contains('search-result-item-inner')) {
// console.log(e.target)
// input.value = e.target.attributes.display.value;
// resultsContainer.style.display = 'none';
// }
// });
// 点击外部关闭下拉(可选)
const handleClickOutside = (e) => {
if (!container.contains(e.target)) {
resultsContainer.style.display = 'none';
}
};
document.addEventListener('click', handleClickOutside);
// 清理函数(可选:用于弹窗关闭时移除监听)
container._cleanup = () => {
document.removeEventListener('click', handleClickOutside);
};
};
function renderResults(container, users) {
container.innerHTML = '';
if (!Array.isArray(users.users) || users.total === 0) {
const item = document.createElement('div');
item.className = 'aui-list-item search-result-item-no-options';
item.textContent = "THERE'S NO OPTIONS";
container.appendChild(item);
}
users.users?.forEach(user => {
const item = document.createElement('div');
item.className = 'aui-list-item search-result-item';
item.innerHTML = '<div class="search-result-item-inner" style="display:flex;"><div style="font-size:0;margin-right: 5px"><img width="24" src="'+user.avatarUrl+'" /></div><div style="line-height:24px">'+user.html+'</div>'; // 假设接口返回 { name: "..." }
item.addEventListener('click', (e) => {
const thisInput = document.querySelector('#sr-dialog .search-input');
const resultsContainer = document.querySelector('#sr-dialog .search-results');
e.stopPropagation()
console.log(thisInput,resultsContainer,e.target)
thisInput.value = user.displayName;
resultsContainer.style.display = 'none';
});
container.appendChild(item);
});
container.style.display = 'block';
}
global.AUI = AUI;
})(window);
var container = document.getElementById("sr-dialog");
AUI.SearchSelect.init(container);
</script>
</section>
"""
Response.ok().type(MediaType.TEXT_HTML).entity(dialog.toString()).build()
} |
二、通过 UI Fragments 添加一个web-item
输入位置与文本后选择按钮类型
输入刚刚创建的rest的链接地址并保存即可








