feat(04-07): add Phase 4 localization, shared dialogs, and example CSV resources

- Add 80+ Phase 4 EN/FR localization keys to Strings.resx and Strings.fr.resx (tabs, transfer, bulkmembers, bulksites, folderstruct, templates, bulk-shared, folderbrowser)
- Add ResourceManager property accessors for all new keys to Strings.Designer.cs
- Create ConfirmBulkOperationDialog (XAML + code-behind) with Proceed/Cancel buttons
- Create FolderBrowserDialog (XAML + code-behind) with lazy-loading TreeView of SharePoint libraries/folders
- Bundle bulk_add_members.csv, bulk_create_sites.csv, folder_structure.csv as EmbeddedResource in csproj
This commit is contained in:
Dev
2026-04-03 10:13:39 +02:00
parent fdb1108e76
commit 1a2cc13224
11 changed files with 507 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<Window x:Class="SharepointToolbox.Views.Dialogs.ConfirmBulkOperationDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.confirm.title]}"
Width="450" Height="220" WindowStartupLocation="CenterOwner"
ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="MessageText" Grid.Row="0"
TextWrapping="Wrap" FontSize="14"
VerticalAlignment="Center" />
<StackPanel Grid.Row="1" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,20,0,0">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.confirm.cancel]}"
Width="100" Margin="0,0,10,0" IsCancel="True"
Click="Cancel_Click" />
<Button x:Name="ProceedButton"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.confirm.proceed]}"
Width="100" IsDefault="True"
Click="Proceed_Click" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,28 @@
using System.Windows;
namespace SharepointToolbox.Views.Dialogs;
public partial class ConfirmBulkOperationDialog : Window
{
public bool IsConfirmed { get; private set; }
public ConfirmBulkOperationDialog(string message)
{
InitializeComponent();
MessageText.Text = message;
}
private void Proceed_Click(object sender, RoutedEventArgs e)
{
IsConfirmed = true;
DialogResult = true;
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
IsConfirmed = false;
DialogResult = false;
Close();
}
}

View File

@@ -0,0 +1,28 @@
<Window x:Class="SharepointToolbox.Views.Dialogs.FolderBrowserDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.title]}"
Width="400" Height="500" WindowStartupLocation="CenterOwner"
ResizeMode="CanResizeWithGrip">
<DockPanel Margin="10">
<!-- Status -->
<TextBlock x:Name="StatusText" DockPanel.Dock="Top" Margin="0,0,0,10"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.loading]}" />
<!-- Buttons -->
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.cancel]}"
Width="80" Margin="0,0,10,0" IsCancel="True"
Click="Cancel_Click" />
<Button x:Name="SelectButton"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.select]}"
Width="80" IsDefault="True" IsEnabled="False"
Click="Select_Click" />
</StackPanel>
<!-- Tree -->
<TreeView x:Name="FolderTree" SelectedItemChanged="FolderTree_SelectedItemChanged" />
</DockPanel>
</Window>

View File

@@ -0,0 +1,125 @@
using System.Windows;
using System.Windows.Controls;
using Microsoft.SharePoint.Client;
using SharepointToolbox.Core.Helpers;
namespace SharepointToolbox.Views.Dialogs;
public partial class FolderBrowserDialog : Window
{
private readonly ClientContext _ctx;
public string SelectedLibrary { get; private set; } = string.Empty;
public string SelectedFolderPath { get; private set; } = string.Empty;
public FolderBrowserDialog(ClientContext ctx)
{
InitializeComponent();
_ctx = ctx;
Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
try
{
// Load libraries
var web = _ctx.Web;
var lists = _ctx.LoadQuery(web.Lists
.Include(l => l.Title, l => l.Hidden, l => l.BaseType, l => l.RootFolder)
.Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary));
var progress = new Progress<Core.Models.OperationProgress>();
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None);
foreach (var list in lists)
{
var libNode = new TreeViewItem
{
Header = list.Title,
Tag = new FolderNodeInfo(list.Title, string.Empty),
};
// Add dummy child for expand arrow
libNode.Items.Add(new TreeViewItem { Header = "Loading..." });
libNode.Expanded += LibNode_Expanded;
FolderTree.Items.Add(libNode);
}
StatusText.Text = $"{FolderTree.Items.Count} libraries loaded.";
}
catch (Exception ex)
{
StatusText.Text = $"Error: {ex.Message}";
}
}
private async void LibNode_Expanded(object sender, RoutedEventArgs e)
{
if (sender is not TreeViewItem node || node.Tag is not FolderNodeInfo info)
return;
// Only load children once
if (node.Items.Count == 1 && node.Items[0] is TreeViewItem dummy && dummy.Header?.ToString() == "Loading...")
{
node.Items.Clear();
try
{
var folderUrl = string.IsNullOrEmpty(info.FolderPath)
? GetLibraryRootUrl(info.LibraryTitle)
: info.FolderPath;
var folder = _ctx.Web.GetFolderByServerRelativeUrl(folderUrl);
_ctx.Load(folder, f => f.Folders.Include(sf => sf.Name, sf => sf.ServerRelativeUrl));
var progress = new Progress<Core.Models.OperationProgress>();
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None);
foreach (var subFolder in folder.Folders)
{
if (subFolder.Name.StartsWith("_") || subFolder.Name == "Forms")
continue;
var childNode = new TreeViewItem
{
Header = subFolder.Name,
Tag = new FolderNodeInfo(info.LibraryTitle, subFolder.ServerRelativeUrl),
};
childNode.Items.Add(new TreeViewItem { Header = "Loading..." });
childNode.Expanded += LibNode_Expanded;
node.Items.Add(childNode);
}
}
catch (Exception ex)
{
node.Items.Add(new TreeViewItem { Header = $"Error: {ex.Message}" });
}
}
}
private string GetLibraryRootUrl(string libraryTitle)
{
var uri = new Uri(_ctx.Url);
return $"{uri.AbsolutePath.TrimEnd('/')}/{libraryTitle}";
}
private void FolderTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (e.NewValue is TreeViewItem node && node.Tag is FolderNodeInfo info)
{
SelectedLibrary = info.LibraryTitle;
SelectedFolderPath = info.FolderPath;
SelectButton.IsEnabled = true;
}
}
private void Select_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private record FolderNodeInfo(string LibraryTitle, string FolderPath);
}