add version planning feature - resolves #446 default tip

Mon, 06 Oct 2025 13:37:52 +0200

author
Mike Becker <universe@uap-core.de>
date
Mon, 06 Oct 2025 13:37:52 +0200
changeset 399
0b59551086f4
parent 398
384a98f0220d

add version planning feature - resolves #446

src/main/kotlin/de/uapcore/lightpit/Constants.kt 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/servlet/ProjectServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Versions.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/version-plan.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-list.jspf file | annotate | diff | comparison | revisions
--- 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}&nbsp;-&nbsp;<c:out value="${issue.subject}" />
                     </a>
                 </span>

mercurial