humainflow-web/src/app/shared/nodes/node-parameters/node-parameters.ts

117 lines
4.8 KiB
TypeScript

// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
// SPDX-License-Identifier: AGPL-3.0-or-later
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
import { CommonModule } from '@angular/common';
import { Component, effect, ElementRef, Input, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NodeListFieldComponent } from '../node-list-field/node-list-field';
import { humanizeChoice } from '../node-utility';
/**
* What a node shows of its configuration, built from its schema: the same fields whatever kind of
* node it is, so a block and a resource are edited the same way while they are drawn differently.
*
* <p>The node itself stays the host: it holds the schema, the values and the editors, and this
* only draws them. Hence the single `host` input rather than an input and an output per field.
*/
export type NodeParametersHost = {
parameterDisplaySections: readonly any[];
schemaReady: boolean;
readonly isReadonly: boolean;
formatDynamicInputToken(text: string): string;
openParameterEditor(path: string, event: Event): void;
openFieldPreview(field: any, event: Event): void;
openMainContentEditor(path: string, event: Event): void;
openMainContentPreview(field: any, event: Event): void;
openOptionalGroupEditor(path: string, event: Event): void;
toggleBooleanParameter(path: string, event: Event): void;
addArrayItem(path: string): void;
editArrayItem(path: string, index: number): void;
removeArrayItem(path: string, index: number): void;
viewItemDefinition(value: unknown): void;
/** The one-field editor, opened from a field and rendered here as a modal. */
localEditorOpen: boolean;
localEditorLabel: string;
localEditorPath: string | null;
localEditorType: string | null;
localEditorValue: any;
localEditorOptions: readonly any[];
localEditorRows: number | null;
localEditorMin: number | null;
localEditorMax: number | null;
localEditorMaxLength: number | null;
localEditorTip: string | null;
localEditorWidget: string | null;
localEditorFreeText: boolean;
localEditorHasRetriever: boolean;
localEditorLoading: boolean;
localEditorDefaultHint(): string | null;
localEditorUseInput: boolean;
localEditorBindableAsInput: boolean;
localEditorBindableInputName: string | null;
localEditorFromGlobal: boolean;
localEditorGlobalName: any;
localEditorGlobalInputs: readonly any[];
localEditorError(): string | null;
localEditorSourceMode(): any;
localEditorUsesDefault(): boolean;
showLocalEditorDefault(): boolean;
canTakeGlobalInput(): boolean;
canSaveLocalEditor(): boolean;
canUseLocalEditorDefault(): boolean;
editorPlaceholder(): string;
globalInputLabel(globalInput: any): string;
onLocalEditorGlobalInputChange(value: any): void;
onLocalEditorSourceModeChange(value: any): void;
useLocalEditorDefault(event: Event): void;
saveSimpleParamEditor(event: Event): void;
closeSimpleParamEditor(event: Event): void;
};
@Component({
selector: 'app-node-parameters',
imports: [CommonModule, FormsModule, NodeListFieldComponent],
templateUrl: './node-parameters.html',
styleUrl: './node-parameters.css'
// Default change detection on purpose: the host holds the values and mutates them in place, so
// there is no input to change for OnPush to notice. Checked whenever the node's own view is.
})
export class NodeParametersComponent {
@Input({ required: true }) host!: NodeParametersHost;
/** The editor dialog must stay mounted even while the card's fields are collapsed. */
@Input() showParameters = true;
/**
* This renders inside a node card, which rete.js positions with a CSS `transform` for pan and
* zoom, so a plain fixed-position backdrop would be confined to the node's box. A native
* `<dialog>` shown with `showModal()` escapes that through the browser's top layer.
*/
private readonly simpleEditorDialog = viewChild<ElementRef<HTMLDialogElement>>('simpleEditorDialog');
constructor() {
effect(() => {
const dialog = this.simpleEditorDialog()?.nativeElement;
if (dialog && typeof dialog.showModal === 'function' && !dialog.open) {
dialog.showModal();
}
});
}
/** A field's value as shown: a fixed choice in words, anything else as written. */
shownValue(field: { value: string; choice?: boolean }): string {
return field.choice ? humanizeChoice(field.value) : field.value;
}
/** An option of a fixed choice in words, when it has no label of its own. */
optionLabel(option: { value: unknown; label: string }): string {
return option.label === String(option.value) ? humanizeChoice(option.label) : option.label;
}
onSimpleEditorDialogClick(event: MouseEvent) {
if (event.target === this.simpleEditorDialog()?.nativeElement) {
this.host.closeSimpleParamEditor(event);
}
}
}