diff --git a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html
index 0b98586..768dbcb 100644
--- a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html
+++ b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html
@@ -71,20 +71,22 @@
No manual inputs required.
} @else {
- @if (globalExecutionInputs().length) {
-
Flow inputs
+
Inputs
- {{ providedGlobalCount() }} of {{ globalExecutionInputs().length }} provided
+ {{ providedCount() }} of {{ editableInputs().length }} provided
+
+ @if (globalExecutionInputs().length) {
+ Global inputs
@for (executionInput of globalExecutionInputs(); track executionInput.key) {
}
}
@if (nodeExecutionInputs().length) {
- Manual inputs
+ Node inputs
@for (executionInput of nodeExecutionInputs(); track executionInput.key) {
}
diff --git a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.spec.ts b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.spec.ts
index cb62b08..5694fa3 100644
--- a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.spec.ts
+++ b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.spec.ts
@@ -14,13 +14,13 @@ function makeInput(overrides: Partial = {}): EditableExe
type: 'TEXT',
multiple: false,
value: '',
+ provided: false,
...overrides
};
}
async function build(inputs: EditableExecutionInput[], options: {
pendingKeys?: string[];
- missing?: string[];
saving?: Record;
readOnly?: boolean;
} = {}) {
@@ -29,7 +29,6 @@ async function build(inputs: EditableExecutionInput[], options: {
const fixture = TestBed.createComponent(TaskExecutionInputsPanelComponent);
fixture.componentRef.setInput('editableInputs', inputs);
fixture.componentRef.setInput('pendingKeys', options.pendingKeys ?? []);
- fixture.componentRef.setInput('missingGlobalInputNames', options.missing ?? []);
fixture.componentRef.setInput('savingInputs', options.saving ?? {});
fixture.componentRef.setInput('readOnly', options.readOnly ?? false);
fixture.detectChanges();
@@ -41,30 +40,37 @@ async function build(inputs: EditableExecutionInput[], options: {
describe('TaskExecutionInputsPanelComponent', () => {
afterEach(() => TestBed.resetTestingModule());
- it('reports how many flow inputs are still missing', async () => {
- const fixture = await build(
- [makeInput({ key: 'g:a', inputName: 'a' }), makeInput({ key: 'g:b', inputName: 'b' })],
- { missing: ['b'] }
- );
+ it('counts every input, node ones included, since all of them are required', async () => {
+ // A step without its manual input never reaches READY, so a node input blocks the start just
+ // as a global one does and must be part of the tally.
+ const fixture = await build([
+ makeInput({ key: 'g:a', inputName: 'a', provided: true }),
+ makeInput({ key: 'g:b', inputName: 'b' }),
+ makeInput({ key: 'n:c', scope: 'node', inputName: 'c', title: 'Reviewer' })
+ ]);
- expect(fixture.componentInstance.providedGlobalCount()).toBe(1);
- expect(fixture.nativeElement.textContent).toContain('1 of 2 provided');
+ expect(fixture.componentInstance.providedCount()).toBe(1);
+ expect(fixture.nativeElement.textContent).toContain('1 of 3 provided');
});
- it('marks only the inputs the backend still considers unsatisfied', async () => {
- const provided = makeInput({ key: 'g:a', inputName: 'a' });
- const missing = makeInput({ key: 'g:b', inputName: 'b' });
- const fixture = await build([provided, missing], { missing: ['b'] });
+ it('marks exactly the inputs with nothing stored', async () => {
+ const provided = makeInput({ key: 'g:a', inputName: 'a', provided: true });
+ const missing = makeInput({ key: 'n:b', scope: 'node', inputName: 'b', title: 'Reviewer' });
+ const fixture = await build([provided, missing]);
expect(fixture.componentInstance.isMissing(provided)).toBe(false);
expect(fixture.componentInstance.isMissing(missing)).toBe(true);
});
- it('never marks a node input as missing, since only globals gate the start', async () => {
- const nodeInput = makeInput({ key: 'n:x', scope: 'node', inputName: 'x', title: 'Reviewer' });
- const fixture = await build([nodeInput], { missing: ['x'] });
+ it('names the two groups after the domain: global and node inputs', async () => {
+ const fixture = await build([
+ makeInput({ key: 'g:a', inputName: 'a' }),
+ makeInput({ key: 'n:b', scope: 'node', inputName: 'b', title: 'Reviewer' })
+ ]);
- expect(fixture.componentInstance.isMissing(nodeInput)).toBe(false);
+ const text = fixture.nativeElement.textContent;
+ expect(text).toContain('Global inputs');
+ expect(text).toContain('Node inputs');
});
it('offers a single save for every pending edit', async () => {
diff --git a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts
index 70c275f..18ba2c7 100644
--- a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts
+++ b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts
@@ -18,6 +18,11 @@ export type EditableExecutionInput = {
type: string;
multiple: boolean;
value: string | string[];
+ /**
+ * Whether a value is actually stored in the execution - unsaved edits do not count. Node inputs
+ * gate the start just as globals do: a step without its manual input never reaches READY.
+ */
+ provided: boolean;
};
@Component({
@@ -38,8 +43,7 @@ export class TaskExecutionInputsPanelComponent {
readonly readOnly = input(false);
/** Keys the user has edited but not saved; drives the single Save at the foot of the panel. */
readonly pendingKeys = input([]);
- /** Names the backend still considers unsatisfied - the ones actually blocking the start. */
- readonly missingGlobalInputNames = input([]);
+
readonly textInputChange = output<{ input: EditableExecutionInput; value: string | string[] }>();
readonly textInputSubmit = output();
@@ -53,7 +57,6 @@ export class TaskExecutionInputsPanelComponent {
private readonly authorizationVisibility = new Map();
private readonly pendingKeySet = computed(() => new Set(this.pendingKeys()));
- private readonly missingNameSet = computed(() => new Set(this.missingGlobalInputNames()));
readonly pendingCount = computed(() => this.editableInputs()
.filter((input) => this.pendingKeySet().has(input.key)).length);
@@ -63,16 +66,15 @@ export class TaskExecutionInputsPanelComponent {
readonly canSubmitAll = computed(() =>
!this.readOnly() && this.pendingCount() > 0 && !this.anySaving());
- /** Completion is reported for globals only: those are what gate the start. */
- readonly providedGlobalCount = computed(() => this.globalExecutionInputs()
- .filter((input) => !this.isMissing(input)).length);
+ /** Every manual input is required, node ones included, so the count covers the whole panel. */
+ readonly providedCount = computed(() => this.editableInputs().filter((input) => input.provided).length);
isPending(input: EditableExecutionInput): boolean {
return this.pendingKeySet().has(input.key);
}
isMissing(input: EditableExecutionInput): boolean {
- return input.scope === 'global' && this.missingNameSet().has(input.inputName);
+ return !input.provided;
}
submitAll(event?: Event) {
diff --git a/src/app/shared/task-execution-viewer/execution-viewer.utils.spec.ts b/src/app/shared/task-execution-viewer/execution-viewer.utils.spec.ts
index 4843098..b27600b 100644
--- a/src/app/shared/task-execution-viewer/execution-viewer.utils.spec.ts
+++ b/src/app/shared/task-execution-viewer/execution-viewer.utils.spec.ts
@@ -1,11 +1,5 @@
import { TaskExecution, TaskExecutionStep } from '@models/task-execution';
-import {
- buildAuthorizationGate,
- buildVisibleExecutionLogs,
- getExecutionInputValues,
- getExecutionOutputValues,
- isExecutionStartable
-} from './execution-viewer.utils';
+import { buildAuthorizationGate, buildVisibleExecutionLogs, getExecutionInputValues, getExecutionOutputValues, hasStoredValue, isExecutionStartable } from './execution-viewer.utils';
describe('execution viewer runtime values', () => {
const documentedStep: TaskExecutionStep = {
@@ -172,3 +166,25 @@ describe('authorization gate', () => {
expect(isExecutionStartable(running, buildAuthorizationGate(running, readyState))).toBe(false);
});
});
+
+describe('hasStoredValue', () => {
+ it('treats an empty or blank value as not supplied', () => {
+ expect(hasStoredValue(null)).toBe(false);
+ expect(hasStoredValue(undefined)).toBe(false);
+ expect(hasStoredValue('')).toBe(false);
+ expect(hasStoredValue(' ')).toBe(false);
+ });
+
+ it('treats an empty list, or a list of blanks, as not supplied', () => {
+ // The step would still be waiting for it, so the panel must not report it as provided.
+ expect(hasStoredValue([])).toBe(false);
+ expect(hasStoredValue(['', ' '])).toBe(false);
+ });
+
+ it('accepts any non-blank content', () => {
+ expect(hasStoredValue('Backend Developer')).toBe(true);
+ expect(hasStoredValue(['', 'one'])).toBe(true);
+ expect(hasStoredValue(0)).toBe(true);
+ expect(hasStoredValue(false)).toBe(true);
+ });
+});
diff --git a/src/app/shared/task-execution-viewer/execution-viewer.utils.ts b/src/app/shared/task-execution-viewer/execution-viewer.utils.ts
index 068f890..3b94c19 100644
--- a/src/app/shared/task-execution-viewer/execution-viewer.utils.ts
+++ b/src/app/shared/task-execution-viewer/execution-viewer.utils.ts
@@ -535,3 +535,15 @@ export function isExecutionStartable(
return true;
}
+
+/**
+ * Whether a value counts as supplied. An empty string, an empty list, or a list of blanks is not:
+ * the step would still be waiting for it.
+ */
+export function hasStoredValue(value: unknown): boolean {
+ if (value === null || value === undefined) return false;
+ if (Array.isArray(value)) {
+ return value.some((item) => String(item ?? '').trim().length > 0);
+ }
+ return String(value).trim().length > 0;
+}
diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.html b/src/app/shared/task-execution-viewer/task-execution-viewer.html
index 1744061..3b58e3d 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.html
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html
@@ -400,7 +400,6 @@
[savingErrors]="savingErrors()"
[readOnly]="inputsReadOnly()"
[pendingKeys]="pendingInputKeys()"
- [missingGlobalInputNames]="missingGlobalInputNames()"
(authorizationValueChange)="onAuthorizationValueChange($event.requirement, $event.value)"
(authorizationSubmit)="submitAuthorization($event)"
(textInputChange)="onTextInputChange($event.input, $event.value)"
diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.ts b/src/app/shared/task-execution-viewer/task-execution-viewer.ts
index f1af8bd..da55f60 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts
@@ -83,6 +83,7 @@ import {
getConnectedInputs,
getConnectedOutputs,
getExecutionErrors,
+ hasStoredValue,
getExecutionWarnings,
AuthorizationGate,
VaultAuthorizationEntry,
@@ -141,8 +142,6 @@ export class TaskExecutionViewerComponent implements OnDestroy {
/** Edited but not yet sent, so the panel can offer one Save for the lot. */
readonly pendingInputKeys = computed(() => Object.keys(this.pendingTextInputs()));
- /** The backend's own answer on what still blocks the start, by input name. */
- readonly missingGlobalInputNames = computed(() => this.execution()?.missingGlobalInputKeys ?? []);
readonly pendingAuthorizationValues = signal>({});
readonly savingAuthorizations = signal>({});
readonly authorizationErrors = signal>({});
@@ -699,6 +698,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
});
readonly editableInputs = computed(() => {
+ const missingGlobalInputNames = new Set(this.execution()?.missingGlobalInputKeys ?? []);
const execution = this.execution();
if (!execution) return [];
@@ -726,6 +726,9 @@ export class TaskExecutionViewerComponent implements OnDestroy {
subtitle: inputName,
type: String(descriptor?.kind ?? 'TEXT').toUpperCase(),
multiple: Boolean(descriptor?.multiple),
+ // The backend's own answer, and it reports what is stored - so an unsaved edit does not
+ // make an input look satisfied.
+ provided: !missingGlobalInputNames.has(inputName),
value: pendingValue ?? normalizeEditableInputValue(rawValue, Boolean(descriptor?.multiple))
});
}
@@ -752,6 +755,8 @@ export class TaskExecutionViewerComponent implements OnDestroy {
subtitle: inputName,
type: String(input.descriptor?.type ?? 'TEXT').toUpperCase(),
multiple: Boolean(input.descriptor?.multiple),
+ // No per-input flag from the backend here, so judge the stored value, ignoring pending.
+ provided: hasStoredValue(rawValue),
value: pendingValue ?? normalizeEditableInputValue(rawValue, Boolean(input.descriptor?.multiple))
});
}