Files
Dev 816fb5e3b5 feat(11-03): inject IBrandingService into all 5 export ViewModels and assemble branding in ExportHtmlAsync
- Add IBrandingService field and DI constructor parameter to all 5 ViewModels
- Add optional IBrandingService? parameter to test constructors (PermissionsViewModel, StorageViewModel, UserAccessAuditViewModel)
- Assemble ReportBranding from GetMspLogoAsync + _currentProfile.ClientLogo before each WriteAsync call
- Pass branding as last parameter to WriteAsync in all ExportHtmlAsync methods
- Guard clause: branding assembly skipped (branding = null) when _brandingService is null (test constructors)
- Build: 0 warnings, 0 errors; tests: 254 passed / 0 failed / 26 skipped
2026-04-08 14:50:54 +02:00

204 lines
8.1 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
using SharepointToolbox.Services.Export;
namespace SharepointToolbox.ViewModels.Tabs;
public partial class SearchViewModel : FeatureViewModelBase
{
private readonly ISearchService _searchService;
private readonly ISessionManager _sessionManager;
private readonly SearchCsvExportService _csvExportService;
private readonly SearchHtmlExportService _htmlExportService;
private readonly IBrandingService _brandingService;
private readonly ILogger<FeatureViewModelBase> _logger;
private TenantProfile? _currentProfile;
// ── Filter observable properties ─────────────────────────────────────────
[ObservableProperty] private string _extensions = string.Empty;
[ObservableProperty] private string _regex = string.Empty;
[ObservableProperty] private bool _useCreatedAfter;
[ObservableProperty] private DateTime _createdAfter = DateTime.Today.AddMonths(-1);
[ObservableProperty] private bool _useCreatedBefore;
[ObservableProperty] private DateTime _createdBefore = DateTime.Today;
[ObservableProperty] private bool _useModifiedAfter;
[ObservableProperty] private DateTime _modifiedAfter = DateTime.Today.AddMonths(-1);
[ObservableProperty] private bool _useModifiedBefore;
[ObservableProperty] private DateTime _modifiedBefore = DateTime.Today;
[ObservableProperty] private string _createdBy = string.Empty;
[ObservableProperty] private string _modifiedBy = string.Empty;
[ObservableProperty] private string _library = string.Empty;
[ObservableProperty] private int _maxResults = 5000;
private ObservableCollection<SearchResult> _results = new();
public ObservableCollection<SearchResult> Results
{
get => _results;
private set
{
_results = value;
OnPropertyChanged();
ExportCsvCommand.NotifyCanExecuteChanged();
ExportHtmlCommand.NotifyCanExecuteChanged();
}
}
public IAsyncRelayCommand ExportCsvCommand { get; }
public IAsyncRelayCommand ExportHtmlCommand { get; }
public TenantProfile? CurrentProfile => _currentProfile;
public SearchViewModel(
ISearchService searchService,
ISessionManager sessionManager,
SearchCsvExportService csvExportService,
SearchHtmlExportService htmlExportService,
IBrandingService brandingService,
ILogger<FeatureViewModelBase> logger)
: base(logger)
{
_searchService = searchService;
_sessionManager = sessionManager;
_csvExportService = csvExportService;
_htmlExportService = htmlExportService;
_brandingService = brandingService;
_logger = logger;
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
}
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
{
if (_currentProfile == null)
{
StatusMessage = "No tenant selected. Please connect to a tenant first.";
return;
}
var urls = GlobalSites.Select(s => s.Url).Where(u => !string.IsNullOrWhiteSpace(u)).ToList();
if (urls.Count == 0)
{
StatusMessage = "Select at least one site from the toolbar.";
return;
}
var allItems = new List<SearchResult>();
foreach (var url in urls)
{
var siteProfile = new TenantProfile
{
TenantUrl = url.TrimEnd('/'),
ClientId = _currentProfile.ClientId,
Name = _currentProfile.Name
};
var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct);
var opts = new SearchOptions(
Extensions: ParseExtensions(Extensions),
Regex: string.IsNullOrWhiteSpace(Regex) ? null : Regex,
CreatedAfter: UseCreatedAfter ? CreatedAfter : null,
CreatedBefore: UseCreatedBefore ? CreatedBefore : null,
ModifiedAfter: UseModifiedAfter ? ModifiedAfter : null,
ModifiedBefore: UseModifiedBefore ? ModifiedBefore : null,
CreatedBy: string.IsNullOrWhiteSpace(CreatedBy) ? null : CreatedBy,
ModifiedBy: string.IsNullOrWhiteSpace(ModifiedBy) ? null : ModifiedBy,
Library: string.IsNullOrWhiteSpace(Library) ? null : Library,
MaxResults: Math.Clamp(MaxResults, 1, 50_000),
SiteUrl: url.TrimEnd('/')
);
var items = await _searchService.SearchFilesAsync(ctx, opts, progress, ct);
allItems.AddRange(items);
}
if (Application.Current?.Dispatcher is { } dispatcher)
await dispatcher.InvokeAsync(() => Results = new ObservableCollection<SearchResult>(allItems));
else
Results = new ObservableCollection<SearchResult>(allItems);
}
protected override void OnTenantSwitched(Core.Models.TenantProfile profile)
{
_currentProfile = profile;
Results = new ObservableCollection<SearchResult>();
OnPropertyChanged(nameof(CurrentProfile));
ExportCsvCommand.NotifyCanExecuteChanged();
ExportHtmlCommand.NotifyCanExecuteChanged();
}
internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile;
private bool CanExport() => Results.Count > 0;
private async Task ExportCsvAsync()
{
if (Results.Count == 0) return;
var dialog = new SaveFileDialog
{
Title = "Export search results to CSV",
Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*",
DefaultExt = "csv",
FileName = "search_results"
};
if (dialog.ShowDialog() != true) return;
try
{
await _csvExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None);
OpenFile(dialog.FileName);
}
catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "CSV export failed."); }
}
private async Task ExportHtmlAsync()
{
if (Results.Count == 0) return;
var dialog = new SaveFileDialog
{
Title = "Export search results to HTML",
Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*",
DefaultExt = "html",
FileName = "search_results"
};
if (dialog.ShowDialog() != true) return;
try
{
ReportBranding? branding = null;
if (_brandingService is not null)
{
var mspLogo = await _brandingService.GetMspLogoAsync();
var clientLogo = _currentProfile?.ClientLogo;
branding = new ReportBranding(mspLogo, clientLogo);
}
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);
OpenFile(dialog.FileName);
}
catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); }
}
private static string[] ParseExtensions(string input)
{
if (string.IsNullOrWhiteSpace(input)) return Array.Empty<string>();
return input.Split(new[] { ' ', ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.TrimStart('.').ToLowerInvariant())
.Where(e => e.Length > 0)
.Distinct()
.ToArray();
}
private static void OpenFile(string filePath)
{
try { Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true }); }
catch { }
}
}