This commit is contained in:
Dev
2026-04-24 10:54:47 +02:00
19 changed files with 1113 additions and 51 deletions
@@ -0,0 +1,42 @@
<Window x:Class="SharepointToolbox.Views.Dialogs.LibraryPickerDialog"
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=[librarypicker.title]}"
Width="420" Height="520" WindowStartupLocation="CenterOwner"
Background="{DynamicResource AppBgBrush}"
Foreground="{DynamicResource TextBrush}"
TextOptions.TextFormattingMode="Ideal"
ResizeMode="CanResizeWithGrip">
<DockPanel Margin="10">
<TextBlock x:Name="StatusText" DockPanel.Dock="Top" Margin="0,0,0,8"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[librarypicker.loading]}" />
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,6">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[librarypicker.selectAll]}"
Click="SelectAll_Click" Margin="0,0,6,0" Padding="6,2" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[librarypicker.selectNone]}"
Click="SelectNone_Click" Padding="6,2" />
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,8,0,0">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.cancel]}"
Width="80" Margin="0,0,8,0" IsCancel="True" Click="Cancel_Click" />
<Button x:Name="OkButton"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.select]}"
Width="80" IsDefault="True" IsEnabled="False" Click="Ok_Click" />
</StackPanel>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="LibrariesList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Title}" IsChecked="{Binding IsSelected, Mode=TwoWay}"
Margin="2,4" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Window>
@@ -0,0 +1,105 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using Microsoft.SharePoint.Client;
using SharepointToolbox.Services;
namespace SharepointToolbox.Views.Dialogs;
public partial class LibraryPickerDialog : Window
{
private readonly ClientContext _ctx;
private readonly IVersionCleanupService _libraryLister;
private readonly ObservableCollection<LibraryItem> _items = new();
public IReadOnlyList<string> SelectedLibraryTitles { get; private set; } = Array.Empty<string>();
public LibraryPickerDialog(
ClientContext ctx,
IVersionCleanupService libraryLister,
IReadOnlyCollection<string>? preselected = null)
{
InitializeComponent();
_ctx = ctx;
_libraryLister = libraryLister;
LibrariesList.ItemsSource = _items;
Loaded += async (_, _) => await LoadAsync(preselected ?? Array.Empty<string>());
}
private async Task LoadAsync(IReadOnlyCollection<string> preselected)
{
try
{
var titles = await _libraryLister.ListLibraryTitlesAsync(_ctx, CancellationToken.None);
var preset = new HashSet<string>(preselected, StringComparer.OrdinalIgnoreCase);
foreach (var t in titles)
{
var item = new LibraryItem { Title = t, IsSelected = preset.Contains(t) };
item.PropertyChanged += OnItemChanged;
_items.Add(item);
}
StatusText.Text = string.Format(
Localization.TranslationSource.Instance["librarypicker.loaded"],
_items.Count);
UpdateOkEnabled();
}
catch (Exception ex)
{
StatusText.Text = $"Error: {ex.Message}";
}
}
private void OnItemChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LibraryItem.IsSelected)) UpdateOkEnabled();
}
private void UpdateOkEnabled()
=> OkButton.IsEnabled = _items.Any(i => i.IsSelected);
private void SelectAll_Click(object sender, RoutedEventArgs e)
{
foreach (var i in _items) i.IsSelected = true;
}
private void SelectNone_Click(object sender, RoutedEventArgs e)
{
foreach (var i in _items) i.IsSelected = false;
}
private void Ok_Click(object sender, RoutedEventArgs e)
{
SelectedLibraryTitles = _items.Where(i => i.IsSelected).Select(i => i.Title).ToList();
DialogResult = true;
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
protected override void OnClosed(EventArgs e)
{
foreach (var i in _items) i.PropertyChanged -= OnItemChanged;
base.OnClosed(e);
}
public class LibraryItem : INotifyPropertyChanged
{
private bool _isSelected;
public string Title { get; init; } = string.Empty;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value) return;
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
}
@@ -20,6 +20,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Profile list -->
@@ -58,8 +59,31 @@
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.clientid.hint]}" />
</Grid>
<!-- Profile CRUD buttons (placed under fields for natural flow) -->
<Grid Grid.Row="3" Margin="0,4,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.add]}"
Command="{Binding AddCommand}"
MinWidth="90" Padding="10,4" Margin="0,0,6,0"
ToolTip="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.add.tooltip]}" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.save]}"
Command="{Binding SaveCommand}"
MinWidth="90" Padding="10,4"
ToolTip="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.save.tooltip]}" />
</StackPanel>
<Button Grid.Column="1" Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.delete]}"
Command="{Binding DeleteCommand}"
MinWidth="90" Padding="10,4"
Foreground="{DynamicResource DangerBrush}"
ToolTip="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.delete.tooltip]}" />
</Grid>
<!-- Client Logo -->
<StackPanel Grid.Row="3" Margin="0,8,0,8">
<StackPanel Grid.Row="4" 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="{DynamicResource BorderSoftBrush}" BorderThickness="1" Padding="8" CornerRadius="4"
HorizontalAlignment="Left" MinWidth="200" MinHeight="50">
@@ -96,7 +120,7 @@
</StackPanel>
<!-- App Registration -->
<StackPanel Grid.Row="4" Margin="0,8,0,8">
<StackPanel Grid.Row="5" Margin="0,8,0,8">
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.title]}"
FontWeight="SemiBold" Padding="0,0,0,4"
Visibility="{Binding ShowFallbackInstructions, Converter={StaticResource BooleanToVisibilityConverter}}" />
@@ -134,16 +158,10 @@
</Border>
</StackPanel>
<!-- Buttons -->
<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.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" />
<!-- Close button -->
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[common.close]}"
Width="60" Margin="4,0"
MinWidth="80" Padding="10,4"
Click="CloseButton_Click" IsCancel="True" />
</StackPanel>
</Grid>
@@ -9,6 +9,15 @@ public partial class ProfileManagementDialog : Window
{
InitializeComponent();
DataContext = viewModel;
viewModel.ConfirmRegisterApp = msg =>
{
var result = MessageBox.Show(
this, msg,
Localization.TranslationSource.Instance["profile.register"],
MessageBoxButton.OKCancel,
MessageBoxImage.Information);
return result == MessageBoxResult.OK;
};
Loaded += async (_, _) => await viewModel.LoadAsync();
}
@@ -0,0 +1,118 @@
<UserControl x:Class="SharepointToolbox.Views.Tabs.VersionCleanupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
<DockPanel LastChildFill="True">
<ScrollViewer DockPanel.Dock="Left" Width="280" VerticalScrollBarVisibility="Auto" Margin="8,8,4,8">
<StackPanel>
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.grp.libs]}"
Margin="0,0,0,8">
<StackPanel Margin="4">
<TextBlock Text="{Binding SelectedLibrariesLabel}" Margin="0,0,0,6"
Foreground="{DynamicResource TextMutedBrush}" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.btn.pickLibs]}"
Command="{Binding SelectLibrariesCommand}" Height="26" Margin="0,0,0,4" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.btn.clearLibs]}"
Command="{Binding ClearLibrariesCommand}" Height="26" />
</StackPanel>
</GroupBox>
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.grp.policy]}"
Margin="0,0,0,8">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,2">
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.lbl.keepLast]}"
VerticalAlignment="Center" Padding="0,0,4,0" />
<TextBox Text="{Binding KeepLast, UpdateSourceTrigger=PropertyChanged}"
Width="50" Height="22" VerticalAlignment="Center" />
</StackPanel>
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.chk.keepFirst]}"
IsChecked="{Binding KeepFirstVersion}" Margin="0,4,0,2" />
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.chk.confirm]}"
IsChecked="{Binding ConfirmDelete}" Margin="0,4,0,2" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.note]}"
TextWrapping="Wrap" FontSize="11" Foreground="{DynamicResource TextMutedBrush}"
Margin="0,6,0,0" />
</StackPanel>
</GroupBox>
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.btn.run]}"
Command="{Binding RunCommand}" Height="28" Margin="0,0,0,4" />
<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=[audit.btn.exportCsv]}"
Command="{Binding ExportCsvCommand}" Height="28" Margin="0,0,0,4" />
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" FontSize="11"
Foreground="{DynamicResource TextMutedBrush}" Margin="0,6" />
</StackPanel>
</ScrollViewer>
<Grid Margin="4,8,8,8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<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" />
<Style.Triggers>
<DataTrigger Binding="{Binding HasResults}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Margin="0,0,24,0">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.summary.files]}"
FontWeight="SemiBold" />
<TextBlock Text="{Binding TotalFilesAffected, StringFormat=N0, Mode=OneWay}" Margin="4,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,24,0">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.summary.deleted]}"
FontWeight="SemiBold" />
<TextBlock Text="{Binding TotalVersionsDeleted, StringFormat=N0, Mode=OneWay}" Margin="4,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.summary.freed]}"
FontWeight="SemiBold" />
<TextBlock Text="{Binding TotalBytesFreed, Converter={StaticResource BytesConverter}, Mode=OneWay}"
Margin="4,0,0,0" />
</StackPanel>
</StackPanel>
</Border>
<DataGrid Grid.Row="1" ItemsSource="{Binding Results}" IsReadOnly="True" AutoGenerateColumns="False"
VirtualizingPanel.IsVirtualizing="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.library]}"
Binding="{Binding Library}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.file]}"
Binding="{Binding FileName}" Width="200" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.before]}"
Binding="{Binding VersionsBefore, StringFormat=N0}" Width="80"
ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.deleted]}"
Binding="{Binding VersionsDeleted, StringFormat=N0}" Width="80"
ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.remaining]}"
Binding="{Binding VersionsRemaining, StringFormat=N0}" Width="90"
ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.freed]}"
Binding="{Binding BytesFreed, Converter={StaticResource BytesConverter}}" Width="100"
ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.path]}"
Binding="{Binding FileServerRelativeUrl}" Width="*" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[versions.col.error]}"
Binding="{Binding Error}" Width="160" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</DockPanel>
</UserControl>
@@ -0,0 +1,54 @@
using System.Windows;
using System.Windows.Controls;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
using SharepointToolbox.Views.Dialogs;
namespace SharepointToolbox.Views.Tabs;
public partial class VersionCleanupView : UserControl
{
private readonly ViewModels.Tabs.VersionCleanupViewModel _viewModel;
private readonly ISessionManager _sessionManager;
private readonly IVersionCleanupService _versionService;
public VersionCleanupView(
ViewModels.Tabs.VersionCleanupViewModel viewModel,
ISessionManager sessionManager,
IVersionCleanupService versionService)
{
InitializeComponent();
_viewModel = viewModel;
_sessionManager = sessionManager;
_versionService = versionService;
DataContext = viewModel;
viewModel.PickLibrariesAsync = async (siteUrl, preselected) =>
{
if (viewModel.CurrentProfile == null) return null;
var profile = new TenantProfile
{
TenantUrl = siteUrl,
ClientId = viewModel.CurrentProfile.ClientId,
Name = viewModel.CurrentProfile.Name,
};
var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None);
var dlg = new LibraryPickerDialog(ctx, _versionService, preselected)
{
Owner = Window.GetWindow(this)
};
if (dlg.ShowDialog() != true) return null;
return dlg.SelectedLibraryTitles;
};
viewModel.ConfirmAction = msg =>
{
var result = MessageBox.Show(
Window.GetWindow(this), msg,
Localization.TranslationSource.Instance["versions.tab"],
MessageBoxButton.OKCancel,
MessageBoxImage.Warning);
return result == MessageBoxResult.OK;
};
}
}