- StorageViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride - SearchViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride - DuplicatesViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride - FolderStructureViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride - All four VMs pre-fill SiteUrl from first global site; local typing sets override flag - Tenant switch resets _hasLocalSiteOverride in all four VMs
246 lines
7.9 KiB
C#
246 lines
7.9 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 StorageViewModel : FeatureViewModelBase
|
|
{
|
|
private readonly IStorageService _storageService;
|
|
private readonly ISessionManager _sessionManager;
|
|
private readonly StorageCsvExportService _csvExportService;
|
|
private readonly StorageHtmlExportService _htmlExportService;
|
|
private readonly ILogger<FeatureViewModelBase> _logger;
|
|
private TenantProfile? _currentProfile;
|
|
private bool _hasLocalSiteOverride;
|
|
|
|
[ObservableProperty]
|
|
private string _siteUrl = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private bool _perLibrary = true;
|
|
|
|
[ObservableProperty]
|
|
private bool _includeSubsites;
|
|
|
|
[ObservableProperty]
|
|
private int _folderDepth;
|
|
|
|
public bool IsMaxDepth
|
|
{
|
|
get => FolderDepth >= 999;
|
|
set
|
|
{
|
|
if (value) FolderDepth = 999;
|
|
else if (FolderDepth >= 999) FolderDepth = 0;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
private ObservableCollection<StorageNode> _results = new();
|
|
public ObservableCollection<StorageNode> 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 StorageViewModel(
|
|
IStorageService storageService,
|
|
ISessionManager sessionManager,
|
|
StorageCsvExportService csvExportService,
|
|
StorageHtmlExportService htmlExportService,
|
|
ILogger<FeatureViewModelBase> logger)
|
|
: base(logger)
|
|
{
|
|
_storageService = storageService;
|
|
_sessionManager = sessionManager;
|
|
_csvExportService = csvExportService;
|
|
_htmlExportService = htmlExportService;
|
|
_logger = logger;
|
|
|
|
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
|
|
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
|
|
}
|
|
|
|
/// <summary>Test constructor — omits export services.</summary>
|
|
internal StorageViewModel(
|
|
IStorageService storageService,
|
|
ISessionManager sessionManager,
|
|
ILogger<FeatureViewModelBase> logger)
|
|
: base(logger)
|
|
{
|
|
_storageService = storageService;
|
|
_sessionManager = sessionManager;
|
|
_csvExportService = null!;
|
|
_htmlExportService = null!;
|
|
_logger = logger;
|
|
|
|
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
|
|
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
|
|
}
|
|
|
|
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
|
{
|
|
if (_hasLocalSiteOverride) return;
|
|
SiteUrl = sites.Count > 0 ? sites[0].Url : string.Empty;
|
|
}
|
|
|
|
partial void OnSiteUrlChanged(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
_hasLocalSiteOverride = false;
|
|
if (GlobalSites.Count > 0)
|
|
SiteUrl = GlobalSites[0].Url;
|
|
}
|
|
else if (GlobalSites.Count == 0 || value != GlobalSites[0].Url)
|
|
{
|
|
_hasLocalSiteOverride = true;
|
|
}
|
|
}
|
|
|
|
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
|
{
|
|
if (_currentProfile == null)
|
|
{
|
|
StatusMessage = "No tenant selected. Please connect to a tenant first.";
|
|
return;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(SiteUrl))
|
|
{
|
|
StatusMessage = "Please enter a site URL.";
|
|
return;
|
|
}
|
|
|
|
// Build a site-specific profile: same ClientId and Name, but TenantUrl points to the
|
|
// site URL the user entered (may differ from the tenant root).
|
|
var siteProfile = new TenantProfile
|
|
{
|
|
TenantUrl = SiteUrl.TrimEnd('/'),
|
|
ClientId = _currentProfile.ClientId,
|
|
Name = _currentProfile.Name
|
|
};
|
|
var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct);
|
|
|
|
var options = new StorageScanOptions(
|
|
PerLibrary: PerLibrary,
|
|
IncludeSubsites: IncludeSubsites,
|
|
FolderDepth: FolderDepth);
|
|
|
|
var nodes = await _storageService.CollectStorageAsync(ctx, options, progress, ct);
|
|
|
|
// Flatten tree to one level for DataGrid display (assign IndentLevel during flatten)
|
|
var flat = new List<StorageNode>();
|
|
foreach (var node in nodes)
|
|
FlattenNode(node, 0, flat);
|
|
|
|
if (Application.Current?.Dispatcher is { } dispatcher)
|
|
{
|
|
await dispatcher.InvokeAsync(() =>
|
|
{
|
|
Results = new ObservableCollection<StorageNode>(flat);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Results = new ObservableCollection<StorageNode>(flat);
|
|
}
|
|
}
|
|
|
|
protected override void OnTenantSwitched(TenantProfile profile)
|
|
{
|
|
_currentProfile = profile;
|
|
_hasLocalSiteOverride = false;
|
|
Results = new ObservableCollection<StorageNode>();
|
|
SiteUrl = string.Empty;
|
|
OnPropertyChanged(nameof(CurrentProfile));
|
|
ExportCsvCommand.NotifyCanExecuteChanged();
|
|
ExportHtmlCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile;
|
|
|
|
internal Task TestRunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
|
=> RunOperationAsync(ct, progress);
|
|
|
|
private bool CanExport() => Results.Count > 0;
|
|
|
|
private async Task ExportCsvAsync()
|
|
{
|
|
if (Results.Count == 0) return;
|
|
var dialog = new SaveFileDialog
|
|
{
|
|
Title = "Export storage metrics to CSV",
|
|
Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*",
|
|
DefaultExt = "csv",
|
|
FileName = "storage_metrics"
|
|
};
|
|
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 storage metrics to HTML",
|
|
Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*",
|
|
DefaultExt = "html",
|
|
FileName = "storage_metrics"
|
|
};
|
|
if (dialog.ShowDialog() != true) return;
|
|
try
|
|
{
|
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None);
|
|
OpenFile(dialog.FileName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Export failed: {ex.Message}";
|
|
_logger.LogError(ex, "HTML export failed.");
|
|
}
|
|
}
|
|
|
|
private static void FlattenNode(StorageNode node, int level, List<StorageNode> result)
|
|
{
|
|
node.IndentLevel = level;
|
|
result.Add(node);
|
|
foreach (var child in node.Children)
|
|
FlattenNode(child, level + 1, result);
|
|
}
|
|
|
|
private static void OpenFile(string filePath)
|
|
{
|
|
try { Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true }); }
|
|
catch { /* ignore — file may open but this is best-effort */ }
|
|
}
|
|
}
|