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) {
