Put the failed-save message in the sticky bar, beside Save

At the top of the panel the message could be scrolled out of sight by the very
fields it was about, while the button that produced it sits at the foot. It now
shares the sticky bar with that button, so it cannot scroll away.

The bar stacks: the message takes a full-width line that wraps, above the row
with the status and the button, so Save stays exactly where it was and the text
is not clipped to one line in a 320px aside.

The pending count stays alongside it. The edits are still there and the save is
retryable, which is worth showing rather than replacing with the failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-03 16:14:29 +02:00
parent 96c3679f2b
commit ef1c2081ec
3 changed files with 44 additions and 24 deletions

View File

@ -289,15 +289,21 @@
position: sticky;
bottom: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-direction: column;
gap: 6px;
margin: 4px -12px -12px;
padding: 8px 12px;
border-top: 1px solid #e2e8f0;
background: #ffffff;
}
.inputs-panel-savebar-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.inputs-panel-savebar-status {
min-width: 0;
overflow: hidden;
@ -420,17 +426,21 @@
color: #64748b;
}
/* One message for the whole panel, kept above the fields it concerns. */
/*
* One message for the whole panel, in the sticky bar beside the button that triggered it: at the
* top of the panel it could be scrolled out of sight by the very fields it was about.
*/
.inputs-panel-alert {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 7px 9px;
padding: 6px 8px;
border: 1px solid #fecaca;
border-radius: 6px;
background: #fef2f2;
color: #b91c1c;
font-size: 12px;
font-size: 11.5px;
line-height: 1.35;
}
.inputs-panel-alert .mat-icon {

View File

@ -71,13 +71,6 @@
<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">
@ -113,16 +106,24 @@
@if (editableInputs().length && !readOnly()) {
<div class="inputs-panel-savebar">
<span class="inputs-panel-savebar-status">
@if (pendingCount()) {
{{ pendingCount() }} unsaved {{ pendingCount() === 1 ? 'change' : 'changes' }}
} @else {
All changes saved
}
</span>
<button type="button" mat-flat-button [disabled]="!canSubmitAll()" (click)="submitAll($event)">
{{ anySaving() ? 'Saving…' : 'Save' }}
</button>
@if (saveError(); as error) {
<div class="inputs-panel-alert" role="alert">
<mat-icon fontIcon="error_outline"></mat-icon>
<span>{{ error }}</span>
</div>
}
<div class="inputs-panel-savebar-row">
<span class="inputs-panel-savebar-status">
@if (pendingCount()) {
{{ pendingCount() }} unsaved {{ pendingCount() === 1 ? 'change' : 'changes' }}
} @else {
All changes saved
}
</span>
<button type="button" mat-flat-button [disabled]="!canSubmitAll()" (click)="submitAll($event)">
{{ anySaving() ? 'Saving…' : 'Save' }}
</button>
</div>
</div>
}
}

View File

@ -269,11 +269,20 @@ describe('TaskExecutionInputsPanelComponent', () => {
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.' });
], {
pendingKeys: ['g:a', 'g:b', 'g: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');
// In the sticky bar, next to the button that triggered it: at the top of a long panel it could
// be scrolled out of sight by the very fields it was about.
expect(fixture.nativeElement.querySelector('.inputs-panel-savebar .inputs-panel-alert')).not.toBeNull();
// The edits are still pending, so the bar still counts them: the save is retryable.
expect(fixture.nativeElement.textContent).toContain('3 unsaved changes');
expect(fixture.componentInstance.canSubmitAll()).toBe(true);
});
it('shows no message when nothing has failed', async () => {