Support typed execution inputs and task node IO labels
This commit is contained in:
parent
34f400ca40
commit
c0d2f75285
|
|
@ -12,12 +12,24 @@ export abstract class TaskExecutionsCallServiceBase {
|
|||
inputName: string,
|
||||
value: string
|
||||
): Observable<TaskExecution>;
|
||||
abstract prepareStringArrayInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
inputName: string,
|
||||
values: string[]
|
||||
): Observable<TaskExecution>;
|
||||
abstract prepareFileInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
inputName: string,
|
||||
file: File
|
||||
): Observable<TaskExecution>;
|
||||
abstract prepareFileArrayInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
inputName: string,
|
||||
files: File[]
|
||||
): Observable<TaskExecution>;
|
||||
abstract submitInteractionText(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
|
|||
|
|
@ -506,6 +506,18 @@ export class TaskExecutionsCallServiceFake extends TaskExecutionsCallServiceBase
|
|||
return of(execution);
|
||||
}
|
||||
|
||||
override prepareStringArrayInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
inputName: string,
|
||||
values: string[]
|
||||
): Observable<TaskExecution> {
|
||||
const execution = this.findExecution(executionId);
|
||||
execution.context.inputs[`${nodeId}:${inputName}`] = values;
|
||||
execution.context.status = execution.context.waitingSteps.length ? 'WAITING' : execution.context.status;
|
||||
return of(execution);
|
||||
}
|
||||
|
||||
override prepareFileInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
@ -518,6 +530,18 @@ export class TaskExecutionsCallServiceFake extends TaskExecutionsCallServiceBase
|
|||
return of(execution);
|
||||
}
|
||||
|
||||
override prepareFileArrayInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
inputName: string,
|
||||
files: File[]
|
||||
): Observable<TaskExecution> {
|
||||
const execution = this.findExecution(executionId);
|
||||
execution.context.inputs[`${nodeId}:${inputName}`] = files.map((file) => file?.name ?? '');
|
||||
execution.context.status = execution.context.waitingSteps.length ? 'WAITING' : execution.context.status;
|
||||
return of(execution);
|
||||
}
|
||||
|
||||
override submitInteractionText(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
|
|||
});
|
||||
}
|
||||
|
||||
override prepareStringArrayInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
inputName: string,
|
||||
values: string[]
|
||||
): Observable<TaskExecution> {
|
||||
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/texts`;
|
||||
return this.http.put<TaskExecution>(url, values);
|
||||
}
|
||||
|
||||
override prepareFileInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
@ -48,6 +58,20 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
|
|||
return this.http.put<TaskExecution>(url, formData);
|
||||
}
|
||||
|
||||
override prepareFileArrayInput(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
inputName: string,
|
||||
files: File[]
|
||||
): Observable<TaskExecution> {
|
||||
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/files`;
|
||||
const formData = new FormData();
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
return this.http.put<TaskExecution>(url, formData);
|
||||
}
|
||||
|
||||
override submitInteractionText(
|
||||
executionId: string,
|
||||
nodeId: string,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,16 @@ export class TaskExecutionsService {
|
|||
);
|
||||
}
|
||||
|
||||
prepareStringArrayInput(executionId: string, nodeId: string, inputName: string, values: string[]) {
|
||||
return this.taskExecutionsCallService.prepareStringArrayInput(executionId, nodeId, inputName, values).pipe(
|
||||
tap(() => this.refresh()),
|
||||
catchError((err) => {
|
||||
console.error('Prepare string array input failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
prepareFileInput(executionId: string, nodeId: string, inputName: string, file: File) {
|
||||
return this.taskExecutionsCallService.prepareFileInput(executionId, nodeId, inputName, file).pipe(
|
||||
tap(() => this.refresh()),
|
||||
|
|
@ -87,6 +97,16 @@ export class TaskExecutionsService {
|
|||
);
|
||||
}
|
||||
|
||||
prepareFileArrayInput(executionId: string, nodeId: string, inputName: string, files: File[]) {
|
||||
return this.taskExecutionsCallService.prepareFileArrayInput(executionId, nodeId, inputName, files).pipe(
|
||||
tap(() => this.refresh()),
|
||||
catchError((err) => {
|
||||
console.error('Prepare file array input failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
submitInteractionText(executionId: string, nodeId: string, fieldName: string, value: string) {
|
||||
return this.taskExecutionsCallService.submitInteractionText(executionId, nodeId, fieldName, value).pipe(
|
||||
tap(() => this.refresh()),
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@
|
|||
.llm-row {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
min-height: 34px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -312,12 +312,12 @@
|
|||
}
|
||||
|
||||
.llm-pill {
|
||||
height: 24px;
|
||||
min-height: 32px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 0 8px;
|
||||
padding: 4px 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
|
@ -325,13 +325,32 @@
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.llm-pill-label {
|
||||
.llm-pill-meta {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.llm-pill-meta-output {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.llm-pill-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.llm-pill-kind {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.llm-pill-input {
|
||||
justify-content: flex-start;
|
||||
color: #166534;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,10 @@
|
|||
</span>
|
||||
}
|
||||
<span class="llm-pill llm-pill-input">
|
||||
<span class="llm-pill-label">{{ input.key }}</span>
|
||||
<span class="llm-pill-meta">
|
||||
<span class="llm-pill-name">{{ inputDisplayLabel(input.key) }}</span>
|
||||
<span class="llm-pill-kind">{{ inputKindLabel(input.key) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -109,7 +112,10 @@
|
|||
@for (output of outputs; track output.key) {
|
||||
<div class="llm-row llm-row-output">
|
||||
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">
|
||||
<span class="llm-pill-label">{{ output.key }}</span>
|
||||
<span class="llm-pill-meta llm-pill-meta-output">
|
||||
<span class="llm-pill-name">{{ outputDisplayLabel(output.key) }}</span>
|
||||
<span class="llm-pill-kind">{{ outputKindLabel(output.key) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
@if (isOutputConnected(output.key)) {
|
||||
<span class="llm-socket-wrap llm-socket-wrap-output">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||
import { ChangeDetectorRef, Component, HostBinding, Input, inject } from '@angular/core';
|
||||
import { ClassicPreset } from 'rete';
|
||||
import { ReteModule } from 'rete-angular-plugin/21';
|
||||
import { FlowPort } from '@models/flow';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { HumanInteractionDialogService } from '@services/dialogs/human-interaction-dialog';
|
||||
import { TaskExecutionsService } from '@services/task-executions/task-executions';
|
||||
|
|
@ -206,6 +207,22 @@ export class TaskStepNodeComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
inputDisplayLabel(inputKey: string): string {
|
||||
return this.portDisplayLabel('input', inputKey);
|
||||
}
|
||||
|
||||
outputDisplayLabel(outputKey: string): string {
|
||||
return this.portDisplayLabel('output', outputKey);
|
||||
}
|
||||
|
||||
inputKindLabel(inputKey: string): string {
|
||||
return this.portKindLabel('input', inputKey);
|
||||
}
|
||||
|
||||
outputKindLabel(outputKey: string): string {
|
||||
return this.portKindLabel('output', outputKey);
|
||||
}
|
||||
|
||||
hasMainContent(): boolean {
|
||||
return this.mainContentFields.length > 0;
|
||||
}
|
||||
|
|
@ -389,6 +406,28 @@ export class TaskStepNodeComponent {
|
|||
return values.filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
||||
}
|
||||
|
||||
private portDisplayLabel(kind: 'input' | 'output', key: string): string {
|
||||
const ports = this.resolvePorts(kind);
|
||||
const port = ports.find((candidate) => candidate.name === key);
|
||||
return port?.name ?? key;
|
||||
}
|
||||
|
||||
private portTypeLabel(port: FlowPort): string {
|
||||
const type = String(port.type ?? 'TEXT').toUpperCase();
|
||||
return port.multiple ? `${type}[]` : type;
|
||||
}
|
||||
|
||||
private portKindLabel(kind: 'input' | 'output', key: string): string {
|
||||
const ports = this.resolvePorts(kind);
|
||||
const port = ports.find((candidate) => candidate.name === key);
|
||||
return port ? this.portTypeLabel(port) : 'ANY';
|
||||
}
|
||||
|
||||
private resolvePorts(kind: 'input' | 'output'): FlowPort[] {
|
||||
const ports = this.data?.data?.[kind === 'input' ? 'inputs' : 'outputs'];
|
||||
return Array.isArray(ports) ? ports as FlowPort[] : [];
|
||||
}
|
||||
|
||||
private toMainContentParts(path: string, value: string): { text: string; isDynamicInput: boolean }[] {
|
||||
if (this.variablePlaceholderPaths.has(path)) {
|
||||
return splitTemplatedTextParts(value);
|
||||
|
|
@ -590,11 +629,16 @@ export class TaskStepNodeComponent {
|
|||
});
|
||||
}
|
||||
|
||||
private isPathVisible(path: string): boolean {
|
||||
private isPathVisible(path: string, visited = new Set<string>()): boolean {
|
||||
if (visited.has(path)) return true;
|
||||
visited.add(path);
|
||||
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
return ui.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath))
|
||||
);
|
||||
return ui.visibleWhen.every((rule) => {
|
||||
if (!rule) return true;
|
||||
if (!this.isPathVisible(rule.field, visited)) return false;
|
||||
return evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
});
|
||||
}
|
||||
|
||||
private groupLabelForPath(path: string): string | null {
|
||||
|
|
|
|||
|
|
@ -79,21 +79,62 @@
|
|||
<div class="text-[14px] font-extrabold leading-tight">{{ executionInput.title }}</div>
|
||||
<div class="mt-1 text-[11px] font-semibold uppercase tracking-[0.04em] text-blue-600">{{ executionInput.subtitle }}</div>
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-500 mb-2">Type: {{ executionInput.type }}</div>
|
||||
<div class="mb-2 text-[10px] text-slate-500">Type: {{ inputTypeLabel(executionInput) }}</div>
|
||||
|
||||
@if (isFileInput(executionInput)) {
|
||||
<input type="file" class="block w-full text-sm text-slate-700" [disabled]="readOnly()" (change)="onFileInputChange(executionInput, $event)" />
|
||||
<input
|
||||
type="file"
|
||||
class="block w-full text-sm text-slate-700"
|
||||
[attr.multiple]="isMultipleInput(executionInput) ? '' : null"
|
||||
[disabled]="readOnly()"
|
||||
(change)="onFileInputChange(executionInput, $event)" />
|
||||
} @else {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ executionInput.subtitle }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
rows="4"
|
||||
[readonly]="readOnly()"
|
||||
[ngModel]="executionInput.value"
|
||||
(ngModelChange)="onTextInputChange(executionInput, $event)">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
@if (isMultipleInput(executionInput)) {
|
||||
<div class="space-y-3">
|
||||
@for (textValue of textValues(executionInput); track $index) {
|
||||
<div class="flex items-start gap-2">
|
||||
<mat-form-field appearance="outline" class="flex-1">
|
||||
<mat-label>{{ executionInput.subtitle }} {{ $index + 1 }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
rows="3"
|
||||
[readonly]="readOnly()"
|
||||
[ngModel]="textValue"
|
||||
(ngModelChange)="updateTextItem(executionInput, $index, $event)">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
class="shrink-0"
|
||||
[disabled]="readOnly() || textValues(executionInput).length <= 1"
|
||||
(click)="removeTextItem(executionInput, $index)">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
[disabled]="readOnly()"
|
||||
(click)="addTextItem(executionInput)">
|
||||
Add item
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-form-field appearance="outline" class="w-full">
|
||||
<mat-label>{{ executionInput.subtitle }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
rows="4"
|
||||
[readonly]="readOnly()"
|
||||
[ngModel]="executionInput.value"
|
||||
(ngModelChange)="onTextInputChange(executionInput, $event)">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
}
|
||||
}
|
||||
|
||||
@if (isInputSaving(executionInput.key)) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ export type EditableExecutionInput = {
|
|||
title: string;
|
||||
subtitle: string;
|
||||
type: string;
|
||||
value: string;
|
||||
multiple: boolean;
|
||||
value: string | string[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
|
|
@ -31,8 +32,8 @@ export class TaskExecutionInputsPanelComponent {
|
|||
readonly savingErrors = input<Record<string, string>>({});
|
||||
readonly readOnly = input<boolean>(false);
|
||||
|
||||
readonly textInputChange = output<{ input: EditableExecutionInput; value: string }>();
|
||||
readonly fileInputChange = output<{ input: EditableExecutionInput; file: File }>();
|
||||
readonly textInputChange = output<{ input: EditableExecutionInput; value: string | string[] }>();
|
||||
readonly fileInputChange = output<{ input: EditableExecutionInput; files: File[] }>();
|
||||
readonly authorizationValueChange = output<{ requirement: TaskExecutionAuthorizationRequirement; value: string }>();
|
||||
readonly authorizationSubmit = output<TaskExecutionAuthorizationRequirement>();
|
||||
|
||||
|
|
@ -42,7 +43,18 @@ export class TaskExecutionInputsPanelComponent {
|
|||
return input.type.includes('FILE') || input.type.includes('BINARY');
|
||||
}
|
||||
|
||||
onTextInputChange(input: EditableExecutionInput, value: string) {
|
||||
isMultipleInput(input: EditableExecutionInput): boolean {
|
||||
return input.multiple;
|
||||
}
|
||||
|
||||
textValues(input: EditableExecutionInput): string[] {
|
||||
if (Array.isArray(input.value)) {
|
||||
return input.value;
|
||||
}
|
||||
return [input.value ?? ''];
|
||||
}
|
||||
|
||||
onTextInputChange(input: EditableExecutionInput, value: string | string[]) {
|
||||
if (this.readOnly()) return;
|
||||
this.textInputChange.emit({ input, value });
|
||||
}
|
||||
|
|
@ -50,9 +62,30 @@ export class TaskExecutionInputsPanelComponent {
|
|||
onFileInputChange(input: EditableExecutionInput, event: Event) {
|
||||
if (this.readOnly()) return;
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
const file = target?.files?.[0];
|
||||
if (!file) return;
|
||||
this.fileInputChange.emit({ input, file });
|
||||
const files = target?.files ? Array.from(target.files) : [];
|
||||
if (!files.length) return;
|
||||
this.fileInputChange.emit({ input, files });
|
||||
}
|
||||
|
||||
addTextItem(input: EditableExecutionInput) {
|
||||
const values = [...this.textValues(input), ''];
|
||||
this.onTextInputChange(input, values);
|
||||
}
|
||||
|
||||
updateTextItem(input: EditableExecutionInput, index: number, value: string) {
|
||||
const values = [...this.textValues(input)];
|
||||
values[index] = value;
|
||||
this.onTextInputChange(input, values);
|
||||
}
|
||||
|
||||
removeTextItem(input: EditableExecutionInput, index: number) {
|
||||
const values = [...this.textValues(input)];
|
||||
values.splice(index, 1);
|
||||
this.onTextInputChange(input, values);
|
||||
}
|
||||
|
||||
inputTypeLabel(input: EditableExecutionInput): string {
|
||||
return input.multiple ? `${input.type}[]` : input.type;
|
||||
}
|
||||
|
||||
isInputSaving(key: string): boolean {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@
|
|||
(authorizationValueChange)="onAuthorizationValueChange($event.requirement, $event.value)"
|
||||
(authorizationSubmit)="submitAuthorization($event)"
|
||||
(textInputChange)="onTextInputChange($event.input, $event.value)"
|
||||
(fileInputChange)="onFileInputChange($event.input, $event.file)">
|
||||
(fileInputChange)="onFileInputChange($event.input, $event.files)">
|
||||
</app-task-execution-inputs-panel>
|
||||
} @else {
|
||||
<div class="p-3 space-y-3">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Component, computed, effect, inject, input, OnDestroy, signal } from '@
|
|||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FlowBlockConnection, FlowData } from '@models/flow';
|
||||
import { areFlowValueKindsCompatible, FlowBlockConnection, FlowData, normalizeFlowPortValueKinds } from '@models/flow';
|
||||
import {
|
||||
getExecutionStatusGroup,
|
||||
TaskExecution,
|
||||
|
|
@ -44,7 +44,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
readonly startInProgress = signal(false);
|
||||
readonly savingInputs = signal<Record<string, boolean>>({});
|
||||
readonly savingErrors = signal<Record<string, string>>({});
|
||||
readonly pendingTextInputs = signal<Record<string, string>>({});
|
||||
readonly pendingTextInputs = signal<Record<string, string | string[]>>({});
|
||||
readonly pendingAuthorizationValues = signal<Record<string, string>>({});
|
||||
readonly savingAuthorizations = signal<Record<string, boolean>>({});
|
||||
readonly authorizationErrors = signal<Record<string, string>>({});
|
||||
|
|
@ -207,7 +207,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
? execution.context.inputs[key]
|
||||
: input.value;
|
||||
|
||||
if (!this.isInputSet(value)) return false;
|
||||
if (!this.isInputSet(value, Boolean(input.descriptor?.multiple))) return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +241,8 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
title: step.block.name,
|
||||
subtitle: inputName,
|
||||
type: String(input.descriptor?.type ?? 'TEXT').toUpperCase(),
|
||||
value: pendingValue ?? (rawValue == null ? '' : String(rawValue))
|
||||
multiple: Boolean(input.descriptor?.multiple),
|
||||
value: pendingValue ?? this.normalizeEditableInputValue(rawValue, Boolean(input.descriptor?.multiple))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -282,7 +283,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
onTextInputChange(input: EditableExecutionInput, value: string) {
|
||||
onTextInputChange(input: EditableExecutionInput, value: string | string[]) {
|
||||
if (this.inputsReadOnly()) return;
|
||||
const executionId = this.execution()?.id;
|
||||
if (!executionId) return;
|
||||
|
|
@ -298,13 +299,17 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
this.textInputDebounceTimers.set(timerKey, timer);
|
||||
}
|
||||
|
||||
onFileInputChange(input: EditableExecutionInput, file: File) {
|
||||
onFileInputChange(input: EditableExecutionInput, files: File[]) {
|
||||
if (this.inputsReadOnly()) return;
|
||||
const executionId = this.execution()?.id;
|
||||
if (!executionId) return;
|
||||
if (!executionId || !files.length) return;
|
||||
|
||||
this.setInputSaving(input.key, true);
|
||||
this.taskExecutionsService.prepareFileInput(executionId, input.nodeId, input.inputName, file).subscribe({
|
||||
const request$ = input.multiple
|
||||
? this.taskExecutionsService.prepareFileArrayInput(executionId, input.nodeId, input.inputName, files)
|
||||
: this.taskExecutionsService.prepareFileInput(executionId, input.nodeId, input.inputName, files[0]);
|
||||
|
||||
request$.subscribe({
|
||||
next: () => this.clearInputSaving(input.key),
|
||||
error: () => this.setInputError(input.key, 'Failed to upload file')
|
||||
});
|
||||
|
|
@ -400,9 +405,25 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
private sendPreparedTextInput(input: EditableExecutionInput, executionId: string) {
|
||||
if (this.inputsReadOnly() || this.execution()?.id !== executionId) return;
|
||||
|
||||
const value = this.pendingTextInputs()[input.key] ?? '';
|
||||
const value = this.pendingTextInputs()[input.key] ?? this.normalizeEditableInputValue(input.value, input.multiple);
|
||||
this.setInputSaving(input.key, true);
|
||||
this.taskExecutionsService.prepareStringInput(executionId, input.nodeId, input.inputName, value).subscribe({
|
||||
const request$ = input.multiple
|
||||
? this.taskExecutionsService.prepareStringArrayInput(
|
||||
executionId,
|
||||
input.nodeId,
|
||||
input.inputName,
|
||||
(Array.isArray(value) ? value : [String(value)])
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0)
|
||||
)
|
||||
: this.taskExecutionsService.prepareStringInput(
|
||||
executionId,
|
||||
input.nodeId,
|
||||
input.inputName,
|
||||
String(Array.isArray(value) ? value[0] ?? '' : value)
|
||||
);
|
||||
|
||||
request$.subscribe({
|
||||
next: () => {
|
||||
this.pendingTextInputs.update((current) => {
|
||||
const next = { ...current };
|
||||
|
|
@ -491,7 +512,13 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
.filter(({ step }) => step.id !== targetStep.id)
|
||||
.flatMap((sourceEntry) =>
|
||||
(sourceEntry.step.outputs ?? [])
|
||||
.filter((output) => output.connected && output.descriptor.type === input.descriptor.type)
|
||||
.filter((output) => output.connected)
|
||||
.filter((output) =>
|
||||
areFlowValueKindsCompatible(
|
||||
normalizeFlowPortValueKinds(output.descriptor),
|
||||
normalizeFlowPortValueKinds(input.descriptor)
|
||||
)
|
||||
)
|
||||
.map((output) => ({
|
||||
sourceStep: sourceEntry.step,
|
||||
sourceIndex: sourceEntry.index,
|
||||
|
|
@ -625,9 +652,38 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
return raw && raw.trim().length > 0 ? [raw] : [];
|
||||
}
|
||||
|
||||
private isInputSet(value: unknown): boolean {
|
||||
private isInputSet(value: unknown, multiple = false): boolean {
|
||||
if (multiple) {
|
||||
if (!Array.isArray(value)) return false;
|
||||
return value.some((item) => typeof item === 'string' ? item.trim().length > 0 : item != null);
|
||||
}
|
||||
if (value == null) return false;
|
||||
if (typeof value === 'string') return value.trim().length > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private normalizeEditableInputValue(value: unknown, multiple: boolean): string | string[] {
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => this.stringifyEditableInputItem(item));
|
||||
}
|
||||
if (value == null) {
|
||||
return [''];
|
||||
}
|
||||
return [this.stringifyEditableInputItem(value)];
|
||||
}
|
||||
|
||||
if (value == null) return '';
|
||||
return this.stringifyEditableInputItem(value);
|
||||
}
|
||||
|
||||
private stringifyEditableInputItem(value: unknown): string {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue