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 904fcb54b7 Issue #4248 : Ask where to save an inherited metadata 
duplicate (#8688)
904fcb54b7 is described below

commit 904fcb54b753ce7e9d3e4461c522b61dde3f7f25
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 09:13:01 2026 +0200

    Issue #4248 : Ask where to save an inherited metadata duplicate (#8688)
---
 .../apache/hop/metadata/util/HopMetadataUtil.java  | 27 ++++++++
 .../hop/metadata/serializer/MetadataGuiFlows.java  | 20 +++++-
 .../multi/MultiMetadataLegacyKeysTest.java         | 74 +++++++++++++++++++++-
 .../ROOT/pages/hop-gui/perspective-metadata.adoc   |  1 +
 .../perspective/metadata/MetadataPerspective.java  | 24 +++++++
 .../metadata/messages/messages_en_US.properties    |  2 +
 6 files changed, 144 insertions(+), 4 deletions(-)

diff --git 
a/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java 
b/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java
index 92bfc3ad86..fc29287bf9 100644
--- a/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java
+++ b/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java
@@ -112,6 +112,33 @@ public class HopMetadataUtil {
     return providers;
   }
 
+  /**
+   * Metadata locations a duplicate can be written to when the original is not 
stored in the active
+   * project. The active project (the last provider) is first.
+   *
+   * @param provider the metadata provider of the project, possibly a {@link 
MultiMetadataProvider}
+   * @param sourceProviderName {@link IHopMetadata#getMetadataProviderName()} 
of the original, or
+   *     null
+   * @return provider descriptions, active project first, or an empty list 
when there is nothing to
+   *     choose (a single location, or the original already belongs to the 
active project)
+   */
+  public static List<String> duplicateProviderChoices(
+      IHopMetadataProvider provider, String sourceProviderName) {
+    List<IHopMetadataProvider> providers = getProviders(provider);
+    if (providers.size() < 2) {
+      return Collections.emptyList();
+    }
+    String activeProject = providers.get(providers.size() - 
1).getDescription();
+    if (StringUtils.isEmpty(sourceProviderName) || 
sourceProviderName.equals(activeProject)) {
+      return Collections.emptyList();
+    }
+    List<String> choices = new ArrayList<>();
+    for (int i = providers.size() - 1; i >= 0; i--) {
+      choices.add(providers.get(i).getDescription());
+    }
+    return choices;
+  }
+
   public static <T extends IHopMetadata> HopMetadata getHopMetadataAnnotation(
       Class<T> managedClass) {
     return managedClass.getAnnotation(HopMetadata.class);
diff --git 
a/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java 
b/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java
index ebe12ee59a..77f0ffaa4c 100644
--- 
a/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java
+++ 
b/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java
@@ -17,6 +17,7 @@
 
 package org.apache.hop.metadata.serializer;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.metadata.api.IHopMetadata;
 import org.apache.hop.metadata.api.IHopMetadataSerializer;
@@ -72,20 +73,37 @@ public final class MetadataGuiFlows {
    * Duplicating an element in the metadata perspective: {@code
    * MetadataPerspective.duplicateMetadata()}.
    *
+   * <p>The copy is a new object of the active project (the last metadata 
provider). That is the
+   * place the duplicate dialog selects by default when the original belongs 
to a parent project.
+   * Pass a provider description to save the copy there instead.
+   *
+   * @param targetProviderName provider to save the copy in, or null for the 
active project
    * @return the name of the copy
    */
   public static <T extends IHopMetadata> String duplicate(
-      IHopMetadataSerializer<T> serializer, String name) throws HopException {
+      IHopMetadataSerializer<T> serializer, String name, String 
targetProviderName)
+      throws HopException {
     T metadata = serializer.load(name);
     int copyNr = 2;
     while (true) {
       String newName = name + " " + copyNr;
       if (!serializer.exists(newName)) {
         metadata.setName(newName);
+        metadata.setMetadataProviderName(
+            StringUtils.isEmpty(targetProviderName) ? null : 
targetProviderName);
         serializer.save(metadata);
         return newName;
       }
       copyNr++;
     }
   }
+
+  /**
+   * @return the name of the copy, saved in the active project
+   * @see #duplicate(IHopMetadataSerializer, String, String)
+   */
+  public static <T extends IHopMetadata> String duplicate(
+      IHopMetadataSerializer<T> serializer, String name) throws HopException {
+    return duplicate(serializer, name, null);
+  }
 }
diff --git 
a/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
 
b/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
index 2007b0f8d5..a9c4ae71a1 100644
--- 
a/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
+++ 
b/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
@@ -227,21 +227,89 @@ class MultiMetadataLegacyKeysTest {
     assertEquals("child", serializer.load("mine").getDescription());
   }
 
-  /** The copy goes where the original lives, in the current folder. */
+  /**
+   * The copy is a new object of the active (child) project, in the current 
folder. It is not
+   * written next to the original in the parent project, which would share it 
with every project
+   * that inherits that parent.
+   */
   @Test
-  void testDuplicateInParentProject() throws Exception {
+  void testDuplicateInParentProjectGoesToTheChild() throws Exception {
     write(parentFolder, LEGACY, "conn", "parent");
     write(childFolder, LEGACY, "conn 2", "child");
 
     String copy = MetadataGuiFlows.duplicate(serializer, "conn");
 
     assertEquals("conn 3", copy);
-    assertTrue(has(parentFolder, CURRENT, "conn 3"));
+    assertTrue(has(childFolder, CURRENT, "conn 3"));
+    assertFalse(has(parentFolder, CURRENT, "conn 3"));
+    assertFalse(has(parentFolder, LEGACY, "conn 3"));
     assertTrue(has(parentFolder, LEGACY, "conn"), "the original isn't moved");
     assertTrue(has(childFolder, LEGACY, "conn 2"));
+    assertEquals(
+        multi.getProviders().get(1).getDescription(),
+        serializer.load(copy).getMetadataProviderName());
     assertEquals(List.of("conn", "conn 2", "conn 3"), sortedNames());
   }
 
+  /** Accepting the dialog default passes the active project's provider 
description. */
+  @Test
+  void testDuplicateInParentProjectTargetsTheChildExplicitly() throws 
Exception {
+    write(parentFolder, LEGACY, "conn", "parent");
+
+    String childProvider = multi.getProviders().get(1).getDescription();
+    String copy = MetadataGuiFlows.duplicate(serializer, "conn", 
childProvider);
+
+    assertEquals("conn 2", copy);
+    assertTrue(has(childFolder, CURRENT, "conn 2"));
+    assertFalse(has(parentFolder, CURRENT, "conn 2"));
+    assertTrue(has(parentFolder, LEGACY, "conn"), "the original isn't moved");
+    assertEquals(childProvider, 
serializer.load(copy).getMetadataProviderName());
+  }
+
+  /** The duplicate dialog can still save the copy in the project that owns 
the original. */
+  @Test
+  void testDuplicateCanStayWithTheOriginal() throws Exception {
+    write(parentFolder, LEGACY, "conn", "parent");
+    write(childFolder, CURRENT, "other", "child");
+
+    String parentProvider = multi.getProviders().get(0).getDescription();
+    String copy = MetadataGuiFlows.duplicate(serializer, "conn", 
parentProvider);
+
+    assertEquals("conn 2", copy);
+    assertTrue(has(parentFolder, CURRENT, "conn 2"));
+    assertFalse(has(childFolder, CURRENT, "conn 2"));
+    assertTrue(has(parentFolder, LEGACY, "conn"), "the original isn't moved");
+    assertEquals(parentProvider, 
serializer.load(copy).getMetadataProviderName());
+  }
+
+  @Test
+  void testDuplicateOfLocalObjectStaysInTheChild() throws Exception {
+    write(childFolder, CURRENT, "local", "child");
+
+    String copy = MetadataGuiFlows.duplicate(serializer, "local");
+
+    assertEquals("local 2", copy);
+    assertTrue(has(childFolder, CURRENT, "local 2"));
+    assertFalse(has(parentFolder, CURRENT, "local 2"));
+    assertFalse(Files.exists(parentFolder.resolve(CURRENT)));
+  }
+
+  @Test
+  void testDuplicateProviderChoices() {
+    List<IHopMetadataProvider> providers = multi.getProviders();
+    String parent = providers.get(0).getDescription();
+    String child = providers.get(1).getDescription();
+
+    assertEquals(List.of(), HopMetadataUtil.duplicateProviderChoices(multi, 
null));
+    assertEquals(List.of(), HopMetadataUtil.duplicateProviderChoices(multi, 
""));
+    assertEquals(List.of(), HopMetadataUtil.duplicateProviderChoices(multi, 
child));
+    assertEquals(List.of(), 
HopMetadataUtil.duplicateProviderChoices(providers.get(1), parent));
+    assertEquals(List.of(child, parent), 
HopMetadataUtil.duplicateProviderChoices(multi, parent));
+    assertEquals(
+        List.of(child, parent),
+        HopMetadataUtil.duplicateProviderChoices(multi, "JSON metadata in 
folder /missing"));
+  }
+
   /**
    * Deleting an object the child overrides removes the child's copies only, 
in all its folders. The
    * parent's object then shows through again, as it did before legacy keys 
existed.
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc
index 84a11eafd7..516803a67c 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc
@@ -37,6 +37,7 @@ 
image::hop-gui/perspective-data-orchestration-left-hand-tree.png[The left-hand t
 === Managing elements
 
 You can right-click on any element to edit, rename, duplicate or delete an 
element.  You can also use this menu to create new elements.
+Duplicating an element that is stored in a parent project asks which metadata 
folder should receive the copy. The active project is selected by default, so 
the copy is not written next to the original and shared with every project that 
inherits it.
 To create a new element you can also double-click on the element type itself.
 You can also use the toolbar icons in the toolbar located right above the 
metadata tree.
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
index 4fb6192724..5399853e5d 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
@@ -70,6 +70,7 @@ import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.bus.HopGuiEvents;
 import org.apache.hop.ui.core.dialog.BaseDialog;
 import org.apache.hop.ui.core.dialog.DetailsDialog;
+import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
 import org.apache.hop.ui.core.dialog.EnterStringDialog;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
@@ -2291,11 +2292,34 @@ public class MetadataPerspective implements 
IHopPerspective, TabClosable, IMetad
         MetadataManager<IHopMetadata> manager = getMetadataManager(objectKey);
         IHopMetadata metadata = manager.loadElement(objectName);
 
+        String targetProviderName = metadata.getMetadataProviderName();
+        List<String> providerChoices =
+            HopMetadataUtil.duplicateProviderChoices(
+                hopGui.getMetadataProvider(), targetProviderName);
+        if (!providerChoices.isEmpty()) {
+          EnterSelectionDialog dialog =
+              new EnterSelectionDialog(
+                  getShell(),
+                  providerChoices.toArray(new String[0]),
+                  BaseMessages.getString(
+                      PKG, 
"MetadataPerspective.DuplicateMetadata.SelectProvider.Title"),
+                  BaseMessages.getString(
+                      PKG,
+                      
"MetadataPerspective.DuplicateMetadata.SelectProvider.Message",
+                      objectName));
+          String chosen = dialog.open(0);
+          if (chosen == null) {
+            return;
+          }
+          targetProviderName = chosen;
+        }
+
         int copyNr = 2;
         while (true) {
           String newName = objectName + " " + copyNr;
           if (!manager.getSerializer().exists(newName)) {
             metadata.setName(newName);
+            metadata.setMetadataProviderName(targetProviderName);
             manager.getSerializer().save(metadata);
             break;
           } else {
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
index 0832d56102..aeaebe20fd 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
@@ -25,6 +25,8 @@ MetadataPerspective.CreateMetadata.Error.Message=Error 
creating metadata
 MetadataPerspective.Description=The Hop Metatada Perspective
 MetadataPerspective.DuplicateMetadata.Error.Header=Error
 MetadataPerspective.DuplicateMetadata.Error.Message=Error duplicating metadata
+MetadataPerspective.DuplicateMetadata.SelectProvider.Title=Duplicate metadata
+MetadataPerspective.DuplicateMetadata.SelectProvider.Message=''{0}'' is not 
stored in the active project.\nSelect where to save the copy. The active 
project is already selected.
 MetadataPerspective.EditMetadata.Error.Header=Error
 MetadataPerspective.EditMetadata.Error.Message=Error editing metadata
 MetadataPerspective.GuiPlugin.Description=This perspective allows you to see 
and edit all available metadata

Reply via email to