docs: start milestone v2.2 Report Branding & User Directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-08 10:35:27 +02:00
parent fd442f3b4c
commit 8447e78db9
513 changed files with 6496 additions and 946 deletions

View File

@@ -0,0 +1,131 @@
# Phase 6: Global Site Selection - Context
**Gathered:** 2026-04-07
**Status:** Ready for planning
<domain>
## Phase Boundary
Administrators can select one or more target sites once from the toolbar and have every feature tab use that selection by default — eliminating the need to re-enter site URLs on each tab. Individual tabs can override the global selection without clearing the global state.
Requirements: SITE-01, SITE-02
Success Criteria:
1. A multi-site picker control is visible in the main toolbar at all times, regardless of which tab is active
2. Selecting sites in the toolbar causes all feature tabs to default to those sites when an operation is run
3. A user can override the global selection on any individual tab without clearing the global state
4. The global site selection persists across tab switches within the same session
</domain>
<decisions>
## Implementation Decisions
### Toolbar site picker placement
- Add a "Select Sites" button to the existing ToolBar (after the Clear Session button, separated by a Separator)
- Next to the button, show a summary label: "3 site(s) selected" or "No sites selected"
- Clicking the button opens the existing SitePickerDialog pattern (reuse from PermissionsViewModel)
- The picker requires a connected tenant (button disabled when no profile is connected)
### Global selection broadcast
- Create a new `GlobalSitesChangedMessage` (ValueChangedMessage<IReadOnlyList<SiteInfo>>) sent via WeakReferenceMessenger when the toolbar selection changes
- `MainWindowViewModel` owns the global site selection state: `ObservableCollection<SiteInfo> GlobalSelectedSites`
- On tenant switch, clear the global selection (sites belong to a tenant)
### Tab consumption of global selection
- `FeatureViewModelBase` registers for `GlobalSitesChangedMessage` in `OnActivated()` and stores the global sites in a protected property `IReadOnlyList<SiteInfo> GlobalSites`
- Each tab's `RunOperationAsync` checks: if local override sites exist, use those; else if GlobalSites is non-empty, use those; else fall back to the SiteUrl text box
- The SiteUrl TextBox on each tab shows a placeholder/hint when global sites are active (e.g., "Using 3 globally selected sites" as watermark text)
### Local override behavior
- Tabs that already have per-tab site pickers (like Permissions) keep them
- When a user picks sites locally on a tab, that overrides the global selection for that tab only
- A "Clear local selection" action resets the tab back to using global sites
- The global selection in the toolbar is never modified by per-tab overrides
### Tabs that DO NOT consume global sites
- Settings tab: no site URL needed
- Bulk Sites tab: creates sites from CSV, does not target existing sites
- Templates tab (apply): creates a new site, does not target existing sites
### Tabs that consume global sites (single-site)
- Storage, Search, Duplicates, Folder Structure: these currently take a single SiteUrl
- When global sites are selected, these tabs use the first site in the global list by default
- The SiteUrl TextBox is pre-filled with the first global site URL (user can change it = local override)
### Tabs that consume global sites (multi-site)
- Permissions: already supports multi-site; global sites pre-populate its SelectedSites collection
- Transfer: source site pre-filled from first global site
### Claude's Discretion
- Exact XAML layout of the toolbar site picker button and label
- Whether to refactor SitePickerDialog or reuse as-is from MainWindow code-behind
- Internal naming of properties and helper methods
- Whether to add a chip/tag display for selected sites or keep it as a count label
- Localization key names for new strings
</decisions>
<code_context>
## Existing Code Insights
### Reusable Assets
- `SitePickerDialog` (Views/Dialogs/): Filterable checkbox list of sites with Select All/Deselect All — loads from `ISiteListService.GetSitesAsync()`. Currently only wired from PermissionsView; needs to be wired from MainWindow toolbar too.
- `SiteInfo(string Url, string Title)` record (Core/Models/): Already used by SitePickerDialog and PermissionsViewModel
- `ISiteListService.GetSitesAsync(TenantProfile, progress, ct)`: Enumerates all sites in a tenant. Already registered in DI.
- `TenantSwitchedMessage`: Broadcast pattern for tenant changes — global site selection follows the same pattern
- `WeakReferenceMessenger`: Already used for TenantSwitched and ProgressUpdated messages
- `FeatureViewModelBase.OnActivated()`: Already registers for TenantSwitchedMessage — extend to also register for GlobalSitesChangedMessage
### Established Patterns
- Dialog factories set on ViewModels as `Func<Window>?` from View code-behind (keeps Window refs out of VMs)
- `[ObservableProperty]` for bindable state
- `ObservableCollection<T>` for list-bound UI elements
- Tab content resolved from DI in MainWindow.xaml.cs
- Localization via `TranslationSource.Instance["key"]` with Strings.resx / Strings.fr.resx
### Integration Points
- `MainWindow.xaml`: Add site picker button + label to ToolBar
- `MainWindowViewModel.cs`: Add GlobalSelectedSites, OpenGlobalSitePickerCommand, GlobalSitesChangedMessage broadcast
- `MainWindow.xaml.cs`: Wire SitePickerDialog factory for the toolbar (same pattern as PermissionsView)
- `FeatureViewModelBase.cs`: Register for GlobalSitesChangedMessage, add GlobalSites property
- `Core/Messages/`: New GlobalSitesChangedMessage class
- Each tab ViewModel: Update RunOperationAsync to check GlobalSites before falling back to SiteUrl
- `Strings.resx` / `Strings.fr.resx`: New localization keys for toolbar site picker
- `App.xaml.cs`: No new DI registrations needed (SitePickerDialog factory and ISiteListService already registered)
### Key Files
| File | Role |
|------|------|
| `MainWindow.xaml` | Toolbar XAML — add site picker controls |
| `MainWindowViewModel.cs` | Global selection state + command |
| `MainWindow.xaml.cs` | Wire SitePickerDialog factory for toolbar |
| `FeatureViewModelBase.cs` | Base class — receive global sites message |
| `Core/Messages/TenantSwitchedMessage.cs` | Pattern reference for new message |
| `Views/Dialogs/SitePickerDialog.xaml.cs` | Reuse as-is |
| `ViewModels/Tabs/PermissionsViewModel.cs` | Already has multi-site pattern — adapt to consume global sites |
| `ViewModels/Tabs/StorageViewModel.cs` | Single-site pattern — adapt to consume global sites |
</code_context>
<specifics>
## Specific Ideas
- The toolbar site count label should update live when sites are selected/deselected
- When no tenant is connected, the "Select Sites" button should be disabled with a tooltip explaining why
- Clearing the session (Clear Session button) should also clear the global site selection
- The global selection should survive tab switching (it lives on MainWindowViewModel, not on any tab)
</specifics>
<deferred>
## Deferred Ideas
None — all items are within phase scope
</deferred>
---
*Phase: 06-global-site-selection*
*Context gathered: 2026-04-07*

View File

@@ -0,0 +1,163 @@
---
phase: 07-user-access-audit
plan: 09
type: execute
wave: 6
depends_on: ["07-05"]
files_modified:
- SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml
autonomous: true
requirements:
- UACC-01
- UACC-02
gap_closure: true
source_gaps:
- "Gap 1: Missing DataGrid visual indicators (guest badge + warning icon)"
- "Gap 2: Missing ObjectType column in DataGrid"
must_haves:
truths:
- "High-privilege entries show a warning icon (⚠) in the Permission Level column cell template"
- "External users show a guest badge (👤 Guest) in the User column cell template when IsExternalUser is true"
- "DataGrid columns include Object Type bound to ObjectType between Object and Permission Level"
artifacts:
- path: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"
provides: "DataGrid with visual indicators for high-privilege/external users and ObjectType column"
contains: "IsExternalUser DataTrigger, IsHighPrivilege warning icon, ObjectType column"
key_links:
- from: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"
to: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
via: "Bindings on IsExternalUser, IsHighPrivilege, ObjectType properties"
pattern: "DataTrigger Binding"
---
<objective>
Add missing visual indicators and ObjectType column to the UserAccessAuditView DataGrid.
Purpose: Close verification gaps 1 and 2 — the XAML currently lacks per-row guest badges for external users, warning icons for high-privilege entries, and the ObjectType column.
Output: Updated UserAccessAuditView.xaml with all three additions.
</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/07-user-access-audit/07-CONTEXT.md
@.planning/phases/07-user-access-audit/07-05-SUMMARY.md
@.planning/phases/07-user-access-audit/07-VERIFICATION.md
<interfaces>
<!-- UserAccessEntry fields available for binding -->
From SharepointToolbox/Core/Models/UserAccessEntry.cs:
```csharp
public record UserAccessEntry(
string UserDisplayName, string UserLogin,
string SiteUrl, string SiteTitle,
string ObjectType, string ObjectTitle, string ObjectUrl,
string PermissionLevel, AccessType AccessType, string GrantedThrough,
bool IsHighPrivilege, bool IsExternalUser);
```
<!-- Current DataGrid columns (lines 219-249 of UserAccessAuditView.xaml) -->
Current columns: User (UserLogin), Site (SiteTitle), Object (ObjectTitle), Permission Level (PermissionLevel), Access Type (template), Granted Through (GrantedThrough).
Missing: ObjectType column, guest badge in User column, warning icon in Permission Level column.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add guest badge, warning icon, and ObjectType column to DataGrid</name>
<files>SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml</files>
<action>
Modify the DataGrid columns section (lines 219-249) with three changes:
**Change 1 — Convert User column to DataGridTemplateColumn with guest badge:**
Replace the plain `DataGridTextColumn Header="User"` with a `DataGridTemplateColumn`:
```xml
<DataGridTemplateColumn Header="User" Width="180">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding UserLogin}" VerticalAlignment="Center" />
<Border Background="#F39C12" CornerRadius="3" Padding="4,1" Margin="6,0,0,0"
VerticalAlignment="Center">
<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="10" Foreground="White" FontWeight="SemiBold" />
</Border>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
```
**Change 2 — Convert Permission Level column to DataGridTemplateColumn with warning icon:**
Replace the plain `DataGridTextColumn Header="Permission Level"` with a `DataGridTemplateColumn`:
```xml
<DataGridTemplateColumn Header="Permission Level" Width="140">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="⚠" Foreground="#E74C3C" Margin="0,0,4,0"
FontSize="12" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsHighPrivilege}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding PermissionLevel}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
```
**Change 3 — Add ObjectType column between Object and Permission Level:**
```xml
<DataGridTextColumn Header="Object Type" Binding="{Binding ObjectType}" Width="90" />
```
Insert this column after the "Object" column and before the "Permission Level" column.
Final column order: User (with guest badge), Site, Object, Object Type, Permission Level (with ⚠ icon), Access Type, Granted Through.
</action>
<verify>
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore 2>&1 | tail -5</automated>
</verify>
<done>DataGrid now shows: guest badge on external user rows (orange "Guest" pill), warning icon (⚠) on high-privilege permission levels, and ObjectType column showing Site Collection/Site/List/Folder distinction.</done>
</task>
</tasks>
<verification>
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` — XAML compiles without errors
- Visual inspection: DataGrid columns order is User (with guest badge), Site, Object, Object Type, Permission Level (with ⚠), Access Type, Granted Through
- Guest badge visible only when IsExternalUser=true
- Warning icon visible only when IsHighPrivilege=true
</verification>
<success_criteria>
The DataGrid shows guest badges for external users, warning icons for high-privilege entries, and the ObjectType column — closing verification gaps 1 and 2.
</success_criteria>
<output>
After completion, create `.planning/phases/07-user-access-audit/07-09-SUMMARY.md`
</output>

View File

@@ -0,0 +1,171 @@
---
phase: 07-user-access-audit
plan: 10
type: execute
wave: 6
depends_on: ["07-08"]
files_modified:
- SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs
autonomous: true
requirements:
- UACC-01
gap_closure: true
source_gaps:
- "Gap 3: Debounced search test absent (Plan 08 truth partially unmet)"
must_haves:
truths:
- "A unit test verifies that setting SearchQuery to a value of length >= 2 triggers IGraphUserSearchService.SearchUsersAsync after the debounce delay"
artifacts:
- path: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs"
provides: "Debounced search unit test"
contains: "SearchQuery_debounced_calls_SearchUsersAsync"
key_links:
- from: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs"
to: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
via: "Tests SearchQuery property change → DebounceSearchAsync → SearchUsersAsync"
pattern: "SearchUsersAsync"
---
<objective>
Add a unit test for the debounced search path in UserAccessAuditViewModel.
Purpose: Close verification gap 3 — plan 08 required "ViewModel tests verify: debounced search triggers service" but no such test exists.
Output: One new test method added to UserAccessAuditViewModelTests.cs.
</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/07-user-access-audit/07-CONTEXT.md
@.planning/phases/07-user-access-audit/07-08-SUMMARY.md
@.planning/phases/07-user-access-audit/07-VERIFICATION.md
<interfaces>
<!-- ViewModel debounce path (from UserAccessAuditViewModel.cs) -->
```csharp
// Line 281-290: OnSearchQueryChanged triggers DebounceSearchAsync
partial void OnSearchQueryChanged(string value)
{
_searchCts?.Cancel();
_searchCts?.Dispose();
_searchCts = new CancellationTokenSource();
var ct = _searchCts.Token;
_ = DebounceSearchAsync(value, ct);
}
// Line 406-458: DebounceSearchAsync waits 300ms then calls SearchUsersAsync
private async Task DebounceSearchAsync(string query, CancellationToken ct)
{
await Task.Delay(300, ct);
// ... guard: query null/whitespace or < 2 chars → clear and return
var clientId = _currentProfile?.ClientId ?? string.Empty;
var results = await _graphUserSearchService.SearchUsersAsync(clientId, query, 10, ct);
// ... dispatches results to SearchResults collection
}
```
<!-- Existing test patterns (from UserAccessAuditViewModelTests.cs) -->
```csharp
// Uses Moq + xUnit. CreateViewModel helper returns (vm, auditMock).
// mockGraph is Mock<IGraphUserSearchService> created inside CreateViewModel.
// The test needs access to mockGraph — may need to extend CreateViewModel to return it.
```
<!-- IGraphUserSearchService contract -->
```csharp
public interface IGraphUserSearchService
{
Task<IReadOnlyList<GraphUserResult>> SearchUsersAsync(
string clientId, string query, int maxResults, CancellationToken ct);
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add debounced search unit test</name>
<files>SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs</files>
<action>
**Step 1**: Extend the `CreateViewModel` helper to also return the `Mock<IGraphUserSearchService>` so tests can set up expectations and verify calls on it. Change the return tuple from `(vm, auditMock)` to `(vm, auditMock, graphMock)`. Update all 8 existing test calls to destructure the third element (use `_` discard).
**Step 2**: Add the following test method after Test 8:
```csharp
// ── Test 9: Debounced search triggers SearchUsersAsync ──────────────
[Fact]
public async Task SearchQuery_debounced_calls_SearchUsersAsync()
{
var graphResults = new List<GraphUserResult>
{
new("Alice Smith", "alice@contoso.com", "alice@contoso.com")
};
var (vm, _, graphMock) = CreateViewModel();
graphMock
.Setup(s => s.SearchUsersAsync(
It.IsAny<string>(),
It.Is<string>(q => q == "Ali"),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(graphResults);
// Set a TenantProfile so _currentProfile is non-null
var profile = new TenantProfile
{
Name = "Test",
TenantUrl = "https://contoso.sharepoint.com",
ClientId = "test-client-id"
};
WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(profile));
// Act: set SearchQuery which triggers OnSearchQueryChanged → DebounceSearchAsync
vm.SearchQuery = "Ali";
// Wait longer than 300ms debounce to allow async fire-and-forget to complete
await Task.Delay(600);
// Assert: SearchUsersAsync was called with the query
graphMock.Verify(
s => s.SearchUsersAsync(
It.IsAny<string>(),
"Ali",
It.IsAny<int>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
```
**Important notes:**
- The `DebounceSearchAsync` method uses `Application.Current?.Dispatcher` which will be null in tests. The else branch (lines 438-442) handles this by adding directly to SearchResults — this is the test-safe path.
- The 600ms delay in the test ensures the 300ms debounce + async execution has time to complete.
- The TenantSwitchedMessage sets `_currentProfile` so that `_currentProfile?.ClientId` is non-null.
</action>
<verify>
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests" 2>&1 | tail -10</automated>
</verify>
<done>Debounced search test passes: setting SearchQuery to "Ali" triggers SearchUsersAsync after the 300ms debounce. All 9 ViewModel tests pass with no regressions.</done>
</task>
</tasks>
<verification>
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests"` — all 9 tests pass
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` — no regressions
</verification>
<success_criteria>
The debounced search path (SearchQuery → 300ms delay → SearchUsersAsync) has unit test coverage, closing verification gap 3.
</success_criteria>
<output>
After completion, create `.planning/phases/07-user-access-audit/07-10-SUMMARY.md`
</output>