Tue, 11 Mar 2025 17:01:30 +0100
dynamically load suggestions for related issues
resolves #616
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Tue Mar 11 17:01:30 2025 +0100 @@ -88,10 +88,9 @@ fun listIssues(includeDone: Boolean): List<Issue> /** - * Lists issues for the specified [project]. - * This list will NOT include variant data and is intended for simple lookups. + * Lists issue IDs for the specified [project]. */ - fun listIssues(project: Project): List<Issue> + fun listIssueIds(project: Project): List<Int> /** * Search for issue by subject and id using the case-insensitive [query].
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Tue Mar 11 17:01:30 2025 +0100 @@ -748,11 +748,10 @@ } } - override fun listIssues(project: Project): List<Issue> = - withStatement("$issueQuery where i.project = ?") { + override fun listIssueIds(project: Project): List<Int> = + withStatement("select issueid from lpit_issue where project = ?") { setInt(1, project.id) - queryAll { it.extractIssue() } - // do not add variant data here - not needed in this use case! + queryAll { it.getInt(1) } } override fun searchIssues(query: String, project: Project?): List<String> {
--- a/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Tue Mar 11 17:01:30 2025 +0100 @@ -154,7 +154,6 @@ view = IssueDetailView( issue, comments, - dao.listIssues(issue.project), dao.listIssueRelations(issue), dao.listCommitRefs(issue), relationError,
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Tue Mar 11 17:01:30 2025 +0100 @@ -187,8 +187,8 @@ return } - // obtain the list of issues for this project to filter cross-project references - val knownIds = dao.listIssues(path.project).map { it.id } + // obtain the list of issue IDs for this project to filter cross-project references + val knownIds = dao.listIssueIds(path.project) // read the provided commit log and merge only the refs that relate issues from the current project dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) })
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Tue Mar 11 17:01:30 2025 +0100 @@ -119,7 +119,6 @@ class IssueDetailView( val issue: Issue, val comments: List<IssueComment>, - projectIssues: List<Issue>, val currentRelations: List<IssueRelation>, commitRefs: List<CommitRef>, /** @@ -129,7 +128,6 @@ val pathInfos: PathInfos? = null ) : View() { val relationTypes = RelationType.entries - val linkableIssues = projectIssues.filterNot { it.id == issue.id } val commitLinks: List<CommitLink> private val parser: Parser
--- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp Tue Mar 11 17:01:30 2025 +0100 @@ -252,12 +252,8 @@ </select> </td> <td> - <input name="issue" list="linkable-issues" autocomplete="off"> - <datalist id="linkable-issues"> - <c:forEach var="linkableIssue" items="${viewmodel.linkableIssues}"> - <option value="#${linkableIssue.id} - <c:out value="${linkableIssue.subject}"/> (<fmt:message key="issue.category.${linkableIssue.category}" /> | <fmt:message key="issue.status.${linkableIssue.status}" />)"></option> - </c:forEach> - </datalist> + <input id="linkable-issues" data-project="${issue.project.id}" name="issue" list="linkable-issues-list" autocomplete="off"> + <datalist id="linkable-issues-list"></datalist> </td> </tr> <c:forEach var="relation" items="${viewmodel.currentRelations}">
--- a/src/main/webapp/WEB-INF/jsp/site.jsp Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Tue Mar 11 17:01:30 2025 +0100 @@ -31,7 +31,7 @@ <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%-- Version suffix for forcing browsers to update the CSS / JS files --%> -<c:set scope="page" var="versionSuffix" value="20250309"/> +<c:set scope="page" var="versionSuffix" value="20250311"/> <%-- Make the base href easily available at request scope --%> <c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/>
--- a/src/main/webapp/issue-editor.js Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/webapp/issue-editor.js Tue Mar 11 17:01:30 2025 +0100 @@ -56,4 +56,8 @@ } } -window.addEventListener("load", (_) => toggleVariantStatus()); +window.addEventListener("load", () => { + toggleVariantStatus(); + const project = document.getElementById('linkable-issues').dataset.project; + configureSearchBox('linkable-issues', project); +});
--- a/src/main/webapp/issue-search.js Tue Mar 11 14:01:48 2025 +0100 +++ b/src/main/webapp/issue-search.js Tue Mar 11 17:01:30 2025 +0100 @@ -25,21 +25,20 @@ function debounce(func){ let timer; - return () => { + return (...args) => { clearTimeout(timer); - timer = setTimeout(() => { func.apply(this); }, 300); + timer = setTimeout(() => { func.apply(this, args); }, 300); }; } -const issueSearch = debounce(() => issueSearchImpl()) let searchBoxOldContent = ''; -function issueSearchImpl() { - const searchBox = document.getElementById('search-box'); +function issueSearchImpl(elementId, project) { + const searchBox = document.getElementById(elementId); if (searchBoxOldContent !== searchBox.value) { searchBoxOldContent = searchBox.value; const req = new XMLHttpRequest(); req.addEventListener("load", (evt) => { - const dataList = document.getElementById('search-box-list'); + const dataList = document.getElementById(elementId+'-list'); dataList.innerHTML = ''; JSON.parse(evt.target.responseText).forEach(function(item){ const option = document.createElement('option'); @@ -47,27 +46,36 @@ dataList.appendChild(option); }); }); - req.open("GET", baseHref+'issues/search?q='+encodeURIComponent(searchBox.value)); + let url = baseHref+'issues/search?q='+encodeURIComponent(searchBox.value); + if (project > 0) url+='&p='+project; + req.open("GET", url); req.send(); } } +const issueSearch = debounce((elementId, project = 0) => issueSearchImpl(elementId, project)) + +function configureSearchBox(elementId, project, navigateOnEnter = false) { + const searchBox = document.getElementById(elementId); + searchBox.addEventListener('change', () => issueSearch(elementId, project)); + if (navigateOnEnter) { + searchBox.addEventListener('keyup', (evt) => { + if (evt.code === 'Enter' || evt.code === 'NumpadEnter') { + let stext = searchBox.value.trim() + if (stext.length === 0) return; + if (stext[0] === '#') stext = stext.substring(1); + const snum = Number.parseInt(stext.split(' ')[0], 10); + if (snum !== Number.NaN) { + location.assign(baseHref + 'issues/' + snum + '?in_project=true'); + } + } else { + issueSearch(elementId, project); + } + }) + } else { + searchBox.addEventListener('keyup', () => issueSearch(elementId, project)); + } +} -// configure the search box -window.addEventListener('load', () => { - const searchBox = document.getElementById('search-box'); - searchBox.addEventListener('change', () => issueSearch()) - searchBox.addEventListener('keyup', (evt) => { - if (evt.code === 'Enter' || evt.code === 'NumpadEnter') { - let stext = searchBox.value.trim() - if (stext.length === 0) return; - if (stext[0] === '#') stext = stext.substring(1); - const snum = Number.parseInt(stext.split(' ')[0], 10); - if (snum !== Number.NaN) { - location.assign(baseHref+'issues/'+snum+'?in_project=true'); - } - } else { - issueSearch(); - } - }) -}); +// configure the global search box +window.addEventListener('load', () => configureSearchBox('search-box', 0, true));