Sun, 08 Jan 2023 17:07:26 +0100
#15 add issue filters
--- 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)