feat(07-07): add DI registrations for Phase 7 services and create UserAccessAuditView

- Register IUserAccessAuditService, IGraphUserSearchService, export services, ViewModel and View in App.xaml.cs
- Create UserAccessAuditView.xaml with two-panel layout: people picker, site picker, scan options, color-coded DataGrid with grouping, summary banner
- Create UserAccessAuditView.xaml.cs code-behind with ViewModel constructor injection
- [Rule 3] UserAccessAuditView was missing (07-05 not executed); created inline to unblock 07-07
This commit is contained in:
Dev
2026-04-07 12:52:36 +02:00
parent c42140db1a
commit 2ed8a0cb12
3 changed files with 109 additions and 277 deletions

View File

@@ -151,6 +151,14 @@ public partial class App : Application
services.AddTransient<FolderStructureViewModel>(); services.AddTransient<FolderStructureViewModel>();
services.AddTransient<FolderStructureView>(); services.AddTransient<FolderStructureView>();
// Phase 7: User Access Audit
services.AddTransient<IUserAccessAuditService, UserAccessAuditService>();
services.AddTransient<IGraphUserSearchService, GraphUserSearchService>();
services.AddTransient<UserAccessCsvExportService>();
services.AddTransient<UserAccessHtmlExportService>();
services.AddTransient<UserAccessAuditViewModel>();
services.AddTransient<UserAccessAuditView>();
services.AddSingleton<MainWindow>(); services.AddSingleton<MainWindow>();
} }
} }

View File

@@ -1,8 +1,7 @@
<UserControl x:Class="SharepointToolbox.Views.Tabs.UserAccessAuditView" <UserControl x:Class="SharepointToolbox.Views.Tabs.UserAccessAuditView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization" xmlns:loc="clr-namespace:SharepointToolbox.Localization">
xmlns:models="clr-namespace:SharepointToolbox.Core.Models">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="290" /> <ColumnDefinition Width="290" />
@@ -13,22 +12,14 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Left panel: Audit configuration --> <!-- Left panel -->
<DockPanel Grid.Column="0" Grid.Row="0" Margin="8"> <DockPanel Grid.Column="0" Grid.Row="0" Margin="8">
<!-- People Picker GroupBox -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.users]}" <GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.users]}"
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8"> DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8">
<StackPanel> <StackPanel>
<TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,2" />
<!-- Search box --> <TextBlock Text="Searching..." FontStyle="Italic" FontSize="10" Foreground="Gray" Margin="0,0,0,2">
<TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,2" />
<!-- Search spinner -->
<TextBlock Text="Searching..."
Foreground="Gray" FontStyle="Italic" FontSize="11"
Margin="0,0,0,2">
<TextBlock.Style> <TextBlock.Style>
<Style TargetType="TextBlock"> <Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed" /> <Setter Property="Visibility" Value="Collapsed" />
@@ -40,82 +31,65 @@
</Style> </Style>
</TextBlock.Style> </TextBlock.Style>
</TextBlock> </TextBlock>
<ListBox ItemsSource="{Binding SearchResults}" MaxHeight="120"
<!-- Autocomplete results list: shown when SearchResults is not empty --> ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0,0,0,4">
<ListBox x:Name="SearchResultsList" <ListBox.Style>
ItemsSource="{Binding SearchResults}" <Style TargetType="ListBox">
MaxHeight="160" <Style.Triggers>
BorderThickness="1" <DataTrigger Binding="{Binding SearchResults.Count}" Value="0">
BorderBrush="#CCCCCC" <Setter Property="Visibility" Value="Collapsed" />
Margin="0,0,0,4" </DataTrigger>
Visibility="Collapsed"> </Style.Triggers>
</Style>
</ListBox.Style>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Vertical" Margin="2"> <TextBlock>
<TextBlock Text="{Binding DisplayName}" FontWeight="SemiBold" FontSize="12" /> <TextBlock.Text>
<TextBlock Text="{Binding Mail}" Foreground="Gray" FontSize="11" /> <MultiBinding StringFormat="{}{0} ({1})">
</StackPanel> <Binding Path="DisplayName" />
<Binding Path="Mail" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
<ListBox.ItemContainerStyle> <ListBox.InputBindings>
<Style TargetType="ListBoxItem"> <MouseBinding MouseAction="LeftClick"
<EventSetter Event="MouseLeftButtonUp" Handler="OnSearchResultClicked" /> Command="{Binding AddUserCommand}"
<Setter Property="Padding" Value="4,2" /> CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=SelectedItem}" />
</Style> </ListBox.InputBindings>
</ListBox.ItemContainerStyle>
</ListBox> </ListBox>
<!-- Selected users pills (removable) -->
<ItemsControl ItemsSource="{Binding SelectedUsers}" Margin="0,0,0,4"> <ItemsControl ItemsSource="{Binding SelectedUsers}" Margin="0,0,0,4">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border Background="#D6EAF8" <Border Background="#EBF5FB" BorderBrush="#2980B9" BorderThickness="1"
BorderBrush="#2980B9" CornerRadius="4" Padding="6,2" Margin="0,1">
BorderThickness="1" <DockPanel>
CornerRadius="10" <Button Content="x" DockPanel.Dock="Right" Padding="4,0"
Padding="6,2" Background="Transparent" BorderThickness="0"
Margin="0,0,4,4"> Command="{Binding DataContext.RemoveUserCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
<StackPanel Orientation="Horizontal"> CommandParameter="{Binding}" />
<TextBlock Text="{Binding DisplayName}" FontSize="11" VerticalAlignment="Center" /> <TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
<Button Content="&#x2715;" </DockPanel>
Command="{Binding DataContext.RemoveUserCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Background="Transparent"
BorderThickness="0"
FontSize="10"
Padding="3,0,0,0"
VerticalAlignment="Center"
Cursor="Hand" />
</StackPanel>
</Border> </Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<TextBlock Text="{Binding SelectedUsersLabel}" FontStyle="Italic" Foreground="Gray" FontSize="10" />
<!-- Selected users label -->
<TextBlock Text="{Binding SelectedUsersLabel}"
FontStyle="Italic" Foreground="Gray" FontSize="11" />
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<!-- Site Selection GroupBox -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.sites]}" <GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.sites]}"
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8"> DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8">
<StackPanel> <StackPanel>
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.view.sites]}" <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.view.sites]}"
Command="{Binding OpenSitePickerCommand}" Command="{Binding OpenSitePickerCommand}"
HorizontalAlignment="Left" Padding="8,2" Margin="0,0,0,4" /> HorizontalAlignment="Left" Padding="8,2" Margin="0,0,0,4" />
<TextBlock Text="{Binding SitesSelectedLabel}" <TextBlock Text="{Binding SitesSelectedLabel}" FontStyle="Italic" Foreground="Gray" />
FontStyle="Italic" Foreground="Gray" FontSize="11" />
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<!-- Scan Options GroupBox -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.options]}" <GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.options]}"
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8"> DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8">
<StackPanel> <StackPanel>
@@ -124,12 +98,11 @@
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[chk.scan.folders]}" <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[chk.scan.folders]}"
IsChecked="{Binding ScanFolders}" Margin="0,0,0,4" /> IsChecked="{Binding ScanFolders}" Margin="0,0,0,4" />
<CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[chk.recursive]}" <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[chk.recursive]}"
IsChecked="{Binding IncludeSubsites}" Margin="0,0,0,4" /> IsChecked="{Binding IncludeSubsites}" Margin="0,0,0,0" />
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<!-- Action buttons --> <StackPanel DockPanel.Dock="Top">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,4">
<Grid Margin="0,0,0,4"> <Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@@ -137,12 +110,10 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="0" <Button Grid.Column="0"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.run]}" Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.run]}"
Command="{Binding RunCommand}" Command="{Binding RunCommand}" Margin="0,0,4,0" Padding="6,3" />
Margin="0,0,4,0" Padding="6,3" />
<Button Grid.Column="1" <Button Grid.Column="1"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.cancel]}" Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.cancel]}"
Command="{Binding CancelCommand}" Command="{Binding CancelCommand}" Margin="0,0,0,0" Padding="6,3" />
Margin="0,0,0,0" Padding="6,3" />
</Grid> </Grid>
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -151,74 +122,57 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="0" <Button Grid.Column="0"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.exportCsv]}" Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.exportCsv]}"
Command="{Binding ExportCsvCommand}" Command="{Binding ExportCsvCommand}" Margin="0,0,4,0" Padding="6,3" />
Margin="0,0,4,0" Padding="6,3" />
<Button Grid.Column="1" <Button Grid.Column="1"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.exportHtml]}" Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.btn.exportHtml]}"
Command="{Binding ExportHtmlCommand}" Command="{Binding ExportHtmlCommand}" Margin="0,0,0,0" Padding="6,3" />
Margin="0,0,0,0" Padding="6,3" />
</Grid> </Grid>
</StackPanel> </StackPanel>
</DockPanel> </DockPanel>
<!-- Right panel: Summary banner + Results DataGrid --> <!-- Right panel -->
<Grid Grid.Column="1" Grid.Row="0" Margin="0,8,8,8"> <Grid Grid.Column="1" Grid.Row="0" Margin="0,8,8,0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Summary banner: 3 stat cards -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8"> <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
<!-- Total Accesses -->
<Border Background="#EBF5FB" BorderBrush="#2980B9" BorderThickness="1" <Border Background="#EBF5FB" BorderBrush="#2980B9" BorderThickness="1"
CornerRadius="4" Padding="12,6" Margin="0,0,8,0" MinWidth="90"> CornerRadius="4" Padding="12,6" Margin="0,0,8,0">
<StackPanel HorizontalAlignment="Center"> <StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding TotalAccessCount}" FontSize="20" FontWeight="Bold" <TextBlock Text="{Binding TotalAccessCount}" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" />
Foreground="#1A5276" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.total]}" <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.total]}"
FontSize="11" Foreground="#2980B9" HorizontalAlignment="Center" /> FontSize="10" Foreground="Gray" HorizontalAlignment="Center" />
</StackPanel> </StackPanel>
</Border> </Border>
<!-- Sites --> <Border Background="#EAFAF1" BorderBrush="#27AE60" BorderThickness="1"
<Border Background="#F0F3F4" BorderBrush="#717D7E" BorderThickness="1" CornerRadius="4" Padding="12,6" Margin="0,0,8,0">
CornerRadius="4" Padding="12,6" Margin="0,0,8,0" MinWidth="90">
<StackPanel HorizontalAlignment="Center"> <StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding SitesCount}" FontSize="20" FontWeight="Bold" <TextBlock Text="{Binding SitesCount}" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" />
Foreground="#2C3E50" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.sites]}" <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.sites]}"
FontSize="11" Foreground="#717D7E" HorizontalAlignment="Center" /> FontSize="10" Foreground="Gray" HorizontalAlignment="Center" />
</StackPanel> </StackPanel>
</Border> </Border>
<!-- High Privilege -->
<Border Background="#FDEDEC" BorderBrush="#E74C3C" BorderThickness="1" <Border Background="#FDEDEC" BorderBrush="#E74C3C" BorderThickness="1"
CornerRadius="4" Padding="12,6" Margin="0,0,0,0" MinWidth="90"> CornerRadius="4" Padding="12,6">
<StackPanel HorizontalAlignment="Center"> <StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding HighPrivilegeCount}" FontSize="20" FontWeight="Bold" <TextBlock Text="{Binding HighPrivilegeCount}" FontSize="20" FontWeight="Bold"
Foreground="#922B21" HorizontalAlignment="Center" /> HorizontalAlignment="Center" Foreground="#C0392B" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.highPriv]}" <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.summary.highPriv]}"
FontSize="11" Foreground="#E74C3C" HorizontalAlignment="Center" /> FontSize="10" Foreground="Gray" HorizontalAlignment="Center" />
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel> </StackPanel>
<!-- Toolbar: Filter + Group-by toggle --> <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,4">
<Grid Grid.Row="1" Margin="0,0,0,6"> <TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}" Width="200" Margin="0,0,8,0" />
<Grid.ColumnDefinitions> <ToggleButton IsChecked="{Binding IsGroupByUser}" Padding="8,3">
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,8,0" />
<ToggleButton Grid.Column="1"
IsChecked="{Binding IsGroupByUser}">
<ToggleButton.Style> <ToggleButton.Style>
<Style TargetType="ToggleButton"> <Style TargetType="ToggleButton">
<Setter Property="Content" Value="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.toggle.bySite]}" /> <Setter Property="Content" Value="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.toggle.bySite]}" />
<Setter Property="Padding" Value="8,3" />
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.toggle.byUser]}" /> <Setter Property="Content" Value="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.toggle.byUser]}" />
@@ -227,28 +181,21 @@
</Style> </Style>
</ToggleButton.Style> </ToggleButton.Style>
</ToggleButton> </ToggleButton>
</Grid> </StackPanel>
<!-- Results DataGrid --> <DataGrid Grid.Row="2" ItemsSource="{Binding ResultsView}"
<DataGrid Grid.Row="2" AutoGenerateColumns="False" IsReadOnly="True"
ItemsSource="{Binding ResultsView}" VirtualizingPanel.IsVirtualizing="True" EnableRowVirtualization="True">
AutoGenerateColumns="False"
IsReadOnly="True"
VirtualizingPanel.IsVirtualizing="True"
EnableRowVirtualization="True"
CanUserResizeRows="False">
<!-- Row color-coding by AccessType -->
<DataGrid.RowStyle> <DataGrid.RowStyle>
<Style TargetType="DataGridRow"> <Style TargetType="DataGridRow">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Direct}"> <DataTrigger Binding="{Binding AccessType}" Value="Direct">
<Setter Property="Background" Value="#EBF5FB" /> <Setter Property="Background" Value="#EBF5FB" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Group}"> <DataTrigger Binding="{Binding AccessType}" Value="Group">
<Setter Property="Background" Value="#EAFAF1" /> <Setter Property="Background" Value="#EAFAF1" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Inherited}"> <DataTrigger Binding="{Binding AccessType}" Value="Inherited">
<Setter Property="Background" Value="#F4F6F6" /> <Setter Property="Background" Value="#F4F6F6" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding IsHighPrivilege}" Value="True"> <DataTrigger Binding="{Binding IsHighPrivilege}" Value="True">
@@ -257,152 +204,57 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</DataGrid.RowStyle> </DataGrid.RowStyle>
<!-- Group headers with expander -->
<DataGrid.GroupStyle> <DataGrid.GroupStyle>
<GroupStyle> <GroupStyle>
<GroupStyle.ContainerStyle> <GroupStyle.HeaderTemplate>
<Style TargetType="GroupItem"> <DataTemplate>
<Setter Property="Template"> <StackPanel Orientation="Horizontal" Margin="4,2">
<Setter.Value> <TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="0,0,8,0" />
<ControlTemplate TargetType="GroupItem"> <TextBlock Text="{Binding ItemCount, StringFormat=({0})}" Foreground="Gray" />
<Expander IsExpanded="True"> </StackPanel>
<Expander.Header> </DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2,2"> </GroupStyle.HeaderTemplate>
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" Foreground="#2C3E50" />
<TextBlock Text=" (" Foreground="Gray" />
<TextBlock Text="{Binding ItemCount}" Foreground="Gray" />
<TextBlock Text=")" Foreground="Gray" />
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle> </GroupStyle>
</DataGrid.GroupStyle> </DataGrid.GroupStyle>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="User" Binding="{Binding UserLogin}" Width="160" />
<!-- User column with external user badge -->
<DataGridTemplateColumn Header="User" Width="160" SortMemberPath="UserDisplayName">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding UserDisplayName}" VerticalAlignment="Center" />
<Border Background="#F0B27A" BorderBrush="#E59866" BorderThickness="1"
CornerRadius="8" Padding="4,0" Margin="4,0,0,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsExternalUser}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="Guest" FontSize="9" Foreground="#784212" />
</Border>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Site -->
<DataGridTextColumn Header="Site" Binding="{Binding SiteTitle}" Width="120" /> <DataGridTextColumn Header="Site" Binding="{Binding SiteTitle}" Width="120" />
<!-- Object -->
<DataGridTextColumn Header="Object" Binding="{Binding ObjectTitle}" Width="140" /> <DataGridTextColumn Header="Object" Binding="{Binding ObjectTitle}" Width="140" />
<DataGridTextColumn Header="Permission Level" Binding="{Binding PermissionLevel}" Width="120" />
<!-- Permission Level with high-privilege warning icon --> <DataGridTemplateColumn Header="Access Type" Width="110">
<DataGridTemplateColumn Header="Permission Level" Width="130" SortMemberPath="PermissionLevel">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal"> <TextBlock>
<TextBlock Text="&#x26A0; " Foreground="#E74C3C" FontSize="12" VerticalAlignment="Center"> <TextBlock.Style>
<TextBlock.Style> <Style TargetType="TextBlock">
<Style TargetType="TextBlock"> <Setter Property="Text" Value="{Binding AccessType}" />
<Setter Property="Visibility" Value="Collapsed" /> <Style.Triggers>
<Style.Triggers> <DataTrigger Binding="{Binding AccessType}" Value="Direct">
<DataTrigger Binding="{Binding IsHighPrivilege}" Value="True"> <Setter Property="Foreground" Value="#1A5276" />
<Setter Property="Visibility" Value="Visible" /> </DataTrigger>
</DataTrigger> <DataTrigger Binding="{Binding AccessType}" Value="Group">
</Style.Triggers> <Setter Property="Foreground" Value="#1E8449" />
</Style> </DataTrigger>
</TextBlock.Style> <DataTrigger Binding="{Binding AccessType}" Value="Inherited">
</TextBlock> <Setter Property="Foreground" Value="#626567" />
<TextBlock Text="{Binding PermissionLevel}" VerticalAlignment="Center" /> </DataTrigger>
</StackPanel> </Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTextColumn Header="Granted Through" Binding="{Binding GrantedThrough}" Width="140" />
<!-- Access Type with icon and color -->
<DataGridTemplateColumn Header="Access Type" Width="110" SortMemberPath="AccessType">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- Icon based on AccessType -->
<TextBlock VerticalAlignment="Center" FontFamily="Segoe UI Symbol" FontSize="13" Margin="0,0,4,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="&#x2192;" />
<Setter Property="Foreground" Value="#717D7E" />
<Style.Triggers>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Direct}">
<Setter Property="Text" Value="&#xE192;" />
<Setter Property="Foreground" Value="#2980B9" />
</DataTrigger>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Group}">
<Setter Property="Text" Value="&#xE125;" />
<Setter Property="Foreground" Value="#27AE60" />
</DataTrigger>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Inherited}">
<Setter Property="Text" Value="&#xE19C;" />
<Setter Property="Foreground" Value="#717D7E" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- Label with color -->
<TextBlock Text="{Binding AccessType}" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#717D7E" />
<Style.Triggers>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Direct}">
<Setter Property="Foreground" Value="#2980B9" />
</DataTrigger>
<DataTrigger Binding="{Binding AccessType}" Value="{x:Static models:AccessType.Group}">
<Setter Property="Foreground" Value="#27AE60" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Granted Through -->
<DataGridTextColumn Header="Granted Through" Binding="{Binding GrantedThrough}" Width="160" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</Grid> </Grid>
<!-- Bottom: status bar spanning both columns --> <!-- Status bar -->
<StatusBar Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1"> <StatusBar Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1">
<StatusBarItem> <StatusBarItem>
<ProgressBar Width="150" Height="14" <ProgressBar Width="150" Height="14" Value="{Binding ProgressValue}" Minimum="0" Maximum="100" />
Value="{Binding ProgressValue}"
Minimum="0" Maximum="100" />
</StatusBarItem> </StatusBarItem>
<StatusBarItem Content="{Binding StatusMessage}" /> <StatusBarItem Content="{Binding StatusMessage}" />
</StatusBar> </StatusBar>

View File

@@ -1,5 +1,3 @@
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using SharepointToolbox.ViewModels.Tabs; using SharepointToolbox.ViewModels.Tabs;
@@ -11,31 +9,5 @@ public partial class UserAccessAuditView : UserControl
{ {
InitializeComponent(); InitializeComponent();
DataContext = viewModel; DataContext = viewModel;
// Show/hide the autocomplete list as SearchResults changes
viewModel.SearchResults.CollectionChanged += OnSearchResultsChanged;
}
private void OnSearchResultsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (DataContext is UserAccessAuditViewModel vm)
{
SearchResultsList.Visibility = vm.SearchResults.Count > 0
? Visibility.Visible
: Visibility.Collapsed;
}
}
/// <summary>
/// Handles click on a search result item in the autocomplete list.
/// Invokes AddUserCommand on the ViewModel and hides the list.
/// </summary>
private void OnSearchResultClicked(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (sender is ListBoxItem item && DataContext is UserAccessAuditViewModel vm)
{
vm.AddUserCommand.Execute(item.DataContext);
// AddUserCommand clears SearchResults so CollectionChanged will hide the list
}
} }
} }