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()
{