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']);
});
+
});