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:
@@ -45,3 +45,4 @@ public class InverseBoolConverter : IValueConverter
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is bool b && !b;
|
||||
}
|
||||
|
||||
|
||||
81
SharepointToolbox/Views/Tabs/TransferView.xaml
Normal file
81
SharepointToolbox/Views/Tabs/TransferView.xaml
Normal file
@@ -0,0 +1,81 @@
|
||||
<UserControl x:Class="SharepointToolbox.Views.Tabs.TransferView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
|
||||
<DockPanel Margin="10">
|
||||
<!-- Options Panel (Left) -->
|
||||
<StackPanel DockPanel.Dock="Left" Width="340" Margin="0,0,10,0">
|
||||
<!-- Source -->
|
||||
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.sourcesite]}"
|
||||
Margin="0,0,0,10">
|
||||
<StackPanel Margin="5">
|
||||
<TextBox Text="{Binding SourceSiteUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsReadOnly="True" Margin="0,0,0,5" />
|
||||
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.browse]}"
|
||||
Click="BrowseSource_Click" Margin="0,0,0,5" />
|
||||
<TextBlock Text="{Binding SourceLibrary}" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding SourceFolderPath}" Foreground="Gray" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Destination -->
|
||||
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.destsite]}"
|
||||
Margin="0,0,0,10">
|
||||
<StackPanel Margin="5">
|
||||
<TextBox Text="{Binding DestSiteUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsReadOnly="True" Margin="0,0,0,5" />
|
||||
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.browse]}"
|
||||
Click="BrowseDest_Click" Margin="0,0,0,5" />
|
||||
<TextBlock Text="{Binding DestLibrary}" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding DestFolderPath}" Foreground="Gray" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Transfer Mode -->
|
||||
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.mode]}"
|
||||
Margin="0,0,0,10">
|
||||
<StackPanel Margin="5">
|
||||
<RadioButton Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.mode.copy]}"
|
||||
IsChecked="{Binding TransferMode, Converter={StaticResource EnumBoolConverter}, ConverterParameter=Copy}"
|
||||
Margin="0,0,0,3" />
|
||||
<RadioButton Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.mode.move]}"
|
||||
IsChecked="{Binding TransferMode, Converter={StaticResource EnumBoolConverter}, ConverterParameter=Move}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Conflict Policy -->
|
||||
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.conflict]}"
|
||||
Margin="0,0,0,10">
|
||||
<ComboBox SelectedIndex="0" x:Name="ConflictCombo" SelectionChanged="ConflictCombo_SelectionChanged">
|
||||
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.conflict.skip]}" />
|
||||
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.conflict.overwrite]}" />
|
||||
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.conflict.rename]}" />
|
||||
</ComboBox>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Actions -->
|
||||
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.start]}"
|
||||
Command="{Binding RunCommand}" Margin="0,0,0,5"
|
||||
IsEnabled="{Binding IsRunning, Converter={StaticResource InverseBoolConverter}}" />
|
||||
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[status.cancelled]}"
|
||||
Command="{Binding CancelCommand}"
|
||||
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
|
||||
<!-- Progress -->
|
||||
<ProgressBar Height="20" Margin="0,10,0,5"
|
||||
Value="{Binding ProgressValue}"
|
||||
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" />
|
||||
|
||||
<!-- Results -->
|
||||
<TextBlock Text="{Binding ResultSummary}" FontWeight="Bold" Margin="0,10,0,5"
|
||||
Visibility="{Binding ResultSummary, Converter={StaticResource StringToVisibilityConverter}}" />
|
||||
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.exportfailed]}"
|
||||
Command="{Binding ExportFailedCommand}"
|
||||
Visibility="{Binding HasFailures, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right panel placeholder for future enhancements -->
|
||||
<Border />
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
99
SharepointToolbox/Views/Tabs/TransferView.xaml.cs
Normal file
99
SharepointToolbox/Views/Tabs/TransferView.xaml.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services;
|
||||
using SharepointToolbox.Views.Dialogs;
|
||||
|
||||
namespace SharepointToolbox.Views.Tabs;
|
||||
|
||||
public partial class TransferView : UserControl
|
||||
{
|
||||
private readonly ViewModels.Tabs.TransferViewModel _viewModel;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly Func<TenantProfile, SitePickerDialog> _sitePickerFactory;
|
||||
|
||||
public TransferView(
|
||||
ViewModels.Tabs.TransferViewModel viewModel,
|
||||
ISessionManager sessionManager,
|
||||
Func<TenantProfile, SitePickerDialog> sitePickerFactory)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = viewModel;
|
||||
_sessionManager = sessionManager;
|
||||
_sitePickerFactory = sitePickerFactory;
|
||||
DataContext = viewModel;
|
||||
|
||||
viewModel.ShowConfirmDialog = message =>
|
||||
{
|
||||
var dlg = new ConfirmBulkOperationDialog(message) { Owner = Window.GetWindow(this) };
|
||||
dlg.ShowDialog();
|
||||
return dlg.IsConfirmed;
|
||||
};
|
||||
}
|
||||
|
||||
private async void BrowseSource_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel.CurrentProfile == null) return;
|
||||
|
||||
// Pick source site — SitePickerDialog returns a list; take the first selection for transfer
|
||||
var sitePicker = _sitePickerFactory(_viewModel.CurrentProfile);
|
||||
sitePicker.Owner = Window.GetWindow(this);
|
||||
if (sitePicker.ShowDialog() != true) return;
|
||||
|
||||
var selectedSite = sitePicker.SelectedUrls.FirstOrDefault();
|
||||
if (selectedSite == null) return;
|
||||
|
||||
_viewModel.SourceSiteUrl = selectedSite.Url;
|
||||
|
||||
// Browse library/folder for the selected source site
|
||||
var profile = new TenantProfile
|
||||
{
|
||||
Name = _viewModel.CurrentProfile.Name,
|
||||
TenantUrl = selectedSite.Url,
|
||||
ClientId = _viewModel.CurrentProfile.ClientId,
|
||||
};
|
||||
var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None);
|
||||
var folderBrowser = new FolderBrowserDialog(ctx) { Owner = Window.GetWindow(this) };
|
||||
if (folderBrowser.ShowDialog() == true)
|
||||
{
|
||||
_viewModel.SourceLibrary = folderBrowser.SelectedLibrary;
|
||||
_viewModel.SourceFolderPath = folderBrowser.SelectedFolderPath;
|
||||
}
|
||||
}
|
||||
|
||||
private async void BrowseDest_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel.CurrentProfile == null) return;
|
||||
|
||||
var sitePicker = _sitePickerFactory(_viewModel.CurrentProfile);
|
||||
sitePicker.Owner = Window.GetWindow(this);
|
||||
if (sitePicker.ShowDialog() != true) return;
|
||||
|
||||
var selectedSite = sitePicker.SelectedUrls.FirstOrDefault();
|
||||
if (selectedSite == null) return;
|
||||
|
||||
_viewModel.DestSiteUrl = selectedSite.Url;
|
||||
|
||||
var profile = new TenantProfile
|
||||
{
|
||||
Name = _viewModel.CurrentProfile.Name,
|
||||
TenantUrl = selectedSite.Url,
|
||||
ClientId = _viewModel.CurrentProfile.ClientId,
|
||||
};
|
||||
var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None);
|
||||
var folderBrowser = new FolderBrowserDialog(ctx) { Owner = Window.GetWindow(this) };
|
||||
if (folderBrowser.ShowDialog() == true)
|
||||
{
|
||||
_viewModel.DestLibrary = folderBrowser.SelectedLibrary;
|
||||
_viewModel.DestFolderPath = folderBrowser.SelectedFolderPath;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConflictCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (ConflictCombo.SelectedIndex >= 0)
|
||||
{
|
||||
_viewModel.ConflictPolicy = (ConflictPolicy)ConflictCombo.SelectedIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user