Compare commits

..

9 Commits

Author SHA1 Message Date
Dev f41dbd333e chore: archive v2.2 Report Branding & User Directory milestone
Release SharePoint Toolbox v2 / release (push) Failing after 14s
5 phases (10-14), 14 plans, 11/11 requirements complete.
Key features: HTML report branding with MSP/client logos, user directory
browse mode with paginated load and member/guest filtering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:27:33 +02:00
Dev b9511bd2b0 docs(14): mark phase 14 plan checkboxes complete in roadmap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:31:52 +02:00
Dev febb67ab64 docs(14-02): complete directory browse UI plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:31:08 +02:00
Dev 1a1e83cfad feat(14-02): add directory browse mode UI with mode toggle, DataGrid, and loading UX
- Mode toggle (Search/Browse) RadioButtons at top of left panel
- Search panel uses DataTrigger inverse visibility (collapses when IsBrowseMode=true)
- Browse panel with Load/Cancel buttons, IncludeGuests checkbox, filter TextBox, status/count
- Directory DataGrid with 5 columns (Name, Email, Department, Job Title, Type)
- Guest users highlighted in orange via DataTrigger on UserType
- SelectedUsers extracted to shared section visible in both modes
- DataGrid wired to DirectoryDataGrid_MouseDoubleClick handler
- Scan Options and Run/Export buttons remain always visible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:29:59 +02:00
Dev f11bfefe52 docs(14-01): complete directory UI infrastructure plan
- SUMMARY.md with 3 tasks, 4 commits, 5 files modified
- STATE.md updated with position and decisions
- ROADMAP.md updated with phase 14 progress (1/2 plans)
- REQUIREMENTS.md: UDIR-05 marked complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:27:44 +02:00
Dev d1282cea5d feat(14-01): add DirectoryDataGrid_MouseDoubleClick code-behind handler
- Extracts GraphDirectoryUser from DataGrid.SelectedItem on double-click
- Invokes SelectDirectoryUserCommand to add user to audit pipeline
- Using added for SharepointToolbox.Core.Models

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:26:41 +02:00
Dev e6ba2d8146 feat(14-01): add SelectDirectoryUserCommand bridging directory to audit pipeline
- RelayCommand<GraphDirectoryUser> converts to GraphUserResult and adds to SelectedUsers
- Duplicate UPN check prevents adding same user twice
- Initialized in both DI and test constructors
- 4 new tests pass (add, skip duplicate, null, auditable)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:26:12 +02:00
Dev 381081da18 test(14-01): add failing tests for SelectDirectoryUserCommand
- Test 17: adds user to SelectedUsers
- Test 18: skips duplicates
- Test 19: null does nothing
- Test 20: user is auditable after selection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:25:18 +02:00
Dev 70e8d121fd feat(14-01): add 14 localization keys for directory browse UI (EN + FR)
- audit.mode.search, audit.mode.browse for mode toggle labels
- directory.grp.browse, directory.btn.load, directory.btn.cancel
- directory.filter.placeholder, directory.chk.guests, directory.status.count
- directory.hint.doubleclick, directory.col.name/upn/department/jobtitle/type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:24:54 +02:00
13 changed files with 602 additions and 217 deletions
+26 -18
View File
@@ -8,28 +8,35 @@ A C#/WPF desktop application for IT administrators and MSPs to audit and manage
Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application. Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application.
## Current Milestone: v2.2 Report Branding & User Directory
**Goal:** Add customizable logos to HTML reports and a full user directory browse mode in the user access audit tab.
**Target features:**
- HTML report branding with MSP logo (global) and client logo (per tenant — pull from tenant or import)
- User directory browse mode as alternative to search in user access audit tab
## Current State ## Current State
**Shipped:** v1.1 Enhanced Reports (2026-04-08) **Shipped:** v2.2 Report Branding & User Directory (2026-04-09)
**Status:** Active milestone v2.2 **Status:** Between milestones — ready for `/gsd:new-milestone`
<details>
<summary>v2.2 shipped features</summary>
- HTML report branding with MSP logo (global) and client logo (per tenant)
- Auto-pull client logo from Entra branding API
- Logo validation (PNG/JPG, 512 KB limit) with auto-compression
- User directory browse mode in user access audit tab with paginated load
- Member/guest filter and department/job title columns
- Directory user selection triggers existing audit pipeline
</details>
<details>
<summary>v1.1 shipped features</summary>
**v1.1 shipped features:**
- Global multi-site selection in toolbar (pick sites once, all tabs use them) - Global multi-site selection in toolbar (pick sites once, all tabs use them)
- User access audit tab with Graph API people-picker, direct/group/inherited access distinction - User access audit tab with Graph API people-picker, direct/group/inherited access distinction
- Simplified permissions with plain-language labels, color-coded risk levels, detail-level toggle - Simplified permissions with plain-language labels, color-coded risk levels, detail-level toggle
- Storage visualization with LiveCharts2 pie/donut and bar charts by file type - Storage visualization with LiveCharts2 pie/donut and bar charts by file type
</details>
Tech stack: C# / WPF / .NET 10 / PnP Framework / Microsoft Graph SDK / MSAL / Serilog / CommunityToolkit.Mvvm / LiveCharts2 Tech stack: C# / WPF / .NET 10 / PnP Framework / Microsoft Graph SDK / MSAL / Serilog / CommunityToolkit.Mvvm / LiveCharts2
Tests: 205 automated (xUnit), 22 skipped (require live SharePoint tenant) Tests: 285 automated (xUnit), 26 skipped (require live SharePoint tenant)
Distribution: 200 MB self-contained EXE (win-x64) Distribution: 200 MB self-contained EXE (win-x64)
LOC: ~16,900 C#
## Requirements ## Requirements
@@ -48,10 +55,10 @@ Distribution: 200 MB self-contained EXE (win-x64)
- [x] Simplified permissions reports (plain language, summary views) (SIMP-01/02/03) — v1.1 - [x] Simplified permissions reports (plain language, summary views) (SIMP-01/02/03) — v1.1
- [x] Storage metrics graph by file type (pie/donut and bar chart, toggleable) (VIZZ-01/02/03) — v1.1 - [x] Storage metrics graph by file type (pie/donut and bar chart, toggleable) (VIZZ-01/02/03) — v1.1
### Active ### Shipped in v2.2
- [ ] HTML report branding with MSP logo (global) and client logo (per tenant) - [x] HTML report branding with MSP and client logos (BRAND-01/02/03/04/05/06) — v2.2
- [ ] User directory browse mode in user access audit tab - [x] User directory browse mode in user access audit tab (UDIR-01/02/03/04/05) — v2.2
### Out of Scope ### Out of Scope
@@ -68,8 +75,9 @@ Distribution: 200 MB self-contained EXE (win-x64)
- **v1.0 shipped** with full feature parity: permissions, storage, search, duplicates, bulk operations, templates, folder provisioning - **v1.0 shipped** with full feature parity: permissions, storage, search, duplicates, bulk operations, templates, folder provisioning
- **v1.1 shipped** with enhanced reports: user access audit, simplified permissions, storage charts, global site selection - **v1.1 shipped** with enhanced reports: user access audit, simplified permissions, storage charts, global site selection
- **Localization:** 220+ EN/FR keys, full parity verified - **v2.2 shipped** with report branding (logos in HTML exports) and user directory browse mode
- **Architecture:** 120+ C# files + 17 XAML files across Core/Infrastructure/Services/ViewModels/Views layers - **Localization:** 230+ EN/FR keys, full parity verified
- **Architecture:** 140+ C# files + 17 XAML files across Core/Infrastructure/Services/ViewModels/Views layers
## Constraints ## Constraints
@@ -93,4 +101,4 @@ Distribution: 200 MB self-contained EXE (win-x64)
| Wave 0 scaffold pattern | Models + interfaces + test stubs before implementation | ✓ Good — all phases had test targets from day 1 | | Wave 0 scaffold pattern | Models + interfaces + test stubs before implementation | ✓ Good — all phases had test targets from day 1 |
--- ---
*Last updated: 2026-04-08 after v2.2 milestone started* *Last updated: 2026-04-09 after v2.2 milestone shipped*
+10 -85
View File
@@ -4,7 +4,7 @@
-**v1.0 MVP** — Phases 1-5 (shipped 2026-04-07) — [archive](milestones/v1.0-ROADMAP.md) -**v1.0 MVP** — Phases 1-5 (shipped 2026-04-07) — [archive](milestones/v1.0-ROADMAP.md)
-**v1.1 Enhanced Reports** — Phases 6-9 (shipped 2026-04-08) — [archive](milestones/v1.1-ROADMAP.md) -**v1.1 Enhanced Reports** — Phases 6-9 (shipped 2026-04-08) — [archive](milestones/v1.1-ROADMAP.md)
- 🔄 **v2.2 Report Branding & User Directory** — Phases 10-14 (active) - **v2.2 Report Branding & User Directory** — Phases 10-14 (shipped 2026-04-09) — [archive](milestones/v2.2-ROADMAP.md)
## Phases ## Phases
@@ -29,87 +29,16 @@
</details> </details>
### v2.2 Report Branding & User Directory (Phases 10-14) <details>
<summary>✅ v2.2 Report Branding & User Directory (Phases 10-14) — SHIPPED 2026-04-09</summary>
- [x] **Phase 10: Branding Data Foundation** — Models, repository, and services for logo storage and user directory enumeration (completed 2026-04-08) - [x] Phase 10: Branding Data Foundation (3/3 plans) — completed 2026-04-08
- [x] **Phase 11: HTML Export Branding + ViewModel Integration** — Inject logos into all 5 HTML report types; wire branding into export-triggering ViewModels and logo management commands (completed 2026-04-08) - [x] Phase 11: HTML Export Branding + ViewModel Integration (4/4 plans) — completed 2026-04-08
- [x] **Phase 12: Branding UI Views** — Settings and profile dialog logo sections with live preview; auto-pull client logo from Entra branding API (completed 2026-04-08) - [x] Phase 12: Branding UI Views (3/3 plans) — completed 2026-04-08
- [x] **Phase 13: User Directory ViewModel** — Browse mode state, paginated directory load, member/guest filter, and department/job title columns (completed 2026-04-08) - [x] Phase 13: User Directory ViewModel (2/2 plans) — completed 2026-04-08
- [ ] **Phase 14: User Directory View** — Toggle panel in UserAccessAuditView, user selection to trigger existing audit pipeline - [x] Phase 14: User Directory View (2/2 plans) — completed 2026-04-09
## Phase Details </details>
### Phase 10: Branding Data Foundation
**Goal**: The application can store, validate, and retrieve MSP and client logos as portable base64 strings in JSON, and can enumerate a full tenant user list with pagination.
**Depends on**: Nothing (additive to existing infrastructure)
**Requirements**: BRAND-01, BRAND-03, BRAND-06
**Success Criteria** (what must be TRUE):
1. An MSP logo imported as a PNG or JPG file is persisted as a base64 string in `branding.json` and survives an application restart
2. A client logo imported per tenant profile is persisted as a base64 string inside the tenant's profile JSON and is not affected by other tenants' profiles
3. A file larger than 512 KB or not a valid PNG/JPG is rejected at import time with an error; no invalid data reaches the JSON store
4. `GraphUserDirectoryService.GetUsersAsync` returns all enabled member users for a tenant, following `@odata.nextLink` until exhausted, without truncating at 999
**Plans**: 3 plans
Plans:
- [x] 10-01-PLAN.md — Logo models, BrandingRepository, BrandingService with validation/compression
- [x] 10-02-PLAN.md — GraphUserDirectoryService with PageIterator pagination
- [x] 10-03-PLAN.md — DI registration in App.xaml.cs and full test suite gate
### Phase 11: HTML Export Branding + ViewModel Integration
**Goal**: All five HTML reports display MSP and client logos in a consistent header, and administrators can manage logos from Settings and the profile dialog without touching the View layer.
**Depends on**: Phase 10
**Requirements**: BRAND-05, BRAND-04
**Success Criteria** (what must be TRUE):
1. Running any of the five HTML exports (Permissions, Storage, Search, Duplicates, User Access) produces an HTML file whose header contains the MSP logo `<img>` tag when an MSP logo is configured
2. When a client logo is configured for the active tenant, the same HTML export header contains both the MSP logo and the client logo side by side
3. When no logo is configured, the HTML export header contains no broken image placeholder and the report renders identically to the pre-branding output
4. SettingsViewModel exposes browse/clear commands for MSP logo; ProfileManagementViewModel exposes browse/clear commands for client logo — both commands are exercisable without opening any View
5. Auto-pulling the client logo from the tenant's Entra branding API stores the logo in the tenant profile and falls back silently when no Entra branding is configured
**Plans**: 4 plans
Plans:
- [ ] 11-01-PLAN.md — ReportBranding model + BrandingHtmlHelper static class with unit tests
- [ ] 11-02-PLAN.md — Add optional branding param to all 5 HTML export services
- [ ] 11-03-PLAN.md — Wire IBrandingService into all 5 export ViewModels
- [ ] 11-04-PLAN.md — Logo management commands (Settings + Profile) and Entra auto-pull
### Phase 12: Branding UI Views
**Goal**: Administrators can see, import, preview, and clear logos directly in the Settings and profile management dialogs.
**Depends on**: Phase 11
**Requirements**: BRAND-02, BRAND-04 (view layer for Entra pull)
**Success Criteria** (what must be TRUE):
1. Opening Settings shows the MSP logo section: an import button, a live thumbnail preview of the current logo, and a clear button that removes the logo immediately
2. Opening a tenant profile dialog shows the client logo section with the same import/preview/clear controls
3. Importing a logo via the UI shows the thumbnail preview without requiring an application restart
4. Clicking "Pull from Entra" in the profile dialog fetches and displays the tenant's banner logo if one exists, and shows a clear user-facing message if none is configured
**Plans**: 3 plans
Plans:
- [x] 12-01-PLAN.md — Base64ToImageSourceConverter, localization keys, App.xaml registration, ClientLogoPreview property
- [x] 12-02-PLAN.md — SettingsView MSP logo section (preview, import, clear)
- [x] 12-03-PLAN.md — ProfileManagementDialog client logo section (preview, import, clear, Entra pull)
### Phase 13: User Directory ViewModel
**Goal**: The UserAccessAuditViewModel supports a full directory browse mode with paginated load, member/guest filtering, and department/job title display, fully testable without the View.
**Depends on**: Phase 10
**Requirements**: UDIR-01, UDIR-02, UDIR-03, UDIR-04
**Success Criteria** (what must be TRUE):
1. `UserAccessAuditViewModel` exposes a toggle property that switches between Search mode (existing people-picker behavior) and Browse mode (directory list behavior), with no regression to Search mode behavior
2. Invoking the load-directory command fetches all enabled member users via `PageIterator`, updates a progress observable with the running user count, and supports cancellation mid-load
3. A "Members only / Include guests" toggle filters the displayed list in-memory without issuing a new Graph request
4. Each user row in the observable collection exposes DisplayName, UPN, Department, and JobTitle; Department and JobTitle columns are visible and sortable in the ViewModel's `ICollectionView`
**Plans**: 2 plans
Plans:
- [x] 13-01-PLAN.md — Extend GraphDirectoryUser with UserType + service includeGuests parameter
- [x] 13-02-PLAN.md — UserAccessAuditViewModel directory browse mode (toggle, load, filter, sort, tests)
### Phase 14: User Directory View
**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.
**Depends on**: Phase 13
**Requirements**: UDIR-05, UDIR-01 (view layer)
**Success Criteria** (what must be TRUE):
1. The user access audit tab shows a mode toggle control (e.g., radio buttons or segmented control) that visibly switches the left panel between the existing people-picker and the directory browse panel
2. In browse mode, selecting a user from the directory list and clicking Run Audit (or equivalent) launches the existing audit pipeline for that user, producing the same results as if the user had been found via search
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
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
**Plans**: TBD
## Progress ## Progress
@@ -117,8 +46,4 @@ Plans:
|-------|-----------|-------|--------|-----------| |-------|-----------|-------|--------|-----------|
| 1-5 | v1.0 | 36/36 | Shipped | 2026-04-07 | | 1-5 | v1.0 | 36/36 | Shipped | 2026-04-07 |
| 6-9 | v1.1 | 25/25 | Shipped | 2026-04-08 | | 6-9 | v1.1 | 25/25 | Shipped | 2026-04-08 |
| 10. Branding Data Foundation | v2.2 | 3/3 | Complete | 2026-04-08 | | 10-14 | v2.2 | 14/14 | Shipped | 2026-04-09 |
| 11. HTML Export Branding + ViewModel Integration | 4/4 | Complete | 2026-04-08 | — |
| 12. Branding UI Views | 3/3 | Complete | 2026-04-08 | — |
| 13. User Directory ViewModel | 2/2 | Complete | 2026-04-08 | — |
| 14. User Directory View | v2.2 | 0/? | Not started | — |
+24 -59
View File
@@ -1,37 +1,38 @@
--- ---
gsd_state_version: 1.0 gsd_state_version: 1.0
milestone: v2.2 milestone: none
milestone_name: Report Branding & User Directory milestone_name: Between milestones
status: completed status: idle
stopped_at: Completed 13-02-PLAN.md stopped_at: v2.2 milestone archived
last_updated: "2026-04-08T14:08:49.579Z" last_updated: "2026-04-09"
last_activity: 2026-04-08Phase 11 planning completed last_activity: 2026-04-09v2.2 milestone completed and archived
progress: progress:
total_phases: 5 total_phases: 0
completed_phases: 4 completed_phases: 0
total_plans: 12 total_plans: 0
completed_plans: 12 completed_plans: 0
--- ---
# Project State # Project State
## Project Reference ## Project Reference
See: .planning/PROJECT.md (updated 2026-04-08) See: .planning/PROJECT.md (updated 2026-04-09)
**Core value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application. **Core value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application.
**Current focus:** v2.2 Report Branding & User Directory — HTML report logos (Phases 10-12), user directory browse mode (Phases 13-14) **Current focus:** Between milestones — v2.2 shipped, ready for `/gsd:new-milestone`
## Current Position ## Current Position
Phase: 11 (planned, ready to execute) Phase: None (between milestones)
Plan: 4 plans (11-01 through 11-04) in 3 waves Status: v2.2 Report Branding & User Directory shipped 2026-04-09
Status: Phase 10 complete, Phase 11 planned — ready to execute Next step: `/gsd:new-milestone` to start next milestone
Last activity: 2026-04-08 — Phase 11 planning completed
``` ## Shipped Milestones
v2.2 Progress: [██░░░░░░░░] 20% (1/5 phases, 3/7 plans)
``` - v1.0 MVP — Phases 1-5 (shipped 2026-04-07)
- v1.1 Enhanced Reports — Phases 6-9 (shipped 2026-04-08)
- v2.2 Report Branding & User Directory — Phases 10-14 (shipped 2026-04-09)
## Accumulated Context ## Accumulated Context
@@ -39,45 +40,9 @@ v2.2 Progress: [██░░░░░░░░] 20% (1/5 phases, 3/7 plans)
Decisions are logged in PROJECT.md Key Decisions table. Decisions are logged in PROJECT.md Key Decisions table.
**v2.2 architectural decisions (locked at roadmap):**
- Logos stored as base64 strings in JSON (not file paths). `BrandingSettings.cs` holds MSP logo; `TenantProfile` holds client logo. File path is discarded after import. This decision is locked — all downstream phases depend on it.
- Client logo lives on `TenantProfile`, NOT in `BrandingSettings`. Per-tenant ownership; prevents serialization and deletion awkwardness.
- Export services use optional `ReportBranding? branding = null` parameter. All existing call sites compile unchanged. No new `IHtmlExportService` interface needed.
- `GraphUserDirectoryService` is a new service, separate from `GraphUserSearchService`. Different pagination model (`PageIterator`), different cancellation needs.
- Directory does NOT load automatically on tab open. Explicit "Load Directory" button required to avoid blocking UI on large tenants.
- SVG logo support: rejected. XSS risk in data-URIs. PNG/JPG only.
- No new NuGet packages for v2.2. All capabilities provided by existing stack (BCL, Microsoft.Graph 5.74.0, WPF PresentationCore).
**v1.1 architectural notes (carried forward):**
- Global site selection (Phase 6) changes the toolbar; all tabs bind to shared `GlobalSiteSelectionViewModel`. `WeakReferenceMessenger` for cross-tab site-changed notifications.
- Per-tab override (SITE-02): each `FeatureViewModelBase` subclass stores a nullable local site override; null means "use global".
- Storage Visualization (Phase 9): LiveCharts2, WPF-native, self-contained friendly.
- [Phase 10-branding-data-foundation]: No ConsistencyLevel header on equality filter for GetUsersAsync (unlike GraphUserSearchService startsWith which requires it)
- [Phase 10-branding-data-foundation]: MapUser extracted as internal static in GraphUserDirectoryService for direct unit testability without live Graph endpoint
- [Phase 10-branding-data-foundation]: Type alias AppGraphClientFactory used in GraphUserDirectoryService to disambiguate from Microsoft.Graph.GraphClientFactory
- [Phase 10-branding-data-foundation]: Used WPF PresentationCore (BitmapDecoder/TransformedBitmap/JpegBitmapEncoder) for image compression instead of System.Drawing.Bitmap — System.Drawing.Common is not available without a new NuGet package on .NET 10, and WPF PresentationCore is already in the stack
- [Phase 10-branding-data-foundation]: LogoData is a non-positional record with init properties (not positional constructor) to avoid System.Text.Json deserialization failure
- [Phase 10-branding-data-foundation]: No new using statements required for Phase 10 DI registrations — SharepointToolbox.Infrastructure.Persistence and SharepointToolbox.Services were already imported
- [Phase 11-html-export-branding]: BrandingHtmlHelper is internal — only used within Services.Export namespace, tests access via InternalsVisibleTo
- [Phase 11-html-export-branding]: InternalsVisibleTo added via MSBuild AssemblyAttribute ItemGroup in csproj
- [Phase 11-html-export-branding]: branding parameter placed AFTER CancellationToken ct in WriteAsync — existing positional callers unaffected
- [Phase 11-html-export-branding]: MakeBranding helper added locally to each test class — test files stay self-contained
- [Phase 11]: Test constructors on 3 ViewModels received optional IBrandingService? brandingService = null as last parameter to preserve all existing test call sites
- [Phase 11]: Guard clause (if _brandingService is not null) used for graceful degradation — branding = null fallback preserves backward compat
- [Phase 11]: No App.xaml.cs changes needed for ViewModel branding injection — IBrandingService already registered as singleton, ViewModel registrations auto-resolve
- [Phase 12]: Skipped BitmapImage creation test due to missing Xunit.StaFact; STA thread required for WPF BitmapImage
- [Phase 12]: Used Grid overlay with DataTrigger for logo/placeholder visibility toggle in SettingsView
- [Phase 12]: Label+StackPanel layout for logo section in ProfileManagementDialog, consistent with SettingsView pattern
- [Phase 13]: UserType added as last positional param for backward compat; includeGuests defaults false; userType always in Select
- [Phase 13]: Directory always fetches with includeGuests=true from Graph; member/guest filtering is in-memory via ICollectionView
- [Phase 13]: Separate _directoryCts for directory load cancellation (independent from base class _cts)
### Pending Todos ### Pending Todos
- Confirm `$filter=accountEnabled eq true and userType eq 'Member'` behavior without `ConsistencyLevel: eventual` against a real tenant before Phase 13 planning. None — milestone complete.
- Verify Entra `bannerLogo` stream endpoint returns empty body (not HTTP 404) when no tenant branding is configured — determines error handling branch for BRAND-04 auto-pull.
- Decide report header layout before Phase 11: logos side-by-side (current spec: `display: flex; gap: 16px`, MSP left + client right).
- Decide "Load Directory" button placement before Phase 14: inside browse panel (recommended) or tab-level toolbar.
### Blockers/Concerns ### Blockers/Concerns
@@ -85,7 +50,7 @@ None.
## Session Continuity ## Session Continuity
Last session: 2026-04-08T14:08:49.577Z Last session: 2026-04-09
Stopped at: Completed 13-02-PLAN.md Stopped at: v2.2 milestone archived
Resume file: None Resume file: None
Next step: `/gsd:execute-phase 11` Next step: `/gsd:new-milestone`
@@ -1,11 +1,10 @@
# Requirements: SharePoint Toolbox v2.2 # Requirements Archive: SharePoint Toolbox v2.2 Report Branding & User Directory
**Defined:** 2026-04-08 **Defined:** 2026-04-08
**Core Value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application. **Completed:** 2026-04-09
**Coverage:** 11/11 requirements complete
## v2.2 Requirements ## Requirements
Requirements for v2.2 Report Branding & User Directory. Each maps to roadmap phases.
### Report Branding ### Report Branding
@@ -22,17 +21,28 @@ Requirements for v2.2 Report Branding & User Directory. Each maps to roadmap pha
- [x] **UDIR-02**: User can browse full tenant user directory with pagination (handles 999+ users) - [x] **UDIR-02**: User can browse full tenant user directory with pagination (handles 999+ users)
- [x] **UDIR-03**: User can filter directory by user type (member vs guest) - [x] **UDIR-03**: User can filter directory by user type (member vs guest)
- [x] **UDIR-04**: User can see department and job title columns in directory list - [x] **UDIR-04**: User can see department and job title columns in directory list
- [ ] **UDIR-05**: User can select one or more users from directory to run the access audit - [x] **UDIR-05**: User can select one or more users from directory to run the access audit
## Future Requirements ## Traceability
### Report Branding (Deferred) | Requirement | Phase | Status | Notes |
|-------------|-------|--------|-------|
| BRAND-01 | Phase 10 | Complete | Base64 JSON persistence via BrandingRepository |
| BRAND-02 | Phase 12 | Complete | Base64ToImageSourceConverter + live preview |
| BRAND-03 | Phase 10 | Complete | Per-tenant logo on TenantProfile |
| BRAND-04 | Phase 11 | Complete | Entra bannerLogo stream endpoint |
| BRAND-05 | Phase 11 | Complete | BrandingHtmlHelper + optional param on all 5 services |
| BRAND-06 | Phase 10 | Complete | Magic-byte validation, 512 KB limit, auto-compression |
| UDIR-01 | Phase 13 | Complete | IsDirectoryBrowseMode toggle property |
| UDIR-02 | Phase 13 | Complete | PageIterator pagination via GraphUserDirectoryService |
| UDIR-03 | Phase 13 | Complete | In-memory ICollectionView filter |
| UDIR-04 | Phase 13 | Complete | Sortable Department/JobTitle columns |
| UDIR-05 | Phase 14 | Complete | SelectDirectoryUserCommand + double-click handler |
## Deferred to Future Milestones
- **BRAND-F01**: PDF export with embedded logos - **BRAND-F01**: PDF export with embedded logos
- **BRAND-F02**: Custom report title/footer text per tenant - **BRAND-F02**: Custom report title/footer text per tenant
### User Directory (Deferred)
- **UDIR-F01**: Session-scoped directory cache (avoid re-fetching on tab switch) - **UDIR-F01**: Session-scoped directory cache (avoid re-fetching on tab switch)
- **UDIR-F02**: Export user directory list to CSV - **UDIR-F02**: Export user directory list to CSV
@@ -45,29 +55,5 @@ Requirements for v2.2 Report Branding & User Directory. Each maps to roadmap pha
| User directory as standalone tab | Directory browse is a mode within existing user access audit tab | | User directory as standalone tab | Directory browse is a mode within existing user access audit tab |
| Real-time directory sync | One-time load with manual refresh is sufficient for audit workflows | | Real-time directory sync | One-time load with manual refresh is sufficient for audit workflows |
## Traceability
Which phases cover which requirements. Updated during roadmap creation.
| Requirement | Phase | Status |
|-------------|-------|--------|
| BRAND-01 | Phase 10 | Complete |
| BRAND-03 | Phase 10 | Complete |
| BRAND-06 | Phase 10 | Complete |
| BRAND-05 | Phase 11 | Complete |
| BRAND-04 | Phase 11 | Complete |
| BRAND-02 | Phase 12 | Complete |
| UDIR-01 | Phase 13 | Complete |
| UDIR-02 | Phase 13 | Complete |
| UDIR-03 | Phase 13 | Complete |
| UDIR-04 | Phase 13 | Complete |
| UDIR-05 | Phase 14 | Pending |
**Coverage:**
- v2.2 requirements: 11 total
- Mapped to phases: 11
- Unmapped: 0
--- ---
*Requirements defined: 2026-04-08* *Archived: 2026-04-09*
*Last updated: 2026-04-08 after roadmap creation — all 11 requirements mapped to Phases 10-14*
+73
View File
@@ -0,0 +1,73 @@
# v2.2 Report Branding & User Directory — Milestone Archive
**Goal:** Add customizable logos to HTML reports and a full user directory browse mode in the user access audit tab
**Status:** Shipped 2026-04-09
**Timeline:** 2026-04-08 to 2026-04-09
## Stats
| Metric | Value |
|--------|-------|
| Phases | 5 (Phases 10-14) |
| Plans | 14 |
| Commits | 47 |
| C# LOC (total) | 16,916 |
| Tests | 285 pass / 26 skip |
| Requirements | 11/11 complete |
## Key Accomplishments
1. **Branding Data Foundation (Phase 10)** — Logo models with base64 JSON persistence, BrandingRepository, BrandingService with magic-byte validation (PNG/JPG) and auto-compression via WPF PresentationCore, GraphUserDirectoryService with PageIterator pagination for full tenant user enumeration.
2. **HTML Export Branding (Phase 11)** — BrandingHtmlHelper static class for consistent header generation, optional `ReportBranding` parameter added to all 5 HTML export services (Permissions, Storage, Search, Duplicates, User Access), ViewModel injection via IBrandingService, logo management commands (browse/clear) on Settings and Profile ViewModels, Entra branding API auto-pull for client logos.
3. **Branding UI Views (Phase 12)** — Base64ToImageSourceConverter for live logo preview, MSP logo section in SettingsView (import/preview/clear), client logo section in ProfileManagementDialog (import/preview/clear/Entra pull), Grid overlay with DataTrigger for placeholder visibility toggle.
4. **User Directory ViewModel (Phase 13)** — Browse mode toggle on UserAccessAuditViewModel, paginated directory load with cancellation via separate CancellationTokenSource, in-memory member/guest filter (fetches all users once, filters via ICollectionView), sortable columns for DisplayName, UPN, Department, JobTitle.
5. **User Directory View (Phase 14)** — Search/Browse RadioButton mode toggle, directory DataGrid with loading counter and cancel button, SelectDirectoryUserCommand bridging directory selection to existing audit pipeline, double-click code-behind handler, 14 localization keys (EN + FR).
## Phases
### Phase 10: Branding Data Foundation (3 plans)
- Logo models, BrandingRepository, BrandingService with validation/compression
- GraphUserDirectoryService with PageIterator pagination
- DI registration in App.xaml.cs and full test suite gate
### Phase 11: HTML Export Branding + ViewModel Integration (4 plans)
- ReportBranding model + BrandingHtmlHelper static class with unit tests
- Add optional branding param to all 5 HTML export services
- Wire IBrandingService into all 5 export ViewModels
- Logo management commands (Settings + Profile) and Entra auto-pull
### Phase 12: Branding UI Views (3 plans)
- Base64ToImageSourceConverter, localization keys, App.xaml registration, ClientLogoPreview property
- SettingsView MSP logo section (preview, import, clear)
- ProfileManagementDialog client logo section (preview, import, clear, Entra pull)
### Phase 13: User Directory ViewModel (2 plans)
- Extend GraphDirectoryUser with UserType + service includeGuests parameter
- UserAccessAuditViewModel directory browse mode (toggle, load, filter, sort, tests)
### Phase 14: User Directory View (2 plans)
- Localization keys (EN+FR), SelectDirectoryUserCommand, code-behind double-click handler
- XAML: mode toggle (Search/Browse RadioButtons), directory DataGrid, loading UX, shared SelectedUsers panel
## Requirements Covered
| Requirement | Description | Status |
|-------------|-------------|--------|
| BRAND-01 | Import MSP logo in application settings | Complete |
| BRAND-02 | Preview imported MSP logo in settings UI | Complete |
| BRAND-03 | Import client logo per tenant profile | Complete |
| BRAND-04 | Auto-pull client logo from Entra branding API | Complete |
| BRAND-05 | All 5 HTML reports display logos in consistent header | Complete |
| BRAND-06 | Logo validation (PNG/JPG, 512 KB limit) | Complete |
| UDIR-01 | Toggle between search and directory browse mode | Complete |
| UDIR-02 | Browse full tenant user directory with pagination | Complete |
| UDIR-03 | Filter directory by user type (member vs guest) | Complete |
| UDIR-04 | Department and job title columns in directory list | Complete |
| UDIR-05 | Select users from directory to run access audit | Complete |
---
*Archived: 2026-04-09*
@@ -0,0 +1,103 @@
---
phase: 14-user-directory-view
plan: 01
subsystem: ui
tags: [wpf, localization, resx, relay-command, datagrid, directory]
# Dependency graph
requires:
- phase: 13-user-directory-data
provides: "GraphDirectoryUser model, IGraphUserDirectoryService, directory browse mode properties on ViewModel"
provides:
- "14 localization keys (EN+FR) for directory browse UI"
- "SelectDirectoryUserCommand bridging directory selection to audit pipeline"
- "DirectoryDataGrid_MouseDoubleClick code-behind event handler"
affects: [14-user-directory-view]
# Tech tracking
tech-stack:
added: []
patterns:
- "SelectDirectoryUserCommand follows same RelayCommand<T> + duplicate UPN check pattern as AddUserCommand"
- "Code-behind event handler pattern: extract model from DataGrid.SelectedItem, invoke ViewModel command"
key-files:
created: []
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"
key-decisions:
- "SelectDirectoryUserCommand does not clear SearchQuery/SearchResults since it operates in browse mode context"
- "ExecuteSelectDirectoryUser placed alongside ExecuteAddUser/ExecuteRemoveUser in command implementations section"
patterns-established:
- "Directory-to-audit bridge: GraphDirectoryUser -> GraphUserResult conversion via SelectDirectoryUserCommand"
requirements-completed: [UDIR-05, UDIR-01]
# Metrics
duration: 3min
completed: 2026-04-09
---
# Phase 14 Plan 01: Directory UI Infrastructure Summary
**Localization keys (EN+FR), SelectDirectoryUserCommand bridging directory selection to audit pipeline, and DataGrid double-click code-behind handler**
## Performance
- **Duration:** 3 min
- **Started:** 2026-04-09T07:24:15Z
- **Completed:** 2026-04-09T07:27:00Z
- **Tasks:** 3
- **Files modified:** 5
## Accomplishments
- 14 localization keys added to both EN and FR resource files for directory browse UI
- SelectDirectoryUserCommand converts GraphDirectoryUser to GraphUserResult and adds to SelectedUsers with duplicate UPN check
- DirectoryDataGrid_MouseDoubleClick code-behind handler ready for XAML wiring in Plan 14-02
- 4 new tests added (20 total in directory test file), all passing
## Task Commits
Each task was committed atomically:
1. **Task 1: Add localization keys (EN + FR)** - `70e8d12` (feat)
2. **Task 2: Add SelectDirectoryUserCommand (TDD RED)** - `381081d` (test)
3. **Task 2: Add SelectDirectoryUserCommand (TDD GREEN)** - `e6ba2d8` (feat)
4. **Task 3: Add code-behind event handler** - `d1282ce` (feat)
## Files Created/Modified
- `SharepointToolbox/Localization/Strings.resx` - 14 EN localization keys for directory browse UI
- `SharepointToolbox/Localization/Strings.fr.resx` - 14 FR localization keys for directory browse UI
- `SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs` - SelectDirectoryUserCommand declaration, initialization in both constructors, ExecuteSelectDirectoryUser method
- `SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs` - DirectoryDataGrid_MouseDoubleClick handler, using for Core.Models
- `SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs` - 4 new tests (17-20) for SelectDirectoryUserCommand
## Decisions Made
- SelectDirectoryUserCommand does not clear SearchQuery/SearchResults since it operates in browse mode context (unlike AddUserCommand which clears search state)
- ExecuteSelectDirectoryUser placed in command implementations section alongside ExecuteAddUser/ExecuteRemoveUser for code locality
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- All infrastructure for Plan 14-02 (XAML view) is in place
- Localization keys ready for binding
- SelectDirectoryUserCommand ready for DataGrid double-click binding
- Code-behind handler ready to be wired via MouseDoubleClick event in XAML
---
*Phase: 14-user-directory-view*
*Completed: 2026-04-09*
@@ -0,0 +1,95 @@
---
phase: 14-user-directory-view
plan: 02
subsystem: ui
tags: [wpf, xaml, datagrid, radio-button, data-trigger, directory-browse]
requires:
- phase: 14-user-directory-view/01
provides: "Code-behind handler DirectoryDataGrid_MouseDoubleClick and localization keys"
- phase: 13-user-directory-data
provides: "ViewModel properties: IsBrowseMode, DirectoryUsersView, LoadDirectoryCommand, etc."
provides:
- "Complete directory browse UI in UserAccessAuditView with mode toggle, DataGrid, and loading UX"
- "Mode switching between search and browse panels"
- "Guest user highlighting in directory DataGrid"
affects: [user-directory-view]
tech-stack:
added: []
patterns: ["DataTrigger inverse visibility for mode-conditional panels", "Shared SelectedUsers section visible across modes"]
key-files:
created: []
modified: ["SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"]
key-decisions:
- "Used DataTrigger inverse visibility for search panel instead of ConverterParameter=Inverse (more reliable in WPF)"
- "Used plain English DataGrid column headers instead of localized bindings (DataGridTextColumn.Header binding is unreliable)"
- "GroupBox.Header uses nested TextBlock for localized binding compatibility with GroupBox.Style"
patterns-established:
- "DataTrigger inverse visibility: Style with default Visible, DataTrigger sets Collapsed on true"
- "Mode-conditional panels: search/browse GroupBoxes with opposite visibility triggers"
requirements-completed: [UDIR-05, UDIR-01]
duration: 2min
completed: 2026-04-09
---
# Phase 14 Plan 02: Directory Browse UI Summary
**Full directory browse mode UI with mode toggle RadioButtons, 5-column DataGrid, loading status, guest highlighting, and shared SelectedUsers section**
## Performance
- **Duration:** 2 min
- **Started:** 2026-04-09T07:28:21Z
- **Completed:** 2026-04-09T07:30:10Z
- **Tasks:** 1
- **Files modified:** 1
## Accomplishments
- Mode toggle (Search/Browse Directory) RadioButtons at top of left panel with InverseBoolConverter binding
- Search panel collapses when IsBrowseMode=true via DataTrigger approach; Browse panel shows via BoolToVisibilityConverter
- Directory panel with Load/Cancel buttons, IncludeGuests checkbox, filter TextBox, status/count display
- DataGrid with 5 columns (Name, Email, Department, Job Title, Type) bound to DirectoryUsersView
- Guest users highlighted in orange (#F39C12) with SemiBold font weight via DataTrigger on UserType
- SelectedUsers ItemsControl extracted from search GroupBox to shared section visible in both modes
- Scan Options and Run/Export buttons remain always visible in both modes
## Task Commits
Each task was committed atomically:
1. **Task 1: Restructure left panel with mode toggle and conditional panels** - `1a1e83c` (feat)
## Files Created/Modified
- `SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml` - Added mode toggle, browse panel with DataGrid, extracted SelectedUsers to shared section
## Decisions Made
- Used DataTrigger inverse visibility for search panel (Visible by default, Collapsed when IsBrowseMode=True) instead of ConverterParameter=Inverse -- more reliable across WPF versions
- Used plain English strings for DataGrid column headers ("Name", "Email", "Department", "Job Title", "Type") instead of localized bindings -- DataGridTextColumn.Header does not reliably support binding in standard WPF
- Moved GroupBox.Header to nested TextBlock element for search panel to avoid conflict between inline Header binding and GroupBox.Style on the same element
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Phase 14 is now complete (both plans executed)
- All directory browse UI elements are wired to ViewModel properties from Phase 13
- Manual testing recommended to verify visual layout, mode switching, DataGrid scrolling, and double-click selection
---
*Phase: 14-user-directory-view*
*Completed: 2026-04-09*
## Self-Check: PASSED
@@ -348,4 +348,59 @@ public class UserAccessAuditViewModelDirectoryTests
Assert.Single(visible); Assert.Single(visible);
Assert.Equal("Alice", visible[0].DisplayName); Assert.Equal("Alice", visible[0].DisplayName);
} }
// ── Test 17: SelectDirectoryUserCommand adds user to SelectedUsers ──────
[Fact]
public void SelectDirectoryUserCommand_adds_user_to_SelectedUsers()
{
var (vm, _, _) = CreateViewModel();
var dirUser = MakeMember("Alice");
vm.SelectDirectoryUserCommand.Execute(dirUser);
Assert.Single(vm.SelectedUsers);
Assert.Equal("Alice", vm.SelectedUsers[0].DisplayName);
Assert.Equal("alice@contoso.com", vm.SelectedUsers[0].UserPrincipalName);
}
// ── Test 18: SelectDirectoryUserCommand skips duplicates ─────────────────
[Fact]
public void SelectDirectoryUserCommand_skips_duplicates()
{
var (vm, _, _) = CreateViewModel();
var dirUser = MakeMember("Alice");
vm.SelectDirectoryUserCommand.Execute(dirUser);
vm.SelectDirectoryUserCommand.Execute(dirUser);
Assert.Single(vm.SelectedUsers);
}
// ── Test 19: SelectDirectoryUserCommand with null does nothing ───────────
[Fact]
public void SelectDirectoryUserCommand_with_null_does_nothing()
{
var (vm, _, _) = CreateViewModel();
vm.SelectDirectoryUserCommand.Execute(null);
Assert.Empty(vm.SelectedUsers);
}
// ── Test 20: After SelectDirectoryUser, user can be audited ──────────────
[Fact]
public void SelectDirectoryUser_adds_auditable_user_to_SelectedUsers()
{
var (vm, _, _) = CreateViewModel();
var dirUser = MakeMember("Alice");
vm.SelectDirectoryUserCommand.Execute(dirUser);
Assert.True(vm.SelectedUsers.Count > 0);
Assert.Equal("alice@contoso.com", vm.SelectedUsers[0].UserPrincipalName);
}
} }
@@ -394,4 +394,19 @@
<data name="profile.logo.clear" xml:space="preserve"><value>Effacer</value></data> <data name="profile.logo.clear" xml:space="preserve"><value>Effacer</value></data>
<data name="profile.logo.autopull" xml:space="preserve"><value>Importer depuis Entra</value></data> <data name="profile.logo.autopull" xml:space="preserve"><value>Importer depuis Entra</value></data>
<data name="profile.logo.nopreview" xml:space="preserve"><value>Aucun logo configur&#233;</value></data> <data name="profile.logo.nopreview" xml:space="preserve"><value>Aucun logo configur&#233;</value></data>
<!-- Phase 14: Directory Browse UI -->
<data name="audit.mode.search" xml:space="preserve"><value>Recherche</value></data>
<data name="audit.mode.browse" xml:space="preserve"><value>Parcourir l'annuaire</value></data>
<data name="directory.grp.browse" xml:space="preserve"><value>Annuaire utilisateurs</value></data>
<data name="directory.btn.load" xml:space="preserve"><value>Charger l'annuaire</value></data>
<data name="directory.btn.cancel" xml:space="preserve"><value>Annuler</value></data>
<data name="directory.filter.placeholder" xml:space="preserve"><value>Filtrer les utilisateurs...</value></data>
<data name="directory.chk.guests" xml:space="preserve"><value>Inclure les invit&#233;s</value></data>
<data name="directory.status.count" xml:space="preserve"><value>utilisateurs</value></data>
<data name="directory.hint.doubleclick" xml:space="preserve"><value>Double-cliquez sur un utilisateur pour l'ajouter &#224; l'audit</value></data>
<data name="directory.col.name" xml:space="preserve"><value>Nom</value></data>
<data name="directory.col.upn" xml:space="preserve"><value>Courriel</value></data>
<data name="directory.col.department" xml:space="preserve"><value>D&#233;partement</value></data>
<data name="directory.col.jobtitle" xml:space="preserve"><value>Poste</value></data>
<data name="directory.col.type" xml:space="preserve"><value>Type</value></data>
</root> </root>
@@ -394,4 +394,19 @@
<data name="profile.logo.clear" xml:space="preserve"><value>Clear</value></data> <data name="profile.logo.clear" xml:space="preserve"><value>Clear</value></data>
<data name="profile.logo.autopull" xml:space="preserve"><value>Pull from Entra</value></data> <data name="profile.logo.autopull" xml:space="preserve"><value>Pull from Entra</value></data>
<data name="profile.logo.nopreview" xml:space="preserve"><value>No logo configured</value></data> <data name="profile.logo.nopreview" xml:space="preserve"><value>No logo configured</value></data>
<!-- Phase 14: Directory Browse UI -->
<data name="audit.mode.search" xml:space="preserve"><value>Search</value></data>
<data name="audit.mode.browse" xml:space="preserve"><value>Browse Directory</value></data>
<data name="directory.grp.browse" xml:space="preserve"><value>User Directory</value></data>
<data name="directory.btn.load" xml:space="preserve"><value>Load Directory</value></data>
<data name="directory.btn.cancel" xml:space="preserve"><value>Cancel</value></data>
<data name="directory.filter.placeholder" xml:space="preserve"><value>Filter users...</value></data>
<data name="directory.chk.guests" xml:space="preserve"><value>Include guests</value></data>
<data name="directory.status.count" xml:space="preserve"><value>users</value></data>
<data name="directory.hint.doubleclick" xml:space="preserve"><value>Double-click a user to add to audit</value></data>
<data name="directory.col.name" xml:space="preserve"><value>Name</value></data>
<data name="directory.col.upn" xml:space="preserve"><value>Email</value></data>
<data name="directory.col.department" xml:space="preserve"><value>Department</value></data>
<data name="directory.col.jobtitle" xml:space="preserve"><value>Job Title</value></data>
<data name="directory.col.type" xml:space="preserve"><value>Type</value></data>
</root> </root>
@@ -137,6 +137,7 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
public IAsyncRelayCommand ExportHtmlCommand { get; } public IAsyncRelayCommand ExportHtmlCommand { get; }
public RelayCommand<GraphUserResult> AddUserCommand { get; } public RelayCommand<GraphUserResult> AddUserCommand { get; }
public RelayCommand<GraphUserResult> RemoveUserCommand { get; } public RelayCommand<GraphUserResult> RemoveUserCommand { get; }
public RelayCommand<GraphDirectoryUser> SelectDirectoryUserCommand { get; }
public IAsyncRelayCommand LoadDirectoryCommand { get; } public IAsyncRelayCommand LoadDirectoryCommand { get; }
public RelayCommand CancelDirectoryLoadCommand { get; } public RelayCommand CancelDirectoryLoadCommand { get; }
@@ -174,6 +175,7 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser); AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser);
RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser); RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser);
SelectDirectoryUserCommand = new RelayCommand<GraphDirectoryUser>(ExecuteSelectDirectoryUser);
SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel)); SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel));
@@ -216,6 +218,7 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser); AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser);
RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser); RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser);
SelectDirectoryUserCommand = new RelayCommand<GraphDirectoryUser>(ExecuteSelectDirectoryUser);
SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel)); SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel));
@@ -548,6 +551,16 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
SelectedUsers.Remove(user); SelectedUsers.Remove(user);
} }
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);
}
}
private async Task DebounceSearchAsync(string query, CancellationToken ct) private async Task DebounceSearchAsync(string query, CancellationToken ct)
{ {
try try
@@ -15,8 +15,30 @@
<!-- Left panel --> <!-- Left panel -->
<DockPanel Grid.Column="0" Grid.Row="0" Margin="8"> <DockPanel Grid.Column="0" Grid.Row="0" Margin="8">
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.users]}" <!-- Mode toggle -->
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8"> <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 DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8">
<GroupBox.Header>
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[audit.grp.users]}" />
</GroupBox.Header>
<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>
<StackPanel> <StackPanel>
<TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,2" /> <TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,2" />
<TextBlock Text="Searching..." FontStyle="Italic" FontSize="10" Foreground="Gray" Margin="0,0,0,2"> <TextBlock Text="Searching..." FontStyle="Italic" FontSize="10" Foreground="Gray" Margin="0,0,0,2">
@@ -57,6 +79,104 @@
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</StackPanel>
</GroupBox>
<!-- BROWSE MODE PANEL (visible when IsBrowseMode=true) -->
<GroupBox DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8"
Visibility="{Binding IsBrowseMode, Converter={StaticResource BoolToVisibilityConverter}}">
<GroupBox.Header>
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[directory.grp.browse]}" />
</GroupBox.Header>
<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="Name"
Binding="{Binding DisplayName}" Width="120" />
<DataGridTextColumn Header="Email"
Binding="{Binding UserPrincipalName}" Width="140" />
<DataGridTextColumn Header="Department"
Binding="{Binding Department}" Width="90" />
<DataGridTextColumn Header="Job Title"
Binding="{Binding JobTitle}" Width="90" />
<DataGridTemplateColumn Header="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"> <ItemsControl ItemsSource="{Binding SelectedUsers}" Margin="0,0,0,4">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
@@ -75,8 +195,8 @@
</ItemsControl> </ItemsControl>
<TextBlock Text="{Binding SelectedUsersLabel}" FontStyle="Italic" Foreground="Gray" FontSize="10" /> <TextBlock Text="{Binding SelectedUsersLabel}" FontStyle="Italic" Foreground="Gray" FontSize="10" />
</StackPanel> </StackPanel>
</GroupBox>
<!-- Scan Options (always visible) -->
<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>
@@ -89,6 +209,7 @@
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<!-- Run/Export buttons (always visible) -->
<StackPanel DockPanel.Dock="Top"> <StackPanel DockPanel.Dock="Top">
<Grid Margin="0,0,0,4"> <Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -1,4 +1,5 @@
using System.Windows.Controls; using System.Windows.Controls;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services; using SharepointToolbox.Services;
using SharepointToolbox.ViewModels.Tabs; using SharepointToolbox.ViewModels.Tabs;
@@ -24,4 +25,14 @@ public partial class UserAccessAuditView : UserControl
listBox.SelectedItem = null; listBox.SelectedItem = null;
} }
} }
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);
}
}
} }