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

SvenO3 pushed a commit to branch 
4568-allow-reordering-and-restructuring-of-assets-in-asset-view-edit-mode
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/4568-allow-reordering-and-restructuring-of-assets-in-asset-view-edit-mode
 by this push:
     new bd8e248c13 Add asset reordering via drag and drop
bd8e248c13 is described below

commit bd8e248c1357fc1463336fb85e6fecac8aa44df9
Author: Sven Oehler <[email protected]>
AuthorDate: Fri Jun 19 17:50:29 2026 +0200

    Add asset reordering via drag and drop
---
 .../asset-selection-panel.component.html           | 146 +++++++++++++++--
 .../asset-selection-panel.component.scss           |  42 ++++-
 .../asset-selection-panel.component.ts             | 181 ++++++++++++++++++++-
 3 files changed, 346 insertions(+), 23 deletions(-)

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..accf15e8ec 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]="
@@ -92,14 +133,20 @@
                                                     'add-asset-' +
                                                     node.assetName
                                                 "
-                                                (click)="addAsset(node)"
+                                                (click)="
+                                                    addAsset(node);
+                                                    $event.stopPropagation()
+                                                "
                                                 color="accent"
                                             >
                                                 <mat-icon>add</mat-icon>
                                             </button>
                                             <button
                                                 mat-icon-button
-                                                (click)="deleteAsset(node)"
+                                                (click)="
+                                                    deleteAsset(node);
+                                                    $event.stopPropagation()
+                                                "
                                                 color="accent"
                                             >
                                                 <mat-icon>delete</mat-icon>
@@ -112,7 +159,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 +200,44 @@
                                     </button>
                                     <div
                                         class="asset-node"
-                                        [class.root-node]="
-                                            node.assetId === assetModel.assetId
+                                        [class.root-node]="isRootNode(node)"
+                                        (click)="
+                                            selectNode(node, isRootNode(node))
                                         "
-                                        (click)="selectNode(node, true)"
+                                        cdkDrag
+                                        [cdkDragData]="node"
+                                        [cdkDragDisabled]="
+                                            !editMode || isRootNode(node)
+                                        "
+                                        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 +245,12 @@
                                                     selectedAsset.assetId
                                                 "
                                             >
-                                                account_tree
+                                                {{
+                                                    isRootNode(node) ||
+                                                    node.assets?.length
+                                                        ? 'account_tree'
+                                                        : 'device_hub'
+                                                }}
                                             </mat-icon>
                                             <div class="node-text">
                                                 <div
@@ -159,11 +262,11 @@
                                                 >
                                                     {{ 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>
@@ -171,7 +274,10 @@
                                             <div class="node-actions">
                                                 <button
                                                     mat-icon-button
-                                                    (click)="addAsset(node)"
+                                                    (click)="
+                                                        addAsset(node);
+                                                        
$event.stopPropagation()
+                                                    "
                                                     [attr.data-cy]="
                                                         'add-asset-' +
                                                         node.assetName
@@ -180,6 +286,20 @@
                                                 >
                                                     <mat-icon>add</mat-icon>
                                                 </button>
+                                                @if (!node.assets?.length) {
+                                                    <button
+                                                        mat-icon-button
+                                                        (click)="
+                                                            deleteAsset(node);
+                                                            
$event.stopPropagation()
+                                                        "
+                                                        color="accent"
+                                                    >
+                                                        <mat-icon
+                                                            >delete</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..e018a4fbfa 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,9 @@ 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';
+
+const HOVER_EXPAND_DELAY_MS = 500;
 
 @Component({
     selector: 'sp-asset-selection-panel',
@@ -65,9 +75,13 @@ import { TranslatePipe } from '@ngx-translate/core';
         MatNestedTreeNode,
         MatTreeNodeOutlet,
         TranslatePipe,
+        CdkDropList,
+        CdkDrag,
+        CdkDragHandle,
+        MatTooltip,
     ],
 })
-export class SpAssetSelectionPanelComponent implements OnInit {
+export class SpAssetSelectionPanelComponent implements OnInit, OnDestroy {
     @Input()
     assetModel: SpAssetModel;
 
@@ -87,7 +101,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,16 +113,17 @@ 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());
+        this.getChildAssets(node).push(this.makeNewAsset());
         this.dataSource.data = [this.assetModel];
         this.treeControl.dataNodes = [this.assetModel];
         this.rerenderTree();
@@ -113,7 +132,7 @@ export class SpAssetSelectionPanelComponent implements 
OnInit {
     rerenderTree(): void {
         this.dataSource.data = [];
         this.dataSource.data = [this.assetModel];
-        this.treeControl.expandAll();
+        this.refreshDropListIds();
     }
 
     deleteAsset(node: SpAsset) {
@@ -121,6 +140,85 @@ export class SpAssetSelectionPanelComponent implements 
OnInit {
         this.rerenderTree();
     }
 
+    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;
+    }
+
     removeAssetWithId(assets: SpAsset[], id: string) {
         for (let i = 0; i < assets.length; i++) {
             if (assets[i].assetId === id) {
@@ -154,9 +252,78 @@ 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 {
+        const dropTarget = this.getAllAssets(this.assetModel).find(
+            node => this.getDropTargetId(node) === dropListId,
+        );
+        return dropTarget;
+    }
+
+    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