diff --git a/src/app/services/dialogs/node-settings-dialog.ts b/src/app/services/dialogs/node-settings-dialog.ts index a627983..6359745 100644 --- a/src/app/services/dialogs/node-settings-dialog.ts +++ b/src/app/services/dialogs/node-settings-dialog.ts @@ -1,6 +1,6 @@ import { Injectable, signal } from "@angular/core"; -export type NodeSettingFieldType = "text" | "password" | "textarea" | "select" | "checkbox" | "number" | "display"; +export type NodeSettingFieldType = "text" | "password" | "textarea" | "select" | "checkbox" | "number" | "display" | "template"; export type NodeSettingOption = { label: string; @@ -52,6 +52,13 @@ export type NodeSettingField = { */ group?: string; options?: NodeSettingOption[]; + /** + * Only for type `template`: the substitution map for `${{name}}` placeholders in the field's + * value. Rendered as per-placeholder expandable segments (see TemplatePlaceholderTextComponent) + * instead of a flat string replace, so a resolved value never gets silently duplicated or turns + * the preview into an unreadable wall of text. + */ + templateValues?: Record; }; export type NodeSettingsValues = Record; @@ -77,7 +84,7 @@ export function validateFieldValue( constraints: FieldValueConstraints, value: string | boolean | number | null | undefined ): string | null { - if (constraints.type === 'checkbox' || constraints.type === 'display') return null; + if (constraints.type === 'checkbox' || constraints.type === 'display' || constraints.type === 'template') return null; const text = typeof value === 'string' ? value.trim() : value == null ? '' : String(value); if (text.length === 0) { diff --git a/src/app/shared/node-settings-dialog/node-settings-dialog.html b/src/app/shared/node-settings-dialog/node-settings-dialog.html index fd5453d..a3bfe89 100644 --- a/src/app/shared/node-settings-dialog/node-settings-dialog.html +++ b/src/app/shared/node-settings-dialog/node-settings-dialog.html @@ -85,6 +85,15 @@
{{ draft[field.key] }}
} + @case ('template') { +
+ {{ field.label }} + + +
+ } @case ('textarea') { {{ field.label }} diff --git a/src/app/shared/node-settings-dialog/node-settings-dialog.ts b/src/app/shared/node-settings-dialog/node-settings-dialog.ts index b783f02..bbf8f4c 100644 --- a/src/app/shared/node-settings-dialog/node-settings-dialog.ts +++ b/src/app/shared/node-settings-dialog/node-settings-dialog.ts @@ -14,11 +14,12 @@ import { NodeSettingsValues, validateFieldValue } from '@services/dialogs/node-settings-dialog'; +import { TemplatePlaceholderTextComponent } from '@shared/template-placeholder-text/template-placeholder-text'; @Component({ selector: 'app-node-settings-dialog-host', standalone: true, - imports: [CommonModule, FormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule, MatTooltipModule], + imports: [CommonModule, FormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule, MatTooltipModule, TemplatePlaceholderTextComponent], templateUrl: './node-settings-dialog.html', changeDetection: ChangeDetectionStrategy.OnPush }) @@ -151,6 +152,11 @@ export class NodeSettingsDialogHostComponent { } } + draftText(key: string): string { + const value = this.draft[key]; + return typeof value === 'string' ? value : String(value ?? ''); + } + isPasswordVisible(key: string): boolean { return this.passwordVisibility[key] === true; } 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 8bfd121..0304ec1 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 @@ -648,14 +648,14 @@ export class TaskStepNodeComponent { event?.preventDefault(); event?.stopPropagation(); if (!field.expandable) return; - void this.openReadonlyTextDialog(field.label, this.resolvePreviewText(field.value)); + void this.openReadonlyTextDialog(field.label, field.value); } openMainContentPreview(field: MainContentView, event?: Event) { event?.preventDefault(); event?.stopPropagation(); if (!field.expandable) return; - void this.openReadonlyTextDialog(field.label, this.resolvePreviewText(field.rawValue)); + void this.openReadonlyTextDialog(field.label, field.rawValue); } private get blockConfiguration(): Record | null { @@ -1531,46 +1531,41 @@ export class TaskStepNodeComponent { return lineCount > 2 || normalized.length > 80; } + /** + * A value with `${{name}}` placeholders is shown as text plus its own expandable segment per + * placeholder (via the `template` field type / TemplatePlaceholderTextComponent), never as a + * flat string replace - the latter silently duplicates a resolved value wherever the same + * placeholder repeats in the source text (e.g. `${{x}} != null && ${{x}}.contains(...)`), and + * dumps a raw, unbounded runtime value inline. + */ private async openReadonlyTextDialog(label: string, value: string) { + const source = String(value ?? ''); + const hasPlaceholders = source.includes('${{'); + await this.settingsDialog.open({ title: label, previewOnly: true, fields: [ - { - key: 'value', - label, - type: 'textarea', - readonly: true, - rows: 18 - } + hasPlaceholders + ? { + key: 'value', + label, + type: 'template', + readonly: true, + templateValues: this.templateSubstitutions() + } + : { + key: 'value', + label, + type: 'textarea', + readonly: true, + rows: 18 + } ], initial: { - value + value: source } }); } - private resolvePreviewText(value: string): string { - const source = String(value ?? ''); - if (!source.includes('${{')) return source; - - return source.replace(/\$\{\{\s*([^}]+?)\s*\}\}/g, (token, rawKey: string) => { - const key = String(rawKey ?? '').trim(); - if (!key) return token; - - const configInputs = this.blockConfiguration?.['__executionInputs']; - const inputs = configInputs && typeof configInputs === 'object' && !Array.isArray(configInputs) - ? configInputs as Record - : null; - if (!inputs || !Object.prototype.hasOwnProperty.call(inputs, key)) { - return token; - } - - const resolved = inputs[key]; - if (resolved == null) return token; - if (typeof resolved === 'string') return resolved; - return valueToDisplayString(resolved); - }); - } - }