Mon, 06 Oct 2025 13:37:52 +0200
add version planning feature - resolves #446
--- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt Mon Oct 06 13:37:52 2025 +0200 @@ -29,7 +29,7 @@ /** * A data in yyyy-mm-dd format to identify the release. */ - const val VERSION_DATE = "2025-10-05" + const val VERSION_DATE = "2025-10-06" /** * The path where the JSP files reside.
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Mon Oct 06 13:37:52 2025 +0200 @@ -121,9 +121,12 @@ specificVariant: Boolean, variant: Variant? ): List<Issue> + fun listIssues(project: Project, includeDone: Boolean) = + listIssues(project, includeDone, false, null, false, null, false, null) fun findIssue(id: Int): Issue? fun insertIssue(issue: Issue): Int fun updateIssue(issue: Issue) + fun updateIssueResolvedVersion(issue: Issue, version: Version) fun listComments(issue: Issue): List<IssueComment> fun findComment(id: Int): IssueComment?
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Mon Oct 06 13:37:52 2025 +0200 @@ -977,6 +977,15 @@ updateVariantStatus(issue.id, issue.variantStatus) } + override fun updateIssueResolvedVersion(issue: Issue, version: Version) { + issue.resolved = version // leave the DTO in a consistent state + withStatement("update lpit_issue set updated = now(), resolved = ? where issueid = ?") { + setInt(1, version.id) + setInt(2, issue.id) + executeUpdate() + } + } + override fun listCommitRefs(issue: Issue): List<CommitRef> = withStatement("select commit_hash, commit_brief, commit_time from lpit_commit_ref where issueid = ? order by commit_time") { setInt(1, issue.id)
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Mon Oct 06 13:37:52 2025 +0200 @@ -48,6 +48,8 @@ get("/%project/versions/", this::versions) get("/%project/versions/%version/edit", this::versionForm) + get("/%project/versions/%version/plan", this::versionPlanning) + post("/%project/versions/%version/plan", this::versionPlanningCommit) get("/%project/versions/-/create", this::versionForm) post("/%project/versions/-/commit", this::versionCommit) @@ -266,6 +268,52 @@ http.renderCommit(http.sanitizeReferer(http.param("returnLink")) ?: "projects/${project.node}/versions/") } + private fun versionPlanning(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.let { path -> + val project = path.project + val versionInfo = path.versionInfo as? OptionalPathInfo.Specific + if (versionInfo == null || versionInfo.elem.status.isReleased) { + http.response.sendError(404) + return + } + val filter = IssueFilter(http, dao) + val issues = dao.listIssues(project, filter.includeDone) + .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) + .filter(issueFilterFunction(filter, dao, http.user?.username ?: "<Anonymous>", project)) + + with(http) { + pageTitle = "${project.name} - ${i18n("version.planning.title")} ${versionInfo.elem.name}" + view = VersionPlanning(path.projectInfo, versionInfo.elem, issues, filter) + navigationMenu = projectNavMenu(dao.listProjects(), path) + styleSheets = listOf("projects") + javascript = "issue-overview" + render("version-plan") + } + } + } + + private fun versionPlanningCommit(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.let { path -> + val project = path.project + val versionInfo = path.versionInfo as? OptionalPathInfo.Specific + if (versionInfo == null || versionInfo.elem.status.isReleased) { + http.response.sendError(404) + return + } + val version = versionInfo.elem + val issueIds = http.paramArray("issueIds").mapNotNull { it.toIntOrNull() } + + for (issueId in issueIds) { + dao.findIssue(issueId)?.let { issue -> + dao.updateIssueResolvedVersion(issue, version) + dao.insertHistoryEvent(http.user, issue) + } + } + + http.renderCommit("projects/${project.node}/versions/${version.node}/plan") + } + } + private fun components(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let { path -> with(http) {
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Versions.kt Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Versions.kt Mon Oct 06 13:37:52 2025 +0200 @@ -74,3 +74,18 @@ ) : EditView() { val versionStatus = VersionStatus.entries } + +class VersionPlanning( + val projectInfo: ProjectInfo, + val version: Version, + val issues: List<Issue>, + val filter: IssueFilter +) : View() { + val versionInfo = VersionInfo(version, issues) + val issueSummary = IssueSummary() + val candidateIssues = issues.filter { it.resolved != version } + + init { + issues.forEach(issueSummary::add) + } +}
--- a/src/main/resources/localization/strings.properties Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/resources/localization/strings.properties Mon Oct 06 13:37:52 2025 +0200 @@ -50,6 +50,7 @@ button.variant.edit=Edit Variant button.version.create=New Version button.version.edit=Edit Version +button.version.plan=Plan Version button.whats-new=Show Changelog commit.redirect-link=If redirection does not work, click the following link: commit.success=Operation successful - you will be redirected in a second. @@ -204,6 +205,8 @@ version.status.Unreleased=Unreleased version.status=Status version=Version +version.planning.title=Planning Version +version.planning.open=Select Issues whats-new.info = A new version of LightPIT has been released. Do you want to check the release notes? issue.filter.sort.primary=Order by issue.filter.sort.secondary=then by
--- a/src/main/resources/localization/strings_de.properties Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/resources/localization/strings_de.properties Mon Oct 06 13:37:52 2025 +0200 @@ -50,6 +50,7 @@ button.variant.edit=Variante Bearbeiten button.version.create=Neue Version button.version.edit=Version Bearbeiten +button.version.plan=Version Planen button.whats-new=Versionshinweise \u00d6ffnen commit.redirect-link=Falls die Weiterleitung nicht klappt, klicken Sie bitte hier: commit.success=Operation erfolgreich - Sie werden jeden Moment weitergeleitet. @@ -204,6 +205,8 @@ version.status.Unreleased=Unver\u00f6ffentlicht version.status=Status version=Version +version.planning.title=Plane Version +version.planning.open=Auswahl whats-new.info = Eine neue LightPIT-Version wurde ver\u00f6ffentlicht. Wollen Sie mehr erfahren? issue.filter.sort.primary=Sortiere nach issue.filter.sort.secondary=dann nach
--- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Mon Oct 06 13:37:52 2025 +0200 @@ -27,6 +27,7 @@ <h3>Version 1.6.0 (Vorschau)</h3> <ul> + <li>Neues Feature zur Planung von Versionen hinzugefügt.</li> <li>Pop-Up hinzugefügt, das über eine neue LightPIT-Version informiert.</li> <li>Neue Schaltflächen zur Vorgangsansicht hinzugefügt, mit denen der Status des Vorgangs schnell geändert werden kann.</li> <li>"In Projekt Öffnen" Schaltfläche zur (globalen) Vorgangsansicht hinzugefügt.</li>
--- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf Mon Oct 06 13:37:52 2025 +0200 @@ -27,6 +27,7 @@ <h3>Version 1.6.0 (preview)</h3> <ul> + <li>Add new version planning UI.</li> <li>Add popup informing about a new LightPIT release.</li> <li>Add convenience buttons to the issue view to quickly change the issue status.</li> <li>Add convenience OPEN IN PROJECT button to the global issue view.</li>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp Mon Oct 06 13:37:52 2025 +0200 @@ -39,6 +39,9 @@ <div> <a href="${issuesHref}-/create" class="button"><fmt:message key="button.issue.create"/></a> <c:if test="${not empty viewmodel.versionInfo}"> + <c:if test="${not viewmodel.versionInfo.version.status.released}"> + <a href="./projects/${project.node}/versions/${viewmodel.versionInfo.version.node}/plan" class="button"><fmt:message key="button.version.plan"/></a> + </c:if> <a href="./projects/${project.node}/versions/${viewmodel.versionInfo.version.node}/edit" class="button"><fmt:message key="button.version.edit"/></a> </c:if> <c:if test="${not empty component}">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/version-plan.jsp Mon Oct 06 13:37:52 2025 +0200 @@ -0,0 +1,86 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2025 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.VersionPlanning" scope="request" /> + +<c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/> +<c:set var="version" scope="page" value="${viewmodel.versionInfo.version}"/> +<c:set var="issuesHref" value="./projects/${project.node}/issues/${version.node}/-/-/"/> +<%@include file="../jspf/project-header.jspf"%> + +<h2><fmt:message key="progress" /></h2> + +<c:set var="summary" value="${viewmodel.projectInfo.issueSummary}" /> +<%@include file="../jspf/issue-summary.jspf"%> + +<h2><fmt:message key="version.planning.title"/> <c:out value="${version.name}"/></h2> + +<h3><fmt:message key="issue.filter" /></h3> +<%@include file="../jspf/issue-filter.jspf"%> + +<c:set var="showProjectInfo" value="false"/> +<c:set var="showVersionInfo" value="true"/> +<c:set var="showComponentInfo" value="true"/> +<c:set var="showVariantInfo" value="true"/> +<c:set var="openIssuesInTabs" value="true"/> +<c:set var="versionInfo" value="${viewmodel.versionInfo}"/> + +<h3><fmt:message key="version.planning.open" /></h3> + +<form method="post"> +<div> + <c:if test="${not empty viewmodel.versionInfo}"> + <a href="${issuesHref}" class="button"><fmt:message key="button.cancel"/></a> + <button type="submit"><fmt:message key="button.apply"/></button> + </c:if> +</div> + +<c:set var="issues" value="${viewmodel.candidateIssues}"/> +<c:set var="showCheckbox" value="true"/> +<%@include file="../jspf/issue-list.jspf"%> +<c:set var="showCheckbox" value="false"/> +</form> + +<h3><fmt:message key="issues.resolved"/> </h3> +<c:set var="showVersionInfo" value="false"/> +<c:set var="summary" value="${versionInfo.resolvedTotal}"/> +<%@include file="../jspf/issue-summary.jspf"%> +<c:set var="issues" value="${versionInfo.resolved}"/> +<c:if test="${not empty issues}"> + <%@include file="../jspf/issue-list.jspf"%> +</c:if> +<% // corner case: a version was released before and is now open again for planning %> +<c:set var="issues" value="${versionInfo.reported}"/> +<c:if test="${not empty issues}"> + <h3><fmt:message key="issues.reported"/> </h3> + <c:set var="summary" value="${versionInfo.reportedTotal}"/> + <%@include file="../jspf/issue-summary.jspf"%> + <%@include file="../jspf/issue-list.jspf"%> +</c:if>
--- a/src/main/webapp/WEB-INF/jspf/issue-list.jspf Mon Oct 06 13:02:12 2025 +0200 +++ b/src/main/webapp/WEB-INF/jspf/issue-list.jspf Mon Oct 06 13:37:52 2025 +0200 @@ -1,6 +1,8 @@ <%-- issues: List<Issue> issuesHref: String +openIssuesInTabs: boolean +showCheckbox: boolean showComponentInfo: boolean showVersionInfo: boolean showVariantInfo: boolean @@ -8,6 +10,9 @@ --%> <table class="fullwidth datatable medskip"> <colgroup> + <c:if test="${showCheckbox}"> + <col style="width: 3ex" /> + </c:if> <c:if test="${showProjectInfo}"> <col style="width: 10%" /> </c:if> @@ -18,6 +23,9 @@ </colgroup> <thead> <tr> + <c:if test="${showCheckbox}"> + <th></th> + </c:if> <c:if test="${showProjectInfo}"> <th><fmt:message key="project"/></th> </c:if> @@ -30,6 +38,11 @@ <tbody> <c:forEach var="issue" items="${issues}"> <tr> + <c:if test="${showCheckbox}"> + <td> + <input type="checkbox" name="issueIds" value="${issue.id}" /> + </td> + </c:if> <c:if test="${showProjectInfo}"> <td> <a href="./projects/${issue.project.node}"> @@ -39,7 +52,7 @@ </c:if> <td> <span class="phase-${issue.status.phase.number}"> - <a href="${issuesHref}${issue.id}"> + <a href="${issuesHref}${issue.id}" <c:if test="${openIssuesInTabs}">target="_blank"</c:if> > #${issue.id} - <c:out value="${issue.subject}" /> </a> </span>