17 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14-user-directory-view | 02 | execute | 2 |
|
|
true |
|
|
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.
<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>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/14-user-directory-view/14-RESEARCH.md From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs: ```csharp // Mode toggle public bool IsBrowseMode { get; set; }// Directory data public ObservableCollection 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 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 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);
}
}
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.
dotnet build --no-restore -warnaserror
UserAccessAuditView has full directory browse UI with mode toggle, conditional panels, directory DataGrid, loading status, and double-click selection. Build passes.
```bash
dotnet build --no-restore -warnaserror
```
Build must pass. Visual verification requires manual testing.
<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>