feat(04-08): create TransferViewModel and TransferView
- TransferViewModel: source/dest site selection, transfer mode, conflict policy, confirmation dialog, per-item results, failed-items CSV export - TransferView.xaml: DockPanel layout with GroupBoxes for source/dest, mode radio buttons, conflict policy ComboBox, progress bar, cancel button, export failed items button - TransferView.xaml.cs: code-behind wires SitePickerDialog + FolderBrowserDialog for source and dest browsing - Added EnumBoolConverter and StringToVisibilityConverter to IndentConverter.cs - Registered converters in App.xaml; registered TransferViewModel, TransferView, IFileTransferService, BulkResultCsvExportService in App.xaml.cs
This commit is contained in:
165
SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs
Normal file
165
SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs
Normal file
@@ -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<FeatureViewModelBase> _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<string>? _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<string, bool>? ShowConfirmDialog { get; set; }
|
||||
|
||||
public TransferViewModel(
|
||||
IFileTransferService transferService,
|
||||
ISessionManager sessionManager,
|
||||
BulkResultCsvExportService exportService,
|
||||
ILogger<FeatureViewModelBase> logger)
|
||||
: base(logger)
|
||||
{
|
||||
_transferService = transferService;
|
||||
_sessionManager = sessionManager;
|
||||
_exportService = exportService;
|
||||
_logger = logger;
|
||||
|
||||
ExportFailedCommand = new AsyncRelayCommand(ExportFailedAsync, () => HasFailures);
|
||||
}
|
||||
|
||||
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user