- 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
166 lines
6.0 KiB
C#
166 lines
6.0 KiB
C#
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;
|
|
}
|
|
}
|