docs(13-02): complete User Directory ViewModel plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-08 16:08:59 +02:00
parent 4ba4de6106
commit df6f4949a8
140 changed files with 2862 additions and 74 deletions

View File

@@ -0,0 +1,351 @@
---
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>

View File

@@ -0,0 +1,182 @@
---
phase: 12-branding-ui-views
plan: 02
type: execute
wave: 2
depends_on: [12-01]
files_modified:
- SharepointToolbox/Views/Tabs/SettingsView.xaml
autonomous: true
requirements:
- BRAND-02
must_haves:
truths:
- "SettingsView displays an MSP Logo section with a labeled GroupBox below the data folder section"
- "The logo section shows a live thumbnail preview bound to MspLogoPreview via Base64ToImageConverter"
- "When MspLogoPreview is null, the preview area shows a 'No logo configured' placeholder text"
- "Import and Clear buttons are bound to BrowseMspLogoCommand and ClearMspLogoCommand respectively"
- "StatusMessage displays below the logo section when set"
artifacts:
- path: "SharepointToolbox/Views/Tabs/SettingsView.xaml"
provides: "MSP logo section with live preview, import, and clear controls"
contains: "MspLogoPreview"
key_links:
- from: "SharepointToolbox/Views/Tabs/SettingsView.xaml"
to: "SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs"
via: "data binding"
pattern: "BrowseMspLogoCommand|ClearMspLogoCommand|MspLogoPreview"
- from: "SharepointToolbox/Views/Tabs/SettingsView.xaml"
to: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs"
via: "StaticResource"
pattern: "Base64ToImageConverter"
---
<objective>
Add the MSP logo section to SettingsView.xaml with live thumbnail preview, Import and Clear buttons.
Purpose: Allows administrators to see the current MSP logo and manage it directly from the Settings tab.
Output: Updated SettingsView.xaml with a logo section that binds to existing ViewModel commands and properties.
</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 properties and commands (already exist from Phase 11) -->
From SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs:
```csharp
public string? MspLogoPreview { get; } // data URI string or null
public IAsyncRelayCommand BrowseMspLogoCommand { get; }
public IAsyncRelayCommand ClearMspLogoCommand { get; }
public string StatusMessage { get; set; } // inherited from FeatureViewModelBase
```
<!-- Current SettingsView.xaml structure -->
From SharepointToolbox/Views/Tabs/SettingsView.xaml:
```xml
<UserControl ...
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
<StackPanel Margin="16">
<!-- Language section -->
<Label Content="{Binding Source=..., Path=[settings.language]}" />
<ComboBox ... />
<Separator Margin="0,12" />
<!-- Data folder section -->
<Label Content="{Binding Source=..., Path=[settings.folder]}" />
<DockPanel>
<Button DockPanel.Dock="Right" ... Command="{Binding BrowseFolderCommand}" />
<TextBox Text="{Binding DataFolder, ...}" />
</DockPanel>
</StackPanel>
</UserControl>
```
<!-- Available converters from App.xaml -->
- `{StaticResource Base64ToImageConverter}` — converts data URI string to BitmapImage (added in 12-01)
- `{StaticResource StringToVisibilityConverter}` — returns Visible if string non-empty, else Collapsed
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add MSP logo section to SettingsView.xaml</name>
<files>
SharepointToolbox/Views/Tabs/SettingsView.xaml
</files>
<behavior>
- Below the data folder DockPanel, a Separator and a new MSP Logo section appears
- The section has a Label with localized "MSP Logo" text
- A Border contains either an Image (when logo exists) or a TextBlock placeholder (when no logo)
- Image is bound to MspLogoPreview via Base64ToImageConverter, max 80px height, max 240px width
- Placeholder TextBlock shows localized "No logo configured" text, visible only when MspLogoPreview is null/empty
- Two buttons (Import, Clear) are horizontally aligned below the preview
- A TextBlock shows StatusMessage when set (for error feedback)
</behavior>
<action>
1. Edit `SharepointToolbox/Views/Tabs/SettingsView.xaml`:
After the Data folder `</DockPanel>`, before `</StackPanel>`, add:
```xml
<Separator Margin="0,12" />
<!-- MSP Logo -->
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.logo.title]}" />
<Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="8" CornerRadius="4"
HorizontalAlignment="Left" MinWidth="200" MinHeight="60" Margin="0,4,0,0">
<Grid>
<Image Source="{Binding MspLogoPreview, Converter={StaticResource Base64ToImageConverter}}"
MaxHeight="80" MaxWidth="240" Stretch="Uniform" HorizontalAlignment="Left"
Visibility="{Binding MspLogoPreview, Converter={StaticResource StringToVisibilityConverter}}" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.logo.nopreview]}"
VerticalAlignment="Center" HorizontalAlignment="Center"
Foreground="#999999" FontStyle="Italic">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding MspLogoPreview, Converter={StaticResource StringToVisibilityConverter}}" Value="Visible">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Border>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.logo.browse]}"
Command="{Binding BrowseMspLogoCommand}" Width="80" Margin="0,0,8,0" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[settings.logo.clear]}"
Command="{Binding ClearMspLogoCommand}" Width="80" />
</StackPanel>
<TextBlock Text="{Binding StatusMessage}" Foreground="#CC0000" FontSize="11" Margin="0,4,0,0"
Visibility="{Binding StatusMessage, Converter={StaticResource StringToVisibilityConverter}}" />
```
Key decisions:
- Border with light gray outline creates a visual container for the logo preview
- Grid overlays Image and placeholder TextBlock — only one visible at a time
- DataTrigger hides placeholder when StringToVisibilityConverter returns Visible
- MaxHeight="80" and MaxWidth="240" keep the preview small but readable
- Stretch="Uniform" preserves aspect ratio
- StatusMessage in red only shows when non-empty (error feedback from import failures)
</action>
<verify>
<automated>dotnet build --no-restore -warnaserror</automated>
</verify>
<done>SettingsView shows MSP logo section with live preview, Import/Clear buttons, and error message area. Build passes.</done>
</task>
</tasks>
<verification>
```bash
dotnet build --no-restore -warnaserror
```
Build must pass with zero failures. Visual verification requires manual testing.
</verification>
<success_criteria>
- SettingsView.xaml has a visible MSP Logo section below the data folder
- Image binds to MspLogoPreview via Base64ToImageConverter
- Placeholder text shows when no logo is configured
- Import and Clear buttons bind to existing ViewModel commands
- StatusMessage displays in red when set
- Build passes with zero warnings
</success_criteria>
<output>
After completion, create `.planning/phases/12-branding-ui-views/12-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,203 @@
---
phase: 12-branding-ui-views
plan: 03
type: execute
wave: 2
depends_on: [12-01]
files_modified:
- SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml
autonomous: true
requirements:
- BRAND-04
must_haves:
truths:
- "ProfileManagementDialog shows a Client Logo section between the input fields and the action buttons"
- "The logo section shows a live thumbnail preview bound to ClientLogoPreview via Base64ToImageConverter"
- "When ClientLogoPreview is null, the preview area shows a 'No logo configured' placeholder text"
- "Import, Clear, and Pull from Entra buttons are bound to BrowseClientLogoCommand, ClearClientLogoCommand, and AutoPullClientLogoCommand respectively"
- "All three logo buttons are disabled when no profile is selected"
- "ValidationMessage displays below the logo buttons when set"
- "Dialog height is increased to accommodate the new section"
artifacts:
- path: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml"
provides: "Client logo section with live preview, import, clear, and auto-pull controls"
contains: "ClientLogoPreview"
key_links:
- from: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml"
to: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
via: "data binding"
pattern: "BrowseClientLogoCommand|ClearClientLogoCommand|AutoPullClientLogoCommand|ClientLogoPreview"
- from: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml"
to: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs"
via: "StaticResource"
pattern: "Base64ToImageConverter"
---
<objective>
Add the client logo section to ProfileManagementDialog.xaml with live thumbnail preview, Import, Clear, and Pull from Entra buttons.
Purpose: Allows administrators to see, import, clear, and auto-pull client logos per tenant directly from the profile management dialog.
Output: Updated ProfileManagementDialog.xaml with a client logo section and increased dialog height.
</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>
<!-- ProfileManagementViewModel properties and commands (Phase 11 + 12-01) -->
From SharepointToolbox/ViewModels/ProfileManagementViewModel.cs:
```csharp
public string? ClientLogoPreview { get; } // data URI string or null (added in 12-01)
public IAsyncRelayCommand BrowseClientLogoCommand { get; } // gated on SelectedProfile != null
public IAsyncRelayCommand ClearClientLogoCommand { get; } // gated on SelectedProfile != null
public IAsyncRelayCommand AutoPullClientLogoCommand { get; } // gated on SelectedProfile != null
public string ValidationMessage { get; set; } // set on error or success feedback
public TenantProfile? SelectedProfile { get; set; }
```
<!-- Current ProfileManagementDialog.xaml structure -->
From SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml:
```xml
<Window ... Width="500" Height="480" ResizeMode="NoResize">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Row 0: Label "Profiles" -->
<RowDefinition Height="*" /> <!-- Row 1: Profile ListBox -->
<RowDefinition Height="Auto" /> <!-- Row 2: Input fields (Name/URL/ClientId) -->
<RowDefinition Height="Auto" /> <!-- Row 3: Action buttons -->
</Grid.RowDefinitions>
...
</Grid>
</Window>
```
<!-- Available converters from App.xaml -->
- `{StaticResource Base64ToImageConverter}` — converts data URI string to BitmapImage
- `{StaticResource StringToVisibilityConverter}` — Visible if non-empty, else Collapsed
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add client logo section and resize ProfileManagementDialog</name>
<files>
SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml
</files>
<behavior>
- Dialog height increases from 480 to 620 to accommodate the logo section
- A new row (Row 3) is inserted between the input fields (Row 2) and buttons (now Row 4)
- The client logo section contains:
a) A labeled GroupBox "Client Logo" (localized)
b) Inside: a Border with either an Image preview or placeholder text
c) Three buttons: Import, Clear, Pull from Entra — horizontally aligned
d) A TextBlock for ValidationMessage feedback
- All logo controls are visually disabled when no profile is selected (via command CanExecute)
- ValidationMessage shows success/error messages (already set by ViewModel commands)
</behavior>
<action>
1. Edit `SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml`:
a) Increase dialog height from 480 to 620:
Change `Height="480"` to `Height="620"`
b) Add a new Row 3 for the logo section. Update RowDefinitions to:
```xml
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Row 0: Label -->
<RowDefinition Height="*" /> <!-- Row 1: ListBox -->
<RowDefinition Height="Auto" /> <!-- Row 2: Input fields -->
<RowDefinition Height="Auto" /> <!-- Row 3: Client logo (NEW) -->
<RowDefinition Height="Auto" /> <!-- Row 4: Buttons (was Row 3) -->
</Grid.RowDefinitions>
```
c) Move existing buttons StackPanel from Grid.Row="3" to Grid.Row="4"
d) Add the client logo section at Grid.Row="3":
```xml
<!-- Client Logo -->
<StackPanel Grid.Row="3" Margin="0,8,0,8">
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.title]}" Padding="0,0,0,4" />
<Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="8" CornerRadius="4"
HorizontalAlignment="Left" MinWidth="200" MinHeight="50">
<Grid>
<Image Source="{Binding ClientLogoPreview, Converter={StaticResource Base64ToImageConverter}}"
MaxHeight="60" MaxWidth="200" Stretch="Uniform" HorizontalAlignment="Left"
Visibility="{Binding ClientLogoPreview, Converter={StaticResource StringToVisibilityConverter}}" />
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.nopreview]}"
VerticalAlignment="Center" HorizontalAlignment="Center"
Foreground="#999999" FontStyle="Italic">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding ClientLogoPreview, Converter={StaticResource StringToVisibilityConverter}}" Value="Visible">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Border>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.browse]}"
Command="{Binding BrowseClientLogoCommand}" Width="80" Margin="0,0,8,0" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.clear]}"
Command="{Binding ClearClientLogoCommand}" Width="80" Margin="0,0,8,0" />
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.logo.autopull]}"
Command="{Binding AutoPullClientLogoCommand}" Width="130" />
</StackPanel>
<TextBlock Text="{Binding ValidationMessage}" Foreground="#CC0000" FontSize="11" Margin="0,4,0,0"
Visibility="{Binding ValidationMessage, Converter={StaticResource StringToVisibilityConverter}}" />
</StackPanel>
```
Key decisions:
- GroupBox replaced with Label + StackPanel for consistency with SettingsView pattern
- Smaller preview (60px height vs 80px in Settings) because dialog has less space
- Pull from Entra button is wider (130px) to fit localized text
- ValidationMessage already set by Browse/Clear/AutoPull commands — just needs display
- All three buttons auto-disable via ICommand.CanExecute when SelectedProfile is null
</action>
<verify>
<automated>dotnet build --no-restore -warnaserror</automated>
</verify>
<done>ProfileManagementDialog shows client logo section with preview, three buttons, and feedback. Dialog resized. Build passes.</done>
</task>
</tasks>
<verification>
```bash
dotnet build --no-restore -warnaserror
```
Build must pass with zero failures. Visual verification requires manual testing.
</verification>
<success_criteria>
- ProfileManagementDialog.xaml has a visible Client Logo section between input fields and buttons
- Image binds to ClientLogoPreview via Base64ToImageConverter
- Placeholder text shows when no logo is configured
- Import, Clear, and Pull from Entra buttons bind to existing ViewModel commands
- All logo buttons disabled when no profile selected
- ValidationMessage displays feedback when set
- Dialog height increased to 620 to accommodate new section
- Build passes with zero warnings
</success_criteria>
<output>
After completion, create `.planning/phases/12-branding-ui-views/12-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,54 @@
# Phase 12 Research: Branding UI Views
## What Exists (Phase 11 Deliverables)
### SettingsViewModel (already complete)
- `BrowseMspLogoCommand` (IAsyncRelayCommand) — opens file dialog, imports via IBrandingService, saves, updates preview
- `ClearMspLogoCommand` (IAsyncRelayCommand) — clears via IBrandingService, nulls preview
- `MspLogoPreview` (string?) — data URI format `data:{mime};base64,{b64}`, set on load and after browse/clear
- `StatusMessage` — inherited from FeatureViewModelBase, set on error
### ProfileManagementViewModel (already complete)
- `BrowseClientLogoCommand` — opens file dialog, imports, persists to profile
- `ClearClientLogoCommand` — nulls ClientLogo, persists
- `AutoPullClientLogoCommand` — fetches from Entra branding API, persists
- `ValidationMessage` — set on error or success feedback
- **GAP**: No `ClientLogoPreview` string property — SelectedProfile.ClientLogo is a LogoData object, NOT a data URI string. TenantProfile is not ObservableObject, so binding to SelectedProfile.ClientLogo won't notify UI on change.
### SettingsView.xaml (NO logo UI)
- Current: Language combo + Data folder text+browse — that's it
- Need: Add MSP logo section with Image preview, Browse, Clear buttons
### ProfileManagementDialog.xaml (NO logo UI)
- Current: Profile ListBox, Name/URL/ClientId fields, Add/Rename/Delete/Close buttons
- Window: 500x480, NoResize
- Need: Add client logo section with Image preview, Browse, Clear, Auto-Pull buttons; resize dialog
## Infrastructure Gaps
### No Image Converter
- `MspLogoPreview` is a data URI string — WPF `<Image Source=...>` does NOT natively bind to data URI strings
- Need `Base64ToImageSourceConverter` IValueConverter: parse data URI → decode base64 → create BitmapImage from byte stream
- Register in App.xaml as global resource
### Localization Keys Missing
- No keys for logo UI labels/buttons in Strings.resx / Strings.fr.resx
- Need: `settings.logo.msp`, `settings.logo.browse`, `settings.logo.clear`, `profile.logo.client`, `profile.logo.browse`, `profile.logo.clear`, `profile.logo.autopull`, `logo.nopreview`
## Available Patterns
### Converters
- Live in `SharepointToolbox/Views/Converters/` (IndentConverter.cs has multiple converters)
- Registered in App.xaml under `<Application.Resources>`
- `StringToVisibilityConverter` already exists — can show/hide preview based on non-null string
### XAML Layout
- SettingsView uses `<StackPanel>` with `<Separator>` between sections
- ProfileManagementDialog uses `<Grid>` with row definitions
- Buttons: `<Button Content="{Binding Source=...}" Command="{Binding ...}" Width="60" Margin="4,0" />`
## Plan Breakdown
1. **12-01**: Base64ToImageSourceConverter + localization keys + App.xaml registration + ClientLogoPreview ViewModel property
2. **12-02**: SettingsView.xaml MSP logo section
3. **12-03**: ProfileManagementDialog.xaml client logo section + dialog resize