220 lines
9.2 KiB
Markdown
220 lines
9.2 KiB
Markdown
---
|
|
phase: 11-html-export-branding
|
|
plan: 03
|
|
type: execute
|
|
wave: 3
|
|
depends_on: ["11-02"]
|
|
files_modified:
|
|
- SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs
|
|
- SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs
|
|
- SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs
|
|
- SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs
|
|
- SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
|
- SharepointToolbox/App.xaml.cs
|
|
autonomous: true
|
|
requirements:
|
|
- BRAND-05
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Each of the 5 export ViewModels injects IBrandingService and assembles ReportBranding before calling WriteAsync"
|
|
- "ReportBranding is assembled from IBrandingService.GetMspLogoAsync() for MSP logo and _currentProfile.ClientLogo for client logo"
|
|
- "The branding ReportBranding is passed as the last parameter to WriteAsync"
|
|
- "DI container provides IBrandingService to all 5 export ViewModels"
|
|
artifacts:
|
|
- path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
|
|
provides: "Permissions export with branding assembly"
|
|
contains: "IBrandingService"
|
|
- path: "SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs"
|
|
provides: "Search export with branding assembly"
|
|
contains: "IBrandingService"
|
|
- path: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs"
|
|
provides: "Storage export with branding assembly"
|
|
contains: "IBrandingService"
|
|
- path: "SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs"
|
|
provides: "Duplicates export with branding assembly"
|
|
contains: "IBrandingService"
|
|
- path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
|
|
provides: "User access export with branding assembly"
|
|
contains: "IBrandingService"
|
|
key_links:
|
|
- from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
|
|
to: "SharepointToolbox/Services/IBrandingService.cs"
|
|
via: "constructor injection"
|
|
pattern: "IBrandingService _brandingService"
|
|
- from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
|
|
to: "SharepointToolbox/Services/Export/HtmlExportService.cs"
|
|
via: "WriteAsync with branding"
|
|
pattern: "WriteAsync.*branding"
|
|
---
|
|
|
|
<objective>
|
|
Wire IBrandingService into all 5 export ViewModels so each ExportHtmlAsync method assembles a ReportBranding from the MSP logo and the active tenant's client logo, then passes it to WriteAsync.
|
|
|
|
Purpose: Connects the branding infrastructure (Plan 01) and export service changes (Plan 02) to the user-facing export commands. After this plan, HTML exports include branding logos when configured.
|
|
|
|
Output: All 5 export ViewModels inject IBrandingService, assemble ReportBranding in ExportHtmlAsync, and pass it to WriteAsync.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
|
|
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/11-html-export-branding/11-CONTEXT.md
|
|
@.planning/phases/11-html-export-branding/11-RESEARCH.md
|
|
@.planning/phases/11-html-export-branding/11-02-SUMMARY.md
|
|
|
|
<interfaces>
|
|
<!-- From Plan 11-01 -->
|
|
From SharepointToolbox/Core/Models/ReportBranding.cs:
|
|
```csharp
|
|
public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo);
|
|
```
|
|
|
|
<!-- From Phase 10 -->
|
|
From SharepointToolbox/Services/IBrandingService.cs:
|
|
```csharp
|
|
public interface IBrandingService
|
|
{
|
|
Task<LogoData> ImportLogoAsync(string filePath);
|
|
Task SaveMspLogoAsync(LogoData logo);
|
|
Task ClearMspLogoAsync();
|
|
Task<LogoData?> GetMspLogoAsync();
|
|
}
|
|
```
|
|
|
|
<!-- Current ViewModel patterns (all 5 follow this same shape) -->
|
|
DuplicatesViewModel constructor pattern:
|
|
```csharp
|
|
public DuplicatesViewModel(
|
|
IDuplicatesService duplicatesService,
|
|
ISessionManager sessionManager,
|
|
DuplicatesHtmlExportService htmlExportService,
|
|
ILogger<FeatureViewModelBase> logger) : base(logger)
|
|
```
|
|
|
|
Each ViewModel has:
|
|
- `private TenantProfile? _currentProfile;` field set via OnTenantSwitched
|
|
- `ExportHtmlAsync()` method calling `_htmlExportService.WriteAsync(..., CancellationToken.None)`
|
|
|
|
PermissionsViewModel has two constructors: full (DI) and test (internal, omits export services).
|
|
UserAccessAuditViewModel also has two constructors.
|
|
The other 3 ViewModels (Search, Storage, Duplicates) have a single constructor each.
|
|
|
|
DI registrations in App.xaml.cs:
|
|
```csharp
|
|
services.AddSingleton<IBrandingService, BrandingService>();
|
|
```
|
|
IBrandingService is already registered — no new DI registration needed for the service itself.
|
|
But each ViewModel registration must now resolve IBrandingService in addition to existing deps.
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Inject IBrandingService into all 5 export ViewModels and assemble branding in ExportHtmlAsync</name>
|
|
<files>
|
|
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs,
|
|
SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs,
|
|
SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs,
|
|
SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs,
|
|
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
|
</files>
|
|
<action>
|
|
Apply the same pattern to all 5 ViewModels:
|
|
|
|
**For each ViewModel:**
|
|
|
|
1. Add `using SharepointToolbox.Core.Models;` if not already present (needed for `ReportBranding`).
|
|
|
|
2. Add field: `private readonly IBrandingService _brandingService;`
|
|
|
|
3. Modify the DI constructor to accept `IBrandingService brandingService` parameter and assign `_brandingService = brandingService;`.
|
|
|
|
4. For ViewModels with a test constructor (PermissionsViewModel, UserAccessAuditViewModel): add `IBrandingService? brandingService = null` as the last parameter, assign `_brandingService = brandingService!;`. Using `null!` is acceptable because test constructors are only used in tests where branding is not exercised. Alternatively, create a no-op implementation — but `null!` matches existing pattern where `_htmlExportService = null` is already used in test constructors.
|
|
|
|
5. Modify `ExportHtmlAsync()` — add branding assembly BEFORE the WriteAsync call:
|
|
```csharp
|
|
// Assemble branding
|
|
var mspLogo = await _brandingService.GetMspLogoAsync();
|
|
var clientLogo = _currentProfile?.ClientLogo;
|
|
var branding = new ReportBranding(mspLogo, clientLogo);
|
|
```
|
|
Then pass `branding` as the last argument to each `WriteAsync` call.
|
|
|
|
**Specific details per ViewModel:**
|
|
|
|
**PermissionsViewModel** (2 WriteAsync calls in ExportHtmlAsync):
|
|
```csharp
|
|
// Before:
|
|
await _htmlExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CancellationToken.None);
|
|
// After:
|
|
await _htmlExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CancellationToken.None, branding);
|
|
```
|
|
Same for the non-simplified path.
|
|
|
|
**SearchViewModel** (1 WriteAsync call):
|
|
```csharp
|
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);
|
|
```
|
|
|
|
**StorageViewModel** (1 WriteAsync call — the one with FileTypeMetrics):
|
|
```csharp
|
|
await _htmlExportService.WriteAsync(Results, FileTypeMetrics, dialog.FileName, CancellationToken.None, branding);
|
|
```
|
|
|
|
**DuplicatesViewModel** (1 WriteAsync call):
|
|
```csharp
|
|
await _htmlExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None, branding);
|
|
```
|
|
|
|
**UserAccessAuditViewModel** (1 WriteAsync call):
|
|
```csharp
|
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);
|
|
```
|
|
|
|
**Guard clause:** Add a null check on `_brandingService` before the branding assembly to be safe (in case the test constructor was used). If `_brandingService is null`, set `branding = null` (which means no branding header — graceful degradation):
|
|
```csharp
|
|
ReportBranding? branding = null;
|
|
if (_brandingService is not null)
|
|
{
|
|
var mspLogo = await _brandingService.GetMspLogoAsync();
|
|
var clientLogo = _currentProfile?.ClientLogo;
|
|
branding = new ReportBranding(mspLogo, clientLogo);
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --no-build -q</automated>
|
|
</verify>
|
|
<done>All 5 export ViewModels inject IBrandingService, assemble ReportBranding from MSP logo + active profile's ClientLogo, and pass it to WriteAsync. Build and all tests pass. Test constructors gracefully handle null IBrandingService.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
```bash
|
|
dotnet build --no-restore -warnaserror
|
|
dotnet test SharepointToolbox.Tests --no-build -q
|
|
```
|
|
Both commands must pass. Full test suite must pass — existing ViewModel tests must not break from the constructor changes.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- All 5 export ViewModels have IBrandingService injected via constructor
|
|
- ExportHtmlAsync assembles ReportBranding from GetMspLogoAsync + _currentProfile.ClientLogo
|
|
- ReportBranding is passed to WriteAsync as the last parameter
|
|
- Test constructors handle null IBrandingService gracefully (branding = null fallback)
|
|
- All existing ViewModel and export tests pass without modification
|
|
- Build succeeds with zero warnings
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/11-html-export-branding/11-03-SUMMARY.md`
|
|
</output>
|