diff --git a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.css b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.css
index 67f3c9e..ea69858 100644
--- a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.css
+++ b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.css
@@ -419,3 +419,24 @@
background: #f8fafc;
color: #64748b;
}
+
+/* One message for the whole panel, kept above the fields it concerns. */
+.inputs-panel-alert {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ padding: 7px 9px;
+ border: 1px solid #fecaca;
+ border-radius: 6px;
+ background: #fef2f2;
+ color: #b91c1c;
+ font-size: 12px;
+}
+
+.inputs-panel-alert .mat-icon {
+ flex: 0 0 auto;
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ line-height: 16px;
+}
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 27d849e..9fa4af7 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,6 +71,13 @@
No manual inputs required.
} @else {
+ @if (saveError(); as error) {
+
Inputs
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 2f289a7..1336470 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
@@ -27,6 +27,7 @@ async function build(inputs: EditableExecutionInput[], options: {
pendingKeys?: string[];
saving?: Record;
readOnly?: boolean;
+ saveError?: string | null;
} = {}) {
await TestBed.configureTestingModule({ imports: [TaskExecutionInputsPanelComponent] }).compileComponents();
@@ -35,6 +36,7 @@ async function build(inputs: EditableExecutionInput[], options: {
fixture.componentRef.setInput('pendingKeys', options.pendingKeys ?? []);
fixture.componentRef.setInput('savingInputs', options.saving ?? {});
fixture.componentRef.setInput('readOnly', options.readOnly ?? false);
+ fixture.componentRef.setInput('saveError', options.saveError ?? null);
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
@@ -260,6 +262,26 @@ describe('TaskExecutionInputsPanelComponent', () => {
expect(changed).not.toHaveBeenCalled();
});
+ it('shows one message for a save that failed as a whole, not one per field', async () => {
+ // The globals travel in a single request, so a failure belongs to the batch. The same text
+ // repeated on three fields read as three separate problems.
+ const fixture = await build([
+ makeInput({ key: 'g:a', inputName: 'a' }),
+ makeInput({ key: 'g:b', inputName: 'b' }),
+ makeInput({ key: 'g:c', inputName: 'c' })
+ ], { saveError: 'Could not save the global inputs, so none of them were saved.' });
+
+ const alerts = fixture.nativeElement.querySelectorAll('.inputs-panel-alert');
+ expect(alerts.length).toBe(1);
+ expect(alerts[0].textContent).toContain('none of them were saved');
+ });
+
+ it('shows no message when nothing has failed', async () => {
+ const fixture = await build([makeInput()]);
+
+ expect(fixture.nativeElement.querySelector('.inputs-panel-alert')).toBeNull();
+ });
+
it('offers the JSON import only on a multi-value input', async () => {
const fixture = await build([
makeInput({ key: 'g:single', inputName: 'positionTitle' }),
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 4544d77..e1311f1 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
@@ -91,6 +91,12 @@ 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([]);
+ /**
+ * One message for a save that failed as a whole. The globals go in a single request, so a failure
+ * is not attributable to any one of them - repeating it on each field said the same thing N times
+ * and implied N separate problems.
+ */
+ readonly saveError = input(null);
readonly textInputChange = output<{ input: EditableExecutionInput; value: string | string[] }>();
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 3b58e3d..1484aa0 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.html
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html
@@ -400,6 +400,7 @@
[savingErrors]="savingErrors()"
[readOnly]="inputsReadOnly()"
[pendingKeys]="pendingInputKeys()"
+ [saveError]="globalSaveError()"
(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 1f4d75d..36e6637 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts
@@ -143,6 +143,8 @@ 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()));
+ /** Set when the single request carrying every edited global fails; cleared on the next edit. */
+ readonly globalSaveError = signal(null);
readonly pendingAuthorizationValues = signal>({});
readonly savingAuthorizations = signal>({});
@@ -1113,6 +1115,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
onTextInputChange(input: EditableExecutionInput, value: string | string[]) {
if (this.inputsReadOnly()) return;
+ this.globalSaveError.set(null);
this.pendingTextInputs.update((current) => ({ ...current, [input.key]: value }));
this.savingErrors.update((current) => {
const next = { ...current };
@@ -1283,6 +1286,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
values: Record,
executionId: string
): Observable {
+ this.globalSaveError.set(null);
globals.forEach((input) => this.setInputSaving(input.key, true));
return this.taskExecutionsService.prepareGlobalInputs(executionId, values).pipe(
@@ -1290,9 +1294,11 @@ export class TaskExecutionViewerComponent implements OnDestroy {
this.clearPendingInput(input.key);
this.clearInputSaving(input.key);
})),
- // The batch failed as a batch, so say so on each input in it rather than guessing a culprit.
+ // One request, so either every global was saved or none was: one message says that, where
+ // the same text on each field implied as many separate problems as there were inputs.
catchError(() => {
- globals.forEach((input) => this.setInputError(input.key, 'Failed to update inputs'));
+ globals.forEach((input) => this.clearInputSaving(input.key));
+ this.globalSaveError.set('Could not save the global inputs, so none of them were saved.');
return of(null);
})
);