# HG changeset patch # User Mike Becker # Date 1698673476 -3600 # Node ID 703591e739f431a71cf280516d5f7bb9d7fc0c03 # Parent bcf05cccac6f803ab292b1bab82fc3451379299e add possibility to show issues w/o version or component - fixes #335 diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt --- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Mon Oct 30 14:44:36 2023 +0100 @@ -26,6 +26,7 @@ package de.uapcore.lightpit import de.uapcore.lightpit.dao.DataAccessObject +import de.uapcore.lightpit.entities.HasNode import de.uapcore.lightpit.viewmodel.NavMenu import de.uapcore.lightpit.viewmodel.View import jakarta.servlet.http.HttpServletRequest @@ -38,6 +39,14 @@ typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit typealias PathParameters = Map +sealed class OptionalPathInfo(info: T) { + class Specific(val elem: T) : OptionalPathInfo(elem) + data object All : OptionalPathInfo(object : HasNode { override val node = "-"}) + data object None : OptionalPathInfo(object : HasNode { override val node = "~"}) + data object NotFound : OptionalPathInfo(object : HasNode { override val node = ""}) + val node = info.node +} + sealed interface ValidationResult class ValidationError(val message: String): ValidationResult class ValidatedValue(val result: T): ValidationResult @@ -173,6 +182,18 @@ } } + + fun lookupPathParam(paramName: String, list: List): OptionalPathInfo { + return when (val node = this.pathParams[paramName]) { + null -> OptionalPathInfo.All + "-" -> OptionalPathInfo.All + "~" -> OptionalPathInfo.None + else -> list.find { it.node == node } + ?.let { OptionalPathInfo.Specific(it) } + ?: OptionalPathInfo.NotFound + } + } + val body: String by lazy { request.reader.lineSequence().joinToString("\n") } diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Mon Oct 30 14:44:36 2023 +0100 @@ -74,7 +74,14 @@ fun mergeCommitRefs(refs: List) fun listIssues(project: Project, includeDone: Boolean): List - fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List + fun listIssues( + project: Project, + includeDone: Boolean, + specificVersion: Boolean, + version: Version?, + specificComponent: Boolean, + component: Component? + ): List fun findIssue(id: Int): Issue? fun insertIssue(issue: Issue): Int fun updateIssue(issue: Issue) diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Mon Oct 30 14:44:36 2023 +0100 @@ -557,7 +557,14 @@ queryAll { it.extractIssue() } } - override fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List = + override fun listIssues( + project: Project, + includeDone: Boolean, + specificVersion: Boolean, + version: Version?, + specificComponent: Boolean, + component: Component? + ): List = withStatement( """$issueQuery where i.project = ? and (? or phase < 2) and @@ -565,21 +572,16 @@ (not ? or component = ?) and (not ? or component is null) """.trimIndent() ) { - fun applyFilter(search: T?, fflag: Int, nflag: Int, idcol: Int) { - if (search == null) { - setBoolean(fflag, false) - setBoolean(nflag, false) - setInt(idcol, 0) - } else { - setBoolean(fflag, true) - setBoolean(nflag, false) - setInt(idcol, search.id) - } - } setInt(1, project.id) setBoolean(2, includeDone) - applyFilter(version, 3, 5, 4) - applyFilter(component, 6, 8, 7) + + setBoolean(3, specificVersion && version != null) + setInt(4, version?.id ?: 0) + setBoolean(5, specificVersion && version == null) + + setBoolean(6, specificComponent && component != null) + setInt(7, component?.id ?: 0) + setBoolean(8, specificComponent && component == null) queryAll { it.extractIssue() } } diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Mon Oct 30 14:44:36 2023 +0100 @@ -25,11 +25,8 @@ package de.uapcore.lightpit.servlet -import de.uapcore.lightpit.AbstractServlet -import de.uapcore.lightpit.HttpRequest -import de.uapcore.lightpit.boolValidator +import de.uapcore.lightpit.* import de.uapcore.lightpit.dao.DataAccessObject -import de.uapcore.lightpit.dateOptValidator import de.uapcore.lightpit.entities.* import de.uapcore.lightpit.types.* import de.uapcore.lightpit.viewmodel.* @@ -86,53 +83,6 @@ } } - private fun activeProjectNavMenu( - projects: List, - projectInfo: ProjectInfo, - selectedVersion: Version? = null, - selectedComponent: Component? = null - ) = - projectNavMenu( - projects, - projectInfo.versions, - projectInfo.components, - projectInfo.project, - selectedVersion, - selectedComponent - ) - - private sealed interface LookupResult - private class NotFound : LookupResult - private data class Found(val elem: T?) : LookupResult - - private fun HttpRequest.lookupPathParam(paramName: String, list: List): LookupResult { - val node = pathParams[paramName] - return if (node == null || node == "-") { - Found(null) - } else { - val result = list.find { it.node == node } - if (result == null) { - NotFound() - } else { - Found(result) - } - } - } - - private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? { - val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null - - val versions: List = dao.listVersions(project) - val components: List = dao.listComponents(project) - - return ProjectInfo( - project, - versions, - components, - dao.collectIssueSummary(project) - ) - } - private fun sanitizeNode(name: String): String { val san = name.replace(Regex("[/\\\\]"), "-") return if (san.startsWith(".")) { @@ -144,46 +94,9 @@ private fun feedPath(project: Project) = "feed/${project.node}/issues.rss" - private data class PathInfos( - val projectInfo: ProjectInfo, - val version: Version?, - val component: Component? - ) { - val project = projectInfo.project - val issuesHref by lazyOf("projects/${project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/") - } - - private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? { - val projectInfo = obtainProjectInfo(http, dao) - if (projectInfo == null) { - http.response.sendError(404) - return null - } - - val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) { - is NotFound -> { - http.response.sendError(404) - return null - } - is Found -> { - result.elem - } - } - val component = when (val result = http.lookupPathParam("component", projectInfo.components)) { - is NotFound -> { - http.response.sendError(404) - return null - } - is Found -> { - result.elem - } - } - - return PathInfos(projectInfo, version, component) - } - private fun project(http: HttpRequest, dao: DataAccessObject) { - withPathInfo(http, dao)?.run { + withPathInfo(http, dao)?.let {path -> + val project = path.projectInfo.project val filter = IssueFilter(http) @@ -191,7 +104,12 @@ val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap() - val issues = dao.listIssues(project, filter.includeDone, version, component) + val specificVersion = path.versionInfo !is OptionalPathInfo.All + val version = if (path.versionInfo is OptionalPathInfo.Specific) path.versionInfo.elem else null + val specificComponent = path.componentInfo !is OptionalPathInfo.All + val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else null + + val issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component) .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) .filter { (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "")) && @@ -202,14 +120,9 @@ with(http) { pageTitle = project.name - view = ProjectDetails(projectInfo, issues, filter, version, component) + view = ProjectDetails(path, issues, filter) feedPath = feedPath(project) - navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo, - version, - component - ) + navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") javascript = "project-details" render("project-details") @@ -218,23 +131,18 @@ } private fun projectForm(http: HttpRequest, dao: DataAccessObject) { + http.styleSheets = listOf("projects") if (!http.pathParams.containsKey("project")) { http.view = ProjectEditView(Project(-1), dao.listUsers()) http.navigationMenu = projectNavMenu(dao.listProjects()) + http.render("project-form") } else { - val projectInfo = obtainProjectInfo(http, dao) - if (projectInfo == null) { - http.response.sendError(404) - return + withPathInfo(http, dao)?.let { path -> + http.view = ProjectEditView(path.projectInfo.project, dao.listUsers()) + http.navigationMenu = projectNavMenu(dao.listProjects(), path) + http.render("project-form") } - http.view = ProjectEditView(projectInfo.project, dao.listUsers()) - http.navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo - ) } - http.styleSheets = listOf("projects") - http.render("project-form") } private fun projectCommit(http: HttpRequest, dao: DataAccessObject) { @@ -264,77 +172,50 @@ } private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) { - val projectInfo = obtainProjectInfo(http, dao) - if (projectInfo == null) { - http.response.sendError(404) - return - } + withPathInfo(http, dao)?.let { path -> + // if analysis is not configured, reject the request + if (path.projectInfo.project.vcs == VcsType.None) { + http.response.sendError(404) + return + } - // if analysis is not configured, reject the request - if (projectInfo.project.vcs == VcsType.None) { - http.response.sendError(404) - return - } + // obtain the list of issues for this project to filter cross-project references + val knownIds = dao.listIssues(path.projectInfo.project, true).map { it.id } - // obtain the list of issues for this project to filter cross-project references - val knownIds = dao.listIssues(projectInfo.project, true).map { it.id } - - // read the provided commit log and merge only the refs that relate issues from the current project - dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) }) + // read the provided commit log and merge only the refs that relate issues from the current project + dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) }) + } } private fun versions(http: HttpRequest, dao: DataAccessObject) { - val projectInfo = obtainProjectInfo(http, dao) - if (projectInfo == null) { - http.response.sendError(404) - return - } - - with(http) { - pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.versions")}" - view = VersionsView( - projectInfo, - dao.listVersionSummaries(projectInfo.project) - ) - feedPath = feedPath(projectInfo.project) - navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo - ) - styleSheets = listOf("projects") - javascript = "project-details" - render("versions") + withPathInfo(http, dao)?.let { path -> + with(http) { + pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.versions")}" + view = VersionsView( + path.projectInfo, + dao.listVersionSummaries(path.projectInfo.project) + ) + feedPath = feedPath(path.projectInfo.project) + navigationMenu = projectNavMenu(dao.listProjects(), path) + styleSheets = listOf("projects") + javascript = "project-details" + render("versions") + } } } private fun versionForm(http: HttpRequest, dao: DataAccessObject) { - val projectInfo = obtainProjectInfo(http, dao) - if (projectInfo == null) { - http.response.sendError(404) - return - } + withPathInfo(http, dao)?.let { path -> + val version = if (path.versionInfo is OptionalPathInfo.Specific) + path.versionInfo.elem else Version(-1, path.projectInfo.project.id) - val version: Version - when (val result = http.lookupPathParam("version", projectInfo.versions)) { - is NotFound -> { - http.response.sendError(404) - return + with(http) { + view = VersionEditView(path.projectInfo, version) + feedPath = feedPath(path.projectInfo.project) + navigationMenu = projectNavMenu(dao.listProjects(), path) + styleSheets = listOf("projects") + render("version-form") } - is Found -> { - version = result.elem ?: Version(-1, projectInfo.project.id) - } - } - - with(http) { - view = VersionEditView(projectInfo, version) - feedPath = feedPath(projectInfo.project) - navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo, - selectedVersion = version - ) - styleSheets = listOf("projects") - render("version-form") } } @@ -385,57 +266,34 @@ } private fun components(http: HttpRequest, dao: DataAccessObject) { - val projectInfo = obtainProjectInfo(http, dao) - if (projectInfo == null) { - http.response.sendError(404) - return - } - - with(http) { - pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.components")}" - view = ComponentsView( - projectInfo, - dao.listComponentSummaries(projectInfo.project) - ) - feedPath = feedPath(projectInfo.project) - navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo - ) - styleSheets = listOf("projects") - javascript = "project-details" - render("components") + withPathInfo(http, dao)?.let { path -> + with(http) { + pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.components")}" + view = ComponentsView( + path.projectInfo, + dao.listComponentSummaries(path.projectInfo.project) + ) + feedPath = feedPath(path.projectInfo.project) + navigationMenu = projectNavMenu(dao.listProjects(), path) + styleSheets = listOf("projects") + javascript = "project-details" + render("components") + } } } private fun componentForm(http: HttpRequest, dao: DataAccessObject) { - val projectInfo = obtainProjectInfo(http, dao) - if (projectInfo == null) { - http.response.sendError(404) - return - } + withPathInfo(http, dao)?.let { path -> + val component = if (path.componentInfo is OptionalPathInfo.Specific) + path.componentInfo.elem else Component(-1, path.projectInfo.project.id) - val component: Component - when (val result = http.lookupPathParam("component", projectInfo.components)) { - is NotFound -> { - http.response.sendError(404) - return + with(http) { + view = ComponentEditView(path.projectInfo, component, dao.listUsers()) + feedPath = feedPath(path.projectInfo.project) + navigationMenu = projectNavMenu(dao.listProjects(), path) + styleSheets = listOf("projects") + render("component-form") } - is Found -> { - component = result.elem ?: Component(-1, projectInfo.project.id) - } - } - - with(http) { - view = ComponentEditView(projectInfo, component, dao.listUsers()) - feedPath = feedPath(projectInfo.project) - navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo, - selectedComponent = component - ) - styleSheets = listOf("projects") - render("component-form") } } @@ -484,29 +342,23 @@ issue: Issue, relationError: String? = null ) { - withPathInfo(http, dao)?.run { + withPathInfo(http, dao)?.let {path -> val comments = dao.listComments(issue) with(http) { - pageTitle = "${projectInfo.project.name}: #${issue.id} ${issue.subject}" + pageTitle = "${path.projectInfo.project.name}: #${issue.id} ${issue.subject}" view = IssueDetailView( + path, issue, comments, - project, - version, - component, - dao.listIssues(project, true), + path.projectInfo.project, + dao.listIssues(path.projectInfo.project, true), dao.listIssueRelations(issue), relationError, dao.listCommitRefs(issue) ) - feedPath = feedPath(projectInfo.project) - navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo, - version, - component - ) + feedPath = feedPath(path.projectInfo.project) + navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") javascript = "issue-editor" render("issue-view") @@ -515,23 +367,25 @@ } private fun issueForm(http: HttpRequest, dao: DataAccessObject) { - withPathInfo(http, dao)?.run { + withPathInfo(http, dao)?.let { path -> val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue( -1, - project, + path.projectInfo.project, ) // for new issues set some defaults if (issue.id < 0) { // pre-select component, if available in the path info - issue.component = component + if (path.componentInfo is OptionalPathInfo.Specific) { + issue.component = path.componentInfo.elem + } // pre-select version, if available in the path info - if (version != null) { - if (version.status.isReleased) { - issue.affected = version + if (path.versionInfo is OptionalPathInfo.Specific) { + if (path.versionInfo.elem.status.isReleased) { + issue.affected = path.versionInfo.elem } else { - issue.resolved = version + issue.resolved = path.versionInfo.elem } } } @@ -539,20 +393,14 @@ with(http) { view = IssueEditView( issue, - projectInfo.versions, - projectInfo.components, + path.projectInfo.versions, + path.projectInfo.components, dao.listUsers(), - project, - version, - component + path.projectInfo.project, + path ) - feedPath = feedPath(projectInfo.project) - navigationMenu = activeProjectNavMenu( - dao.listProjects(), - projectInfo, - version, - component - ) + feedPath = feedPath(path.projectInfo.project) + navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") javascript = "issue-editor" render("issue-form") @@ -606,7 +454,7 @@ withPathInfo(http, dao)?.run { val issue = Issue( http.param("id")?.toIntOrNull() ?: -1, - project + projectInfo.project ).apply { component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1) category = IssueCategory.valueOf(http.param("category") ?: "") diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Mon Oct 30 14:44:36 2023 +0100 @@ -102,11 +102,10 @@ data class CommitLink(val url: String, val hash: String, val message: String) class IssueDetailView( + val pathInfos: PathInfos, val issue: Issue, val comments: List, val project: Project, - val version: Version?, - val component: Component?, projectIssues: List, val currentRelations: List, /** @@ -168,8 +167,7 @@ val components: List, val users: List, val project: Project, // TODO: allow null values to create issues from the IssuesServlet - val version: Version? = null, - val component: Component? = null + val pathInfos: PathInfos ) : EditView() { val versionsUpcoming: List diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt Mon Oct 30 14:44:36 2023 +0100 @@ -25,6 +25,7 @@ package de.uapcore.lightpit.viewmodel +import de.uapcore.lightpit.OptionalPathInfo import de.uapcore.lightpit.entities.Component import de.uapcore.lightpit.entities.Project import de.uapcore.lightpit.entities.Version @@ -44,19 +45,26 @@ class NavMenu(val entries: List) -fun projectNavMenu( - projects: List, - versions: List = emptyList(), - components: List = emptyList(), - selectedProject: Project? = null, - selectedVersion: Version? = null, - selectedComponent: Component? = null -) = NavMenu( +fun projectNavMenu(projects: List) = NavMenu( sequence { - val cnode = selectedComponent?.node ?: "-" - val vnode = selectedVersion?.node ?: "-" for (project in projects) { - val active = project == selectedProject + yield( + NavMenuEntry( + level = 0, + caption = project.name, + href = "projects/${project.node}", + ) + ) + } + }.toList() +) + +fun projectNavMenu(projects: List, pathInfos: PathInfos) = NavMenu( + sequence { + val cnode = pathInfos.componentInfo.node + val vnode = pathInfos.versionInfo.node + for (project in projects) { + val active = project == pathInfos.projectInfo.project yield( NavMenuEntry( level = 0, @@ -80,10 +88,22 @@ caption = "navmenu.all", resolveCaption = true, href = "projects/${project.node}/issues/-/${cnode}/", - iconColor = "#000000" + iconColor = "#000000", + active = vnode == "-", ) ) - for (version in versions.filter { it.status != VersionStatus.Deprecated }) { + yield( + NavMenuEntry( + level = 2, + caption = "navmenu.none", + resolveCaption = true, + href = "projects/${project.node}/issues/~/${cnode}/", + iconColor = "#000000", + active = vnode == "~", + ) + ) + for (version in pathInfos.projectInfo.versions) { + if (version.status == VersionStatus.Deprecated && vnode != version.node) continue yield( NavMenuEntry( level = 2, @@ -91,7 +111,7 @@ title = "version.status.${version.status}", href = "projects/${project.node}/issues/${version.node}/${cnode}/", iconColor = "version-${version.status}", - active = version == selectedVersion + active = version.node == vnode ) ) } @@ -109,18 +129,29 @@ caption = "navmenu.all", resolveCaption = true, href = "projects/${project.node}/issues/${vnode}/-/", - iconColor = "#000000" + iconColor = "#000000", + active = cnode == "-", ) ) - for (component in components) { - if (!component.active && component != selectedComponent) continue + yield( + NavMenuEntry( + level = 2, + caption = "navmenu.none", + resolveCaption = true, + href = "projects/${project.node}/issues/${vnode}/~/", + iconColor = "#000000", + active = cnode == "~", + ) + ) + for (component in pathInfos.projectInfo.components) { + if (!component.active && component.node != cnode) continue yield( NavMenuEntry( level = 2, caption = component.name, href = "projects/${project.node}/issues/${vnode}/${component.node}/", iconColor = "${component.color}", - active = component == selectedComponent + active = component.node == cnode ) ) } diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/viewmodel/PathInfos.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/PathInfos.kt Mon Oct 30 14:44:36 2023 +0100 @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import de.uapcore.lightpit.HttpRequest +import de.uapcore.lightpit.OptionalPathInfo +import de.uapcore.lightpit.dao.DataAccessObject +import de.uapcore.lightpit.entities.Component +import de.uapcore.lightpit.entities.Version + +data class PathInfos( + val projectInfo: ProjectInfo, + val versionInfo: OptionalPathInfo, + val componentInfo: OptionalPathInfo +) { + val issuesHref by lazyOf("projects/${projectInfo.project.node}/issues/${versionInfo.node}/${componentInfo.node}/") +} + +private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? { + val pathParam = http.pathParams["project"] ?: return null + val project = dao.findProjectByNode(pathParam) ?: return null + + val versions: List = dao.listVersions(project) + val components: List = dao.listComponents(project) + + return ProjectInfo( + project, + versions, + components, + dao.collectIssueSummary(project) + ) +} + +fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? { + val projectInfo = obtainProjectInfo(http, dao) + if (projectInfo == null) { + http.response.sendError(404) + return null + } + + val version = http.lookupPathParam("version", projectInfo.versions) + val component = http.lookupPathParam("component", projectInfo.components) + + if (version == OptionalPathInfo.NotFound || component == OptionalPathInfo.NotFound) { + http.response.sendError(404) + return null + } + + return PathInfos(projectInfo, version, component) +} diff -r bcf05cccac6f -r 703591e739f4 src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt Mon Oct 30 14:44:36 2023 +0100 @@ -25,6 +25,7 @@ package de.uapcore.lightpit.viewmodel +import de.uapcore.lightpit.OptionalPathInfo import de.uapcore.lightpit.entities.* class ProjectInfo( @@ -45,18 +46,25 @@ ) : View() class ProjectDetails( - val projectInfo: ProjectInfo, + val pathInfos: PathInfos, val issues: List, val filter: IssueFilter, - val version: Version? = null, - val component: Component? = null ) : View() { + val projectInfo = pathInfos.projectInfo val issueSummary = IssueSummary() val versionInfo: VersionInfo? + val componentDetails: Component? init { issues.forEach(issueSummary::add) - versionInfo = version?.let { VersionInfo(it, issues) } + versionInfo = when (val vinfo = pathInfos.versionInfo){ + is OptionalPathInfo.Specific -> VersionInfo(vinfo.elem, issues) + else -> null + } + componentDetails = when (val cinfo = pathInfos.componentInfo){ + is OptionalPathInfo.Specific -> cinfo.elem + else -> null + } } } diff -r bcf05cccac6f -r 703591e739f4 src/main/resources/localization/strings.properties --- a/src/main/resources/localization/strings.properties Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/resources/localization/strings.properties Mon Oct 30 14:44:36 2023 +0100 @@ -141,6 +141,7 @@ menu.users=Developer navmenu.all=all navmenu.components=Components +navmenu.none=none navmenu.versions=Versions no-projects=Welcome to LightPIT. Start off by creating a new project! no-users=No developers have been configured yet. diff -r bcf05cccac6f -r 703591e739f4 src/main/resources/localization/strings_de.properties --- a/src/main/resources/localization/strings_de.properties Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/resources/localization/strings_de.properties Mon Oct 30 14:44:36 2023 +0100 @@ -141,6 +141,7 @@ menu.users=Entwickler navmenu.all=Alle navmenu.components=Komponenten +navmenu.none=Keine navmenu.versions=Versionen no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes! no-users=Bislang wurden keine Entwickler hinterlegt. diff -r bcf05cccac6f -r 703591e739f4 src/main/webapp/WEB-INF/changelogs/changelog-de.jspf --- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Mon Oct 30 14:44:36 2023 +0100 @@ -24,6 +24,16 @@ --%> <%@ page contentType="text/html;charset=UTF-8" %> +

Version 1.2 (Vorschau)

+ +
    +
  • + Im Seitenmenü können nun alle Vorgänge gewählt werden, die + keiner Version oder Komponente zugeordnet sind. +
  • +
  • Einige kleinere Fehlerbehebungen im Zusammenhang mit dem Seitenmenü.
  • +
+

Version 1.1.2

    diff -r bcf05cccac6f -r 703591e739f4 src/main/webapp/WEB-INF/changelogs/changelog.jspf --- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf Mon Oct 30 14:44:36 2023 +0100 @@ -24,6 +24,16 @@ --%> <%@ page contentType="text/html;charset=UTF-8" %> +

    Version 1.2 (snapshot)

    + +
      +
    • + The left menu now allows selection of issues without assigned + version or component. +
    • +
    • Several minor bugfixes regarding the left menu.
    • +
    +

    Version 1.1.2

      diff -r bcf05cccac6f -r 703591e739f4 src/main/webapp/WEB-INF/jsp/issue-form.jsp --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Mon Oct 30 14:44:36 2023 +0100 @@ -32,10 +32,7 @@ - - - - +
      diff -r bcf05cccac6f -r 703591e739f4 src/main/webapp/WEB-INF/jsp/issue-view.jsp --- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp Mon Oct 30 14:44:36 2023 +0100 @@ -32,11 +32,9 @@ - - - + diff -r bcf05cccac6f -r 703591e739f4 src/main/webapp/WEB-INF/jsp/project-details.jsp --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp Mon Oct 30 14:44:36 2023 +0100 @@ -31,12 +31,12 @@ - - + + <%@include file="../jspf/project-header.jspf"%>
      - +
      diff -r bcf05cccac6f -r 703591e739f4 src/main/webapp/WEB-INF/jspf/issue-list.jspf --- a/src/main/webapp/WEB-INF/jspf/issue-list.jspf Mon Oct 30 10:06:22 2023 +0100 +++ b/src/main/webapp/WEB-INF/jspf/issue-list.jspf Mon Oct 30 14:44:36 2023 +0100 @@ -1,7 +1,6 @@ <%-- issues: List -version: Version? -component: Component? +issuesHref: String --%>
      @@ -23,7 +22,7 @@
      - + #${issue.id} -