--- 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" --- 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. @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/12-branding-ui-views/12-RESEARCH.md 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 ``` Task 1: Create Base64ToImageSourceConverter with tests SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs, SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs - 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 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). dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Base64ToImageSourceConverter" --no-build -q Converter class exists, handles all edge cases without throwing, tests pass. Task 2: Register converter in App.xaml SharepointToolbox/App.xaml - App.xaml contains a Base64ToImageSourceConverter resource with key "Base64ToImageConverter" 1. In `SharepointToolbox/App.xaml`, add inside ``: ```xml ``` Place it after the existing converter registrations. dotnet build --no-restore -warnaserror Converter is globally available via StaticResource Base64ToImageConverter. Task 3: Add localization keys for logo UI (EN + FR) SharepointToolbox/Localization/Strings.resx, SharepointToolbox/Localization/Strings.fr.resx - Both resx files contain matching keys for logo UI labels 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é" dotnet build --no-restore -warnaserror All 9 localization keys exist in both EN and FR resource files. Task 4: Add ClientLogoPreview property to ProfileManagementViewModel SharepointToolbox/ViewModels/ProfileManagementViewModel.cs, SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs - 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 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 dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~ProfileManagementViewModel" --no-build -q ClientLogoPreview property exists and stays in sync with SelectedProfile.ClientLogo across all mutations. Tests pass. ```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. - 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 After completion, create `.planning/phases/12-branding-ui-views/12-01-SUMMARY.md`