Merge remote-tracking branch 'kawa/main'
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user