docs(16-report-consolidation-toggle): create phase plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@
|
|||||||
### v2.3 Tenant Management & Report Enhancements (Phases 15-19)
|
### v2.3 Tenant Management & Report Enhancements (Phases 15-19)
|
||||||
|
|
||||||
- [x] **Phase 15: Consolidation Data Model** (2 plans) — PermissionConsolidator service and merged-row model; zero API calls, pure data shapes (completed 2026-04-09)
|
- [x] **Phase 15: Consolidation Data Model** (2 plans) — PermissionConsolidator service and merged-row model; zero API calls, pure data shapes (completed 2026-04-09)
|
||||||
- [ ] **Phase 16: Report Consolidation Toggle** — Export settings toggle wired to PermissionConsolidator; first user-visible consolidation behavior
|
- [ ] **Phase 16: Report Consolidation Toggle** (2 plans) — Export settings toggle wired to PermissionConsolidator; first user-visible consolidation behavior
|
||||||
- [ ] **Phase 17: Group Expansion in HTML Reports** — Clickable group expansion in HTML exports with transitive membership resolution
|
- [ ] **Phase 17: Group Expansion in HTML Reports** — Clickable group expansion in HTML exports with transitive membership resolution
|
||||||
- [ ] **Phase 18: Auto-Take Ownership** — Global toggle and automatic site collection admin elevation on access denied
|
- [ ] **Phase 18: Auto-Take Ownership** — Global toggle and automatic site collection admin elevation on access denied
|
||||||
- [ ] **Phase 19: App Registration & Removal** — Automated Entra app registration with guided fallback and clean removal
|
- [ ] **Phase 19: App Registration & Removal** — Automated Entra app registration with guided fallback and clean removal
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
4. Existing HTML export services compile and produce identical output when consolidation is not applied (opt-in, defaults off)
|
4. Existing HTML export services compile and produce identical output when consolidation is not applied (opt-in, defaults off)
|
||||||
**Plans:** 2/2 plans complete
|
**Plans:** 2/2 plans complete
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 15-01-PLAN.md — Models (LocationInfo, ConsolidatedPermissionEntry) + PermissionConsolidator service
|
- [x] 15-01-PLAN.md — Models (LocationInfo, ConsolidatedPermissionEntry) + PermissionConsolidator service
|
||||||
- [ ] 15-02-PLAN.md — Unit tests (10 test cases) + full solution build verification
|
- [x] 15-02-PLAN.md — Unit tests (10 test cases) + full solution build verification
|
||||||
|
|
||||||
### Phase 16: Report Consolidation Toggle
|
### Phase 16: Report Consolidation Toggle
|
||||||
**Goal**: Users can choose to merge duplicate permission entries per export through a toggle in the export settings dialog
|
**Goal**: Users can choose to merge duplicate permission entries per export through a toggle in the export settings dialog
|
||||||
@@ -74,7 +74,10 @@ Plans:
|
|||||||
2. When the toggle is OFF, the exported HTML report is byte-for-byte identical to the pre-v2.3 output
|
2. When the toggle is OFF, the exported HTML report is byte-for-byte identical to the pre-v2.3 output
|
||||||
3. When the toggle is ON, the exported HTML report merges rows for the same user with identical access levels into a single row showing all affected locations
|
3. When the toggle is ON, the exported HTML report merges rows for the same user with identical access levels into a single row showing all affected locations
|
||||||
4. The toggle state is remembered for the session (does not reset between exports within the same session)
|
4. The toggle state is remembered for the session (does not reset between exports within the same session)
|
||||||
**Plans**: TBD
|
**Plans:** 2 plans
|
||||||
|
Plans:
|
||||||
|
- [ ] 16-01-PLAN.md — ViewModel properties + XAML Export Options GroupBox + localization + CSV consolidation
|
||||||
|
- [ ] 16-02-PLAN.md — HTML consolidated rendering with expandable location sub-lists + full test verification
|
||||||
|
|
||||||
### Phase 17: Group Expansion in HTML Reports
|
### Phase 17: Group Expansion in HTML Reports
|
||||||
**Goal**: Users can expand SharePoint group entries in HTML reports to see the group's members, including members of nested groups
|
**Goal**: Users can expand SharePoint group entries in HTML reports to see the group's members, including members of nested groups
|
||||||
@@ -108,7 +111,7 @@ Plans:
|
|||||||
3. Registration creates the Azure AD application, service principal, and grants all required API permissions in a single atomic operation — if any step fails, all partial changes are rolled back and the user sees a specific error explaining what failed and why
|
3. Registration creates the Azure AD application, service principal, and grants all required API permissions in a single atomic operation — if any step fails, all partial changes are rolled back and the user sees a specific error explaining what failed and why
|
||||||
4. A "Remove App" action in the profile dialog removes the Azure AD application registration from the target tenant
|
4. A "Remove App" action in the profile dialog removes the Azure AD application registration from the target tenant
|
||||||
5. After removal, all cached MSAL tokens and session state for that tenant are cleared, and subsequent operations require re-authentication
|
5. After removal, all cached MSAL tokens and session state for that tenant are cleared, and subsequent operations require re-authentication
|
||||||
**Plans**: 15-01 (Models + Consolidator), 15-02 (Tests + Build Verification)
|
**Plans**: TBD
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
@@ -117,8 +120,8 @@ 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-14 | v2.2 | 14/14 | Shipped | 2026-04-09 |
|
| 10-14 | v2.2 | 14/14 | Shipped | 2026-04-09 |
|
||||||
| 15. Consolidation Data Model | 2/2 | Complete | 2026-04-09 | — |
|
| 15. Consolidation Data Model | v2.3 | 2/2 | Complete | 2026-04-09 |
|
||||||
| 16. Report Consolidation Toggle | v2.3 | 0/? | Not started | — |
|
| 16. Report Consolidation Toggle | v2.3 | 0/2 | Not started | — |
|
||||||
| 17. Group Expansion in HTML Reports | v2.3 | 0/? | Not started | — |
|
| 17. Group Expansion in HTML Reports | v2.3 | 0/? | Not started | — |
|
||||||
| 18. Auto-Take Ownership | v2.3 | 0/? | Not started | — |
|
| 18. Auto-Take Ownership | v2.3 | 0/? | Not started | — |
|
||||||
| 19. App Registration & Removal | v2.3 | 0/? | Not started | — |
|
| 19. App Registration & Removal | v2.3 | 0/? | Not started | — |
|
||||||
|
|||||||
279
.planning/phases/16-report-consolidation-toggle/16-01-PLAN.md
Normal file
279
.planning/phases/16-report-consolidation-toggle/16-01-PLAN.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
---
|
||||||
|
phase: 16-report-consolidation-toggle
|
||||||
|
plan: "01"
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs
|
||||||
|
- SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml
|
||||||
|
- SharepointToolbox/Views/Tabs/PermissionsView.xaml
|
||||||
|
- SharepointToolbox/Localization/Strings.resx
|
||||||
|
- SharepointToolbox/Localization/Strings.fr.resx
|
||||||
|
- SharepointToolbox/Services/Export/UserAccessCsvExportService.cs
|
||||||
|
autonomous: true
|
||||||
|
requirements: [RPT-03]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "MergePermissions property exists on UserAccessAuditViewModel and defaults to false"
|
||||||
|
- "MergePermissions property exists on PermissionsViewModel and defaults to false (no-op placeholder)"
|
||||||
|
- "Export Options GroupBox with 'Merge duplicate permissions' checkbox is visible in both audit tabs"
|
||||||
|
- "CSV export with mergePermissions=false produces byte-identical output to current behavior"
|
||||||
|
- "CSV export with mergePermissions=true writes consolidated rows with Locations column"
|
||||||
|
artifacts:
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
|
||||||
|
provides: "MergePermissions ObservableProperty + export call site wiring"
|
||||||
|
contains: "_mergePermissions"
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
|
||||||
|
provides: "MergePermissions ObservableProperty (no-op placeholder)"
|
||||||
|
contains: "_mergePermissions"
|
||||||
|
- path: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"
|
||||||
|
provides: "Export Options GroupBox with checkbox"
|
||||||
|
contains: "Export Options"
|
||||||
|
- path: "SharepointToolbox/Views/Tabs/PermissionsView.xaml"
|
||||||
|
provides: "Export Options GroupBox with checkbox"
|
||||||
|
contains: "Export Options"
|
||||||
|
- path: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs"
|
||||||
|
provides: "Consolidated CSV export path"
|
||||||
|
contains: "mergePermissions"
|
||||||
|
key_links:
|
||||||
|
- from: "UserAccessAuditView.xaml"
|
||||||
|
to: "UserAccessAuditViewModel.MergePermissions"
|
||||||
|
via: "XAML Binding"
|
||||||
|
pattern: "IsChecked.*Binding MergePermissions"
|
||||||
|
- from: "UserAccessAuditViewModel.ExportCsvAsync"
|
||||||
|
to: "UserAccessCsvExportService.WriteSingleFileAsync"
|
||||||
|
via: "MergePermissions parameter passthrough"
|
||||||
|
pattern: "WriteSingleFileAsync.*MergePermissions"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Add the MergePermissions toggle property to both ViewModels, wire Export Options GroupBox in both XAML views, add localization keys, and implement the consolidated CSV export path.
|
||||||
|
|
||||||
|
Purpose: Establishes the user-facing toggle and the simpler CSV consolidation path, leaving the complex HTML rendering for Plan 02.
|
||||||
|
Output: Working toggle UI in both tabs, consolidated CSV export, non-consolidated paths unchanged.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/16-report-consolidation-toggle/16-CONTEXT.md
|
||||||
|
@.planning/phases/16-report-consolidation-toggle/16-RESEARCH.md
|
||||||
|
@.planning/phases/15-consolidation-data-model/15-01-SUMMARY.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Phase 15 consolidation API — use directly, do not re-implement -->
|
||||||
|
|
||||||
|
From SharepointToolbox/Core/Helpers/PermissionConsolidator.cs:
|
||||||
|
```csharp
|
||||||
|
public static class PermissionConsolidator
|
||||||
|
{
|
||||||
|
internal static string MakeKey(UserAccessEntry e);
|
||||||
|
public static IReadOnlyList<ConsolidatedPermissionEntry> Consolidate(IReadOnlyList<UserAccessEntry> entries);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs:
|
||||||
|
```csharp
|
||||||
|
public record ConsolidatedPermissionEntry(
|
||||||
|
string UserDisplayName, string UserLogin, string PermissionLevel,
|
||||||
|
string AccessType, string GrantedThrough,
|
||||||
|
bool IsExternalUser, bool IsHighPrivilege,
|
||||||
|
IReadOnlyList<LocationInfo> Locations)
|
||||||
|
{
|
||||||
|
public int LocationCount => Locations.Count;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/Core/Models/LocationInfo.cs:
|
||||||
|
```csharp
|
||||||
|
public record LocationInfo(string SiteUrl, string SiteTitle, string ObjectTitle, string ObjectUrl, string ObjectType);
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/Services/Export/UserAccessCsvExportService.cs:
|
||||||
|
```csharp
|
||||||
|
public class UserAccessCsvExportService
|
||||||
|
{
|
||||||
|
public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList<UserAccessEntry> entries);
|
||||||
|
public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string outputDirectory, CancellationToken ct);
|
||||||
|
public async Task WriteSingleFileAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs (export call sites):
|
||||||
|
```csharp
|
||||||
|
// Line 495 — ExportCsvAsync:
|
||||||
|
await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None);
|
||||||
|
// Line 526 — ExportHtmlAsync:
|
||||||
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add MergePermissions property to both ViewModels and localization keys</name>
|
||||||
|
<files>
|
||||||
|
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs,
|
||||||
|
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs,
|
||||||
|
SharepointToolbox/Localization/Strings.resx,
|
||||||
|
SharepointToolbox/Localization/Strings.fr.resx
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. In `UserAccessAuditViewModel.cs`, add a new `[ObservableProperty]` field after the existing observable properties block (around line 101):
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _mergePermissions;
|
||||||
|
```
|
||||||
|
No partial handler needed — the property defaults to `false` and has no side effects on change.
|
||||||
|
|
||||||
|
2. In `PermissionsViewModel.cs`, add the same `[ObservableProperty]` field in the observable properties section:
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _mergePermissions;
|
||||||
|
```
|
||||||
|
This is a no-op placeholder — PermissionsViewModel does NOT use this value in any export logic.
|
||||||
|
|
||||||
|
3. In `Strings.resx`, add two new entries following the existing naming convention (look at existing keys like `audit.grp.scanOptions`, `chk.includeInherited` etc. for the exact naming pattern):
|
||||||
|
- Key: `audit.grp.export` — Value: `Export Options`
|
||||||
|
- Key: `chk.merge.permissions` — Value: `Merge duplicate permissions`
|
||||||
|
|
||||||
|
4. In `Strings.fr.resx`, add the same two keys:
|
||||||
|
- Key: `audit.grp.export` — Value: `Options d'exportation`
|
||||||
|
- Key: `chk.merge.permissions` — Value: `Fusionner les permissions en double`
|
||||||
|
|
||||||
|
IMPORTANT: Both .resx files MUST have the keys added. Missing French keys cause empty strings in French locale.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Both ViewModels have MergePermissions property that defaults to false. Both .resx files have the two new localization keys. Solution builds without errors or warnings.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add Export Options GroupBox to both XAML views</name>
|
||||||
|
<files>
|
||||||
|
SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml,
|
||||||
|
SharepointToolbox/Views/Tabs/PermissionsView.xaml
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. In `UserAccessAuditView.xaml`, locate the "Scan Options" GroupBox (around lines 199-210 — look for `GroupBox Header="{Binding [audit.grp.scanOptions]..."` or similar). Add a new GroupBox immediately AFTER the Scan Options GroupBox, within the same DockPanel, using the identical pattern:
|
||||||
|
```xml
|
||||||
|
<GroupBox Header="{Binding [audit.grp.export], Source={x:Static loc:TranslationSource.Instance}}"
|
||||||
|
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox Content="{Binding [chk.merge.permissions], Source={x:Static loc:TranslationSource.Instance}}"
|
||||||
|
IsChecked="{Binding MergePermissions}" />
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
```
|
||||||
|
Match the exact `Source={x:Static loc:TranslationSource.Instance}` pattern used by the existing Scan Options GroupBox for localized headers and checkbox labels.
|
||||||
|
|
||||||
|
2. In `PermissionsView.xaml`, locate the "Display Options" GroupBox in the left panel. Add the same Export Options GroupBox after it, using the same XAML pattern as above. The binding `{Binding MergePermissions}` will bind to `PermissionsViewModel.MergePermissions` (the no-op placeholder).
|
||||||
|
|
||||||
|
IMPORTANT: Do NOT modify any existing XAML elements. Only ADD the new GroupBox. The GroupBox must be always visible (not conditionally hidden).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Both XAML views show an "Export Options" GroupBox with a "Merge duplicate permissions" checkbox bound to MergePermissions. Existing UI elements are unchanged.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 3: Implement consolidated CSV export path and wire ViewModel call site</name>
|
||||||
|
<files>
|
||||||
|
SharepointToolbox/Services/Export/UserAccessCsvExportService.cs,
|
||||||
|
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs,
|
||||||
|
SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs
|
||||||
|
</files>
|
||||||
|
<behavior>
|
||||||
|
- RPT-03-f: `WriteSingleFileAsync(entries, path, ct, mergePermissions: false)` produces byte-identical output to current `WriteSingleFileAsync(entries, path, ct)` — capture current output first, then verify no change
|
||||||
|
- RPT-03-g: `WriteSingleFileAsync(entries, path, ct, mergePermissions: true)` writes consolidated CSV with header `"User","User Login","Permission Level","Access Type","Granted Through","Locations","Location Count"` and semicolon-separated site titles in Locations column
|
||||||
|
- Edge case: single-location consolidated entry has LocationCount=1 and Locations=single site title (no semicolons)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. In `UserAccessCsvExportService.cs`, add `bool mergePermissions = false` parameter to `WriteSingleFileAsync`:
|
||||||
|
```csharp
|
||||||
|
public async Task WriteSingleFileAsync(
|
||||||
|
IReadOnlyList<UserAccessEntry> entries,
|
||||||
|
string filePath,
|
||||||
|
CancellationToken ct,
|
||||||
|
bool mergePermissions = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. At the top of `WriteSingleFileAsync`, add an early-return consolidated branch:
|
||||||
|
```csharp
|
||||||
|
if (mergePermissions)
|
||||||
|
{
|
||||||
|
var consolidated = PermissionConsolidator.Consolidate(entries);
|
||||||
|
// Build consolidated CSV with distinct header and rows
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
// Summary section (same pattern as existing)
|
||||||
|
sb.AppendLine("\"User Access Audit Report (Consolidated)\"");
|
||||||
|
sb.AppendLine($"\"Users Audited\",\"{consolidated.Select(e => e.UserLogin).Distinct().Count()}\"");
|
||||||
|
sb.AppendLine($"\"Total Entries\",\"{consolidated.Count}\"");
|
||||||
|
sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("\"User\",\"User Login\",\"Permission Level\",\"Access Type\",\"Granted Through\",\"Locations\",\"Location Count\"");
|
||||||
|
foreach (var entry in consolidated)
|
||||||
|
{
|
||||||
|
var locations = string.Join("; ", entry.Locations.Select(l => l.SiteTitle));
|
||||||
|
sb.AppendLine(string.Join(",", new[]
|
||||||
|
{
|
||||||
|
$"\"{entry.UserDisplayName}\"",
|
||||||
|
$"\"{entry.UserLogin}\"",
|
||||||
|
$"\"{entry.PermissionLevel}\"",
|
||||||
|
$"\"{entry.AccessType}\"",
|
||||||
|
$"\"{entry.GrantedThrough}\"",
|
||||||
|
$"\"{locations}\"",
|
||||||
|
$"\"{entry.LocationCount}\""
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
await File.WriteAllTextAsync(filePath, sb.ToString(), new UTF8Encoding(false), ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Leave the existing code path below this branch COMPLETELY UNTOUCHED.
|
||||||
|
|
||||||
|
3. In `UserAccessAuditViewModel.cs`, update the `ExportCsvAsync` method call site (line ~495):
|
||||||
|
Change: `await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None);`
|
||||||
|
To: `await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions);`
|
||||||
|
|
||||||
|
4. Add test methods in `UserAccessCsvExportServiceTests.cs` for RPT-03-f (non-consolidated identical) and RPT-03-g (consolidated CSV format). Use test data with 2-3 UserAccessEntry rows where 2 share the same consolidation key.
|
||||||
|
|
||||||
|
IMPORTANT: Do NOT modify `BuildCsv` — consolidation applies only at `WriteSingleFileAsync` level per RESEARCH.md Pitfall 4. Do NOT touch the existing code path below the `if (mergePermissions)` branch.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests" --no-restore -v q 2>&1 | tail -10</automated>
|
||||||
|
</verify>
|
||||||
|
<done>CSV export with mergePermissions=false is identical to pre-toggle output. CSV export with mergePermissions=true writes consolidated rows with Locations and LocationCount columns. ViewModel passes MergePermissions to the service.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `dotnet build` succeeds with 0 errors, 0 warnings
|
||||||
|
2. `dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests"` — all tests pass including new consolidation tests
|
||||||
|
3. `dotnet test` — full suite green, no regressions
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- MergePermissions property exists on both ViewModels, defaults to false
|
||||||
|
- Export Options GroupBox visible in both XAML tabs with localized labels
|
||||||
|
- CSV consolidated path produces correct output with merged rows
|
||||||
|
- Non-consolidated CSV path is byte-identical to pre-Phase-16 output
|
||||||
|
- All existing tests pass without modification
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/16-report-consolidation-toggle/16-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
245
.planning/phases/16-report-consolidation-toggle/16-02-PLAN.md
Normal file
245
.planning/phases/16-report-consolidation-toggle/16-02-PLAN.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
---
|
||||||
|
phase: 16-report-consolidation-toggle
|
||||||
|
plan: "02"
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["16-01"]
|
||||||
|
files_modified:
|
||||||
|
- SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
||||||
|
- SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs
|
||||||
|
autonomous: true
|
||||||
|
requirements: [RPT-03]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "HTML export with mergePermissions=false produces byte-identical output to pre-Phase-16 behavior"
|
||||||
|
- "HTML export with mergePermissions=true renders consolidated by-user rows with Sites column"
|
||||||
|
- "Consolidated rows with 1 location show site title inline (no badge)"
|
||||||
|
- "Consolidated rows with 2+ locations show clickable [N sites] badge that expands sub-list"
|
||||||
|
- "By-site view toggle is omitted from HTML when consolidation is ON"
|
||||||
|
- "ViewModel passes MergePermissions to HTML export service"
|
||||||
|
artifacts:
|
||||||
|
- path: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs"
|
||||||
|
provides: "Consolidated HTML rendering with expandable location sub-lists"
|
||||||
|
contains: "mergePermissions"
|
||||||
|
- path: "SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs"
|
||||||
|
provides: "Tests for consolidated and non-consolidated HTML paths"
|
||||||
|
contains: "mergePermissions"
|
||||||
|
key_links:
|
||||||
|
- from: "UserAccessAuditViewModel.ExportHtmlAsync"
|
||||||
|
to: "UserAccessHtmlExportService.WriteAsync"
|
||||||
|
via: "MergePermissions parameter passthrough"
|
||||||
|
pattern: "WriteAsync.*MergePermissions"
|
||||||
|
- from: "UserAccessHtmlExportService.BuildHtml"
|
||||||
|
to: "PermissionConsolidator.Consolidate"
|
||||||
|
via: "Early-return branch when mergePermissions=true"
|
||||||
|
pattern: "PermissionConsolidator\\.Consolidate"
|
||||||
|
- from: "Consolidated HTML"
|
||||||
|
to: "toggleGroup JS"
|
||||||
|
via: "data-group='loc{idx}' on location sub-rows"
|
||||||
|
pattern: "data-group.*loc"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement the consolidated HTML rendering path in UserAccessHtmlExportService — expandable location sub-lists using existing toggleGroup() JS, by-site view suppression, and wire the ViewModel HTML export call site.
|
||||||
|
|
||||||
|
Purpose: Completes the user-visible consolidation behavior for HTML reports — the primary export format.
|
||||||
|
Output: Working consolidated HTML export with expandable site lists, full test coverage.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/16-report-consolidation-toggle/16-CONTEXT.md
|
||||||
|
@.planning/phases/16-report-consolidation-toggle/16-RESEARCH.md
|
||||||
|
@.planning/phases/16-report-consolidation-toggle/16-01-SUMMARY.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- From Plan 01 — MergePermissions is now on the ViewModel -->
|
||||||
|
|
||||||
|
From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs:
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _mergePermissions;
|
||||||
|
|
||||||
|
// ExportHtmlAsync call site (line ~526):
|
||||||
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);
|
||||||
|
// Must become:
|
||||||
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions, branding);
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs:
|
||||||
|
```csharp
|
||||||
|
public class UserAccessHtmlExportService
|
||||||
|
{
|
||||||
|
public string BuildHtml(IReadOnlyList<UserAccessEntry> entries, ReportBranding? branding = null);
|
||||||
|
public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct, ReportBranding? branding = null);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/Core/Helpers/PermissionConsolidator.cs:
|
||||||
|
```csharp
|
||||||
|
public static IReadOnlyList<ConsolidatedPermissionEntry> Consolidate(IReadOnlyList<UserAccessEntry> entries);
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs:
|
||||||
|
```csharp
|
||||||
|
public record ConsolidatedPermissionEntry(
|
||||||
|
string UserDisplayName, string UserLogin, string PermissionLevel,
|
||||||
|
string AccessType, string GrantedThrough,
|
||||||
|
bool IsExternalUser, bool IsHighPrivilege,
|
||||||
|
IReadOnlyList<LocationInfo> Locations)
|
||||||
|
{
|
||||||
|
public int LocationCount => Locations.Count;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing toggleGroup JS (reuse as-is, already in BuildHtml inline JS):
|
||||||
|
```javascript
|
||||||
|
function toggleGroup(id) {
|
||||||
|
var rows = document.querySelectorAll('tr[data-group="' + id + '"]');
|
||||||
|
var isHidden = rows.length > 0 && rows[0].style.display === 'none';
|
||||||
|
rows.forEach(function(r) { r.style.display = isHidden ? '' : 'none'; });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Consolidated HTML column layout (from RESEARCH.md Pattern 4):
|
||||||
|
| Column | Source Field |
|
||||||
|
|--------|-------------|
|
||||||
|
| User | UserDisplayName (+ Guest badge if IsExternalUser) |
|
||||||
|
| Permission Level | PermissionLevel (+ high-priv icon if IsHighPrivilege) |
|
||||||
|
| Access Type | badge from AccessType |
|
||||||
|
| Granted Through | GrantedThrough |
|
||||||
|
| Sites | inline title OR [N sites] badge + expandable sub-list |
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Implement consolidated HTML rendering path in BuildHtml and wire WriteAsync</name>
|
||||||
|
<files>
|
||||||
|
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs,
|
||||||
|
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs,
|
||||||
|
SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs
|
||||||
|
</files>
|
||||||
|
<behavior>
|
||||||
|
- RPT-03-b: `BuildHtml(entries, mergePermissions: false)` output is byte-identical to current `BuildHtml(entries)` output
|
||||||
|
- RPT-03-c: `BuildHtml(entries, mergePermissions: true)` includes consolidated rows and a "Sites" column header
|
||||||
|
- RPT-03-d: When a consolidated entry has LocationCount >= 2, the HTML contains an `[N sites]` badge with `onclick="toggleGroup('loc...')"` and hidden sub-rows with `data-group="loc..."`
|
||||||
|
- RPT-03-e: When mergePermissions=true, the HTML does NOT contain the "By Site" button or `view-site` div
|
||||||
|
- Edge: Single-location consolidated entry renders site title inline (no badge, no expandable rows)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Change `BuildHtml` signature to add `bool mergePermissions = false` as the second parameter (before `branding`):
|
||||||
|
```csharp
|
||||||
|
public string BuildHtml(IReadOnlyList<UserAccessEntry> entries, bool mergePermissions = false, ReportBranding? branding = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. At the very beginning of `BuildHtml`, after the stats computation block, add an early-return branch:
|
||||||
|
```csharp
|
||||||
|
if (mergePermissions)
|
||||||
|
{
|
||||||
|
var consolidated = PermissionConsolidator.Consolidate(entries);
|
||||||
|
return BuildConsolidatedHtml(consolidated, entries, branding);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Leave the ENTIRE existing code path below this branch COMPLETELY UNTOUCHED. Not a single character change.
|
||||||
|
|
||||||
|
3. Create a private `BuildConsolidatedHtml` method that builds the consolidated HTML report:
|
||||||
|
- Reuse the same HTML shell (DOCTYPE, head, CSS, header, stats section, user summary cards) from the existing `BuildHtml` — extract the stats from `entries` (the original flat list) for accurate counts.
|
||||||
|
- Include the existing `toggleGroup()` and `toggleView()` JS functions (copy from existing inline JS).
|
||||||
|
- **OMIT the "By Site" button** from the view toggle bar — only render the "By User" button (or omit the view toggle entirely since there's only one view).
|
||||||
|
- **OMIT the `view-site` div** and its by-site table entirely.
|
||||||
|
- Render a single by-user table with columns: User, Permission Level, Access Type, Granted Through, Sites.
|
||||||
|
- Group consolidated entries by UserLogin (similar to existing user grouping pattern with `ugrp{n}` group headers).
|
||||||
|
- For each `ConsolidatedPermissionEntry` row:
|
||||||
|
- **Sites column — 1 location:** Render `entry.Locations[0].SiteTitle` as plain text.
|
||||||
|
- **Sites column — 2+ locations:** Render `<span class="badge" onclick="toggleGroup('loc{locIdx}')" style="cursor:pointer">{entry.LocationCount} sites</span>`. Immediately after the main `<tr>`, emit hidden sub-rows:
|
||||||
|
```html
|
||||||
|
<tr data-group="loc{locIdx}" style="display:none">
|
||||||
|
<td colspan="5" style="padding-left:2em">
|
||||||
|
<a href="{loc.SiteUrl}">{loc.SiteTitle}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
```
|
||||||
|
- Use a SEPARATE counter `int locIdx = 0` for location group IDs, distinct from user group counter `int grpIdx = 0` (RESEARCH.md Pitfall 2).
|
||||||
|
- Apply the same CSS classes: `.guest-badge` for external users, `.high-priv` for high-privilege rows, `.badge` for access type badges.
|
||||||
|
- Include the same search/filter JS if present in the existing template.
|
||||||
|
|
||||||
|
4. Change `WriteAsync` signature to add `bool mergePermissions = false` after `ct` and before `branding`:
|
||||||
|
```csharp
|
||||||
|
public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct, bool mergePermissions = false, ReportBranding? branding = null)
|
||||||
|
```
|
||||||
|
Pass `mergePermissions` through to `BuildHtml`:
|
||||||
|
```csharp
|
||||||
|
var html = BuildHtml(entries, mergePermissions, branding);
|
||||||
|
```
|
||||||
|
|
||||||
|
5. In `UserAccessAuditViewModel.cs`, update the `ExportHtmlAsync` call site (line ~526):
|
||||||
|
Change: `await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);`
|
||||||
|
To: `await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions, branding);`
|
||||||
|
|
||||||
|
6. Add test methods in `UserAccessHtmlExportServiceTests.cs`:
|
||||||
|
- **RPT-03-b test:** Capture `BuildHtml(testEntries)` output (old signature), then verify `BuildHtml(testEntries, mergePermissions: false)` produces identical string.
|
||||||
|
- **RPT-03-c test:** `BuildHtml(testEntries, mergePermissions: true)` contains "Sites" column header and consolidated row content.
|
||||||
|
- **RPT-03-d test:** Create test data with 2+ entries sharing the same consolidation key. Verify output contains `onclick="toggleGroup('loc` and `data-group="loc` patterns.
|
||||||
|
- **RPT-03-e test:** `BuildHtml(testEntries, mergePermissions: true)` does NOT contain `btn-site` or `view-site`.
|
||||||
|
- Use 3-4 UserAccessEntry test rows where 2 share the same key (same UserLogin+PermissionLevel+AccessType+GrantedThrough but different sites).
|
||||||
|
|
||||||
|
CRITICAL ANTI-PATTERNS:
|
||||||
|
- Do NOT modify any line of the existing non-consolidated code path in BuildHtml. The early-return branch guarantees isolation.
|
||||||
|
- Do NOT reuse the `ugrp` counter for location groups — use `loc{locIdx}` with its own counter.
|
||||||
|
- Do NOT forget to pass mergePermissions through WriteAsync to BuildHtml (Pitfall 3).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests" --no-restore -v q 2>&1 | tail -10</automated>
|
||||||
|
</verify>
|
||||||
|
<done>HTML export with mergePermissions=false is byte-identical to pre-toggle output. HTML export with mergePermissions=true renders consolidated rows with Sites column, expandable location sub-lists for 2+ locations, and omits the by-site view. All tests pass.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Full solution build and test suite verification</name>
|
||||||
|
<files></files>
|
||||||
|
<action>
|
||||||
|
1. Run `dotnet build` on the entire solution to verify zero errors and zero warnings.
|
||||||
|
2. Run `dotnet test` to verify the full test suite passes — all existing tests plus new Phase 16 tests.
|
||||||
|
3. Verify test count has increased by at least 4 (RPT-03-b through RPT-03-g from Plans 01 and 02).
|
||||||
|
4. If any test fails, diagnose and fix. Common issues:
|
||||||
|
- Existing tests calling `BuildHtml(entries)` still work because `mergePermissions` defaults to `false`.
|
||||||
|
- Existing tests calling `WriteAsync(entries, path, ct, branding)` — verify the parameter order change doesn't break existing callers. Since `mergePermissions` is now between `ct` and `branding`, check that no existing call site passes `branding` positionally without the new parameter. If so, fix the call site.
|
||||||
|
- Existing tests calling `WriteSingleFileAsync(entries, path, ct)` still work because `mergePermissions` defaults to `false`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet test -v q 2>&1 | tail -15</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Full solution builds with 0 errors, 0 warnings. All tests pass (existing + new). No regressions.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `dotnet build` — 0 errors, 0 warnings
|
||||||
|
2. `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests"` — all HTML export tests pass
|
||||||
|
3. `dotnet test` — full suite green, no regressions
|
||||||
|
4. Manual spot-check: `BuildHtml(entries, false)` output matches `BuildHtml(entries)` character-for-character
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- HTML consolidated path renders expandable [N sites] badges with toggleGroup integration
|
||||||
|
- HTML non-consolidated path is byte-identical to pre-Phase-16 output
|
||||||
|
- By-site view is suppressed when consolidation is ON
|
||||||
|
- ViewModel wiring passes MergePermissions to WriteAsync
|
||||||
|
- Full test suite passes with no regressions
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/16-report-consolidation-toggle/16-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user