From f1305c16e70ed275ee9e1853e2fd2b7c1cc0e5c3 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Mon, 14 Sep 2026 14:51:41 +0200 Subject: [PATCH] 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) --- .../task-execution-inputs-panel.css | 7 ++++ .../task-execution-inputs-panel.html | 7 ++++ .../task-execution-inputs-panel.spec.ts | 39 +++++++++++++++++++ 3 files changed, 53 insertions(+) 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 88e510f..ac0643a 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 @@ -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; 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 c6c167b..8b04b08 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 @@ -277,6 +277,13 @@ {{ uploadError }} + } @else if (executionInput.provided && !isInputSaving(executionInput.key)) { + +
Stored as soon as it is chosen; the Save below is for the other inputs.
} } @else if (isMultipleInput(executionInput)) { @if (itemsOpen(executionInput)) { 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 22339f1..5039e6b 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 @@ -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']); }); + });