352 lines
14 KiB
Markdown
352 lines
14 KiB
Markdown
---
|
|
phase: 12-branding-ui-views
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs
|
|
- SharepointToolbox/App.xaml
|
|
- SharepointToolbox/Localization/Strings.resx
|
|
- SharepointToolbox/Localization/Strings.fr.resx
|
|
- SharepointToolbox/ViewModels/ProfileManagementViewModel.cs
|
|
- SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs
|
|
- SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs
|
|
autonomous: true
|
|
requirements:
|
|
- BRAND-02
|
|
- BRAND-04
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Base64ToImageSourceConverter converts a data URI string to a non-null BitmapImage"
|
|
- "Base64ToImageSourceConverter returns null for null, empty, or malformed input"
|
|
- "Converter is registered in App.xaml as a global resource with key Base64ToImageConverter"
|
|
- "ProfileManagementViewModel exposes ClientLogoPreview (string?) that updates when SelectedProfile changes, and after Browse/Clear/AutoPull commands"
|
|
- "Localization keys for logo UI exist in both EN and FR resource files"
|
|
artifacts:
|
|
- path: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs"
|
|
provides: "IValueConverter converting data URI strings to BitmapImage for WPF Image binding"
|
|
contains: "class Base64ToImageSourceConverter"
|
|
- path: "SharepointToolbox/App.xaml"
|
|
provides: "Global converter registration"
|
|
contains: "Base64ToImageConverter"
|
|
- path: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
|
|
provides: "ClientLogoPreview observable property for client logo display"
|
|
contains: "ClientLogoPreview"
|
|
- path: "SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs"
|
|
provides: "Unit tests for converter behavior"
|
|
min_lines: 30
|
|
key_links:
|
|
- from: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs"
|
|
to: "SharepointToolbox/App.xaml"
|
|
via: "resource registration"
|
|
pattern: "Base64ToImageConverter"
|
|
- from: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
|
|
to: "SharepointToolbox/Core/Models/LogoData.cs"
|
|
via: "data URI formatting"
|
|
pattern: "ClientLogoPreview"
|
|
---
|
|
|
|
<objective>
|
|
Create the Base64ToImageSourceConverter, add localization keys for logo UI, register the converter globally, and add the ClientLogoPreview property to ProfileManagementViewModel.
|
|
|
|
Purpose: Provides the infrastructure (converter, localization, ViewModel property) that Plans 02 and 03 need to build the XAML views.
|
|
|
|
Output: Converter with tests, localization keys (EN+FR), App.xaml registration, ClientLogoPreview property with test coverage.
|
|
</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/STATE.md
|
|
@.planning/phases/12-branding-ui-views/12-RESEARCH.md
|
|
|
|
<interfaces>
|
|
<!-- SettingsViewModel pattern for logo preview (reference for ProfileManagementViewModel) -->
|
|
|
|
From SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs:
|
|
```csharp
|
|
private string? _mspLogoPreview;
|
|
public string? MspLogoPreview
|
|
{
|
|
get => _mspLogoPreview;
|
|
private set { _mspLogoPreview = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
// Set in LoadAsync:
|
|
var mspLogo = await _brandingService.GetMspLogoAsync();
|
|
MspLogoPreview = mspLogo is not null ? $"data:{mspLogo.MimeType};base64,{mspLogo.Base64}" : null;
|
|
|
|
// Set in BrowseMspLogoAsync:
|
|
MspLogoPreview = $"data:{logo.MimeType};base64,{logo.Base64}";
|
|
|
|
// Set in ClearMspLogoAsync:
|
|
MspLogoPreview = null;
|
|
```
|
|
|
|
From SharepointToolbox/ViewModels/ProfileManagementViewModel.cs (current state):
|
|
```csharp
|
|
// BrowseClientLogoAsync sets SelectedProfile.ClientLogo = logo (LogoData)
|
|
// ClearClientLogoAsync sets SelectedProfile.ClientLogo = null
|
|
// AutoPullClientLogoAsync sets SelectedProfile.ClientLogo = logo
|
|
// NO ClientLogoPreview string property exists — this plan adds it
|
|
```
|
|
|
|
From SharepointToolbox/Core/Models/LogoData.cs:
|
|
```csharp
|
|
public record LogoData
|
|
{
|
|
public string Base64 { get; init; } = string.Empty;
|
|
public string MimeType { get; init; } = string.Empty;
|
|
}
|
|
```
|
|
|
|
From SharepointToolbox/Views/Converters/IndentConverter.cs (converter pattern):
|
|
```csharp
|
|
public class StringToVisibilityConverter : IValueConverter
|
|
{
|
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
=> value is string s && !string.IsNullOrEmpty(s) ? Visibility.Visible : Visibility.Collapsed;
|
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
=> throw new NotImplementedException();
|
|
}
|
|
```
|
|
|
|
From SharepointToolbox/App.xaml (converter registration pattern):
|
|
```xml
|
|
<conv:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Create Base64ToImageSourceConverter with tests</name>
|
|
<files>
|
|
SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs,
|
|
SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs
|
|
</files>
|
|
<behavior>
|
|
- Test 1: Convert with null value returns null
|
|
- Test 2: Convert with empty string returns null
|
|
- Test 3: Convert with non-string value returns null
|
|
- Test 4: Convert with valid data URI "data:image/png;base64,{validBase64}" returns a non-null BitmapImage
|
|
- Test 5: Convert with malformed string (no "base64," prefix) returns null (does not throw)
|
|
- Test 6: ConvertBack throws NotImplementedException
|
|
</behavior>
|
|
<action>
|
|
1. Create `SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs`:
|
|
```csharp
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Windows.Data;
|
|
using System.Windows.Media.Imaging;
|
|
|
|
namespace SharepointToolbox.Views.Converters;
|
|
|
|
public class Base64ToImageSourceConverter : IValueConverter
|
|
{
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
if (value is not string dataUri || string.IsNullOrEmpty(dataUri))
|
|
return null;
|
|
|
|
try
|
|
{
|
|
var marker = "base64,";
|
|
var idx = dataUri.IndexOf(marker, StringComparison.Ordinal);
|
|
if (idx < 0) return null;
|
|
|
|
var base64 = dataUri[(idx + marker.Length)..];
|
|
var bytes = System.Convert.FromBase64String(base64);
|
|
|
|
var image = new BitmapImage();
|
|
using var ms = new MemoryStream(bytes);
|
|
image.BeginInit();
|
|
image.CacheOption = BitmapCacheOption.OnLoad;
|
|
image.StreamSource = ms;
|
|
image.EndInit();
|
|
image.Freeze();
|
|
return image;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> throw new NotImplementedException();
|
|
}
|
|
```
|
|
Key decisions:
|
|
- Parses data URI by finding "base64," marker — works with any mime type
|
|
- `BitmapCacheOption.OnLoad` ensures the stream can be disposed immediately
|
|
- `Freeze()` makes the image cross-thread safe (required for WPF binding)
|
|
- Catches all exceptions to avoid binding errors — returns null on failure
|
|
|
|
2. Create `SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs`:
|
|
Write tests FIRST (RED), then verify GREEN.
|
|
Use `[Trait("Category", "Unit")]` per project convention.
|
|
Note: Tests that create BitmapImage need `[STAThread]` or run on STA thread. Use xUnit's `[WpfFact]` from `Xunit.StaFact` if available, or mark tests with `[Fact]` and handle STA requirement.
|
|
For the valid data URI test, use a minimal valid 1x1 PNG base64: `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==`
|
|
|
|
IMPORTANT: Check if `Xunit.StaFact` NuGet package is referenced in the test project. If not, the BitmapImage tests may need to be skipped or use a workaround (run converter logic that doesn't need STA for null/empty cases, skip the BitmapImage creation test if STA not available).
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Base64ToImageSourceConverter" --no-build -q</automated>
|
|
</verify>
|
|
<done>Converter class exists, handles all edge cases without throwing, tests pass.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Register converter in App.xaml</name>
|
|
<files>
|
|
SharepointToolbox/App.xaml
|
|
</files>
|
|
<behavior>
|
|
- App.xaml contains a Base64ToImageSourceConverter resource with key "Base64ToImageConverter"
|
|
</behavior>
|
|
<action>
|
|
1. In `SharepointToolbox/App.xaml`, add inside `<Application.Resources>`:
|
|
```xml
|
|
<conv:Base64ToImageSourceConverter x:Key="Base64ToImageConverter" />
|
|
```
|
|
Place it after the existing converter registrations.
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror</automated>
|
|
</verify>
|
|
<done>Converter is globally available via StaticResource Base64ToImageConverter.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Add localization keys for logo UI (EN + FR)</name>
|
|
<files>
|
|
SharepointToolbox/Localization/Strings.resx,
|
|
SharepointToolbox/Localization/Strings.fr.resx
|
|
</files>
|
|
<behavior>
|
|
- Both resx files contain matching keys for logo UI labels
|
|
</behavior>
|
|
<action>
|
|
1. Add to `Strings.resx` (EN):
|
|
- `settings.logo.title` = "MSP Logo"
|
|
- `settings.logo.browse` = "Import"
|
|
- `settings.logo.clear` = "Clear"
|
|
- `settings.logo.nopreview` = "No logo configured"
|
|
- `profile.logo.title` = "Client Logo"
|
|
- `profile.logo.browse` = "Import"
|
|
- `profile.logo.clear` = "Clear"
|
|
- `profile.logo.autopull` = "Pull from Entra"
|
|
- `profile.logo.nopreview` = "No logo configured"
|
|
|
|
2. Add to `Strings.fr.resx` (FR):
|
|
- `settings.logo.title` = "Logo MSP"
|
|
- `settings.logo.browse` = "Importer"
|
|
- `settings.logo.clear` = "Effacer"
|
|
- `settings.logo.nopreview` = "Aucun logo configuré"
|
|
- `profile.logo.title` = "Logo client"
|
|
- `profile.logo.browse` = "Importer"
|
|
- `profile.logo.clear` = "Effacer"
|
|
- `profile.logo.autopull` = "Importer depuis Entra"
|
|
- `profile.logo.nopreview` = "Aucun logo configuré"
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror</automated>
|
|
</verify>
|
|
<done>All 9 localization keys exist in both EN and FR resource files.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 4: Add ClientLogoPreview property to ProfileManagementViewModel</name>
|
|
<files>
|
|
SharepointToolbox/ViewModels/ProfileManagementViewModel.cs,
|
|
SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs
|
|
</files>
|
|
<behavior>
|
|
- ProfileManagementViewModel exposes ClientLogoPreview (string?) property
|
|
- ClientLogoPreview updates to data URI when SelectedProfile changes and has a ClientLogo
|
|
- ClientLogoPreview updates to null when SelectedProfile is null or has no ClientLogo
|
|
- BrowseClientLogoAsync updates ClientLogoPreview after successful import
|
|
- ClearClientLogoAsync sets ClientLogoPreview to null
|
|
- AutoPullClientLogoAsync updates ClientLogoPreview after successful pull
|
|
</behavior>
|
|
<action>
|
|
1. Add to `ProfileManagementViewModel.cs`:
|
|
```csharp
|
|
private string? _clientLogoPreview;
|
|
public string? ClientLogoPreview
|
|
{
|
|
get => _clientLogoPreview;
|
|
private set { _clientLogoPreview = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private static string? FormatLogoPreview(LogoData? logo)
|
|
=> logo is not null ? $"data:{logo.MimeType};base64,{logo.Base64}" : null;
|
|
```
|
|
|
|
2. Update `OnSelectedProfileChanged` to refresh preview:
|
|
```csharp
|
|
partial void OnSelectedProfileChanged(TenantProfile? value)
|
|
{
|
|
ClientLogoPreview = FormatLogoPreview(value?.ClientLogo);
|
|
// ... existing NotifyCanExecuteChanged calls ...
|
|
}
|
|
```
|
|
|
|
3. Update `BrowseClientLogoAsync` — after `SelectedProfile.ClientLogo = logo;` add:
|
|
```csharp
|
|
ClientLogoPreview = FormatLogoPreview(logo);
|
|
```
|
|
|
|
4. Update `ClearClientLogoAsync` — after `SelectedProfile.ClientLogo = null;` add:
|
|
```csharp
|
|
ClientLogoPreview = null;
|
|
```
|
|
|
|
5. Update `AutoPullClientLogoAsync` — after `SelectedProfile.ClientLogo = logo;` add:
|
|
```csharp
|
|
ClientLogoPreview = FormatLogoPreview(logo);
|
|
```
|
|
|
|
6. Update existing tests in `ProfileManagementViewModelLogoTests.cs`:
|
|
- Add test: ClientLogoPreview is null when no profile selected
|
|
- Add test: ClientLogoPreview updates when SelectedProfile with logo is selected
|
|
- Add test: ClearClientLogoAsync sets ClientLogoPreview to null
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~ProfileManagementViewModel" --no-build -q</automated>
|
|
</verify>
|
|
<done>ClientLogoPreview property exists and stays in sync with SelectedProfile.ClientLogo across all mutations. Tests pass.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
```bash
|
|
dotnet build --no-restore -warnaserror
|
|
dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Base64ToImageSourceConverter|FullyQualifiedName~ProfileManagementViewModel" --no-build -q
|
|
```
|
|
Both commands must pass with zero failures.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Base64ToImageSourceConverter converts data URI strings to BitmapImage, returns null on bad input
|
|
- Converter registered in App.xaml as "Base64ToImageConverter"
|
|
- 9 localization keys present in both Strings.resx and Strings.fr.resx
|
|
- ProfileManagementViewModel.ClientLogoPreview stays in sync with SelectedProfile.ClientLogo
|
|
- All tests pass, build succeeds with zero warnings
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/12-branding-ui-views/12-01-SUMMARY.md`
|
|
</output>
|