Merge remote-tracking branch 'kawa/main'

This commit is contained in:
Dev
2026-04-29 17:55:56 +02:00
20 changed files with 782 additions and 82 deletions
@@ -41,6 +41,39 @@ public partial class StorageViewModel : FeatureViewModelBase
[ObservableProperty]
private bool _isDonutChart = true;
// ── Scan-time flags (control what is captured during the CSOM scan) ─────
[ObservableProperty] private bool _scanHiddenLibraries = true;
[ObservableProperty] private bool _scanPreservationHold = true;
[ObservableProperty] private bool _scanListAttachments = true;
[ObservableProperty] private bool _scanRecycleBin = true;
// ── Report filter flags (gate which kinds appear in DataGrid + exports) ─
[ObservableProperty] private bool _showLibraries = true;
[ObservableProperty] private bool _showHiddenLibraries = true;
[ObservableProperty] private bool _showPreservationHold = true;
[ObservableProperty] private bool _showListAttachments = true;
[ObservableProperty] private bool _showRecycleBin = true;
[ObservableProperty] private bool _showSubsites = true;
/// <summary>
/// When true, recycle bin stage 1 + stage 2 collapse into a single
/// "[Recycle Bin] Total" row whose size is the sum of both stages.
/// When false, both stages render as separate rows.
/// </summary>
[ObservableProperty] private bool _combineRecycleBinStages = true;
// SPO-reported site total (Site.Usage.Storage). Independent reference
// value the user can compare against the scanned total.
[ObservableProperty] private long _spoReportedTotalSize;
partial void OnShowLibrariesChanged(bool value) => RebuildFilteredResults();
partial void OnShowHiddenLibrariesChanged(bool value) => RebuildFilteredResults();
partial void OnShowPreservationHoldChanged(bool value) => RebuildFilteredResults();
partial void OnShowListAttachmentsChanged(bool value) => RebuildFilteredResults();
partial void OnShowRecycleBinChanged(bool value) => RebuildFilteredResults();
partial void OnShowSubsitesChanged(bool value) => RebuildFilteredResults();
partial void OnCombineRecycleBinStagesChanged(bool value) => RebuildFilteredResults();
/// <summary>0 = Single file, 1 = Split by site.</summary>
[ObservableProperty] private int _splitModeIndex;
/// <summary>0 = Separate HTML files, 1 = Single tabbed HTML.</summary>
@@ -111,6 +144,10 @@ public partial class StorageViewModel : FeatureViewModelBase
}
}
// Raw scan output — never filtered. RebuildFilteredResults projects this
// into Results based on the Show* flags.
private List<StorageNode> _allNodes = new();
private ObservableCollection<StorageNode> _results = new();
public ObservableCollection<StorageNode> Results
{
@@ -136,12 +173,22 @@ public partial class StorageViewModel : FeatureViewModelBase
/// <summary>Sum of TotalFileCount across root-level library nodes.</summary>
public long SummaryFileCount => Results.Where(n => n.IndentLevel == 0).Sum(n => n.TotalFileCount);
/// <summary>
/// Aggregate recycle-bin size (stage 1 + stage 2 across all sites). Reads
/// from the raw scan so it stays visible even when the user hides the
/// recycle-bin row in the report filter.
/// </summary>
public long SummaryRecycleBinSize => _allNodes
.Where(n => n.Kind == StorageNodeKind.RecycleBin)
.Sum(n => n.TotalSizeBytes);
public bool HasResults => Results.Count > 0;
private void NotifySummaryProperties()
{
OnPropertyChanged(nameof(SummaryTotalSize));
OnPropertyChanged(nameof(SummaryVersionSize));
OnPropertyChanged(nameof(SummaryRecycleBinSize));
OnPropertyChanged(nameof(SummaryFileCount));
OnPropertyChanged(nameof(HasResults));
}
@@ -220,10 +267,15 @@ public partial class StorageViewModel : FeatureViewModelBase
var options = new StorageScanOptions(
PerLibrary: PerLibrary,
IncludeSubsites: IncludeSubsites,
FolderDepth: FolderDepth);
FolderDepth: FolderDepth,
IncludeHiddenLibraries: ScanHiddenLibraries,
IncludePreservationHold: ScanPreservationHold,
IncludeListAttachments: ScanListAttachments,
IncludeRecycleBin: ScanRecycleBin);
var allNodes = new List<StorageNode>();
var allTypeMetrics = new List<FileTypeMetric>();
long spoReportedTotal = 0;
var autoOwnership = await IsAutoTakeOwnershipEnabled();
@@ -273,6 +325,8 @@ public partial class StorageViewModel : FeatureViewModelBase
progress.Report(OperationProgress.Indeterminate($"Scanning file types: {url}..."));
var typeMetrics = await _storageService.CollectFileTypeMetricsAsync(ctx, progress, ct);
allTypeMetrics.AddRange(typeMetrics);
spoReportedTotal += await _storageService.GetSiteUsageStorageBytesAsync(ctx, progress, ct);
}
// Flatten tree for DataGrid display
@@ -291,20 +345,113 @@ public partial class StorageViewModel : FeatureViewModelBase
{
await dispatcher.InvokeAsync(() =>
{
Results = new ObservableCollection<StorageNode>(flat);
_allNodes = flat;
SpoReportedTotalSize = spoReportedTotal;
RebuildFilteredResults();
FileTypeMetrics = new ObservableCollection<FileTypeMetric>(mergedMetrics);
});
}
else
{
Results = new ObservableCollection<StorageNode>(flat);
_allNodes = flat;
SpoReportedTotalSize = spoReportedTotal;
RebuildFilteredResults();
FileTypeMetrics = new ObservableCollection<FileTypeMetric>(mergedMetrics);
}
}
/// <summary>
/// Project <see cref="_allNodes"/> into <see cref="Results"/> using the
/// Show* flags. Nodes whose root ancestor is excluded by the flags are
/// dropped along with their entire subtree, preserving DFS ordering.
/// </summary>
private void RebuildFilteredResults()
{
if (_allNodes.Count == 0)
{
Results = new ObservableCollection<StorageNode>();
return;
}
var filtered = new List<StorageNode>(_allNodes.Count);
bool includeCurrentSubtree = true;
foreach (var node in _allNodes)
{
if (node.IndentLevel == 0)
includeCurrentSubtree = IsKindShown(node.Kind);
if (includeCurrentSubtree)
filtered.Add(node);
}
if (CombineRecycleBinStages)
filtered = CombineRecycleBins(filtered);
Results = new ObservableCollection<StorageNode>(filtered);
}
/// <summary>
/// Replaces all root recycle-bin nodes (stage 1 + stage 2) with a single
/// aggregate row inserted at the position of the first recycle-bin node
/// encountered. Preserves SiteTitle grouping when scans cover multiple
/// sites by aggregating per SiteTitle.
/// </summary>
private static List<StorageNode> CombineRecycleBins(List<StorageNode> input)
{
var byPath = new Dictionary<string, StorageNode>(StringComparer.OrdinalIgnoreCase);
var result = new List<StorageNode>(input.Count);
foreach (var node in input)
{
if (node.IndentLevel == 0 && node.Kind == StorageNodeKind.RecycleBin)
{
string key = node.SiteTitle ?? string.Empty;
if (!byPath.TryGetValue(key, out var agg))
{
agg = new StorageNode
{
Name = "[Recycle Bin] Total",
SiteTitle = node.SiteTitle ?? string.Empty,
Library = "RecycleBin",
Kind = StorageNodeKind.RecycleBin,
IndentLevel = 0,
Children = new List<StorageNode>()
};
byPath[key] = agg;
result.Add(agg);
}
agg.TotalSizeBytes += node.TotalSizeBytes;
agg.FileStreamSizeBytes += node.FileStreamSizeBytes;
agg.TotalFileCount += node.TotalFileCount;
if (node.LastModified.HasValue &&
(!agg.LastModified.HasValue || node.LastModified > agg.LastModified))
agg.LastModified = node.LastModified;
}
else
{
result.Add(node);
}
}
return result;
}
private bool IsKindShown(StorageNodeKind kind) => kind switch
{
StorageNodeKind.Library => ShowLibraries,
StorageNodeKind.HiddenLibrary => ShowHiddenLibraries,
StorageNodeKind.PreservationHold => ShowPreservationHold,
StorageNodeKind.ListAttachments => ShowListAttachments,
StorageNodeKind.RecycleBin => ShowRecycleBin,
StorageNodeKind.Subsite => ShowSubsites,
_ => true
};
protected override void OnTenantSwitched(TenantProfile profile)
{
_currentProfile = profile;
_allNodes = new List<StorageNode>();
SpoReportedTotalSize = 0;
Results = new ObservableCollection<StorageNode>();
FileTypeMetrics = new ObservableCollection<FileTypeMetric>();
OnPropertyChanged(nameof(CurrentProfile));