From 36fb0817a4d4c911221f09710bfc4650384aeb86 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Fri, 2 Oct 2026 14:11:41 +0200 Subject: [PATCH] Transfers: say so when a drop or a download is refused because the screen is busy --- .../ViewModels/TransfersViewModel.cs | 31 +++++++++++++++---- .../TransferQueueingTests.cs | 16 ++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs index ad7058f..80047a9 100644 --- a/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs +++ b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs @@ -2289,6 +2289,10 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo // ---- Moving files ---- + /// What a transfer asked for while another command is still running is told. + private const string StillBusy = + "Still working on the last request. Nothing was queued — try again when it has finished."; + /// Queues the chosen remote file for download into the local directory showing. [RelayCommand] private void Download() @@ -2306,7 +2310,8 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo await QueueDownloadsAsync(rows, CancellationToken.None).ConfigureAwait(true); ClearRemoteChecks(); - }); + }, + StillBusy); } /// Queues the chosen local file for upload into the remote directory showing. @@ -2328,7 +2333,8 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo await QueueUploadsAsync(paths, CancellationToken.None).ConfigureAwait(true); ClearLocalChecks(); - }); + }, + StillBusy); } /// Unticks everything on the host's side. @@ -2393,13 +2399,15 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo internal void QueueUploads(IReadOnlyList paths) => _ = RunAsync( "Working out what to upload…", - () => QueueUploadsAsync(paths, CancellationToken.None)); + () => QueueUploadsAsync(paths, CancellationToken.None), + StillBusy); /// internal void QueueDownloads(IReadOnlyList rows) => _ = RunAsync( "Working out what to download…", - () => QueueDownloadsAsync(rows, CancellationToken.None)); + () => QueueDownloadsAsync(rows, CancellationToken.None), + StillBusy); internal async Task QueueUploadsAsync(IReadOnlyList paths, CancellationToken cancellationToken) { @@ -2580,7 +2588,8 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo _ = RunAsync( "Working out what to upload…", - () => QueueUploadsAsync(paths, CancellationToken.None)); + () => QueueUploadsAsync(paths, CancellationToken.None), + StillBusy); } /// @@ -4154,11 +4163,21 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo /// The same funnel VaultViewModel uses, and here for the same reason: every command on this screen /// can fail with a path the server refused, and one that forgot to clear the busy flag would leave the /// pane permanently disabled. + /// + /// is for the commands somebody can fire while another is running — + /// a drop, or the download and upload buttons pressed twice — which used to vanish without a word: the + /// files were not queued and nothing said so. A command given one says why it did nothing instead. + /// /// - private async Task RunAsync(string busyMessage, Func work) + private async Task RunAsync(string busyMessage, Func work, string? refusedWhileBusy = null) { if (IsBusy) { + if (refusedWhileBusy is not null) + { + Status = refusedWhileBusy; + } + return; } diff --git a/tests/DodoSSH.Client.App.Tests/TransferQueueingTests.cs b/tests/DodoSSH.Client.App.Tests/TransferQueueingTests.cs index 3417f9b..6d3dd4c 100644 --- a/tests/DodoSSH.Client.App.Tests/TransferQueueingTests.cs +++ b/tests/DodoSSH.Client.App.Tests/TransferQueueingTests.cs @@ -79,6 +79,22 @@ public sealed class TransferQueueingTests : IDisposable transfers.Status.ShouldContain("Connect to a host first"); } + /// + /// A drop landing while another command on this screen was running used to vanish: nothing queued and + /// nothing said. It still queues nothing — the command in flight owns the pane — but it says so. + /// + [Fact] + public void DroppingFilesWhileBusy_QueuesNothingAndSaysWhy() + { + Connected(); + transfers.IsBusy = true; + + transfers.QueueUploads([File("one.txt")]); + + Queued().ShouldBeEmpty(); + transfers.Status.ShouldContain("Nothing was queued"); + } + [Fact] public void DroppingSeveralFiles_QueuesEachOfThem() {