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 f6cfca5bba Issue #2347 : Add row distribution and partitioning context 
actions to pipeline hops (#8253)
f6cfca5bba is described below

commit f6cfca5bba671f55863bc86336118496b2d828e9
Author: Matt Casters <[email protected]>
AuthorDate: Fri Sep 4 16:49:38 2026 +0200

    Issue #2347 : Add row distribution and partitioning context actions to 
pipeline hops (#8253)
    
    - Add Copy rows / Distribute rows contextual actions to toggle source 
transform row routing
    - Add Set partitioning / Remove partitioning contextual actions to 
configure or clear target transform partitioning
    - Register row distribution and partitioning changes with 
undoDelegate.addUndoChange() and update GUI/toolbar state
    - Allow clicking hop icons to open the hop context menu
    - Add unit tests for hop context actions and filtering
    - Update user manual documentation with context action details and new 
screenshot
    - Update .gitignore to ignore .agents
---
 .gitignore                                         |   1 +
 .../pipeline-hop-context-dialog-data-routing.png   | Bin 0 -> 107037 bytes
 .../ROOT/pages/hop-gui/hop-gui-popup-dialog.adoc   |  18 +-
 .../modules/ROOT/pages/pipeline/partitioning.adoc  |   2 +
 .../org/apache/hop/pipeline/PipelinePainter.java   |  12 +-
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  | 229 ++++++++++++++++++++-
 .../delegates/HopGuiPipelineTransformDelegate.java |   1 +
 .../pipeline/messages/messages_en_US.properties    |   8 +
 .../pipeline/HopGuiPipelineHopContextTest.java     | 223 ++++++++++++++++++++
 9 files changed, 468 insertions(+), 26 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3cc07bb270..f81e6e54bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 .classpath
 .settings
 .factorypath
+.agents
 build
 target
 release.properties
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/pipeline-hop-context-dialog-data-routing.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/pipeline-hop-context-dialog-data-routing.png
new file mode 100644
index 0000000000..10fe52a65a
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/pipeline-hop-context-dialog-data-routing.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-popup-dialog.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-popup-dialog.adoc
index 7a4fe0b706..9619e5ed05 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-popup-dialog.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/hop-gui-popup-dialog.adoc
@@ -195,13 +195,19 @@ The Hop options, available from both the workflow and 
pipeline editors, offer a
 For pipelines, the dialog offers the following options:
 
 * Basic
-** **disable/enable hop**: enable the current hop (if disabled) or disable the 
current hop (if enabled). This option will only show the applicable options 
(e.g. 'enable hop' won't be shown for active hops and vice versa)
-** **delete hop**: delete the current hop
+** **disable/enable hop**: enable the current hop (if disabled) or disable the 
current hop (if enabled). This option will only show the applicable option 
(e.g. 'enable hop' won't be shown for active hops and vice versa).
+** **delete hop**: delete the current hop.
+** **Insert transform**: split the hop and insert a new transform.
 * Bulk
-** **Enable downstream hops**: enable the current hop, and all hops that 
follow later in the workflow (or pipeline).
-** **Disable downstream hops**: disable the current hop and all hops that 
follow later in the current workflow (or pipeline).
-
-image:hop-gui/popup-hop-pipeline.png[Hop Gui Popup Dialog - Pipeline Hop 
Options, width="65%"]
+** **Enable downstream hops**: enable the current hop and all hops that follow 
downstream in the pipeline.
+** **Disable downstream hops**: disable the current hop and all hops that 
follow downstream in the pipeline.
+** **Enable hops between selection**: enable all hops between selected 
transforms.
+** **Disable hops between selection**: disable all hops between selected 
transforms.
+* Data routing
+** **Distribute rows / Copy rows**: toggle whether the source transform 
distributes or copies rows to subsequent transforms. If the source transform is 
currently copying rows, **Distribute rows** is shown; if it is distributing 
rows, **Copy rows** is shown.
+** **Set partitioning / Remove partitioning**: configure or remove data 
partitioning on the target transform. If the target transform does not yet have 
partitioning configured, **Set partitioning** opens the partitioning dialog; if 
partitioning is already configured, **Remove partitioning** clears it.
+
+image:hop-gui/pipeline-hop-context-dialog-data-routing.png[Hop Gui Popup 
Dialog - Pipeline Hop Options, width="65%"]
 
 When working in a workflow, the hops popup dialog has all the options as the 
pipeline hop dialog, with one additional category:
 
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/partitioning.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/partitioning.adoc
index 28772b5102..a58b8da37c 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/partitioning.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/partitioning.adoc
@@ -88,6 +88,8 @@ The names used for the partitions can be anything you like.
 . Next, apply the partition schema to the Group By transform.
 By applying a partition schema to a transform, a matching set of transform 
copies is started automatically (for example, if applying a partition schema 
with three partitions, three transform copies are launched).
 
+TIP: You can also set or remove partitioning directly on any hop pointing to 
the target transform by clicking the hop and selecting **Set partitioning** (or 
**Remove partitioning**) under the **Data routing** category in the 
xref:hop-gui/hop-gui-popup-dialog.adoc#_hop_options[hop context dialog].
+
 Step Two: select the partitioning method:
 
 * Establish the partitioning method for the transform, which defines the rule 
for row distribution across the copies.
diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java 
b/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
index 8d04fd51c1..dd15713c24 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
@@ -1241,14 +1241,7 @@ public class PipelinePainter extends 
BasePainter<PipelineHopMeta, TransformMeta>
           gc.drawImage(svgFile, mx, my, 16, 16, magnification, 0);
           areaOwners.add(
               new AreaOwner(
-                  AreaType.ROW_DISTRIBUTION_ICON,
-                  mx,
-                  my,
-                  16,
-                  16,
-                  offset,
-                  fs,
-                  STRING_ROW_DISTRIBUTION));
+                  AreaType.ROW_DISTRIBUTION_ICON, mx, my, 16, 16, offset, fs, 
pipelineHop));
           mx += 16;
         }
 
@@ -1262,8 +1255,7 @@ public class PipelinePainter extends 
BasePainter<PipelineHopMeta, TransformMeta>
         gc.drawImage(image, mx, my, magnification);
 
         areaOwners.add(
-            new AreaOwner(
-                AreaType.HOP_COPY_ICON, mx, my, 16, 16, offset, fs, 
STRING_HOP_TYPE_COPY));
+            new AreaOwner(AreaType.HOP_COPY_ICON, mx, my, 16, 16, offset, fs, 
pipelineHop));
         mx += 16;
       }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index 31bdae5841..dcc34101d6 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -134,6 +134,7 @@ import org.apache.hop.pipeline.transform.ITransformMeta;
 import org.apache.hop.pipeline.transform.RowDistributionPluginType;
 import org.apache.hop.pipeline.transform.TransformErrorMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.TransformPartitioningMeta;
 import org.apache.hop.pipeline.transform.stream.IStream;
 import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
 import org.apache.hop.pipeline.transform.stream.Stream;
@@ -174,6 +175,7 @@ import org.apache.hop.ui.hopgui.context.GuiActionFavorites;
 import org.apache.hop.ui.hopgui.context.GuiContextUtil;
 import org.apache.hop.ui.hopgui.context.IGuiContextHandler;
 import org.apache.hop.ui.hopgui.delegates.HopGuiServerDelegate;
+import org.apache.hop.ui.hopgui.delegates.HopGuiUndoDelegate;
 import org.apache.hop.ui.hopgui.dialog.EnterPreviewRowsDialog;
 import org.apache.hop.ui.hopgui.dialog.NotePadDialog;
 import org.apache.hop.ui.hopgui.dialog.SearchFieldsProgressDialog;
@@ -316,6 +318,14 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       "pipeline-graph-hop-10010-hop-enable";
   public static final String ACTION_ID_PIPELINE_GRAPH_HOP_DISABLE =
       "pipeline-graph-hop-10015-hop-disable";
+  public static final String ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_DISTRIBUTE =
+      "pipeline-graph-hop-10600-rows-distribute";
+  public static final String ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_COPY =
+      "pipeline-graph-hop-10650-rows-copy";
+  public static final String ACTION_ID_PIPELINE_GRAPH_HOP_SET_PARTITIONING =
+      "pipeline-graph-hop-10700-set-partitioning";
+  public static final String ACTION_ID_PIPELINE_GRAPH_HOP_REMOVE_PARTITIONING =
+      "pipeline-graph-hop-10710-remove-partitioning";
   public static final String ACTION_ID_PIPELINE_GRAPH_TRANSFORM_ROWS_COPY =
       "pipeline-graph-transform-10650-rows-copy";
   public static final String 
ACTION_ID_PIPELINE_GRAPH_TRANSFORM_ROWS_DISTRIBUTE =
@@ -932,6 +942,11 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
           break;
 
         case HOP_COPY_ICON:
+        case ROW_DISTRIBUTION_ICON:
+          clickedPipelineHop =
+              areaOwner.getOwner() instanceof PipelineHopMeta
+                  ? (PipelineHopMeta) areaOwner.getOwner()
+                  : findPipelineHop(real.x, real.y);
           done = true;
           break;
 
@@ -3539,9 +3554,27 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Routing.Text",
       categoryOrder = "2")
   public void setDistributes(HopGuiPipelineTransformContext context) {
-    context.getTransformMeta().setDistributes(true);
-    context.getTransformMeta().setRowDistribution(null);
+    TransformMeta transformMeta = context.getTransformMeta();
+    TransformMeta before = (TransformMeta) transformMeta.clone();
+    transformMeta.setDistributes(true);
+    transformMeta.setRowDistribution(null);
+    transformMeta.setChanged();
+    TransformMeta after = (TransformMeta) transformMeta.clone();
+    PipelineMeta meta =
+        context.getPipelineMeta() != null ? context.getPipelineMeta() : 
pipelineMeta;
+    if (meta != null) {
+      meta.setChanged();
+      HopGuiUndoDelegate undo = getUndoDelegate();
+      if (undo != null) {
+        undo.addUndoChange(
+            meta,
+            new TransformMeta[] {before},
+            new TransformMeta[] {after},
+            new int[] {meta.indexOfTransform(transformMeta)});
+      }
+    }
     redraw();
+    updateGui();
   }
 
   @GuiContextAction(
@@ -3554,9 +3587,27 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Routing.Text",
       categoryOrder = "2")
   public void setCopies(HopGuiPipelineTransformContext context) {
-    context.getTransformMeta().setDistributes(false);
-    context.getTransformMeta().setRowDistribution(null);
+    TransformMeta transformMeta = context.getTransformMeta();
+    TransformMeta before = (TransformMeta) transformMeta.clone();
+    transformMeta.setDistributes(false);
+    transformMeta.setRowDistribution(null);
+    transformMeta.setChanged();
+    TransformMeta after = (TransformMeta) transformMeta.clone();
+    PipelineMeta meta =
+        context.getPipelineMeta() != null ? context.getPipelineMeta() : 
pipelineMeta;
+    if (meta != null) {
+      meta.setChanged();
+      HopGuiUndoDelegate undo = getUndoDelegate();
+      if (undo != null) {
+        undo.addUndoChange(
+            meta,
+            new TransformMeta[] {before},
+            new TransformMeta[] {after},
+            new int[] {meta.indexOfTransform(transformMeta)});
+      }
+    }
     redraw();
+    updateGui();
   }
 
   /**
@@ -3758,14 +3809,33 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
    */
   @GuiContextActionFilter(parentId = HopGuiPipelineHopContext.CONTEXT_ID)
   public boolean filterHopActions(String contextActionId, 
HopGuiPipelineHopContext context) {
-    if (contextActionId.equals(ACTION_ID_PIPELINE_GRAPH_HOP_ENABLE)) {
-      return !context.getHopMeta().isEnabled();
+    PipelineHopMeta hop = context.getHopMeta();
+    if (hop == null) {
+      return false;
     }
-    if (contextActionId.equals(ACTION_ID_PIPELINE_GRAPH_HOP_DISABLE)) {
-      return context.getHopMeta().isEnabled();
+    switch (contextActionId) {
+      case ACTION_ID_PIPELINE_GRAPH_HOP_ENABLE -> {
+        return !hop.isEnabled();
+      }
+      case ACTION_ID_PIPELINE_GRAPH_HOP_DISABLE -> {
+        return hop.isEnabled();
+      }
+      case ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_DISTRIBUTE -> {
+        return hop.getFromTransform() != null && 
!hop.getFromTransform().isDistributes();
+      }
+      case ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_COPY -> {
+        return hop.getFromTransform() != null && 
hop.getFromTransform().isDistributes();
+      }
+      case ACTION_ID_PIPELINE_GRAPH_HOP_SET_PARTITIONING -> {
+        return hop.getToTransform() != null && 
!hop.getToTransform().isPartitioned();
+      }
+      case ACTION_ID_PIPELINE_GRAPH_HOP_REMOVE_PARTITIONING -> {
+        return hop.getToTransform() != null && 
hop.getToTransform().isPartitioned();
+      }
+      default -> {
+        return true;
+      }
     }
-
-    return true;
   }
 
   /**
@@ -4061,6 +4131,132 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     }
   }
 
+  @GuiContextAction(
+      id = ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_DISTRIBUTE,
+      parentId = HopGuiPipelineHopContext.CONTEXT_ID,
+      type = GuiActionType.Modify,
+      name = "i18n::HopGuiPipelineGraph.HopAction.DistributeRows.Name",
+      tooltip = "i18n::HopGuiPipelineGraph.HopAction.DistributeRows.Tooltip",
+      image = "ui/images/distribute.svg",
+      category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Routing.Text",
+      categoryOrder = "2")
+  public void setHopDistributes(HopGuiPipelineHopContext context) {
+    PipelineHopMeta hop = context.getHopMeta();
+    if (hop != null && hop.getFromTransform() != null) {
+      TransformMeta from = hop.getFromTransform();
+      TransformMeta before = (TransformMeta) from.clone();
+      from.setDistributes(true);
+      from.setRowDistribution(null);
+      from.setChanged();
+      TransformMeta after = (TransformMeta) from.clone();
+      PipelineMeta meta =
+          context.getPipelineMeta() != null ? context.getPipelineMeta() : 
pipelineMeta;
+      if (meta != null) {
+        meta.setChanged();
+        HopGuiUndoDelegate undo = getUndoDelegate();
+        if (undo != null) {
+          undo.addUndoChange(
+              meta,
+              new TransformMeta[] {before},
+              new TransformMeta[] {after},
+              new int[] {meta.indexOfTransform(from)});
+        }
+      }
+      redraw();
+      updateGui();
+    }
+  }
+
+  @GuiContextAction(
+      id = ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_COPY,
+      parentId = HopGuiPipelineHopContext.CONTEXT_ID,
+      type = GuiActionType.Modify,
+      name = "i18n::HopGuiPipelineGraph.HopAction.CopyRows.Name",
+      tooltip = "i18n::HopGuiPipelineGraph.HopAction.CopyRows.Tooltip",
+      image = "ui/images/copy-rows.svg",
+      category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Routing.Text",
+      categoryOrder = "2")
+  public void setHopCopies(HopGuiPipelineHopContext context) {
+    PipelineHopMeta hop = context.getHopMeta();
+    if (hop != null && hop.getFromTransform() != null) {
+      TransformMeta from = hop.getFromTransform();
+      TransformMeta before = (TransformMeta) from.clone();
+      from.setDistributes(false);
+      from.setRowDistribution(null);
+      from.setChanged();
+      TransformMeta after = (TransformMeta) from.clone();
+      PipelineMeta meta =
+          context.getPipelineMeta() != null ? context.getPipelineMeta() : 
pipelineMeta;
+      if (meta != null) {
+        meta.setChanged();
+        HopGuiUndoDelegate undo = getUndoDelegate();
+        if (undo != null) {
+          undo.addUndoChange(
+              meta,
+              new TransformMeta[] {before},
+              new TransformMeta[] {after},
+              new int[] {meta.indexOfTransform(from)});
+        }
+      }
+      redraw();
+      updateGui();
+    }
+  }
+
+  @GuiContextAction(
+      id = ACTION_ID_PIPELINE_GRAPH_HOP_SET_PARTITIONING,
+      parentId = HopGuiPipelineHopContext.CONTEXT_ID,
+      type = GuiActionType.Modify,
+      name = "i18n::HopGuiPipelineGraph.HopAction.SetPartitioning.Name",
+      tooltip = "i18n::HopGuiPipelineGraph.HopAction.SetPartitioning.Tooltip",
+      image = "ui/images/partition_schema.svg",
+      category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Routing.Text",
+      categoryOrder = "2")
+  public void setHopPartitioning(HopGuiPipelineHopContext context) {
+    PipelineHopMeta hop = context.getHopMeta();
+    if (hop != null && hop.getToTransform() != null) {
+      PipelineMeta meta =
+          context.getPipelineMeta() != null ? context.getPipelineMeta() : 
pipelineMeta;
+      pipelineTransformDelegate.editTransformPartitioning(meta, 
hop.getToTransform());
+    }
+  }
+
+  @GuiContextAction(
+      id = ACTION_ID_PIPELINE_GRAPH_HOP_REMOVE_PARTITIONING,
+      parentId = HopGuiPipelineHopContext.CONTEXT_ID,
+      type = GuiActionType.Modify,
+      name = "i18n::HopGuiPipelineGraph.HopAction.RemovePartitioning.Name",
+      tooltip = 
"i18n::HopGuiPipelineGraph.HopAction.RemovePartitioning.Tooltip",
+      image = "ui/images/partition_schema.svg",
+      category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Routing.Text",
+      categoryOrder = "2")
+  public void removeHopPartitioning(HopGuiPipelineHopContext context) {
+    PipelineHopMeta hop = context.getHopMeta();
+    if (hop != null && hop.getToTransform() != null) {
+      TransformMeta to = hop.getToTransform();
+      TransformMeta before = (TransformMeta) to.clone();
+      to.setTransformPartitioningMeta(new TransformPartitioningMeta());
+      to.setTargetTransformPartitioningMeta(null);
+      to.setChanged();
+      TransformMeta after = (TransformMeta) to.clone();
+      PipelineMeta meta =
+          context.getPipelineMeta() != null ? context.getPipelineMeta() : 
pipelineMeta;
+      if (meta != null) {
+        meta.setChanged();
+        HopGuiUndoDelegate undo = getUndoDelegate();
+        if (undo != null) {
+          undo.addUndoChange(
+              meta,
+              new TransformMeta[] {before},
+              new TransformMeta[] {after},
+              new int[] {meta.indexOfTransform(to)});
+        }
+      }
+      redraw();
+      updateGui();
+    }
+  }
+
   @GuiContextAction(
       id = "pipeline-graph-10-edit-note",
       parentId = HopGuiPipelineNoteContext.CONTEXT_ID,
@@ -6576,6 +6772,19 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     this.hopGui = hopGui;
   }
 
+  protected HopGuiUndoDelegate undoDelegate;
+
+  public HopGuiUndoDelegate getUndoDelegate() {
+    if (undoDelegate != null) {
+      return undoDelegate;
+    }
+    return hopGui != null ? hopGui.undoDelegate : null;
+  }
+
+  public void setUndoDelegate(HopGuiUndoDelegate undoDelegate) {
+    this.undoDelegate = undoDelegate;
+  }
+
   @Override
   public Object getSubject() {
     return pipelineMeta;
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
index e1a5ee83ec..3eba6a6a0f 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
@@ -613,6 +613,7 @@ public class HopGuiPipelineTransformDelegate {
           transformMeta.setChanged(partitionBefore.hasChanged());
         }
         pipelineGraph.redraw();
+        pipelineGraph.updateGui();
       }
     } catch (Exception e) {
       new ErrorDialog(
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/file/pipeline/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/pipeline/messages/messages_en_US.properties
index 621354a144..83c741500d 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/file/pipeline/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/pipeline/messages/messages_en_US.properties
@@ -32,6 +32,8 @@ HopGuiPipelineGraph.GuiAction.ZoomFitToScreen.Tooltip=Zoom to 
fit screen size
 HopGuiPipelineGraph.GuiAction.ZoomIn.Tooltip=Zoom in 10%
 HopGuiPipelineGraph.GuiAction.ZoomInOut.Tooltip=Zoom in or out
 HopGuiPipelineGraph.GuiAction.ZoomOut.Tooltip=Zoom out 10%
+HopGuiPipelineGraph.HopAction.CopyRows.Name=Copy rows
+HopGuiPipelineGraph.HopAction.CopyRows.Tooltip=Make the from transform copy 
rows to next transforms
 HopGuiPipelineGraph.HopAction.CreateHop.Name=Create hop
 HopGuiPipelineGraph.HopAction.CreateHop.Tooltip=Create a new hop between 2 
transforms
 HopGuiPipelineGraph.HopAction.DeleteHop.Name=Delete hop
@@ -42,6 +44,8 @@ 
HopGuiPipelineGraph.HopAction.DisableDownstreamHop.Name=Disable downstream hops
 HopGuiPipelineGraph.HopAction.DisableDownstreamHop.Tooltip=Disable all enabled 
downstream hops
 HopGuiPipelineGraph.HopAction.DisableHop.Name=Disable hop
 HopGuiPipelineGraph.HopAction.DisableHop.Tooltip=Disable the hop
+HopGuiPipelineGraph.HopAction.DistributeRows.Name=Distribute rows
+HopGuiPipelineGraph.HopAction.DistributeRows.Tooltip=Make the from transform 
distribute rows to next transforms
 HopGuiPipelineGraph.HopAction.EnableBetweenSelectedTransforms.Name=Enable hops 
between selection
 HopGuiPipelineGraph.HopAction.EnableBetweenSelectedTransforms.Tooltip=Enable 
all hops between selected transforms
 HopGuiPipelineGraph.HopAction.EnableDownstreamHop.Name=Enable downstream hops
@@ -50,6 +54,10 @@ HopGuiPipelineGraph.HopAction.EnableHop.Name=Enable hop
 HopGuiPipelineGraph.HopAction.EnableHop.Tooltip=Enable the hop
 HopGuiPipelineGraph.HopAction.InsetTransform.Text=Insert transform
 HopGuiPipelineGraph.HopAction.InsetTransform.Tooltip=Split the hop and insert 
a new transform
+HopGuiPipelineGraph.HopAction.RemovePartitioning.Name=Remove partitioning
+HopGuiPipelineGraph.HopAction.RemovePartitioning.Tooltip=Remove partitioning 
from the target transform
+HopGuiPipelineGraph.HopAction.SetPartitioning.Name=Set partitioning
+HopGuiPipelineGraph.HopAction.SetPartitioning.Tooltip=Specify how rows of data 
need to be grouped into partitions on the target transform
 HopGuiPipelineGraph.NoteAction.BringToFront.Text=Bring to front
 HopGuiPipelineGraph.NoteAction.BringToFront.Tooltip=Bring note to front
 HopGuiPipelineGraph.NoteAction.CreateNote.Name=Create a note
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineHopContextTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineHopContextTest.java
new file mode 100644
index 0000000000..912be45357
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineHopContextTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.pipeline;
+
+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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import org.apache.hop.core.gui.Point;
+import org.apache.hop.partition.PartitionSchema;
+import org.apache.hop.pipeline.PipelineHopMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.IRowDistribution;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.TransformPartitioningMeta;
+import org.apache.hop.ui.hopgui.delegates.HopGuiUndoDelegate;
+import org.apache.hop.ui.hopgui.file.pipeline.context.HopGuiPipelineHopContext;
+import 
org.apache.hop.ui.hopgui.file.pipeline.delegates.HopGuiPipelineTransformDelegate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class HopGuiPipelineHopContextTest {
+
+  private HopGuiPipelineGraph graph;
+  private HopGuiUndoDelegate undoDelegate;
+  private PipelineMeta pipelineMeta;
+  private TransformMeta fromTransform;
+  private TransformMeta toTransform;
+  private PipelineHopMeta hopMeta;
+  private HopGuiPipelineHopContext context;
+
+  @BeforeEach
+  void setUp() {
+    graph = mock(HopGuiPipelineGraph.class, 
org.mockito.Mockito.CALLS_REAL_METHODS);
+    undoDelegate = mock(HopGuiUndoDelegate.class);
+    graph.setUndoDelegate(undoDelegate);
+    pipelineMeta = mock(PipelineMeta.class);
+    fromTransform = new TransformMeta("From", null);
+    toTransform = new TransformMeta("To", null);
+    hopMeta = new PipelineHopMeta(fromTransform, toTransform);
+    context = new HopGuiPipelineHopContext(pipelineMeta, hopMeta, graph, new 
Point(0, 0));
+  }
+
+  @Test
+  void testFilterHopActions_EnableDisable() {
+    hopMeta.setEnabled(true);
+    assertFalse(
+        
graph.filterHopActions(HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ENABLE, 
context));
+    assertTrue(
+        
graph.filterHopActions(HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_DISABLE,
 context));
+
+    hopMeta.setEnabled(false);
+    assertTrue(
+        
graph.filterHopActions(HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ENABLE, 
context));
+    assertFalse(
+        
graph.filterHopActions(HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_DISABLE,
 context));
+  }
+
+  @Test
+  void testFilterHopActions_DistributeAndCopy() {
+    // When from-transform is distributing rows
+    fromTransform.setDistributes(true);
+    assertTrue(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_COPY, 
context));
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_DISTRIBUTE, 
context));
+
+    // When from-transform is copying rows
+    fromTransform.setDistributes(false);
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_COPY, 
context));
+    assertTrue(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_DISTRIBUTE, 
context));
+  }
+
+  @Test
+  void testFilterHopActions_Partitioning() {
+    // When to-transform is not partitioned
+    toTransform.setTransformPartitioningMeta(new TransformPartitioningMeta());
+    assertFalse(toTransform.isPartitioned());
+    assertTrue(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_SET_PARTITIONING, 
context));
+    assertFalse(
+        graph.filterHopActions(
+            
HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_REMOVE_PARTITIONING, context));
+
+    // When to-transform is partitioned
+    TransformPartitioningMeta partMeta = new TransformPartitioningMeta();
+    
partMeta.setMethodType(TransformPartitioningMeta.PARTITIONING_METHOD_MIRROR);
+    partMeta.setPartitionSchema(new PartitionSchema("schema1", new 
java.util.ArrayList<>()));
+    toTransform.setTransformPartitioningMeta(partMeta);
+    assertTrue(toTransform.isPartitioned());
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_SET_PARTITIONING, 
context));
+    assertTrue(
+        graph.filterHopActions(
+            
HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_REMOVE_PARTITIONING, context));
+  }
+
+  @Test
+  void testFilterHopActions_NullHopAndTransforms() {
+    HopGuiPipelineHopContext nullHopContext =
+        new HopGuiPipelineHopContext(pipelineMeta, null, graph, new Point(0, 
0));
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ENABLE, 
nullHopContext));
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_COPY, 
nullHopContext));
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_SET_PARTITIONING, 
nullHopContext));
+
+    PipelineHopMeta emptyHop = new PipelineHopMeta((TransformMeta) null, 
(TransformMeta) null);
+    HopGuiPipelineHopContext emptyHopContext =
+        new HopGuiPipelineHopContext(pipelineMeta, emptyHop, graph, new 
Point(0, 0));
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_COPY, 
emptyHopContext));
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_ROWS_DISTRIBUTE, 
emptyHopContext));
+    assertFalse(
+        graph.filterHopActions(
+            HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_SET_PARTITIONING, 
emptyHopContext));
+    assertFalse(
+        graph.filterHopActions(
+            
HopGuiPipelineGraph.ACTION_ID_PIPELINE_GRAPH_HOP_REMOVE_PARTITIONING, 
emptyHopContext));
+  }
+
+  @Test
+  void testFilterHopActions_DefaultAction() {
+    assertTrue(graph.filterHopActions("some-other-action", context));
+  }
+
+  @Test
+  void testSetHopDistributes() {
+    fromTransform.setDistributes(false);
+    fromTransform.setRowDistribution(mock(IRowDistribution.class));
+    doNothing().when(graph).redraw();
+    doNothing().when(graph).updateGui();
+
+    graph.setHopDistributes(context);
+
+    assertTrue(fromTransform.isDistributes());
+    assertNull(fromTransform.getRowDistribution());
+    verify(graph).redraw();
+    verify(graph).updateGui();
+    verify(undoDelegate).addUndoChange(any(), any(), any(), any());
+  }
+
+  @Test
+  void testSetHopCopies() {
+    fromTransform.setDistributes(true);
+    fromTransform.setRowDistribution(mock(IRowDistribution.class));
+    doNothing().when(graph).redraw();
+    doNothing().when(graph).updateGui();
+
+    graph.setHopCopies(context);
+
+    assertFalse(fromTransform.isDistributes());
+    assertNull(fromTransform.getRowDistribution());
+    verify(graph).redraw();
+    verify(graph).updateGui();
+    verify(undoDelegate).addUndoChange(any(), any(), any(), any());
+  }
+
+  @Test
+  void testSetHopPartitioning() {
+    HopGuiPipelineTransformDelegate delegate = 
mock(HopGuiPipelineTransformDelegate.class);
+    graph.pipelineTransformDelegate = delegate;
+
+    graph.setHopPartitioning(context);
+
+    verify(delegate).editTransformPartitioning(pipelineMeta, toTransform);
+  }
+
+  @Test
+  void testRemoveHopPartitioning() {
+    TransformPartitioningMeta partMeta = new TransformPartitioningMeta();
+    
partMeta.setMethodType(TransformPartitioningMeta.PARTITIONING_METHOD_MIRROR);
+    partMeta.setPartitionSchema(new PartitionSchema("schema1", new 
java.util.ArrayList<>()));
+    toTransform.setTransformPartitioningMeta(partMeta);
+    toTransform.setChanged(false);
+
+    doNothing().when(graph).redraw();
+    doNothing().when(graph).updateGui();
+
+    graph.removeHopPartitioning(context);
+
+    assertFalse(toTransform.isPartitioned());
+    assertNull(toTransform.getTargetTransformPartitioningMeta());
+    assertTrue(toTransform.hasChanged());
+    verify(graph).redraw();
+    verify(graph).updateGui();
+    verify(undoDelegate).addUndoChange(any(), any(), any(), any());
+  }
+}

Reply via email to