Added max list size circumvention for file transfers between sites.

This commit is contained in:
Dev
2026-05-13 15:58:16 +02:00
parent 4b51c8e3c3
commit 5d305ccc4c
27 changed files with 996 additions and 145 deletions
@@ -54,6 +54,14 @@ public partial class PermissionsViewModel : FeatureViewModelBase
[ObservableProperty]
private bool _hideSystemGroupRaw = true;
/// <summary>When true, sharing link entries (SharingLinkType != null) are removed from results and exports.</summary>
[ObservableProperty]
private bool _excludeSharingLinks;
/// <summary>When true, "Limited Access System Group For Web/List" entries are removed from results and exports.</summary>
[ObservableProperty]
private bool _excludeSystemGroups;
[ObservableProperty]
private bool _includeSubsites;
@@ -102,6 +110,17 @@ public partial class PermissionsViewModel : FeatureViewModelBase
private ReportSplitMode CurrentSplit => SplitModeIndex == 1 ? ReportSplitMode.BySite : ReportSplitMode.Single;
private HtmlSplitLayout CurrentLayout => HtmlLayoutIndex == 1 ? HtmlSplitLayout.SingleTabbed : HtmlSplitLayout.SeparateFiles;
/// <summary>
/// Results after applying ExcludeSharingLinks / ExcludeSystemGroups filters.
/// Rebuilt when Results changes or filter flags change.
/// </summary>
private IReadOnlyList<PermissionEntry> _filteredResults = Array.Empty<PermissionEntry>();
public IReadOnlyList<PermissionEntry> FilteredResults
{
get => _filteredResults;
private set => SetProperty(ref _filteredResults, value);
}
/// <summary>
/// Simplified wrappers computed from Results. Rebuilt when Results changes.
/// </summary>
@@ -124,16 +143,37 @@ public partial class PermissionsViewModel : FeatureViewModelBase
/// <summary>
/// The collection the DataGrid actually binds to. Returns:
/// - Results (raw) when simplified mode is OFF
/// - FilteredResults (raw) when simplified mode is OFF
/// - SimplifiedResults when simplified mode is ON and detail view is ON
/// - (View handles summary display separately via Summaries property)
/// </summary>
public object ActiveItemsSource => IsSimplifiedMode
? (object)SimplifiedResults
: Results;
: FilteredResults;
partial void OnFolderDepthChanged(int value) => OnPropertyChanged(nameof(IsMaxDepth));
partial void OnExcludeSharingLinksChanged(bool value) => RefreshAfterFilterChange();
partial void OnExcludeSystemGroupsChanged(bool value) => RefreshAfterFilterChange();
private void RefreshAfterFilterChange()
{
if (Results.Count == 0) return;
RebuildFilteredResults();
if (IsSimplifiedMode) RebuildSimplifiedData();
OnPropertyChanged(nameof(ActiveItemsSource));
}
private void RebuildFilteredResults()
{
IEnumerable<PermissionEntry> filtered = Results;
if (ExcludeSharingLinks)
filtered = filtered.Where(e => string.IsNullOrEmpty(e.SharingLinkType));
if (ExcludeSystemGroups)
filtered = filtered.Where(e => !e.GrantedThrough.Contains("Limited Access System Group", StringComparison.OrdinalIgnoreCase));
FilteredResults = filtered.ToList();
}
// ── Commands ────────────────────────────────────────────────────────────
public IAsyncRelayCommand ExportCsvCommand { get; }
@@ -172,8 +212,8 @@ public partial class PermissionsViewModel : FeatureViewModelBase
_settingsService = settingsService;
_ownershipService = ownershipService;
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
ExportCsvCommand = new AsyncRelayCommand(ct => ExportCsvAsync(ct), CanExport);
ExportHtmlCommand = new AsyncRelayCommand(ct => ExportHtmlAsync(ct), CanExport);
}
/// <summary>
@@ -199,8 +239,8 @@ public partial class PermissionsViewModel : FeatureViewModelBase
_settingsService = settingsService;
_ownershipService = ownershipService;
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
ExportCsvCommand = new AsyncRelayCommand(ct => ExportCsvAsync(ct), CanExport);
ExportHtmlCommand = new AsyncRelayCommand(ct => ExportHtmlAsync(ct), CanExport);
}
// ── FeatureViewModelBase implementation ─────────────────────────────────
@@ -221,9 +261,18 @@ public partial class PermissionsViewModel : FeatureViewModelBase
/// Recomputes SimplifiedResults and Summaries from the current Results collection.
/// Called when Results changes or when simplified mode is toggled on.
/// </summary>
private static bool IsSimplifiedModeNoise(PermissionEntry e)
{
if (e.Users.Contains("SharePointHome", StringComparison.OrdinalIgnoreCase)) return true;
if (e.GrantedThrough.Contains("SharePointHome", StringComparison.OrdinalIgnoreCase)) return true;
if (e.UserLogins.Split(';').Any(l => l.Trim().StartsWith("c:0u.c|tenant|", StringComparison.OrdinalIgnoreCase))) return true;
return false;
}
private void RebuildSimplifiedData()
{
SimplifiedResults = SimplifiedPermissionEntry.WrapAll(Results);
var forSimplified = FilteredResults.Where(e => !IsSimplifiedModeNoise(e));
SimplifiedResults = SimplifiedPermissionEntry.WrapAll(forSimplified);
Summaries = PermissionSummaryBuilder.Build(SimplifiedResults);
}
@@ -303,6 +352,7 @@ public partial class PermissionsViewModel : FeatureViewModelBase
await dispatcher.InvokeAsync(() =>
{
Results = new ObservableCollection<PermissionEntry>(allEntries);
RebuildFilteredResults();
if (IsSimplifiedMode)
RebuildSimplifiedData();
OnPropertyChanged(nameof(ActiveItemsSource));
@@ -311,6 +361,7 @@ public partial class PermissionsViewModel : FeatureViewModelBase
else
{
Results = new ObservableCollection<PermissionEntry>(allEntries);
RebuildFilteredResults();
if (IsSimplifiedMode)
RebuildSimplifiedData();
OnPropertyChanged(nameof(ActiveItemsSource));
@@ -384,6 +435,7 @@ public partial class PermissionsViewModel : FeatureViewModelBase
{
_currentProfile = profile;
Results = new ObservableCollection<PermissionEntry>();
FilteredResults = Array.Empty<PermissionEntry>();
SimplifiedResults = Array.Empty<SimplifiedPermissionEntry>();
Summaries = Array.Empty<PermissionSummary>();
OnPropertyChanged(nameof(ActiveItemsSource));
@@ -404,7 +456,7 @@ public partial class PermissionsViewModel : FeatureViewModelBase
private bool CanExport() => Results.Count > 0;
private async Task ExportCsvAsync()
private async Task ExportCsvAsync(CancellationToken ct)
{
if (_csvExportService == null || Results.Count == 0) return;
var dialog = new SaveFileDialog
@@ -418,9 +470,9 @@ public partial class PermissionsViewModel : FeatureViewModelBase
try
{
if (IsSimplifiedMode && SimplifiedResults.Count > 0)
await _csvExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CurrentSplit, CancellationToken.None);
await _csvExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CurrentSplit, ct);
else
await _csvExportService.WriteAsync((IReadOnlyList<PermissionEntry>)Results, dialog.FileName, CurrentSplit, CancellationToken.None);
await _csvExportService.WriteAsync(FilteredResults, dialog.FileName, CurrentSplit, ct);
OpenFile(dialog.FileName);
}
catch (Exception ex)
@@ -430,7 +482,7 @@ public partial class PermissionsViewModel : FeatureViewModelBase
}
}
private async Task ExportHtmlAsync()
private async Task ExportHtmlAsync(CancellationToken ct)
{
if (_htmlExportService == null || Results.Count == 0) return;
var dialog = new SaveFileDialog
@@ -458,7 +510,7 @@ public partial class PermissionsViewModel : FeatureViewModelBase
// by the site it was observed on, then resolve against that
// site's context. Using the root tenant ctx for a group that
// lives on a sub-site makes CSOM fail with "Group not found".
var groupsBySite = Results
var groupsBySite = FilteredResults
.Where(r => r.PrincipalType == "SharePointGroup")
.SelectMany(r => r.Users
.Split(';', StringSplitOptions.RemoveEmptyEntries)
@@ -488,9 +540,9 @@ public partial class PermissionsViewModel : FeatureViewModelBase
Name = _currentProfile.Name
};
var ctx = await _sessionManager.GetOrCreateContextAsync(
siteProfile, CancellationToken.None);
siteProfile, ct);
var resolved = await _groupResolver.ResolveGroupsAsync(
ctx, _currentProfile.ClientId, distinctNames, CancellationToken.None);
ctx, _currentProfile.ClientId, distinctNames, ct);
foreach (var kv in resolved)
merged[kv.Key] = kv.Value;
}
@@ -507,9 +559,9 @@ public partial class PermissionsViewModel : FeatureViewModelBase
}
if (IsSimplifiedMode && SimplifiedResults.Count > 0)
await _htmlExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CurrentSplit, CurrentLayout, CancellationToken.None, branding, groupMembers, HideSystemGroupRaw);
await _htmlExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CurrentSplit, CurrentLayout, ct, branding, groupMembers, HideSystemGroupRaw);
else
await _htmlExportService.WriteAsync((IReadOnlyList<PermissionEntry>)Results, dialog.FileName, CurrentSplit, CurrentLayout, CancellationToken.None, branding, groupMembers, HideSystemGroupRaw);
await _htmlExportService.WriteAsync(FilteredResults, dialog.FileName, CurrentSplit, CurrentLayout, ct, branding, groupMembers, HideSystemGroupRaw);
OpenFile(dialog.FileName);
}
catch (Exception ex)