Mark the file the folder download is on

During "Download all to a folder" the recipient saw only the file name
under the progress bar. The file's row in the list now shows the loading
indicator while it is on its way, and the list scrolls that row into
view, so a run through hundreds of files can be followed in the list.
The row is scrolled rather than the indicator, which is still hidden
when the run moves on to the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-10-08 23:28:23 +02:00
co-authored by Claude Opus 5.5
parent 6578335568
commit 30a485f5d9
3 changed files with 52 additions and 2 deletions
+5 -1
View File
@@ -68,9 +68,11 @@ document.addEventListener('alpine:init', () => {
/** How many of the run's files are in the folder. */
saved: 0,
/** The file on its way. */
/** The file on its way: its path for the status line, its id for its row in the list. */
current: null,
currentId: null,
/** The files that failed, kept for the Retry button. */
failed: [],
@@ -140,6 +142,7 @@ document.addEventListener('alpine:init', () => {
for (const file of list) {
const before = this.received
this.current = file.path
this.currentId = file.id
try {
await this.save(file, controller.signal)
@@ -162,6 +165,7 @@ document.addEventListener('alpine:init', () => {
this.running = false
this.current = null
this.currentId = null
this.finished = ! controller.signal.aborted
},
@@ -105,7 +105,16 @@
<x-slot:description><span class="md-tabular">{{ Number::fileSize($file->file_size) }}</span></x-slot:description>
<x-slot:end>
<x-row gap="space100">
<span x-show="isDone({{ $file->id }})" x-cloak data-test="file-downloaded">
{{-- The file the folder download is on; its row, always shown, is brought into view in the list as it starts. --}}
<span
x-show="currentId === {{ $file->id }}"
x-effect="if (currentId === {{ $file->id }}) $el.closest('[data-md-list-item]').scrollIntoView({ block: 'nearest' })"
x-cloak
data-test="file-downloading"
>
<x-loading size="24" :label="__('Downloading')" />
</span>
<span x-show="isDone({{ $file->id }}) && currentId !== {{ $file->id }}" x-cloak data-test="file-downloaded">
<x-icon name="download_done" class="md-ink-success" :label="__('Downloaded')" />
</span>
<x-button icon="download" :link="route('share.download.file', [$share, $file])" no-wire-navigate :aria-label="__('Download :name', ['name' => $file->original_name])" x-on:click="downloaded = true; markDone({{ $file->id }})" />
+37
View File
@@ -313,3 +313,40 @@ test('a long file list scrolls on its own and keeps the download buttons on the
->assertScript("[...document.querySelectorAll('[data-md-button]')].filter((button) => button.offsetParent !== null && button.textContent.trim().toLowerCase().startsWith('download all')).every((button) => button.getBoundingClientRect().bottom <= window.innerHeight)")
->assertNoJavaScriptErrors();
});
test('the folder download marks the file it is on and scrolls the list to it', function () {
$share = app(ShareService::class)->createShare(collect(range(1, 40))->map(fn (int $number): array => [
'file' => UploadedFile::fake()->createWithContent("file-{$number}.txt", "file {$number}"),
'relativePath' => null,
])->all());
$last = $share->files->last();
$page = ready(visit(route('share.download', $share, false))->resize(1280, 800));
// Every file but the last is already in the folder, and the last one's request never answers,
// so the run stays on it.
$page->script("(async () => {
const root = await navigator.storage.getDirectory();
for await (const name of root.keys()) { await root.removeEntry(name, { recursive: true }); }
for (let number = 1; number < 40; number++) {
const writable = await (await root.getFileHandle(`file-\${number}.txt`, { create: true })).createWritable();
await writable.write(`file \${number}`);
await writable.close();
}
window.showDirectoryPicker = () => Promise.resolve(root);
window.fetch = () => new Promise(() => {});
Alpine.\$data(document.querySelector('[data-test=download-to-folder]')).supported = true;
})()");
$page->click('[data-test="download-to-folder"]');
$page->assertScript("[...document.querySelectorAll('[data-test=file-downloading]')].filter((mark) => mark.offsetParent !== null).length === 1")
->assertScript("(() => {
const mark = [...document.querySelectorAll('[data-test=file-downloading]')].find((mark) => mark.offsetParent !== null);
const row = mark.closest('[data-md-list-item]');
const list = document.querySelector('[data-test=share-file-list]').getBoundingClientRect();
const rect = row.getBoundingClientRect();
return row.textContent.includes('{$last->original_name}') && rect.top >= list.top - 1 && rect.bottom <= list.bottom + 1;
})()")
->assertNoJavaScriptErrors();
});