This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new be34e499a7 Issue #2703 : Group projects by topic in the project menu
(#8677)
be34e499a7 is described below
commit be34e499a73936f62945e73d8f5f9d72946fa49d
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 09:09:26 2026 +0200
Issue #2703 : Group projects by topic in the project menu (#8677)
---
.../ROOT/pages/projects/projects-environments.adoc | 3 +
.../hop/projects/gui/ProjectConfigDialog.java | 2 +
.../apache/hop/projects/gui/ProjectMenuGroups.java | 146 +++++++++++++++++++++
.../apache/hop/projects/gui/ProjectsGuiPlugin.java | 70 ++++++++--
.../apache/hop/projects/project/ProjectDialog.java | 24 ++++
.../gui/messages/messages_en_US.properties | 1 +
.../project/messages/messages_en_US.properties | 2 +
.../hop/projects/gui/ProjectMenuGroupsTest.java | 106 +++++++++++++++
8 files changed, 345 insertions(+), 9 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
index cbe0e847a5..0894a872e5 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
@@ -49,6 +49,7 @@ image:hop-gui/environment/create-project-dialog.png[Project
Properties Basic tab
|Home folder|The folder where the project is located. Paths starting with `~`
(e.g. `~/my-project` or `~\my-project` on Windows) automatically resolve to the
user's home directory (`+${user.home}+`).|Yes|No|
|This project is read only|When checked, Hop does not write
`project-config.json` (archives, HTTP, and similar)|No|No|unchecked
|Configuration file (relative path)|The project's configuration json, relative
to the home folder.|Yes|Yes|`project-config.json`
+|Group|Optional topic stored on the project registration in `hop-config.json`
(not in `project-config.json`). Projects that share a group are listed together
in a submenu of the project menu. Leave empty to keep the project in the recent
list.|Yes|No|
|Description|A description for this project|No|No|
|Company|The company this project belongs to|No|No|
|Department|The department this project is created for|No|No|
@@ -239,6 +240,8 @@ These checks run on the JVM that enables the environment.
They do not validate r
Click the project indicator at the bottom left and choose a recent project
from the menu, or *More projects...* to pick from a list.
+Projects with a group are not repeated in that recent list. Each group is its
own submenu and lists every project in the group, so a long flat list becomes
one entry per topic. Set the group on the project properties *Basic* tab, or
when editing the project registration from *More projects...*. Projects without
a group stay in the recent list (up to 15).
+
image:hop-gui/environment/switch-project-list.png[Switch project]
After switching to a project, click the environment indicator (cube icon) next
to the project name and choose an environment from the menu.
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectConfigDialog.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectConfigDialog.java
index 6b3b9f69e1..c84f470034 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectConfigDialog.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectConfigDialog.java
@@ -182,6 +182,7 @@ public class ProjectConfigDialog extends Dialog {
Label wlGroup = new Label(shell, SWT.RIGHT);
PropsUi.setLook(wlGroup);
wlGroup.setText(BaseMessages.getString(PKG,
"ProjectConfigDialog.Label.Group"));
+ wlGroup.setToolTipText(BaseMessages.getString(PKG,
"ProjectConfigDialog.Label.Group.Tooltip"));
FormData fdlGroup = new FormData();
fdlGroup.left = new FormAttachment(0, 0);
fdlGroup.right = new FormAttachment(middle, 0);
@@ -189,6 +190,7 @@ public class ProjectConfigDialog extends Dialog {
wlGroup.setLayoutData(fdlGroup);
wGroup = new ComboVar(variables, shell, SWT.SINGLE | SWT.BORDER |
SWT.LEFT);
PropsUi.setLook(wGroup);
+ wGroup.setToolTipText(BaseMessages.getString(PKG,
"ProjectConfigDialog.Label.Group.Tooltip"));
FormData fdGroup = new FormData();
fdGroup.left = new FormAttachment(middle, margin);
fdGroup.right = new FormAttachment(100, 0);
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectMenuGroups.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectMenuGroups.java
new file mode 100644
index 0000000000..a03c0a8318
--- /dev/null
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectMenuGroups.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.gui;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.projects.project.ProjectConfig;
+
+/**
+ * How registered projects are split between the recent list and group
submenus.
+ *
+ * <p>A project with a group topic is not repeated in the recent list. It is
listed under that topic
+ * instead, so a long project list collapses to one menu entry per topic.
+ */
+public final class ProjectMenuGroups {
+
+ private ProjectMenuGroups() {}
+
+ /**
+ * @param projectConfig registration, or null
+ * @return trimmed group topic, or an empty string when the project is
ungrouped
+ */
+ public static String groupOf(ProjectConfig projectConfig) {
+ if (projectConfig == null) {
+ return "";
+ }
+ return StringUtils.trimToEmpty(projectConfig.getGroup());
+ }
+
+ /**
+ * @param projectConfig registration, or null
+ * @return true when the project belongs in a group submenu
+ */
+ public static boolean isGrouped(ProjectConfig projectConfig) {
+ return StringUtils.isNotEmpty(groupOf(projectConfig));
+ }
+
+ /**
+ * Projects that share a group topic.
+ *
+ * <p>The map iteration order is the topic names sorted case-insensitively.
Names inside a topic
+ * are sorted the same way. Blank topics, blank names and null registrations
are omitted.
+ * Whitespace around a topic is ignored, so {@code " Clients "} joins {@code
"Clients"}. Topics
+ * that differ only by case stay separate. A name is listed once per topic.
+ *
+ * @param projects registered projects, or null
+ * @return topic to project names; empty when nothing is grouped
+ */
+ public static Map<String, List<String>> byGroup(List<ProjectConfig>
projects) {
+ if (projects == null || projects.isEmpty()) {
+ return Map.of();
+ }
+ Map<String, List<String>> exact = new LinkedHashMap<>();
+ for (ProjectConfig projectConfig : projects) {
+ if (projectConfig == null) {
+ continue;
+ }
+ String group = groupOf(projectConfig);
+ String name = StringUtils.trimToEmpty(projectConfig.getProjectName());
+ if (StringUtils.isEmpty(group) || StringUtils.isEmpty(name)) {
+ continue;
+ }
+ List<String> names = exact.computeIfAbsent(group, key -> new
ArrayList<>());
+ if (!containsIgnoreCase(names, name)) {
+ names.add(name);
+ }
+ }
+ if (exact.isEmpty()) {
+ return Map.of();
+ }
+ List<String> labels = new ArrayList<>(exact.keySet());
+ labels.sort(String.CASE_INSENSITIVE_ORDER);
+ Map<String, List<String>> ordered = new LinkedHashMap<>();
+ for (String label : labels) {
+ List<String> names = exact.get(label);
+ names.sort(String.CASE_INSENSITIVE_ORDER);
+ ordered.put(label, names);
+ }
+ return ordered;
+ }
+
+ /**
+ * Flat recent-menu entries.
+ *
+ * <p>Only the first {@code max} names are considered, which is the same
window the menu used
+ * before grouping. Names in that window that have a group topic are left
out (they belong in
+ * {@link #byGroup}). A missing registration is treated as ungrouped. Empty
names are skipped but
+ * still consume a slot in the window.
+ *
+ * @param recentNames recent project names, most relevant first, or null
+ * @param lookup registration for a name; null treats every name as ungrouped
+ * @param max number of leading names to consider
+ * @return names to show as top-level recent items, in the same order
+ */
+ public static List<String> recentUngrouped(
+ List<String> recentNames, Function<String, ProjectConfig> lookup, int
max) {
+ if (recentNames == null || recentNames.isEmpty() || max <= 0) {
+ return List.of();
+ }
+ List<String> shown = new ArrayList<>();
+ int considered = 0;
+ for (String name : recentNames) {
+ if (considered == max) {
+ break;
+ }
+ considered++;
+ if (StringUtils.isEmpty(name)) {
+ continue;
+ }
+ ProjectConfig projectConfig = lookup == null ? null : lookup.apply(name);
+ if (isGrouped(projectConfig)) {
+ continue;
+ }
+ shown.add(name);
+ }
+ return shown;
+ }
+
+ private static boolean containsIgnoreCase(List<String> names, String name) {
+ for (String existing : names) {
+ if (existing.equalsIgnoreCase(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
index 10574d65b8..9d81c1e0a8 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
@@ -744,7 +744,8 @@ public class ProjectsGuiPlugin {
new ProjectDialog(
hopGui.getActiveShell(), project, projectConfig,
hopGui.getVariables(), true);
if (projectDialog.open() != null) {
- // Persist project registration (name, home, config path, read-only)
in hop-config.json.
+ // Persist project registration (name, home, config path, group,
read-only) in
+ // hop-config.json.
// A rename also updates the projects using this one as their parent,
all or nothing.
//
ProjectsUtil.saveProjectConfig(
@@ -897,7 +898,8 @@ public class ProjectsGuiPlugin {
new MenuItem(menu, SWT.SEPARATOR);
- // Display the last-used projects that are still registered in hop-config.
+ // Recent projects that have no group. Grouped projects are listed under
their topic below,
+ // not in this flat list, so the menu stays short when projects share a
topic.
// The in-memory list can briefly lag a deletion; drop names that are
already gone.
List<String> names = new ArrayList<>(getLastUsedProjects());
List<String> registeredNames =
ProjectsConfigSingleton.getConfig().listProjectConfigNames();
@@ -916,13 +918,32 @@ public class ProjectsGuiPlugin {
}
String currentProjectName = HopNamespace.getNamespace();
-
- for (String name : names) {
- MenuItem item = new MenuItem(menu, SWT.NONE);
- item.setText(name);
- item.addListener(SWT.Selection, e -> selectProject(name));
- if (currentProjectName.equalsIgnoreCase(name)) {
- item.setImage(GuiResource.getInstance().getImageCheck());
+ ProjectsConfig config = ProjectsConfigSingleton.getConfig();
+ List<String> recent =
+ ProjectMenuGroups.recentUngrouped(
+ names, config::findProjectConfig, LAST_USED_PROJECTS_MAX_ENTRIES);
+ for (String name : recent) {
+ addProjectMenuItem(menu, name, currentProjectName);
+ }
+
+ Map<String, List<String>> groups = groupedProjectNames(config);
+ if (!recent.isEmpty() && !groups.isEmpty()) {
+ new MenuItem(menu, SWT.SEPARATOR);
+ }
+ for (Map.Entry<String, List<String>> entry : groups.entrySet()) {
+ MenuItem groupItem = new MenuItem(menu, SWT.CASCADE);
+ groupItem.setText(entry.getKey());
+ Menu subMenu = new Menu(menu);
+ groupItem.setMenu(subMenu);
+ boolean currentInGroup = false;
+ for (String name : entry.getValue()) {
+ addProjectMenuItem(subMenu, name, currentProjectName);
+ if (isCurrentProject(currentProjectName, name)) {
+ currentInGroup = true;
+ }
+ }
+ if (currentInGroup) {
+ groupItem.setImage(GuiResource.getInstance().getImageCheck());
}
}
@@ -935,6 +956,37 @@ public class ProjectsGuiPlugin {
return menu;
}
+ private void addProjectMenuItem(Menu menu, String name, String
currentProjectName) {
+ MenuItem item = new MenuItem(menu, SWT.NONE);
+ item.setText(name);
+ item.addListener(SWT.Selection, e -> selectProject(name));
+ if (isCurrentProject(currentProjectName, name)) {
+ item.setImage(GuiResource.getInstance().getImageCheck());
+ }
+ }
+
+ private static boolean isCurrentProject(String currentProjectName, String
name) {
+ return StringUtils.isNotEmpty(currentProjectName) &&
currentProjectName.equalsIgnoreCase(name);
+ }
+
+ /**
+ * Group topics to show in the project menu. Projects the current user may
not open are omitted.
+ */
+ private static Map<String, List<String>> groupedProjectNames(ProjectsConfig
config) {
+ if (config == null || config.getProjectConfigurations() == null) {
+ return Map.of();
+ }
+ List<ProjectConfig> visible = new ArrayList<>();
+ for (ProjectConfig projectConfig : config.getProjectConfigurations()) {
+ if (projectConfig == null
+ ||
!ProjectsAccessControl.isProjectAllowed(projectConfig.getProjectName())) {
+ continue;
+ }
+ visible.add(projectConfig);
+ }
+ return ProjectMenuGroups.byGroup(visible);
+ }
+
private Menu createEnvironmentContextMenu() {
Shell shell = HopGui.getInstance().getActiveShell();
Menu menu = new Menu(shell, SWT.POP_UP);
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
index 2ab2d37142..abf07787a8 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
@@ -90,6 +90,7 @@ public class ProjectDialog extends Dialog {
private ComboVar wParentProject;
private TextVar wConfigFile;
private Button wbConfigFile;
+ private ComboVar wGroup;
private Text wDescription;
private Text wCompany;
private Text wDepartment;
@@ -352,6 +353,25 @@ public class ProjectDialog extends Dialog {
wConfigFile.setLayoutData(fdConfigFile);
lastControl = wConfigFile;
+ Label wlGroup = new Label(comp, SWT.RIGHT);
+ PropsUi.setLook(wlGroup);
+ wlGroup.setText(BaseMessages.getString(PKG, "ProjectDialog.Label.Group"));
+ wlGroup.setToolTipText(BaseMessages.getString(PKG,
"ProjectDialog.Label.Group.Tooltip"));
+ FormData fdlGroup = new FormData();
+ fdlGroup.left = new FormAttachment(0, 0);
+ fdlGroup.right = new FormAttachment(middle, 0);
+ fdlGroup.top = new FormAttachment(lastControl, margin);
+ wlGroup.setLayoutData(fdlGroup);
+ wGroup = new ComboVar(variables, comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
+ PropsUi.setLook(wGroup);
+ wGroup.setToolTipText(BaseMessages.getString(PKG,
"ProjectDialog.Label.Group.Tooltip"));
+ FormData fdGroup = new FormData();
+ fdGroup.left = new FormAttachment(middle, margin);
+ fdGroup.right = new FormAttachment(100, 0);
+ fdGroup.top = new FormAttachment(wlGroup, 0, SWT.CENTER);
+ wGroup.setLayoutData(fdGroup);
+ lastControl = wGroup;
+
lastControl =
addLabeledText(
comp,
@@ -1005,6 +1025,9 @@ public class ProjectDialog extends Dialog {
wName.setText(Const.NVL(projectConfig.getProjectName(), ""));
wHome.setText(Const.NVL(projectConfig.getProjectHome(), ""));
wConfigFile.setText(Const.NVL(projectConfig.getConfigFilename(), ""));
+ wGroup.setText(Const.NVL(projectConfig.getGroup(), ""));
+ List<String> groups =
ProjectsConfigSingleton.getConfig().listProjectGroups();
+ wGroup.setItems(groups.toArray(new String[0]));
wReadOnly.setSelection(
projectConfig.isReadOnly()
||
ProjectConfig.isArchiveUri(variables.resolve(projectConfig.getProjectHome())));
@@ -1079,6 +1102,7 @@ public class ProjectDialog extends Dialog {
projectConfig.setProjectName(wName.getText());
projectConfig.setProjectHome(sanitizePath(wHome.getText()));
projectConfig.setConfigFilename(wConfigFile.getText());
+ projectConfig.setGroup(StringUtils.trimToEmpty(wGroup.getText()));
projectConfig.setReadOnly(wReadOnly.getSelection());
project.setParentProjectName(wParentProject.getText());
diff --git
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
index 481988830a..0e1c909b72 100644
---
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
+++
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/gui/messages/messages_en_US.properties
@@ -86,6 +86,7 @@ ProjectConfigDialog.Label.Name=Name
ProjectConfigDialog.Label.Home=Home folder
ProjectConfigDialog.Label.ConfigFile=Configuration file (relative path)
ProjectConfigDialog.Label.Group=Group
+ProjectConfigDialog.Label.Group.Tooltip=Optional topic. Projects that share a
group are listed together under that topic in the project menu, instead of in
the recent list. Leave empty to keep this project in the recent list.
ProjectConfigDialog.Label.Tags=Tags
ProjectConfigDialog.Label.Tags.Tooltip=Comma-separated tags (e.g. etl,
customer-a, legacy)
ProjectConfigDialog.Label.ReadOnly=This project is read only
diff --git
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
index 5f0b5bd38f..ea42215d19 100644
---
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
+++
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
@@ -53,6 +53,8 @@ ProjectDialog.Label.DatasetCSVFolder=Data Sets CSV Folder
(HOP_DATASETS_FOLDER)
ProjectDialog.Label.Department=Department
ProjectDialog.Label.Description=Description
ProjectDialog.Label.EnforceExecutionInHome=Enforce executions in project home
+ProjectDialog.Label.Group=Group
+ProjectDialog.Label.Group.Tooltip=Optional topic. Projects that share a group
are listed together under that topic in the project menu, instead of in the
recent list. Leave empty to keep this project in the recent list.
ProjectDialog.Label.HomeFolder=Home folder
ProjectDialog.Label.MetadataBaseFolder=Metadata base folder
(HOP_METADATA_FOLDER)
ProjectDialog.Label.AutoExportMetadata=Auto-export metadata
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/gui/ProjectMenuGroupsTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/gui/ProjectMenuGroupsTest.java
new file mode 100644
index 0000000000..c3e044453d
--- /dev/null
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/gui/ProjectMenuGroupsTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.gui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.projects.project.ProjectConfig;
+import org.junit.jupiter.api.Test;
+
+class ProjectMenuGroupsTest {
+
+ @Test
+ void blankGroupIsUngrouped() {
+ assertFalse(ProjectMenuGroups.isGrouped(null));
+ assertFalse(ProjectMenuGroups.isGrouped(config("plain", null)));
+ assertFalse(ProjectMenuGroups.isGrouped(config("plain", "")));
+ assertFalse(ProjectMenuGroups.isGrouped(config("plain", " ")));
+ assertTrue(ProjectMenuGroups.isGrouped(config("client", " Clients ")));
+ assertEquals("Clients", ProjectMenuGroups.groupOf(config("client", "
Clients ")));
+ assertEquals("", ProjectMenuGroups.groupOf(null));
+ }
+
+ @Test
+ void groupsTopicsAndSkipsProjectsWithoutATopic() {
+ List<ProjectConfig> projects = new ArrayList<>();
+ projects.add(config("zeta", "beta"));
+ projects.add(config("alpha", "Alpha"));
+ projects.add(config("mid", " alpha "));
+ projects.add(config("none", " "));
+ projects.add(config(" ", "Clients"));
+ projects.add(config("Demo", "Clients"));
+ projects.add(config("demo", "Clients"));
+ projects.add(config("other", "clients"));
+ projects.add(null);
+
+ Map<String, List<String>> groups = ProjectMenuGroups.byGroup(projects);
+
+ assertEquals(
+ List.of("Alpha", "alpha", "beta", "Clients", "clients"), new
ArrayList<>(groups.keySet()));
+ assertEquals(List.of("alpha"), groups.get("Alpha"));
+ assertEquals(List.of("mid"), groups.get("alpha"));
+ assertEquals(List.of("zeta"), groups.get("beta"));
+ assertEquals(List.of("Demo"), groups.get("Clients"));
+ assertEquals(List.of("other"), groups.get("clients"));
+ assertEquals(Map.of(), ProjectMenuGroups.byGroup(null));
+ assertEquals(Map.of(), ProjectMenuGroups.byGroup(List.of()));
+ assertEquals(Map.of(), ProjectMenuGroups.byGroup(List.of(config("solo",
null))));
+ }
+
+ @Test
+ void namesInsideATopicAreSortedIgnoringCase() {
+ Map<String, List<String>> groups =
+ ProjectMenuGroups.byGroup(
+ List.of(config("b", "Topic"), config("A", "Topic"), config("c",
"Topic")));
+
+ assertEquals(List.of("A", "b", "c"), groups.get("Topic"));
+ }
+
+ @Test
+ void recentWindowDropsGroupedNamesAndIgnoresNamesPastTheCap() {
+ Map<String, ProjectConfig> byName = new HashMap<>();
+ byName.put("grouped", config("grouped", "Clients"));
+ byName.put("plain", config("plain", null));
+ byName.put("later", config("later", null));
+
+ assertEquals(
+ List.of("missing", "plain"),
+ ProjectMenuGroups.recentUngrouped(
+ List.of("grouped", "missing", "plain", "later"), byName::get, 3));
+ assertEquals(
+ List.of("missing"),
+ ProjectMenuGroups.recentUngrouped(List.of("grouped", "missing",
"plain"), byName::get, 2));
+ assertEquals(List.of("a"), ProjectMenuGroups.recentUngrouped(List.of("a",
"b"), null, 1));
+ assertEquals(List.of(), ProjectMenuGroups.recentUngrouped(List.of("",
"a"), null, 1));
+ assertEquals(List.of("a"), ProjectMenuGroups.recentUngrouped(List.of("",
"a"), null, 2));
+ assertEquals(List.of(), ProjectMenuGroups.recentUngrouped(null,
byName::get, 5));
+ assertEquals(List.of(),
ProjectMenuGroups.recentUngrouped(List.of("plain"), byName::get, 0));
+ }
+
+ private static ProjectConfig config(String name, String group) {
+ ProjectConfig projectConfig = new ProjectConfig(name, "/tmp/" + name,
"project-config.json");
+ projectConfig.setGroup(group);
+ return projectConfig;
+ }
+}