Mon, 30 Oct 2023 14:44:36 +0100
add possibility to show issues w/o version or component - fixes #335
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} - <c:out value="${issue.subject}" /> 16.19 </a> 16.20 </span>