This is an automated email from the ASF dual-hosted git repository.

tenthe pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new d7fd5039bb feat(#4568): Allow reordering and restructuring of assets 
in asset view edit mode (#4605)
d7fd5039bb is described below

commit d7fd5039bb8e208e20bd5d35062c32c617951110
Author: Sven Oehler <[email protected]>
AuthorDate: Tue Jun 23 07:30:04 2026 +0200

    feat(#4568): Allow reordering and restructuring of assets in asset view 
edit mode (#4605)
    
    Co-authored-by: Dominik Riemer <[email protected]>
---
 ui/deployment/i18n/de.json                         |   4 +-
 ui/deployment/i18n/en.json                         |   4 +-
 ui/deployment/i18n/pl.json                         |   4 +-
 .../asset-selection-panel.component.html           | 396 +++++++++++++++++++--
 .../asset-selection-panel.component.scss           |  42 ++-
 .../asset-selection-panel.component.ts             | 232 +++++++++++-
 6 files changed, 630 insertions(+), 52 deletions(-)

diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index b8f936636e..0e6ac402a3 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -691,6 +691,8 @@
   "Monitoring": "Monitoring",
   "More Details:": "Mehr Details:",
   "More options": "Weitere Optionen",
+  "Move asset down": "Asset nach unten verschieben",
+  "Move asset up": "Asset nach oben verschieben",
   "Must be a valid email address.": "Muss eine gültige E-Mail-Adresse sein.",
   "Name": "Name",
   "Name + Percent": "Name + Prozent",
@@ -844,7 +846,6 @@
   "Re-enter mail address": "E-Mail-Adresse erneut eingeben",
   "Recipient for test mail": "Empfänger für Test-Mail",
   "Reduce event rate": "Datenrate reduzieren",
-  "Referenced resources": "Verwendete Ressourcen",
   "Refresh": "Neu laden",
   "Refresh Fields": "Felder aktualisieren",
   "Refresh adapters": "Adapter neu laden",
@@ -874,6 +875,7 @@
   "Rename files": "Dateien umbenennen",
   "Rename the changed fields": "Geänderte Felder umbenennen",
   "Rename the fields with changed data types by using the \"Field Renamer\" 
data processor": "Benennen Sie die Felder mit geänderten Datentypen mit dem 
Datenprozessor \"Field Renamer\" um",
+  "Reorder asset": "Asset neu anordnen",
   "Repeat password": "Passwort wiederholen",
   "Replace existing": "Vorhandene ersetzen",
   "Replace existing documents with the same ID": "Vorhandene Dokumente mit 
derselben ID ersetzen",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index afd3952a86..df7bb3ca73 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -691,6 +691,8 @@
   "Monitoring": null,
   "More Details:": null,
   "More options": null,
+  "Move asset down": null,
+  "Move asset up": null,
   "Must be a valid email address.": null,
   "Name": null,
   "Name + Percent": null,
@@ -844,7 +846,6 @@
   "Re-enter mail address": null,
   "Recipient for test mail": null,
   "Reduce event rate": null,
-  "Referenced resources": null,
   "Refresh": null,
   "Refresh Fields": null,
   "Refresh adapters": null,
@@ -874,6 +875,7 @@
   "Rename files": null,
   "Rename the changed fields": null,
   "Rename the fields with changed data types by using the \"Field Renamer\" 
data processor": null,
+  "Reorder asset": null,
   "Repeat password": null,
   "Replace existing": null,
   "Replace existing documents with the same ID": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index 4a2e1b3513..f9f62616af 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -691,6 +691,8 @@
   "Monitoring": "Monitorowanie",
   "More Details:": "Więcej szczegółów:",
   "More options": "Więcej opcji",
+  "Move asset down": "Przenieś zasób w dół",
+  "Move asset up": "Przenieś zasób w górę",
   "Must be a valid email address.": "Musi być prawidłowym adresem e-mail.",
   "Name": "Nazwa",
   "Name + Percent": "Nazwa + Procent",
@@ -844,7 +846,6 @@
   "Re-enter mail address": "Wprowadź ponownie adres e-mail",
   "Recipient for test mail": "Odbiorca wiadomości testowej",
   "Reduce event rate": "Zredukuj częstość zdarzeń",
-  "Referenced resources": "Wykaz źródeł",
   "Refresh": "Odśwież",
   "Refresh Fields": "Odśwież pola",
   "Refresh adapters": "Odśwież adaptery",
@@ -874,6 +875,7 @@
   "Rename files": "Zmień nazwy plików",
   "Rename the changed fields": "Zmień nazwy zmienionych pól",
   "Rename the fields with changed data types by using the \"Field Renamer\" 
data processor": "Zmień nazwy pól ze zmienionymi typami danych za pomocą 
procesora danych \"Field Renamer\"",
+  "Reorder asset": "Zmień kolejność zasobu",
   "Repeat password": "Powtórz hasło",
   "Replace existing": "Zastąp istniejące",
   "Replace existing documents with the same ID": "Zastąp istniejące dokumenty 
o tym samym ID",
diff --git 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.html
 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.html
index 04e6ce96d4..66a9a21c60 100644
--- 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.html
+++ 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.html
@@ -46,7 +46,22 @@
                             <mat-tree-node
                                 *matTreeNodeDef="let node"
                                 matTreeNodeToggle
-                                class="tree-leaf"
+                                class="tree-leaf tree-row"
+                                cdkDropList
+                                [id]="getDropTargetId(node)"
+                                [cdkDropListData]="getChildAssets(node)"
+                                [cdkDropListConnectedTo]="dropTargetIds"
+                                [cdkDropListDisabled]="!editMode"
+                                [cdkDropListEnterPredicate]="canEnterDropList"
+                                [cdkDropListSortingDisabled]="true"
+                                [class.drop-target-active]="
+                                    activeDropTargetAssetId === node.assetId
+                                "
+                                (cdkDropListEntered)="markDropTarget(node)"
+                                (cdkDropListExited)="clearDropTarget(node)"
+                                (cdkDropListDropped)="
+                                    dropAssetIntoParent($event, node)
+                                "
                             >
                                 <div
                                     class="asset-node"
@@ -57,8 +72,34 @@
                                         'select-asset-' + node.assetName
                                     "
                                     (click)="selectNode(node, false)"
+                                    cdkDrag
+                                    [cdkDragData]="node"
+                                    [cdkDragDisabled]="!editMode"
+                                    cdkDragLockAxis="y"
+                                    cdkDragBoundary=".tree-wrapper"
                                 >
                                     <div class="node-main">
+                                        @if (editMode) {
+                                            <button
+                                                mat-icon-button
+                                                type="button"
+                                                class="node-drag-handle"
+                                                cdkDragHandle
+                                                [matTooltip]="
+                                                    'Reorder asset' | translate
+                                                "
+                                                [attr.aria-label]="
+                                                    'Reorder asset' | translate
+                                                "
+                                                (click)="
+                                                    $event.stopPropagation()
+                                                "
+                                            >
+                                                <mat-icon
+                                                    >drag_indicator</mat-icon
+                                                >
+                                            </button>
+                                        }
                                         <mat-icon
                                             class="node-icon"
                                             [class.node-icon-selected]="
@@ -86,24 +127,125 @@
                                     </div>
                                     @if (editMode) {
                                         <div class="node-actions">
-                                            <button
-                                                mat-icon-button
-                                                [attr.data-cy]="
-                                                    'add-asset-' +
-                                                    node.assetName
-                                                "
-                                                (click)="addAsset(node)"
-                                                color="accent"
-                                            >
-                                                <mat-icon>add</mat-icon>
-                                            </button>
-                                            <button
-                                                mat-icon-button
-                                                (click)="deleteAsset(node)"
-                                                color="accent"
-                                            >
-                                                <mat-icon>delete</mat-icon>
-                                            </button>
+                                            @if (!isRootNode(node)) {
+                                                <button
+                                                    mat-icon-button
+                                                    type="button"
+                                                    [disabled]="
+                                                        !canMoveAssetUp(node)
+                                                    "
+                                                    [matTooltip]="
+                                                        'Move asset up'
+                                                            | translate
+                                                    "
+                                                    [attr.aria-label]="
+                                                        'Move asset up'
+                                                            | translate
+                                                    "
+                                                    (click)="
+                                                        moveAsset(node, -1);
+                                                        
$event.stopPropagation()
+                                                    "
+                                                >
+                                                    <mat-icon
+                                                        >arrow_upward</mat-icon
+                                                    >
+                                                </button>
+                                                <button
+                                                    mat-icon-button
+                                                    type="button"
+                                                    [disabled]="
+                                                        !canMoveAssetDown(node)
+                                                    "
+                                                    [matTooltip]="
+                                                        'Move asset down'
+                                                            | translate
+                                                    "
+                                                    [attr.aria-label]="
+                                                        'Move asset down'
+                                                            | translate
+                                                    "
+                                                    (click)="
+                                                        moveAsset(node, 1);
+                                                        
$event.stopPropagation()
+                                                    "
+                                                >
+                                                    <mat-icon
+                                                        
>arrow_downward</mat-icon
+                                                    >
+                                                </button>
+                                            }
+                                            @if (canDeleteAsset(node)) {
+                                                <button
+                                                    mat-icon-button
+                                                    type="button"
+                                                    [matMenuTriggerFor]="
+                                                        assetMenu
+                                                    "
+                                                    [matTooltip]="
+                                                        'More options'
+                                                            | translate
+                                                    "
+                                                    [attr.aria-label]="
+                                                        'More options'
+                                                            | translate
+                                                    "
+                                                    (click)="
+                                                        
$event.stopPropagation()
+                                                    "
+                                                >
+                                                    <mat-icon
+                                                        >more_vert</mat-icon
+                                                    >
+                                                </button>
+                                                <mat-menu #assetMenu="matMenu">
+                                                    <button
+                                                        mat-menu-item
+                                                        [attr.data-cy]="
+                                                            'add-asset-' +
+                                                            node.assetName
+                                                        "
+                                                        (click)="
+                                                            addAsset(node);
+                                                            
$event.stopPropagation()
+                                                        "
+                                                    >
+                                                        
<mat-icon>add</mat-icon>
+                                                        <span>{{
+                                                            'Add' | translate
+                                                        }}</span>
+                                                    </button>
+                                                    <button
+                                                        mat-menu-item
+                                                        (click)="
+                                                            deleteAsset(node);
+                                                            
$event.stopPropagation()
+                                                        "
+                                                    >
+                                                        <mat-icon
+                                                            >delete</mat-icon
+                                                        >
+                                                        <span>{{
+                                                            'Delete' | 
translate
+                                                        }}</span>
+                                                    </button>
+                                                </mat-menu>
+                                            } @else {
+                                                <button
+                                                    mat-icon-button
+                                                    [attr.data-cy]="
+                                                        'add-asset-' +
+                                                        node.assetName
+                                                    "
+                                                    (click)="
+                                                        addAsset(node);
+                                                        
$event.stopPropagation()
+                                                    "
+                                                    color="accent"
+                                                >
+                                                    <mat-icon>add</mat-icon>
+                                                </button>
+                                            }
                                         </div>
                                     }
                                 </div>
@@ -112,7 +254,26 @@
                             <mat-nested-tree-node
                                 *matTreeNodeDef="let node; when: hasChild"
                             >
-                                <div class="mat-tree-node">
+                                <div
+                                    class="mat-tree-node tree-row"
+                                    cdkDropList
+                                    [id]="getDropTargetId(node)"
+                                    [cdkDropListData]="getChildAssets(node)"
+                                    [cdkDropListConnectedTo]="dropTargetIds"
+                                    [cdkDropListDisabled]="!editMode"
+                                    [cdkDropListEnterPredicate]="
+                                        canEnterDropList
+                                    "
+                                    [cdkDropListSortingDisabled]="true"
+                                    [class.drop-target-active]="
+                                        activeDropTargetAssetId === 
node.assetId
+                                    "
+                                    (cdkDropListEntered)="markDropTarget(node)"
+                                    (cdkDropListExited)="clearDropTarget(node)"
+                                    (cdkDropListDropped)="
+                                        dropAssetIntoParent($event, node)
+                                    "
+                                >
                                     <button
                                         mat-icon-button
                                         matTreeNodeToggle
@@ -134,12 +295,44 @@
                                     </button>
                                     <div
                                         class="asset-node"
-                                        [class.root-node]="
-                                            node.assetId === assetModel.assetId
+                                        [class.root-node]="isRootNode(node)"
+                                        (click)="
+                                            selectNode(node, isRootNode(node))
+                                        "
+                                        cdkDrag
+                                        [cdkDragData]="node"
+                                        [cdkDragDisabled]="
+                                            !editMode || isRootNode(node)
                                         "
-                                        (click)="selectNode(node, true)"
+                                        cdkDragLockAxis="y"
+                                        cdkDragBoundary=".tree-wrapper"
                                     >
                                         <div class="node-main">
+                                            @if (
+                                                editMode && !isRootNode(node)
+                                            ) {
+                                                <button
+                                                    mat-icon-button
+                                                    type="button"
+                                                    class="node-drag-handle"
+                                                    cdkDragHandle
+                                                    [matTooltip]="
+                                                        'Reorder asset'
+                                                            | translate
+                                                    "
+                                                    [attr.aria-label]="
+                                                        'Reorder asset'
+                                                            | translate
+                                                    "
+                                                    (click)="
+                                                        
$event.stopPropagation()
+                                                    "
+                                                >
+                                                    <mat-icon
+                                                        
>drag_indicator</mat-icon
+                                                    >
+                                                </button>
+                                            }
                                             <mat-icon
                                                 class="node-icon"
                                                 [class.node-icon-selected]="
@@ -147,7 +340,12 @@
                                                     selectedAsset.assetId
                                                 "
                                             >
-                                                account_tree
+                                                {{
+                                                    isRootNode(node) ||
+                                                    node.assets?.length
+                                                        ? 'account_tree'
+                                                        : 'device_hub'
+                                                }}
                                             </mat-icon>
                                             <div class="node-text">
                                                 <div
@@ -159,27 +357,147 @@
                                                 >
                                                     {{ node.assetName }}
                                                 </div>
+                                                <div class="node-meta">
+                                                    {{ node.assetDescription }}
+                                                </div>
                                             </div>
-                                            @if (
-                                                node.assetId ===
-                                                assetModel.assetId
-                                            ) {
+                                            @if (isRootNode(node)) {
                                                 <span class="badge">Root</span>
                                             }
                                         </div>
                                         @if (editMode) {
                                             <div class="node-actions">
-                                                <button
-                                                    mat-icon-button
-                                                    (click)="addAsset(node)"
-                                                    [attr.data-cy]="
-                                                        'add-asset-' +
-                                                        node.assetName
-                                                    "
-                                                    color="accent"
-                                                >
-                                                    <mat-icon>add</mat-icon>
-                                                </button>
+                                                @if (!isRootNode(node)) {
+                                                    <button
+                                                        mat-icon-button
+                                                        type="button"
+                                                        [disabled]="
+                                                            !canMoveAssetUp(
+                                                                node
+                                                            )
+                                                        "
+                                                        [matTooltip]="
+                                                            'Move asset up'
+                                                                | translate
+                                                        "
+                                                        [attr.aria-label]="
+                                                            'Move asset up'
+                                                                | translate
+                                                        "
+                                                        (click)="
+                                                            moveAsset(node, 
-1);
+                                                            
$event.stopPropagation()
+                                                        "
+                                                    >
+                                                        <mat-icon
+                                                            
>arrow_upward</mat-icon
+                                                        >
+                                                    </button>
+                                                    <button
+                                                        mat-icon-button
+                                                        type="button"
+                                                        [disabled]="
+                                                            !canMoveAssetDown(
+                                                                node
+                                                            )
+                                                        "
+                                                        [matTooltip]="
+                                                            'Move asset down'
+                                                                | translate
+                                                        "
+                                                        [attr.aria-label]="
+                                                            'Move asset down'
+                                                                | translate
+                                                        "
+                                                        (click)="
+                                                            moveAsset(node, 1);
+                                                            
$event.stopPropagation()
+                                                        "
+                                                    >
+                                                        <mat-icon
+                                                            
>arrow_downward</mat-icon
+                                                        >
+                                                    </button>
+                                                }
+                                                @if (canDeleteAsset(node)) {
+                                                    <button
+                                                        mat-icon-button
+                                                        type="button"
+                                                        [matMenuTriggerFor]="
+                                                            assetMenu
+                                                        "
+                                                        [matTooltip]="
+                                                            'More options'
+                                                                | translate
+                                                        "
+                                                        [attr.aria-label]="
+                                                            'More options'
+                                                                | translate
+                                                        "
+                                                        (click)="
+                                                            
$event.stopPropagation()
+                                                        "
+                                                    >
+                                                        <mat-icon
+                                                            
>more_vert</mat-icon
+                                                        >
+                                                    </button>
+                                                    <mat-menu
+                                                        #assetMenu="matMenu"
+                                                    >
+                                                        <button
+                                                            mat-menu-item
+                                                            (click)="
+                                                                addAsset(node);
+                                                                
$event.stopPropagation()
+                                                            "
+                                                            [attr.data-cy]="
+                                                                'add-asset-' +
+                                                                node.assetName
+                                                            "
+                                                        >
+                                                            <mat-icon
+                                                                >add</mat-icon
+                                                            >
+                                                            <span>{{
+                                                                'Add'
+                                                                    | translate
+                                                            }}</span>
+                                                        </button>
+                                                        <button
+                                                            mat-menu-item
+                                                            (click)="
+                                                                deleteAsset(
+                                                                    node
+                                                                );
+                                                                
$event.stopPropagation()
+                                                            "
+                                                        >
+                                                            <mat-icon
+                                                                
>delete</mat-icon
+                                                            >
+                                                            <span>{{
+                                                                'Delete'
+                                                                    | translate
+                                                            }}</span>
+                                                        </button>
+                                                    </mat-menu>
+                                                } @else {
+                                                    <button
+                                                        mat-icon-button
+                                                        (click)="
+                                                            addAsset(node);
+                                                            
$event.stopPropagation()
+                                                        "
+                                                        [attr.data-cy]="
+                                                            'add-asset-' +
+                                                            node.assetName
+                                                        "
+                                                        color="accent"
+                                                    >
+                                                        
<mat-icon>add</mat-icon>
+                                                    </button>
+                                                }
                                             </div>
                                         }
                                     </div>
diff --git 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.scss
 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.scss
index 7a8e01c302..0d70087255 100644
--- 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.scss
+++ 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.scss
@@ -53,12 +53,18 @@
     padding-left: 6px;
 }
 
-.mat-tree-node {
+.mat-tree-node,
+.tree-row {
     min-height: 48px;
     font-family: inherit;
 }
 
-.mat-tree-node:hover .asset-node {
+.tree-row {
+    width: 100%;
+    border-radius: 10px;
+}
+
+.tree-row:hover .asset-node {
     background: var(--color-bg-1);
 }
 
@@ -77,10 +83,33 @@
     background: var(--color-bg-0);
 }
 
-.asset-node:hover {
+.cdk-drag-preview .asset-node,
+.cdk-drag-preview.asset-node {
+    box-shadow: 0 4px 12px rgb(0 0 0 / 20%);
+}
+
+.cdk-drag-placeholder {
+    opacity: 0.35;
+}
+
+.cdk-drag-animating,
+.cdk-drop-list-dragging .mat-tree-node:not(.cdk-drag-placeholder) {
+    transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+}
+
+.tree-row:hover .asset-node {
     border-color: var(--color-primary);
 }
 
+.tree-row.drop-target-active .asset-node {
+    background: color-mix(in srgb, var(--color-primary) 12%, white);
+    box-shadow: inset 0 0 0 2px var(--color-primary);
+}
+
+.tree-row.drop-target-active .node-toggle {
+    color: var(--color-primary);
+}
+
 .asset-node-disabled {
     color: var(--color-bg-3);
     cursor: default;
@@ -102,6 +131,7 @@
     display: flex;
     align-items: center;
     gap: 10px;
+    min-width: 0;
 }
 
 .node-icon {
@@ -119,6 +149,7 @@
 .node-text {
     display: flex;
     flex-direction: column;
+    min-width: 0;
 }
 
 .node-title {
@@ -135,6 +166,11 @@
     gap: 6px;
 }
 
+.node-drag-handle {
+    cursor: grab;
+    flex: 0 0 auto;
+}
+
 .node-toggle {
     margin-right: 6px;
 }
diff --git 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.ts
 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.ts
index e4efb77e9e..beeede21ba 100644
--- 
a/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.ts
+++ 
b/ui/src/app/assets/components/asset-details/edit-asset/asset-selection-panel/asset-selection-panel.component.ts
@@ -20,12 +20,19 @@ import {
     Component,
     EventEmitter,
     Input,
+    OnDestroy,
     OnInit,
     Output,
     ViewChild,
 } from '@angular/core';
 import { SpAsset, SpAssetModel } from '@streampipes/platform-services';
 import { NestedTreeControl } from '@angular/cdk/tree';
+import {
+    CdkDrag,
+    CdkDragDrop,
+    CdkDragHandle,
+    CdkDropList,
+} from '@angular/cdk/drag-drop';
 import {
     MatNestedTreeNode,
     MatTree,
@@ -45,6 +52,10 @@ import { SpBasicViewComponent } from 
'@streampipes/shared-ui';
 import { MatIcon } from '@angular/material/icon';
 import { MatIconButton } from '@angular/material/button';
 import { TranslatePipe } from '@ngx-translate/core';
+import { MatTooltip } from '@angular/material/tooltip';
+import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
+
+const HOVER_EXPAND_DELAY_MS = 500;
 
 @Component({
     selector: 'sp-asset-selection-panel',
@@ -65,9 +76,16 @@ import { TranslatePipe } from '@ngx-translate/core';
         MatNestedTreeNode,
         MatTreeNodeOutlet,
         TranslatePipe,
+        CdkDropList,
+        CdkDrag,
+        CdkDragHandle,
+        MatTooltip,
+        MatMenuTrigger,
+        MatMenu,
+        MatMenuItem,
     ],
 })
-export class SpAssetSelectionPanelComponent implements OnInit {
+export class SpAssetSelectionPanelComponent implements OnInit, OnDestroy {
     @Input()
     assetModel: SpAssetModel;
 
@@ -87,7 +105,11 @@ export class SpAssetSelectionPanelComponent implements 
OnInit {
     @ViewChild('tree') tree;
 
     hasChild = (_: number, node: SpAsset) =>
-        !!node.assets && node.assets.length > 0;
+        this.editMode || (!!node.assets && node.assets.length > 0);
+
+    dropTargetIds: string[] = [];
+    activeDropTargetAssetId: string | undefined;
+    private hoverExpandTimer: ReturnType<typeof setTimeout> | undefined;
 
     ngOnInit(): void {
         this.treeControl = new NestedTreeControl<SpAsset>(node => node.assets);
@@ -95,25 +117,28 @@ export class SpAssetSelectionPanelComponent implements 
OnInit {
         this.resetTree();
     }
 
+    ngOnDestroy(): void {
+        this.cancelHoverExpand();
+    }
+
     selectNode(asset: SpAsset, rootNode: boolean) {
         this.selectedAssetEmitter.emit({ asset, rootNode });
         this.expandToAsset(asset.assetId);
     }
 
     addAsset(node: SpAsset) {
-        if (!node.assets) {
-            node.assets = [];
-        }
-        node.assets.push(this.makeNewAsset());
+        const newAsset = this.makeNewAsset();
+        this.getChildAssets(node).push(newAsset);
         this.dataSource.data = [this.assetModel];
         this.treeControl.dataNodes = [this.assetModel];
         this.rerenderTree();
+        this.expandToAsset(newAsset.assetId);
     }
 
     rerenderTree(): void {
         this.dataSource.data = [];
         this.dataSource.data = [this.assetModel];
-        this.treeControl.expandAll();
+        this.refreshDropListIds();
     }
 
     deleteAsset(node: SpAsset) {
@@ -121,6 +146,109 @@ export class SpAssetSelectionPanelComponent implements 
OnInit {
         this.rerenderTree();
     }
 
+    moveAsset(node: SpAsset, offset: number): void {
+        const move = this.getAssetMove(node, offset);
+        if (!move) {
+            return;
+        }
+
+        const [movedAsset] = move.siblingAssets.splice(move.sourceIndex, 1);
+        move.siblingAssets.splice(move.targetIndex, 0, movedAsset);
+        this.rerenderTree();
+        this.expandToAsset(node.assetId);
+    }
+
+    canMoveAssetUp(node: SpAsset): boolean {
+        return this.canMoveAsset(node, -1);
+    }
+
+    canMoveAssetDown(node: SpAsset): boolean {
+        return this.canMoveAsset(node, 1);
+    }
+
+    dropAssetIntoParent(
+        event: CdkDragDrop<SpAsset[]>,
+        targetParent: SpAsset,
+    ): void {
+        this.cancelHoverExpand();
+        this.activeDropTargetAssetId = undefined;
+        const draggedAsset = event.item.data as SpAsset;
+        let assetWasMoved = false;
+        if (this.canDropAsset(draggedAsset, targetParent)) {
+            const sourceAssets = this.findParentAssets(draggedAsset.assetId);
+            const targetAssets = this.getChildAssets(targetParent);
+            const sourceIndex = sourceAssets?.findIndex(
+                asset => asset.assetId === draggedAsset.assetId,
+            );
+
+            if (sourceAssets && sourceIndex !== undefined && sourceIndex >= 0) 
{
+                sourceAssets.splice(sourceIndex, 1);
+                targetAssets.push(draggedAsset);
+                assetWasMoved = true;
+            }
+        }
+
+        this.rerenderTree();
+        if (assetWasMoved) {
+            this.expandToAsset(draggedAsset.assetId);
+            if (this.selectedAsset?.assetId === draggedAsset.assetId) {
+                this.selectNode(draggedAsset, this.isRootNode(draggedAsset));
+            }
+        }
+    }
+
+    canEnterDropList = (drag: CdkDrag, drop: CdkDropList): boolean => {
+        const targetParent = this.findAssetByDropListId(drop.id);
+        return this.canDropAsset(drag.data as SpAsset, targetParent);
+    };
+
+    markDropTarget(node: SpAsset): void {
+        this.activeDropTargetAssetId = node.assetId;
+        this.scheduleHoverExpand(node);
+    }
+
+    clearDropTarget(node: SpAsset): void {
+        if (this.activeDropTargetAssetId === node.assetId) {
+            this.activeDropTargetAssetId = undefined;
+            this.cancelHoverExpand();
+        }
+    }
+
+    canDropAsset(
+        draggedAsset: SpAsset | undefined,
+        targetParent: SpAsset | undefined,
+    ): boolean {
+        if (!draggedAsset || !targetParent) {
+            return false;
+        }
+        const sourceAssets = this.findParentAssets(draggedAsset.assetId);
+        return (
+            !this.isRootNode(draggedAsset) &&
+            draggedAsset.assetId !== targetParent.assetId &&
+            sourceAssets !== (targetParent.assets || []) &&
+            !this.isDescendantOf(draggedAsset, targetParent.assetId)
+        );
+    }
+
+    getDropTargetId(node: SpAsset): string {
+        return `asset-drop-target-${node.assetId}`;
+    }
+
+    getChildAssets(node: SpAsset): SpAsset[] {
+        if (!node.assets) {
+            node.assets = [];
+        }
+        return node.assets;
+    }
+
+    isRootNode(node: SpAsset): boolean {
+        return node.assetId === this.assetModel.assetId;
+    }
+
+    canDeleteAsset(node: SpAsset): boolean {
+        return !this.isRootNode(node) && !node.assets?.length;
+    }
+
     removeAssetWithId(assets: SpAsset[], id: string) {
         for (let i = 0; i < assets.length; i++) {
             if (assets[i].assetId === id) {
@@ -154,9 +282,99 @@ export class SpAssetSelectionPanelComponent implements 
OnInit {
     private resetTree() {
         this.dataSource.data = [this.assetModel];
         this.treeControl.dataNodes = [this.assetModel];
+        this.refreshDropListIds();
         this.treeControl.expandAll();
     }
 
+    private refreshDropListIds(): void {
+        this.dropTargetIds = this.getAllAssets(this.assetModel).map(node =>
+            this.getDropTargetId(node),
+        );
+    }
+
+    private getAllAssets(node: SpAsset): SpAsset[] {
+        return [
+            node,
+            ...(node.assets?.flatMap(child => this.getAllAssets(child)) || []),
+        ];
+    }
+
+    private isDescendantOf(asset: SpAsset, targetAssetId: string): boolean {
+        return (
+            asset.assets?.some(
+                child =>
+                    child.assetId === targetAssetId ||
+                    this.isDescendantOf(child, targetAssetId),
+            ) || false
+        );
+    }
+
+    private findParentAssets(assetId: string): SpAsset[] | undefined {
+        return this.findParentAssetsRecursive(this.assetModel, assetId);
+    }
+
+    private findParentAssetsRecursive(
+        parent: SpAsset,
+        assetId: string,
+    ): SpAsset[] | undefined {
+        const children = parent.assets || [];
+        if (children.some(child => child.assetId === assetId)) {
+            return children;
+        }
+        for (const child of children) {
+            const parentAssets = this.findParentAssetsRecursive(child, 
assetId);
+            if (parentAssets) {
+                return parentAssets;
+            }
+        }
+        return undefined;
+    }
+
+    private findAssetByDropListId(dropListId: string): SpAsset | undefined {
+        return this.getAllAssets(this.assetModel).find(
+            node => this.getDropTargetId(node) === dropListId,
+        );
+    }
+
+    private canMoveAsset(node: SpAsset, offset: number): boolean {
+        return !!this.getAssetMove(node, offset);
+    }
+
+    private getAssetMove(node: SpAsset, offset: number) {
+        const siblingAssets = this.findParentAssets(node.assetId);
+        const sourceIndex = siblingAssets?.findIndex(
+            asset => asset.assetId === node.assetId,
+        );
+        const targetIndex =
+            sourceIndex !== undefined ? sourceIndex + offset : -1;
+
+        return !this.isRootNode(node) &&
+            siblingAssets &&
+            sourceIndex !== undefined &&
+            sourceIndex >= 0 &&
+            targetIndex >= 0 &&
+            targetIndex < siblingAssets.length
+            ? { siblingAssets, sourceIndex, targetIndex }
+            : undefined;
+    }
+
+    private scheduleHoverExpand(node: SpAsset): void {
+        this.cancelHoverExpand();
+        this.hoverExpandTimer = setTimeout(() => {
+            if (this.activeDropTargetAssetId === node.assetId) {
+                this.treeControl.expand(node);
+            }
+            this.hoverExpandTimer = undefined;
+        }, HOVER_EXPAND_DELAY_MS);
+    }
+
+    private cancelHoverExpand(): void {
+        if (this.hoverExpandTimer) {
+            clearTimeout(this.hoverExpandTimer);
+            this.hoverExpandTimer = undefined;
+        }
+    }
+
     private expandToAsset(assetId: string) {
         const path = this.findPath(this.assetModel, assetId);
         if (path) {


Reply via email to