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 6991014ec7 Issue #5275 : Add an Open button to dialogs that reference 
a pipeline or workflow (#8697)
6991014ec7 is described below

commit 6991014ec7374b8441591ea53ae2aab44b78a9db
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 09:16:59 2026 +0200

    Issue #5275 : Add an Open button to dialogs that reference a pipeline or 
workflow (#8697)
---
 .../hop/workflow/actions/repeat/RepeatDialog.java  |  24 ++-
 .../eventhubs/listen/AzureListenerDialog.java      |  24 ++-
 .../kafka/consumer/KafkaConsumerInputDialog.java   |  26 ++-
 .../transforms/mapping/SimpleMappingDialog.java    |  25 ++-
 .../multimapping/MultiMappingDialog.java           |  24 ++-
 .../transforms/metainject/MetaInjectDialog.java    |  25 ++-
 .../pipelineexecutor/PipelineExecutorDialog.java   |  27 ++-
 .../workflowexecutor/WorkflowExecutorDialog.java   |  27 ++-
 .../hop/ui/hopgui/file/ReferencedFileOpener.java   | 186 +++++++++++++++++++++
 .../hop/ui/workflow/actions/ActionBaseDialog.java  |  26 ++-
 .../hopgui/file/messages/messages_en_US.properties |   7 +
 .../ui/hopgui/file/ReferencedFileOpenerTest.java   |  69 ++++++++
 12 files changed, 469 insertions(+), 21 deletions(-)

diff --git 
a/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/RepeatDialog.java
 
b/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/RepeatDialog.java
index 028cf506f4..14b54ad2f4 100644
--- 
a/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/RepeatDialog.java
+++ 
b/plugins/actions/repeat/src/main/java/org/apache/hop/workflow/actions/repeat/RepeatDialog.java
@@ -34,6 +34,7 @@ import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.hopgui.file.workflow.HopWorkflowFileType;
 import org.apache.hop.ui.workflow.action.ActionDialog;
@@ -125,11 +126,19 @@ public class RepeatDialog extends ActionDialog {
     wbbFilename.setLayoutData(fdbFilename);
     wbbFilename.addListener(SWT.Selection, e -> browseForFile());
 
+    Button wbOpen = new Button(shell, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbbFilename, -margin);
+    fdOpen.top = new FormAttachment(wlFilename, 0, SWT.CENTER);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
+
     wFilename = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
     PropsUi.setLook(wFilename);
     FormData fdFilename = new FormData();
     fdFilename.left = new FormAttachment(middle, 0);
-    fdFilename.right = new FormAttachment(wbbFilename, -margin);
+    fdFilename.right = new FormAttachment(wbOpen, -margin);
     fdFilename.top = new FormAttachment(wlFilename, 0, SWT.CENTER);
     wFilename.setLayoutData(fdFilename);
     lastControl = wFilename;
@@ -566,6 +575,19 @@ public class RepeatDialog extends ActionDialog {
     action.setChanged();
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wFilename.getText(),
+        ReferencedFileOpener.isDialogModified(
+            action.hasChanged(), wFilename.getText(), action.getFilename()),
+        () -> {
+          ok();
+          return isDisposed() ? action.getFilename() : null;
+        });
+  }
+
   private void ok() {
     if (Utils.isEmpty(wName.getText())) {
       MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
diff --git 
a/plugins/tech/azure/src/main/java/org/apache/hop/pipeline/transforms/eventhubs/listen/AzureListenerDialog.java
 
b/plugins/tech/azure/src/main/java/org/apache/hop/pipeline/transforms/eventhubs/listen/AzureListenerDialog.java
index 4e7e8a322a..687d33911f 100644
--- 
a/plugins/tech/azure/src/main/java/org/apache/hop/pipeline/transforms/eventhubs/listen/AzureListenerDialog.java
+++ 
b/plugins/tech/azure/src/main/java/org/apache/hop/pipeline/transforms/eventhubs/listen/AzureListenerDialog.java
@@ -29,6 +29,7 @@ import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
@@ -412,12 +413,19 @@ public class AzureListenerDialog extends 
BaseTransformDialog {
     fdbBatchPipeline.right = new FormAttachment(100, -margin);
     fdbBatchPipeline.top = new FormAttachment(wlBatchPipeline, 0, SWT.CENTER);
     wbBatchPipeline.setLayoutData(fdbBatchPipeline);
+    Button wbOpen = new Button(wContent, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbBatchPipeline, -margin);
+    fdOpen.top = new FormAttachment(wlBatchPipeline, 0, SWT.CENTER);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
     wBatchPipeline = new TextVar(variables, wContent, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
     PropsUi.setLook(wBatchPipeline);
     wBatchPipeline.addModifyListener(lsMod);
     FormData fdBatchPipeline = new FormData();
     fdBatchPipeline.left = new FormAttachment(middle, 0);
-    fdBatchPipeline.right = new FormAttachment(wbBatchPipeline, -margin);
+    fdBatchPipeline.right = new FormAttachment(wbOpen, -margin);
     fdBatchPipeline.top = new FormAttachment(wlBatchPipeline, 0, SWT.CENTER);
     wBatchPipeline.setLayoutData(fdBatchPipeline);
     lastControl = wBatchPipeline;
@@ -501,12 +509,26 @@ public class AzureListenerDialog extends 
BaseTransformDialog {
     sc.setMinHeight(bounds.height);
 
     getData();
+    input.setChanged(changed);
     focusTransformName();
     BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
 
     return transformName;
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wBatchPipeline.getText(),
+        ReferencedFileOpener.isDialogModified(
+            input.hasChanged(), wBatchPipeline.getText(), 
input.getBatchPipeline()),
+        () -> {
+          ok();
+          return isDisposed() ? input.getBatchPipeline() : null;
+        });
+  }
+
   private void browseForPipeline() {
     HopPipelineFileType<PipelineMeta> type = 
HopGui.getExplorerPerspective().getPipelineFileType();
 
diff --git 
a/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputDialog.java
 
b/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputDialog.java
index fc3a9f29d0..65bfc0504b 100644
--- 
a/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputDialog.java
+++ 
b/plugins/transforms/kafka/src/main/java/org/apache/hop/pipeline/transforms/kafka/consumer/KafkaConsumerInputDialog.java
@@ -56,6 +56,7 @@ import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.apache.kafka.clients.consumer.ConsumerConfig;
@@ -199,11 +200,20 @@ public class KafkaConsumerInputDialog extends 
BaseTransformDialog {
               true);
         });
 
+    Button wbOpen = new Button(shell, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbFilename, -margin);
+    fdOpen.top = new FormAttachment(wlFilename, 0, SWT.CENTER);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
+
     wFilename = new TextVar(variables, shell, SWT.SINGLE | SWT.BORDER | 
SWT.LEFT);
     PropsUi.setLook(wFilename);
+    wFilename.addModifyListener(lsMod);
     FormData fdFilename = new FormData();
     fdFilename.left = new FormAttachment(wlFilename, margin);
-    fdFilename.right = new FormAttachment(wbFilename, -PropsUi.getMargin());
+    fdFilename.right = new FormAttachment(wbOpen, -PropsUi.getMargin());
     fdFilename.top = new FormAttachment(wlFilename, 0, SWT.CENTER);
     wFilename.setLayoutData(fdFilename);
 
@@ -263,6 +273,7 @@ public class KafkaConsumerInputDialog extends 
BaseTransformDialog {
     createAdditionalTabs();
 
     getData();
+    meta.setChanged(changed);
     wTabFolder.setSelection(0);
     focusTransformName();
     BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
@@ -975,6 +986,19 @@ public class KafkaConsumerInputDialog extends 
BaseTransformDialog {
         .toArray();
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wFilename.getText(),
+        ReferencedFileOpener.isDialogModified(
+            meta.hasChanged(), wFilename.getText(), meta.getFilename()),
+        () -> {
+          ok();
+          return isDisposed() ? meta.getFilename() : null;
+        });
+  }
+
   protected void createNewKafkaPipeline() {
     PipelineMeta kafkaPipelineMeta = createSubPipelineMeta();
 
diff --git 
a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingDialog.java
 
b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingDialog.java
index 855f84e485..4e5b7944bf 100644
--- 
a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingDialog.java
+++ 
b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingDialog.java
@@ -45,6 +45,7 @@ import org.apache.hop.ui.core.widget.ColumnsResizer;
 import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.apache.hop.ui.pipeline.transform.ITableItemInsertListener;
@@ -191,12 +192,21 @@ public class SimpleMappingDialog extends 
BaseTransformDialog {
     wbBrowse.setLayoutData(fdBrowse);
     wbBrowse.addListener(SWT.Selection, e -> selectFilePipeline());
 
+    Button wbOpen = new Button(shell, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbBrowse, -margin);
+    fdOpen.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
+
     wPath = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
     PropsUi.setLook(wPath);
+    wPath.addModifyListener(lsMod);
     FormData fdTransformation = new FormData();
     fdTransformation.left = new FormAttachment(wlPath, margin);
     fdTransformation.top = new FormAttachment(wlPath, 0, SWT.CENTER);
-    fdTransformation.right = new FormAttachment(wbBrowse, -margin);
+    fdTransformation.right = new FormAttachment(wbOpen, -margin);
     wPath.setLayoutData(fdTransformation);
 
     // The run configuration
@@ -241,6 +251,19 @@ public class SimpleMappingDialog extends 
BaseTransformDialog {
     return transformName;
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wPath.getText(),
+        ReferencedFileOpener.isDialogModified(
+            mappingMeta.hasChanged(), wPath.getText(), 
mappingMeta.getFilename()),
+        () -> {
+          ok();
+          return isDisposed() ? mappingMeta.getFilename() : null;
+        });
+  }
+
   private void selectFilePipeline() {
 
     try {
diff --git 
a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
 
b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
index d2324aad4c..77829cd161 100644
--- 
a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
+++ 
b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
@@ -52,7 +52,7 @@ import org.apache.hop.ui.core.widget.ColumnsResizer;
 import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
-import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
@@ -281,18 +281,16 @@ public class MultiMappingDialog extends 
BaseTransformDialog {
   }
 
   private void editPipeline() {
-    try {
-      if (Utils.isEmpty(wPath.getText())) {
-        return;
-      }
-      
HopGui.getInstance().fileDelegate.fileOpen(variables.resolve(wPath.getText()));
-    } catch (Exception ex) {
-      new ErrorDialog(
-          shell,
-          BaseMessages.getString(PKG, 
"MultiMappingDialog.ErrorLoadingPipeline.DialogTitle"),
-          BaseMessages.getString(PKG, 
"MultiMappingDialog.ErrorLoadingPipeline.DialogMessage"),
-          ex);
-    }
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wPath.getText(),
+        ReferencedFileOpener.isDialogModified(
+            mappingMeta.hasChanged(), wPath.getText(), 
mappingMeta.getFilename()),
+        () -> {
+          ok();
+          return isDisposed() ? mappingMeta.getFilename() : null;
+        });
   }
 
   private void loadPipelineFile(String fname) throws HopException {
diff --git 
a/plugins/transforms/metainject/src/main/java/org/apache/hop/pipeline/transforms/metainject/MetaInjectDialog.java
 
b/plugins/transforms/metainject/src/main/java/org/apache/hop/pipeline/transforms/metainject/MetaInjectDialog.java
index 991ae51e14..6db0eda5f1 100644
--- 
a/plugins/transforms/metainject/src/main/java/org/apache/hop/pipeline/transforms/metainject/MetaInjectDialog.java
+++ 
b/plugins/transforms/metainject/src/main/java/org/apache/hop/pipeline/transforms/metainject/MetaInjectDialog.java
@@ -59,6 +59,7 @@ import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
@@ -201,12 +202,21 @@ public class MetaInjectDialog extends BaseTransformDialog 
{
     fdBrowse.top = new FormAttachment(wlPath, Const.isOSX() ? 0 : 5);
     wbBrowse.setLayoutData(fdBrowse);
 
+    Button wbOpen = new Button(shell, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbBrowse, -PropsUi.getMargin());
+    fdOpen.top = new FormAttachment(wlPath, Const.isOSX() ? 0 : 5);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
+
     wPath = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
     PropsUi.setLook(wPath);
+    wPath.addModifyListener(lsMod);
     FormData fdTransformation = new FormData();
     fdTransformation.left = new FormAttachment(0, 0);
     fdTransformation.top = new FormAttachment(wlPath, 5);
-    fdTransformation.right = new FormAttachment(wbBrowse, 
-PropsUi.getMargin());
+    fdTransformation.right = new FormAttachment(wbOpen, -PropsUi.getMargin());
     wPath.setLayoutData(fdTransformation);
     wPath.addFocusListener(
         new FocusAdapter() {
@@ -859,6 +869,19 @@ public class MetaInjectDialog extends BaseTransformDialog {
     }
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wPath.getText(),
+        ReferencedFileOpener.isDialogModified(
+            metaInjectMeta.hasChanged(), wPath.getText(), 
metaInjectMeta.getTemplateFileName()),
+        () -> {
+          ok();
+          return isDisposed() ? metaInjectMeta.getTemplateFileName() : null;
+        });
+  }
+
   private void selectFileTrans() {
     try {
       HopPipelineFileType<PipelineMeta> fileType =
diff --git 
a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorDialog.java
 
b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorDialog.java
index 0b1b2fbf5e..45ec3481b7 100644
--- 
a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorDialog.java
+++ 
b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorDialog.java
@@ -48,6 +48,7 @@ import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
@@ -88,6 +89,7 @@ public class PipelineExecutorDialog extends 
BaseTransformDialog {
 
   private Label wlPath;
   private TextVar wPath;
+  private Button wbOpen;
 
   protected MetaSelectionLine<PipelineRunConfiguration> wRunConfiguration;
 
@@ -180,12 +182,21 @@ public class PipelineExecutorDialog extends 
BaseTransformDialog {
     wbBrowse.setLayoutData(fdBrowse);
     wbBrowse.addListener(SWT.Selection, e -> selectPipelineFile());
 
+    wbOpen = new Button(shell, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbBrowse, -margin);
+    fdOpen.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
+
     wPath = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
     PropsUi.setLook(wPath);
+    wPath.addModifyListener(lsMod);
     FormData fdTransformation = new FormData();
     fdTransformation.left = new FormAttachment(middle, 0);
     fdTransformation.top = new FormAttachment(wlPath, 0, SWT.CENTER);
-    fdTransformation.right = new FormAttachment(wbBrowse, -margin);
+    fdTransformation.right = new FormAttachment(wbOpen, -margin);
     wPath.setLayoutData(fdTransformation);
 
     wbPipelineNameInField = new Button(shell, SWT.CHECK);
@@ -721,11 +732,25 @@ public class PipelineExecutorDialog extends 
BaseTransformDialog {
     }
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wPath.getText(),
+        ReferencedFileOpener.isDialogModified(
+            pipelineExecutorMeta.hasChanged(), wPath.getText(), 
pipelineExecutorMeta.getFilename()),
+        () -> {
+          ok();
+          return isDisposed() ? pipelineExecutorMeta.getFilename() : null;
+        });
+  }
+
   private void activePipelineNameField() {
     wlPipelineNameField.setEnabled(wbPipelineNameInField.getSelection());
     wPipelineNameField.setEnabled(wbPipelineNameInField.getSelection());
     wPath.setEnabled(!wbPipelineNameInField.getSelection());
     wlPath.setEnabled(!wbPipelineNameInField.getSelection());
+    wbOpen.setEnabled(!wbPipelineNameInField.getSelection());
     if (wbPipelineNameInField.getSelection()) {
       wPath.setText("");
     } else {
diff --git 
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorDialog.java
 
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorDialog.java
index 7051086578..c868989d71 100644
--- 
a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorDialog.java
+++ 
b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorDialog.java
@@ -46,6 +46,7 @@ import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.hopgui.file.workflow.HopWorkflowFileType;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.apache.hop.workflow.WorkflowMeta;
@@ -87,6 +88,7 @@ public class WorkflowExecutorDialog extends 
BaseTransformDialog {
   private Label wlPath;
   private TextVar wPath;
   private Button wbBrowse;
+  private Button wbOpen;
 
   private Button wbWorkflowNameInField;
 
@@ -182,12 +184,21 @@ public class WorkflowExecutorDialog extends 
BaseTransformDialog {
     wbBrowse.setLayoutData(fdBrowse);
     wbBrowse.addListener(SWT.Selection, e -> selectWorkflowFile());
 
+    wbOpen = new Button(shell, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbBrowse, -margin);
+    fdOpen.bottom = new FormAttachment(wlPath, 0, SWT.CENTER);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
+
     wPath = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
     PropsUi.setLook(wPath);
+    wPath.addModifyListener(lsMod);
     FormData fdPath = new FormData();
     fdPath.left = new FormAttachment(wlPath, margin);
     fdPath.top = new FormAttachment(wlPath, 0, SWT.CENTER);
-    fdPath.right = new FormAttachment(wbBrowse, -margin);
+    fdPath.right = new FormAttachment(wbOpen, -margin);
     wPath.setLayoutData(fdPath);
 
     wbWorkflowNameInField = new Button(shell, SWT.CHECK);
@@ -332,12 +343,26 @@ public class WorkflowExecutorDialog extends 
BaseTransformDialog {
     }
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wPath.getText(),
+        ReferencedFileOpener.isDialogModified(
+            workflowExecutorMeta.hasChanged(), wPath.getText(), 
workflowExecutorMeta.getFilename()),
+        () -> {
+          ok();
+          return isDisposed() ? workflowExecutorMeta.getFilename() : null;
+        });
+  }
+
   private void activeWorkflowNameField() {
     wlWorkflowNameField.setEnabled(wbWorkflowNameInField.getSelection());
     wWorkflowNameField.setEnabled(wbWorkflowNameInField.getSelection());
     wPath.setEnabled(!wbWorkflowNameInField.getSelection());
     wlPath.setEnabled(!wbWorkflowNameInField.getSelection());
     wbBrowse.setEnabled(!wbWorkflowNameInField.getSelection());
+    wbOpen.setEnabled(!wbWorkflowNameInField.getSelection());
     if (wbWorkflowNameInField.getSelection()) {
       wPath.setText("");
     } else {
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/ReferencedFileOpener.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/ReferencedFileOpener.java
new file mode 100644
index 0000000000..c174ac82b7
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/ReferencedFileOpener.java
@@ -0,0 +1,186 @@
+/*
+ * 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 java.util.function.Supplier;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.security.HopDialogEditGuard;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Opens the pipeline or workflow selected in a transform or action dialog.
+ *
+ * <p>When the dialog has unsaved edits, the user is asked to save them first. 
Saving applies the
+ * dialog (the same as OK) and then opens the file. Declining opens the file 
and leaves the dialog
+ * open. Cancelling does nothing.
+ */
+public final class ReferencedFileOpener {
+  private static final Class<?> PKG = ReferencedFileOpener.class;
+
+  private ReferencedFileOpener() {}
+
+  /** What to do after the optional save question. */
+  public enum Decision {
+    APPLY_AND_OPEN,
+    OPEN,
+    CANCEL
+  }
+
+  /**
+   * @param modified whether the dialog has edits that OK would keep
+   * @param answer a {@link SWT} yes/no/cancel code; ignored when {@code 
modified} is false
+   */
+  public static Decision decide(boolean modified, int answer) {
+    if (!modified) {
+      return Decision.OPEN;
+    }
+    if (answer == SWT.YES) {
+      return Decision.APPLY_AND_OPEN;
+    }
+    if (answer == SWT.NO) {
+      return Decision.OPEN;
+    }
+    return Decision.CANCEL;
+  }
+
+  /** True when {@code filename} resolves to a non-blank path. */
+  public static boolean hasOpenableFilename(IVariables variables, String 
filename) {
+    if (variables == null || StringUtils.isBlank(filename)) {
+      return false;
+    }
+    return StringUtils.isNotBlank(variables.resolve(filename));
+  }
+
+  /**
+   * True when the dialog metadata is already marked changed, or the filename 
field differs from the
+   * value stored on the transform or action.
+   */
+  public static boolean isDialogModified(
+      boolean changed, String currentFilename, String storedFilename) {
+    if (changed) {
+      return true;
+    }
+    return !StringUtils.equals(
+        StringUtils.defaultString(currentFilename), 
StringUtils.defaultString(storedFilename));
+  }
+
+  /** Label, tooltip and read-only behaviour shared by the Open buttons. */
+  public static void configureOpenButton(Button button) {
+    PropsUi.setLook(button);
+    button.setText(BaseMessages.getString(PKG, "System.Button.Open"));
+    button.setToolTipText(BaseMessages.getString(PKG, 
"ReferencedFileOpener.Open.Tooltip"));
+    BaseDialog.keepEnabledInReadOnly(button);
+  }
+
+  /**
+   * Open {@code filename} from a dialog.
+   *
+   * @param apply saves the dialog and returns the filename to open, or {@code 
null} when saving did
+   *     not complete
+   */
+  public static void openFromDialog(
+      Shell shell,
+      IVariables variables,
+      String filename,
+      boolean modified,
+      Supplier<String> apply) {
+    if (!hasOpenableFilename(variables, filename)) {
+      MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+      box.setText(BaseMessages.getString(PKG, 
"ReferencedFileOpener.FilenameMissing.Title"));
+      box.setMessage(BaseMessages.getString(PKG, 
"ReferencedFileOpener.FilenameMissing.Message"));
+      box.open();
+      return;
+    }
+    if (isReadOnly(shell)) {
+      modified = false;
+    }
+    Decision decision = decide(modified, modified ? askToSave(shell) : 
SWT.NONE);
+    if (decision == Decision.CANCEL) {
+      return;
+    }
+    String toOpen = filename;
+    if (decision == Decision.APPLY_AND_OPEN) {
+      toOpen = apply == null ? null : apply.get();
+      if (!hasOpenableFilename(variables, toOpen)) {
+        return;
+      }
+    }
+    openResolved(variables.resolve(toOpen), shell);
+  }
+
+  private static boolean isReadOnly(Shell shell) {
+    if (shell == null || shell.isDisposed()) {
+      return false;
+    }
+    return 
HopDialogEditGuard.isReadOnly(shell.getData(BaseDialog.DIALOG_SUBJECT));
+  }
+
+  private static int askToSave(Shell shell) {
+    MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.CANCEL | 
SWT.ICON_QUESTION);
+    box.setText(BaseMessages.getString(PKG, 
"ReferencedFileOpener.SaveChanges.Title"));
+    box.setMessage(BaseMessages.getString(PKG, 
"ReferencedFileOpener.SaveChanges.Message"));
+    return box.open();
+  }
+
+  private static void openResolved(String filename, Shell dialogShell) {
+    try {
+      HopGui hopGui = HopGui.getInstance();
+      ExplorerPerspective perspective = HopGui.getExplorerPerspective();
+      if (perspective != null) {
+        IHopFileTypeHandler existing = 
perspective.findFileTypeHandlerByFilename(filename);
+        if (existing != null) {
+          perspective.setActiveFileTypeHandler(existing);
+          perspective.activate();
+          return;
+        }
+      }
+      hopGui.fileDelegate.fileOpen(filename);
+    } catch (Exception e) {
+      Shell parent = parentShell(dialogShell);
+      if (parent == null) {
+        return;
+      }
+      new ErrorDialog(
+          parent,
+          BaseMessages.getString(PKG, 
"ReferencedFileOpener.ErrorOpening.Title"),
+          BaseMessages.getString(PKG, 
"ReferencedFileOpener.ErrorOpening.Message", filename),
+          e);
+    }
+  }
+
+  private static Shell parentShell(Shell dialogShell) {
+    if (dialogShell != null && !dialogShell.isDisposed()) {
+      return dialogShell;
+    }
+    HopGui hopGui = HopGui.getInstance();
+    if (hopGui == null || hopGui.getShell() == null || 
hopGui.getShell().isDisposed()) {
+      return null;
+    }
+    return hopGui.getShell();
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/workflow/actions/ActionBaseDialog.java 
b/ui/src/main/java/org/apache/hop/ui/workflow/actions/ActionBaseDialog.java
index 48d7fe4562..adc4d03669 100644
--- a/ui/src/main/java/org/apache/hop/ui/workflow/actions/ActionBaseDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/workflow/actions/ActionBaseDialog.java
@@ -37,6 +37,7 @@ import org.apache.hop.ui.core.widget.NamingSchemeTypes;
 import org.apache.hop.ui.core.widget.NamingSchemeWidgetSupport;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.file.ReferencedFileOpener;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.apache.hop.ui.workflow.action.ActionDialog;
 import org.apache.hop.workflow.WorkflowMeta;
@@ -70,6 +71,7 @@ public abstract class ActionBaseDialog extends ActionDialog {
   protected TextVar wPath;
 
   protected Button wbBrowse;
+  protected Button wbOpen;
 
   protected Group gLogFile;
 
@@ -241,12 +243,21 @@ public abstract class ActionBaseDialog extends 
ActionDialog {
     fdBrowse.top = new FormAttachment(wlPath, Const.isOSX() ? 0 : 5);
     wbBrowse.setLayoutData(fdBrowse);
 
+    wbOpen = new Button(shell, SWT.PUSH);
+    ReferencedFileOpener.configureOpenButton(wbOpen);
+    FormData fdOpen = new FormData();
+    fdOpen.right = new FormAttachment(wbBrowse, -5);
+    fdOpen.top = new FormAttachment(wlPath, Const.isOSX() ? 0 : 5);
+    wbOpen.setLayoutData(fdOpen);
+    wbOpen.addListener(SWT.Selection, e -> openReferencedFile());
+
     wPath = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
     PropsUi.setLook(wPath);
+    wPath.addModifyListener(lsMod);
     FormData fdPath = new FormData();
     fdPath.left = new FormAttachment(0, 0);
     fdPath.top = new FormAttachment(wlPath, 5);
-    fdPath.right = new FormAttachment(wbBrowse, -5);
+    fdPath.right = new FormAttachment(wbOpen, -5);
     wPath.setLayoutData(fdPath);
 
     Label wlRunConfiguration = new Label(shell, SWT.LEFT);
@@ -640,6 +651,19 @@ public abstract class ActionBaseDialog extends 
ActionDialog {
     }
   }
 
+  private void openReferencedFile() {
+    ReferencedFileOpener.openFromDialog(
+        shell,
+        variables,
+        wPath.getText(),
+        ReferencedFileOpener.isDialogModified(
+            getAction().hasChanged(), wPath.getText(), 
getAction().getFilename()),
+        () -> {
+          ok();
+          return isDisposed() ? getAction().getFilename() : null;
+        });
+  }
+
   protected abstract void ok();
 
   protected abstract void cancel();
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/file/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/messages/messages_en_US.properties
index 7c7fbe2d57..30227c4708 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/file/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/messages/messages_en_US.properties
@@ -17,3 +17,10 @@
 #
 
 HopFileTypeRegistry.FileTypes.All.Label=All Hop file types
+ReferencedFileOpener.Open.Tooltip=Open the pipeline or workflow
+ReferencedFileOpener.SaveChanges.Title=Save changes?
+ReferencedFileOpener.SaveChanges.Message=This dialog has been modified. Do you 
want to save your changes before opening the file?
+ReferencedFileOpener.FilenameMissing.Title=Filename is missing
+ReferencedFileOpener.FilenameMissing.Message=Please enter a filename before 
opening the file.
+ReferencedFileOpener.ErrorOpening.Title=Error opening file
+ReferencedFileOpener.ErrorOpening.Message=Error opening file ''{0}''
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/file/ReferencedFileOpenerTest.java 
b/ui/src/test/java/org/apache/hop/ui/hopgui/file/ReferencedFileOpenerTest.java
new file mode 100644
index 0000000000..5f617c61c5
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/file/ReferencedFileOpenerTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.eclipse.swt.SWT;
+import org.junit.jupiter.api.Test;
+
+class ReferencedFileOpenerTest {
+
+  @Test
+  void unchangedDialogOpensWithoutApplying() {
+    assertEquals(ReferencedFileOpener.Decision.OPEN, 
ReferencedFileOpener.decide(false, SWT.YES));
+    assertEquals(
+        ReferencedFileOpener.Decision.OPEN, ReferencedFileOpener.decide(false, 
SWT.CANCEL));
+  }
+
+  @Test
+  void modifiedDialogFollowsTheSaveAnswer() {
+    assertEquals(
+        ReferencedFileOpener.Decision.APPLY_AND_OPEN, 
ReferencedFileOpener.decide(true, SWT.YES));
+    assertEquals(ReferencedFileOpener.Decision.OPEN, 
ReferencedFileOpener.decide(true, SWT.NO));
+    assertEquals(
+        ReferencedFileOpener.Decision.CANCEL, 
ReferencedFileOpener.decide(true, SWT.CANCEL));
+    assertEquals(ReferencedFileOpener.Decision.CANCEL, 
ReferencedFileOpener.decide(true, SWT.NONE));
+  }
+
+  @Test
+  void filenameFieldOrChangedFlagMarksTheDialogModified() {
+    assertFalse(ReferencedFileOpener.isDialogModified(false, "pipe.hpl", 
"pipe.hpl"));
+    assertFalse(ReferencedFileOpener.isDialogModified(false, null, null));
+    assertFalse(ReferencedFileOpener.isDialogModified(false, "", null));
+    assertTrue(ReferencedFileOpener.isDialogModified(false, "other.hpl", 
"pipe.hpl"));
+    assertTrue(ReferencedFileOpener.isDialogModified(true, "pipe.hpl", 
"pipe.hpl"));
+  }
+
+  @Test
+  void blankOrUnresolvedEmptyFilenameIsNotOpened() {
+    IVariables variables = new Variables();
+    variables.setVariable("FOLDER", "/data");
+    variables.setVariable("EMPTY", "");
+
+    assertFalse(ReferencedFileOpener.hasOpenableFilename(null, "pipe.hpl"));
+    assertFalse(ReferencedFileOpener.hasOpenableFilename(variables, null));
+    assertFalse(ReferencedFileOpener.hasOpenableFilename(variables, "  "));
+    assertFalse(ReferencedFileOpener.hasOpenableFilename(variables, 
"${EMPTY}"));
+    assertTrue(ReferencedFileOpener.hasOpenableFilename(variables, 
"${FOLDER}/pipe.hpl"));
+  }
+}

Reply via email to