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:
351
.planning/phases/12-branding-ui-views/12-01-PLAN.md
Normal file
351
.planning/phases/12-branding-ui-views/12-01-PLAN.md
Normal 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>
|
||||
182
.planning/phases/12-branding-ui-views/12-02-PLAN.md
Normal file
182
.planning/phases/12-branding-ui-views/12-02-PLAN.md
Normal 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>
|
||||
203
.planning/phases/12-branding-ui-views/12-03-PLAN.md
Normal file
203
.planning/phases/12-branding-ui-views/12-03-PLAN.md
Normal 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>
|
||||
54
.planning/phases/12-branding-ui-views/12-RESEARCH.md
Normal file
54
.planning/phases/12-branding-ui-views/12-RESEARCH.md
Normal 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
|
||||
Reference in New Issue
Block a user