chore: archive v1.1 Enhanced Reports milestone
Some checks failed
Release SharePoint Toolbox v2 / release (push) Failing after 14s

v1.1 shipped with 4 phases (25 plans), 10/10 requirements complete:
- Global site selection (toolbar picker, all tabs consume)
- User access audit (Graph people-picker, direct/group/inherited)
- Simplified permissions (plain-language labels, risk levels, detail toggle)
- Storage visualization (LiveCharts2 pie/donut + bar charts)

Post-phase polish: centralized site selection (removed per-tab pickers),
claims prefix stripping, StorageMetrics backfill, chart tooltip fix,
summary stats in app + HTML exports.

205 tests passing, 10,484 LOC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-08 10:21:02 +02:00
parent fa793c5489
commit fd442f3b4c
35 changed files with 1062 additions and 760 deletions

View File

@@ -30,9 +30,6 @@ public partial class PermissionsViewModel : FeatureViewModelBase
// ── Observable properties ───────────────────────────────────────────────
[ObservableProperty]
private string _siteUrl = string.Empty;
[ObservableProperty]
private bool _includeInherited;
@@ -115,43 +112,11 @@ public partial class PermissionsViewModel : FeatureViewModelBase
public IAsyncRelayCommand ExportCsvCommand { get; }
public IAsyncRelayCommand ExportHtmlCommand { get; }
public RelayCommand OpenSitePickerCommand { get; }
// ── Multi-site ──────────────────────────────────────────────────────────
public ObservableCollection<SiteInfo> SelectedSites { get; } = new();
/// <summary>
/// True when the user has manually selected sites via the site picker on this tab.
/// Prevents global site changes from overwriting the user's local selection.
/// </summary>
private bool _hasLocalSiteOverride;
// ── Dialog factory (set by View layer — keeps Window out of ViewModel) ──
/// <summary>
/// Factory function set by the View layer to open the SitePickerDialog.
/// Returns the opened Window; ViewModel calls ShowDialog() on it.
/// </summary>
public Func<Window>? OpenSitePickerDialog { get; set; }
// ── Current tenant profile (received via WeakReferenceMessenger) ────────
internal TenantProfile? _currentProfile;
/// <summary>
/// Public accessor for the current tenant profile — used by View layer dialog factory.
/// </summary>
public TenantProfile? CurrentProfile => _currentProfile;
/// <summary>
/// Label shown in the UI: "3 site(s) selected" or empty when none are selected.
/// </summary>
public string SitesSelectedLabel =>
SelectedSites.Count > 0
? string.Format(Localization.TranslationSource.Instance["perm.sites.selected"], SelectedSites.Count)
: string.Empty;
// ── Constructors ────────────────────────────────────────────────────────
/// <summary>
@@ -175,8 +140,6 @@ public partial class PermissionsViewModel : FeatureViewModelBase
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
OpenSitePickerCommand = new RelayCommand(ExecuteOpenSitePicker);
SelectedSites.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SitesSelectedLabel));
}
/// <summary>
@@ -198,21 +161,10 @@ public partial class PermissionsViewModel : FeatureViewModelBase
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
OpenSitePickerCommand = new RelayCommand(ExecuteOpenSitePicker);
SelectedSites.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SitesSelectedLabel));
}
// ── FeatureViewModelBase implementation ─────────────────────────────────
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
{
if (_hasLocalSiteOverride) return;
SelectedSites.Clear();
foreach (var site in sites)
SelectedSites.Add(site);
}
partial void OnIsSimplifiedModeChanged(bool value)
{
if (value && Results.Count > 0)
@@ -237,17 +189,15 @@ public partial class PermissionsViewModel : FeatureViewModelBase
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
{
var urls = SelectedSites.Count > 0
? SelectedSites.Select(s => s.Url).ToList()
: new List<string> { SiteUrl };
var nonEmpty = urls.Where(u => !string.IsNullOrWhiteSpace(u)).ToList();
if (nonEmpty.Count == 0)
var urls = GlobalSites.Select(s => s.Url).Where(u => !string.IsNullOrWhiteSpace(u)).ToList();
if (urls.Count == 0)
{
StatusMessage = "Enter a site URL or select sites.";
StatusMessage = "Select at least one site from the toolbar.";
return;
}
var nonEmpty = urls;
var allEntries = new List<PermissionEntry>();
var scanOptions = new ScanOptions(
IncludeInherited: IncludeInherited,
@@ -303,15 +253,10 @@ public partial class PermissionsViewModel : FeatureViewModelBase
protected override void OnTenantSwitched(TenantProfile profile)
{
_currentProfile = profile;
_hasLocalSiteOverride = false;
Results = new ObservableCollection<PermissionEntry>();
SimplifiedResults = Array.Empty<SimplifiedPermissionEntry>();
Summaries = Array.Empty<PermissionSummary>();
OnPropertyChanged(nameof(ActiveItemsSource));
SiteUrl = string.Empty;
SelectedSites.Clear();
OnPropertyChanged(nameof(SitesSelectedLabel));
OnPropertyChanged(nameof(CurrentProfile));
ExportCsvCommand.NotifyCanExecuteChanged();
ExportHtmlCommand.NotifyCanExecuteChanged();
}
@@ -381,19 +326,6 @@ public partial class PermissionsViewModel : FeatureViewModelBase
}
}
private void ExecuteOpenSitePicker()
{
if (OpenSitePickerDialog == null) return;
var dialog = OpenSitePickerDialog.Invoke();
if (dialog?.ShowDialog() == true && dialog is Views.Dialogs.SitePickerDialog picker)
{
_hasLocalSiteOverride = true;
SelectedSites.Clear();
foreach (var site in picker.SelectedUrls)
SelectedSites.Add(site);
}
}
private static void OpenFile(string filePath)
{
try