- ISTI-CNR
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
+
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, signal } from '@angular/core';
+
+export type NodeListFieldItem = {
+ index: number;
+ summary: string;
+ /** Something the item can be opened to read in full, when it has one. */
+ definition?: unknown | null;
+};
+
+/**
+ * A node parameter that is a list: its name and how many items it has, the items themselves on
+ * demand. Shared by every node that shows lists - blocks, the execution view, and containers once
+ * one has a list parameter - so they all read and behave the same.
+ *
+ * Starts closed. With no items there is nothing to open, and the add button is the way in; an
+ * item added opens the list, so what was just added is there to see rather than only counted.
+ */
+@Component({
+ selector: 'app-node-list-field',
+ standalone: true,
+ templateUrl: './node-list-field.html',
+ styleUrl: './node-list-field.css',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class NodeListFieldComponent implements OnChanges {
+ @Input({ required: true }) label = '';
+ @Input() items: NodeListFieldItem[] = [];
+ /** Read-only views - an execution - show the items and offer nothing to change them. */
+ @Input() readonly = false;
+
+ @Output() readonly add = new EventEmitter();
+ @Output() readonly edit = new EventEmitter();
+ @Output() readonly remove = new EventEmitter();
+ @Output() readonly view = new EventEmitter();
+
+ readonly expanded = signal(false);
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const change = changes['items'];
+ if (!change || change.firstChange) return;
+ const before = Array.isArray(change.previousValue) ? change.previousValue.length : 0;
+ const after = this.items?.length ?? 0;
+ if (after > before) this.expanded.set(true);
+ if (after === 0) this.expanded.set(false);
+ }
+
+ toggle(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+ if (!this.items.length) return;
+ this.expanded.update((open) => !open);
+ }
+
+ emit(emitter: EventEmitter, value: T, event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+ emitter.emit(value);
+ }
+}
diff --git a/src/app/shared/nodes/task-step-node/task-step-node.html b/src/app/shared/nodes/task-step-node/task-step-node.html
index 17b7f3a..445b322 100644
--- a/src/app/shared/nodes/task-step-node/task-step-node.html
+++ b/src/app/shared/nodes/task-step-node/task-step-node.html
@@ -406,16 +406,7 @@
@for (arrayField of arrayFields; track arrayField.path) {
@if (arrayField.items.length) {
-
-
-
{{ arrayField.label }}
-
- @for (item of arrayField.items; track item.index) {
-
- {{ item.summary }}
-
- }
-
+
}
}
diff --git a/src/app/shared/nodes/task-step-node/task-step-node.ts b/src/app/shared/nodes/task-step-node/task-step-node.ts
index bbc4dec..bf99854 100644
--- a/src/app/shared/nodes/task-step-node/task-step-node.ts
+++ b/src/app/shared/nodes/task-step-node/task-step-node.ts
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
+import { NodeListFieldComponent } from '../node-list-field/node-list-field';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, inject, viewChild } from '@angular/core';
import { ClassicPreset } from 'rete';
@@ -87,7 +88,7 @@ type FieldUiMeta = {
@Component({
selector: 'app-task-step-node',
- imports: [CommonModule, ReteModule, BiasAnnotationsComponent],
+ imports: [CommonModule, ReteModule, BiasAnnotationsComponent, NodeListFieldComponent],
templateUrl: './task-step-node.html',
styleUrl: './task-step-node.css',
host: {