feat(02-06): implement PermissionsViewModel with multi-site scan and SitePickerDialog

- PermissionsViewModel extends FeatureViewModelBase, implements RunOperationAsync
- Multi-site mode: loops SelectedSites; single-site mode: uses SiteUrl
- ExportCsvCommand and ExportHtmlCommand enabled only when Results.Count > 0
- OpenSitePickerCommand uses dialog factory pattern (Func<Window>?)
- OnTenantSwitched clears Results, SiteUrl, SelectedSites
- Flat ObservableProperty booleans (IncludeInherited, ScanFolders, etc.) build ScanOptions record
- SitePickerDialog XAML: filterable list with CheckBox column, Title, URL columns
- SitePickerDialog code-behind: loads sites on Window.Loaded, exposes SelectedUrls
- ISessionManager interface extracted for testability (SessionManager implements it)
- StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl test passes (60/60 + 3 skip)
This commit is contained in:
Dev
2026-04-02 14:06:39 +02:00
parent c462a0b310
commit f98ca60990
4 changed files with 425 additions and 11 deletions

View File

@@ -0,0 +1,60 @@
<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"
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
Loaded="Window_Loaded">
<Grid Margin="12">
<Grid.RowDefinitions>
<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" />
<TextBox x:Name="FilterBox" TextChanged="FilterBox_TextChanged" />
</DockPanel>
<!-- Site list with checkboxes -->
<ListView x:Name="SiteList" Grid.Row="1" Margin="0,0,0,8"
SelectionMode="Single"
BorderThickness="1" BorderBrush="#CCCCCC">
<ListView.View>
<GridView>
<GridViewColumn Header="" Width="32">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Title" Width="200" DisplayMemberBinding="{Binding Title}" />
<GridViewColumn Header="URL" Width="320" DisplayMemberBinding="{Binding Url}" />
</GridView>
</ListView.View>
</ListView>
<!-- Status text -->
<TextBlock x:Name="StatusText" Grid.Row="2" Margin="0,0,0,8"
Foreground="#555555" FontSize="11" />
<!-- Button row -->
<DockPanel Grid.Row="3">
<Button x:Name="LoadButton" Content="Load Sites" Width="80" Margin="0,0,8,0"
Click="LoadButton_Click" />
<Button Content="Select All" Width="80" Margin="0,0,8,0"
Click="SelectAll_Click" />
<Button Content="Deselect All" Width="80" 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"
Click="OK_Click" />
<Button Content="Cancel" Width="70" Margin="4,0" IsCancel="True" />
</StackPanel>
</DockPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,127 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
namespace SharepointToolbox.Views.Dialogs;
/// <summary>
/// Dialog for selecting multiple SharePoint sites.
/// Loads sites from ISiteListService, shows them in a filterable list with checkboxes.
/// </summary>
public partial class SitePickerDialog : Window
{
private readonly ISiteListService _siteListService;
private readonly TenantProfile _profile;
private List<SitePickerItem> _allItems = new();
/// <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();
public SitePickerDialog(ISiteListService siteListService, TenantProfile profile)
{
InitializeComponent();
_siteListService = siteListService;
_profile = profile;
}
private async void Window_Loaded(object sender, RoutedEventArgs e) => await LoadSitesAsync();
private async Task LoadSitesAsync()
{
StatusText.Text = "Loading sites...";
LoadButton.IsEnabled = false;
try
{
var sites = await _siteListService.GetSitesAsync(
_profile,
new Progress<OperationProgress>(),
System.Threading.CancellationToken.None);
_allItems = sites.Select(s => new SitePickerItem(s.Url, s.Title)).ToList();
ApplyFilter();
StatusText.Text = $"{_allItems.Count} sites loaded.";
}
catch (InvalidOperationException ex)
{
StatusText.Text = ex.Message;
}
catch (Exception ex)
{
StatusText.Text = $"Error: {ex.Message}";
}
finally
{
LoadButton.IsEnabled = true;
}
}
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();
}
private void FilterBox_TextChanged(object sender, TextChangedEventArgs e) => ApplyFilter();
private void SelectAll_Click(object sender, RoutedEventArgs e)
{
foreach (var item in _allItems) item.IsSelected = true;
ApplyFilter();
}
private void DeselectAll_Click(object sender, RoutedEventArgs e)
{
foreach (var item in _allItems) item.IsSelected = false;
ApplyFilter();
}
private async void LoadButton_Click(object sender, RoutedEventArgs e) => await LoadSitesAsync();
private void OK_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
}
/// <summary>
/// Mutable wrapper for a site entry shown in the SitePickerDialog list.
/// Supports two-way CheckBox binding via INotifyPropertyChanged.
/// </summary>
public class SitePickerItem : INotifyPropertyChanged
{
private bool _isSelected;
public string Url { get; init; } = string.Empty;
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;
public SitePickerItem(string url, string title)
{
Url = url;
Title = title;
}
}