Show step preview placeholders as expandable values, not a flat string replace
A node's Condition/prompt preview resolved ${{name}} placeholders with a
single-pass string replace, so a runtime value got duplicated wherever the
same placeholder repeated in the source text (e.g. a Conditional's
${{x}} != null && ${{x}}.contains(...) pattern) and long/verbose values were
dumped inline unbounded. Reuse the existing template-placeholder machinery
(already used for HumanDecisionBlock/HumanInteractionBlock text) instead: a
new "template" field type on the settings dialog renders each placeholder as
its own expandable segment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
81f2a890f2
commit
f41962993c
|
|
@ -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<string, unknown>;
|
||||
};
|
||||
|
||||
export type NodeSettingsValues = Record<string, string | boolean | number>;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,15 @@
|
|||
<div class="text-sm text-slate-800 whitespace-pre-wrap break-words">{{ draft[field.key] }}</div>
|
||||
</fieldset>
|
||||
}
|
||||
@case ('template') {
|
||||
<fieldset class="mt-1 border border-slate-200 rounded-md bg-slate-50 p-3">
|
||||
<legend class="px-1 text-xs font-semibold text-slate-600">{{ field.label }}</legend>
|
||||
<app-template-placeholder-text
|
||||
[text]="draftText(field.key)"
|
||||
[values]="field.templateValues ?? {}">
|
||||
</app-template-placeholder-text>
|
||||
</fieldset>
|
||||
}
|
||||
@case ('textarea') {
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="mt-1">
|
||||
<mat-label>{{ field.label }}</mat-label>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any> | 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<string, unknown>
|
||||
: 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue