Repository: kitodo/kitodo-production
Pull request #6482: Enable linking pages via gallery while media is hidden in structure tree
Author: login: solth
Created: 2025-03-29T12:22:49Z | Merged: 2025-04-04T15:02:46Z

Fixes #6098 

To fix the linked issue, some logic had to be adapted. The node relationships in the structure tree are used to determine whether a page can be linked to the next structure. Since the structure tree was rebuild _skipping_ the leaves representing pages or other media entirely when toggling the "hide media" switch, though, those relations could not be evalutated correctly for linking when the "show structures only" option was selected. 

I now changed this behavior so that nodes/leaves representing media in the structure tree are never omitted, but only _hidden_ when the option mentioned above is activated.

@thomaslow do you remember why you chose to actually exclude those page nodes from the structure tree instead of just not rendering them when you implemented this feature? I analysed the code but couldn't find a reason why the pages would have to be discarded entirely from the tree when the "show structures only" option was selected, but perhaps you remember some special constellations I missed.

Linked issue #6098: Allow always "Assign to next element" 
**Is your feature request related to a problem? Please describe.**
If "Show only structure elements" is activated in "customize display" for the structure, it is not possible anymore to apply "Assign to next element" for the pages in the structured view of the gallery.
Therefore, it is necessary to switch in between the two states of the structure tree. That is annoying for the users.

Probably, this is connected to the reason, that "Assign to next element" is not possible in the "Extended structure tree".
- #4380

**Describe the solution you'd like**
It should be possible to assign pages to the next element in the structured view of the gallery, even if only structure elements are shown in the structure tree.

**Describe alternatives you've considered**
If the implementation of the requirement causes too far-reaching adjustments to the software, the current status should be retained. 
These use cases probably do not occur so frequently and the resources can be invested in other developments.

**Additional context**
_Activate "Show only structure elements"_
![assignToNextElement01](https://github.com/kitodo/kitodo-production/assets/14835770/aca4616e-9912-418a-a628-06c36706095d)

_Example: "Assign to next element" is not possible_
![assignToNextElement02](https://github.com/kitodo/kitodo-production/assets/14835770/c1ec9b75-6e07-4e80-b933-4c9e43784737)

_Example: "Assign to next element" is possible_
![assignToNextElement03](https://github.com/kitodo/kitodo-production/assets/14835770/63e199c4-a0e3-449f-b985-61d1b71ae6f0)

Diff:
diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java
index 1c43db7765f..ae3ce40e270 100644
--- a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java
+++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java
@@ -432,7 +432,7 @@ private static MediaVariant getMediaVariant(Folder folderSettings, List<Physical
     }
 
     private void addStripesRecursive(LogicalDivision structure) {
-        List<Integer> treeNodeIdList = new ArrayList<Integer>();
+        List<Integer> treeNodeIdList = new ArrayList<>();
         Integer idx = 0;
         Process process = dataEditor.getProcess();
         if (Objects.nonNull(process) && Objects.nonNull(process.getParent())) {
@@ -458,7 +458,7 @@ private void addStripesRecursive(LogicalDivision structure, List<Integer> treeNo
                 // add child
                 if (Objects.isNull(child.getLink())) {
                     List<Integer> childTreeNodeIdList = new ArrayList<>(treeNodeIdList);
-                    if (!dataEditor.getStructurePanel().logicalStructureTreeContainsMedia()) {
+                    if (dataEditor.getStructurePanel().isSeparateMedia()) {
                         childTreeNodeIdList.add(siblingWithoutViewsIdx);
                     } else {
                         childTreeNodeIdList.add(siblingWithViewsIdx);
diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
index b45492ce923..f85cd5831d9 100644
--- a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
+++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
@@ -482,11 +482,7 @@ void preserve() throws UnknownTreeNodeDataException {
             this.preserveLogical();
             this.preservePhysical();
         } else {
-            if (isHideMediaInLogicalTree()) {
-                this.preserveLogical();
-            } else {
-                this.preserveLogicalAndPhysical();
-            }
+            this.preserveLogicalAndPhysical();
         }
     }
 
@@ -729,7 +725,7 @@ private Collection<View> buildStructureTreeRecursively(LogicalDivision structure
         }
 
         Set<View> viewsShowingOnAChild = new HashSet<>();
-        if (!this.logicalStructureTreeContainsMedia()) {
+        if (this.isSeparateMedia()) {
             for (LogicalDivision child : structure.getChildren()) {
                 viewsShowingOnAChild.addAll(buildStructureTreeRecursively(child, parent, processTypeMap, viewCache));
             }
@@ -1270,10 +1266,10 @@ private void updateSelectionAfterDragDrop(TreeDragDropEvent event) {
      */
     private void preserveAfterDragDrop(boolean logicalMoved, boolean pageMoved, boolean physicalMoved) {
         if (logicalMoved || pageMoved) {
-            if (logicalStructureTreeContainsMedia()) {
-                preserveLogicalAndPhysical();
-            } else {
+            if (this.isSeparateMedia()) {
                 preserveLogical();
+            } else {
+                preserveLogicalAndPhysical();
             }
             this.dataEditor.getGalleryPanel().updateStripes();
             this.dataEditor.getPaginationPanel().show();
@@ -1814,9 +1810,19 @@ public int getMultipleAssignmentsIndex(StructureTreeNode treeNode) {
      * @return {@code true} when the PhysicalDivision is assigned to more than one logical element
      */
     public boolean isAssignedSeveralTimes() {
-        TreeNode selected = getSelectedLogicalNodeIfSingle();
-        if (Objects.nonNull(selected) && selected.getData() instanceof StructureTreeNode) {
-            StructureTreeNode structureTreeNode = (StructureTreeNode) selected.getData();
+        return isAssignedSeveralTimes(getSelectedLogicalNodeIfSingle());
+    }
+
+    /**
+     * Check if the given TreeNode's PhysicalDivision is assigned to several LogicalDivisions.
+     *
+     * @param treeNode TreeNode to be checked for assignment to multiple LogicalDivisions
+     *
+     * @return {@code true} when the PhysicalDivision is assigned to more than one logical element
+     */
+    private boolean isAssignedSeveralTimes(TreeNode treeNode) {
+        if (Objects.nonNull(treeNode) && treeNode.getData() instanceof  StructureTreeNode) {
+            StructureTreeNode structureTreeNode = (StructureTreeNode) treeNode.getData();
             if (structureTreeNode.getDataObject() instanceof View) {
                 View view = (View) structureTreeNode.getDataObject();
                 return view.getPhysicalDivision().getLogicalDivisions().size() > 1;
@@ -1829,7 +1835,7 @@ public boolean isAssignedSeveralTimes() {
      * Find the next logical structure node that can be used to create a new link to the currently selected node. 
      * The node needs to be the last node amongst its siblings.
      * 
-     * @param node the tree node of the currently selected physical devision node
+     * @param node the tree node of the currently selected physical division node
      * @return the next logical tree node 
      */
     private TreeNode findNextLogicalNodeForViewAssignment(TreeNode node) {
@@ -1899,9 +1905,9 @@ private TreeNode findNextLogicalNodeForViewAssignmentRecursive(TreeNode node) {
     }
 
     /**
-     * Check if the selected Node's PhysicalDivision can be assigned to the next logical element in addition to the 
+     * Check if the selected Node's PhysicalDivision can be assigned to the next logical element in addition to the
      * current assignment.
-     * 
+     *
      * @return {@code true} if the PhysicalDivision can be assigned to the next LogicalDivision
      */
     public boolean isAssignableSeveralTimes() {
@@ -1909,13 +1915,25 @@ public boolean isAssignableSeveralTimes() {
         if (Objects.isNull(selectedLogicalNode)) {
             return false;
         }
-        TreeNode nextLogical = findNextLogicalNodeForViewAssignment(selectedLogicalNode);
+        return isAssignableSeveralTimes(selectedLogicalNode);
+    }
+
+    /**
+     * Check if the given TreeNode's PhysicalDivision can be assigned to the next logical element in addition to the
+     * current assignment.
+     *
+     * @param treeNode TreeNode representing View to be checked
+     *
+     * @return {@code true} if the PhysicalDivision can be assigned to the next LogicalDivision
+     */
+    private boolean isAssignableSeveralTimes(TreeNode treeNode) {
+        TreeNode nextLogical = findNextLogicalNodeForViewAssignment(treeNode);
         if (Objects.nonNull(nextLogical)) {
-            // check whether first child is already view of current node (too avoid adding views multiple times)
+            // check whether first child is already view of current node (to avoid adding views multiple times)
             if (!nextLogical.getChildren().isEmpty()) {
                 TreeNode childNode = nextLogical.getChildren().get(0);
                 View childNodeView = getTreeNodeView(childNode);
-                View selectedView = getTreeNodeView(selectedLogicalNode);
+                View selectedView = getTreeNodeView(treeNode);
                 if (Objects.nonNull(childNodeView) && Objects.nonNull(selectedView)) {
                     if (childNodeView.equals(selectedView)) {
                         // first child is already a view for the currently selected node
@@ -1937,9 +1955,18 @@ public void assign() {
             logger.error("assign called without selection or too many selected, should not happen");
             return;
         }
-        TreeNode nextLogical = findNextLogicalNodeForViewAssignment(selectedLogicalNode);
+        assign(selectedLogicalNode);
+    }
+
+    /**
+     * Assign given TreeNode's PhysicalDivision to the next LogicalDivision.
+     *
+     * @param treeNode TreeNode representing View to be assigned to next LogicalDivision
+     */
+    private void assign(TreeNode treeNode) {
+        TreeNode nextLogical = findNextLogicalNodeForViewAssignment(treeNode);
         if (Objects.nonNull(nextLogical)) {
-            View view = (View) ((StructureTreeNode) selectedLogicalNode.getData()).getDataObject();
+            View view = (View) ((StructureTreeNode) treeNode.getData()).getDataObject();
             View viewToAssign = new View();
             viewToAssign.setPhysicalDivision(view.getPhysicalDivision());
             StructureTreeNode structureTreeNodeSibling = (StructureTreeNode) nextLogical.getData();
@@ -1954,6 +1981,7 @@ public void assign() {
 
     /**
      * Unassign the selected Node's PhysicalDivision from the LogicalDivision parent at the selected position.
+     * If media is hidden in the structure tree the selected node is retrieved from the selected logical structure first.
      * This does not remove it from other LogicalDivisions.
      */
     public void unassign() {
@@ -1962,11 +1990,21 @@ public void unassign() {
             logger.error("unassign called without selection or too many selected, should not happen");
             return;
         }
+        unassign(selectedLogicalNode);
+    }
+
+    /**
+     * Unassign the givens TreeNode's PhysicalDivision from the LogicalDivision parent at the selected position.
+     * This does not remove it from other LogicalDivisions.
+     *
+     * @param treeNode TreeNode representing View to be unassigned from the LogicalDivision parent at the selected position
+     */
+    private void unassign(TreeNode treeNode) {
         if (isAssignedSeveralTimes()) {
-            StructureTreeNode structureTreeNode = (StructureTreeNode) selectedLogicalNode.getData();
+            StructureTreeNode structureTreeNode = (StructureTreeNode) treeNode.getData();
             View view = (View) structureTreeNode.getDataObject();
-            if (selectedLogicalNode.getParent().getData() instanceof StructureTreeNode) {
-                StructureTreeNode structureTreeNodeParent = (StructureTreeNode) selectedLogicalNode.getParent().getData();
+            if (treeNode.getParent().getData() instanceof StructureTreeNode) {
+                StructureTreeNode structureTreeNodeParent = (StructureTreeNode) treeNode.getParent().getData();
                 if (structureTreeNodeParent.getDataObject() instanceof LogicalDivision) {
                     LogicalDivision logicalDivision =
                             (LogicalDivision) structureTreeNodeParent.getDataObject();
@@ -2000,15 +2038,6 @@ public void setNodeLabelOption(String nodeLabelOption) {
         this.nodeLabelOption = nodeLabelOption;
     }
 
-    /**
-     * Returns true if the logical structure tree is a combined tree of structure nodes and view nodes (media).
-     *
-     * @return true if logical structure tree contains media
-     */
-    public boolean logicalStructureTreeContainsMedia() {
-        return !this.isSeparateMedia() && !this.isHideMediaInLogicalTree();
-    }
-
     /**
      * Return true if the users selected the option to hide media in the logical structure tree.
      *
diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css
index 360c459a46d..c04779343cc 100644
--- a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css
+++ b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css
@@ -2788,6 +2788,11 @@ Column content
     margin-right: 3px;
 }
 
+#structureTreeForm .hidden-tree-node + .ui-tree-droppoint,
+#structureTreeForm .hidden-tree-node {
+    display: none;
+}
+
 #structureTreeForm .ui-treenode .pageRange {
     margin-left: 0.3em;
 }
diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/logicalStructureMenu.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/logicalStructureMenu.xhtml
index fbf390254e5..91ef2165a2e 100644
--- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/logicalStructureMenu.xhtml
+++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/logicalStructureMenu.xhtml
@@ -36,7 +36,7 @@
                     itemLabel="#{msgs.showOnlyStructureElements}">
                         <p:ajax event="change"
                             listener="#{DataEditorForm.structurePanel.onHideMediaInLogicalTreeChange}"
-                            update="@this logicalTree @(.pageList) imagePreviewForm:thumbnailStripe"/>
+                            update="@this logicalTree @(.pageList) imagePreviewForm:thumbnailStripe imagePreviewForm:mediaContextMenu"/>
                     </p:selectBooleanCheckbox>
                 </li>
                 <li>
diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml
index b79fe2a5c6b..842eec31daf 100644
--- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml
+++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml
@@ -138,6 +138,7 @@
                               styleClass="pageRange"/>
             </p:treeNode>
             <p:treeNode type="#{StructurePanel.VIEW_NODE_TYPE}"
+                        styleClass="#{DataEditorForm.structurePanel.hideMediaInLogicalTree ? 'hidden-tree-node' : ''}"
                         icon="ui-icon-document">
                 <h:outputText value="#{empty logicalNode.label ? msgs['dataEditor.withoutType'] : logicalNode.label}"
                               title="#{logicalNode.undefined ? msgs['dataEditor.undefinedStructure'] : ''}"/>
diff --git a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java
index c6ad220a8b3..a709c9556c8 100644
--- a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java
+++ b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java
@@ -23,6 +23,7 @@
 import java.nio.file.Paths;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
@@ -633,13 +634,63 @@ public void linkPageToNextDivision() throws Exception {
         // click on 2nd menu entry "remove assignment"
         metaDataEditor.clickStructureTreeContextMenuEntry("unassign");
 
-        // check page "2" is not marked as "linked" any more
+        // check page "2" is not marked as "linked" anymore
         assertFalse(metaDataEditor.isStructureTreeNodeAssignedSeveralTimes("0_0_0_0"));
 
         // check page "3" is now only child of folder again
         assertTrue(Browser.getDriver().findElements(By.cssSelector("#logicalTree\\:0_1_0_1")).isEmpty());
     }
 
+    /*
+     * Check if media can be linked to next structure element via thumbnail context menu in gallery when media is hidden
+     * in structure tree.
+     */
+    @Test
+    public void linkPageToNextDivisionViaGalleryWhileMediaIsHiddenInStructureTreeTest() throws Exception {
+        login("kowal");
+
+        // open metadata editor
+        Pages.getProcessesPage().goTo().editMetadata(LINK_PAGE_TO_NEXT_DIVISION_PROCESS_TITLE);
+
+        MetadataEditorPage metaDataEditor = Pages.getMetadataEditorPage();
+
+        // wait until structure tree is shown
+        await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS)
+                .until(metaDataEditor::isLogicalTreeVisible);
+
+        String menuButtonId = "logicalStructureMenuButton";
+        String hideMediaOptionId ="logicalStructureMenuForm:hideMediaCheckbox";
+
+        // hide media!
+        pollAssertTrue(() -> Browser.getDriver().findElement(By.id(menuButtonId)).isDisplayed());
+        Browser.getDriver().findElement(By.id(menuButtonId)).click();
+        pollAssertTrue(() -> Browser.getDriver().findElement(By.id(hideMediaOptionId)).isDisplayed());
+        Browser.getDriver().findElement(By.id(hideMediaOptionId)).click();
+        pollAssertTrue(() -> Browser.getDriver().findElement(By.id(menuButtonId)).isDisplayed());
+        Browser.getDriver().findElement(By.id(menuButtonId)).click();
+
+        String thumbnailId = "#imagePreviewForm\\:structuredPages\\:2\\:structureElementDataList\\:0\\:structuredPagePanel";
+
+        pollAssertTrue(() -> Browser.getDriver().findElement(By.cssSelector(thumbnailId)).isDisplayed());
+
+        // assert first thumbnail is not yet linked
+        assertTrue(Browser.getDriver().findElements(By.cssSelector(thumbnailId + " .assigned-several-times")).isEmpty());
+
+        // open context menu of first thumbnail
+        Actions rightClickAction = new Actions(Browser.getDriver());
+        rightClickAction.contextClick(Browser.getDriver().findElement(By.cssSelector(thumbnailId))).build().perform();
+        pollAssertTrue(() -> Browser.getDriver().findElement(By.id("imagePreviewForm:mediaContextMenu")).isDisplayed());
+
+        // link to next structure via gallery
+        pollAssertTrue(() -> Browser.getDriver().findElement(By.className("assignToNextElement")).isDisplayed());
+        Browser.getDriver().findElement(By.className("assignToNextElement")).click();
+
+        pollAssertTrue(() -> Browser.getDriver().findElement(By.cssSelector(thumbnailId)).isDisplayed());
+
+        // assert first thumbnail is linked
+        assertFalse(Browser.getDriver().findElements(By.cssSelector(thumbnailId + " .assigned-several-times")).isEmpty());
+    }
+
     /**
      * Tests that a metadata row of the metadata table is highlighted as soon as a user adds a new
      * row via the add metadata dialog.
@@ -919,4 +970,12 @@ private static void updateChildProcessIdsInParentProcessMetadataFile() throws IO
         xmlContent = xmlContent.replaceAll(SECOND_CHILD_ID, String.valueOf(secondChildId));
         Files.write(metaXml, xmlContent.getBytes());
     }
+
+    private void pollAssertTrue(Callable<Boolean> conditionEvaluator) {
+        await().ignoreExceptions()
+                .pollDelay(1, TimeUnit.SECONDS)
+                .pollInterval(100, TimeUnit.MILLISECONDS)
+                .atMost(5, TimeUnit.SECONDS)
+                .until(conditionEvaluator);
+    }
 }


Reviews:
user: login: andre-hohmann
body: Thanks a lot!
It works for elements of the same hierarchy level. 
It does not work for elements of different hierarchy levels. 

In the issue it has not been mentioned that it should work for elements of different hierarchy levels. Additionally, this has been generally implemented not long ago: 
- https://github.com/kitodo/kitodo-production/issues/5457

I would therefore accept the current state, if it was complicated to apply the change for different hierarchy levels. I cannot assess the aditional effort. 

@solth: I think you are the person wh can decide it.  
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-31T07:54:55Z
user: login: andre-hohmann
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T09:31:13Z
user: login: henning-gerhardt
body: There are incomplete JavaDoc comments.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T09:56:12Z
user: login: henning-gerhardt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T06:14:59Z
user: login: henning-gerhardt
body: JavaDoc for method parameter `treeNode`  for method `unassign` in class `StructurePanel` is still missing.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T06:16:52Z
user: login: henning-gerhardt
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T06:29:12Z
user: login: henning-gerhardt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T08:26:54Z
user: login: solth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T08:34:21Z
user: login: andre-hohmann
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T12:48:04Z
user: login: henning-gerhardt
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T14:09:40Z
user: login: thomaslow
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T14:35:14Z

Review comments:
diff_hunk: @@ -1881,9 +1908,20 @@ public boolean isAssignableSeveralTimes() {
      * Assign selected Node's PhysicalDivision to the next LogicalDivision.
      */
     public void assign() {
-        TreeNode nextLogical = findNextLogicalNodeForViewAssignment(selectedLogicalNode);
+        if (isHideMediaInLogicalTree()) {
+            assign(getSelectedView(selectedLogicalNode));
+        } else {
+            assign(selectedLogicalNode);
+        }
+    }
+
+    /**
+     * Assign given TreeNode's PhysicalDivision to the next LogicalDivision.
+     */
path: Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
user: login: henning-gerhardt
body: JavaDoc is not complete.
created_at: 2025-04-03T09:54:13Z
author_association: COLLABORATOR
diff_hunk: @@ -1768,8 +1768,21 @@ public int getMultipleAssignmentsIndex(StructureTreeNode treeNode) {
      * @return {@code true} when the PhysicalDivision is assigned to more than one logical element
      */
     public boolean isAssignedSeveralTimes() {
-        if (Objects.nonNull(selectedLogicalNode) && selectedLogicalNode.getData() instanceof  StructureTreeNode) {
-            StructureTreeNode structureTreeNode = (StructureTreeNode) selectedLogicalNode.getData();
+        if (isHideMediaInLogicalTree()) {
+            return isAssignedSeveralTimes(getSelectedView(selectedLogicalNode));
+        } else {
+            return isAssignedSeveralTimes(selectedLogicalNode);
+        }
+    }
+
+    /**
+     * Check if the given TreeNode's PhysicalDivision is assigned to several LogicalDivisions.
+     *
path: Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
user: login: henning-gerhardt
body: JavaDoc is not complete.
created_at: 2025-04-03T09:55:08Z
author_association: COLLABORATOR
diff_hunk: @@ -1852,19 +1865,33 @@ private TreeNode findNextLogicalNodeForViewAssignmentRecursive(TreeNode node) {
     }
 
     /**
-     * Check if the selected Node's PhysicalDivision can be assigned to the next logical element in addition to the 
+     * Check if the selected Node's PhysicalDivision can be assigned to the next logical element in addition to the
      * current assignment.
-     * 
+     *
      * @return {@code true} if the PhysicalDivision can be assigned to the next LogicalDivision
      */
     public boolean isAssignableSeveralTimes() {
-        TreeNode nextLogical = findNextLogicalNodeForViewAssignment(selectedLogicalNode);
+        if (isHideMediaInLogicalTree()) {
+            return isAssignableSeveralTimes(getSelectedView(selectedLogicalNode));
+        } else {
+            return isAssignableSeveralTimes(selectedLogicalNode);
+        }
+    }
+
+    /**
+     * Check if the given TreeNode's PhysicalDivision can be assigned to the next logical element in addition to the
+     * current assignment.
+     *
path: Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
user: login: henning-gerhardt
body: JavaDoc is not complete.
created_at: 2025-04-03T09:55:31Z
author_association: COLLABORATOR
diff_hunk: @@ -1901,11 +1939,23 @@ public void assign() {
      * This does not remove it from other LogicalDivisions.
      */
     public void unassign() {
+        if (isHideMediaInLogicalTree()) {
+            unassign(getSelectedView(selectedLogicalNode));
+        } else {
+            unassign(selectedLogicalNode);
+        }
+    }
+
+    /**
+     * Unassign the givens TreeNode's PhysicalDivision from the LogicalDivision parent at the selected position.
path: Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
user: login: henning-gerhardt
body: JavaDoc is not complete.
created_at: 2025-04-03T09:55:51Z
author_association: COLLABORATOR
diff_hunk: @@ -1901,11 +1939,23 @@ public void assign() {
      * This does not remove it from other LogicalDivisions.
      */
     public void unassign() {
+        if (isHideMediaInLogicalTree()) {
+            unassign(getSelectedView(selectedLogicalNode));
+        } else {
+            unassign(selectedLogicalNode);
+        }
+    }
+
+    /**
+     * Unassign the givens TreeNode's PhysicalDivision from the LogicalDivision parent at the selected position.
path: Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
user: login: henning-gerhardt
body: JavaDoc for method parameter `treeNode`  is still missing.
created_at: 2025-04-04T06:14:59Z
author_association: COLLABORATOR
diff_hunk: @@ -1924,11 +1919,7 @@ public boolean isAssignableSeveralTimes() {
         if (Objects.isNull(selectedLogicalNode)) {
             return false;
         }
-        if (isHideMediaInLogicalTree()) {
-            return isAssignableSeveralTimes(getSelectedView(selectedLogicalNode));
-        } else {
-            return isAssignableSeveralTimes(selectedLogicalNode);
-        }
+        return isAssignableSeveralTimes(selectedLogicalNode);
path: Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
user: login: henning-gerhardt
body: This is a recursive call and the only one exit statement is false when the node is null. Is this really correct or missed I something?

Edit: this is happen in even the other changed methods. I'm unsure if the result is ever something else than false.

Edit2: never mind. I oversee the overloaded method. So the result can be both boolean values.
created_at: 2025-04-04T08:26:53Z
author_association: COLLABORATOR
diff_hunk: @@ -1924,11 +1919,7 @@ public boolean isAssignableSeveralTimes() {
         if (Objects.isNull(selectedLogicalNode)) {
             return false;
         }
-        if (isHideMediaInLogicalTree()) {
-            return isAssignableSeveralTimes(getSelectedView(selectedLogicalNode));
-        } else {
-            return isAssignableSeveralTimes(selectedLogicalNode);
-        }
+        return isAssignableSeveralTimes(selectedLogicalNode);
path: Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java
user: login: solth
body: This is not a recursive call. It calls an different method with the same name, but with a parameter (so an overloaded function).
created_at: 2025-04-04T08:34:21Z
author_association: MEMBER

PR comments:
user: login: thomaslow
created_at: 2025-03-31T09:47:15Z
body: > @thomaslow do you remember why you chose to actually exclude those page nodes from the structure tree instead of just not rendering them when you implemented this feature? I analysed the code but couldn't find a reason why the pages would have to be discarded entirely from the tree when the "show structures only" option was selected, but perhaps you remember some special constellations I missed.

@solth I don't remember exactly, sorry. I guess my thought was that this exact variation of the tree was already implemented in the "separate media" version of the metadata editor, and it would make sense to just re-use that code instead of assuming the burden of implementing a new variation of the tree that might introduce new issues.

Since PrimeFaces has direct access to the `TreeNode` tree and modifies it in-place based on various user interactions (selection, drag & drop), maybe there could be issues if the backend tree does not exactly match the rendered tree. I don't know.

If your approach (not rendering page nodes) works fine, maybe it would make sense to get rid of the different backend `TreeNode` tree variations altogether and also use this approach in the "separate media" version of the metadata editor as well?
author_association: COLLABORATOR
user: login: solth
created_at: 2025-04-02T14:21:50Z
body: @thomaslow thank you for the response. PrimeFaces nodes not being rendered shouldn't be a problem since they do not behave differently from other components that can be "hidden" in the frontend while still having a backing bean property associated to them. Actually, that is the very reason why I think hidding the media notes instead of re-building the tree and while skipping them is the correct solution.

I think the idea to adopt this approach for the "Extended tree" is interesting, albeit out of scope of this pull request.
author_association: MEMBER
user: login: solth
created_at: 2025-04-02T14:46:44Z
body: > It does not work for elements of different hierarchy levels.

@andre-hohmann I might have missed a detail, but I just tested this again and it does indeed seem to work:

<img width="1498" alt="Bildschirmfoto 2025-04-02 um 16 39 38" src="https://github.com/user-attachments/assets/774532d8-af2a-4cda-b2d6-af546a20dd41" />
(never mind the nested "books" here!)

Or are we takling about a different use case?
author_association: MEMBER
user: login: andre-hohmann
created_at: 2025-04-02T14:57:14Z
body: No, that's the use case i meant. 
See GIF below. 

Maybe @henning-gerhardt can have a look, if something (another PR, ...) is missing. 

<details>
  <summary>Click to see GIF</summary>

![VerknuepfungHierarchie](https://github.com/user-attachments/assets/a04c9258-e030-473a-9933-e7036c92e1c1)
</details>
author_association: COLLABORATOR
user: login: henning-gerhardt
created_at: 2025-04-02T15:02:03Z
body: Changes of #6483 are applied to the 3.8.x test version.
author_association: COLLABORATOR
user: login: andre-hohmann
created_at: 2025-04-03T05:24:35Z
body: It seems to be a caching problem.
The option is available, but only belated and/or after some actions.
It is a general behavior for both expanded and collapsed structure tree and difficult to explain. Thus, i created GIF-files.

**1 expanded structure tree**
<details>
  <summary>click here</summary>

![VerknuepfungHierarchie01](https://github.com/user-attachments/assets/61cc26d6-050e-4cea-88b0-0e7cd8e28d3e)
</details>

**2 collapsed structure tree**
<details>
  <summary>click here</summary>

![VerknuepfungHierarchie02](https://github.com/user-attachments/assets/375b6344-7ba1-4348-833a-0343f81c461e)
</details>
author_association: COLLABORATOR
user: login: andre-hohmann
created_at: 2025-04-03T09:30:00Z
body: This is not a caching problem, but very weird. 

- It seems in Firefox, it is more confusing then in Chrome
- It seems the reason is the check, if a link is allowed 
  - links to/from unstructured elements are not allowed
  - links to subordinate objects are not (always) allowed
- It seems to depend, if the selected object is displayed in the first row of the gallery

Nevertheless, this behavior does not depend on the collapsed structrure tree and should be examined in a new issue. 
author_association: COLLABORATOR
user: login: solth
created_at: 2025-04-04T08:23:36Z
body: @andre-hohmann & @henning-gerhardt after merging #6340 I needed to update this branch to resolve some merge conflicts. If you don't mind I think it would be prudent that you take another look and check one more time if everything still works as expected (I also checked all cased discussed above, of course, but in case something slipped my eye you should verify the current state independently). The updated branch has _less_ changes than before, so I hope you agree it is an approvement over the previous code changes you already approved (given all functionality is retained, of course!)

 If you would rather make this test/review on the base of Kitodo 3.8 you can wait for @thomaslow to review the changes first and for me to apply the same updates to #6483 after merging #6440 
author_association: MEMBER
user: login: henning-gerhardt
created_at: 2025-04-04T08:45:19Z
body: @solth: Its look like that applying 6440 and then 6482 on top of the 3.8.x branch is possible with only small glitches which can patch solve by it self. Deed we need something more on the current 3.8.x branch?
author_association: COLLABORATOR
user: login: solth
created_at: 2025-04-04T08:50:27Z
body: > @solth: Its look like that applying 6440 and then 6482 on top of the 3.8.x branch is possible with only small glitches which can patch solve by it self. Deed we need something more on the current 3.8.x branch?

The changes in #6483 will probably be identical to those in this pull request after merging #6440 and then resolving the resulting conflicts, since the code base doesn't differ between `main` and `3.8.x` branches in the affected files.
author_association: MEMBER
user: login: henning-gerhardt
created_at: 2025-04-04T08:55:34Z
body: > > @solth: Its look like that applying 6440 and then 6482 on top of the 3.8.x branch is possible with only small glitches which can patch solve by it self. Deed we need something more on the current 3.8.x branch?
> 
> The changes in #6483 will probably be identical to those in this pull request after merging #6440 and then resolving the resulting conflicts, since the code base doesn't differ between `main` and `3.8.x` branches in the affected files.

Thanks. I will do it in this way and provide @andre-hohmann a new version in our testing system so he can check this changes.
author_association: COLLABORATOR
user: login: solth
created_at: 2025-04-04T09:21:59Z
body: > Thanks. I will do it in this way and provide @andre-hohmann a new version in our testing system so he can check this changes.

@henning-gerhardt I just reviewed #6440 and will merge it into the `3.8.x` branch now, and afterwards will be updating #6483 accordingly, so you can wait for that updated branch/pull request as well, if you want.
author_association: MEMBER
user: login: thomaslow
created_at: 2025-04-04T09:58:00Z
body: I'm sorry to be the "buzzkill", but these changes seem to cause a drag & drop issue that is not present in the current main branch (including the new drag and drop changes). When dropping a folder into another folder that contains both folders and hidden pages, the drop-point seems to be calculated incorrectly. 

@solth I'm not sure what the problem is exactly. When you asked me a few days ago, I was mostly worried whether the tree nodes are correctly labelled (`id="logicalTree:3_2_0_2"`) if some nodes are not rendered by PrimeFaces. However, it seems that all nodes are still labelled consecutively as if all nodes (including hidden ones) are rendered. Hidden nodes are simply not added to the DOM. The ids are correctly referencing the backend tree node order. So, I have no idea what the problem is.

Demo for this pull request:

https://github.com/user-attachments/assets/098feda4-caf4-40f0-893a-4f68c431afdc

Demo for current main branch:

https://github.com/user-attachments/assets/5cc8215b-84da-4b0a-8868-2b6a874b5355
author_association: COLLABORATOR
user: login: thomaslow
created_at: 2025-04-04T10:20:01Z
body: @solth I think the issue is this line:

https://github.com/primefaces/primefaces/blob/cd4fbdf1d9d4ae054da19b9a84001d0c34d142eb/src/main/resources/META-INF/resources/primefaces/tree/tree.vertical.js#L599

PrimeFaces calculates the drop-position based on the parent node and child index within the list of children of that node. On the frontend side, the index is determined by counting the `li.ui-treenode` elements that are listed before the drop point. Since hidden tree nodes are not in the DOM, the index does not match the backend tree. PrimeFaces will insert the dragged node at the wrong child index further up the tree.

In theory it should be possible to correct the order in the drag and drop event handler knowing that PrimeFaces made a mistake here. 

Updating to PrimeFaces 15 probably will not resolve the issue. The calculation is the same in PrimeFaces 15:

https://github.com/primefaces/primefaces/blob/9f631c984e3e3b83f650542897634061f843d047/primefaces/src/main/resources/META-INF/resources/primefaces/tree/tree.vertical.js#L766
author_association: COLLABORATOR
user: login: solth
created_at: 2025-04-04T10:36:13Z
body: > In theory it should be possible to correct the order in the drag and drop event handler knowing that PrimeFaces made a mistake here.

@thomaslow thanks for investigating this issue. Hopefully I can find a way to adjust the drop index manually in the drop event handler 🤞.
author_association: MEMBER
user: login: solth
created_at: 2025-04-04T12:26:49Z
body: @thomaslow I think the correct solution here is to simply hide the TreeNodes representing `View`s using CSS instead of JSFs 'rendered' attribute. This way we don't need to fiddle with the PrimeFaces row index calculation and potentially disrupt the intricit JSF/PrimeFaces logic while still being able to hide the pages in the frontend. I wasn't able to reproduce the issue you described when applying this change. Do you see any problem with this - in my opinion cleaner - low-tech fix?
author_association: MEMBER
user: login: thomaslow
created_at: 2025-04-04T14:34:01Z
body: @solth Yes, your solution to hide tree nodes via CSS seems reasonable. I tested drag & drop again and it work fine now.
author_association: COLLABORATOR