feat(11-04): add logo management commands to SettingsViewModel and ProfileManagementViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-08 14:40:08 +02:00
parent d4fa402f04
commit b02b75e5bc
5 changed files with 346 additions and 3 deletions

View File

@@ -1,15 +1,20 @@
using System.Collections.ObjectModel;
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
namespace SharepointToolbox.ViewModels;
public partial class ProfileManagementViewModel : ObservableObject
{
private readonly ProfileService _profileService;
private readonly IBrandingService _brandingService;
private readonly AppGraphClientFactory _graphClientFactory;
private readonly ILogger<ProfileManagementViewModel> _logger;
[ObservableProperty]
@@ -32,15 +37,27 @@ public partial class ProfileManagementViewModel : ObservableObject
public IAsyncRelayCommand AddCommand { get; }
public IAsyncRelayCommand RenameCommand { get; }
public IAsyncRelayCommand DeleteCommand { get; }
public IAsyncRelayCommand BrowseClientLogoCommand { get; }
public IAsyncRelayCommand ClearClientLogoCommand { get; }
public IAsyncRelayCommand AutoPullClientLogoCommand { get; }
public ProfileManagementViewModel(ProfileService profileService, ILogger<ProfileManagementViewModel> logger)
public ProfileManagementViewModel(
ProfileService profileService,
IBrandingService brandingService,
AppGraphClientFactory graphClientFactory,
ILogger<ProfileManagementViewModel> logger)
{
_profileService = profileService;
_brandingService = brandingService;
_graphClientFactory = graphClientFactory;
_logger = logger;
AddCommand = new AsyncRelayCommand(AddAsync, CanAdd);
RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedProfile != null && !string.IsNullOrWhiteSpace(NewName));
DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedProfile != null);
BrowseClientLogoCommand = new AsyncRelayCommand(BrowseClientLogoAsync, () => SelectedProfile != null);
ClearClientLogoCommand = new AsyncRelayCommand(ClearClientLogoAsync, () => SelectedProfile != null);
AutoPullClientLogoCommand = new AsyncRelayCommand(AutoPullClientLogoAsync, () => SelectedProfile != null);
}
public async Task LoadAsync()
@@ -62,6 +79,15 @@ public partial class ProfileManagementViewModel : ObservableObject
partial void OnNewTenantUrlChanged(string value) => NotifyCommandsCanExecuteChanged();
partial void OnNewClientIdChanged(string value) => NotifyCommandsCanExecuteChanged();
partial void OnSelectedProfileChanged(TenantProfile? value)
{
BrowseClientLogoCommand.NotifyCanExecuteChanged();
ClearClientLogoCommand.NotifyCanExecuteChanged();
AutoPullClientLogoCommand.NotifyCanExecuteChanged();
RenameCommand.NotifyCanExecuteChanged();
DeleteCommand.NotifyCanExecuteChanged();
}
private void NotifyCommandsCanExecuteChanged()
{
AddCommand.NotifyCanExecuteChanged();
@@ -132,4 +158,88 @@ public partial class ProfileManagementViewModel : ObservableObject
_logger.LogError(ex, "Failed to delete profile.");
}
}
private async Task BrowseClientLogoAsync()
{
if (SelectedProfile == null) return;
var dialog = new OpenFileDialog
{
Title = "Select client logo",
Filter = "Image files (*.png;*.jpg;*.jpeg)|*.png;*.jpg;*.jpeg",
};
if (dialog.ShowDialog() != true) return;
try
{
var logo = await _brandingService.ImportLogoAsync(dialog.FileName);
SelectedProfile.ClientLogo = logo;
await _profileService.UpdateProfileAsync(SelectedProfile);
ValidationMessage = string.Empty;
}
catch (Exception ex)
{
ValidationMessage = ex.Message;
_logger.LogError(ex, "Failed to import client logo.");
}
}
private async Task ClearClientLogoAsync()
{
if (SelectedProfile == null) return;
try
{
SelectedProfile.ClientLogo = null;
await _profileService.UpdateProfileAsync(SelectedProfile);
ValidationMessage = string.Empty;
}
catch (Exception ex)
{
ValidationMessage = ex.Message;
_logger.LogError(ex, "Failed to clear client logo.");
}
}
private async Task AutoPullClientLogoAsync()
{
if (SelectedProfile == null) return;
try
{
var graphClient = await _graphClientFactory.CreateClientAsync(
SelectedProfile.ClientId, CancellationToken.None);
var orgs = await graphClient.Organization.GetAsync();
var orgId = orgs?.Value?.FirstOrDefault()?.Id;
if (orgId is null)
{
ValidationMessage = "Could not determine organization ID.";
return;
}
var stream = await graphClient.Organization[orgId]
.Branding.Localizations["default"].SquareLogo.GetAsync();
if (stream is null || stream.Length == 0)
{
ValidationMessage = "No branding logo found for this tenant.";
return;
}
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var bytes = ms.ToArray();
var logo = await _brandingService.ImportLogoFromBytesAsync(bytes);
SelectedProfile.ClientLogo = logo;
await _profileService.UpdateProfileAsync(SelectedProfile);
ValidationMessage = "Client logo pulled from Entra branding.";
}
catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == 404)
{
ValidationMessage = "No Entra branding configured for this tenant.";
}
catch (Exception ex)
{
ValidationMessage = $"Failed to pull logo: {ex.Message}";
_logger.LogWarning(ex, "Auto-pull client logo failed.");
}
}
}