Sun, 02 Feb 2025 17:08:18 +0100
implement changing and saving variant status
relates to #491
--- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Sun Feb 02 17:08:18 2025 +0100 @@ -156,6 +156,16 @@ private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext) private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp") + fun paramIndexed(prefix: String): Map<Int, String> = buildMap { + for (name in request.parameterNames) { + if (name.startsWith(prefix)) { + val key = name.substring(prefix.length).toIntOrNull() + if (key != null) { + put(key, request.getParameter(name)) + } + } + } + } fun param(name: String): String? = request.getParameter(name) fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sun Feb 02 17:08:18 2025 +0100 @@ -879,7 +879,7 @@ """ insert into lpit_issue_variant_status (issueid, variant, status) values (?, ?, ?::issue_status) - on conflict do update + on conflict (issueid, variant) do update set status = ?::issue_status, outdated = false """.trimIndent() ) {
--- a/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Sun Feb 02 17:08:18 2025 +0100 @@ -55,6 +55,9 @@ fun removeVariant(variant: Variant) { variantStatus.remove(variant) } + fun removeAllVariants() { + variantStatus.clear() + } fun getStatusForVariant(variant: Variant): IssueStatus? { return variantStatus[variant] }
--- a/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Sun Feb 02 17:08:18 2025 +0100 @@ -3,9 +3,7 @@ import de.uapcore.lightpit.HttpRequest import de.uapcore.lightpit.dao.DataAccessObject import de.uapcore.lightpit.dateOptValidator -import de.uapcore.lightpit.entities.Issue -import de.uapcore.lightpit.entities.IssueComment -import de.uapcore.lightpit.entities.IssueRelation +import de.uapcore.lightpit.entities.* import de.uapcore.lightpit.types.IssueCategory import de.uapcore.lightpit.types.IssueStatus import de.uapcore.lightpit.types.RelationType @@ -15,7 +13,10 @@ import de.uapcore.lightpit.viewmodel.projectNavMenu import java.sql.Date -fun Issue.hasChanged(reference: Issue) = !(component == reference.component && +fun Issue.hasChanged(reference: Issue) = + (isTrackingVariantStatus xor reference.isTrackingVariantStatus) + || (isTrackingVariantStatus && !variantStatus.equals(reference)) + || !(component == reference.component && status == reference.status && category == reference.category && subject == reference.subject && @@ -33,7 +34,10 @@ else eta.compareTo(date) } -fun Issue.applyFormData(http: HttpRequest, dao: DataAccessObject): Issue = this.apply { +fun Issue.applyFormData( + http: HttpRequest, dao: DataAccessObject, + versions: List<Version>? = null, variants: List<Variant>? = null, +): Issue = this.apply { component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1) category = IssueCategory.valueOf(http.param("category") ?: "") status = IssueStatus.valueOf(http.param("status") ?: "") @@ -49,10 +53,32 @@ // TODO: process error messages eta = http.param("eta", ::dateOptValidator, null, mutableListOf()) + // if versions are selected, but we don't have a version cache, request the dao + val versionsLookup = versions + ?: if (http.param("affected") != null || http.param("resolved") != null) + dao.listVersions(project) else emptyList() + // we must resolve the versions with the DAO, otherwise we do not get the names for the history - val vlookup = {paramKey: String -> http.param(paramKey)?.toIntOrNull()?.takeIf { it > 0 }?.let { dao.findVersion(it) }} + val vlookup = {paramKey: String -> http.param(paramKey)?.toIntOrNull()?.takeIf { it > 0 }?.let {p -> versionsLookup.find { v -> v.id == p } }} affected = vlookup("affected") resolved = vlookup("resolved") + + // process possible issue variants + if (http.param("use-variants") == null) { + removeAllVariants() + } else { + val variantsLookup = variants ?: dao.listVariants(project) + http.paramIndexed("status-variant-").forEach { (variantid, status) -> + variantsLookup.find { v -> v.id == variantid }?.let { variant -> + if (status == "not-relevant") { + removeVariant(variant) + } else { + setStatusForVariant(variant, IssueStatus.valueOf(status)) + } + } + } + // TODO: compute overall status + } } fun processIssueForm(issue: Issue, reference: Issue, http: HttpRequest, dao: DataAccessObject) {
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt Sun Feb 02 17:08:18 2025 +0100 @@ -71,6 +71,7 @@ issue, dao.listVersions(issue.project), dao.listComponents(issue.project), + dao.listVariants(issue.project), dao.listUsers(), issue.project )
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Sun Feb 02 17:08:18 2025 +0100 @@ -434,6 +434,7 @@ issue, path.projectInfo.versions, path.projectInfo.components, + path.projectInfo.variants, dao.listUsers(), path.projectInfo.project, path @@ -457,7 +458,7 @@ val issue = Issue( http.param("id")?.toIntOrNull() ?: -1, project - ).applyFormData(http, dao) + ).applyFormData(http, dao, projectInfo.versions, projectInfo.variants) val openId = if (issue.id < 0) { val remoteUser = http.remoteUser?.let { dao.findUserByName(it) }
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Sun Feb 02 17:08:18 2025 +0100 @@ -178,6 +178,7 @@ val issue: Issue, val versions: List<Version>, val components: List<Component>, + val variants: List<Variant>, val users: List<User>, val project: Project, val pathInfos: PathInfos? = null
--- a/src/main/resources/localization/strings.properties Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/resources/localization/strings.properties Sun Feb 02 17:08:18 2025 +0100 @@ -128,6 +128,9 @@ issue.status=Status issue.subject=Subject issue.updated=Updated +issue.variants=Variants +issue.variants.checkbox-text=Use individual status for each variant +issue.variants.not-relevant=Not Relevant issues.active=In Progress issues.done=Done issues.open=Open
--- a/src/main/resources/localization/strings_de.properties Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/resources/localization/strings_de.properties Sun Feb 02 17:08:18 2025 +0100 @@ -128,6 +128,9 @@ issue.status=Status issue.subject=Thema issue.updated=Aktualisiert +issue.variants=Varianten +issue.variants.checkbox-text=Individueller Status f\u00fcr jede Variante +issue.variants.not-relevant=Nicht Relevant issues.active=In Arbeit issues.done=Erledigt issues.open=Offen
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Sun Feb 02 17:08:18 2025 +0100 @@ -81,6 +81,20 @@ </select> </td> </tr> + <c:if test="${not empty viewmodel.variants}"> + <tr> + <th><fmt:message key="issue.variants"/></th> + <td> + <input type="checkbox" id="use-variants" name="use-variants" + <c:if test="${issue.trackingVariantStatus}">checked</c:if> + onclick="toggleVariantStatus()" + /> + <label for="use-variants"> + <fmt:message key="issue.variants.checkbox-text"/> + </label> + </td> + </tr> + </c:if> <tr> <th><label for="issue-status"><fmt:message key="issue.status"/></label></th> <td> @@ -93,6 +107,29 @@ </option> </c:forEach> </select> + <div id="issue-variant-status"> + <c:forEach items="${viewmodel.variants}" var="variant"> + <div title="<c:out value="${variant.description}" />"> + <label for="issue-status-variant-${variant.id}" > + <c:out value="${variant.name}"/>: + </label> + <select id="issue-status-variant-${variant.id}" name="status-variant-${variant.id}"> + <option value="not-relevant" + <c:if test="${empty issue.getStatusForVariant(variant)}">selected</c:if> + > + <fmt:message key="issue.variants.not-relevant"/> + </option> + <c:forEach var="status" items="${viewmodel.issueStatus}"> + <option + <c:if test="${status eq issue.getStatusForVariant(variant)}">selected</c:if> + value="${status}"> + <fmt:message key="issue.status.${status}" /> + </option> + </c:forEach> + </select> + </div> + </c:forEach> + </div> </td> </tr> <tr>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Sun Feb 02 17:08:18 2025 +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="20250130"/> +<c:set scope="page" var="versionSuffix" value="20250202"/> <%-- Make the base href easily available at request scope --%> <c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/>
--- a/src/main/webapp/issue-editor.js Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/webapp/issue-editor.js Sun Feb 02 17:08:18 2025 +0100 @@ -41,3 +41,19 @@ editor.style.display='none' view.style.display='block' } + +function toggleVariantStatus() { + const cbox = document.getElementById('use-variants') + if (!cbox) return + const issue_status = document.getElementById('issue-status') + const variant_status = document.getElementById('issue-variant-status') + if (cbox.checked) { + issue_status.style.display = 'none' + variant_status.style.display = 'flex' + } else { + issue_status.style.display = 'inline-block' + variant_status.style.display = 'none' + } +} + +window.addEventListener("load", (_) => toggleVariantStatus());
--- a/src/main/webapp/projects.css Sun Feb 02 14:12:02 2025 +0100 +++ b/src/main/webapp/projects.css Sun Feb 02 17:08:18 2025 +0100 @@ -193,6 +193,12 @@ white-space: nowrap; } +#issue-variant-status { + display: flex; + gap: 1em; + flex-wrap: wrap; +} + table.relation-editor input, table.relation-editor button, table.relation-editor .button {