This is an automated email from the ASF dual-hosted git repository.

mattcasters 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 fefa41f87b Fixes #8736 : Keep the Problems tab on detach, list 
whole-file remarks first and leave submenus out of the actions view (#8741)
fefa41f87b is described below

commit fefa41f87ba331866b9866f610630e2c6334cae8
Author: Bart Maertens <[email protected]>
AuthorDate: Mon Oct 5 21:11:22 2026 +0200

    Fixes #8736 : Keep the Problems tab on detach, list whole-file remarks 
first and leave submenus out of the actions view (#8741)
---
 .../apache/hop/ui/hopgui/file/ProblemsTabTest.java | 173 +++++++++++++++++++++
 .../ui/hopgui/context/menu/MenuContextHandler.java |   6 +
 .../delegates/HopGuiPipelineCheckDelegate.java     |  32 +++-
 .../delegates/HopGuiWorkflowCheckDelegate.java     |  32 +++-
 .../ui/hopgui/messages/messages_en_US.properties   |   2 +
 .../context/menu/MenuContextHandlerTest.java       |  67 ++++++++
 6 files changed, 310 insertions(+), 2 deletions(-)

diff --git 
a/rcp/src/test/java/org/apache/hop/ui/hopgui/file/ProblemsTabTest.java 
b/rcp/src/test/java/org/apache/hop/ui/hopgui/file/ProblemsTabTest.java
new file mode 100644
index 0000000000..7ebd1dbdeb
--- /dev/null
+++ b/rcp/src/test/java/org/apache/hop/ui/hopgui/file/ProblemsTabTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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
+ *
+ *      http://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.ui.hopgui.file;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.ICheckResultSource;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+import 
org.apache.hop.ui.hopgui.file.pipeline.delegates.HopGuiPipelineCheckDelegate;
+import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
+import 
org.apache.hop.ui.hopgui.file.workflow.delegates.HopGuiWorkflowCheckDelegate;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The Problems tab of a pipeline or workflow.
+ *
+ * <p>Detaching the execution results builds the tab again, and it came back 
empty. Remarks about
+ * the pipeline as a whole were listed between two transforms' groups, as if 
they belonged to one.
+ *
+ * @see <a href="https://github.com/apache/hop/issues/8736";>#8736</a>
+ */
+@Tag("uitest")
+class ProblemsTabTest extends SwtBotTestBase {
+
+  private static TransformMeta transform(String name) {
+    TransformMeta transformMeta = new TransformMeta();
+    transformMeta.setName(name);
+    transformMeta.setTransformPluginId("Dummy");
+    return transformMeta;
+  }
+
+  private static ICheckResultSource action(String name) {
+    ICheckResultSource source = mock(ICheckResultSource.class);
+    when(source.getName()).thenReturn(name);
+    return source;
+  }
+
+  private static ICheckResult warning(String text, ICheckResultSource source) {
+    return new CheckResult(ICheckResult.TYPE_RESULT_WARNING, text, source);
+  }
+
+  /** The top-level rows of the tab, with the first child of each, as "group > 
first remark". */
+  private static List<String> rows(Composite parent) {
+    List<String> rows = new ArrayList<>();
+    Tree tree = findTree(parent);
+    for (TreeItem item : tree.getItems()) {
+      rows.add(
+          item.getItemCount() == 0
+              ? item.getText()
+              : item.getText() + " > " + item.getItem(0).getText());
+    }
+    return rows;
+  }
+
+  private static Tree findTree(Composite parent) {
+    for (Control child : parent.getChildren()) {
+      if (child instanceof Tree tree && !tree.isDisposed()) {
+        return tree;
+      }
+      if (child instanceof Composite composite) {
+        Tree tree = findTree(composite);
+        if (tree != null) {
+          return tree;
+        }
+      }
+    }
+    return null;
+  }
+
+  @Test
+  void pipelineRemarksAreOnTopAndSurviveADetach() {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.setName("load-customers");
+    HopGuiPipelineGraph graph = mock(HopGuiPipelineGraph.class);
+    when(graph.getPipelineMeta()).thenReturn(pipelineMeta);
+    List<ICheckResult> remarks =
+        List.of(
+            warning("No description", transform("Dummy (do nothing) 2")),
+            warning("[STRUCT-003] The pipeline has disabled hops", null),
+            warning("No description", transform("Dummy (do nothing)")));
+    List<String> expected =
+        List.of(
+            "load-customers > [STRUCT-003] The pipeline has disabled hops",
+            "Dummy (do nothing) 2 > No description",
+            "Dummy (do nothing) > No description");
+
+    List<List<String>> seen = new ArrayList<>();
+    withScene(
+        shell -> {
+          shell.setLayout(new FillLayout());
+          graph.extraViewTabFolder = new CTabFolder(shell, SWT.NONE);
+          HopGuiPipelineCheckDelegate delegate = new 
HopGuiPipelineCheckDelegate(null, graph);
+          delegate.addPipelineCheck();
+          delegate.refresh(remarks);
+          seen.add(rows(shell));
+
+          graph.extraViewTabFolder.dispose();
+          graph.extraViewTabFolder = new CTabFolder(shell, SWT.NONE);
+          delegate.addPipelineCheck();
+          seen.add(rows(shell));
+        },
+        bot -> {});
+
+    assertEquals(expected, seen.get(0), "as listed");
+    assertEquals(expected, seen.get(1), "after the results view was detached");
+  }
+
+  @Test
+  void workflowRemarksAreOnTopAndSurviveADetach() {
+    WorkflowMeta workflowMeta = new WorkflowMeta();
+    workflowMeta.setName("nightly");
+    HopGuiWorkflowGraph graph = mock(HopGuiWorkflowGraph.class);
+    when(graph.getWorkflowMeta()).thenReturn(workflowMeta);
+    List<ICheckResult> remarks =
+        List.of(
+            warning("No server", action("Mail")),
+            warning("[STRUCT-003] The workflow has disabled hops", null));
+    List<String> expected =
+        List.of("nightly > [STRUCT-003] The workflow has disabled hops", "Mail 
> No server");
+
+    List<List<String>> seen = new ArrayList<>();
+    withScene(
+        shell -> {
+          shell.setLayout(new FillLayout());
+          graph.extraViewTabFolder = new CTabFolder(shell, SWT.NONE);
+          HopGuiWorkflowCheckDelegate delegate = new 
HopGuiWorkflowCheckDelegate(null, graph);
+          delegate.addWorkflowCheck();
+          delegate.refresh(remarks);
+          seen.add(rows(shell));
+
+          graph.extraViewTabFolder.dispose();
+          graph.extraViewTabFolder = new CTabFolder(shell, SWT.NONE);
+          delegate.addWorkflowCheck();
+          seen.add(rows(shell));
+        },
+        bot -> {});
+
+    assertEquals(expected, seen.get(0), "as listed");
+    assertEquals(expected, seen.get(1), "after the results view was detached");
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/context/menu/MenuContextHandler.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/context/menu/MenuContextHandler.java
index 38096cff14..91751e8ef8 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/context/menu/MenuContextHandler.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/context/menu/MenuContextHandler.java
@@ -70,6 +70,12 @@ public class MenuContextHandler implements 
IGuiContextHandler {
         continue;
       }
 
+      // An item with children is a submenu: the menu opens it, and as an 
action it did nothing.
+      // Its children are listed under its label as their category.
+      if (!registry.findChildGuiMenuItems(rootMenuId, item.getId()).isEmpty()) 
{
+        continue;
+      }
+
       String parentId = item.getParentId();
       if (parentId != null) {
         GuiMenuItem parentMenuItem = registry.findGuiMenuItem(rootMenuId, 
parentId);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineCheckDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineCheckDelegate.java
index 56ef579fe1..d9db46aab8 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineCheckDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineCheckDelegate.java
@@ -30,6 +30,7 @@ import org.apache.hop.core.Props;
 import org.apache.hop.core.SwtUniversalImage;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
@@ -71,6 +72,12 @@ public class HopGuiPipelineCheckDelegate {
   @Getter private GuiToolbarWidgets toolBarWidgets;
   private Tree wTree;
 
+  /**
+   * What the tab shows. Detaching or docking the results view builds the tab 
again, empty; the
+   * remarks are put back from here.
+   */
+  private List<ICheckResult> shownRemarks = List.of();
+
   /**
    * Check pipeline and transforms
    *
@@ -151,6 +158,10 @@ public class HopGuiPipelineCheckDelegate {
     fdTree.bottom = new FormAttachment(100, 0);
     wTree.setLayoutData(fdTree);
     wTree.addListener(SWT.DefaultSelection, this::edit);
+
+    if (!shownRemarks.isEmpty()) {
+      refresh(shownRemarks);
+    }
   }
 
   @GuiToolbarElement(
@@ -257,9 +268,13 @@ public class HopGuiPipelineCheckDelegate {
    * @param remarks the remarks to show
    */
   public void refresh(List<ICheckResult> remarks) {
+    shownRemarks = List.copyOf(remarks);
     wTree.setRedraw(false);
     wTree.removeAll();
 
+    // Remarks about the pipeline as a whole sit together at the top, under 
its name. Added at the
+    // top level in arrival order, they landed between two groups and read as 
belonging to one.
+    TreeItem pipelineItem = null;
     Map<ICheckResultSource, TreeItem> mapSourceItems = new HashMap<>();
     for (ICheckResult cr : remarks) {
       // Ignore OK result
@@ -268,7 +283,12 @@ public class HopGuiPipelineCheckDelegate {
       ICheckResultSource source = cr.getSourceInfo();
       TreeItem item = mapSourceItems.get(source);
       if (source == null) {
-        item = new TreeItem(wTree, SWT.NONE);
+        if (pipelineItem == null) {
+          pipelineItem = new TreeItem(wTree, SWT.NONE, 0);
+          pipelineItem.setText(pipelineLabel());
+          pipelineItem.setImage(GuiResource.getInstance().getImagePipeline());
+        }
+        item = new TreeItem(pipelineItem, SWT.NONE);
       } else if (item == null) {
         TreeItem parentItem = new TreeItem(wTree, SWT.NONE);
         parentItem.setText(source.getName());
@@ -297,9 +317,19 @@ public class HopGuiPipelineCheckDelegate {
         item.setImage(image);
       }
     }
+    if (pipelineItem != null) {
+      pipelineItem.setExpanded(true);
+    }
     wTree.setRedraw(true);
   }
 
+  private String pipelineLabel() {
+    String name = pipelineGraph.getPipelineMeta().getName();
+    return Utils.isEmpty(name)
+        ? BaseMessages.getString(PKG, "PipelineGraph.Check.PipelineRemarks")
+        : name;
+  }
+
   private Image getImage(ICheckResult cr) {
     return switch (cr.getType()) {
       case ICheckResult.TYPE_RESULT_OK -> 
GuiResource.getInstance().getImageTrue();
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowCheckDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowCheckDelegate.java
index 98c77b93f8..cc8fc28df1 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowCheckDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowCheckDelegate.java
@@ -29,6 +29,7 @@ import org.apache.hop.core.Props;
 import org.apache.hop.core.SwtUniversalImage;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
@@ -71,6 +72,12 @@ public class HopGuiWorkflowCheckDelegate {
   @Getter private GuiToolbarWidgets toolBarWidgets;
   private Tree wTree;
 
+  /**
+   * What the tab shows. Detaching or docking the results view builds the tab 
again, empty; the
+   * remarks are put back from here.
+   */
+  private List<ICheckResult> shownRemarks = List.of();
+
   /**
    * Check workflow and actions
    *
@@ -152,6 +159,10 @@ public class HopGuiWorkflowCheckDelegate {
     wTree.addListener(SWT.DefaultSelection, this::edit);
 
     workflowCheckTab.setControl(checkComposite);
+
+    if (!shownRemarks.isEmpty()) {
+      refresh(shownRemarks);
+    }
   }
 
   @GuiToolbarElement(
@@ -231,9 +242,13 @@ public class HopGuiWorkflowCheckDelegate {
    * @param remarks the remarks to show
    */
   public void refresh(List<ICheckResult> remarks) {
+    shownRemarks = List.copyOf(remarks);
     wTree.setRedraw(false);
     wTree.removeAll();
 
+    // Remarks about the workflow as a whole sit together at the top, under 
its name. Added at the
+    // top level in arrival order, they landed between two groups and read as 
belonging to one.
+    TreeItem workflowItem = null;
     Map<ICheckResultSource, TreeItem> mapSourceItems = new HashMap<>();
     for (ICheckResult cr : remarks) {
       // Ignore OK result
@@ -242,7 +257,12 @@ public class HopGuiWorkflowCheckDelegate {
       ICheckResultSource source = cr.getSourceInfo();
       TreeItem item = mapSourceItems.get(source);
       if (source == null) {
-        item = new TreeItem(wTree, SWT.NONE);
+        if (workflowItem == null) {
+          workflowItem = new TreeItem(wTree, SWT.NONE, 0);
+          workflowItem.setText(workflowLabel());
+          workflowItem.setImage(GuiResource.getInstance().getImageWorkflow());
+        }
+        item = new TreeItem(workflowItem, SWT.NONE);
       } else if (item == null) {
         TreeItem parentItem = new TreeItem(wTree, SWT.NONE);
         parentItem.setText(source.getName());
@@ -271,9 +291,19 @@ public class HopGuiWorkflowCheckDelegate {
       }
     }
 
+    if (workflowItem != null) {
+      workflowItem.setExpanded(true);
+    }
     wTree.setRedraw(true);
   }
 
+  private String workflowLabel() {
+    String name = workflowGraph.getWorkflowMeta().getName();
+    return Utils.isEmpty(name)
+        ? BaseMessages.getString(PKG, "WorkflowGraph.Check.WorkflowRemarks")
+        : name;
+  }
+
   private Image getImage(ICheckResult cr) {
     return switch (cr.getType()) {
       case ICheckResult.TYPE_RESULT_OK -> 
GuiResource.getInstance().getImageTrue();
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
index 83ee807799..37e33ec483 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
@@ -195,6 +195,7 @@ LogBrowser.Filter.CaseSensitive.Tooltip=Match the filter 
text with case sensitiv
 LogBrowser.Filter.Exclude.Label=Exclude
 LogBrowser.Filter.Exclude.Tooltip=Hide log lines that match the filter text 
(exclude from the log view)
 PipelineGraph.Check.Tab.Name=Problems
+PipelineGraph.Check.PipelineRemarks=Pipeline
 PipelineGraph.Check.ErrorCheckingPipeline.Exception=Error checking pipeline: 
{0}
 PipelineGraph.Check.ErrorCheckingPipeline.Message=Error checking pipeline
 PipelineGraph.ContextualActionDialog.Hop.Header=Select the hop action to take:
@@ -355,6 +356,7 @@ PipelineLog.System.ERROR2=ERROR
 PipelineLog.System.EXCEPTION=EXCEPTION
 PipelineLog.System.EXCEPTION2=EXCEPTION
 WorkflowGraph.Check.Tab.Name=Problems
+WorkflowGraph.Check.WorkflowRemarks=Workflow
 WorkflowGraph.Check.ErrorCheckingWorkflow.Exception=Error checking workflow: 
{0}
 WorkflowGraph.Check.ErrorCheckingWorkflow.Message=Error checking workflow
 WorkflowGraph.Dialog.HopCausesLoop.Message=This hop causes a loop. This is not 
allowed.
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/context/menu/MenuContextHandlerTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/context/menu/MenuContextHandlerTest.java
new file mode 100644
index 0000000000..082f4fcefb
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/context/menu/MenuContextHandlerTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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
+ *
+ *      http://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.ui.hopgui.context.menu;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import org.apache.hop.core.gui.plugin.GuiRegistry;
+import org.apache.hop.core.gui.plugin.action.GuiAction;
+import org.apache.hop.core.gui.plugin.menu.GuiMenuElementType;
+import org.apache.hop.core.gui.plugin.menu.GuiMenuItem;
+import org.apache.hop.ui.core.gui.GuiMenuWidgets;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The menu as actions, in the searchable actions view.
+ *
+ * <p>A submenu was listed as an action of its own that did nothing when 
clicked, next to the
+ * actions it contains.
+ *
+ * @see <a href="https://github.com/apache/hop/issues/8736";>#8736</a>
+ */
+class MenuContextHandlerTest {
+
+  private static final String ROOT = "MenuContextHandlerTest-Menu";
+
+  private static void addItem(String id, String parentId, String label) {
+    GuiMenuItem item = new GuiMenuItem();
+    item.setRoot(ROOT);
+    item.setId(id);
+    item.setParentId(parentId);
+    item.setLabel(label);
+    item.setType(GuiMenuElementType.MENU_ITEM);
+    item.setClassLoader(MenuContextHandlerTest.class.getClassLoader());
+    GuiRegistry.getInstance().addGuiMenuItem(ROOT, item);
+  }
+
+  @Test
+  void aSubmenuIsNotAnAction() {
+    addItem("10000-tools", ROOT, "Tools");
+    addItem("10100-tools-export", "10000-tools", "Export");
+    addItem("10110-tools-export-csv", "10100-tools-export", "Export to CSV");
+    addItem("10200-tools-search", "10000-tools", "Search");
+
+    List<GuiAction> actions =
+        new MenuContextHandler(ROOT, new 
GuiMenuWidgets()).getSupportedActions();
+
+    assertEquals(
+        List.of("10110-tools-export-csv", "10200-tools-search"),
+        actions.stream().map(GuiAction::getId).toList());
+    assertEquals("Export", actions.get(0).getCategory(), "the submenu names 
its children's group");
+  }
+}

Reply via email to