Fix upload UI stuck on "Processing files..."
The `uploading` flag was only cleared by `x-init="uploading = false"` on the file list, which Alpine runs when the element is initialized. Adding a second batch of files to the same share only patches that existing element, so the flag stayed true forever: the spinner never went away, the drop zone stayed disabled and the submit button stayed disabled, making the share impossible to create. `updatedFiles()` now dispatches `files-processed`, which runs for every batch, and the form resets its upload state on that event instead. Also fixes two adjacent bugs in the drag & drop path: `uploadMultiple()` was called without callbacks so dropped files showed no progress at all, and `relativePaths` was replaced instead of appended, shifting every earlier file's path onto the wrong file on a second drop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
084cefe023
commit
d2e8a135c5
@@ -65,8 +65,17 @@ class FileUploader extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a freshly uploaded batch of files.
|
||||
*
|
||||
* Dispatches `files-processed` so the front end can drop its "uploading" state.
|
||||
* This runs for every batch, including additional files added to an existing
|
||||
* selection, which a one-off `x-init` on the file list cannot cover.
|
||||
*/
|
||||
public function updatedFiles(): void
|
||||
{
|
||||
$this->dispatch('files-processed')->self();
|
||||
|
||||
$maxFileSize = (int) Setting::get('max_file_size', 100 * 1024 * 1024);
|
||||
$maxFileSizeMb = $maxFileSize / (1024 * 1024);
|
||||
$maxFilesPerShare = (int) Setting::get('max_files_per_share', 50);
|
||||
|
||||
@@ -38,16 +38,36 @@
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (! files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = new DataTransfer();
|
||||
const paths = [];
|
||||
files.forEach(f => {
|
||||
dt.items.add(f.file);
|
||||
paths.push(f.path);
|
||||
});
|
||||
$wire.relativePaths = paths;
|
||||
$wire.uploadMultiple('files', dt.files);
|
||||
|
||||
$wire.relativePaths = [...($wire.relativePaths ?? []), ...paths];
|
||||
|
||||
this.uploading = true;
|
||||
this.progress = 0;
|
||||
|
||||
$wire.uploadMultiple(
|
||||
'files',
|
||||
dt.files,
|
||||
() => this.progress = 100,
|
||||
() => this.resetUpload(),
|
||||
(event) => this.progress = event.detail.progress,
|
||||
() => this.resetUpload(),
|
||||
);
|
||||
}, 500);
|
||||
},
|
||||
resetUpload() {
|
||||
this.uploading = false;
|
||||
this.progress = 0;
|
||||
},
|
||||
traverseEntry(entry, path, files) {
|
||||
if (entry.isFile) {
|
||||
entry.file(file => {
|
||||
@@ -61,10 +81,11 @@
|
||||
}
|
||||
}
|
||||
}"
|
||||
x-init="$wire.$on('files-processed', () => resetUpload())"
|
||||
x-on:livewire-upload-start="uploading = true; progress = 0"
|
||||
x-on:livewire-upload-finish="progress = 100"
|
||||
x-on:livewire-upload-cancel="uploading = false"
|
||||
x-on:livewire-upload-error="uploading = false"
|
||||
x-on:livewire-upload-cancel="resetUpload()"
|
||||
x-on:livewire-upload-error="resetUpload()"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
||||
>
|
||||
{{-- Drop Zone --}}
|
||||
@@ -118,12 +139,12 @@
|
||||
|
||||
{{-- Errors --}}
|
||||
@error('files')
|
||||
<div x-init="uploading = false" class="alert alert-error mb-4">{{ $message }}</div>
|
||||
<div class="alert alert-error mb-4">{{ $message }}</div>
|
||||
@enderror
|
||||
|
||||
{{-- File List --}}
|
||||
@if (count($files))
|
||||
<div x-init="uploading = false" class="mb-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="font-semibold mb-2">{{ __('Selected Files') }} ({{ count($files) }})</h3>
|
||||
<div class="space-y-1 max-h-60 overflow-y-auto">
|
||||
@foreach ($files as $index => $file)
|
||||
|
||||
@@ -94,6 +94,31 @@ test('file upload with max downloads sets limit', function () {
|
||||
expect($share->max_downloads)->toBe(5);
|
||||
});
|
||||
|
||||
test('every upload batch dispatches files-processed to clear the uploading state', function () {
|
||||
Storage::fake('shares');
|
||||
|
||||
Livewire::test(FileUploader::class)
|
||||
->set('files', [UploadedFile::fake()->create('first.txt', 64)])
|
||||
->assertDispatched('files-processed')
|
||||
->set('files', [UploadedFile::fake()->create('second.txt', 64)])
|
||||
->assertDispatched('files-processed');
|
||||
});
|
||||
|
||||
test('files added in multiple batches end up in the same share', function () {
|
||||
Storage::fake('shares');
|
||||
|
||||
Livewire::test(FileUploader::class)
|
||||
->set('files', [UploadedFile::fake()->create('first.txt', 64)])
|
||||
->set('files', [UploadedFile::fake()->create('second.txt', 64)])
|
||||
->call('createShare')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirectContains('/share/');
|
||||
|
||||
$share = Share::query()->first();
|
||||
|
||||
expect($share->files->pluck('original_name')->all())->toBe(['first.txt', 'second.txt']);
|
||||
});
|
||||
|
||||
test('file upload requires at least one file', function () {
|
||||
Livewire::test(FileUploader::class)
|
||||
->set('files', [])
|
||||
|
||||
Reference in New Issue
Block a user