#15 add issue filters

Sun, 08 Jan 2023 17:07:26 +0100

author
Mike Becker <universe@uap-core.de>
date
Sun, 08 Jan 2023 17:07:26 +0100
changeset 268
ca5501d851fa
parent 267
d8ec2d8ffa82
child 269
8646c229bd32

#15 add issue filters

setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
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/entities/IssueRelation.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/types/RelationType.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt file | annotate | diff | comparison | revisions
src/main/resources/localization/strings.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/strings_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog-de.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-filter.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-summary.jspf file | annotate | diff | comparison | revisions
src/main/webapp/lightpit.css file | annotate | diff | comparison | revisions
src/main/webapp/project-details.js file | annotate | diff | comparison | revisions
--- a/setup/postgres/psql_create_tables.sql	Tue Jan 03 18:25:51 2023 +0100
+++ b/setup/postgres/psql_create_tables.sql	Sun Jan 08 17:07:26 2023 +0100
@@ -154,6 +154,7 @@
     'TogetherWith',
     'Before',
     'SubtaskOf',
+    'DefectOf',
     'Blocks',
     'Tests',
     'Duplicates'
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sun Jan 08 17:07:26 2023 +0100
@@ -70,8 +70,8 @@
     fun collectIssueSummary(project: Project): IssueSummary
     fun collectIssueSummary(assignee: User): IssueSummary
 
-    fun listIssues(project: Project): List<Issue>
-    fun listIssues(project: Project, version: Version?, component: Component?): List<Issue>
+    fun listIssues(project: Project, includeDone: Boolean): List<Issue>
+    fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List<Issue>
     fun findIssue(id: Int): Issue?
     fun insertIssue(issue: Issue): Int
     fun updateIssue(issue: Issue)
@@ -87,6 +87,7 @@
     fun insertIssueRelation(rel: IssueRelation)
     fun deleteIssueRelation(rel: IssueRelation)
     fun listIssueRelations(issue: Issue): List<IssueRelation>
+    fun getIssueRelationMap(project: Project, includeDone: Boolean): IssueRelationMap
 
     fun insertHistoryEvent(issue: Issue, newId: Int = 0)
     fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int = 0)
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sun Jan 08 17:07:26 2023 +0100
@@ -27,6 +27,7 @@
 
 import de.uapcore.lightpit.entities.*
 import de.uapcore.lightpit.types.IssueHistoryType
+import de.uapcore.lightpit.types.RelationType
 import de.uapcore.lightpit.types.WebColor
 import de.uapcore.lightpit.viewmodel.ComponentSummary
 import de.uapcore.lightpit.viewmodel.IssueSummary
@@ -480,11 +481,12 @@
         select issueid,
             i.project, p.name as projectname, p.node as projectnode,
             component, c.name as componentname, c.node as componentnode,
-            status, category, subject, i.description,
+            status, phase, category, subject, i.description,
             userid, username, givenname, lastname, mail,
             created, updated, eta, affected, resolved
         from lpit_issue i
         join lpit_project p on i.project = projectid
+        join lpit_issue_phases using (status)
         left join lpit_component c on component = c.id
         left join lpit_user on userid = assignee 
         """.trimIndent()
@@ -534,15 +536,17 @@
         return i
     }
 
-    override fun listIssues(project: Project): List<Issue> =
-        withStatement("$issueQuery where i.project = ?") {
+    override fun listIssues(project: Project, includeDone: Boolean): List<Issue> =
+        withStatement("$issueQuery where i.project = ? and (? or phase < 2)") {
             setInt(1, project.id)
+            setBoolean(2, includeDone)
             queryAll { it.extractIssue() }
         }
 
-    override fun listIssues(project: Project, version: Version?, component: Component?): List<Issue> =
+    override fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List<Issue> =
         withStatement(
-            """$issueQuery where i.project = ? and 
+            """$issueQuery where i.project = ? and
+                (? or phase < 2) and
                 (not ? or ? in (resolved, affected)) and (not ? or (resolved is null and affected is null)) and
                 (not ? or component = ?) and (not ? or component is null)
             """.trimIndent()
@@ -559,8 +563,9 @@
                 }
             }
             setInt(1, project.id)
-            applyFilter(version, 2, 4, 3)
-            applyFilter(component, 5, 7, 6)
+            setBoolean(2, includeDone)
+            applyFilter(version, 3, 5, 4)
+            applyFilter(component, 6, 8, 7)
 
             queryAll { it.extractIssue() }
         }
@@ -678,6 +683,21 @@
             queryAll { IssueRelation(issue, findIssue(it.getInt("from_issue"))!!, it.getEnum("type"), true) }
         }.forEach(this::add)
     }
+
+    override fun getIssueRelationMap(project: Project, includeDone: Boolean): IssueRelationMap =
+        withStatement(
+            """
+                select r.from_issue, r.to_issue, r.type
+                from lpit_issue_relation r
+                join lpit_issue i on i.issueid = r.from_issue
+                join lpit_issue_phases p on i.status = p.status
+                where i.project = ? and (? or p.phase < 2)
+                """.trimIndent()
+        ) {
+            setInt(1, project.id)
+            setBoolean(2, includeDone)
+            queryAll { Pair(it.getInt("from_issue"), Pair(it.getInt("to_issue"), it.getEnum<RelationType>("type"))) }
+        }.groupBy({it.first},{it.second})
     //</editor-fold>
 
     //<editor-fold desc="IssueComment">
--- a/src/main/kotlin/de/uapcore/lightpit/entities/IssueRelation.kt	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueRelation.kt	Sun Jan 08 17:07:26 2023 +0100
@@ -34,3 +34,5 @@
     val type: RelationType,
     val reverse: Boolean = false
 )
+
+typealias IssueRelationMap = Map<Int, List<Pair<Int, RelationType>>>
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Sun Jan 08 17:07:26 2023 +0100
@@ -184,7 +184,13 @@
     private fun project(http: HttpRequest, dao: DataAccessObject) {
         withPathInfo(http, dao)?.run {
 
-            val issues = dao.listIssues(project, version, component)
+            val filter = IssueFilter(http)
+
+            val needRelationsMap = filter.onlyBlocker
+
+            val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap()
+
+            val issues = dao.listIssues(project, filter.includeDone, version, component)
                 .sortedWith(
                     IssueSorter(
                         IssueSorter.Criteria(IssueSorter.Field.DONE),
@@ -192,10 +198,16 @@
                         IssueSorter.Criteria(IssueSorter.Field.UPDATED, false)
                     )
                 )
+                .filter {
+                    (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) &&
+                    (!filter.onlyBlocker || (relationsMap[it.id]?.any { (_,type) -> type.blocking }?:false)) &&
+                    (filter.status.isEmpty() || filter.status.contains(it.status)) &&
+                    (filter.category.isEmpty() || filter.category.contains(it.category))
+                }
 
             with(http) {
                 pageTitle = project.name
-                view = ProjectDetails(projectInfo, issues, version, component)
+                view = ProjectDetails(projectInfo, issues, filter, version, component)
                 feedPath = feedPath(project)
                 navigationMenu = activeProjectNavMenu(
                     dao.listProjects(),
@@ -467,7 +479,7 @@
                     project,
                     version,
                     component,
-                    dao.listIssues(project),
+                    dao.listIssues(project, true),
                     dao.listIssueRelations(issue),
                     relationError
                 )
--- a/src/main/kotlin/de/uapcore/lightpit/types/RelationType.kt	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/types/RelationType.kt	Sun Jan 08 17:07:26 2023 +0100
@@ -26,12 +26,13 @@
 
 package de.uapcore.lightpit.types
 
-enum class RelationType(val bidi: Boolean) {
-    RelatesTo(true),
-    TogetherWith(true),
-    Before(false),
-    SubtaskOf(false),
-    Blocks(false),
-    Tests(false),
-    Duplicates(false)
-}
\ No newline at end of file
+enum class RelationType(val bidi: Boolean, val blocking: Boolean) {
+    RelatesTo(true, false),
+    TogetherWith(true, false),
+    Before(false, true),
+    SubtaskOf(false, true),
+    DefectOf(false, true),
+    Blocks(false, true),
+    Tests(false, true),
+    Duplicates(false, false)
+}
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Sun Jan 08 17:07:26 2023 +0100
@@ -31,6 +31,7 @@
 import com.vladsch.flexmark.parser.Parser
 import com.vladsch.flexmark.util.data.MutableDataSet
 import com.vladsch.flexmark.util.data.SharedDataKeys
+import de.uapcore.lightpit.HttpRequest
 import de.uapcore.lightpit.entities.*
 import de.uapcore.lightpit.types.*
 import kotlin.math.roundToInt
@@ -111,8 +112,9 @@
         val options = MutableDataSet()
             .set(SharedDataKeys.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create()))
         parser = Parser.builder(options).build()
-        renderer = HtmlRenderer.builder(options
-            .set(HtmlRenderer.ESCAPE_HTML, true)
+        renderer = HtmlRenderer.builder(
+            options
+                .set(HtmlRenderer.ESCAPE_HTML, true)
         ).build()
 
         issue.description = formatMarkdown(issue.description ?: "")
@@ -164,3 +166,57 @@
     }
 }
 
+class IssueFilter(http: HttpRequest) {
+
+    val issueStatus = IssueStatus.values()
+    val issueCategory = IssueCategory.values()
+    val flagIncludeDone = "f.0"
+    val flagMine = "f.1"
+    val flagBlocker = "f.2"
+
+    val includeDone: Boolean = evalFlag(http, flagIncludeDone)
+    val onlyMine: Boolean = evalFlag(http, flagMine)
+    val onlyBlocker: Boolean = evalFlag(http, flagBlocker)
+    val status: List<IssueStatus> = evalEnum(http, "s")
+    val category: List<IssueCategory> = evalEnum(http, "c")
+
+    private fun evalFlag(http: HttpRequest, name: String): Boolean {
+        val param = http.paramArray("filter")
+        if (param.isNotEmpty()) {
+            if (param.contains(name)) {
+                http.session.setAttribute(name, true)
+            } else {
+                http.session.removeAttribute(name)
+            }
+        }
+        return http.session.getAttribute(name) != null
+    }
+
+    private inline fun <reified T : Enum<T>> evalEnum(http: HttpRequest, prefix: String): List<T> {
+        val sattr = "f.${prefix}"
+        val param = http.paramArray("filter")
+        if (param.isNotEmpty()) {
+            val list = param.filter { it.startsWith("${prefix}.") }
+                .map { it.substring(prefix.length + 1) }
+                .map {
+                    try {
+                        // quick and very dirty validation
+                        enumValueOf<T>(it)
+                    } catch (_: IllegalArgumentException) {
+                        // skip
+                    }
+                }
+            if (list.isEmpty()) {
+                http.session.removeAttribute(sattr)
+            } else {
+                http.session.setAttribute(sattr, list.joinToString(","))
+            }
+        }
+
+        return http.session.getAttribute(sattr)
+            ?.toString()
+            ?.split(",")
+            ?.map { enumValueOf(it) }
+            ?: emptyList()
+    }
+}
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt	Sun Jan 08 17:07:26 2023 +0100
@@ -47,6 +47,7 @@
 class ProjectDetails(
     val projectInfo: ProjectInfo,
     val issues: List<Issue>,
+    val filter: IssueFilter,
     val version: Version? = null,
     val component: Component? = null
 ) : View() {
--- a/src/main/resources/localization/strings.properties	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/resources/localization/strings.properties	Sun Jan 08 17:07:26 2023 +0100
@@ -25,6 +25,7 @@
 app.license.title=License
 app.name=Lightweight Project and Issue Tracking
 button.add=Add
+button.apply=Apply
 button.back=Back
 button.cancel=Cancel
 button.comment.edit=Edit Comment
@@ -85,6 +86,11 @@
 issue.created=Created
 issue.description=Description
 issue.eta=ETA
+issue.filter=Filter
+issue.filter.blocking=show only blocking
+issue.filter.done=show resolved
+issue.filter.mine=only assigned to me
+issue.filter.more=more filters
 issue.id=Issue ID
 issue.relations=Relations
 issue.relations.issue=Issue
@@ -169,4 +175,6 @@
 version.status.Released=Released
 version.status.Unreleased=Unreleased
 version.status=Status
-version=Version
\ No newline at end of file
+version=Version
+issue.relations.type.DefectOf=defect of
+issue.relations.type.DefectOf.rev=defect
\ No newline at end of file
--- a/src/main/resources/localization/strings_de.properties	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/resources/localization/strings_de.properties	Sun Jan 08 17:07:26 2023 +0100
@@ -24,6 +24,7 @@
 app.changelog=Versionshistorie
 app.license.title=Lizenz (Englisch)
 app.name=Lightweight Project and Issue Tracking
+button.apply=Anwenden
 button.add=Hinzuf\u00fcgen
 button.back=Zur\u00fcck
 button.cancel=Abbrechen
@@ -85,6 +86,11 @@
 issue.created=Erstellt
 issue.description=Beschreibung
 issue.eta=Zieldatum
+issue.filter=Filter
+issue.filter.blocking=zeige nur blockierende
+issue.filter.done=zeige erledigte
+issue.filter.mine=nur mir zugewiesene
+issue.filter.more=mehr Filter
 issue.id=Vorgangs-ID
 issue.relations=Beziehungen
 issue.relations.issue=Vorgang
@@ -170,3 +176,5 @@
 version.status.Unreleased=Unver\u00f6ffentlicht
 version.status=Status
 version=Version
+issue.relations.type.DefectOf.rev=Fehler
+issue.relations.type.DefectOf=Fehler von
--- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Sun Jan 08 17:07:26 2023 +0100
@@ -32,6 +32,7 @@
     <li>Mehrfachauswahl für Versionen im Vorgang entfernt.</li>
     <li>RSS Feeds für Projekte hinzugefügt.</li>
     <li>Vorgangsansicht vereinfacht.</li>
+    <li>Filteroptionen hinzugefügt.</li>
     <li>Möglichkeit zum Deaktivieren einer Komponente hinzugefügt.</li>
     <li>Datum der Veröffentlichung und des Supportendes zu Versionen hinzugefügt.</li>
     <li>Gesamtanzahl der Kommentare wird nun angezeigt.</li>
--- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Sun Jan 08 17:07:26 2023 +0100
@@ -32,6 +32,7 @@
     <li>Remove multi selection of versions within an issue.</li>
     <li>Add RSS feeds for projects.</li>
     <li>Simplify issue view.</li>
+    <li>Add filtering options.</li>
     <li>Add possibility to deactivate a component.</li>
     <li>Add release and end of life dates to versions.</li>
     <li>Add the total number of comments to the caption.</li>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sun Jan 08 17:07:26 2023 +0100
@@ -40,6 +40,9 @@
     <button onclick="toggleProjectDetails()" id="toggle-details-button"><fmt:message key="button.project.details"/></button>
 </div>
 
+<h3><fmt:message key="issue.filter" /></h3>
+<%@include file="../jspf/issue-filter.jspf"%>
+
 <h2><fmt:message key="progress" /></h2>
 
 <c:set var="summary" value="${viewmodel.projectInfo.issueSummary}" />
--- a/src/main/webapp/WEB-INF/jsp/site.jsp	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Sun Jan 08 17:07:26 2023 +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="20230103"/>
+<c:set scope="page" var="versionSuffix" value="20230108"/>
 
 <%-- Make the base href easily available at request scope --%>
 <c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jspf/issue-filter.jspf	Sun Jan 08 17:07:26 2023 +0100
@@ -0,0 +1,60 @@
+<%--
+
+--%>
+<form method="GET">
+    <div>
+        <label>
+            <input name="filter"
+                   type="checkbox"
+                   value="${viewmodel.filter.flagIncludeDone}"
+                   <c:if test="${viewmodel.filter.includeDone}">checked</c:if>
+            >
+            <fmt:message key="issue.filter.done"/>
+        </label>
+        <label>
+            <input name="filter"
+                   type="checkbox"
+                   value="${viewmodel.filter.flagMine}"
+                   <c:if test="${viewmodel.filter.onlyMine}">checked</c:if>
+            >
+            <fmt:message key="issue.filter.mine"/>
+        </label>
+        <label>
+            <input name="filter"
+                   type="checkbox"
+                   value="${viewmodel.filter.flagBlocker}"
+                   <c:if test="${viewmodel.filter.onlyBlocker}">checked</c:if>
+            >
+            <fmt:message key="issue.filter.blocking"/>
+        </label>
+        <label>
+            <input id="show-more-filters" type="checkbox" onclick="toggleFilterDetails()">
+            <fmt:message key="issue.filter.more"/>
+        </label>
+    </div>
+    <div id="more-filters" style="display: flex; gap: 1em">
+        <div style="display: inline-block">
+            <label class="caption" style="display:block;" for="filter-category"><fmt:message key="issue.category"/></label>
+            <select id="filter-category" name="filter" multiple size="10">
+                <c:forEach var="category" items="${viewmodel.filter.issueCategory}">
+                    <option value="c.${category}" <c:if test="${viewmodel.filter.category.contains(category) }">selected</c:if> >
+                        <fmt:message key="issue.category.${category}"/>
+                    </option>
+                </c:forEach>
+            </select>
+        </div>
+        <div style="display: inline-block">
+            <label class="caption" style="display:block;" for="filter-status"><fmt:message key="issue.status"/></label>
+            <select id="filter-status" name="filter" multiple size="10">
+                <c:forEach var="status" items="${viewmodel.filter.issueStatus}">
+                    <option value="s.${status}" <c:if test="${viewmodel.filter.status.contains(status) }">selected</c:if>>
+                        <fmt:message key="issue.status.${status}"/>
+                    </option>
+                </c:forEach>
+            </select>
+        </div>
+    </div>
+    <div class="medskip">
+        <button name="filter" type="submit"><fmt:message key="button.apply"/></button>
+    </div>
+</form>
--- a/src/main/webapp/WEB-INF/jspf/issue-summary.jspf	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/webapp/WEB-INF/jspf/issue-summary.jspf	Sun Jan 08 17:07:26 2023 +0100
@@ -9,7 +9,9 @@
         <div><c:out value="${summary.open}"/></div>
         <div class="caption"><fmt:message key="issues.active"/>:</div>
         <div><c:out value="${summary.active}"/></div>
+        <c:if test="${summary.done gt 0}">
         <div class="caption"><fmt:message key="issues.done"/>:</div>
         <div><c:out value="${summary.done}"/></div>
+        </c:if>
     </div>
 </div>
\ No newline at end of file
--- a/src/main/webapp/lightpit.css	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/webapp/lightpit.css	Sun Jan 08 17:07:26 2023 +0100
@@ -73,6 +73,14 @@
     padding: 0;
 }
 
+h2 {
+    margin: 0.75em 0;
+}
+
+h3 {
+    margin: 0.25em 0;
+}
+
 textarea, input, button, select {
     font-family: inherit;
     font-size: inherit;
--- a/src/main/webapp/project-details.js	Tue Jan 03 18:25:51 2023 +0100
+++ b/src/main/webapp/project-details.js	Sun Jan 08 17:07:26 2023 +0100
@@ -52,4 +52,20 @@
         full.style.display = 'none'
     }
 }
-window.addEventListener('load', function() { toggleProjectDetails() }, false)
+
+function toggleFilterDetails() {
+    const filters = document.getElementById('more-filters')
+    const toggle = document.getElementById('show-more-filters')
+    if (toggle.checked) {
+        filters.style.display = 'flex'
+    } else {
+        filters.style.display = 'none'
+    }
+}
+
+function toggleDetails() {
+    toggleProjectDetails()
+    toggleFilterDetails()
+}
+
+window.addEventListener('load', function() { toggleDetails() }, false)

mercurial