# HG changeset patch # User Mike Becker # Date 1673194046 -3600 # Node ID ca5501d851fac8674f9331141c5f0adaafe9ac61 # Parent d8ec2d8ffa8230c48c3cf17a8ba44180a4c1eebe #15 add issue filters diff -r d8ec2d8ffa82 -r ca5501d851fa setup/postgres/psql_create_tables.sql --- 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' diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt --- 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 - fun listIssues(project: Project, version: Version?, component: Component?): List + fun listIssues(project: Project, includeDone: Boolean): List + fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List 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 + fun getIssueRelationMap(project: Project, includeDone: Boolean): IssueRelationMap fun insertHistoryEvent(issue: Issue, newId: Int = 0) fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int = 0) diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt --- 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 = - withStatement("$issueQuery where i.project = ?") { + override fun listIssues(project: Project, includeDone: Boolean): List = + 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 = + override fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List = 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("type"))) } + }.groupBy({it.first},{it.second}) // // diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/kotlin/de/uapcore/lightpit/entities/IssueRelation.kt --- 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>> diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt --- 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 ?: "")) && + (!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 ) diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/kotlin/de/uapcore/lightpit/types/RelationType.kt --- 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) +} diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt --- 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 = evalEnum(http, "s") + val category: List = 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 > evalEnum(http: HttpRequest, prefix: String): List { + 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(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() + } +} diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt --- 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, + val filter: IssueFilter, val version: Version? = null, val component: Component? = null ) : View() { diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/resources/localization/strings.properties --- 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 diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/resources/localization/strings_de.properties --- 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 diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/WEB-INF/changelogs/changelog-de.jspf --- 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 @@
  • Mehrfachauswahl für Versionen im Vorgang entfernt.
  • RSS Feeds für Projekte hinzugefügt.
  • Vorgangsansicht vereinfacht.
  • +
  • Filteroptionen hinzugefügt.
  • Möglichkeit zum Deaktivieren einer Komponente hinzugefügt.
  • Datum der Veröffentlichung und des Supportendes zu Versionen hinzugefügt.
  • Gesamtanzahl der Kommentare wird nun angezeigt.
  • diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/WEB-INF/changelogs/changelog.jspf --- 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 @@
  • Remove multi selection of versions within an issue.
  • Add RSS feeds for projects.
  • Simplify issue view.
  • +
  • Add filtering options.
  • Add possibility to deactivate a component.
  • Add release and end of life dates to versions.
  • Add the total number of comments to the caption.
  • diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/WEB-INF/jsp/project-details.jsp --- 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 @@ +

    +<%@include file="../jspf/issue-filter.jspf"%> +

    diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/WEB-INF/jsp/site.jsp --- 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 --%> - + <%-- Make the base href easily available at request scope --%> diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/WEB-INF/jspf/issue-filter.jspf --- /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 @@ +<%-- + +--%> +
    +
    + + + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/WEB-INF/jspf/issue-summary.jspf --- 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 @@
    :
    +
    :
    +
    \ No newline at end of file diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/lightpit.css --- 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; diff -r d8ec2d8ffa82 -r ca5501d851fa src/main/webapp/project-details.js --- 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)