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 4262beea4b Allow multi-delete, add a check on existing folders, fixes
#8193 (#8194)
4262beea4b is described below
commit 4262beea4b9bf8731d2b64e7b3d2cb233983539d
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Aug 31 20:26:51 2026 +0200
Allow multi-delete, add a check on existing folders, fixes #8193 (#8194)
---
.../perspective/explorer/ExplorerPerspective.java | 221 +++++++++++++++------
.../explorer/messages/messages_en_US.properties | 7 +
2 files changed, 171 insertions(+), 57 deletions(-)
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 03d0c9aea4..2e957d5a42 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
@@ -32,6 +32,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.BooleanSupplier;
import java.util.function.Function;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
@@ -237,6 +238,10 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
"ExplorerPerspective-ContextMenu-10402-FindReferences";
public static final String CONTEXT_MENU_DELETE =
"ExplorerPerspective-ContextMenu-90000-Delete";
private static final String FILE_EXPLORER_TREE = "File explorer tree";
+
+ /** Maximum number of paths listed in the delete confirmation dialog. */
+ private static final int MAX_LISTED_DELETE_PATHS = 20;
+
private static final String TREE_WIDTH_AUDIT_TYPE =
"explorer-perspective-tree-width";
private static final String EXPLORER_AUDIT_TYPE =
"explorer-perspective-state";
@@ -784,7 +789,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
MenuItem deleteItem = menuWidgets.findMenuItem(CONTEXT_MENU_DELETE);
if (deleteItem != null) {
- deleteItem.setEnabled(selection.length == 1);
+ deleteItem.setEnabled(selection.length >= 1);
}
// Creating anything only makes sense inside a folder.
@@ -1227,72 +1232,82 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
}
- private void deleteFile(final TreeItem treeItem) {
+ private void deleteFiles(final TreeItem[] treeItems) {
try {
- TreeItemFolder tif = (TreeItemFolder) treeItem.getData();
- if (tif == null || tif.fileType == null) {
+ // Items nested inside another selected folder are deleted along with
that folder, so drop
+ // them from the list. Otherwise we'd try to delete and dispose them a
second time.
+ //
+ List<TreeItem> items = new ArrayList<>();
+ List<FileObject> fileObjects = new ArrayList<>();
+ for (TreeItem treeItem : removeNestedSelection(treeItems)) {
+ TreeItemFolder tif = (TreeItemFolder) treeItem.getData();
+ if (tif == null || tif.fileType == null) {
+ continue;
+ }
+ items.add(treeItem);
+ fileObjects.add(HopVfs.getFileObject(tif.path));
+ }
+ if (fileObjects.isEmpty()) {
return;
}
- FileObject fileObject = HopVfs.getFileObject(tif.path);
- // For pipeline/workflow files check if any other files or metadata
objects still
- // reference them before showing the standard confirmation box.
- if (!fileObject.isFolder()) {
- String path = HopVfs.getFilename(fileObject);
- if (path.endsWith(".hpl") || path.endsWith(".hwf")) {
- boolean confirmed =
- confirmDeleteWithReferenceCheck(List.of(path),
fileObject.getName().getBaseName());
- if (!confirmed) {
- return;
- }
- // User confirmed via the reference dialog — proceed directly to
deletion.
- List<String> filenames = getRecursiveFilenames(fileObject, new
ArrayList<>());
- fileObject.deleteAll();
- removeTreeItemAfterDelete(treeItem, filenames);
- return;
- }
- } else {
- // For folders: collect all .hpl/.hwf files and check for references
collectively.
- List<String> pipelineWorkflowFiles = new ArrayList<>();
- for (String filename : getRecursiveFilenames(fileObject, new
ArrayList<>())) {
+ // Collect everything which is about to disappear. The pipelines and
workflows in there are
+ // checked for references collectively, for the complete selection at
once.
+ //
+ List<List<String>> filenamesPerItem = new ArrayList<>();
+ List<String> pipelineWorkflowFiles = new ArrayList<>();
+ List<String> paths = new ArrayList<>();
+ for (FileObject fileObject : fileObjects) {
+ List<String> filenames = getRecursiveFilenames(fileObject, new
ArrayList<>());
+ filenamesPerItem.add(filenames);
+ for (String filename : filenames) {
if (filename.endsWith(".hpl") || filename.endsWith(".hwf")) {
pipelineWorkflowFiles.add(filename);
}
}
- if (!pipelineWorkflowFiles.isEmpty()) {
- String folderName = fileObject.getName().getBaseName();
- boolean confirmed =
confirmDeleteWithReferenceCheck(pipelineWorkflowFiles, folderName);
- if (!confirmed) {
- return;
- }
- // User confirmed — proceed directly to deletion.
- List<String> allFilenames = getRecursiveFilenames(fileObject, new
ArrayList<>());
- fileObject.deleteAll();
- removeTreeItemAfterDelete(treeItem, allFilenames);
- return;
- }
+ paths.add(HopVfs.getFilename(fileObject));
}
+ boolean singleFolder = fileObjects.size() == 1 &&
fileObjects.get(0).isFolder();
- // No pipeline/workflow files involved — use the standard confirmation
box.
- String header =
- fileObject.isFolder()
- ? BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFolder.Confirmation.Header")
- : BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFile.Confirmation.Header");
- String message =
- fileObject.isFolder()
- ? BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFolder.Confirmation.Message")
- : BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFile.Confirmation.Message");
+ boolean confirmed;
+ if (pipelineWorkflowFiles.isEmpty()) {
+ confirmed = showDeleteConfirmation(paths, singleFolder);
+ } else {
+ String displayName =
+ fileObjects.size() == 1
+ ? fileObjects.get(0).getName().getBaseName()
+ : BaseMessages.getString(
+ PKG, "ExplorerPerspective.DeleteFiles.Selection",
fileObjects.size());
+ confirmed =
+ confirmDeleteWithReferenceCheck(
+ pipelineWorkflowFiles,
+ displayName,
+ () -> showDeleteConfirmation(paths, singleFolder));
+ }
+ if (!confirmed) {
+ return;
+ }
- MessageBox box = new MessageBox(hopGui.getShell(), SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
- box.setText(header);
- box.setMessage(message + Const.CR + Const.CR +
HopVfs.getFilename(fileObject));
+ if (fileObjects.size() == 1) {
+ if (fileObjects.get(0).deleteAll() > 0) {
+ removeTreeItemAfterDelete(items.get(0), filenamesPerItem.get(0));
+ }
+ return;
+ }
- if ((box.open() & SWT.YES) != 0) {
- List<String> filenames = getRecursiveFilenames(fileObject, new
ArrayList<>());
- if (fileObject.deleteAll() > 0) {
- removeTreeItemAfterDelete(treeItem, filenames);
+ // Refreshing the tree per item would dispose the tree items still
queued up here, so for a
+ // multiple selection everything is deleted first and the tree is
refreshed once afterwards.
+ //
+ List<String> deletedFilenames = new ArrayList<>();
+ for (int i = 0; i < fileObjects.size(); i++) {
+ if (fileObjects.get(i).deleteAll() > 0) {
+ updateLastSelectedFolderAfterDelete(items.get(i));
+ deletedFilenames.addAll(filenamesPerItem.get(i));
}
}
+ closeTabsForFilenames(deletedFilenames);
+ refresh();
+ updateSelection();
} catch (Exception e) {
new ErrorDialog(
hopGui.getShell(),
@@ -1302,6 +1317,66 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
}
+ /** Removes the tree items which have another item of the same selection as
an ancestor. */
+ private static List<TreeItem> removeNestedSelection(TreeItem[] treeItems) {
+ List<TreeItem> selection = Arrays.asList(treeItems);
+ List<TreeItem> topLevel = new ArrayList<>();
+ for (TreeItem treeItem : treeItems) {
+ boolean nested = false;
+ for (TreeItem parent = treeItem.getParentItem();
+ parent != null;
+ parent = parent.getParentItem()) {
+ if (selection.contains(parent)) {
+ nested = true;
+ break;
+ }
+ }
+ if (!nested) {
+ topLevel.add(treeItem);
+ }
+ }
+ return topLevel;
+ }
+
+ /** The plain "are you sure?" confirmation, listing the files and folders
about to be deleted. */
+ private boolean showDeleteConfirmation(List<String> paths, boolean
singleFolder) {
+ String header;
+ String message;
+ if (paths.size() > 1) {
+ header = BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFiles.Confirmation.Header");
+ message =
+ BaseMessages.getString(
+ PKG, "ExplorerPerspective.DeleteFiles.Confirmation.Message",
paths.size());
+ } else if (singleFolder) {
+ header = BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFolder.Confirmation.Header");
+ message =
+ BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFolder.Confirmation.Message");
+ } else {
+ header = BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFile.Confirmation.Header");
+ message = BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFile.Confirmation.Message");
+ }
+
+ StringBuilder listedPaths = new StringBuilder();
+ for (int i = 0; i < paths.size(); i++) {
+ if (i >= MAX_LISTED_DELETE_PATHS) {
+ listedPaths
+ .append(Const.CR)
+ .append(
+ BaseMessages.getString(
+ PKG,
+ "ExplorerPerspective.DeleteFiles.Confirmation.More",
+ paths.size() - MAX_LISTED_DELETE_PATHS));
+ break;
+ }
+ listedPaths.append(Const.CR).append(paths.get(i));
+ }
+
+ MessageBox box = new MessageBox(hopGui.getShell(), SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
+ box.setText(header);
+ box.setMessage(message + Const.CR + listedPaths);
+ return (box.open() & SWT.YES) != 0;
+ }
+
/**
* Replaces the resolved {@code projectHome} prefix in {@code path} with
{@code ${PROJECT_HOME}}
* so displayed paths are project-relative and shorter.
@@ -1321,6 +1396,16 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
*/
public boolean confirmDeleteWithReferenceCheck(List<String> filePaths,
String displayName)
throws HopException {
+ return confirmDeleteWithReferenceCheck(filePaths, displayName, null);
+ }
+
+ /**
+ * Same as {@link #confirmDeleteWithReferenceCheck(List, String)} but with a
custom confirmation
+ * for the case where no references are found. Pass {@code null} for the
standard one.
+ */
+ private boolean confirmDeleteWithReferenceCheck(
+ List<String> filePaths, String displayName, BooleanSupplier
plainConfirmation)
+ throws HopException {
String projectHome = hopGui.getVariables().resolve(Const.VAR_PROJECT_HOME);
List<String> searchRoots =
(!Utils.isEmpty(projectHome) &&
!Const.VAR_PROJECT_HOME.equals(projectHome))
@@ -1368,6 +1453,9 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
// No references — use the standard confirmation box.
+ if (plainConfirmation != null) {
+ return plainConfirmation.getAsBoolean();
+ }
String header =
BaseMessages.getString(PKG,
"ExplorerPerspective.DeleteFile.Confirmation.Header");
String message =
@@ -3378,6 +3466,27 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
newPath += folder;
try {
FileObject newFolder = HopVfs.getFileObject(newPath);
+ if (newFolder.exists()) {
+ // createFolder() silently does nothing for an existing folder and
fails for an existing
+ // file, so tell the user rather than leaving them guessing.
+ //
+ MessageBox box = new MessageBox(getShell(), SWT.OK |
SWT.ICON_WARNING);
+ box.setText(
+ BaseMessages.getString(PKG,
"ExplorerPerspective.CreateFolder.AlreadyExists.Header"));
+ box.setMessage(
+ BaseMessages.getString(
+ PKG,
+ newFolder.isFolder()
+ ?
"ExplorerPerspective.CreateFolder.AlreadyExists.Message"
+ :
"ExplorerPerspective.CreateFolder.FileAlreadyExists.Message",
+ folder,
+ tif.path));
+ box.open();
+
+ // Select the existing folder so it's clear where it is.
+ selectInTree(newPath, false);
+ return;
+ }
newFolder.createFolder();
refresh();
@@ -3517,18 +3626,16 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@GuiOsxKeyboardShortcut(key = SWT.DEL)
public void deleteFile() {
// Shortcut only fires when focus is in file explorer.
- // Deleting is deliberately limited to a single file: the confirmation
(and the reference
- // check behind it) is written for one file at a time.
//
if (!HopSecurityUi.check(Permission.EXPLORER_WRITE)) {
return;
}
TreeItem[] selection = tree.getSelection();
- if (selection == null || selection.length != 1) {
+ if (selection == null || selection.length == 0) {
return;
}
- deleteFile(selection[0]);
+ deleteFiles(selection);
}
@GuiMenuElement(
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 3af710ed66..4a2ab6e761 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
@@ -21,8 +21,15 @@ ExplorerPerspective.CreateFile.Type.Label=File type
ExplorerPerspective.CreateFile.Preview.Label=Will be created as
ExplorerPerspective.CreateFolder.Header=Create directory
ExplorerPerspective.CreateFolder.Message=Please enter name of the folder to
create in: ''{0}''
+ExplorerPerspective.CreateFolder.AlreadyExists.Header=Folder already exists
+ExplorerPerspective.CreateFolder.AlreadyExists.Message=A folder named ''{0}''
already exists in: ''{1}''\nNothing was created.
+ExplorerPerspective.CreateFolder.FileAlreadyExists.Message=A file named
''{0}'' already exists in: ''{1}''\nNo folder was created.
ExplorerPerspective.DeleteFile.Confirmation.Header=Delete file
ExplorerPerspective.DeleteFile.Confirmation.Message=Are you sure you want to
delete the following file?\nYou might not be able to fully undo this operation.
+ExplorerPerspective.DeleteFiles.Confirmation.Header=Delete files
+ExplorerPerspective.DeleteFiles.Confirmation.Message=Are you sure you want to
delete the following {0} selected files and folders?\nAll files and
subdirectories of the selected folders will be deleted.\nYou might not be able
to fully undo this operation.
+ExplorerPerspective.DeleteFiles.Confirmation.More=... and {0} more
+ExplorerPerspective.DeleteFiles.Selection={0} selected files and folders
ExplorerPerspective.DeleteFolder.Confirmation.Header=Delete folder
ExplorerPerspective.DeleteFolder.Confirmation.Message=Are you sure you want to
delete the following folder and all it's files?\nAll files and subdirectories
will be deleted.\nYou might not be able to fully undo this operation.
ExplorerPerspective.Description=Hop Explorer Perspective