Files
Sharepoint-Toolbox/.planning/phases/12-branding-ui-views/12-01-PLAN.md
Dev df6f4949a8 docs(13-02): complete User Directory ViewModel plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:44:56 +02:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
12-branding-ui-views 01 execute 1
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
true
BRAND-02
BRAND-04
truths artifacts key_links
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
path provides contains
SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs IValueConverter converting data URI strings to BitmapImage for WPF Image binding class Base64ToImageSourceConverter
path provides contains
SharepointToolbox/App.xaml Global converter registration Base64ToImageConverter
path provides contains
SharepointToolbox/ViewModels/ProfileManagementViewModel.cs ClientLogoPreview observable property for client logo display ClientLogoPreview
path provides min_lines
SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs Unit tests for converter behavior 30
from to via pattern
SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs SharepointToolbox/App.xaml resource registration Base64ToImageConverter
from to via pattern
SharepointToolbox/ViewModels/ProfileManagementViewModel.cs SharepointToolbox/Core/Models/LogoData.cs data URI formatting 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.

<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>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/12-branding-ui-views/12-RESEARCH.md

From SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs:

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):

// 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:

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):

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):

<conv:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
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.

<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>
After completion, create `.planning/phases/12-branding-ui-views/12-01-SUMMARY.md`