add possibility to show issues w/o version or component - fixes #335

Mon, 30 Oct 2023 14:44:36 +0100

author
Mike Becker <universe@uap-core.de>
date
Mon, 30 Oct 2023 14:44:36 +0100
changeset 292
703591e739f4
parent 291
bcf05cccac6f
child 293
953c757c368f

add possibility to show issues w/o version or component - fixes #335

src/main/kotlin/de/uapcore/lightpit/RequestMapping.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/Issues.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/PathInfos.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.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-form.jsp 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/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-list.jspf file | annotate | diff | comparison | revisions
     1.1 --- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Mon Oct 30 10:06:22 2023 +0100
     1.2 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Mon Oct 30 14:44:36 2023 +0100
     1.3 @@ -26,6 +26,7 @@
     1.4  package de.uapcore.lightpit
     1.5  
     1.6  import de.uapcore.lightpit.dao.DataAccessObject
     1.7 +import de.uapcore.lightpit.entities.HasNode
     1.8  import de.uapcore.lightpit.viewmodel.NavMenu
     1.9  import de.uapcore.lightpit.viewmodel.View
    1.10  import jakarta.servlet.http.HttpServletRequest
    1.11 @@ -38,6 +39,14 @@
    1.12  typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
    1.13  typealias PathParameters = Map<String, String>
    1.14  
    1.15 +sealed class OptionalPathInfo<in T : HasNode>(info: T) {
    1.16 +    class Specific<T: HasNode>(val elem: T) : OptionalPathInfo<T>(elem)
    1.17 +    data object All : OptionalPathInfo<HasNode>(object : HasNode { override val node = "-"})
    1.18 +    data object None : OptionalPathInfo<HasNode>(object : HasNode { override val node = "~"})
    1.19 +    data object NotFound : OptionalPathInfo<HasNode>(object : HasNode { override val node = ""})
    1.20 +    val node = info.node
    1.21 +}
    1.22 +
    1.23  sealed interface ValidationResult<T>
    1.24  class ValidationError<T>(val message: String): ValidationResult<T>
    1.25  class ValidatedValue<T>(val result: T): ValidationResult<T>
    1.26 @@ -173,6 +182,18 @@
    1.27          }
    1.28      }
    1.29  
    1.30 +
    1.31 +    fun <T : HasNode> lookupPathParam(paramName: String, list: List<T>): OptionalPathInfo<T> {
    1.32 +        return when (val node = this.pathParams[paramName]) {
    1.33 +            null -> OptionalPathInfo.All
    1.34 +            "-" -> OptionalPathInfo.All
    1.35 +            "~" -> OptionalPathInfo.None
    1.36 +            else -> list.find { it.node == node }
    1.37 +                ?.let { OptionalPathInfo.Specific(it) }
    1.38 +                ?: OptionalPathInfo.NotFound
    1.39 +        }
    1.40 +    }
    1.41 +
    1.42      val body: String by lazy {
    1.43          request.reader.lineSequence().joinToString("\n")
    1.44      }
     2.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Mon Oct 30 10:06:22 2023 +0100
     2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Mon Oct 30 14:44:36 2023 +0100
     2.3 @@ -74,7 +74,14 @@
     2.4      fun mergeCommitRefs(refs: List<CommitRef>)
     2.5  
     2.6      fun listIssues(project: Project, includeDone: Boolean): List<Issue>
     2.7 -    fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List<Issue>
     2.8 +    fun listIssues(
     2.9 +        project: Project,
    2.10 +        includeDone: Boolean,
    2.11 +        specificVersion: Boolean,
    2.12 +        version: Version?,
    2.13 +        specificComponent: Boolean,
    2.14 +        component: Component?
    2.15 +    ): List<Issue>
    2.16      fun findIssue(id: Int): Issue?
    2.17      fun insertIssue(issue: Issue): Int
    2.18      fun updateIssue(issue: Issue)
     3.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Mon Oct 30 10:06:22 2023 +0100
     3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Mon Oct 30 14:44:36 2023 +0100
     3.3 @@ -557,7 +557,14 @@
     3.4              queryAll { it.extractIssue() }
     3.5          }
     3.6  
     3.7 -    override fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List<Issue> =
     3.8 +    override fun listIssues(
     3.9 +        project: Project,
    3.10 +        includeDone: Boolean,
    3.11 +        specificVersion: Boolean,
    3.12 +        version: Version?,
    3.13 +        specificComponent: Boolean,
    3.14 +        component: Component?
    3.15 +    ): List<Issue> =
    3.16          withStatement(
    3.17              """$issueQuery where i.project = ? and
    3.18                  (? or phase < 2) and
    3.19 @@ -565,21 +572,16 @@
    3.20                  (not ? or component = ?) and (not ? or component is null)
    3.21              """.trimIndent()
    3.22          ) {
    3.23 -            fun <T : Entity> applyFilter(search: T?, fflag: Int, nflag: Int, idcol: Int) {
    3.24 -                if (search == null) {
    3.25 -                    setBoolean(fflag, false)
    3.26 -                    setBoolean(nflag, false)
    3.27 -                    setInt(idcol, 0)
    3.28 -                } else {
    3.29 -                    setBoolean(fflag, true)
    3.30 -                    setBoolean(nflag, false)
    3.31 -                    setInt(idcol, search.id)
    3.32 -                }
    3.33 -            }
    3.34              setInt(1, project.id)
    3.35              setBoolean(2, includeDone)
    3.36 -            applyFilter(version, 3, 5, 4)
    3.37 -            applyFilter(component, 6, 8, 7)
    3.38 +
    3.39 +            setBoolean(3, specificVersion && version != null)
    3.40 +            setInt(4, version?.id ?: 0)
    3.41 +            setBoolean(5, specificVersion && version == null)
    3.42 +
    3.43 +            setBoolean(6, specificComponent && component != null)
    3.44 +            setInt(7, component?.id ?: 0)
    3.45 +            setBoolean(8, specificComponent && component == null)
    3.46  
    3.47              queryAll { it.extractIssue() }
    3.48          }
     4.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Mon Oct 30 10:06:22 2023 +0100
     4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Mon Oct 30 14:44:36 2023 +0100
     4.3 @@ -25,11 +25,8 @@
     4.4  
     4.5  package de.uapcore.lightpit.servlet
     4.6  
     4.7 -import de.uapcore.lightpit.AbstractServlet
     4.8 -import de.uapcore.lightpit.HttpRequest
     4.9 -import de.uapcore.lightpit.boolValidator
    4.10 +import de.uapcore.lightpit.*
    4.11  import de.uapcore.lightpit.dao.DataAccessObject
    4.12 -import de.uapcore.lightpit.dateOptValidator
    4.13  import de.uapcore.lightpit.entities.*
    4.14  import de.uapcore.lightpit.types.*
    4.15  import de.uapcore.lightpit.viewmodel.*
    4.16 @@ -86,53 +83,6 @@
    4.17          }
    4.18      }
    4.19  
    4.20 -    private fun activeProjectNavMenu(
    4.21 -        projects: List<Project>,
    4.22 -        projectInfo: ProjectInfo,
    4.23 -        selectedVersion: Version? = null,
    4.24 -        selectedComponent: Component? = null
    4.25 -    ) =
    4.26 -        projectNavMenu(
    4.27 -            projects,
    4.28 -            projectInfo.versions,
    4.29 -            projectInfo.components,
    4.30 -            projectInfo.project,
    4.31 -            selectedVersion,
    4.32 -            selectedComponent
    4.33 -        )
    4.34 -
    4.35 -    private sealed interface LookupResult<T>
    4.36 -    private class NotFound<T> : LookupResult<T>
    4.37 -    private data class Found<T>(val elem: T?) : LookupResult<T>
    4.38 -
    4.39 -    private fun <T : HasNode> HttpRequest.lookupPathParam(paramName: String, list: List<T>): LookupResult<T> {
    4.40 -        val node = pathParams[paramName]
    4.41 -        return if (node == null || node == "-") {
    4.42 -            Found(null)
    4.43 -        } else {
    4.44 -            val result = list.find { it.node == node }
    4.45 -            if (result == null) {
    4.46 -                NotFound()
    4.47 -            } else {
    4.48 -                Found(result)
    4.49 -            }
    4.50 -        }
    4.51 -    }
    4.52 -
    4.53 -    private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? {
    4.54 -        val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null
    4.55 -
    4.56 -        val versions: List<Version> = dao.listVersions(project)
    4.57 -        val components: List<Component> = dao.listComponents(project)
    4.58 -
    4.59 -        return ProjectInfo(
    4.60 -            project,
    4.61 -            versions,
    4.62 -            components,
    4.63 -            dao.collectIssueSummary(project)
    4.64 -        )
    4.65 -    }
    4.66 -
    4.67      private fun sanitizeNode(name: String): String {
    4.68          val san = name.replace(Regex("[/\\\\]"), "-")
    4.69          return if (san.startsWith(".")) {
    4.70 @@ -144,46 +94,9 @@
    4.71  
    4.72      private fun feedPath(project: Project) = "feed/${project.node}/issues.rss"
    4.73  
    4.74 -    private data class PathInfos(
    4.75 -        val projectInfo: ProjectInfo,
    4.76 -        val version: Version?,
    4.77 -        val component: Component?
    4.78 -    ) {
    4.79 -        val project = projectInfo.project
    4.80 -        val issuesHref by lazyOf("projects/${project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/")
    4.81 -    }
    4.82 -
    4.83 -    private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? {
    4.84 -        val projectInfo = obtainProjectInfo(http, dao)
    4.85 -        if (projectInfo == null) {
    4.86 -            http.response.sendError(404)
    4.87 -            return null
    4.88 -        }
    4.89 -
    4.90 -        val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) {
    4.91 -            is NotFound -> {
    4.92 -                http.response.sendError(404)
    4.93 -                return null
    4.94 -            }
    4.95 -            is Found -> {
    4.96 -                result.elem
    4.97 -            }
    4.98 -        }
    4.99 -        val component = when (val result = http.lookupPathParam("component", projectInfo.components)) {
   4.100 -            is NotFound -> {
   4.101 -                http.response.sendError(404)
   4.102 -                return null
   4.103 -            }
   4.104 -            is Found -> {
   4.105 -                result.elem
   4.106 -            }
   4.107 -        }
   4.108 -
   4.109 -        return PathInfos(projectInfo, version, component)
   4.110 -    }
   4.111 -
   4.112      private fun project(http: HttpRequest, dao: DataAccessObject) {
   4.113 -        withPathInfo(http, dao)?.run {
   4.114 +        withPathInfo(http, dao)?.let {path ->
   4.115 +            val project = path.projectInfo.project
   4.116  
   4.117              val filter = IssueFilter(http)
   4.118  
   4.119 @@ -191,7 +104,12 @@
   4.120  
   4.121              val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap()
   4.122  
   4.123 -            val issues = dao.listIssues(project, filter.includeDone, version, component)
   4.124 +            val specificVersion = path.versionInfo !is OptionalPathInfo.All
   4.125 +            val version = if (path.versionInfo is OptionalPathInfo.Specific) path.versionInfo.elem else null
   4.126 +            val specificComponent = path.componentInfo !is OptionalPathInfo.All
   4.127 +            val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else null
   4.128 +
   4.129 +            val issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component)
   4.130                  .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary))
   4.131                  .filter {
   4.132                      (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) &&
   4.133 @@ -202,14 +120,9 @@
   4.134  
   4.135              with(http) {
   4.136                  pageTitle = project.name
   4.137 -                view = ProjectDetails(projectInfo, issues, filter, version, component)
   4.138 +                view = ProjectDetails(path, issues, filter)
   4.139                  feedPath = feedPath(project)
   4.140 -                navigationMenu = activeProjectNavMenu(
   4.141 -                    dao.listProjects(),
   4.142 -                    projectInfo,
   4.143 -                    version,
   4.144 -                    component
   4.145 -                )
   4.146 +                navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.147                  styleSheets = listOf("projects")
   4.148                  javascript = "project-details"
   4.149                  render("project-details")
   4.150 @@ -218,23 +131,18 @@
   4.151      }
   4.152  
   4.153      private fun projectForm(http: HttpRequest, dao: DataAccessObject) {
   4.154 +        http.styleSheets = listOf("projects")
   4.155          if (!http.pathParams.containsKey("project")) {
   4.156              http.view = ProjectEditView(Project(-1), dao.listUsers())
   4.157              http.navigationMenu = projectNavMenu(dao.listProjects())
   4.158 +            http.render("project-form")
   4.159          } else {
   4.160 -            val projectInfo = obtainProjectInfo(http, dao)
   4.161 -            if (projectInfo == null) {
   4.162 -                http.response.sendError(404)
   4.163 -                return
   4.164 +            withPathInfo(http, dao)?.let { path ->
   4.165 +                http.view = ProjectEditView(path.projectInfo.project, dao.listUsers())
   4.166 +                http.navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.167 +                http.render("project-form")
   4.168              }
   4.169 -            http.view = ProjectEditView(projectInfo.project, dao.listUsers())
   4.170 -            http.navigationMenu = activeProjectNavMenu(
   4.171 -                dao.listProjects(),
   4.172 -                projectInfo
   4.173 -            )
   4.174          }
   4.175 -        http.styleSheets = listOf("projects")
   4.176 -        http.render("project-form")
   4.177      }
   4.178  
   4.179      private fun projectCommit(http: HttpRequest, dao: DataAccessObject) {
   4.180 @@ -264,77 +172,50 @@
   4.181      }
   4.182  
   4.183      private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) {
   4.184 -        val projectInfo = obtainProjectInfo(http, dao)
   4.185 -        if (projectInfo == null) {
   4.186 -            http.response.sendError(404)
   4.187 -            return
   4.188 +        withPathInfo(http, dao)?.let { path ->
   4.189 +            // if analysis is not configured, reject the request
   4.190 +            if (path.projectInfo.project.vcs == VcsType.None) {
   4.191 +                http.response.sendError(404)
   4.192 +                return
   4.193 +            }
   4.194 +
   4.195 +            // obtain the list of issues for this project to filter cross-project references
   4.196 +            val knownIds = dao.listIssues(path.projectInfo.project, true).map { it.id }
   4.197 +
   4.198 +            // read the provided commit log and merge only the refs that relate issues from the current project
   4.199 +            dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) })
   4.200          }
   4.201 -
   4.202 -        // if analysis is not configured, reject the request
   4.203 -        if (projectInfo.project.vcs == VcsType.None) {
   4.204 -            http.response.sendError(404)
   4.205 -            return
   4.206 -        }
   4.207 -
   4.208 -        // obtain the list of issues for this project to filter cross-project references
   4.209 -        val knownIds = dao.listIssues(projectInfo.project, true).map { it.id }
   4.210 -
   4.211 -        // read the provided commit log and merge only the refs that relate issues from the current project
   4.212 -        dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) })
   4.213      }
   4.214  
   4.215      private fun versions(http: HttpRequest, dao: DataAccessObject) {
   4.216 -        val projectInfo = obtainProjectInfo(http, dao)
   4.217 -        if (projectInfo == null) {
   4.218 -            http.response.sendError(404)
   4.219 -            return
   4.220 -        }
   4.221 -
   4.222 -        with(http) {
   4.223 -            pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.versions")}"
   4.224 -            view = VersionsView(
   4.225 -                projectInfo,
   4.226 -                dao.listVersionSummaries(projectInfo.project)
   4.227 -            )
   4.228 -            feedPath = feedPath(projectInfo.project)
   4.229 -            navigationMenu = activeProjectNavMenu(
   4.230 -                dao.listProjects(),
   4.231 -                projectInfo
   4.232 -            )
   4.233 -            styleSheets = listOf("projects")
   4.234 -            javascript = "project-details"
   4.235 -            render("versions")
   4.236 +        withPathInfo(http, dao)?.let { path ->
   4.237 +            with(http) {
   4.238 +                pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.versions")}"
   4.239 +                view = VersionsView(
   4.240 +                    path.projectInfo,
   4.241 +                    dao.listVersionSummaries(path.projectInfo.project)
   4.242 +                )
   4.243 +                feedPath = feedPath(path.projectInfo.project)
   4.244 +                navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.245 +                styleSheets = listOf("projects")
   4.246 +                javascript = "project-details"
   4.247 +                render("versions")
   4.248 +            }
   4.249          }
   4.250      }
   4.251  
   4.252      private fun versionForm(http: HttpRequest, dao: DataAccessObject) {
   4.253 -        val projectInfo = obtainProjectInfo(http, dao)
   4.254 -        if (projectInfo == null) {
   4.255 -            http.response.sendError(404)
   4.256 -            return
   4.257 -        }
   4.258 +        withPathInfo(http, dao)?.let { path ->
   4.259 +            val version = if (path.versionInfo is OptionalPathInfo.Specific)
   4.260 +                path.versionInfo.elem else Version(-1, path.projectInfo.project.id)
   4.261  
   4.262 -        val version: Version
   4.263 -        when (val result = http.lookupPathParam("version", projectInfo.versions)) {
   4.264 -            is NotFound -> {
   4.265 -                http.response.sendError(404)
   4.266 -                return
   4.267 +            with(http) {
   4.268 +                view = VersionEditView(path.projectInfo, version)
   4.269 +                feedPath = feedPath(path.projectInfo.project)
   4.270 +                navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.271 +                styleSheets = listOf("projects")
   4.272 +                render("version-form")
   4.273              }
   4.274 -            is Found -> {
   4.275 -                version = result.elem ?: Version(-1, projectInfo.project.id)
   4.276 -            }
   4.277 -        }
   4.278 -
   4.279 -        with(http) {
   4.280 -            view = VersionEditView(projectInfo, version)
   4.281 -            feedPath = feedPath(projectInfo.project)
   4.282 -            navigationMenu = activeProjectNavMenu(
   4.283 -                dao.listProjects(),
   4.284 -                projectInfo,
   4.285 -                selectedVersion = version
   4.286 -            )
   4.287 -            styleSheets = listOf("projects")
   4.288 -            render("version-form")
   4.289          }
   4.290      }
   4.291  
   4.292 @@ -385,57 +266,34 @@
   4.293      }
   4.294  
   4.295      private fun components(http: HttpRequest, dao: DataAccessObject) {
   4.296 -        val projectInfo = obtainProjectInfo(http, dao)
   4.297 -        if (projectInfo == null) {
   4.298 -            http.response.sendError(404)
   4.299 -            return
   4.300 -        }
   4.301 -
   4.302 -        with(http) {
   4.303 -            pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.components")}"
   4.304 -            view = ComponentsView(
   4.305 -                projectInfo,
   4.306 -                dao.listComponentSummaries(projectInfo.project)
   4.307 -            )
   4.308 -            feedPath = feedPath(projectInfo.project)
   4.309 -            navigationMenu = activeProjectNavMenu(
   4.310 -                dao.listProjects(),
   4.311 -                projectInfo
   4.312 -            )
   4.313 -            styleSheets = listOf("projects")
   4.314 -            javascript = "project-details"
   4.315 -            render("components")
   4.316 +        withPathInfo(http, dao)?.let { path ->
   4.317 +            with(http) {
   4.318 +                pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.components")}"
   4.319 +                view = ComponentsView(
   4.320 +                    path.projectInfo,
   4.321 +                    dao.listComponentSummaries(path.projectInfo.project)
   4.322 +                )
   4.323 +                feedPath = feedPath(path.projectInfo.project)
   4.324 +                navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.325 +                styleSheets = listOf("projects")
   4.326 +                javascript = "project-details"
   4.327 +                render("components")
   4.328 +            }
   4.329          }
   4.330      }
   4.331  
   4.332      private fun componentForm(http: HttpRequest, dao: DataAccessObject) {
   4.333 -        val projectInfo = obtainProjectInfo(http, dao)
   4.334 -        if (projectInfo == null) {
   4.335 -            http.response.sendError(404)
   4.336 -            return
   4.337 -        }
   4.338 +        withPathInfo(http, dao)?.let { path ->
   4.339 +            val component = if (path.componentInfo is OptionalPathInfo.Specific)
   4.340 +                path.componentInfo.elem else Component(-1, path.projectInfo.project.id)
   4.341  
   4.342 -        val component: Component
   4.343 -        when (val result = http.lookupPathParam("component", projectInfo.components)) {
   4.344 -            is NotFound -> {
   4.345 -                http.response.sendError(404)
   4.346 -                return
   4.347 +            with(http) {
   4.348 +                view = ComponentEditView(path.projectInfo, component, dao.listUsers())
   4.349 +                feedPath = feedPath(path.projectInfo.project)
   4.350 +                navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.351 +                styleSheets = listOf("projects")
   4.352 +                render("component-form")
   4.353              }
   4.354 -            is Found -> {
   4.355 -                component = result.elem ?: Component(-1, projectInfo.project.id)
   4.356 -            }
   4.357 -        }
   4.358 -
   4.359 -        with(http) {
   4.360 -            view = ComponentEditView(projectInfo, component, dao.listUsers())
   4.361 -            feedPath = feedPath(projectInfo.project)
   4.362 -            navigationMenu = activeProjectNavMenu(
   4.363 -                dao.listProjects(),
   4.364 -                projectInfo,
   4.365 -                selectedComponent = component
   4.366 -            )
   4.367 -            styleSheets = listOf("projects")
   4.368 -            render("component-form")
   4.369          }
   4.370      }
   4.371  
   4.372 @@ -484,29 +342,23 @@
   4.373          issue: Issue,
   4.374          relationError: String? = null
   4.375      ) {
   4.376 -        withPathInfo(http, dao)?.run {
   4.377 +        withPathInfo(http, dao)?.let {path ->
   4.378              val comments = dao.listComments(issue)
   4.379  
   4.380              with(http) {
   4.381 -                pageTitle = "${projectInfo.project.name}: #${issue.id} ${issue.subject}"
   4.382 +                pageTitle = "${path.projectInfo.project.name}: #${issue.id} ${issue.subject}"
   4.383                  view = IssueDetailView(
   4.384 +                    path,
   4.385                      issue,
   4.386                      comments,
   4.387 -                    project,
   4.388 -                    version,
   4.389 -                    component,
   4.390 -                    dao.listIssues(project, true),
   4.391 +                    path.projectInfo.project,
   4.392 +                    dao.listIssues(path.projectInfo.project, true),
   4.393                      dao.listIssueRelations(issue),
   4.394                      relationError,
   4.395                      dao.listCommitRefs(issue)
   4.396                  )
   4.397 -                feedPath = feedPath(projectInfo.project)
   4.398 -                navigationMenu = activeProjectNavMenu(
   4.399 -                    dao.listProjects(),
   4.400 -                    projectInfo,
   4.401 -                    version,
   4.402 -                    component
   4.403 -                )
   4.404 +                feedPath = feedPath(path.projectInfo.project)
   4.405 +                navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.406                  styleSheets = listOf("projects")
   4.407                  javascript = "issue-editor"
   4.408                  render("issue-view")
   4.409 @@ -515,23 +367,25 @@
   4.410      }
   4.411  
   4.412      private fun issueForm(http: HttpRequest, dao: DataAccessObject) {
   4.413 -        withPathInfo(http, dao)?.run {
   4.414 +        withPathInfo(http, dao)?.let { path ->
   4.415              val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue(
   4.416                  -1,
   4.417 -                project,
   4.418 +                path.projectInfo.project,
   4.419              )
   4.420  
   4.421              // for new issues set some defaults
   4.422              if (issue.id < 0) {
   4.423                  // pre-select component, if available in the path info
   4.424 -                issue.component = component
   4.425 +                if (path.componentInfo is OptionalPathInfo.Specific) {
   4.426 +                    issue.component = path.componentInfo.elem
   4.427 +                }
   4.428  
   4.429                  // pre-select version, if available in the path info
   4.430 -                if (version != null) {
   4.431 -                    if (version.status.isReleased) {
   4.432 -                        issue.affected = version
   4.433 +                if (path.versionInfo is OptionalPathInfo.Specific) {
   4.434 +                    if (path.versionInfo.elem.status.isReleased) {
   4.435 +                        issue.affected = path.versionInfo.elem
   4.436                      } else {
   4.437 -                        issue.resolved = version
   4.438 +                        issue.resolved = path.versionInfo.elem
   4.439                      }
   4.440                  }
   4.441              }
   4.442 @@ -539,20 +393,14 @@
   4.443              with(http) {
   4.444                  view = IssueEditView(
   4.445                      issue,
   4.446 -                    projectInfo.versions,
   4.447 -                    projectInfo.components,
   4.448 +                    path.projectInfo.versions,
   4.449 +                    path.projectInfo.components,
   4.450                      dao.listUsers(),
   4.451 -                    project,
   4.452 -                    version,
   4.453 -                    component
   4.454 +                    path.projectInfo.project,
   4.455 +                    path
   4.456                  )
   4.457 -                feedPath = feedPath(projectInfo.project)
   4.458 -                navigationMenu = activeProjectNavMenu(
   4.459 -                    dao.listProjects(),
   4.460 -                    projectInfo,
   4.461 -                    version,
   4.462 -                    component
   4.463 -                )
   4.464 +                feedPath = feedPath(path.projectInfo.project)
   4.465 +                navigationMenu = projectNavMenu(dao.listProjects(), path)
   4.466                  styleSheets = listOf("projects")
   4.467                  javascript = "issue-editor"
   4.468                  render("issue-form")
   4.469 @@ -606,7 +454,7 @@
   4.470          withPathInfo(http, dao)?.run {
   4.471              val issue = Issue(
   4.472                  http.param("id")?.toIntOrNull() ?: -1,
   4.473 -                project
   4.474 +                projectInfo.project
   4.475              ).apply {
   4.476                  component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1)
   4.477                  category = IssueCategory.valueOf(http.param("category") ?: "")
     5.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Mon Oct 30 10:06:22 2023 +0100
     5.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Mon Oct 30 14:44:36 2023 +0100
     5.3 @@ -102,11 +102,10 @@
     5.4  data class CommitLink(val url: String, val hash: String, val message: String)
     5.5  
     5.6  class IssueDetailView(
     5.7 +    val pathInfos: PathInfos,
     5.8      val issue: Issue,
     5.9      val comments: List<IssueComment>,
    5.10      val project: Project,
    5.11 -    val version: Version?,
    5.12 -    val component: Component?,
    5.13      projectIssues: List<Issue>,
    5.14      val currentRelations: List<IssueRelation>,
    5.15      /**
    5.16 @@ -168,8 +167,7 @@
    5.17      val components: List<Component>,
    5.18      val users: List<User>,
    5.19      val project: Project, // TODO: allow null values to create issues from the IssuesServlet
    5.20 -    val version: Version? = null,
    5.21 -    val component: Component? = null
    5.22 +    val pathInfos: PathInfos
    5.23  ) : EditView() {
    5.24  
    5.25      val versionsUpcoming: List<Version>
     6.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt	Mon Oct 30 10:06:22 2023 +0100
     6.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt	Mon Oct 30 14:44:36 2023 +0100
     6.3 @@ -25,6 +25,7 @@
     6.4  
     6.5  package de.uapcore.lightpit.viewmodel
     6.6  
     6.7 +import de.uapcore.lightpit.OptionalPathInfo
     6.8  import de.uapcore.lightpit.entities.Component
     6.9  import de.uapcore.lightpit.entities.Project
    6.10  import de.uapcore.lightpit.entities.Version
    6.11 @@ -44,19 +45,26 @@
    6.12  
    6.13  class NavMenu(val entries: List<NavMenuEntry>)
    6.14  
    6.15 -fun projectNavMenu(
    6.16 -    projects: List<Project>,
    6.17 -    versions: List<Version> = emptyList(),
    6.18 -    components: List<Component> = emptyList(),
    6.19 -    selectedProject: Project? = null,
    6.20 -    selectedVersion: Version? = null,
    6.21 -    selectedComponent: Component? = null
    6.22 -) = NavMenu(
    6.23 +fun projectNavMenu(projects: List<Project>) = NavMenu(
    6.24      sequence {
    6.25 -        val cnode = selectedComponent?.node ?: "-"
    6.26 -        val vnode = selectedVersion?.node ?: "-"
    6.27          for (project in projects) {
    6.28 -            val active = project == selectedProject
    6.29 +            yield(
    6.30 +                NavMenuEntry(
    6.31 +                    level = 0,
    6.32 +                    caption = project.name,
    6.33 +                    href = "projects/${project.node}",
    6.34 +                )
    6.35 +            )
    6.36 +        }
    6.37 +    }.toList()
    6.38 +)
    6.39 +
    6.40 +fun projectNavMenu(projects: List<Project>, pathInfos: PathInfos) = NavMenu(
    6.41 +    sequence {
    6.42 +        val cnode = pathInfos.componentInfo.node
    6.43 +        val vnode = pathInfos.versionInfo.node
    6.44 +        for (project in projects) {
    6.45 +            val active = project == pathInfos.projectInfo.project
    6.46              yield(
    6.47                  NavMenuEntry(
    6.48                      level = 0,
    6.49 @@ -80,10 +88,22 @@
    6.50                          caption = "navmenu.all",
    6.51                          resolveCaption = true,
    6.52                          href = "projects/${project.node}/issues/-/${cnode}/",
    6.53 -                        iconColor = "#000000"
    6.54 +                        iconColor = "#000000",
    6.55 +                        active = vnode == "-",
    6.56                      )
    6.57                  )
    6.58 -                for (version in versions.filter { it.status != VersionStatus.Deprecated }) {
    6.59 +                yield(
    6.60 +                    NavMenuEntry(
    6.61 +                        level = 2,
    6.62 +                        caption = "navmenu.none",
    6.63 +                        resolveCaption = true,
    6.64 +                        href = "projects/${project.node}/issues/~/${cnode}/",
    6.65 +                        iconColor = "#000000",
    6.66 +                        active = vnode == "~",
    6.67 +                    )
    6.68 +                )
    6.69 +                for (version in pathInfos.projectInfo.versions) {
    6.70 +                    if (version.status == VersionStatus.Deprecated && vnode != version.node) continue
    6.71                      yield(
    6.72                          NavMenuEntry(
    6.73                              level = 2,
    6.74 @@ -91,7 +111,7 @@
    6.75                              title = "version.status.${version.status}",
    6.76                              href = "projects/${project.node}/issues/${version.node}/${cnode}/",
    6.77                              iconColor = "version-${version.status}",
    6.78 -                            active = version == selectedVersion
    6.79 +                            active = version.node == vnode
    6.80                          )
    6.81                      )
    6.82                  }
    6.83 @@ -109,18 +129,29 @@
    6.84                          caption = "navmenu.all",
    6.85                          resolveCaption = true,
    6.86                          href = "projects/${project.node}/issues/${vnode}/-/",
    6.87 -                        iconColor = "#000000"
    6.88 +                        iconColor = "#000000",
    6.89 +                        active = cnode == "-",
    6.90                      )
    6.91                  )
    6.92 -                for (component in components) {
    6.93 -                    if (!component.active && component != selectedComponent) continue
    6.94 +                yield(
    6.95 +                    NavMenuEntry(
    6.96 +                        level = 2,
    6.97 +                        caption = "navmenu.none",
    6.98 +                        resolveCaption = true,
    6.99 +                        href = "projects/${project.node}/issues/${vnode}/~/",
   6.100 +                        iconColor = "#000000",
   6.101 +                        active = cnode == "~",
   6.102 +                    )
   6.103 +                )
   6.104 +                for (component in pathInfos.projectInfo.components) {
   6.105 +                    if (!component.active && component.node != cnode) continue
   6.106                      yield(
   6.107                          NavMenuEntry(
   6.108                              level = 2,
   6.109                              caption = component.name,
   6.110                              href = "projects/${project.node}/issues/${vnode}/${component.node}/",
   6.111                              iconColor = "${component.color}",
   6.112 -                            active = component == selectedComponent
   6.113 +                            active = component.node == cnode
   6.114                          )
   6.115                      )
   6.116                  }
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/PathInfos.kt	Mon Oct 30 14:44:36 2023 +0100
     7.3 @@ -0,0 +1,73 @@
     7.4 +/*
     7.5 + * Copyright 2023 Mike Becker. All rights reserved.
     7.6 + *
     7.7 + * Redistribution and use in source and binary forms, with or without
     7.8 + * modification, are permitted provided that the following conditions are met:
     7.9 + *
    7.10 + * 1. Redistributions of source code must retain the above copyright
    7.11 + * notice, this list of conditions and the following disclaimer.
    7.12 + *
    7.13 + * 2. Redistributions in binary form must reproduce the above copyright
    7.14 + * notice, this list of conditions and the following disclaimer in the
    7.15 + * documentation and/or other materials provided with the distribution.
    7.16 + *
    7.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    7.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    7.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    7.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    7.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    7.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    7.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    7.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    7.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    7.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    7.27 + */
    7.28 +
    7.29 +package de.uapcore.lightpit.viewmodel
    7.30 +
    7.31 +import de.uapcore.lightpit.HttpRequest
    7.32 +import de.uapcore.lightpit.OptionalPathInfo
    7.33 +import de.uapcore.lightpit.dao.DataAccessObject
    7.34 +import de.uapcore.lightpit.entities.Component
    7.35 +import de.uapcore.lightpit.entities.Version
    7.36 +
    7.37 +data class PathInfos(
    7.38 +    val projectInfo: ProjectInfo,
    7.39 +    val versionInfo: OptionalPathInfo<Version>,
    7.40 +    val componentInfo: OptionalPathInfo<Component>
    7.41 +) {
    7.42 +    val issuesHref by lazyOf("projects/${projectInfo.project.node}/issues/${versionInfo.node}/${componentInfo.node}/")
    7.43 +}
    7.44 +
    7.45 +private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? {
    7.46 +    val pathParam = http.pathParams["project"] ?: return null
    7.47 +    val project = dao.findProjectByNode(pathParam) ?: return null
    7.48 +
    7.49 +    val versions: List<Version> = dao.listVersions(project)
    7.50 +    val components: List<Component> = dao.listComponents(project)
    7.51 +
    7.52 +    return ProjectInfo(
    7.53 +        project,
    7.54 +        versions,
    7.55 +        components,
    7.56 +        dao.collectIssueSummary(project)
    7.57 +    )
    7.58 +}
    7.59 +
    7.60 +fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? {
    7.61 +    val projectInfo = obtainProjectInfo(http, dao)
    7.62 +    if (projectInfo == null) {
    7.63 +        http.response.sendError(404)
    7.64 +        return null
    7.65 +    }
    7.66 +
    7.67 +    val version = http.lookupPathParam("version", projectInfo.versions)
    7.68 +    val component = http.lookupPathParam("component", projectInfo.components)
    7.69 +
    7.70 +    if (version == OptionalPathInfo.NotFound || component == OptionalPathInfo.NotFound) {
    7.71 +        http.response.sendError(404)
    7.72 +        return null
    7.73 +    }
    7.74 +
    7.75 +    return PathInfos(projectInfo, version, component)
    7.76 +}
     8.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt	Mon Oct 30 10:06:22 2023 +0100
     8.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt	Mon Oct 30 14:44:36 2023 +0100
     8.3 @@ -25,6 +25,7 @@
     8.4  
     8.5  package de.uapcore.lightpit.viewmodel
     8.6  
     8.7 +import de.uapcore.lightpit.OptionalPathInfo
     8.8  import de.uapcore.lightpit.entities.*
     8.9  
    8.10  class ProjectInfo(
    8.11 @@ -45,18 +46,25 @@
    8.12  ) : View()
    8.13  
    8.14  class ProjectDetails(
    8.15 -    val projectInfo: ProjectInfo,
    8.16 +    val pathInfos: PathInfos,
    8.17      val issues: List<Issue>,
    8.18      val filter: IssueFilter,
    8.19 -    val version: Version? = null,
    8.20 -    val component: Component? = null
    8.21  ) : View() {
    8.22 +    val projectInfo = pathInfos.projectInfo
    8.23      val issueSummary = IssueSummary()
    8.24      val versionInfo: VersionInfo?
    8.25 +    val componentDetails: Component?
    8.26  
    8.27      init {
    8.28          issues.forEach(issueSummary::add)
    8.29 -        versionInfo = version?.let { VersionInfo(it, issues) }
    8.30 +        versionInfo = when (val vinfo = pathInfos.versionInfo){
    8.31 +            is OptionalPathInfo.Specific -> VersionInfo(vinfo.elem, issues)
    8.32 +            else -> null
    8.33 +        }
    8.34 +        componentDetails = when (val cinfo = pathInfos.componentInfo){
    8.35 +            is OptionalPathInfo.Specific -> cinfo.elem
    8.36 +            else -> null
    8.37 +        }
    8.38      }
    8.39  }
    8.40  
     9.1 --- a/src/main/resources/localization/strings.properties	Mon Oct 30 10:06:22 2023 +0100
     9.2 +++ b/src/main/resources/localization/strings.properties	Mon Oct 30 14:44:36 2023 +0100
     9.3 @@ -141,6 +141,7 @@
     9.4  menu.users=Developer
     9.5  navmenu.all=all
     9.6  navmenu.components=Components
     9.7 +navmenu.none=none
     9.8  navmenu.versions=Versions
     9.9  no-projects=Welcome to LightPIT. Start off by creating a new project!
    9.10  no-users=No developers have been configured yet.
    10.1 --- a/src/main/resources/localization/strings_de.properties	Mon Oct 30 10:06:22 2023 +0100
    10.2 +++ b/src/main/resources/localization/strings_de.properties	Mon Oct 30 14:44:36 2023 +0100
    10.3 @@ -141,6 +141,7 @@
    10.4  menu.users=Entwickler
    10.5  navmenu.all=Alle
    10.6  navmenu.components=Komponenten
    10.7 +navmenu.none=Keine
    10.8  navmenu.versions=Versionen
    10.9  no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes!
   10.10  no-users=Bislang wurden keine Entwickler hinterlegt.
    11.1 --- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Mon Oct 30 10:06:22 2023 +0100
    11.2 +++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Mon Oct 30 14:44:36 2023 +0100
    11.3 @@ -24,6 +24,16 @@
    11.4    --%>
    11.5  <%@ page contentType="text/html;charset=UTF-8" %>
    11.6  
    11.7 +<h3>Version 1.2 (Vorschau)</h3>
    11.8 +
    11.9 +<ul>
   11.10 +    <li>
   11.11 +        Im Seitenmenü können nun alle Vorgänge gewählt werden, die
   11.12 +        keiner Version oder Komponente zugeordnet sind.
   11.13 +    </li>
   11.14 +    <li>Einige kleinere Fehlerbehebungen im Zusammenhang mit dem Seitenmenü.</li>
   11.15 +</ul>
   11.16 +
   11.17  <h3>Version 1.1.2</h3>
   11.18  
   11.19  <ul>
    12.1 --- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Mon Oct 30 10:06:22 2023 +0100
    12.2 +++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Mon Oct 30 14:44:36 2023 +0100
    12.3 @@ -24,6 +24,16 @@
    12.4    --%>
    12.5  <%@ page contentType="text/html;charset=UTF-8" %>
    12.6  
    12.7 +<h3>Version 1.2 (snapshot)</h3>
    12.8 +
    12.9 +<ul>
   12.10 +    <li>
   12.11 +        The left menu now allows selection of issues without assigned
   12.12 +        version or component.
   12.13 +    </li>
   12.14 +    <li>Several minor bugfixes regarding the left menu.</li>
   12.15 +</ul>
   12.16 +
   12.17  <h3>Version 1.1.2</h3>
   12.18  
   12.19  <ul>
    13.1 --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Mon Oct 30 10:06:22 2023 +0100
    13.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Mon Oct 30 14:44:36 2023 +0100
    13.3 @@ -32,10 +32,7 @@
    13.4  
    13.5  <c:set var="issue" scope="page" value="${viewmodel.issue}" />
    13.6  <c:set var="project" scope="page" value="${viewmodel.project}"/>
    13.7 -<c:set var="component" scope="page" value="${viewmodel.component}"/>
    13.8 -<c:set var="version" scope="page" value="${viewmodel.version}"/>
    13.9 -
   13.10 -<c:set var="issuesHref" value="./projects/${project.node}/issues/${empty version ? '-' : version.node }/${empty component ? '-' : component.node}/"/>
   13.11 +<c:set var="issuesHref" value="./${viewmodel.pathInfos.issuesHref}"/>
   13.12  
   13.13  <form action="${issuesHref}-/commit" method="post">
   13.14      <input type="hidden" name="project" value="${issue.project.id}" />
    14.1 --- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Mon Oct 30 10:06:22 2023 +0100
    14.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Mon Oct 30 14:44:36 2023 +0100
    14.3 @@ -32,11 +32,9 @@
    14.4  <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueDetailView" scope="request"/>
    14.5  
    14.6  <c:set var="project" scope="page" value="${viewmodel.project}"/>
    14.7 -<c:set var="component" scope="page" value="${viewmodel.component}"/>
    14.8 -<c:set var="version" scope="page" value="${viewmodel.version}"/>
    14.9  <c:set var="issue" scope="page" value="${viewmodel.issue}" />
   14.10  
   14.11 -<c:set var="issuesHref" scope="page" value="./projects/${project.node}/issues/${empty version ? '-' : version.node }/${empty component ? '-' : component.node}/"/>
   14.12 +<c:set var="issuesHref" scope="page" value="./${viewmodel.pathInfos.issuesHref}"/>
   14.13  
   14.14  <table class="issue-view fullwidth">
   14.15      <colgroup>
    15.1 --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Mon Oct 30 10:06:22 2023 +0100
    15.2 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Mon Oct 30 14:44:36 2023 +0100
    15.3 @@ -31,12 +31,12 @@
    15.4  <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectDetails" scope="request" />
    15.5  
    15.6  <c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
    15.7 -<c:set var="component" scope="page" value="${viewmodel.component}"/>
    15.8 -<c:set var="version" scope="page" value="${viewmodel.version}"/>
    15.9 +<c:set var="component" scope="page" value="${viewmodel.componentDetails}"/>
   15.10 +<c:set var="issuesHref" value="./${viewmodel.pathInfos.issuesHref}"/>
   15.11  <%@include file="../jspf/project-header.jspf"%>
   15.12  
   15.13  <div>
   15.14 -    <a href="./projects/${project.node}/issues/${empty version ? '-' : version.node}/${empty component ? '-' : component.node}/-/create" class="button"><fmt:message key="button.issue.create"/></a>
   15.15 +    <a href=".${issuesHref}-/create" class="button"><fmt:message key="button.issue.create"/></a>
   15.16      <button onclick="toggleProjectDetails()" id="toggle-details-button"><fmt:message key="button.project.details"/></button>
   15.17  </div>
   15.18  
    16.1 --- a/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Mon Oct 30 10:06:22 2023 +0100
    16.2 +++ b/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Mon Oct 30 14:44:36 2023 +0100
    16.3 @@ -1,7 +1,6 @@
    16.4  <%--
    16.5  issues: List<Issue>
    16.6 -version: Version?
    16.7 -component: Component?
    16.8 +issuesHref: String
    16.9  --%>
   16.10  <table class="fullwidth datatable medskip">
   16.11      <colgroup>
   16.12 @@ -23,7 +22,7 @@
   16.13          <tr>
   16.14              <td>
   16.15                  <span class="phase-${issue.status.phase.number}">
   16.16 -                    <a href="./projects/${issue.project.node}/issues/${empty version ? '-' : version.node }/${empty component ? '-' : component.node}/${issue.id}">
   16.17 +                    <a href="./${issuesHref}${issue.id}">
   16.18                          #${issue.id}&nbsp;-&nbsp;<c:out value="${issue.subject}" />
   16.19                      </a>
   16.20                  </span>

mercurial