Say that a file is stored where it is chosen, not on Save

Replacing a file uploads it straight away, while every other input waits
for the Save at the foot of the panel. Nothing said so, so a replacement
left the footer reading "All changes saved" with Save greyed out - which
is exactly what an edit that failed to register looks like.

The row now says it, and a test covers the path it describes: Replace
reopens the picker and the chosen file uploads on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-14 14:51:41 +02:00
parent b4302bf7db
commit f1305c16e7
3 changed files with 53 additions and 0 deletions

View File

@ -398,6 +398,13 @@
word-break: break-all;
}
.inputs-panel-upload-note {
margin-top: 4px;
color: #94a3b8;
font-size: 10.5px;
line-height: 1.4;
}
.inputs-panel-items {
display: flex;
flex-direction: column;

View File

@ -277,6 +277,13 @@
{{ uploadError }}
</span>
</div>
} @else if (executionInput.provided && !isInputSaving(executionInput.key)) {
<!--
A file is stored the moment it is chosen, while every other input waits for the Save below.
Without saying so, replacing one left the footer reading "All changes saved" with Save greyed -
which is what an edit that did not register would look like.
-->
<div class="inputs-panel-upload-note">Stored as soon as it is chosen; the Save below is for the other inputs.</div>
}
} @else if (isMultipleInput(executionInput)) {
@if (itemsOpen(executionInput)) {

View File

@ -168,6 +168,44 @@ describe('TaskExecutionInputsPanelComponent', () => {
expect(text).toContain('Node inputs');
});
it('uploads again when an already-provided file is replaced', async () => {
const fixture = await build([makeInput({
key: 'global:document',
inputName: 'document',
type: 'FILE',
multiple: false,
value: '/tmp/upload-1/plan-01.pdf',
provided: true
})]);
// Nothing is missing, so the group starts closed: open it to reach the row.
fixture.componentInstance.toggleGlobals();
fixture.detectChanges();
// A provided file shows what it holds, with Replace rather than a picker to hunt for.
expect(fixture.nativeElement.textContent).toContain('plan-01.pdf');
const replace = fixture.nativeElement.querySelector('.inputs-panel-upload-replace') as HTMLButtonElement;
expect(replace).toBeTruthy();
const picker = fixture.nativeElement.querySelector('input[type="file"]') as HTMLInputElement;
const clicked = vi.fn();
picker.addEventListener('click', clicked);
replace.click();
expect(clicked).toHaveBeenCalled();
const uploads = vi.fn();
fixture.componentInstance.fileInputChange.subscribe(uploads);
const replacement = new File(['%PDF-1.7'], 'plan-02.pdf', { type: 'application/pdf' });
Object.defineProperty(picker, 'files', { value: [replacement], configurable: true });
picker.dispatchEvent(new Event('change'));
// The replacement uploads on its own: a file is stored where it is chosen, not on a later save.
expect(uploads).toHaveBeenCalledTimes(1);
expect(uploads.mock.calls[0][0].files[0].name).toBe('plan-02.pdf');
// And the row says so, since the footer's Save stays greyed and would otherwise read as an
// edit that did not register.
expect(fixture.nativeElement.textContent).toContain('Stored as soon as it is chosen');
});
it('offers a single save for every pending edit', async () => {
const fixture = await build(
[makeInput({ key: 'g:a', inputName: 'a' }), makeInput({ key: 'g:b', inputName: 'b' })],
@ -519,4 +557,5 @@ describe('parseJsonArrayInput', () => {
it('converts plain numbers and booleans to text', () => {
expect(parseJsonArrayInput('[1, true]').values).toEqual(['1', 'true']);
});
});