dynamically load suggestions for related issues

Tue, 11 Mar 2025 17:01:30 +0100

author
Mike Becker <universe@uap-core.de>
date
Tue, 11 Mar 2025 17:01:30 +0100
changeset 362
576bd8ac4d96
parent 361
749d71470b0f
child 363
817905c30514

dynamically load suggestions for related issues

resolves #616

src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-view.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/issue-editor.js file | annotate | diff | comparison | revisions
src/main/webapp/issue-search.js file | annotate | diff | comparison | revisions
--- 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));
 

mercurial