Fri, 30 Dec 2022 19:04:34 +0100
#29 add possibility to relate issues
1.1 --- a/setup/postgres/psql_create_tables.sql Fri Dec 30 13:21:09 2022 +0100 1.2 +++ b/setup/postgres/psql_create_tables.sql Fri Dec 30 19:04:34 2022 +0100 1.3 @@ -149,3 +149,21 @@ 1.4 comment text not null 1.5 ); 1.6 1.7 +create type relation_type as enum ( 1.8 + 'RelatesTo', 1.9 + 'TogetherWith', 1.10 + 'Before', 1.11 + 'SubtaskOf', 1.12 + 'Blocks', 1.13 + 'Tests', 1.14 + 'Duplicates' 1.15 + ); 1.16 + 1.17 +create table lpit_issue_relation 1.18 +( 1.19 + from_issue integer not null references lpit_issue (issueid) on delete cascade, 1.20 + to_issue integer not null references lpit_issue (issueid) on delete cascade, 1.21 + type relation_type not null 1.22 +); 1.23 + 1.24 +create unique index lpit_issue_relation_unique on lpit_issue_relation (from_issue, to_issue, type);
2.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Fri Dec 30 13:21:09 2022 +0100 2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Fri Dec 30 19:04:34 2022 +0100 2.3 @@ -70,6 +70,7 @@ 2.4 fun collectIssueSummary(project: Project): IssueSummary 2.5 fun collectIssueSummary(assignee: User): IssueSummary 2.6 2.7 + fun listIssues(project: Project): List<Issue> 2.8 fun listIssues(project: Project, version: Version?, component: Component?): List<Issue> 2.9 fun findIssue(id: Int): Issue? 2.10 fun insertIssue(issue: Issue): Int 2.11 @@ -80,6 +81,13 @@ 2.12 fun insertComment(issueComment: IssueComment): Int 2.13 fun updateComment(issueComment: IssueComment) 2.14 2.15 + /** 2.16 + * Inserts an issue relation, if it does not already exist. 2.17 + */ 2.18 + fun insertIssueRelation(rel: IssueRelation) 2.19 + fun deleteIssueRelation(rel: IssueRelation) 2.20 + fun listIssueRelations(issue: Issue): List<IssueRelation> 2.21 + 2.22 fun insertHistoryEvent(issue: Issue, newId: Int = 0) 2.23 fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int = 0) 2.24
3.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Fri Dec 30 13:21:09 2022 +0100 3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Fri Dec 30 19:04:34 2022 +0100 3.3 @@ -534,10 +534,15 @@ 3.4 return i 3.5 } 3.6 3.7 + override fun listIssues(project: Project): List<Issue> = 3.8 + withStatement("$issueQuery where i.project = ?") { 3.9 + setInt(1, project.id) 3.10 + queryAll { it.extractIssue() } 3.11 + } 3.12 + 3.13 override fun listIssues(project: Project, version: Version?, component: Component?): List<Issue> = 3.14 withStatement( 3.15 - """$issueQuery where 3.16 - (not ? or i.project = ?) and 3.17 + """$issueQuery where i.project = ? and 3.18 (not ? or ? in (resolved, affected)) and (not ? or (resolved is null and affected is null)) and 3.19 (not ? or component = ?) and (not ? or component is null) 3.20 """.trimIndent() 3.21 @@ -553,10 +558,9 @@ 3.22 setInt(idcol, search.id) 3.23 } 3.24 } 3.25 - setBoolean(1, true) 3.26 - setInt(2, project.id) 3.27 - applyFilter(version, 3, 5, 4) 3.28 - applyFilter(component, 6, 8, 7) 3.29 + setInt(1, project.id) 3.30 + applyFilter(version, 2, 4, 3) 3.31 + applyFilter(component, 5, 7, 6) 3.32 3.33 queryAll { it.extractIssue() } 3.34 } 3.35 @@ -629,6 +633,53 @@ 3.36 3.37 //</editor-fold> 3.38 3.39 + //<editor-fold desc="Issue Relations"> 3.40 + override fun insertIssueRelation(rel: IssueRelation) { 3.41 + withStatement( 3.42 + """ 3.43 + insert into lpit_issue_relation (from_issue, to_issue, type) 3.44 + values (?, ?, ?::relation_type) 3.45 + on conflict do nothing 3.46 + """.trimIndent() 3.47 + ) { 3.48 + if (rel.reverse) { 3.49 + setInt(2, rel.from.id) 3.50 + setInt(1, rel.to.id) 3.51 + } else { 3.52 + setInt(1, rel.from.id) 3.53 + setInt(2, rel.to.id) 3.54 + } 3.55 + setEnum(3, rel.type) 3.56 + executeUpdate() 3.57 + } 3.58 + } 3.59 + 3.60 + override fun deleteIssueRelation(rel: IssueRelation) { 3.61 + withStatement("delete from lpit_issue_relation where from_issue = ? and to_issue = ? and type=?::relation_type") { 3.62 + if (rel.reverse) { 3.63 + setInt(2, rel.from.id) 3.64 + setInt(1, rel.to.id) 3.65 + } else { 3.66 + setInt(1, rel.from.id) 3.67 + setInt(2, rel.to.id) 3.68 + } 3.69 + setEnum(3, rel.type) 3.70 + executeUpdate() 3.71 + } 3.72 + } 3.73 + 3.74 + override fun listIssueRelations(issue: Issue): List<IssueRelation> = buildList { 3.75 + withStatement("select to_issue, type from lpit_issue_relation where from_issue = ?") { 3.76 + setInt(1, issue.id) 3.77 + queryAll { IssueRelation(issue, findIssue(it.getInt("to_issue"))!!, it.getEnum("type"), false) } 3.78 + }.forEach(this::add) 3.79 + withStatement("select from_issue, type from lpit_issue_relation where to_issue = ?") { 3.80 + setInt(1, issue.id) 3.81 + queryAll { IssueRelation(issue, findIssue(it.getInt("from_issue"))!!, it.getEnum("type"), true) } 3.82 + }.forEach(this::add) 3.83 + } 3.84 + //</editor-fold> 3.85 + 3.86 //<editor-fold desc="IssueComment"> 3.87 3.88 private fun ResultSet.extractIssueComment() =
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueRelation.kt Fri Dec 30 19:04:34 2022 +0100 4.3 @@ -0,0 +1,36 @@ 4.4 +/* 4.5 + * Copyright 2022 Mike Becker. All rights reserved. 4.6 + * 4.7 + * Redistribution and use in source and binary forms, with or without 4.8 + * modification, are permitted provided that the following conditions are met: 4.9 + * 4.10 + * 1. Redistributions of source code must retain the above copyright 4.11 + * notice, this list of conditions and the following disclaimer. 4.12 + * 4.13 + * 2. Redistributions in binary form must reproduce the above copyright 4.14 + * notice, this list of conditions and the following disclaimer in the 4.15 + * documentation and/or other materials provided with the distribution. 4.16 + * 4.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 4.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 4.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 4.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 4.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 4.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 4.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 4.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 4.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 4.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 4.27 + * 4.28 + */ 4.29 + 4.30 +package de.uapcore.lightpit.entities 4.31 + 4.32 +import de.uapcore.lightpit.types.RelationType 4.33 + 4.34 +class IssueRelation( 4.35 + val from: Issue, 4.36 + val to: Issue, 4.37 + val type: RelationType, 4.38 + val reverse: Boolean = false 4.39 +)
5.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Fri Dec 30 13:21:09 2022 +0100 5.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Fri Dec 30 19:04:34 2022 +0100 5.3 @@ -31,10 +31,7 @@ 5.4 import de.uapcore.lightpit.dao.DataAccessObject 5.5 import de.uapcore.lightpit.dateOptValidator 5.6 import de.uapcore.lightpit.entities.* 5.7 -import de.uapcore.lightpit.types.IssueCategory 5.8 -import de.uapcore.lightpit.types.IssueStatus 5.9 -import de.uapcore.lightpit.types.VersionStatus 5.10 -import de.uapcore.lightpit.types.WebColor 5.11 +import de.uapcore.lightpit.types.* 5.12 import de.uapcore.lightpit.viewmodel.* 5.13 import jakarta.servlet.annotation.WebServlet 5.14 import java.sql.Date 5.15 @@ -63,6 +60,8 @@ 5.16 get("/%project/issues/%version/%component/%issue", this::issue) 5.17 get("/%project/issues/%version/%component/%issue/edit", this::issueForm) 5.18 post("/%project/issues/%version/%component/%issue/comment", this::issueComment) 5.19 + post("/%project/issues/%version/%component/%issue/relation", this::issueRelation) 5.20 + get("/%project/issues/%version/%component/%issue/removeRelation", this::issueRemoveRelation) 5.21 get("/%project/issues/%version/%component/-/create", this::issueForm) 5.22 post("/%project/issues/%version/%component/-/commit", this::issueCommit) 5.23 } 5.24 @@ -440,18 +439,35 @@ 5.25 } 5.26 5.27 private fun issue(http: HttpRequest, dao: DataAccessObject) { 5.28 + val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 5.29 + if (issue == null) { 5.30 + http.response.sendError(404) 5.31 + return 5.32 + } 5.33 + renderIssueView(http, dao, issue) 5.34 + } 5.35 + 5.36 + private fun renderIssueView( 5.37 + http: HttpRequest, 5.38 + dao: DataAccessObject, 5.39 + issue: Issue, 5.40 + relationError: String? = null 5.41 + ) { 5.42 withPathInfo(http, dao)?.run { 5.43 - val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) 5.44 - if (issue == null) { 5.45 - http.response.sendError(404) 5.46 - return 5.47 - } 5.48 - 5.49 val comments = dao.listComments(issue) 5.50 5.51 with(http) { 5.52 pageTitle = "${projectInfo.project.name}: #${issue.id} ${issue.subject}" 5.53 - view = IssueDetailView(issue, comments, project, version, component) 5.54 + view = IssueDetailView( 5.55 + issue, 5.56 + comments, 5.57 + project, 5.58 + version, 5.59 + component, 5.60 + dao.listIssues(project), 5.61 + dao.listIssueRelations(issue), 5.62 + relationError 5.63 + ) 5.64 feedPath = feedPath(projectInfo.project) 5.65 navigationMenu = activeProjectNavMenu( 5.66 dao.listProjects(), 5.67 @@ -468,7 +484,7 @@ 5.68 5.69 private fun issueForm(http: HttpRequest, dao: DataAccessObject) { 5.70 withPathInfo(http, dao)?.run { 5.71 - val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) ?: Issue( 5.72 + val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue( 5.73 -1, 5.74 project, 5.75 ) 5.76 @@ -514,7 +530,7 @@ 5.77 5.78 private fun issueComment(http: HttpRequest, dao: DataAccessObject) { 5.79 withPathInfo(http, dao)?.run { 5.80 - val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) 5.81 + val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 5.82 if (issue == null) { 5.83 http.response.sendError(404) 5.84 return 5.85 @@ -616,4 +632,88 @@ 5.86 } 5.87 } 5.88 } 5.89 + 5.90 + private fun issueRelation(http: HttpRequest, dao: DataAccessObject) { 5.91 + withPathInfo(http, dao)?.run { 5.92 + val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 5.93 + if (issue == null) { 5.94 + http.response.sendError(404) 5.95 + return 5.96 + } 5.97 + 5.98 + // determine the relation type 5.99 + val type: Pair<RelationType, Boolean>? = http.param("type")?.let { 5.100 + try { 5.101 + if (it.startsWith("!")) { 5.102 + Pair(RelationType.valueOf(it.substring(1)), true) 5.103 + } else { 5.104 + Pair(RelationType.valueOf(it), false) 5.105 + } 5.106 + } catch (_: IllegalArgumentException) { 5.107 + null 5.108 + } 5.109 + } 5.110 + 5.111 + // if the relation type was invalid, send HTTP 500 5.112 + if (type == null) { 5.113 + http.response.sendError(500) 5.114 + return 5.115 + } 5.116 + 5.117 + // determine the target issue 5.118 + val targetIssue: Issue? = http.param("issue")?.let { 5.119 + if (it.startsWith("#") && it.length > 1) { 5.120 + it.substring(1).split(" ", limit = 2)[0].toIntOrNull() 5.121 + ?.let(dao::findIssue) 5.122 + ?.takeIf { target -> target.project.id == issue.project.id } 5.123 + } else { 5.124 + null 5.125 + } 5.126 + } 5.127 + 5.128 + // check if the target issue is valid 5.129 + if (targetIssue == null) { 5.130 + renderIssueView(http, dao, issue, "issue.relations.target.invalid") 5.131 + return 5.132 + } 5.133 + 5.134 + // commit the result 5.135 + dao.insertIssueRelation(IssueRelation(issue, targetIssue, type.first, type.second)) 5.136 + http.renderCommit("${issuesHref}${issue.id}") 5.137 + } 5.138 + } 5.139 + 5.140 + private fun issueRemoveRelation(http: HttpRequest, dao: DataAccessObject) { 5.141 + withPathInfo(http, dao)?.run { 5.142 + val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 5.143 + if (issue == null) { 5.144 + http.response.sendError(404) 5.145 + return 5.146 + } 5.147 + 5.148 + // determine relation 5.149 + val type = http.param("type")?.let { 5.150 + try {RelationType.valueOf(it)} 5.151 + catch (_:IllegalArgumentException) {null} 5.152 + } 5.153 + if (type == null) { 5.154 + http.response.sendError(500) 5.155 + return 5.156 + } 5.157 + val rel = http.param("to")?.toIntOrNull()?.let(dao::findIssue)?.let { 5.158 + IssueRelation( 5.159 + issue, 5.160 + it, 5.161 + type, 5.162 + http.param("reverse")?.toBoolean() ?: false 5.163 + ) 5.164 + } 5.165 + 5.166 + // execute removal, if there is something to remove 5.167 + rel?.run(dao::deleteIssueRelation) 5.168 + 5.169 + // always pretend that the operation was successful - if there was nothing to remove, it's okay 5.170 + http.renderCommit("${issuesHref}${issue.id}") 5.171 + } 5.172 + } 5.173 } 5.174 \ No newline at end of file
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 6.2 +++ b/src/main/kotlin/de/uapcore/lightpit/types/RelationType.kt Fri Dec 30 19:04:34 2022 +0100 6.3 @@ -0,0 +1,37 @@ 6.4 +/* 6.5 + * Copyright 2022 Mike Becker. All rights reserved. 6.6 + * 6.7 + * Redistribution and use in source and binary forms, with or without 6.8 + * modification, are permitted provided that the following conditions are met: 6.9 + * 6.10 + * 1. Redistributions of source code must retain the above copyright 6.11 + * notice, this list of conditions and the following disclaimer. 6.12 + * 6.13 + * 2. Redistributions in binary form must reproduce the above copyright 6.14 + * notice, this list of conditions and the following disclaimer in the 6.15 + * documentation and/or other materials provided with the distribution. 6.16 + * 6.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 6.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 6.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 6.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 6.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 6.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 6.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 6.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 6.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 6.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 6.27 + * 6.28 + */ 6.29 + 6.30 +package de.uapcore.lightpit.types 6.31 + 6.32 +enum class RelationType(val bidi: Boolean) { 6.33 + RelatesTo(true), 6.34 + TogetherWith(true), 6.35 + Before(false), 6.36 + SubtaskOf(false), 6.37 + Blocks(false), 6.38 + Tests(false), 6.39 + Duplicates(false) 6.40 +} 6.41 \ No newline at end of file
7.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Fri Dec 30 13:21:09 2022 +0100 7.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Fri Dec 30 19:04:34 2022 +0100 7.3 @@ -32,10 +32,7 @@ 7.4 import com.vladsch.flexmark.util.data.MutableDataSet 7.5 import com.vladsch.flexmark.util.data.SharedDataKeys 7.6 import de.uapcore.lightpit.entities.* 7.7 -import de.uapcore.lightpit.types.IssueCategory 7.8 -import de.uapcore.lightpit.types.IssueStatus 7.9 -import de.uapcore.lightpit.types.IssueStatusPhase 7.10 -import de.uapcore.lightpit.types.VersionStatus 7.11 +import de.uapcore.lightpit.types.* 7.12 import kotlin.math.roundToInt 7.13 7.14 class IssueSorter(private vararg val criteria: Criteria) : Comparator<Issue> { 7.15 @@ -98,9 +95,18 @@ 7.16 val issue: Issue, 7.17 val comments: List<IssueComment>, 7.18 val project: Project, 7.19 - val version: Version? = null, 7.20 - val component: Component? = null 7.21 + val version: Version?, 7.22 + val component: Component?, 7.23 + projectIssues: List<Issue>, 7.24 + val currentRelations: List<IssueRelation>, 7.25 + /** 7.26 + * Optional resource key to an error message for the relation editor. 7.27 + */ 7.28 + val relationError: String? 7.29 ) : View() { 7.30 + val relationTypes = RelationType.values() 7.31 + val linkableIssues = projectIssues.filterNot { it.id == issue.id } 7.32 + 7.33 private val parser: Parser 7.34 private val renderer: HtmlRenderer 7.35
8.1 --- a/src/main/resources/localization/strings.properties Fri Dec 30 13:21:09 2022 +0100 8.2 +++ b/src/main/resources/localization/strings.properties Fri Dec 30 19:04:34 2022 +0100 8.3 @@ -24,6 +24,7 @@ 8.4 app.changelog=Changelog 8.5 app.license.title=License 8.6 app.name=Lightweight Project and Issue Tracking 8.7 +button.add=Add 8.8 button.back=Back 8.9 button.cancel=Cancel 8.10 button.comment.edit=Edit Comment 8.11 @@ -38,6 +39,7 @@ 8.12 button.okay=OK 8.13 button.project.create=New Project 8.14 button.project.edit=Edit Project 8.15 +button.remove=Remove 8.16 button.user.create=Add Developer 8.17 button.version.create=New Version 8.18 button.version.edit=Edit Version 8.19 @@ -83,6 +85,24 @@ 8.20 issue.description=Description 8.21 issue.eta=ETA 8.22 issue.id=Issue ID 8.23 +issue.relations=Relations 8.24 +issue.relations.issue=Issue 8.25 +issue.relations.target.invalid=Target issue cannot be linked. 8.26 +issue.relations.type=Type 8.27 +issue.relations.type.RelatesTo=relates to 8.28 +issue.relations.type.RelatesTo.rev=relates to 8.29 +issue.relations.type.TogetherWith=resolve together with 8.30 +issue.relations.type.TogetherWith.rev=resolve together with 8.31 +issue.relations.type.Before=resolve before 8.32 +issue.relations.type.Before.rev=resolve after 8.33 +issue.relations.type.SubtaskOf=subtask of 8.34 +issue.relations.type.SubtaskOf.rev=subtask 8.35 +issue.relations.type.Blocks=blocks 8.36 +issue.relations.type.Blocks.rev=blocked by 8.37 +issue.relations.type.Tests=tests 8.38 +issue.relations.type.Tests.rev=tested by 8.39 +issue.relations.type.Duplicates=duplicates 8.40 +issue.relations.type.Duplicates.rev=duplicated by 8.41 issue.resolved-versions=Target 8.42 issue.status.Done=Done 8.43 issue.status.Duplicate=Duplicate
9.1 --- a/src/main/resources/localization/strings_de.properties Fri Dec 30 13:21:09 2022 +0100 9.2 +++ b/src/main/resources/localization/strings_de.properties Fri Dec 30 19:04:34 2022 +0100 9.3 @@ -23,6 +23,8 @@ 9.4 9.5 app.changelog=Versionshistorie 9.6 app.license.title=Lizenz (Englisch) 9.7 +app.name=Lightweight Project and Issue Tracking 9.8 +button.add=Hinzuf\u00fcgen 9.9 button.back=Zur\u00fcck 9.10 button.cancel=Abbrechen 9.11 button.comment.edit=Absenden 9.12 @@ -37,6 +39,7 @@ 9.13 button.okay=OK 9.14 button.project.create=Neues Projekt 9.15 button.project.edit=Projekt Bearbeiten 9.16 +button.remove=Entfernen 9.17 button.user.create=Neuer Entwickler 9.18 button.version.create=Neue Version 9.19 button.version.edit=Version Bearbeiten 9.20 @@ -82,6 +85,24 @@ 9.21 issue.description=Beschreibung 9.22 issue.eta=Zieldatum 9.23 issue.id=Vorgangs-ID 9.24 +issue.relations=Beziehungen 9.25 +issue.relations.issue=Vorgang 9.26 +issue.relations.target.invalid=Vorgang kann nicht verkn\u00fcpft werden. 9.27 +issue.relations.type=Typ 9.28 +issue.relations.type.RelatesTo=verwandt mit 9.29 +issue.relations.type.RelatesTo.rev=verwandt mit 9.30 +issue.relations.type.TogetherWith=l\u00f6se zusammen mit 9.31 +issue.relations.type.TogetherWith.rev=l\u00f6se zusammen mit 9.32 +issue.relations.type.Before=l\u00f6se vor 9.33 +issue.relations.type.Before.rev=l\u00f6se nach 9.34 +issue.relations.type.SubtaskOf=Unteraufgabe von 9.35 +issue.relations.type.SubtaskOf.rev=Unteraufgabe 9.36 +issue.relations.type.Blocks=blockiert 9.37 +issue.relations.type.Blocks.rev=blockiert von 9.38 +issue.relations.type.Tests=testet 9.39 +issue.relations.type.Tests.rev=getestet durch 9.40 +issue.relations.type.Duplicates=Duplikat von 9.41 +issue.relations.type.Duplicates.rev=Duplikat 9.42 issue.resolved-versions=Ziel 9.43 issue.status.Done=Erledigt 9.44 issue.status.Duplicate=Duplikat
10.1 --- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Fri Dec 30 13:21:09 2022 +0100 10.2 +++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Fri Dec 30 19:04:34 2022 +0100 10.3 @@ -27,6 +27,7 @@ 10.4 <h3>Version 1.0 (Vorschau)</h3> 10.5 10.6 <ul> 10.7 + <li>Vorgänge können nun miteinander verlinkt werden.</li> 10.8 <li>Neuer Status: Bereit (als Antwort auf Im Review).</li> 10.9 <li>Mehrfachauswahl für Versionen im Vorgang entfernt.</li> 10.10 <li>RSS Feeds für Projekte hinzugefügt.</li>
11.1 --- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf Fri Dec 30 13:21:09 2022 +0100 11.2 +++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf Fri Dec 30 19:04:34 2022 +0100 11.3 @@ -27,6 +27,7 @@ 11.4 <h3>Version 1.0 (snapshot)</h3> 11.5 11.6 <ul> 11.7 + <li>Add possibility to relate issues.</li> 11.8 <li>Add issue status: Ready (following Review).</li> 11.9 <li>Remove multi selection of versions within an issue.</li> 11.10 <li>Add RSS feeds for projects.</li>
12.1 --- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp Fri Dec 30 13:21:09 2022 +0100 12.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp Fri Dec 30 19:04:34 2022 +0100 12.3 @@ -155,7 +155,73 @@ 12.4 </a> 12.5 </div> 12.6 12.7 -<hr class="comments-separator"/> 12.8 +<hr class="issue-view-separator"/> 12.9 +<h2> 12.10 + <fmt:message key="issue.relations"/> 12.11 +</h2> 12.12 +<form id="relation-form" action="${issuesHref}${issue.id}/relation" method="post"> 12.13 +<c:if test="${not empty viewmodel.relationError}"> 12.14 + <div class="error-box"> 12.15 + <fmt:message key="${viewmodel.relationError}"/> 12.16 + </div> 12.17 +</c:if> 12.18 +<table class="issue-view relation-editor fullwidth"> 12.19 + <colgroup> 12.20 + <col> 12.21 + <col> 12.22 + <col class="fullwidth"> 12.23 + </colgroup> 12.24 + <thead> 12.25 + <tr> 12.26 + <th></th> 12.27 + <th><fmt:message key="issue.relations.type"/></th> 12.28 + <th><fmt:message key="issue.relations.issue"/></th> 12.29 + </tr> 12.30 + </thead> 12.31 + <tbody> 12.32 + <tr> 12.33 + <td> 12.34 + <button type="submit"><fmt:message key="button.add"/></button> 12.35 + </td> 12.36 + <td> 12.37 + <select name="type"> 12.38 + <c:forEach var="type" items="${viewmodel.relationTypes}"> 12.39 + <option value="${type}"><fmt:message key="issue.relations.type.${type}"/></option> 12.40 + <c:if test="${not type.bidi}"> 12.41 + <option value="!${type}"><fmt:message key="issue.relations.type.${type}.rev"/></option> 12.42 + </c:if> 12.43 + </c:forEach> 12.44 + </select> 12.45 + </td> 12.46 + <td> 12.47 + <input name="issue" list="linkable-issues"> 12.48 + <datalist id="linkable-issues"> 12.49 + <c:forEach var="linkableIssue" items="${viewmodel.linkableIssues}"> 12.50 + <option value="#${linkableIssue.id} - <c:out value="${linkableIssue.subject}"/> (<fmt:message key="issue.status.${linkableIssue.status}" />)"></option> 12.51 + </c:forEach> 12.52 + </datalist> 12.53 + </td> 12.54 + </tr> 12.55 + <c:forEach var="relation" items="${viewmodel.currentRelations}"> 12.56 + <tr> 12.57 + <td> 12.58 + <a href="${issuesHref}${issue.id}/removeRelation?to=${relation.to.id}&type=${relation.type}&reverse=${relation.reverse}" class="button submit"> 12.59 + <fmt:message key="button.remove"/> 12.60 + </a> 12.61 + </td> 12.62 + <td><fmt:message key="issue.relations.type.${relation.type}${relation.reverse?'.rev':''}"/></td> 12.63 + <td> 12.64 + <a href="${issuesHref}${relation.to.id}"> 12.65 + #${relation.to.id} - <c:out value="${relation.to.subject}"/> (<fmt:message key="issue.status.${relation.to.status}" />) 12.66 + </a> 12.67 + </td> 12.68 + </tr> 12.69 + </c:forEach> 12.70 + </tbody> 12.71 +</table> 12.72 +</form> 12.73 + 12.74 +<hr class="issue-view-separator"/> 12.75 <h2> 12.76 <fmt:message key="issue.comments"/> 12.77 <c:if test="${not empty viewmodel.comments}">
13.1 --- a/src/main/webapp/WEB-INF/jsp/site.jsp Fri Dec 30 13:21:09 2022 +0100 13.2 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Fri Dec 30 19:04:34 2022 +0100 13.3 @@ -31,7 +31,7 @@ 13.4 <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> 13.5 13.6 <%-- Version suffix for forcing browsers to update the CSS / JS files --%> 13.7 -<c:set scope="page" var="versionSuffiuniversex" value="20210818b"/> 13.8 +<c:set scope="page" var="versionSuffix" value="20221230"/> 13.9 13.10 <%-- Make the base href easily available at request scope --%> 13.11 <c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/>
14.1 --- a/src/main/webapp/issue-editor.js Fri Dec 30 13:21:09 2022 +0100 14.2 +++ b/src/main/webapp/issue-editor.js Fri Dec 30 19:04:34 2022 +0100 14.3 @@ -24,7 +24,7 @@ 14.4 */ 14.5 14.6 /** 14.7 - * Replaces the formatted comment text with an text area. 14.8 + * Replaces the formatted comment text with a text area. 14.9 * 14.10 * @param {number} id the ID of the comment 14.11 */
15.1 --- a/src/main/webapp/projects.css Fri Dec 30 13:21:09 2022 +0100 15.2 +++ b/src/main/webapp/projects.css Fri Dec 30 19:04:34 2022 +0100 15.3 @@ -138,7 +138,7 @@ 15.4 background: darkgray; 15.5 } 15.6 15.7 -hr.comments-separator { 15.8 +hr.issue-view-separator { 15.9 border-image-source: linear-gradient(to right, rgba(60, 60, 60, .1), rgba(96, 96, 96, 1), rgba(60, 60, 60, .1)); 15.10 border-image-slice: 1; 15.11 border-width: thin; 15.12 @@ -189,3 +189,16 @@ 15.13 table.issue-view th { 15.14 white-space: nowrap; 15.15 } 15.16 + 15.17 +table.relation-editor input, 15.18 +table.relation-editor button, 15.19 +table.relation-editor .button { 15.20 + box-sizing: border-box; 15.21 + width: 100%; 15.22 +} 15.23 + 15.24 +table.relation-editor button, 15.25 +table.relation-editor .button { 15.26 + text-align: center; 15.27 + padding: .1em .25em .1em .25em; 15.28 +}