diff --git a/SharepointToolbox/App.xaml b/SharepointToolbox/App.xaml index a78f1da..6e556b6 100644 --- a/SharepointToolbox/App.xaml +++ b/SharepointToolbox/App.xaml @@ -5,9 +5,12 @@ xmlns:conv="clr-namespace:SharepointToolbox.Views.Converters"> - - - + + + + + + diff --git a/SharepointToolbox/App.xaml.cs b/SharepointToolbox/App.xaml.cs index b49c8ff..bbaf1c4 100644 --- a/SharepointToolbox/App.xaml.cs +++ b/SharepointToolbox/App.xaml.cs @@ -119,6 +119,12 @@ public partial class App : Application services.AddTransient>(sp => profile => new SitePickerDialog(sp.GetRequiredService(), profile)); + // Phase 4: File Transfer + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); } } diff --git a/SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs b/SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs new file mode 100644 index 0000000..a614bc7 --- /dev/null +++ b/SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs @@ -0,0 +1,165 @@ +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; +using Serilog; +using SharepointToolbox.Core.Models; +using SharepointToolbox.Services; +using SharepointToolbox.Services.Export; + +namespace SharepointToolbox.ViewModels.Tabs; + +public partial class TransferViewModel : FeatureViewModelBase +{ + private readonly IFileTransferService _transferService; + private readonly ISessionManager _sessionManager; + private readonly BulkResultCsvExportService _exportService; + private readonly ILogger _logger; + private TenantProfile? _currentProfile; + + // Source selection + [ObservableProperty] private string _sourceSiteUrl = string.Empty; + [ObservableProperty] private string _sourceLibrary = string.Empty; + [ObservableProperty] private string _sourceFolderPath = string.Empty; + + // Destination selection + [ObservableProperty] private string _destSiteUrl = string.Empty; + [ObservableProperty] private string _destLibrary = string.Empty; + [ObservableProperty] private string _destFolderPath = string.Empty; + + // Transfer options + [ObservableProperty] private TransferMode _transferMode = TransferMode.Copy; + [ObservableProperty] private ConflictPolicy _conflictPolicy = ConflictPolicy.Skip; + + // Results + [ObservableProperty] private string _resultSummary = string.Empty; + [ObservableProperty] private bool _hasFailures; + + private BulkOperationSummary? _lastResult; + + public IAsyncRelayCommand ExportFailedCommand { get; } + + // Expose current profile for View code-behind (site picker, folder browser) + public TenantProfile? CurrentProfile => _currentProfile; + + // Dialog factories — set by View code-behind + public Func? ShowConfirmDialog { get; set; } + + public TransferViewModel( + IFileTransferService transferService, + ISessionManager sessionManager, + BulkResultCsvExportService exportService, + ILogger logger) + : base(logger) + { + _transferService = transferService; + _sessionManager = sessionManager; + _exportService = exportService; + _logger = logger; + + ExportFailedCommand = new AsyncRelayCommand(ExportFailedAsync, () => HasFailures); + } + + protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) + { + if (_currentProfile == null) + throw new InvalidOperationException("No tenant connected."); + + if (string.IsNullOrWhiteSpace(SourceSiteUrl) || string.IsNullOrWhiteSpace(SourceLibrary)) + throw new InvalidOperationException("Source site and library must be selected."); + + if (string.IsNullOrWhiteSpace(DestSiteUrl) || string.IsNullOrWhiteSpace(DestLibrary)) + throw new InvalidOperationException("Destination site and library must be selected."); + + // Confirmation dialog + var message = $"{TransferMode} files from {SourceLibrary} to {DestLibrary} ({ConflictPolicy} on conflict)"; + if (ShowConfirmDialog != null && !ShowConfirmDialog(message)) + return; + + var job = new TransferJob + { + SourceSiteUrl = SourceSiteUrl, + SourceLibrary = SourceLibrary, + SourceFolderPath = SourceFolderPath, + DestinationSiteUrl = DestSiteUrl, + DestinationLibrary = DestLibrary, + DestinationFolderPath = DestFolderPath, + Mode = TransferMode, + ConflictPolicy = ConflictPolicy, + }; + + // Build per-site profiles so SessionManager can resolve contexts + var srcProfile = new TenantProfile + { + Name = _currentProfile.Name, + TenantUrl = SourceSiteUrl, + ClientId = _currentProfile.ClientId, + }; + var dstProfile = new TenantProfile + { + Name = _currentProfile.Name, + TenantUrl = DestSiteUrl, + ClientId = _currentProfile.ClientId, + }; + + var srcCtx = await _sessionManager.GetOrCreateContextAsync(srcProfile, ct); + var dstCtx = await _sessionManager.GetOrCreateContextAsync(dstProfile, ct); + + _lastResult = await _transferService.TransferAsync(srcCtx, dstCtx, job, progress, ct); + + // Update UI on dispatcher + await Application.Current.Dispatcher.InvokeAsync(() => + { + HasFailures = _lastResult.HasFailures; + ExportFailedCommand.NotifyCanExecuteChanged(); + + if (_lastResult.HasFailures) + { + ResultSummary = string.Format( + Localization.TranslationSource.Instance["bulk.result.success"], + _lastResult.SuccessCount, _lastResult.FailedCount); + } + else + { + ResultSummary = string.Format( + Localization.TranslationSource.Instance["bulk.result.allsuccess"], + _lastResult.TotalCount); + } + }); + } + + private async Task ExportFailedAsync() + { + if (_lastResult == null || !_lastResult.HasFailures) return; + + var dlg = new SaveFileDialog + { + Filter = "CSV Files (*.csv)|*.csv", + FileName = "transfer_failed_items.csv", + }; + + if (dlg.ShowDialog() == true) + { + await _exportService.WriteFailedItemsCsvAsync( + _lastResult.FailedItems.ToList(), + dlg.FileName, + CancellationToken.None); + Log.Information("Exported failed transfer items to {Path}", dlg.FileName); + } + } + + protected override void OnTenantSwitched(TenantProfile profile) + { + _currentProfile = profile; + SourceSiteUrl = string.Empty; + SourceLibrary = string.Empty; + SourceFolderPath = string.Empty; + DestSiteUrl = string.Empty; + DestLibrary = string.Empty; + DestFolderPath = string.Empty; + ResultSummary = string.Empty; + HasFailures = false; + _lastResult = null; + } +} diff --git a/SharepointToolbox/Views/Converters/IndentConverter.cs b/SharepointToolbox/Views/Converters/IndentConverter.cs index ee89a72..633dc1a 100644 --- a/SharepointToolbox/Views/Converters/IndentConverter.cs +++ b/SharepointToolbox/Views/Converters/IndentConverter.cs @@ -45,3 +45,4 @@ public class InverseBoolConverter : IValueConverter public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value is bool b && !b; } + diff --git a/SharepointToolbox/Views/Tabs/TransferView.xaml b/SharepointToolbox/Views/Tabs/TransferView.xaml new file mode 100644 index 0000000..f77beba --- /dev/null +++ b/SharepointToolbox/Views/Tabs/TransferView.xaml @@ -0,0 +1,81 @@ + + + + + + + + +