#29 add possibility to relate issues

Fri, 30 Dec 2022 19:04:34 +0100

author
Mike Becker <universe@uap-core.de>
date
Fri, 30 Dec 2022 19:04:34 +0100
changeset 263
aa22103809cd
parent 262
c357c4e69b9e
child 264
7d67245e5121

#29 add possibility to relate issues

setup/postgres/psql_create_tables.sql 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/entities/IssueRelation.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/types/RelationType.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.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/issue-view.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/issue-editor.js file | annotate | diff | comparison | revisions
src/main/webapp/projects.css file | annotate | diff | comparison | revisions
     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 +}

mercurial