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

hansva 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 2c3ea87b36 issue #7797: hard-refresh explorer tree so new files appear 
(#7798)
2c3ea87b36 is described below

commit 2c3ea87b364a7711446c4038723748955f24b69a
Author: Matt Casters <[email protected]>
AuthorDate: Thu Aug 6 15:07:41 2026 +0200

    issue #7797: hard-refresh explorer tree so new files appear (#7798)
    
    Toolbar F5 always rebuilds the project tree (no scoped-only refresh),
    forces VFS FileObject.refresh() before directory listings, and keeps
    scoped refresh on a new context-menu action. Path equality is hardened
    for mixed separators so locate/find works more reliably on Windows.
---
 .../java/org/apache/hop/core/vfs/HopVfsTest.java   | 37 ++++++++
 .../perspective/explorer/ExplorerPathUtils.java    | 56 ++++++++++++
 .../perspective/explorer/ExplorerPerspective.java  | 99 +++++++++++++++++-----
 .../explorer/messages/messages_en_US.properties    |  3 +-
 .../explorer/ExplorerPathUtilsTest.java            | 60 +++++++++++++
 5 files changed, 234 insertions(+), 21 deletions(-)

diff --git a/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java 
b/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
index c732b72637..f7d692c3b0 100644
--- a/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
+++ b/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
@@ -17,11 +17,14 @@
 
 package org.apache.hop.core.vfs;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.hop.core.variables.Variables;
 import org.junit.jupiter.api.Test;
@@ -97,4 +100,38 @@ class HopVfsTest {
       outputStream.write("Test-content".getBytes());
     }
   }
+
+  /**
+   * Commons VFS caches directory children until {@link FileObject#refresh()}. 
Explorer hard-refresh
+   * relies on an explicit refresh before re-listing so externally created 
files appear (issue
+   * #7797).
+   */
+  @Test
+  void testRefreshClearsChildrenCacheForExternalCreates() throws Exception {
+    Path dir = Files.createTempDirectory("hop-vfs-children-");
+    try {
+      FileObject folder = 
HopVfs.getFileObject(dir.toAbsolutePath().toString());
+      assertEquals(0, folder.getChildren().length);
+
+      // Create a file outside this FileObject graph (same as a pipeline 
writing to disk)
+      Files.writeString(dir.resolve("external.txt"), "created outside VFS");
+
+      // Cached children may still look empty until refresh
+      folder.refresh();
+      FileObject[] children = folder.getChildren();
+      assertEquals(1, children.length);
+      assertEquals("external.txt", children[0].getName().getBaseName());
+    } finally {
+      Files.walk(dir)
+          .sorted((a, b) -> b.compareTo(a))
+          .forEach(
+              p -> {
+                try {
+                  Files.deleteIfExists(p);
+                } catch (Exception ignored) {
+                  // best-effort cleanup
+                }
+              });
+    }
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtils.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtils.java
new file mode 100644
index 0000000000..0867577bac
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.perspective.explorer;
+
+/**
+ * Path helpers for the file explorer tree (separator normalization, 
equality). Package-private so
+ * unit tests can cover Windows/Unix path friction without loading SWT.
+ */
+final class ExplorerPathUtils {
+
+  private ExplorerPathUtils() {}
+
+  /**
+   * True when both paths refer to the same location after normalizing 
separators and trailing
+   * slashes. Null-safe ({@code null} equals only {@code null}).
+   */
+  static boolean pathsEqual(String a, String b) {
+    if (a == null || b == null) {
+      return a == b;
+    }
+    if (a.equals(b)) {
+      return true;
+    }
+    return normalizePath(a).equals(normalizePath(b));
+  }
+
+  /**
+   * Normalize path separators to {@code /} and strip trailing slashes (except 
a single root {@code
+   * /}).
+   */
+  static String normalizePath(String path) {
+    if (path == null) {
+      return null;
+    }
+    String n = path.replace('\\', '/');
+    while (n.length() > 1 && n.endsWith("/")) {
+      n = n.substring(0, n.length() - 1);
+    }
+    return n;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 3dfe044525..43d52e2b8e 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -211,6 +211,8 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
       "ExplorerPerspective-ContextMenu-10060-ExpandAll";
   public static final String CONTEXT_MENU_COLLAPSE_ALL =
       "ExplorerPerspective-ContextMenu-10070-CollapseAll";
+  public static final String CONTEXT_MENU_REFRESH_FOLDER =
+      "ExplorerPerspective-ContextMenu-10080-RefreshFolder";
   public static final String CONTEXT_MENU_OPEN = 
"ExplorerPerspective-ContextMenu-10100-Open";
   public static final String CONTEXT_MENU_OPEN_AS_TEXT =
       "ExplorerPerspective-ContextMenu-10101-OpenAsText";
@@ -768,6 +770,12 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
             createFolderItem.setEnabled(selection.length == 1);
           }
 
+          MenuItem refreshFolderItem = 
menuWidgets.findMenuItem(CONTEXT_MENU_REFRESH_FOLDER);
+          if (refreshFolderItem != null) {
+            // Scoped refresh needs a single selection so 
lastSelectedFolderPath is meaningful.
+            refreshFolderItem.setEnabled(selection.length == 1 && tif != null);
+          }
+
           // Finding references is only meaningful for a single pipeline or 
workflow file.
           //
           MenuItem findReferencesItem = 
menuWidgets.findMenuItem(CONTEXT_MENU_FIND_REFERENCES);
@@ -1064,7 +1072,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
 
   /** Depth passed to {@link #refreshFolder} when loading children of {@code 
tif}. */
   private int childDepthForFolder(TreeItemFolder tif) {
-    return tif.path.equals(rootFolder) ? 0 : tif.depth + 1;
+    return ExplorerPathUtils.pathsEqual(tif.path, rootFolder) ? 0 : tif.depth 
+ 1;
   }
 
   /**
@@ -2623,7 +2631,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
 
   private boolean selectInTree(TreeItem item, String filename) {
     TreeItemFolder tif = (TreeItemFolder) item.getData();
-    if (tif != null && tif.path.equals(filename)) {
+    if (tif != null && ExplorerPathUtils.pathsEqual(tif.path, filename)) {
       tree.setSelection(tif.treeItem);
       tree.showItem(tif.treeItem);
       return true;
@@ -3171,6 +3179,31 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     tree.setRedraw(true);
   }
 
+  /**
+   * Context menu: refresh only the selected folder (or the parent of a 
selected file). Cheaper than
+   * toolbar F5 full rebuild; use when you know which folder changed.
+   */
+  @GuiMenuElement(
+      root = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+      parentId = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+      id = CONTEXT_MENU_REFRESH_FOLDER,
+      label = "i18n::ExplorerPerspective.Menu.RefreshFolder",
+      image = "ui/images/refresh.svg",
+      separator = true)
+  public void refreshFolderFromMenu() {
+    TreeItem[] selection = tree.getSelection();
+    if (selection == null || selection.length != 1) {
+      return;
+    }
+    TreeItemFolder tif = (TreeItemFolder) selection[0].getData();
+    if (tif == null) {
+      return;
+    }
+    // Ensure lastSelectedFolderPath matches the context-clicked item
+    recordSelectedFolderPath(tif, selection[0]);
+    refreshSelectedFolder();
+  }
+
   public void expandCollapse(TreeItem item, boolean expand) {
     if (item == null || item.isDisposed()) {
       return;
@@ -3383,22 +3416,29 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     }
   }
 
+  /**
+   * Hard-refresh the explorer tree (toolbar / F5).
+   *
+   * <p>Always rebuilds from disk so files and folders created outside the 
current selection (for
+   * example by a running pipeline) become visible. Clears the filter index 
and re-lists folders via
+   * VFS with an explicit {@link FileObject#refresh()} so cached children do 
not hide new entries.
+   * Expand/collapse state is restored from {@link TreeMemory}.
+   *
+   * <p>For a cheaper single-folder update use {@link 
#refreshSelectedFolder()} (context menu).
+   */
   public void refresh() {
     determineRootFolderName(hopGui);
-    // Structural refresh invalidates the filter index
+    // Structural refresh invalidates the filter index and discards lazy tree 
snapshots
     treeModel.clear();
     if (!Utils.isEmpty(filterText)) {
-      // F5 / structural refresh while filtering: update git status, then 
re-index once
+      // F5 while filtering: update git status, then re-index the full project 
once
       for (IExplorerRefreshListener listener : refreshListeners) {
         listener.beforeRefresh();
       }
       applyFilterFromModel();
       return;
     }
-    if (!Utils.isEmpty(lastSelectedFolderPath) && 
isUnderCurrentRoot(lastSelectedFolderPath)) {
-      refreshSelectedFolder();
-      return;
-    }
+    // Always full rebuild — scoped refresh left only for context menu / 
internal ops
     refreshEntireTree();
   }
 
@@ -3523,14 +3563,13 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
 
   private void loadModelChildren(ExplorerTreeModel.Node parent, String 
metadataFolderName)
       throws Exception {
-    FileObject fileObject = HopVfs.getFileObject(parent.getPath());
     FileObject[] children;
     try {
-      children = fileObject.getChildren();
+      children = listChildrenFresh(parent.getPath());
     } catch (Exception e) {
       return;
     }
-    if (children == null || children.length == 0) {
+    if (children.length == 0) {
       return;
     }
 
@@ -3635,13 +3674,16 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     if (Utils.isEmpty(path) || Utils.isEmpty(rootFolder)) {
       return false;
     }
-    if (path.equals(rootFolder)) {
+    if (ExplorerPathUtils.pathsEqual(path, rootFolder)) {
       return true;
     }
     return isDescendant(rootFolder, path);
   }
 
-  /** Refresh only the last selected folder, keeping the rest of the tree 
intact. */
+  /**
+   * Refresh only the last selected folder, keeping the rest of the tree 
intact. Used by the context
+   * menu and by internal operations (delete parent fix-up); toolbar F5 uses 
{@link #refresh()}.
+   */
   private void refreshSelectedFolder() {
     String folderPath = lastSelectedFolderPath;
     if (Utils.isEmpty(folderPath)
@@ -3759,7 +3801,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
    */
   private void updateLastSelectedFolderAfterDelete(TreeItem treeItem) {
     TreeItemFolder tif = (TreeItemFolder) treeItem.getData();
-    if (tif == null || !tif.path.equals(lastSelectedFolderPath)) {
+    if (tif == null || !ExplorerPathUtils.pathsEqual(tif.path, 
lastSelectedFolderPath)) {
       return;
     }
 
@@ -3804,7 +3846,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
   }
 
   private String getParentFolderPath(String path) {
-    if (Utils.isEmpty(path) || path.equals(rootFolder)) {
+    if (Utils.isEmpty(path) || ExplorerPathUtils.pathsEqual(path, rootFolder)) 
{
       return null;
     }
 
@@ -3978,6 +4020,24 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
     return emptyFileType;
   }
 
+  /**
+   * Resolve a folder and force a VFS re-list so external creates (pipelines, 
other processes) are
+   * visible. Commons VFS caches children on {@link FileObject} until {@link 
FileObject#refresh()}.
+   */
+  private FileObject[] listChildrenFresh(String path) throws Exception {
+    FileObject folder = HopVfs.getFileObject(path);
+    try {
+      folder.refresh();
+    } catch (Exception e) {
+      // Still attempt to list; some providers throw on refresh for 
missing/imaginary files
+      if (hopGui != null && hopGui.getLog() != null) {
+        hopGui.getLog().logDebug("VFS refresh failed for '" + path + "': " + 
e.getMessage());
+      }
+    }
+    FileObject[] children = folder.getChildren();
+    return children != null ? children : new FileObject[0];
+  }
+
   private void refreshFolder(TreeItem item, String path, int depth) {
 
     try {
@@ -3987,9 +4047,8 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
         child.dispose();
       }
 
-      FileObject fileObject = HopVfs.getFileObject(path);
-      FileObject[] children = fileObject.getChildren();
-      if (children == null || children.length == 0) {
+      FileObject[] children = listChildrenFresh(path);
+      if (children.length == 0) {
         return;
       }
 
@@ -4361,7 +4420,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
 
   private TreeItem locateTreeItem(TreeItem item, String path) {
     TreeItemFolder tif = (TreeItemFolder) item.getData();
-    if (tif != null && tif.path.equals(path)) {
+    if (tif != null && ExplorerPathUtils.pathsEqual(tif.path, path)) {
       return item;
     }
 
@@ -4381,7 +4440,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
 
   private TreeItem findTreeItem(TreeItem item, String filename) {
     TreeItemFolder tif = (TreeItemFolder) item.getData();
-    if (tif != null && tif.path.equals(filename)) {
+    if (tif != null && ExplorerPathUtils.pathsEqual(tif.path, filename)) {
       return tif.treeItem;
     }
     for (TreeItem child : item.getItems()) {
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
index ab2c289386..1c87791da8 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
@@ -47,13 +47,14 @@ ExplorerPerspective.Menu.OpenInExplorer=Open in explorer
 ExplorerPerspective.Menu.Rename=Rename
 ExplorerPerspective.Menu.ExpandAll=Expand All
 ExplorerPerspective.Menu.CollapseAll=Collapse All
+ExplorerPerspective.Menu.RefreshFolder=Refresh folder
 ExplorerPerspective.Name=File Explorer
 ExplorerPerspective.Search.Placeholder=Search files and folders (min. 3 
characters)...
 ExplorerPerspective.Tooltip.Active=Click the folder to show/hide the project 
tree
 ExplorerPerspective.ToolbarElement.CreateFolder.Tooltip=Create folder
 ExplorerPerspective.ToolbarElement.Delete.Tooltip=Delete selected file
 ExplorerPerspective.ToolbarElement.Open.Tooltip=Open selected file
-ExplorerPerspective.ToolbarElement.Refresh.Tooltip=Refresh
+ExplorerPerspective.ToolbarElement.Refresh.Tooltip=Refresh the entire project 
tree (clears caches)
 ExplorerPerspective.ToolbarElement.Rename.Tooltip=Rename the selected file
 ExplorerPerspective.ToolbarElement.ExpandAll.Tooltip=Expand all folders
 ExplorerPerspective.ToolbarElement.CollapseAll.Tooltip=Collapse all folders
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtilsTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtilsTest.java
new file mode 100644
index 0000000000..c9cb7a23ba
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtilsTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.perspective.explorer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class ExplorerPathUtilsTest {
+
+  @Test
+  void pathsEqualExactMatch() {
+    assertTrue(ExplorerPathUtils.pathsEqual("/project/a", "/project/a"));
+    assertTrue(ExplorerPathUtils.pathsEqual(null, null));
+    assertFalse(ExplorerPathUtils.pathsEqual(null, "/x"));
+    assertFalse(ExplorerPathUtils.pathsEqual("/x", null));
+  }
+
+  @Test
+  void pathsEqualNormalizesSeparators() {
+    assertTrue(ExplorerPathUtils.pathsEqual("C:\\project\\a", "C:/project/a"));
+    assertTrue(ExplorerPathUtils.pathsEqual("C:/project/a/b", 
"C:\\project\\a\\b"));
+    assertFalse(ExplorerPathUtils.pathsEqual("C:/project/a", "C:/project/b"));
+  }
+
+  @Test
+  void pathsEqualIgnoresTrailingSlash() {
+    assertTrue(ExplorerPathUtils.pathsEqual("/project/a", "/project/a/"));
+    assertTrue(ExplorerPathUtils.pathsEqual("/project/a/", "/project/a"));
+    assertTrue(ExplorerPathUtils.pathsEqual("C:\\project\\a\\", 
"C:/project/a"));
+    // Root slash stays
+    assertTrue(ExplorerPathUtils.pathsEqual("/", "/"));
+  }
+
+  @Test
+  void normalizePath() {
+    assertEquals("/project/a", ExplorerPathUtils.normalizePath("/project/a/"));
+    assertEquals("C:/project/a", 
ExplorerPathUtils.normalizePath("C:\\project\\a\\"));
+    assertEquals("/", ExplorerPathUtils.normalizePath("/"));
+    assertNull(ExplorerPathUtils.normalizePath(null));
+  }
+}

Reply via email to