chore: archive v1.1 Enhanced Reports milestone
Some checks failed
Release SharePoint Toolbox v2 / release (push) Failing after 14s
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:
@@ -99,25 +99,9 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
|
||||
public IAsyncRelayCommand ExportCsvCommand { get; }
|
||||
public IAsyncRelayCommand ExportHtmlCommand { get; }
|
||||
public RelayCommand OpenSitePickerCommand { get; }
|
||||
public RelayCommand<GraphUserResult> AddUserCommand { get; }
|
||||
public RelayCommand<GraphUserResult> RemoveUserCommand { get; }
|
||||
|
||||
// ── Multi-site ──────────────────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<SiteInfo> SelectedSites { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// True when the user has manually selected sites via the site picker.
|
||||
/// Prevents global site changes from overwriting the local selection.
|
||||
/// </summary>
|
||||
private bool _hasLocalSiteOverride;
|
||||
|
||||
// ── Dialog factory ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Factory set by the View layer to open the SitePickerDialog without importing Window into ViewModel.</summary>
|
||||
public Func<Window>? OpenSitePickerDialog { get; set; }
|
||||
|
||||
// ── Current tenant profile ──────────────────────────────────────────────
|
||||
|
||||
internal TenantProfile? _currentProfile;
|
||||
@@ -125,12 +109,6 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
/// <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
|
||||
? $"{SelectedSites.Count} site(s) selected"
|
||||
: string.Empty;
|
||||
|
||||
// ── Constructors ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Full constructor — used by DI and production code.</summary>
|
||||
@@ -152,11 +130,9 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
|
||||
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
|
||||
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
|
||||
OpenSitePickerCommand = new RelayCommand(ExecuteOpenSitePicker);
|
||||
AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser);
|
||||
RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser);
|
||||
|
||||
SelectedSites.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SitesSelectedLabel));
|
||||
SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel));
|
||||
|
||||
var cvs = new CollectionViewSource { Source = Results };
|
||||
@@ -181,11 +157,9 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
|
||||
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
|
||||
ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
|
||||
OpenSitePickerCommand = new RelayCommand(ExecuteOpenSitePicker);
|
||||
AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser);
|
||||
RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser);
|
||||
|
||||
SelectedSites.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SitesSelectedLabel));
|
||||
SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel));
|
||||
|
||||
var cvs = new CollectionViewSource { Source = Results };
|
||||
@@ -195,15 +169,6 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
|
||||
// ── FeatureViewModelBase implementation ─────────────────────────────────
|
||||
|
||||
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
||||
{
|
||||
if (_hasLocalSiteOverride) return;
|
||||
|
||||
SelectedSites.Clear();
|
||||
foreach (var site in sites)
|
||||
SelectedSites.Add(site);
|
||||
}
|
||||
|
||||
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
||||
{
|
||||
if (SelectedUsers.Count == 0)
|
||||
@@ -212,16 +177,14 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
return;
|
||||
}
|
||||
|
||||
var effectiveSites = SelectedSites.Count > 0
|
||||
? SelectedSites.ToList()
|
||||
: GlobalSites.ToList();
|
||||
|
||||
if (effectiveSites.Count == 0)
|
||||
if (GlobalSites.Count == 0)
|
||||
{
|
||||
StatusMessage = "Select at least one site to scan.";
|
||||
StatusMessage = "Select at least one site from the toolbar.";
|
||||
return;
|
||||
}
|
||||
|
||||
var effectiveSites = GlobalSites.ToList();
|
||||
|
||||
var userLogins = SelectedUsers.Select(u => u.UserPrincipalName).ToList();
|
||||
var scanOptions = new ScanOptions(
|
||||
IncludeInherited: IncludeInherited,
|
||||
@@ -244,19 +207,24 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
progress,
|
||||
ct);
|
||||
|
||||
// Update Results on the UI thread
|
||||
// Update Results on the UI thread — clear + repopulate (not replace)
|
||||
// so the CollectionViewSource bound to ResultsView stays connected.
|
||||
var dispatcher = Application.Current?.Dispatcher;
|
||||
if (dispatcher != null)
|
||||
{
|
||||
await dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
Results = new ObservableCollection<UserAccessEntry>(entries);
|
||||
Results.Clear();
|
||||
foreach (var entry in entries)
|
||||
Results.Add(entry);
|
||||
NotifySummaryProperties();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Results = new ObservableCollection<UserAccessEntry>(entries);
|
||||
Results.Clear();
|
||||
foreach (var entry in entries)
|
||||
Results.Add(entry);
|
||||
NotifySummaryProperties();
|
||||
}
|
||||
|
||||
@@ -269,14 +237,11 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
protected override void OnTenantSwitched(TenantProfile profile)
|
||||
{
|
||||
_currentProfile = profile;
|
||||
_hasLocalSiteOverride = false;
|
||||
Results = new ObservableCollection<UserAccessEntry>();
|
||||
Results.Clear();
|
||||
SelectedUsers.Clear();
|
||||
SearchQuery = string.Empty;
|
||||
SearchResults.Clear();
|
||||
SelectedSites.Clear();
|
||||
FilterText = string.Empty;
|
||||
OnPropertyChanged(nameof(SitesSelectedLabel));
|
||||
OnPropertyChanged(nameof(CurrentProfile));
|
||||
NotifySummaryProperties();
|
||||
ExportCsvCommand.NotifyCanExecuteChanged();
|
||||
@@ -308,15 +273,10 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
|
||||
|
||||
partial void OnResultsChanged(ObservableCollection<UserAccessEntry> value)
|
||||
{
|
||||
// Rebind CollectionViewSource when the collection reference changes
|
||||
if (ResultsView is CollectionView cv && cv.SourceCollection != value)
|
||||
{
|
||||
// CollectionViewSource.View is already live-bound in constructor;
|
||||
// for a new collection reference we need to refresh grouping/filter
|
||||
ApplyGrouping();
|
||||
ResultsView.Filter = FilterPredicate;
|
||||
ResultsView.Refresh();
|
||||
}
|
||||
// Safety net: if the collection reference ever changes, rebind grouping/filter
|
||||
ApplyGrouping();
|
||||
ResultsView.Filter = FilterPredicate;
|
||||
ResultsView.Refresh();
|
||||
NotifySummaryProperties();
|
||||
}
|
||||
|
||||
@@ -379,19 +339,6 @@ public partial class UserAccessAuditViewModel : 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 void ExecuteAddUser(GraphUserResult? user)
|
||||
{
|
||||
if (user == null) return;
|
||||
|
||||
Reference in New Issue
Block a user