chore: release v2.4

- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager)
- Add InputDialog, Spinner common view
- Add DuplicatesCsvExportService
- Refresh views, dialogs, and view models across tabs
- Update localization strings (en/fr)
- Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-20 11:23:11 +02:00
parent 8f30a60d2a
commit 12dd1de9f2
93 changed files with 8708 additions and 1159 deletions
@@ -0,0 +1,26 @@
<UserControl x:Class="SharepointToolbox.Views.Common.Spinner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="20" Height="20">
<Grid RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<RotateTransform x:Name="Rot" Angle="0" />
</Grid.RenderTransform>
<Grid.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="Rot"
Storyboard.TargetProperty="Angle"
From="0" To="360" Duration="0:0:1"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<Ellipse Stroke="{DynamicResource BorderSoftBrush}" StrokeThickness="3" />
<Ellipse Stroke="{DynamicResource AccentBrush}" StrokeThickness="3"
StrokeDashArray="3 3" StrokeDashCap="Round" />
</Grid>
</UserControl>
@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace SharepointToolbox.Views.Common;
public partial class Spinner : UserControl
{
public Spinner()
{
InitializeComponent();
}
}
@@ -4,6 +4,9 @@
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.confirm.title]}"
Width="450" Height="220" WindowStartupLocation="CenterOwner"
Background="{DynamicResource AppBgBrush}"
Foreground="{DynamicResource TextBrush}"
TextOptions.TextFormattingMode="Ideal"
ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
@@ -3,13 +3,24 @@
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"
Width="520" Height="560" WindowStartupLocation="CenterOwner"
Background="{DynamicResource AppBgBrush}"
Foreground="{DynamicResource TextBrush}"
TextOptions.TextFormattingMode="Ideal"
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]}" />
<!-- Action bar: new folder (destination mode only) -->
<StackPanel x:Name="ActionBar" DockPanel.Dock="Top" Orientation="Horizontal"
Margin="0,0,0,6" Visibility="Collapsed">
<Button x:Name="NewFolderButton"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.new_folder]}"
Padding="8,3" Click="NewFolder_Click" IsEnabled="False" />
</StackPanel>
<!-- Buttons -->
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,10,0,0">
@@ -8,13 +8,38 @@ namespace SharepointToolbox.Views.Dialogs;
public partial class FolderBrowserDialog : Window
{
private readonly ClientContext _ctx;
private readonly bool _allowFileSelection;
private readonly bool _allowFolderCreation;
public string SelectedLibrary { get; private set; } = string.Empty;
public string SelectedFolderPath { get; private set; } = string.Empty;
public FolderBrowserDialog(ClientContext ctx)
/// <summary>
/// Library-relative file paths checked by the user. Only populated when
/// <paramref name="allowFileSelection"/> was true. Empty if the user picked
/// a folder node instead.
/// </summary>
public IReadOnlyList<string> SelectedFilePaths { get; private set; } = Array.Empty<string>();
private readonly List<CheckBox> _fileCheckboxes = new();
private readonly List<TreeViewItem> _expandedNodes = new();
/// <summary>
/// Dialog for browsing library folders. Set <paramref name="allowFileSelection"/>
/// to show individual files (with sizes) and allow ticking them for targeted
/// transfer. Set <paramref name="allowFolderCreation"/> to expose a "New
/// Folder" button that creates a folder under the selected node.
/// </summary>
public FolderBrowserDialog(ClientContext ctx,
bool allowFileSelection = false,
bool allowFolderCreation = false)
{
InitializeComponent();
_ctx = ctx;
_allowFileSelection = allowFileSelection;
_allowFolderCreation = allowFolderCreation;
if (allowFolderCreation)
ActionBar.Visibility = Visibility.Visible;
Loaded += OnLoaded;
}
@@ -22,24 +47,19 @@ public partial class FolderBrowserDialog : Window
{
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)
.Include(l => l.Title, l => l.Hidden, l => l.BaseType,
l => l.RootFolder.ServerRelativeUrl)
.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;
var rootUrl = list.RootFolder.ServerRelativeUrl.TrimEnd('/');
var libNode = MakeFolderNode(list.Title,
new FolderNodeInfo(list.Title, string.Empty, rootUrl));
FolderTree.Items.Add(libNode);
}
@@ -51,52 +71,146 @@ public partial class FolderBrowserDialog : Window
}
}
private async void LibNode_Expanded(object sender, RoutedEventArgs e)
private TreeViewItem MakeFolderNode(string name, FolderNodeInfo info)
{
var node = new TreeViewItem
{
Header = name,
Tag = info,
};
// Placeholder child so the expand arrow appears.
node.Items.Add(new TreeViewItem { Header = "Loading..." });
node.Expanded += FolderNode_Expanded;
_expandedNodes.Add(node);
return node;
}
private async void FolderNode_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...")
// Only load children once.
if (!(node.Items.Count == 1
&& node.Items[0] is TreeViewItem dummy
&& dummy.Header?.ToString() == "Loading..."))
return;
node.Items.Clear();
try
{
node.Items.Clear();
try
var folder = _ctx.Web.GetFolderByServerRelativeUrl(info.ServerRelativeUrl);
_ctx.Load(folder, f => f.StorageMetrics.TotalSize,
f => f.StorageMetrics.TotalFileCount);
var list = _ctx.Web.Lists.GetByTitle(info.LibraryTitle);
_ctx.Load(list, l => l.Title);
var progress = new Progress<Core.Models.OperationProgress>();
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None);
// Annotate the parent node header with total metrics now that they loaded.
node.Header = FormatFolderHeader(info.LibraryTitle == info.RelativePath || string.IsNullOrEmpty(info.RelativePath)
? (string)node.Header!
: System.IO.Path.GetFileName(info.RelativePath),
folder.StorageMetrics.TotalFileCount,
folder.StorageMetrics.TotalSize);
// Enumerate direct children via paginated CAML — Folder.Folders /
// Folder.Files lazy loading hits the list-view threshold on libraries
// above 5,000 items even when only a small folder is being expanded.
var subFolders = new List<(string Name, string Url)>();
var filesInFolder = new List<(string Name, long Length, string Url)>();
await foreach (var item in SharePointPaginationHelper.GetItemsInFolderAsync(
_ctx, list, info.ServerRelativeUrl, recursive: false,
viewFields: new[] { "FSObjType", "FileLeafRef", "FileRef", "File_x0020_Size" },
ct: CancellationToken.None))
{
var folderUrl = string.IsNullOrEmpty(info.FolderPath)
? GetLibraryRootUrl(info.LibraryTitle)
: info.FolderPath;
var fsType = item["FSObjType"]?.ToString();
var name = item["FileLeafRef"]?.ToString() ?? string.Empty;
var fileRef = item["FileRef"]?.ToString() ?? string.Empty;
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 (fsType == "1")
{
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);
if (name.StartsWith("_") || name == "Forms") continue;
subFolders.Add((name, fileRef));
}
else if (fsType == "0" && _allowFileSelection)
{
long.TryParse(item["File_x0020_Size"]?.ToString() ?? "0", out long size);
filesInFolder.Add((name, size, fileRef));
}
}
catch (Exception ex)
// Batch-load StorageMetrics for each subfolder in one round-trip.
var metricFolders = subFolders
.Select(sf => _ctx.Web.GetFolderByServerRelativeUrl(sf.Url))
.ToList();
foreach (var mf in metricFolders)
_ctx.Load(mf, f => f.StorageMetrics.TotalSize,
f => f.StorageMetrics.TotalFileCount);
if (metricFolders.Count > 0)
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None);
for (int i = 0; i < subFolders.Count; i++)
{
node.Items.Add(new TreeViewItem { Header = $"Error: {ex.Message}" });
var (name, url) = subFolders[i];
var childRelative = string.IsNullOrEmpty(info.RelativePath)
? name
: $"{info.RelativePath}/{name}";
var childInfo = new FolderNodeInfo(info.LibraryTitle, childRelative, url);
var childNode = MakeFolderNode(
FormatFolderHeader(name,
metricFolders[i].StorageMetrics.TotalFileCount,
metricFolders[i].StorageMetrics.TotalSize),
childInfo);
node.Items.Add(childNode);
}
// Files under this folder — only shown when selection is enabled.
if (_allowFileSelection)
{
foreach (var (fileName, fileSize, _) in filesInFolder)
{
var fileRel = string.IsNullOrEmpty(info.RelativePath)
? fileName
: $"{info.RelativePath}/{fileName}";
var cb = new CheckBox
{
Content = $"{fileName} ({FormatSize(fileSize)})",
Tag = new FileNodeInfo(info.LibraryTitle, fileRel),
Margin = new Thickness(4, 2, 0, 2),
};
cb.Checked += FileCheckbox_Toggled;
cb.Unchecked += FileCheckbox_Toggled;
_fileCheckboxes.Add(cb);
var fileItem = new TreeViewItem { Header = cb, Focusable = false };
node.Items.Add(fileItem);
}
}
}
catch (Exception ex)
{
node.Items.Add(new TreeViewItem { Header = $"Error: {ex.Message}" });
}
}
private string GetLibraryRootUrl(string libraryTitle)
private static string FormatFolderHeader(string name, long fileCount, long totalBytes)
{
var uri = new Uri(_ctx.Url);
return $"{uri.AbsolutePath.TrimEnd('/')}/{libraryTitle}";
if (fileCount <= 0) return name;
return $"{name} ({fileCount} files, {FormatSize(totalBytes)})";
}
private static string FormatSize(long bytes)
{
if (bytes <= 0) return "0 B";
if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB";
if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB";
if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB";
return $"{bytes} B";
}
private void FolderTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
@@ -104,13 +218,83 @@ public partial class FolderBrowserDialog : Window
if (e.NewValue is TreeViewItem node && node.Tag is FolderNodeInfo info)
{
SelectedLibrary = info.LibraryTitle;
SelectedFolderPath = info.FolderPath;
SelectedFolderPath = info.RelativePath;
SelectButton.IsEnabled = true;
NewFolderButton.IsEnabled = _allowFolderCreation;
}
else
{
// File nodes have CheckBox headers, not FolderNodeInfo tags.
NewFolderButton.IsEnabled = false;
}
}
private void FileCheckbox_Toggled(object sender, RoutedEventArgs e)
{
// Enable "Select" as soon as any file is checked — user can confirm
// purely via file selection without also picking a folder node.
if (_fileCheckboxes.Any(c => c.IsChecked == true))
SelectButton.IsEnabled = true;
}
private async void NewFolder_Click(object sender, RoutedEventArgs e)
{
if (FolderTree.SelectedItem is not TreeViewItem node ||
node.Tag is not FolderNodeInfo info)
return;
var dlg = new InputDialog("New folder name:", string.Empty)
{
Owner = this
};
if (dlg.ShowDialog() != true) return;
var folderName = dlg.ResponseText.Trim();
if (string.IsNullOrEmpty(folderName)) return;
try
{
var parent = _ctx.Web.GetFolderByServerRelativeUrl(info.ServerRelativeUrl);
var created = parent.Folders.Add(folderName);
_ctx.Load(created, f => f.ServerRelativeUrl, f => f.Name);
var progress = new Progress<Core.Models.OperationProgress>();
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None);
var childRelative = string.IsNullOrEmpty(info.RelativePath)
? created.Name
: $"{info.RelativePath}/{created.Name}";
var childInfo = new FolderNodeInfo(
info.LibraryTitle, childRelative, created.ServerRelativeUrl);
var childNode = MakeFolderNode(created.Name, childInfo);
// Expand the parent so the fresh folder is visible immediately.
node.IsExpanded = true;
node.Items.Add(childNode);
StatusText.Text = $"Created: {created.ServerRelativeUrl}";
}
catch (Exception ex)
{
StatusText.Text = $"Error: {ex.Message}";
}
}
private void Select_Click(object sender, RoutedEventArgs e)
{
// Harvest checked files (library-relative paths).
SelectedFilePaths = _fileCheckboxes
.Where(c => c.IsChecked == true && c.Tag is FileNodeInfo)
.Select(c => ((FileNodeInfo)c.Tag!).RelativePath)
.ToList();
// If files were picked but no folder node was selected, borrow the
// library from the first file so the caller still has a valid target.
if (SelectedFilePaths.Count > 0 && string.IsNullOrEmpty(SelectedLibrary))
{
var firstTag = (FileNodeInfo)_fileCheckboxes
.First(c => c.IsChecked == true && c.Tag is FileNodeInfo).Tag!;
SelectedLibrary = firstTag.LibraryTitle;
SelectedFolderPath = string.Empty;
}
DialogResult = true;
Close();
}
@@ -121,5 +305,21 @@ public partial class FolderBrowserDialog : Window
Close();
}
private record FolderNodeInfo(string LibraryTitle, string FolderPath);
protected override void OnClosed(EventArgs e)
{
Loaded -= OnLoaded;
foreach (var node in _expandedNodes)
node.Expanded -= FolderNode_Expanded;
_expandedNodes.Clear();
foreach (var cb in _fileCheckboxes)
{
cb.Checked -= FileCheckbox_Toggled;
cb.Unchecked -= FileCheckbox_Toggled;
}
_fileCheckboxes.Clear();
base.OnClosed(e);
}
private record FolderNodeInfo(string LibraryTitle, string RelativePath, string ServerRelativeUrl);
private record FileNodeInfo(string LibraryTitle, string RelativePath);
}
@@ -0,0 +1,24 @@
<Window x:Class="SharepointToolbox.Views.Dialogs.InputDialog"
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=[input.title]}"
Width="340" Height="140"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource AppBgBrush}"
Foreground="{DynamicResource TextBrush}"
TextOptions.TextFormattingMode="Ideal"
ResizeMode="NoResize">
<DockPanel Margin="12">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.cancel]}"
Width="70" IsCancel="True" Margin="0,0,8,0"
Click="Cancel_Click" />
<Button Content="OK" Width="70" IsDefault="True"
Click="Ok_Click" />
</StackPanel>
<TextBlock x:Name="PromptText" DockPanel.Dock="Top" Margin="0,0,0,6" />
<TextBox x:Name="ResponseBox" VerticalContentAlignment="Center" Padding="4" />
</DockPanel>
</Window>
@@ -0,0 +1,28 @@
using System.Windows;
namespace SharepointToolbox.Views.Dialogs;
public partial class InputDialog : Window
{
public string ResponseText => ResponseBox.Text;
public InputDialog(string prompt, string initialValue)
{
InitializeComponent();
PromptText.Text = prompt;
ResponseBox.Text = initialValue ?? string.Empty;
Loaded += (_, _) => { ResponseBox.Focus(); ResponseBox.SelectAll(); };
}
private void Ok_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
@@ -2,8 +2,12 @@
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="Manage Profiles" Width="500" Height="750"
Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profmgmt.title]}"
Width="500" Height="750"
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
Background="{DynamicResource AppBgBrush}"
Foreground="{DynamicResource TextBrush}"
TextOptions.TextFormattingMode="Ideal"
ResizeMode="NoResize">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
@@ -19,7 +23,7 @@
</Grid.RowDefinitions>
<!-- Profile list -->
<Label Content="Profiles" Grid.Row="0" />
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profmgmt.group]}" Grid.Row="0" />
<ListBox Grid.Row="1" Margin="0,0,0,8"
ItemsSource="{Binding Profiles}"
SelectedItem="{Binding SelectedProfile}"
@@ -50,14 +54,14 @@
<TextBox Text="{Binding NewClientId, UpdateSourceTrigger=PropertyChanged}"
Grid.Row="2" Grid.Column="1" Margin="0,2" />
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,2,0,0"
FontSize="11" FontStyle="Italic" Foreground="#666666" TextWrapping="Wrap"
FontSize="11" FontStyle="Italic" Foreground="{DynamicResource TextMutedBrush}" TextWrapping="Wrap"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.clientid.hint]}" />
</Grid>
<!-- Client Logo -->
<StackPanel Grid.Row="3" Margin="0,8,0,8">
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.title]}" Padding="0,0,0,4" />
<Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="8" CornerRadius="4"
<Border BorderBrush="{DynamicResource BorderSoftBrush}" BorderThickness="1" Padding="8" CornerRadius="4"
HorizontalAlignment="Left" MinWidth="200" MinHeight="50">
<Grid>
<Image Source="{Binding ClientLogoPreview, Converter={StaticResource Base64ToImageConverter}}"
@@ -65,7 +69,7 @@
Visibility="{Binding ClientLogoPreview, Converter={StaticResource StringToVisibilityConverter}}" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.nopreview]}"
VerticalAlignment="Center" HorizontalAlignment="Center"
Foreground="#999999" FontStyle="Italic">
Foreground="{DynamicResource TextMutedBrush}" FontStyle="Italic">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible" />
@@ -87,7 +91,7 @@
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.autopull]}"
Command="{Binding AutoPullClientLogoCommand}" Width="130" />
</StackPanel>
<TextBlock Text="{Binding ValidationMessage}" Foreground="#CC0000" FontSize="11" Margin="0,4,0,0"
<TextBlock Text="{Binding ValidationMessage}" Foreground="{DynamicResource DangerBrush}" FontSize="11" Margin="0,4,0,0"
Visibility="{Binding ValidationMessage, Converter={StaticResource StringToVisibilityConverter}}" />
</StackPanel>
@@ -107,11 +111,11 @@
<!-- Status text -->
<TextBlock Text="{Binding RegistrationStatus}" FontSize="11" Margin="0,2,0,0"
Foreground="#006600"
Foreground="{DynamicResource SuccessBrush}"
Visibility="{Binding RegistrationStatus, Converter={StaticResource StringToVisibilityConverter}}" />
<!-- Fallback instructions panel -->
<Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="8" CornerRadius="4" Margin="0,4,0,0"
<Border BorderBrush="{DynamicResource BorderSoftBrush}" BorderThickness="1" Padding="8" CornerRadius="4" Margin="0,4,0,0"
Visibility="{Binding ShowFallbackInstructions, Converter={StaticResource BooleanToVisibilityConverter}}">
<StackPanel>
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step1]}"
@@ -134,11 +138,12 @@
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.add]}"
Command="{Binding AddCommand}" Width="60" Margin="4,0" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.rename]}"
Command="{Binding RenameCommand}" Width="60" Margin="4,0" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.save]}"
Command="{Binding SaveCommand}" MinWidth="80" Padding="6,0" Margin="4,0" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.delete]}"
Command="{Binding DeleteCommand}" Width="60" Margin="4,0" />
<Button Content="Close" Width="60" Margin="4,0"
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.close]}"
Width="60" Margin="4,0"
Click="CloseButton_Click" IsCancel="True" />
</StackPanel>
</Grid>
@@ -1,30 +1,73 @@
<Window x:Class="SharepointToolbox.Views.Dialogs.SitePickerDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Select Sites" Width="600" Height="500"
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.title]}"
Width="760" Height="560"
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
Background="{DynamicResource AppBgBrush}"
Foreground="{DynamicResource TextBrush}"
TextOptions.TextFormattingMode="Ideal"
Loaded="Window_Loaded">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Filter row -->
<DockPanel Grid.Row="0" Margin="0,0,0,8">
<TextBlock Text="Filter:" VerticalAlignment="Center" Margin="0,0,8,0" />
<!-- Text filter row -->
<DockPanel Grid.Row="0" Margin="0,0,0,6">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.filter]}"
VerticalAlignment="Center" Margin="0,0,8,0" />
<TextBox x:Name="FilterBox" TextChanged="FilterBox_TextChanged" />
</DockPanel>
<!-- Size + type filter row -->
<DockPanel Grid.Row="1" Margin="0,0,0,8" LastChildFill="False">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.type]}"
VerticalAlignment="Center" Margin="0,0,6,0" />
<ComboBox x:Name="TypeFilter" Width="170" SelectedIndex="0"
SelectionChanged="TypeFilter_SelectionChanged" Margin="0,0,12,0">
<ComboBoxItem Tag="All"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.type.all]}" />
<ComboBoxItem Tag="TeamSite"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.type.team]}" />
<ComboBoxItem Tag="CommunicationSite"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.type.communication]}" />
<ComboBoxItem Tag="Classic"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.type.classic]}" />
<ComboBoxItem Tag="Unknown"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.type.other]}" />
</ComboBox>
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.size]}"
VerticalAlignment="Center" Margin="0,0,6,0" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.size.min]}"
VerticalAlignment="Center" Margin="0,0,4,0"
Foreground="{DynamicResource TextMutedBrush}" />
<TextBox x:Name="MinSizeBox" Width="80" Margin="0,0,8,0"
TextChanged="SizeBox_TextChanged" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.size.max]}"
VerticalAlignment="Center" Margin="0,0,4,0"
Foreground="{DynamicResource TextMutedBrush}" />
<TextBox x:Name="MaxSizeBox" Width="80"
TextChanged="SizeBox_TextChanged" />
</DockPanel>
<!-- Site list with checkboxes -->
<ListView x:Name="SiteList" Grid.Row="1" Margin="0,0,0,8"
<ListView x:Name="SiteList" Grid.Row="2" Margin="0,0,0,8"
SelectionMode="Single"
BorderThickness="1" BorderBrush="#CCCCCC">
BorderThickness="1" BorderBrush="{DynamicResource BorderSoftBrush}"
GridViewColumnHeader.Click="SiteList_ColumnHeaderClick">
<ListView.View>
<GridView>
<GridViewColumn Header="" Width="32">
<GridViewColumn Width="32">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="IsSelected" Content="" />
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
@@ -32,28 +75,56 @@
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Title" Width="200" DisplayMemberBinding="{Binding Title}" />
<GridViewColumn Header="URL" Width="320" DisplayMemberBinding="{Binding Url}" />
<GridViewColumn Width="200" DisplayMemberBinding="{Binding Title}">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Title"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.col.title]}" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width="280" DisplayMemberBinding="{Binding Url}">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Url"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.col.url]}" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width="110" DisplayMemberBinding="{Binding KindDisplay}">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Kind"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.col.type]}" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width="80" DisplayMemberBinding="{Binding SizeDisplay}">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="StorageUsedMb"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.col.size]}" />
</GridViewColumn.Header>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<!-- Status text -->
<TextBlock x:Name="StatusText" Grid.Row="2" Margin="0,0,0,8"
Foreground="#555555" FontSize="11" />
<TextBlock x:Name="StatusText" Grid.Row="3" Margin="0,0,0,8"
Foreground="{DynamicResource TextMutedBrush}" FontSize="11" />
<!-- Button row -->
<DockPanel Grid.Row="3">
<Button x:Name="LoadButton" Content="Load Sites" Width="80" Margin="0,0,8,0"
<DockPanel Grid.Row="4">
<Button x:Name="LoadButton"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.btn.load]}"
Width="110" Margin="0,0,8,0"
Click="LoadButton_Click" />
<Button Content="Select All" Width="80" Margin="0,0,8,0"
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.btn.selectAll]}"
Width="120" Margin="0,0,8,0"
Click="SelectAll_Click" />
<Button Content="Deselect All" Width="80" Margin="0,0,8,0"
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.btn.deselectAll]}"
Width="140" Margin="0,0,8,0"
Click="DeselectAll_Click" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
<Button Content="OK" Width="70" Margin="4,0" IsDefault="True"
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.btn.ok]}"
Width="80" Margin="4,0" IsDefault="True"
Click="OK_Click" />
<Button Content="Cancel" Width="70" Margin="4,0" IsCancel="True" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[sitepicker.btn.cancel]}"
Width="80" Margin="4,0" IsCancel="True" />
</StackPanel>
</DockPanel>
</Grid>
@@ -1,52 +1,61 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using SharepointToolbox.Services;
using SharepointToolbox.ViewModels.Dialogs;
namespace SharepointToolbox.Views.Dialogs;
/// <summary>
/// Dialog for selecting multiple SharePoint sites.
/// Loads sites from ISiteListService, shows them in a filterable list with checkboxes.
/// Delegates loading and filter/sort logic to <see cref="SitePickerDialogLogic"/>
/// so the code-behind only handles WPF plumbing.
/// </summary>
public partial class SitePickerDialog : Window
{
private readonly ISiteListService _siteListService;
private readonly TenantProfile _profile;
private readonly SitePickerDialogLogic _logic;
private List<SitePickerItem> _allItems = new();
private string _sortColumn = "Url";
private ListSortDirection _sortDirection = ListSortDirection.Ascending;
/// <summary>
/// Returns the list of sites the user checked before clicking OK.
/// </summary>
public IReadOnlyList<SiteInfo> SelectedUrls =>
_allItems.Where(i => i.IsSelected).Select(i => new SiteInfo(i.Url, i.Title)).ToList();
_allItems.Where(i => i.IsSelected).Select(i => new SiteInfo(i.Url, i.Title)
{
StorageUsedMb = i.StorageUsedMb,
StorageQuotaMb = i.StorageQuotaMb,
Template = i.Template
}).ToList();
public SitePickerDialog(ISiteListService siteListService, TenantProfile profile)
{
InitializeComponent();
_siteListService = siteListService;
_profile = profile;
_logic = new SitePickerDialogLogic(siteListService, profile);
}
private async void Window_Loaded(object sender, RoutedEventArgs e) => await LoadSitesAsync();
private async Task LoadSitesAsync()
{
StatusText.Text = "Loading sites...";
StatusText.Text = TranslationSource.Instance["sitepicker.status.loading"];
LoadButton.IsEnabled = false;
try
{
var sites = await _siteListService.GetSitesAsync(
_profile,
var items = await _logic.LoadAsync(
new Progress<OperationProgress>(),
System.Threading.CancellationToken.None);
_allItems = sites.Select(s => new SitePickerItem(s.Url, s.Title)).ToList();
_allItems = items.ToList();
ApplyFilter();
StatusText.Text = $"{_allItems.Count} sites loaded.";
StatusText.Text = string.Format(
CultureInfo.CurrentUICulture,
TranslationSource.Instance["sitepicker.status.loaded"],
_allItems.Count);
}
catch (InvalidOperationException ex)
{
@@ -54,7 +63,10 @@ public partial class SitePickerDialog : Window
}
catch (Exception ex)
{
StatusText.Text = $"Error: {ex.Message}";
StatusText.Text = string.Format(
CultureInfo.CurrentUICulture,
TranslationSource.Instance["sitepicker.status.error"],
ex.Message);
}
finally
{
@@ -64,25 +76,66 @@ public partial class SitePickerDialog : Window
private void ApplyFilter()
{
var filter = FilterBox.Text.Trim();
SiteList.ItemsSource = string.IsNullOrEmpty(filter)
? (IEnumerable<SitePickerItem>)_allItems
: _allItems.Where(i =>
i.Url.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
i.Title.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList();
var text = FilterBox.Text.Trim();
var minMb = SitePickerDialogLogic.ParseLongOrDefault(MinSizeBox.Text, 0);
var maxMb = SitePickerDialogLogic.ParseLongOrDefault(MaxSizeBox.Text, long.MaxValue);
var kindFilter = (TypeFilter.SelectedItem as ComboBoxItem)?.Tag as string ?? "All";
var filtered = SitePickerDialogLogic.ApplyFilter(_allItems, text, minMb, maxMb, kindFilter);
var sorted = SitePickerDialogLogic.ApplySort(filtered, _sortColumn, _sortDirection);
var list = sorted.ToList();
SiteList.ItemsSource = list;
if (_allItems.Count > 0)
StatusText.Text = string.Format(
CultureInfo.CurrentUICulture,
TranslationSource.Instance["sitepicker.status.shown"],
list.Count, _allItems.Count);
}
private void SiteList_ColumnHeaderClick(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is not GridViewColumnHeader header) return;
if (header.Role == GridViewColumnHeaderRole.Padding) return;
if (header.Tag is not string column || string.IsNullOrEmpty(column)) return;
if (_sortColumn == column)
{
_sortDirection = _sortDirection == ListSortDirection.Ascending
? ListSortDirection.Descending
: ListSortDirection.Ascending;
}
else
{
_sortColumn = column;
_sortDirection = ListSortDirection.Ascending;
}
ApplyFilter();
}
private void FilterBox_TextChanged(object sender, TextChangedEventArgs e) => ApplyFilter();
private void SizeBox_TextChanged(object sender, TextChangedEventArgs e) => ApplyFilter();
private void TypeFilter_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!IsLoaded) return;
ApplyFilter();
}
private void SelectAll_Click(object sender, RoutedEventArgs e)
{
foreach (var item in _allItems) item.IsSelected = true;
if (SiteList.ItemsSource is IEnumerable<SitePickerItem> visible)
{
foreach (var item in visible) item.IsSelected = true;
}
ApplyFilter();
}
private void DeselectAll_Click(object sender, RoutedEventArgs e)
{
foreach (var item in _allItems) item.IsSelected = false;
if (SiteList.ItemsSource is IEnumerable<SitePickerItem> visible)
{
foreach (var item in visible) item.IsSelected = false;
}
ApplyFilter();
}
@@ -105,6 +158,13 @@ public class SitePickerItem : INotifyPropertyChanged
public string Url { get; init; } = string.Empty;
public string Title { get; init; } = string.Empty;
public long StorageUsedMb { get; init; }
public long StorageQuotaMb { get; init; }
public string Template { get; init; } = string.Empty;
public SiteKind Kind => SiteKindHelper.FromTemplate(Template);
public string KindDisplay => SiteKindHelper.DisplayName(Kind);
public string SizeDisplay => FormatSize(StorageUsedMb);
public bool IsSelected
{
@@ -119,9 +179,19 @@ public class SitePickerItem : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
public SitePickerItem(string url, string title)
public SitePickerItem(string url, string title, long storageUsedMb = 0, long storageQuotaMb = 0, string template = "")
{
Url = url;
Title = title;
StorageUsedMb = storageUsedMb;
StorageQuotaMb = storageQuotaMb;
Template = template;
}
private static string FormatSize(long mb)
{
if (mb <= 0) return "—";
if (mb >= 1024) return $"{mb / 1024.0:F1} GB";
return $"{mb} MB";
}
}
@@ -1,10 +1,13 @@
<UserControl x:Class="SharepointToolbox.Views.Tabs.BulkMembersView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
xmlns:common="clr-namespace:SharepointToolbox.Views.Common">
<DockPanel Margin="10">
<!-- Options Panel (Left) -->
<StackPanel DockPanel.Dock="Left" Width="240" Margin="0,0,10,0">
<ScrollViewer DockPanel.Dock="Left" Width="240" Margin="0,0,10,0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulkmembers.import]}"
Command="{Binding ImportCsvCommand}" Margin="0,0,0,5" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulkmembers.example]}"
@@ -19,8 +22,8 @@
Command="{Binding CancelCommand}"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<ProgressBar Height="20" Margin="0,10,0,5" Value="{Binding ProgressValue}"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<common:Spinner Width="20" Height="20" HorizontalAlignment="Left" Margin="0,10,0,5"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" />
<TextBlock Text="{Binding ResultSummary}" FontWeight="Bold" Margin="0,10,0,5" />
@@ -29,6 +32,7 @@
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.exportfailed]}"
Command="{Binding ExportFailedCommand}" />
</StackPanel>
</ScrollViewer>
<!-- Preview DataGrid (Right) -->
<DataGrid ItemsSource="{Binding PreviewRows}" AutoGenerateColumns="False"
@@ -44,14 +48,15 @@
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Valid" Binding="{Binding IsValid}" Width="50" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.valid]}"
Binding="{Binding IsValid}" Width="50" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulkmembers.groupname]}"
Binding="{Binding Record.GroupName}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulkmembers.email]}"
Binding="{Binding Record.Email}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulkmembers.role]}"
Binding="{Binding Record.Role}" Width="80" />
<DataGridTextColumn Header="Errors"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.errors]}"
Binding="{Binding Errors, Converter={StaticResource ListToStringConverter}}"
Width="*" />
</DataGrid.Columns>
@@ -1,9 +1,12 @@
<UserControl x:Class="SharepointToolbox.Views.Tabs.BulkSitesView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
xmlns:common="clr-namespace:SharepointToolbox.Views.Common">
<DockPanel Margin="10">
<StackPanel DockPanel.Dock="Left" Width="240" Margin="0,0,10,0">
<ScrollViewer DockPanel.Dock="Left" Width="240" Margin="0,0,10,0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulksites.import]}"
Command="{Binding ImportCsvCommand}" Margin="0,0,0,5" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulksites.example]}"
@@ -18,8 +21,8 @@
Command="{Binding CancelCommand}"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<ProgressBar Height="20" Margin="0,10,0,5" Value="{Binding ProgressValue}"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<common:Spinner Width="20" Height="20" HorizontalAlignment="Left" Margin="0,10,0,5"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" />
<TextBlock Text="{Binding ResultSummary}" FontWeight="Bold" Margin="0,10,0,5" />
@@ -28,6 +31,7 @@
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.exportfailed]}"
Command="{Binding ExportFailedCommand}" />
</StackPanel>
</ScrollViewer>
<DataGrid ItemsSource="{Binding PreviewRows}" AutoGenerateColumns="False"
IsReadOnly="True" CanUserSortColumns="True">
@@ -42,7 +46,8 @@
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Valid" Binding="{Binding IsValid}" Width="50" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.valid]}"
Binding="{Binding IsValid}" Width="50" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulksites.name]}"
Binding="{Binding Record.Name}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulksites.alias]}"
@@ -51,7 +56,7 @@
Binding="{Binding Record.Type}" Width="100" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulksites.owners]}"
Binding="{Binding Record.Owners}" Width="*" />
<DataGridTextColumn Header="Errors"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.errors]}"
Binding="{Binding Errors, Converter={StaticResource ListToStringConverter}}"
Width="*" />
</DataGrid.Columns>
@@ -18,7 +18,7 @@
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[grp.dup.criteria]}" Margin="0,0,0,8">
<StackPanel Margin="4">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[lbl.dup.note]}"
TextWrapping="Wrap" FontSize="11" Foreground="#555" Margin="0,0,0,6" />
TextWrapping="Wrap" FontSize="11" Foreground="{DynamicResource TextMutedBrush}" Margin="0,0,0,6" />
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[chk.dup.size]}"
IsChecked="{Binding MatchSize}" Margin="0,2" />
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[chk.dup.created]}"
@@ -44,28 +44,51 @@
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.cancel]}"
Command="{Binding CancelCommand}" Height="28" Margin="0,0,0,8" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.open.results]}"
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.label]}" Margin="0,4,0,0" />
<ComboBox SelectedIndex="{Binding SplitModeIndex}" Height="26" Margin="0,0,0,4">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.single]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.bySite]}" />
</ComboBox>
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.label]}" Margin="0,2,0,0" />
<ComboBox SelectedIndex="{Binding HtmlLayoutIndex}" Height="26" Margin="0,0,0,6">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.separate]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.tabbed]}" />
</ComboBox>
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.exportCsv]}"
Command="{Binding ExportCsvCommand}" Height="28" Margin="0,0,0,4" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.exportHtml]}"
Command="{Binding ExportHtmlCommand}" Height="28" Margin="0,0,0,4" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" FontSize="11" Foreground="#555" Margin="0,4" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" FontSize="11" Foreground="{DynamicResource TextMutedBrush}" Margin="0,4" />
</StackPanel>
</ScrollViewer>
<!-- Results DataGrid -->
<DataGrid ItemsSource="{Binding Results}" IsReadOnly="True" AutoGenerateColumns="False"
VirtualizingPanel.IsVirtualizing="True" Margin="4,8,8,8">
VirtualizingPanel.IsVirtualizing="True" Margin="4,8,8,8"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ColumnWidth="Auto">
<DataGrid.Columns>
<DataGridTextColumn Header="Group" Binding="{Binding GroupName}" Width="160" />
<DataGridTextColumn Header="Copies" Binding="{Binding GroupSize}" Width="60"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[duplicates.col.group]}"
Binding="{Binding GroupName}" Width="160" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[duplicates.col.copies]}"
Binding="{Binding GroupSize}" Width="60"
ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="160" />
<DataGridTextColumn Header="Library" Binding="{Binding Library}" Width="120" />
<DataGridTextColumn Header="Size"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.name]}"
Binding="{Binding Name}" Width="160" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.library]}"
Binding="{Binding Library}" Width="120" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.size]}"
Binding="{Binding SizeBytes, Converter={StaticResource BytesConverter}}"
Width="90" ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="Created" Binding="{Binding Created, StringFormat=yyyy-MM-dd}" Width="100" />
<DataGridTextColumn Header="Modified" Binding="{Binding Modified, StringFormat=yyyy-MM-dd}" Width="100" />
<DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.created]}"
Binding="{Binding Created, StringFormat=yyyy-MM-dd}" Width="100" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.modified]}"
Binding="{Binding Modified, StringFormat=yyyy-MM-dd}" Width="100" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.path]}"
Binding="{Binding Path}" Width="400" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -1,9 +1,12 @@
<UserControl x:Class="SharepointToolbox.Views.Tabs.FolderStructureView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
xmlns:common="clr-namespace:SharepointToolbox.Views.Common">
<DockPanel Margin="10">
<StackPanel DockPanel.Dock="Left" Width="280" Margin="0,0,10,0">
<ScrollViewer DockPanel.Dock="Left" Width="280" Margin="0,0,10,0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<!-- Library input -->
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderstruct.library]}"
Margin="0,0,0,3" />
@@ -23,14 +26,15 @@
Command="{Binding CancelCommand}"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<ProgressBar Height="20" Margin="0,10,0,5" Value="{Binding ProgressValue}"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<common:Spinner Width="20" Height="20" HorizontalAlignment="Left" Margin="0,10,0,5"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" />
<TextBlock Text="{Binding ResultSummary}" FontWeight="Bold" Margin="0,10,0,5" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.exportfailed]}"
Command="{Binding ExportFailedCommand}" />
</StackPanel>
</ScrollViewer>
<DataGrid ItemsSource="{Binding PreviewRows}" AutoGenerateColumns="False"
IsReadOnly="True" CanUserSortColumns="True">
@@ -45,12 +49,17 @@
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Valid" Binding="{Binding IsValid}" Width="50" />
<DataGridTextColumn Header="Level 1" Binding="{Binding Record.Level1}" Width="*" />
<DataGridTextColumn Header="Level 2" Binding="{Binding Record.Level2}" Width="*" />
<DataGridTextColumn Header="Level 3" Binding="{Binding Record.Level3}" Width="*" />
<DataGridTextColumn Header="Level 4" Binding="{Binding Record.Level4}" Width="*" />
<DataGridTextColumn Header="Errors"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.valid]}"
Binding="{Binding IsValid}" Width="50" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderstruct.col.level1]}"
Binding="{Binding Record.Level1}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderstruct.col.level2]}"
Binding="{Binding Record.Level2}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderstruct.col.level3]}"
Binding="{Binding Record.Level3}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderstruct.col.level4]}"
Binding="{Binding Record.Level4}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.errors]}"
Binding="{Binding Errors, Converter={StaticResource ListToStringConverter}}"
Width="*" />
</DataGrid.Columns>
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
xmlns:common="clr-namespace:SharepointToolbox.Views.Common"
xmlns:models="clr-namespace:SharepointToolbox.Core.Models"
xmlns:converters="clr-namespace:SharepointToolbox.Core.Converters">
@@ -21,7 +22,9 @@
</Grid.RowDefinitions>
<!-- Left panel: Scan configuration -->
<DockPanel Grid.Column="0" Grid.Row="0" Margin="8">
<ScrollViewer Grid.Column="0" Grid.Row="0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<DockPanel Margin="8">
<!-- Scan Options GroupBox -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[grp.scan.opts]}"
@@ -65,7 +68,7 @@
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSimplifiedMode}" Value="False">
<Setter Property="Foreground" Value="Gray" />
<Setter Property="Foreground" Value="{DynamicResource TextMutedBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
@@ -108,6 +111,16 @@
Command="{Binding CancelCommand}"
Margin="0,0,0,4" Padding="6,3" />
</Grid>
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.label]}" Padding="0,2" />
<ComboBox SelectedIndex="{Binding SplitModeIndex}" Height="24" Margin="0,0,0,4">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.single]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.bySite]}" />
</ComboBox>
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.label]}" Padding="0,2" />
<ComboBox SelectedIndex="{Binding HtmlLayoutIndex}" Height="24" Margin="0,0,0,6">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.separate]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.tabbed]}" />
</ComboBox>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -125,6 +138,7 @@
</StackPanel>
</DockPanel>
</ScrollViewer>
<!-- Right panel: Summary + Results -->
<Grid Grid.Column="1" Grid.Row="0" Margin="0,8,8,8">
@@ -153,7 +167,8 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border CornerRadius="6" Padding="14,10" Margin="0,0,10,4" MinWidth="140">
<Border CornerRadius="6" Padding="14,10" Margin="0,0,10,4" MinWidth="140"
TextElement.Foreground="{DynamicResource OnColoredBgBrush}">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F3F4F6" />
@@ -182,7 +197,7 @@
</Style>
</Border.Style>
<StackPanel>
<TextBlock Text="{Binding Count}" FontSize="22" FontWeight="Bold" />
<TextBlock Text="{Binding Count}" FontSize="22" FontWeight="Bold" Foreground="#1F2430" />
<TextBlock Text="{Binding Label}" FontSize="11" Foreground="#555" />
<TextBlock FontSize="10" Foreground="#888" Margin="0,2,0,0">
<Run Text="{Binding DistinctUsers, Mode=OneWay}" />
@@ -222,19 +237,24 @@
<Style.Triggers>
<DataTrigger Binding="{Binding RiskLevel}" Value="{x:Static models:RiskLevel.High}">
<Setter Property="Background" Value="#FEF2F2" />
<Setter Property="Foreground" Value="#1F2430" />
</DataTrigger>
<DataTrigger Binding="{Binding RiskLevel}" Value="{x:Static models:RiskLevel.Medium}">
<Setter Property="Background" Value="#FFFBEB" />
<Setter Property="Foreground" Value="#1F2430" />
</DataTrigger>
<DataTrigger Binding="{Binding RiskLevel}" Value="{x:Static models:RiskLevel.Low}">
<Setter Property="Background" Value="#ECFDF5" />
<Setter Property="Foreground" Value="#1F2430" />
</DataTrigger>
<DataTrigger Binding="{Binding RiskLevel}" Value="{x:Static models:RiskLevel.ReadOnly}">
<Setter Property="Background" Value="#EFF6FF" />
<Setter Property="Foreground" Value="#1F2430" />
</DataTrigger>
<!-- Phase 18: auto-elevated rows get amber background + tooltip -->
<DataTrigger Binding="{Binding WasAutoElevated}" Value="True">
<Setter Property="Background" Value="#FFF9E6" />
<Setter Property="Foreground" Value="#1F2430" />
<Setter Property="ToolTip" Value="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[permissions.elevated.tooltip]}" />
</DataTrigger>
</Style.Triggers>
@@ -261,15 +281,22 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Object Type" Binding="{Binding ObjectType}" Width="100" />
<DataGridTextColumn Header="Title" Binding="{Binding Title}" Width="140" />
<DataGridTextColumn Header="URL" Binding="{Binding Url}" Width="200" />
<DataGridTextColumn Header="Unique Perms" Binding="{Binding HasUniquePermissions}" Width="90" />
<DataGridTextColumn Header="Users" Binding="{Binding Users}" Width="140" />
<DataGridTextColumn Header="Permission Levels" Binding="{Binding PermissionLevels}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.object_type]}"
Binding="{Binding ObjectType}" Width="100" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.title]}"
Binding="{Binding Title}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.url]}"
Binding="{Binding Url}" Width="200" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[perm.col.unique_perms]}"
Binding="{Binding HasUniquePermissions}" Width="90" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.users_groups]}"
Binding="{Binding Users}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[perm.col.permission_levels]}"
Binding="{Binding PermissionLevels}" Width="140" />
<!-- Simplified Labels column (only visible in simplified mode) -->
<DataGridTextColumn Header="Simplified" Binding="{Binding SimplifiedLabels}" Width="200">
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.simplified]}"
Binding="{Binding SimplifiedLabels}" Width="200">
<DataGridTextColumn.Visibility>
<Binding Path="DataContext.IsSimplifiedMode"
RelativeSource="{RelativeSource AncestorType=DataGrid}"
@@ -277,8 +304,10 @@
</DataGridTextColumn.Visibility>
</DataGridTextColumn>
<DataGridTextColumn Header="Granted Through" Binding="{Binding GrantedThrough}" Width="140" />
<DataGridTextColumn Header="Principal Type" Binding="{Binding PrincipalType}" Width="110" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.granted_through]}"
Binding="{Binding GrantedThrough}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[perm.col.principal_type]}"
Binding="{Binding PrincipalType}" Width="110" />
</DataGrid.Columns>
</DataGrid>
</Grid>
@@ -286,9 +315,8 @@
<!-- Bottom: status bar spanning both columns -->
<StatusBar Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1">
<StatusBarItem>
<ProgressBar Width="150" Height="14"
Value="{Binding ProgressValue}"
Minimum="0" Maximum="100" />
<common:Spinner Width="14" Height="14"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
</StatusBarItem>
<StatusBarItem Content="{Binding StatusMessage}" />
</StatusBar>
+1 -1
View File
@@ -72,7 +72,7 @@
</StackPanel>
</GroupBox>
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" FontSize="11" Foreground="#555" Margin="0,4" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" FontSize="11" Foreground="{DynamicResource TextMutedBrush}" Margin="0,4" />
</StackPanel>
</ScrollViewer>
+21 -4
View File
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16">
<!-- Language -->
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.language]}" />
@@ -16,6 +17,21 @@
<Separator Margin="0,12" />
<!-- Theme -->
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.theme]}" />
<ComboBox Width="200" HorizontalAlignment="Left"
SelectedValue="{Binding SelectedTheme}"
SelectedValuePath="Tag">
<ComboBoxItem Tag="System"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.theme.system]}" />
<ComboBoxItem Tag="Light"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.theme.light]}" />
<ComboBoxItem Tag="Dark"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.theme.dark]}" />
</ComboBox>
<Separator Margin="0,12" />
<!-- Data folder -->
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.folder]}" />
<DockPanel>
@@ -29,7 +45,7 @@
<!-- MSP Logo -->
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.logo.title]}" />
<Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="8" CornerRadius="4"
<Border BorderBrush="{DynamicResource BorderSoftBrush}" BorderThickness="1" Padding="8" CornerRadius="4"
HorizontalAlignment="Left" MinWidth="200" MinHeight="60" Margin="0,4,0,0">
<Grid>
<Image Source="{Binding MspLogoPreview, Converter={StaticResource Base64ToImageConverter}}"
@@ -37,7 +53,7 @@
Visibility="{Binding MspLogoPreview, Converter={StaticResource StringToVisibilityConverter}}" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.logo.nopreview]}"
VerticalAlignment="Center" HorizontalAlignment="Center"
Foreground="#999999" FontStyle="Italic">
Foreground="{DynamicResource TextMutedBrush}" FontStyle="Italic">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible" />
@@ -65,9 +81,10 @@
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.ownership.auto]}"
Margin="0,4,0,0" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.ownership.description]}"
Foreground="#666666" FontSize="11" TextWrapping="Wrap" Margin="20,4,0,0" />
Foreground="{DynamicResource TextMutedBrush}" FontSize="11" TextWrapping="Wrap" Margin="20,4,0,0" />
<TextBlock Text="{Binding StatusMessage}" Foreground="#CC0000" FontSize="11" Margin="0,4,0,0"
<TextBlock Text="{Binding StatusMessage}" Foreground="{DynamicResource DangerBrush}" FontSize="11" Margin="0,4,0,0"
Visibility="{Binding StatusMessage, Converter={StaticResource StringToVisibilityConverter}}" />
</StackPanel>
</ScrollViewer>
</UserControl>
+37 -21
View File
@@ -28,7 +28,7 @@
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[chk.max.depth]}"
IsChecked="{Binding IsMaxDepth}" Margin="0,2" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.note]}"
TextWrapping="Wrap" FontSize="11" Foreground="#888"
TextWrapping="Wrap" FontSize="11" Foreground="{DynamicResource TextMutedBrush}"
Margin="0,6,0,0" />
</StackPanel>
</GroupBox>
@@ -45,6 +45,16 @@
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[grp.export.fmt]}"
Margin="0,0,0,8">
<StackPanel Margin="4">
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.label]}" Margin="0,0,0,0" Padding="0,2" />
<ComboBox SelectedIndex="{Binding SplitModeIndex}" Height="24" Margin="0,0,0,4">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.single]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.bySite]}" />
</ComboBox>
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.label]}" Padding="0,2" />
<ComboBox SelectedIndex="{Binding HtmlLayoutIndex}" Height="24" Margin="0,0,0,6">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.separate]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.tabbed]}" />
</ComboBox>
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.rad.csv]}"
Command="{Binding ExportCsvCommand}"
Height="26" Margin="0,2" />
@@ -68,7 +78,7 @@
<!-- Status -->
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap"
FontSize="11" Foreground="#555" Margin="0,4" />
FontSize="11" Foreground="{DynamicResource TextMutedBrush}" Margin="0,4" />
</StackPanel>
</ScrollViewer>
@@ -82,7 +92,7 @@
</Grid.RowDefinitions>
<!-- Summary bar -->
<Border Grid.Row="0" Background="#F0F7FF" CornerRadius="4" Padding="12,8" Margin="0,0,0,6">
<Border Grid.Row="0" Background="{DynamicResource AccentSoftBrush}" CornerRadius="4" Padding="12,8" Margin="0,0,0,6">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed" />
@@ -94,18 +104,18 @@
</Style>
</Border.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,24,0">
<Run Text="Total Size: " FontWeight="SemiBold" />
<Run Text="{Binding SummaryTotalSize, Converter={StaticResource BytesConverter}, Mode=OneWay}" />
</TextBlock>
<TextBlock Margin="0,0,24,0">
<Run Text="Version Size: " FontWeight="SemiBold" />
<Run Text="{Binding SummaryVersionSize, Converter={StaticResource BytesConverter}, Mode=OneWay}" />
</TextBlock>
<TextBlock>
<Run Text="Files: " FontWeight="SemiBold" />
<Run Text="{Binding SummaryFileCount, StringFormat=N0, Mode=OneWay}" />
</TextBlock>
<StackPanel Orientation="Horizontal" Margin="0,0,24,0">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[storage.lbl.total_size_colon]}" FontWeight="SemiBold" />
<TextBlock Text="{Binding SummaryTotalSize, Converter={StaticResource BytesConverter}, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,24,0">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[storage.lbl.version_size_colon]}" FontWeight="SemiBold" />
<TextBlock Text="{Binding SummaryVersionSize, Converter={StaticResource BytesConverter}, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[storage.lbl.files_colon]}" FontWeight="SemiBold" />
<TextBlock Text="{Binding SummaryFileCount, StringFormat=N0, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Border>
@@ -147,11 +157,11 @@
<!-- Splitter between DataGrid and Chart -->
<GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch"
Background="#DDD" ResizeDirection="Rows" />
Background="{DynamicResource BorderSoftBrush}" ResizeDirection="Rows" />
<!-- Chart panel -->
<Border Grid.Row="3" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4"
Padding="8" Background="White">
<Border Grid.Row="3" BorderBrush="{DynamicResource BorderSoftBrush}" BorderThickness="1" CornerRadius="4"
Padding="8" Background="{DynamicResource SurfaceBrush}">
<Grid>
<!-- Chart title -->
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.chart.title]}"
@@ -160,7 +170,7 @@
<!-- No data placeholder -->
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="#888" FontSize="12"
Foreground="{DynamicResource TextMutedBrush}" FontSize="12"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.chart.nodata]}">
<TextBlock.Style>
<Style TargetType="TextBlock">
@@ -192,7 +202,11 @@
</Grid.Style>
<lvc:PieChart x:Name="StoragePieChart"
Series="{Binding PieChartSeries}"
LegendPosition="Right" />
LegendPosition="Right"
LegendTextPaint="{Binding LegendTextPaint}"
LegendBackgroundPaint="{Binding LegendBackgroundPaint}"
TooltipTextPaint="{Binding TooltipTextPaint}"
TooltipBackgroundPaint="{Binding TooltipBackgroundPaint}" />
</Grid>
<!-- Bar chart wrapper (visible when IsDonutChart=false AND HasChartData=true) -->
@@ -214,7 +228,9 @@
<lvc:CartesianChart Series="{Binding BarChartSeries}"
XAxes="{Binding BarXAxes}"
YAxes="{Binding BarYAxes}"
LegendPosition="Hidden" />
LegendPosition="Hidden"
TooltipTextPaint="{Binding TooltipTextPaint}"
TooltipBackgroundPaint="{Binding TooltipBackgroundPaint}" />
</Grid>
</Grid>
</Border>
@@ -22,16 +22,20 @@ public partial class StorageView : UserControl
/// </summary>
internal sealed class SingleSliceTooltip : IChartTooltip
{
private readonly System.Windows.Controls.ToolTip _tip = new()
private readonly System.Windows.Controls.ToolTip _tip;
public SingleSliceTooltip()
{
Padding = new System.Windows.Thickness(8, 4, 8, 4),
FontSize = 13,
Background = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(255, 255, 255)),
BorderBrush = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(200, 200, 200)),
BorderThickness = new System.Windows.Thickness(1),
};
_tip = new System.Windows.Controls.ToolTip
{
Padding = new System.Windows.Thickness(8, 4, 8, 4),
FontSize = 13,
BorderThickness = new System.Windows.Thickness(1),
};
_tip.SetResourceReference(System.Windows.Controls.Control.BackgroundProperty, "SurfaceBrush");
_tip.SetResourceReference(System.Windows.Controls.Control.ForegroundProperty, "TextBrush");
_tip.SetResourceReference(System.Windows.Controls.Control.BorderBrushProperty, "BorderSoftBrush");
}
public void Show(IEnumerable<ChartPoint> foundPoints, Chart chart)
{
@@ -4,7 +4,9 @@
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
<DockPanel Margin="10">
<!-- Left panel: Capture and Apply -->
<StackPanel DockPanel.Dock="Left" Width="320" Margin="0,0,10,0">
<ScrollViewer DockPanel.Dock="Left" Width="320" Margin="0,0,10,0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<!-- Capture Section -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.capture]}"
Margin="0,0,0,10">
@@ -52,6 +54,7 @@
<!-- Progress -->
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" Margin="0,5,0,0" />
</StackPanel>
</ScrollViewer>
<!-- Right panel: Template list -->
<DockPanel>
@@ -68,10 +71,14 @@
AutoGenerateColumns="False" IsReadOnly="True"
SelectionMode="Single" CanUserSortColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
<DataGridTextColumn Header="Type" Binding="{Binding SiteType}" Width="100" />
<DataGridTextColumn Header="Source" Binding="{Binding SourceUrl}" Width="*" />
<DataGridTextColumn Header="Captured" Binding="{Binding CapturedAt, StringFormat=yyyy-MM-dd HH:mm}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.name]}"
Binding="{Binding Name}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulksites.type]}"
Binding="{Binding SiteType}" Width="100" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.col.source]}"
Binding="{Binding SourceUrl}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.col.captured]}"
Binding="{Binding CapturedAt, StringFormat=yyyy-MM-dd HH:mm}" Width="140" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
+24 -7
View File
@@ -1,10 +1,13 @@
<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">
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
xmlns:common="clr-namespace:SharepointToolbox.Views.Common">
<DockPanel Margin="10">
<!-- Options Panel (Left) -->
<StackPanel DockPanel.Dock="Left" Width="340" Margin="0,0,10,0">
<ScrollViewer DockPanel.Dock="Left" Width="340" Margin="0,0,10,0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<!-- Source -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.sourcesite]}"
Margin="0,0,0,10">
@@ -14,7 +17,21 @@
<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" />
<TextBlock Text="{Binding SourceFolderPath}" Foreground="{DynamicResource TextMutedBrush}" />
<StackPanel Orientation="Horizontal">
<TextBlock Foreground="{DynamicResource AccentBrush}" FontStyle="Italic" FontSize="11"
Text="{Binding SelectedFileCount, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource AccentBrush}" FontStyle="Italic" FontSize="11"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.text.files_selected]}" />
</StackPanel>
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.chk.include_source]}"
IsChecked="{Binding IncludeSourceFolder}"
Margin="0,6,0,0"
ToolTip="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.chk.include_source.tooltip]}" />
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.chk.copy_contents]}"
IsChecked="{Binding CopyFolderContents}"
Margin="0,4,0,0"
ToolTip="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[transfer.chk.copy_contents.tooltip]}" />
</StackPanel>
</GroupBox>
@@ -27,7 +44,7 @@
<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" />
<TextBlock Text="{Binding DestFolderPath}" Foreground="{DynamicResource TextMutedBrush}" />
</StackPanel>
</GroupBox>
@@ -62,9 +79,8 @@
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<!-- Progress -->
<ProgressBar Height="20" Margin="0,10,0,5"
Value="{Binding ProgressValue}"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<common:Spinner Width="20" Height="20" HorizontalAlignment="Left" Margin="0,10,0,5"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" />
<!-- Results -->
@@ -74,6 +90,7 @@
Command="{Binding ExportFailedCommand}"
Visibility="{Binding HasFailures, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel>
</ScrollViewer>
<!-- Right panel placeholder for future enhancements -->
<Border />
@@ -53,11 +53,15 @@ public partial class TransferView : UserControl
ClientId = _viewModel.CurrentProfile.ClientId,
};
var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None);
var folderBrowser = new FolderBrowserDialog(ctx) { Owner = Window.GetWindow(this) };
var folderBrowser = new FolderBrowserDialog(ctx, allowFileSelection: true)
{
Owner = Window.GetWindow(this)
};
if (folderBrowser.ShowDialog() == true)
{
_viewModel.SourceLibrary = folderBrowser.SelectedLibrary;
_viewModel.SourceFolderPath = folderBrowser.SelectedFolderPath;
_viewModel.SetSelectedFiles(folderBrowser.SelectedFilePaths);
}
}
@@ -81,7 +85,10 @@ public partial class TransferView : UserControl
ClientId = _viewModel.CurrentProfile.ClientId,
};
var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None);
var folderBrowser = new FolderBrowserDialog(ctx) { Owner = Window.GetWindow(this) };
var folderBrowser = new FolderBrowserDialog(ctx, allowFolderCreation: true)
{
Owner = Window.GetWindow(this)
};
if (folderBrowser.ShowDialog() == true)
{
_viewModel.DestLibrary = folderBrowser.SelectedLibrary;
@@ -1,7 +1,8 @@
<UserControl x:Class="SharepointToolbox.Views.Tabs.UserAccessAuditView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
xmlns:common="clr-namespace:SharepointToolbox.Views.Common">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="290" />
@@ -13,7 +14,9 @@
</Grid.RowDefinitions>
<!-- Left panel -->
<DockPanel Grid.Column="0" Grid.Row="0" Margin="8">
<ScrollViewer Grid.Column="0" Grid.Row="0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<DockPanel Margin="8">
<!-- Mode toggle -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
@@ -41,7 +44,7 @@
</GroupBox.Style>
<StackPanel>
<TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,2" />
<TextBlock Text="Searching..." FontStyle="Italic" FontSize="10" Foreground="Gray" Margin="0,0,0,2">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.searching]}" FontStyle="Italic" FontSize="10" Foreground="{DynamicResource TextMutedBrush}" Margin="0,0,0,2">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed" />
@@ -115,9 +118,9 @@
<!-- Status row: load status + user count -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="{Binding DirectoryLoadStatus}" FontStyle="Italic" Foreground="Gray" FontSize="10"
<TextBlock Text="{Binding DirectoryLoadStatus}" FontStyle="Italic" Foreground="{DynamicResource TextMutedBrush}" FontSize="10"
Margin="0,0,8,0" />
<TextBlock FontSize="10" Foreground="Gray">
<TextBlock FontSize="10" Foreground="{DynamicResource TextMutedBrush}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="DirectoryUserCount" />
@@ -130,7 +133,7 @@
<!-- Hint text -->
<TextBlock DockPanel.Dock="Bottom"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.hint.doubleclick]}"
FontStyle="Italic" Foreground="Gray" FontSize="10" Margin="0,4,0,0"
FontStyle="Italic" Foreground="{DynamicResource TextMutedBrush}" FontSize="10" Margin="0,4,0,0"
TextWrapping="Wrap" />
<!-- Directory DataGrid -->
@@ -142,17 +145,17 @@
CanUserSortColumns="True"
SelectionMode="Single" SelectionUnit="FullRow"
HeadersVisibility="Column" GridLinesVisibility="Horizontal"
BorderThickness="1" BorderBrush="#DDDDDD">
BorderThickness="1" BorderBrush="{DynamicResource BorderSoftBrush}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.name]}"
Binding="{Binding DisplayName}" Width="120" />
<DataGridTextColumn Header="Email"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulkmembers.email]}"
Binding="{Binding UserPrincipalName}" Width="140" />
<DataGridTextColumn Header="Department"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.department]}"
Binding="{Binding Department}" Width="90" />
<DataGridTextColumn Header="Job Title"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.jobtitle]}"
Binding="{Binding JobTitle}" Width="90" />
<DataGridTemplateColumn Header="Type" Width="60">
<DataGridTemplateColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.type]}" Width="60">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding UserType}" VerticalAlignment="Center">
@@ -181,19 +184,20 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#EBF5FB" BorderBrush="#2980B9" BorderThickness="1"
CornerRadius="4" Padding="6,2" Margin="0,1">
CornerRadius="4" Padding="6,2" Margin="0,1"
TextElement.Foreground="{DynamicResource OnColoredBgBrush}">
<DockPanel>
<Button Content="x" DockPanel.Dock="Right" Padding="4,0"
Background="Transparent" BorderThickness="0"
Command="{Binding DataContext.RemoveUserCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}" />
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" Foreground="#1F2430" />
</DockPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="{Binding SelectedUsersLabel}" FontStyle="Italic" Foreground="Gray" FontSize="10" />
<TextBlock Text="{Binding SelectedUsersLabel}" FontStyle="Italic" Foreground="{DynamicResource TextMutedBrush}" FontSize="10" />
</StackPanel>
<!-- Scan Options (always visible) -->
@@ -232,6 +236,17 @@
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.cancel]}"
Command="{Binding CancelCommand}" Margin="0,0,0,0" Padding="6,3" />
</Grid>
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.label]}" Padding="0,2" />
<ComboBox SelectedIndex="{Binding SplitModeIndex}" Height="24" Margin="0,0,0,4">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.single]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.bySite]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.split.byUser]}" />
</ComboBox>
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.label]}" Padding="0,2" />
<ComboBox SelectedIndex="{Binding HtmlLayoutIndex}" Height="24" Margin="0,0,0,6">
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.separate]}" />
<ComboBoxItem Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[export.html.layout.tabbed]}" />
</ComboBox>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -247,6 +262,7 @@
</StackPanel>
</DockPanel>
</ScrollViewer>
<!-- Right panel -->
<Grid Grid.Column="1" Grid.Row="0" Margin="0,8,8,0">
@@ -258,37 +274,40 @@
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
<Border Background="#EBF5FB" BorderBrush="#2980B9" BorderThickness="1"
CornerRadius="4" Padding="12,6" Margin="0,0,8,0">
CornerRadius="4" Padding="12,6" Margin="0,0,8,0"
TextElement.Foreground="{DynamicResource OnColoredBgBrush}">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding TotalAccessCount}" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock Text="{Binding TotalAccessCount}" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" Foreground="#1F2430" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.total]}"
FontSize="10" Foreground="Gray" HorizontalAlignment="Center" />
FontSize="10" Foreground="#5B6472" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Background="#EAFAF1" BorderBrush="#27AE60" BorderThickness="1"
CornerRadius="4" Padding="12,6" Margin="0,0,8,0">
CornerRadius="4" Padding="12,6" Margin="0,0,8,0"
TextElement.Foreground="{DynamicResource OnColoredBgBrush}">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding SitesCount}" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock Text="{Binding SitesCount}" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" Foreground="#1F2430" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.sites]}"
FontSize="10" Foreground="Gray" HorizontalAlignment="Center" />
FontSize="10" Foreground="#5B6472" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border Background="#FDEDEC" BorderBrush="#E74C3C" BorderThickness="1"
CornerRadius="4" Padding="12,6">
CornerRadius="4" Padding="12,6"
TextElement.Foreground="{DynamicResource OnColoredBgBrush}">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding HighPrivilegeCount}" FontSize="20" FontWeight="Bold"
HorizontalAlignment="Center" Foreground="#C0392B" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.highPriv]}"
FontSize="10" Foreground="Gray" HorizontalAlignment="Center" />
FontSize="10" Foreground="#5B6472" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,4">
<TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}" Width="200" Margin="0,0,8,0" />
<ToggleButton IsChecked="{Binding IsGroupByUser}" Padding="8,3">
<ToggleButton IsChecked="{Binding IsGroupByUser}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Style TargetType="ToggleButton" BasedOn="{StaticResource ThemeToggleButtonStyle}">
<Setter Property="Content" Value="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.toggle.bySite]}" />
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
@@ -308,12 +327,15 @@
<Style.Triggers>
<DataTrigger Binding="{Binding AccessType}" Value="Direct">
<Setter Property="Background" Value="#EBF5FB" />
<Setter Property="Foreground" Value="#1F2430" />
</DataTrigger>
<DataTrigger Binding="{Binding AccessType}" Value="Group">
<Setter Property="Background" Value="#EAFAF1" />
<Setter Property="Foreground" Value="#1F2430" />
</DataTrigger>
<DataTrigger Binding="{Binding AccessType}" Value="Inherited">
<Setter Property="Background" Value="#F4F6F6" />
<Setter Property="Foreground" Value="#1F2430" />
</DataTrigger>
<DataTrigger Binding="{Binding IsHighPrivilege}" Value="True">
<Setter Property="FontWeight" Value="Bold" />
@@ -327,14 +349,14 @@
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="4,2">
<TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="0,0,8,0" />
<TextBlock Text="{Binding ItemCount, StringFormat=({0})}" Foreground="Gray" />
<TextBlock Text="{Binding ItemCount, StringFormat=({0})}" Foreground="{DynamicResource TextMutedBrush}" />
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</DataGrid.GroupStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Header="User" Width="180">
<DataGridTemplateColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.user]}" Width="180">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
@@ -351,20 +373,24 @@
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="Guest" FontSize="10" Foreground="White" FontWeight="SemiBold" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.guest]}"
FontSize="10" Foreground="White" FontWeight="SemiBold" />
</Border>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Site" Binding="{Binding SiteTitle}" Width="120" />
<DataGridTextColumn Header="Object" Binding="{Binding ObjectTitle}" Width="140" />
<DataGridTextColumn Header="Object Type" Binding="{Binding ObjectType}" Width="90" />
<DataGridTemplateColumn Header="Permission Level" Width="140">
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.site]}"
Binding="{Binding SiteTitle}" Width="120" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.object]}"
Binding="{Binding ObjectTitle}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.object_type]}"
Binding="{Binding ObjectType}" Width="90" />
<DataGridTemplateColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.permission_level]}" Width="140">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#x26A0;" Foreground="#E74C3C" Margin="0,0,4,0"
<TextBlock Text="&#x26A0;" Foreground="{DynamicResource DangerBrush}" Margin="0,0,4,0"
FontSize="12" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
@@ -382,7 +408,7 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Access Type" Width="110">
<DataGridTemplateColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.access_type]}" Width="110">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock>
@@ -406,7 +432,8 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Granted Through" Binding="{Binding GrantedThrough}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[report.col.granted_through]}"
Binding="{Binding GrantedThrough}" Width="140" />
</DataGrid.Columns>
</DataGrid>
@@ -415,7 +442,8 @@
<!-- Status bar -->
<StatusBar Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1">
<StatusBarItem>
<ProgressBar Width="150" Height="14" Value="{Binding ProgressValue}" Minimum="0" Maximum="100" />
<common:Spinner Width="14" Height="14"
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
</StatusBarItem>
<StatusBarItem Content="{Binding StatusMessage}" />
</StatusBar>