add the possibility to hide projects from the left menu - resolves #818 default tip

Tue, 23 Jun 2026 14:30:10 +0200

author
Mike Becker <universe@uap-core.de>
date
Tue, 23 Jun 2026 14:30:10 +0200
changeset 436
a07662e829c0
parent 435
5e36330f954b

add the possibility to hide projects from the left menu - resolves #818

build.gradle.kts file | annotate | diff | comparison | revisions
setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
setup/postgres/psql_patch_1.7.0.sql file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/Constants.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Project.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/NavMenus.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/project-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/navmenu.jspf file | annotate | diff | comparison | revisions
src/main/webapp/lightpit.js file | annotate | diff | comparison | revisions
--- a/build.gradle.kts	Tue Jun 23 13:06:07 2026 +0200
+++ b/build.gradle.kts	Tue Jun 23 14:30:10 2026 +0200
@@ -5,7 +5,7 @@
     war
 }
 group = "de.uapcore"
-version = "1.6.2"
+version = "1.7.0"
 
 repositories {
     mavenCentral()
--- a/setup/postgres/psql_create_tables.sql	Tue Jun 23 13:06:07 2026 +0200
+++ b/setup/postgres/psql_create_tables.sql	Tue Jun 23 14:30:10 2026 +0200
@@ -16,6 +16,7 @@
     name        text    not null unique,
     node        text    not null unique,
     ordinal     integer not null default 0,
+    hidden      boolean not null default false,
     description text,
     repoUrl     text,
     vcs         vcstype not null default 'None'::vcstype,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup/postgres/psql_patch_1.7.0.sql	Tue Jun 23 14:30:10 2026 +0200
@@ -0,0 +1,3 @@
+-- apply this script to patch a version 1.6.x database to version 1.7.0
+
+alter table lpit_project add column hidden boolean default false;
--- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Tue Jun 23 14:30:10 2026 +0200
@@ -29,7 +29,7 @@
     /**
      * A date in yyyy-mm-dd format to identify the release.
      */
-    const val VERSION_DATE = "2026-06-11"
+    const val VERSION_DATE = "2026-06-23"
 
     /**
      * The path where the JSP files reside.
@@ -116,4 +116,9 @@
      * Key for the current timezone selection within the session.
      */
     const val SESSION_ATTR_TIMEZONE = "timezone"
+
+    /**
+     * Key for the session attribute that controls whether all projects are shown in the navmenu.
+     */
+    const val SESSION_ATTR_SHOW_ALL_PROJECTS = "show_all_projects"
 }
--- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Tue Jun 23 14:30:10 2026 +0200
@@ -48,7 +48,12 @@
     val node = info.node
 }
 
-sealed interface ValidationResult<T>
+sealed interface ValidationResult<T> {
+    fun getOrNull(): T? = when (this) {
+        is ValidatedValue -> result
+        else -> null
+    }
+}
 class ValidationError<T>(val message: String): ValidationResult<T>
 class ValidatedValue<T>(val result: T): ValidationResult<T>
 
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Tue Jun 23 14:30:10 2026 +0200
@@ -480,7 +480,7 @@
     //language=SQL
     private val projectQuery =
         """
-        select projectid, name, node, ordinal, description, vcs, repourl,
+        select projectid, name, node, ordinal, hidden, description, vcs, repourl,
             userid, username, lastname, givenname, mail
         from lpit_project
         left join lpit_user owner on lpit_project.owner = owner.userid
@@ -491,6 +491,7 @@
             name = getString("${prefix}name")
             node = getString("${prefix}node")
             ordinal = getInt("${prefix}ordinal")
+            hidden = getBoolean("${prefix}hidden")
             description = getString("${prefix}description")
             vcs = getEnum("${prefix}vcs")
             repoUrl = getString("${prefix}repourl")
@@ -503,6 +504,7 @@
             setStringSafe(i++, name)
             setStringSafe(i++, node)
             setInt(i++, ordinal)
+            setBoolean(i++, hidden)
             setStringOrNull(i++, description)
             setEnum(i++, vcs)
             setStringOrNull(i++, repoUrl)
@@ -529,14 +531,14 @@
         }
 
     override fun insertProject(project: Project) {
-        withStatement("insert into lpit_project (name, node, ordinal, description, vcs, repourl, owner) values (?, ?, ?, ?, ?::vcstype, ?, ?)") {
+        withStatement("insert into lpit_project (name, node, ordinal, hidden, description, vcs, repourl, owner) values (?, ?, ?, ?, ?::vcstype, ?, ?)") {
             setProject(1, project)
             executeUpdate()
         }
     }
 
     override fun updateProject(project: Project) {
-        withStatement("update lpit_project set name = ?, node = ?, ordinal = ?, description = ?, vcs = ?::vcstype, repourl = ?, owner = ? where projectid = ?") {
+        withStatement("update lpit_project set name = ?, node = ?, ordinal = ?, hidden = ?, description = ?, vcs = ?::vcstype, repourl = ?, owner = ? where projectid = ?") {
             val col = setProject(1, project)
             setInt(col, project.id)
             executeUpdate()
@@ -626,6 +628,7 @@
             p.name as project_name,
             p.node as project_node,
             p.ordinal as project_ordinal,
+            p.hidden as project_hidden,
             p.description as project_description,
             p.vcs as project_vcs,
             p.repourl as project_repourl,
@@ -648,6 +651,7 @@
             p.name as project_name,
             p.node as project_node,
             p.ordinal as project_ordinal,
+            p.hidden as project_hidden,
             p.description as project_description,
             p.vcs as project_vcs,
             p.repourl as project_repourl,
--- a/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Tue Jun 23 14:30:10 2026 +0200
@@ -31,6 +31,7 @@
     var name: String = ""
     override var node: String = name
     var ordinal = 0
+    var hidden = false
     var description: String? = null
     var vcs: VcsType = VcsType.None
     var repoUrl: String? = null
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Tue Jun 23 14:30:10 2026 +0200
@@ -38,6 +38,7 @@
 class ProjectServlet : AbstractServlet() {
 
     init {
+        post("/settings", this::changeSettings)
         get("/", this::projects)
         get("/%project", this::project)
         get("/%project/issues/%version/%component/%variant/", this::project)
@@ -75,6 +76,13 @@
         post("/%project/issues/%version/%component/%variant/-/commit", this::issueCommit)
     }
 
+    private fun changeSettings(http: HttpRequest, dao: DataAccessObject) {
+        http.param("show_all_projects")?.let(::boolValidator)?.getOrNull()?.let {
+            http.session.setAttribute(Constants.SESSION_ATTR_SHOW_ALL_PROJECTS, it)
+        }
+        http.response.status = 204
+    }
+
     private fun projects(http: HttpRequest, dao: DataAccessObject) {
         val projects = dao.listProjects()
         val projectInfos = projects.map {
@@ -154,6 +162,7 @@
             node = http.param("node") ?: ""
             description = http.param("description") ?: ""
             ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
+            hidden = http.param("hidden", ::boolValidator, false, mutableListOf())
             repoUrl = http.param("repoUrl") ?: ""
             vcs = VcsType.valueOf(http.param("vcs") ?: "None")
             owner = (http.param("owner")?.toIntOrNull() ?: -1).let {
@@ -171,7 +180,11 @@
             dao.updateProject(project)
         }
 
-        http.renderCommit("projects/${project.node}")
+        if (project.hidden) {
+            http.renderCommit("projects")
+        } else {
+            http.renderCommit("projects/${project.node}")
+        }
     }
 
     private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) {
@@ -241,7 +254,6 @@
                     dao.listVersionSummaries(path.project)
                 )
                 navigationMenu = projectNavMenu(dao.listProjects(), path)
-                javascript = "issue-overview"
                 render("versions")
             }
         }
@@ -350,7 +362,6 @@
                     dao.listComponentSummaries(path.project)
                 )
                 navigationMenu = projectNavMenu(dao.listProjects(), path)
-                javascript = "issue-overview"
                 render("components")
             }
         }
@@ -381,7 +392,6 @@
             ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
             color = WebColor(http.param("color") ?: "#000000")
             description = http.param("description")
-            // TODO: process error message
             active = http.param("active", ::boolValidator, true, mutableListOf())
             lead = (http.param("lead")?.toIntOrNull() ?: -1).let {
                 if (it < 0) null else dao.findUser(it)
@@ -410,7 +420,6 @@
                     dao.listVariantSummaries(path.project)
                 )
                 navigationMenu = projectNavMenu(dao.listProjects(), path)
-                javascript = "issue-overview"
                 render("variants")
             }
         }
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt	Tue Jun 23 14:30:10 2026 +0200
@@ -34,6 +34,7 @@
     val href: String,
     val title: String = "",
     val active: Boolean = false,
+    val hidden: Boolean = false,
     val resolveCaption: Boolean = false,
     val iconColor: String? = null,
     val editHref: String? = null
@@ -50,6 +51,7 @@
                 NavMenuEntry(
                     level = 0,
                     caption = project.name,
+                    hidden = project.hidden,
                     href = "projects/${project.node}",
                 )
             )
@@ -68,6 +70,7 @@
                 NavMenuEntry(
                     level = 0,
                     caption = project.name,
+                    hidden = !active && project.hidden,
                     href = "projects/${project.node}",
                     active = active
                 )
--- a/src/main/resources/localization/strings.properties	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/resources/localization/strings.properties	Tue Jun 23 14:30:10 2026 +0200
@@ -158,6 +158,7 @@
 menu.projects=Projects
 menu.settings=Settings
 menu.users=Developer
+navmenu.showall=Show All
 navmenu.all=all
 navmenu.components=Components
 navmenu.none=none
@@ -178,6 +179,8 @@
 placeholder.null-version=None
 placeholder.null-eta=None
 progress=Overall Progress
+project.hidden=Hidden
+project.hidden.tooltip=Hide this project from the left menu.
 project.name=Name
 project.owner=Project Lead
 project.repoUrl=Repository
--- a/src/main/resources/localization/strings_de.properties	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/resources/localization/strings_de.properties	Tue Jun 23 14:30:10 2026 +0200
@@ -158,6 +158,7 @@
 menu.projects=Projekte
 menu.settings=Einstellungen
 menu.users=Entwickler
+navmenu.showall=Alle anzeigen
 navmenu.all=Alle
 navmenu.components=Komponenten
 navmenu.none=Keine
@@ -178,6 +179,8 @@
 placeholder.null-version=Keine
 placeholder.null-eta=Nicht geplant
 progress=Gesamtfortschritt
+project.hidden=Versteckt
+project.hidden.tooltip=Dieses Projekt wird im Navigationsmen\u00fc versteckt.
 project.name=Name
 project.owner=Projektleitung
 project.repoUrl=Repository
--- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Tue Jun 23 14:30:10 2026 +0200
@@ -24,6 +24,12 @@
   --%>
 <%@ page contentType="text/html;charset=UTF-8" %>
 
+<h3>Version 1.7.0 (Vorschau)</h3>
+
+<ul>
+    <li>Die Möglichkeit hinzugefügt, Projekte im Navigationsmenü zu verstecken.</li>
+</ul>
+
 <h3>Version 1.6.2</h3>
 
 <ul>
--- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Tue Jun 23 14:30:10 2026 +0200
@@ -24,6 +24,12 @@
   --%>
 <%@ page contentType="text/html;charset=UTF-8" %>
 
+<h3>Version 1.7.0 (preview)</h3>
+
+<ul>
+    <li>Add the possibility to hide projects from the left menu.</li>
+</ul>
+
 <h3>Version 1.6.2</h3>
 
 <ul>
--- a/src/main/webapp/WEB-INF/jsp/project-form.jsp	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-form.jsp	Tue Jun 23 14:30:10 2026 +0200
@@ -84,6 +84,12 @@
                 <input name="ordinal" type="number" value="${project.ordinal}"/>
             </td>
         </tr>
+        <tr title="<fmt:message key="project.hidden.tooltip" />">
+            <th><fmt:message key="project.hidden"/></th>
+            <td>
+                <input name="hidden" type="checkbox" <c:if test="${project.hidden}">checked</c:if>/>
+            </td>
+        </tr>
         </tbody>
         <tfoot>
         <tr>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Tue Jun 23 14:30:10 2026 +0200
@@ -57,6 +57,9 @@
 <%-- Define an alias for timezone --%>
 <c:set scope="page" var="timezone" value="${sessionScope[Constants.SESSION_ATTR_TIMEZONE]}" />
 
+<%-- Define an alias for project_showall --%>
+<c:set scope="page" var="show_all_projects" value="${sessionScope[Constants.SESSION_ATTR_SHOW_ALL_PROJECTS]}" />
+
 <%-- Load resource bundle --%>
 <fmt:setLocale scope="request" value="${pageContext.response.locale}"/>
 <fmt:setBundle scope="request" basename="localization.strings"/>
@@ -79,16 +82,8 @@
     </c:if>
     <script>
         const baseHref='${baseHref}';
-        <c:if test="${showWhatsNew}">
-        function closeWhatsNew(showMore) {
-            if (showMore) {
-                window.open(baseHref + 'about', '_blank');
-            }
-            document.getElementById('whats-new').style.display = 'none';
-            document.getElementById('page-area').classList.remove('blurred');
-        }
-        </c:if>
     </script>
+    <script src="lightpit.js?v=${versionSuffix}" type="text/javascript"></script>
     <script src="issue-search.js?v=${versionSuffix}" type="text/javascript"></script>
     <c:if test="${not empty javascriptFile}">
     <script src="${javascriptFile}?v=${versionSuffix}" type="text/javascript"></script>
--- a/src/main/webapp/WEB-INF/jspf/navmenu.jspf	Tue Jun 23 13:06:07 2026 +0200
+++ b/src/main/webapp/WEB-INF/jspf/navmenu.jspf	Tue Jun 23 14:30:10 2026 +0200
@@ -30,9 +30,14 @@
 
 <jsp:useBean id="navMenu" type="de.uapcore.lightpit.viewmodel.NavMenu" scope="request" />
 
+<div class="menuEntry level-0">
+    <input id="show-all-projects" type="checkbox" <c:if test="${show_all_projects}">checked</c:if> onclick="toggleShowAllProjects()"/>
+    <label for="show-all-projects"><fmt:message key="navmenu.showall"/> </label>
+</div>
 <c:forEach var="entry" items="${navMenu.entries}">
     <div class="menuEntry level-${entry.level}"
         <c:if test="${entry.active}"> data-active </c:if>
+        <c:if test="${entry.hidden}"> data-hidden </c:if>
         <c:if test="${not empty entry.title}">title="<fmt:message key="${entry.title}"/>" </c:if>
     >
         <c:if test="${not empty entry.iconColor}">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/lightpit.js	Tue Jun 23 14:30:10 2026 +0200
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 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.
+ */
+
+function closeWhatsNew(showMore) {
+    if (showMore) {
+        window.open(baseHref + 'about', '_blank');
+    }
+    document.getElementById('whats-new').style.display = 'none';
+    document.getElementById('page-area').classList.remove('blurred');
+}
+
+function applyShowAllProjects() {
+    const toggle = document.getElementById('show-all-projects');
+    if (!toggle) return; // this page does not show the navmenu
+    const checked = toggle.checked;
+    document.querySelectorAll('#sideMenu .menuEntry[data-hidden]').forEach((elem) => {
+        elem.style.display = checked ? 'initial' : 'none';
+    })
+}
+
+function toggleShowAllProjects() {
+    const toggle = document.getElementById('show-all-projects');
+
+    const req = new XMLHttpRequest();
+    req.open('POST', baseHref+'projects/settings');
+    req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+    req.send('show_all_projects=' + (toggle.checked ? 'true' : 'false'));
+    applyShowAllProjects();
+}
+
+window.addEventListener('load', () => {
+    applyShowAllProjects();
+});

mercurial