117 lines
4.8 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|