Report a failed global save once, at the top of the panel
The globals go in a single request, so a failure belongs to the batch and not to any field in it. Repeating the same text on every input said one thing N times and implied N separate problems. One message now sits above the fields, and it can say what the per-field version could not: the request was all-or-nothing, so none of the values were saved. It clears on the next edit, so a stale notice cannot outlive the state it described. Node inputs keep their per-input errors: each is its own request, so a failure there is attributable and partial success is real. File uploads likewise. 489 frontend tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fee24f174b
commit
96c3679f2b
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,13 @@
|
|||
<div class="text-xs text-slate-500">No manual inputs required.</div>
|
||||
} @else {
|
||||
|
||||
@if (saveError(); as error) {
|
||||
<div class="inputs-panel-alert" role="alert">
|
||||
<mat-icon fontIcon="error_outline"></mat-icon>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-600">Inputs</div>
|
||||
<div class="text-[11px] text-slate-500">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ async function build(inputs: EditableExecutionInput[], options: {
|
|||
pendingKeys?: string[];
|
||||
saving?: Record<string, boolean>;
|
||||
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' }),
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@ export class TaskExecutionInputsPanelComponent {
|
|||
readonly readOnly = input<boolean>(false);
|
||||
/** Keys the user has edited but not saved; drives the single Save at the foot of the panel. */
|
||||
readonly pendingKeys = input<string[]>([]);
|
||||
/**
|
||||
* 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<string | null>(null);
|
||||
|
||||
|
||||
readonly textInputChange = output<{ input: EditableExecutionInput; value: string | string[] }>();
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
|
||||
readonly pendingAuthorizationValues = signal<Record<string, string>>({});
|
||||
readonly savingAuthorizations = signal<Record<string, boolean>>({});
|
||||
|
|
@ -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<string, string | string[]>,
|
||||
executionId: string
|
||||
): Observable<unknown> {
|
||||
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);
|
||||
})
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue