docs: create milestone v2.3 roadmap (5 phases, 15-19)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-09 11:14:03 +02:00
parent d967a8bb65
commit e3ff27a673
60 changed files with 1138 additions and 35 deletions

View File

@@ -0,0 +1,275 @@
---
phase: 14-user-directory-view
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- SharepointToolbox/Localization/Strings.resx
- SharepointToolbox/Localization/Strings.fr.resx
- SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
- SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs
- SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs
autonomous: true
requirements:
- UDIR-05
- UDIR-01
must_haves:
truths:
- "SelectDirectoryUserCommand takes a GraphDirectoryUser, converts it to GraphUserResult, adds it to SelectedUsers via existing logic"
- "After SelectDirectoryUserCommand, the user appears in SelectedUsers and can be audited with RunCommand"
- "SelectDirectoryUserCommand does not add duplicates (same UPN check as existing AddUserCommand)"
- "Localization keys for directory UI exist in both EN and FR resource files"
- "Code-behind has a DirectoryDataGrid_MouseDoubleClick handler that invokes SelectDirectoryUserCommand"
artifacts:
- path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
provides: "SelectDirectoryUserCommand bridging directory selection to audit pipeline"
contains: "SelectDirectoryUserCommand"
- path: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs"
provides: "Event handler for directory DataGrid double-click"
contains: "DirectoryDataGrid_MouseDoubleClick"
- path: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs"
provides: "Tests for SelectDirectoryUserCommand"
contains: "SelectDirectoryUser"
key_links:
- from: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
to: "SharepointToolbox/Core/Models/GraphDirectoryUser.cs"
via: "command parameter type"
pattern: "GraphDirectoryUser"
- from: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs"
to: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
via: "command invocation"
pattern: "SelectDirectoryUserCommand"
---
<objective>
Add localization keys for directory UI, the SelectDirectoryUserCommand that bridges directory selection to the audit pipeline, and a code-behind event handler for DataGrid double-click.
Purpose: Provides the infrastructure (localization, command, event handler) that Plan 14-02 needs to build the XAML view. SC2 requires selecting a directory user to trigger an audit — this command makes that possible.
Output: Localization keys (EN+FR), SelectDirectoryUserCommand with tests, code-behind event handler.
</objective>
<execution_context>
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/14-user-directory-view/14-RESEARCH.md
<interfaces>
<!-- Current ViewModel command pattern (AddUserCommand) -->
From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs:
```csharp
public RelayCommand<GraphUserResult> AddUserCommand { get; }
private void ExecuteAddUser(GraphUserResult? user)
{
if (user == null) return;
if (!SelectedUsers.Any(u => u.UserPrincipalName == user.UserPrincipalName))
{
SelectedUsers.Add(user);
}
SearchQuery = string.Empty;
SearchResults.Clear();
}
```
<!-- GraphDirectoryUser record -->
From SharepointToolbox/Core/Models/GraphDirectoryUser.cs:
```csharp
public record GraphDirectoryUser(
string DisplayName, string UserPrincipalName,
string? Mail, string? Department, string? JobTitle, string? UserType);
```
<!-- GraphUserResult record -->
From SharepointToolbox/Services/IGraphUserSearchService.cs:
```csharp
public record GraphUserResult(string DisplayName, string UserPrincipalName, string? Mail);
```
<!-- Existing code-behind pattern -->
From SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs:
```csharp
private void SearchResultsListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox && listBox.SelectedItem is GraphUserResult user)
{
var vm = (UserAccessAuditViewModel)DataContext;
if (vm.AddUserCommand.CanExecute(user))
vm.AddUserCommand.Execute(user);
listBox.SelectedItem = null;
}
}
```
<!-- Existing localization key pattern -->
From Strings.resx:
```xml
<data name="audit.grp.users" xml:space="preserve"><value>Select Users</value></data>
<data name="audit.btn.run" xml:space="preserve"><value>Run Audit</value></data>
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add localization keys for directory UI (EN + FR)</name>
<files>
SharepointToolbox/Localization/Strings.resx,
SharepointToolbox/Localization/Strings.fr.resx
</files>
<behavior>
- Both resx files contain matching keys for directory browse UI
</behavior>
<action>
1. Add to `Strings.resx` (EN):
- `audit.mode.search` = "Search"
- `audit.mode.browse` = "Browse Directory"
- `directory.grp.browse` = "User Directory"
- `directory.btn.load` = "Load Directory"
- `directory.btn.cancel` = "Cancel"
- `directory.filter.placeholder` = "Filter users..."
- `directory.chk.guests` = "Include guests"
- `directory.status.count` = "users"
- `directory.hint.doubleclick` = "Double-click a user to add to audit"
- `directory.col.name` = "Name"
- `directory.col.upn` = "Email"
- `directory.col.department` = "Department"
- `directory.col.jobtitle` = "Job Title"
- `directory.col.type` = "Type"
2. Add to `Strings.fr.resx` (FR):
- `audit.mode.search` = "Recherche"
- `audit.mode.browse` = "Parcourir l'annuaire"
- `directory.grp.browse` = "Annuaire utilisateurs"
- `directory.btn.load` = "Charger l'annuaire"
- `directory.btn.cancel` = "Annuler"
- `directory.filter.placeholder` = "Filtrer les utilisateurs..."
- `directory.chk.guests` = "Inclure les invités"
- `directory.status.count` = "utilisateurs"
- `directory.hint.doubleclick` = "Double-cliquez sur un utilisateur pour l'ajouter à l'audit"
- `directory.col.name` = "Nom"
- `directory.col.upn` = "Courriel"
- `directory.col.department` = "Département"
- `directory.col.jobtitle` = "Poste"
- `directory.col.type` = "Type"
</action>
<verify>
<automated>dotnet build --no-restore -warnaserror</automated>
</verify>
<done>14 localization keys present in both EN and FR resource files.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Add SelectDirectoryUserCommand to ViewModel</name>
<files>
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs,
SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs
</files>
<behavior>
- SelectDirectoryUserCommand is a RelayCommand<GraphDirectoryUser>
- It converts GraphDirectoryUser to GraphUserResult and adds to SelectedUsers
- Duplicate UPN check (same as AddUserCommand)
- Does NOT clear SearchQuery/SearchResults (not in search mode context)
- After execution, IsBrowseMode stays true — user can continue selecting from directory
</behavior>
<action>
1. Add command declaration in ViewModel:
```csharp
public RelayCommand<GraphDirectoryUser> SelectDirectoryUserCommand { get; }
```
2. Initialize in BOTH constructors:
```csharp
SelectDirectoryUserCommand = new RelayCommand<GraphDirectoryUser>(ExecuteSelectDirectoryUser);
```
3. Implement the command method:
```csharp
private void ExecuteSelectDirectoryUser(GraphDirectoryUser? dirUser)
{
if (dirUser == null) return;
var userResult = new GraphUserResult(dirUser.DisplayName, dirUser.UserPrincipalName, dirUser.Mail);
if (!SelectedUsers.Any(u => u.UserPrincipalName == userResult.UserPrincipalName))
{
SelectedUsers.Add(userResult);
}
}
```
4. Add tests to `UserAccessAuditViewModelDirectoryTests.cs`:
- Test: SelectDirectoryUserCommand adds user to SelectedUsers
- Test: SelectDirectoryUserCommand skips duplicates
- Test: SelectDirectoryUserCommand with null does nothing
- Test: After SelectDirectoryUser, user can be audited with RunCommand (integration: add user + check SelectedUsers.Count > 0)
</action>
<verify>
<automated>dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~UserAccessAuditViewModelDirectory" --no-build -q</automated>
</verify>
<done>SelectDirectoryUserCommand bridges directory selection to audit pipeline. Tests pass.</done>
</task>
<task type="auto">
<name>Task 3: Add code-behind event handler for directory DataGrid</name>
<files>
SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs
</files>
<behavior>
- DirectoryDataGrid_MouseDoubleClick handler extracts the clicked GraphDirectoryUser
- Invokes SelectDirectoryUserCommand with the selected user
- Uses the same pattern as SearchResultsListBox_SelectionChanged
</behavior>
<action>
1. Add to `UserAccessAuditView.xaml.cs`:
```csharp
private void DirectoryDataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (sender is DataGrid grid && grid.SelectedItem is GraphDirectoryUser user)
{
var vm = (UserAccessAuditViewModel)DataContext;
if (vm.SelectDirectoryUserCommand.CanExecute(user))
vm.SelectDirectoryUserCommand.Execute(user);
}
}
```
2. Add the required using statement if not present:
```csharp
using System.Windows.Controls; // Already present
using SharepointToolbox.Core.Models; // For GraphDirectoryUser
```
</action>
<verify>
<automated>dotnet build --no-restore -warnaserror</automated>
</verify>
<done>Code-behind event handler exists, ready to be wired in XAML (Plan 14-02).</done>
</task>
</tasks>
<verification>
```bash
dotnet build --no-restore -warnaserror
dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~UserAccessAuditViewModelDirectory" --no-build -q
```
Both must pass with zero failures.
</verification>
<success_criteria>
- 14 localization keys in both EN and FR resx files
- SelectDirectoryUserCommand converts GraphDirectoryUser → GraphUserResult → SelectedUsers
- Duplicate UPN check prevents adding same user twice
- Code-behind event handler for DataGrid double-click
- All tests pass, build clean
</success_criteria>
<output>
After completion, create `.planning/phases/14-user-directory-view/14-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,338 @@
---
phase: 14-user-directory-view
plan: 02
type: execute
wave: 2
depends_on: [14-01]
files_modified:
- SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml
autonomous: true
requirements:
- UDIR-05
- UDIR-01
must_haves:
truths:
- "The left panel shows a mode toggle (two RadioButtons: Search / Browse Directory) at the top"
- "When Search mode is selected (IsBrowseMode=false), the existing people-picker GroupBox is visible and the directory panel is collapsed"
- "When Browse mode is selected (IsBrowseMode=true), the directory panel is visible and the people-picker GroupBox is collapsed"
- "The Scan Options GroupBox and Run/Export buttons remain visible in both modes"
- "The directory panel contains: Load Directory button, Cancel button, Include guests checkbox, filter TextBox, status text, user count, and a DataGrid"
- "The DataGrid is bound to DirectoryUsersView with columns: Name, Email, Department, Job Title, Type"
- "The DataGrid has MouseDoubleClick wired to DirectoryDataGrid_MouseDoubleClick code-behind handler"
- "While loading, the status text shows DirectoryLoadStatus and Load button is disabled"
- "A hint text tells users to double-click to add a user to the audit"
- "The SelectedUsers ItemsControl remains visible in both modes (users added from directory appear here)"
artifacts:
- path: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"
provides: "Complete directory browse UI with mode toggle, directory DataGrid, and loading UX"
contains: "DirectoryUsersView"
key_links:
- from: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"
to: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
via: "data binding"
pattern: "IsBrowseMode|DirectoryUsersView|LoadDirectoryCommand|DirectoryFilterText|IncludeGuests"
---
<objective>
Add the complete directory browse UI to UserAccessAuditView.xaml with mode toggle, directory DataGrid, loading indicators, and seamless integration with the existing audit workflow.
Purpose: SC1-SC4 require visible UI for mode switching, directory display, loading progress, and cancellation. This plan wires all Phase 13 ViewModel properties to the View layer.
Output: Updated UserAccessAuditView.xaml with full directory browse mode.
</objective>
<execution_context>
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/14-user-directory-view/14-RESEARCH.md
<interfaces>
<!-- ViewModel bindings available (Phase 13 + 14-01) -->
From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs:
```csharp
// Mode toggle
public bool IsBrowseMode { get; set; }
// Directory data
public ObservableCollection<GraphDirectoryUser> DirectoryUsers { get; }
public ICollectionView DirectoryUsersView { get; } // filtered + sorted
public int DirectoryUserCount { get; } // computed filtered count
// Directory commands
public IAsyncRelayCommand LoadDirectoryCommand { get; }
public RelayCommand CancelDirectoryLoadCommand { get; }
public RelayCommand<GraphDirectoryUser> SelectDirectoryUserCommand { get; }
// Directory state
public bool IsLoadingDirectory { get; }
public string DirectoryLoadStatus { get; }
public bool IncludeGuests { get; set; }
public string DirectoryFilterText { get; set; }
// Existing (still visible in both modes)
public ObservableCollection<GraphUserResult> SelectedUsers { get; }
public string SelectedUsersLabel { get; }
public IAsyncRelayCommand RunCommand { get; }
public RelayCommand CancelCommand { get; }
public IAsyncRelayCommand ExportCsvCommand { get; }
public IAsyncRelayCommand ExportHtmlCommand { get; }
```
<!-- Available converters from App.xaml -->
- `{StaticResource BoolToVisibilityConverter}` — true→Visible, false→Collapsed
- `{StaticResource InverseBoolConverter}` — inverts bool
- `{StaticResource StringToVisibilityConverter}` — non-empty→Visible
<!-- Current left panel structure -->
```
DockPanel (290px, Margin 8)
├── GroupBox "Select Users" (DockPanel.Dock="Top") — SEARCH MODE (hide when IsBrowseMode)
│ └── SearchQuery, SearchResults, SelectedUsers, SelectedUsersLabel
├── GroupBox "Scan Options" (DockPanel.Dock="Top") — ALWAYS VISIBLE
│ └── CheckBoxes
└── StackPanel (DockPanel.Dock="Top") — ALWAYS VISIBLE
└── Run/Cancel/Export buttons
```
<!-- Code-behind handler (from 14-01) -->
```csharp
private void DirectoryDataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is DataGrid grid && grid.SelectedItem is GraphDirectoryUser user)
{
var vm = (UserAccessAuditViewModel)DataContext;
if (vm.SelectDirectoryUserCommand.CanExecute(user))
vm.SelectDirectoryUserCommand.Execute(user);
}
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Restructure left panel with mode toggle and conditional panels</name>
<files>
SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml
</files>
<behavior>
- At the top of the left panel DockPanel, a mode toggle section appears with two RadioButtons
- RadioButton "Search" is checked when IsBrowseMode=false (uses InverseBoolConverter)
- RadioButton "Browse Directory" is checked when IsBrowseMode=true
- Below the toggle: existing Search GroupBox (visible when IsBrowseMode=false) OR new Directory GroupBox (visible when IsBrowseMode=true)
- SelectedUsers ItemsControl + label extracted from Search GroupBox and placed in a shared section visible in BOTH modes
- Scan Options GroupBox and buttons remain always visible
- Directory GroupBox contains:
a) Two-button grid: Load Directory + Cancel (like Run/Cancel pattern)
b) CheckBox for IncludeGuests
c) Filter TextBox bound to DirectoryFilterText
d) Status/count row: DirectoryLoadStatus + DirectoryUserCount
e) DataGrid bound to DirectoryUsersView with 5 columns (Name, Email, Department, Job Title, Type)
f) Hint text: "Double-click a user to add to audit"
- DataGrid has MouseDoubleClick="DirectoryDataGrid_MouseDoubleClick"
- DataGrid uses AutoGenerateColumns="False", IsReadOnly="True", virtualization enabled
- DataGrid columns are DataGridTextColumn (simple text, sortable by default)
- Guest users highlighted with a subtle "Guest" badge in the Type column (orange, like the existing UserAccessAuditView pattern)
</behavior>
<action>
1. Read the current `UserAccessAuditView.xaml` to get the exact current content.
2. Replace the left panel DockPanel content with the new structure:
```xml
<!-- Mode toggle -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
<RadioButton Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.mode.search]}"
IsChecked="{Binding IsBrowseMode, Converter={StaticResource InverseBoolConverter}}"
Margin="0,0,12,0" />
<RadioButton Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.mode.browse]}"
IsChecked="{Binding IsBrowseMode}" />
</StackPanel>
<!-- SEARCH MODE PANEL (visible when IsBrowseMode=false) -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.users]}"
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8"
Visibility="{Binding IsBrowseMode, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}">
<!-- Keep existing SearchQuery, SearchResults, but move SelectedUsers OUT -->
<StackPanel>
<TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,2" />
<!-- Searching indicator -->
<TextBlock Text="Searching..." FontStyle="Italic" FontSize="10" Foreground="Gray" Margin="0,0,0,2">
[existing DataTrigger style]
</TextBlock>
<!-- Search results dropdown -->
<ListBox x:Name="SearchResultsListBox" [existing bindings] />
</StackPanel>
</GroupBox>
<!-- BROWSE MODE PANEL (visible when IsBrowseMode=true) -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.grp.browse]}"
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8"
Visibility="{Binding IsBrowseMode, Converter={StaticResource BoolToVisibilityConverter}}">
<DockPanel>
<!-- Load/Cancel buttons -->
<Grid DockPanel.Dock="Top" Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.btn.load]}"
Command="{Binding LoadDirectoryCommand}" Margin="0,0,4,0" Padding="6,3" />
<Button Grid.Column="1"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.btn.cancel]}"
Command="{Binding CancelDirectoryLoadCommand}" Padding="6,3" />
</Grid>
<!-- Include guests checkbox -->
<CheckBox DockPanel.Dock="Top"
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.chk.guests]}"
IsChecked="{Binding IncludeGuests}" Margin="0,0,0,4" />
<!-- Filter text -->
<TextBox DockPanel.Dock="Top"
Text="{Binding DirectoryFilterText, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,4" />
<!-- 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"
Margin="0,0,8,0" />
<TextBlock FontSize="10" Foreground="Gray">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="DirectoryUserCount" />
<Binding Source="{x:Static loc:TranslationSource.Instance}" Path="[directory.status.count]" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<!-- 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"
TextWrapping="Wrap" />
<!-- Directory DataGrid -->
<DataGrid x:Name="DirectoryDataGrid"
ItemsSource="{Binding DirectoryUsersView}"
AutoGenerateColumns="False" IsReadOnly="True"
VirtualizingPanel.IsVirtualizing="True" EnableRowVirtualization="True"
MouseDoubleClick="DirectoryDataGrid_MouseDoubleClick"
CanUserSortColumns="True"
SelectionMode="Single" SelectionUnit="FullRow"
HeadersVisibility="Column" GridLinesVisibility="Horizontal"
BorderThickness="1" BorderBrush="#DDDDDD">
<DataGrid.Columns>
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.name]}"
Binding="{Binding DisplayName}" Width="120" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.upn]}"
Binding="{Binding UserPrincipalName}" Width="140" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.department]}"
Binding="{Binding Department}" Width="90" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.jobtitle]}"
Binding="{Binding JobTitle}" Width="90" />
<DataGridTemplateColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.type]}" Width="60">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding UserType}" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding UserType}" Value="Guest">
<Setter Property="Foreground" Value="#F39C12" />
<Setter Property="FontWeight" Value="SemiBold" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</GroupBox>
<!-- SHARED: Selected users (visible in both modes) -->
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,8">
<ItemsControl ItemsSource="{Binding SelectedUsers}" Margin="0,0,0,4">
[existing ItemTemplate with blue border badges + x remove button]
</ItemsControl>
<TextBlock Text="{Binding SelectedUsersLabel}" FontStyle="Italic" Foreground="Gray" FontSize="10" />
</StackPanel>
<!-- Scan Options GroupBox (unchanged, always visible) -->
<GroupBox Header="..." DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8">
[existing checkboxes]
</GroupBox>
<!-- Run/Export buttons (unchanged, always visible) -->
<StackPanel DockPanel.Dock="Top">
[existing button grids]
</StackPanel>
```
IMPORTANT NOTES:
- The `BoolToVisibilityConverter` natively shows when true. For the Search panel (show when IsBrowseMode=false), we need inverse behavior. Two approaches:
a) Use a DataTrigger-based Style on Visibility (reliable)
b) Check if BoolToVisibilityConverter supports a ConverterParameter for inversion
Since we're not sure the converter supports inversion, use DataTrigger approach for the Search panel:
```xml
<GroupBox.Style>
<Style TargetType="GroupBox">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsBrowseMode}" Value="True">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</GroupBox.Style>
```
And for the Browse panel, use `BoolToVisibilityConverter` directly (shows when IsBrowseMode=true).
- The SelectedUsers ItemsControl must be EXTRACTED from the Search GroupBox and placed in a standalone section — it needs to remain visible when in Browse mode so users can see who they've selected from the directory.
- DataGrid column headers use localized bindings. Note: DataGridTextColumn.Header does NOT support binding in standard WPF — it's not a FrameworkElement. Instead, use DataGridTemplateColumn with HeaderTemplate for localized headers, OR set the Header as a plain string and skip localization for column headers (simpler approach). DECISION: Use plain English headers for DataGrid columns (they are technical column names that don't benefit from localization as much). This avoids the complex HeaderTemplate pattern. Use the localization keys in other UI elements.
Alternative if Header binding works (some WPF versions support it via x:Static): Test with `Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.col.name]}"` — if it compiles and works, great. If not, fall back to plain strings.
</action>
<verify>
<automated>dotnet build --no-restore -warnaserror</automated>
</verify>
<done>UserAccessAuditView has full directory browse UI with mode toggle, conditional panels, directory DataGrid, loading status, and double-click selection. Build passes.</done>
</task>
</tasks>
<verification>
```bash
dotnet build --no-restore -warnaserror
```
Build must pass. Visual verification requires manual testing.
</verification>
<success_criteria>
- SC1: Mode toggle (RadioButtons) visibly switches left panel between search and browse
- SC2: DataGrid double-click adds user to SelectedUsers; Run Audit button works as usual
- SC3: Loading status shows DirectoryLoadStatus, Load button disabled while loading, Cancel button active
- SC4: Cancel clears loading state; status returns to ready; no broken UI
- SelectedUsers visible in both modes
- DataGrid columns: Name, Email, Department, Job Title, Type (Guest highlighted in orange)
- Filter TextBox and IncludeGuests checkbox functional
- Build passes with zero warnings
</success_criteria>
<output>
After completion, create `.planning/phases/14-user-directory-view/14-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,47 @@
# Phase 14 Research: User Directory View
## What Exists (Phase 13 Deliverables)
### ViewModel Properties for Directory Browse
- `IsBrowseMode` (bool) — toggles Search/Browse mode
- `DirectoryUsers` (ObservableCollection<GraphDirectoryUser>) — raw directory list
- `DirectoryUsersView` (ICollectionView) — filtered/sorted view, default sort DisplayName asc
- `IsLoadingDirectory` (bool) — true while loading
- `DirectoryLoadStatus` (string) — "Loading... X users" progress text
- `IncludeGuests` (bool) — in-memory member/guest filter
- `DirectoryFilterText` (string) — text filter on DisplayName, UPN, Department, JobTitle
- `DirectoryUserCount` (int) — filtered count
- `LoadDirectoryCommand` (IAsyncRelayCommand) — disabled while loading
- `CancelDirectoryLoadCommand` (RelayCommand) — enabled only while loading
### Existing People-Picker (Search Mode)
- `SearchQuery` → debounced Graph search → `SearchResults` dropdown
- `AddUserCommand(GraphUserResult)``SelectedUsers` collection
- `RemoveUserCommand(GraphUserResult)` → removes from SelectedUsers
- `RunCommand``RunOperationAsync` → audits SelectedUsers against GlobalSites
### GAP: No SelectDirectoryUserCommand
SC2 requires "selecting a user from directory list launches existing audit pipeline."
Need a command that:
1. Takes a `GraphDirectoryUser` from the directory DataGrid
2. Converts it to `GraphUserResult` (same DisplayName + UPN)
3. Adds to `SelectedUsers` via existing `ExecuteAddUser` logic
This is ViewModel work — needs to be done before the View XAML.
### Current View Structure (UserAccessAuditView.xaml)
- Left panel (290px DockPanel): Users GroupBox + Options GroupBox + Buttons StackPanel
- Right panel: Summary banners + Filter/Toggle row + DataGrid (ResultsView)
- Status bar: ProgressBar + StatusMessage
### Available Converters
- `BoolToVisibilityConverter` — true→Visible, false→Collapsed
- `InverseBoolConverter` — inverts bool
- `StringToVisibilityConverter` — non-empty→Visible, empty→Collapsed
### Localization
- No directory.* keys exist — need to add ~10 keys for EN + FR
## Plan Breakdown
1. **14-01** (Wave 1): Add localization keys + `SelectDirectoryUserCommand` on ViewModel + code-behind event handler
2. **14-02** (Wave 2): Full XAML changes — mode toggle, conditional Search/Browse panels, directory DataGrid, loading UX

View File

@@ -0,0 +1,98 @@
---
phase: 14-user-directory-view
verified: 2026-04-09T12:00:00Z
status: passed
score: 4/4 success criteria verified
gaps: []
---
# Phase 14: User Directory View Verification Report
**Phase Goal:** Administrators can toggle into directory browse mode from the user access audit tab, see the paginated user list with filters, and launch an access audit for a selected user.
**Verified:** 2026-04-09
**Status:** passed
**Re-verification:** No -- initial verification
## Goal Achievement
### Observable Truths (Success Criteria)
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | The user access audit tab shows a mode toggle control that visibly switches the left panel between the existing people-picker and the directory browse panel | VERIFIED | XAML lines 19-25: two RadioButtons (Search/Browse Directory) bound to IsBrowseMode via InverseBoolConverter. Search GroupBox uses DataTrigger to collapse when IsBrowseMode=true (lines 32-40). Browse GroupBox uses BoolToVisibilityConverter on IsBrowseMode (line 87). Both converters exist in App.xaml. |
| 2 | In browse mode, selecting a user from the directory list and clicking Run Audit launches the existing audit pipeline for that user | VERIFIED | SelectDirectoryUserCommand (ViewModel line 554-562) converts GraphDirectoryUser to GraphUserResult and adds to SelectedUsers. DataGrid has MouseDoubleClick="DirectoryDataGrid_MouseDoubleClick" (XAML line 141). Code-behind handler (line 29-37) invokes SelectDirectoryUserCommand. RunCommand operates on SelectedUsers (line 244-246). Tests 17-20 confirm the full flow. |
| 3 | While the directory is loading, the panel shows a "Loading... X users" counter and an active cancel button; the load button is disabled to prevent concurrent requests | VERIFIED | LoadDirectoryAsync sets DirectoryLoadStatus="Loading..." then updates via Progress callback "Loading... {count} users" (ViewModel lines 411-415). LoadDirectoryCommand CanExecute = !IsLoadingDirectory (line 192). CancelDirectoryLoadCommand CanExecute = IsLoadingDirectory (line 194). OnIsLoadingDirectoryChanged notifies both commands (lines 378-382). XAML binds status text (line 118) and both buttons (lines 98-103). |
| 4 | When the directory load is cancelled or fails, the panel returns to a ready state with a clear status message and no broken UI | VERIFIED | Cancellation sets DirectoryLoadStatus="Load cancelled." (line 436). Failure sets "Failed: {message}" (line 440). Both paths set IsLoadingDirectory=false in finally block (line 445). Test 7 confirms cancellation flow. Tenant switch resets all directory state (lines 321-331, test 13). |
**Score:** 4/4 success criteria verified
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml` | Directory browse UI with mode toggle, DataGrid, loading UX | VERIFIED | 415 lines, complete implementation with mode toggle, search panel, browse panel, shared SelectedUsers, scan options, run/export buttons |
| `SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs` | Code-behind with DirectoryDataGrid_MouseDoubleClick | VERIFIED | Handler at line 29, extracts GraphDirectoryUser, invokes SelectDirectoryUserCommand |
| `SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs` | SelectDirectoryUserCommand, LoadDirectoryCommand, browse mode state | VERIFIED | 661 lines, all properties/commands present, full implementation (no stubs) |
| `SharepointToolbox/Localization/Strings.resx` | 14 directory localization keys (EN) | VERIFIED | All 14 keys present (audit.mode.search/browse, directory.grp/btn/chk/col/hint/status/filter) |
| `SharepointToolbox/Localization/Strings.fr.resx` | 14 directory localization keys (FR) | VERIFIED | All 14 keys present with French translations |
| `SharepointToolbox/Core/Models/GraphDirectoryUser.cs` | Record with DisplayName, UPN, Mail, Department, JobTitle, UserType | VERIFIED | 6-field record, matches DataGrid column bindings |
| `SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs` | Tests for directory commands and state | VERIFIED | 20 tests, all passing |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: IsBrowseMode | WIRED | RadioButton IsChecked bindings (lines 21, 23), GroupBox visibility (lines 33-39, 87) |
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: DirectoryUsersView | WIRED | DataGrid ItemsSource="{Binding DirectoryUsersView}" (line 138) |
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: LoadDirectoryCommand | WIRED | Button Command="{Binding LoadDirectoryCommand}" (line 100) |
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: CancelDirectoryLoadCommand | WIRED | Button Command="{Binding CancelDirectoryLoadCommand}" (line 102) |
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: DirectoryFilterText | WIRED | TextBox Text="{Binding DirectoryFilterText}" (line 113) |
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: IncludeGuests | WIRED | CheckBox IsChecked="{Binding IncludeGuests}" (line 109) |
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: DirectoryLoadStatus | WIRED | TextBlock Text="{Binding DirectoryLoadStatus}" (line 118) |
| UserAccessAuditView.xaml | UserAccessAuditViewModel.cs | Data binding: DirectoryUserCount | WIRED | MultiBinding with DirectoryUserCount (line 122) |
| UserAccessAuditView.xaml | UserAccessAuditView.xaml.cs | MouseDoubleClick event | WIRED | MouseDoubleClick="DirectoryDataGrid_MouseDoubleClick" (line 141) |
| UserAccessAuditView.xaml.cs | UserAccessAuditViewModel.cs | SelectDirectoryUserCommand | WIRED | Code-behind casts DataContext, invokes command (lines 31-36) |
| UserAccessAuditViewModel.cs | GraphDirectoryUser.cs | Command parameter type | WIRED | RelayCommand<GraphDirectoryUser> (line 140), ExecuteSelectDirectoryUser parameter (line 554) |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|----------|
| UDIR-01 | 14-01, 14-02 | User can toggle between search mode and directory browse mode | SATISFIED | RadioButtons in XAML, IsBrowseMode property, conditional panel visibility |
| UDIR-05 | 14-01, 14-02 | User can select users from directory to run audit | SATISFIED | SelectDirectoryUserCommand, DataGrid double-click, SelectedUsers shared panel |
### Anti-Patterns Found
No anti-patterns detected. No TODO/FIXME/HACK/PLACEHOLDER comments. No empty implementations. No console.log-only handlers.
### Build and Test Results
- **Build:** dotnet build --no-restore -warnaserror: 0 warnings, 0 errors
- **Tests:** 20 passed, 0 failed, 0 skipped (160ms)
### Human Verification Required
### 1. Mode Toggle Visual Behavior
**Test:** Click Browse Directory radio button, verify search panel collapses and directory panel appears. Click Search radio button, verify the reverse.
**Expected:** Clean toggle with no layout jump or overlap. Both panels fully visible/collapsed.
**Why human:** Visual layout and transition smoothness cannot be verified programmatically.
### 2. Directory Load and Cancel UX
**Test:** Click Load Directory, observe loading status updating with user count, then click Cancel before completion.
**Expected:** Status shows "Loading... N users" incrementally, Cancel button is active during load, Load button is disabled. After cancel: "Load cancelled." message, both buttons return to normal state.
**Why human:** Real-time progress display and button enable/disable transitions require visual observation.
### 3. DataGrid Double-Click to Audit Flow
**Test:** Load directory, double-click a user row. Verify user appears in SelectedUsers badges. Click Run Audit.
**Expected:** User badge appears immediately. Audit runs and produces results identical to search-mode selection.
**Why human:** End-to-end flow through actual Graph API and audit pipeline requires running application.
### 4. Guest Highlighting
**Test:** Load directory with Include Guests checked. Find a Guest-type user in the list.
**Expected:** Guest users show "Guest" in orange semi-bold text in the Type column.
**Why human:** Color and font rendering verification.
---
_Verified: 2026-04-09_
_Verifier: Claude (gsd-verifier)_