chore: release v2.4

- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager)
- Add InputDialog, Spinner common view
- Add DuplicatesCsvExportService
- Refresh views, dialogs, and view models across tabs
- Update localization strings (en/fr)
- Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-20 11:23:11 +02:00
parent 8f30a60d2a
commit f4cc81bb71
64 changed files with 3315 additions and 405 deletions
@@ -27,6 +27,8 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
private readonly UserAccessHtmlExportService? _htmlExportService;
private readonly IBrandingService? _brandingService;
private readonly IGraphUserDirectoryService? _graphUserDirectoryService;
private readonly IOwnershipElevationService? _ownershipService;
private readonly SettingsService? _settingsService;
private readonly ILogger<FeatureViewModelBase> _logger;
// ── People picker debounce ──────────────────────────────────────────────
@@ -163,7 +165,9 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
UserAccessHtmlExportService htmlExportService,
IBrandingService brandingService,
IGraphUserDirectoryService graphUserDirectoryService,
ILogger<FeatureViewModelBase> logger)
ILogger<FeatureViewModelBase> logger,
IOwnershipElevationService? ownershipService = null,
SettingsService? settingsService = null)
: base(logger)
{
_auditService = auditService;
@@ -173,6 +177,8 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
_htmlExportService = htmlExportService;
_brandingService = brandingService;
_graphUserDirectoryService = graphUserDirectoryService;
_ownershipService = ownershipService;
_settingsService = settingsService;
_logger = logger;
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
@@ -273,6 +279,35 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
return;
}
var autoOwnership = await IsAutoTakeOwnershipEnabled();
Func<string, CancellationToken, Task<bool>>? onAccessDenied = null;
if (_ownershipService != null && autoOwnership)
{
onAccessDenied = async (siteUrl, token) =>
{
try
{
_logger.LogWarning("Access denied on {Url}, auto-elevating ownership...", siteUrl);
var adminUrl = DeriveAdminUrl(_currentProfile?.TenantUrl ?? siteUrl);
var adminProfile = new TenantProfile
{
TenantUrl = adminUrl,
ClientId = _currentProfile?.ClientId ?? string.Empty,
Name = _currentProfile?.Name ?? string.Empty
};
var adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, token);
await _ownershipService.ElevateAsync(adminCtx, siteUrl, string.Empty, token);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Auto-elevation failed for {Url}", siteUrl);
return false;
}
};
}
var entries = await _auditService.AuditUsersAsync(
_sessionManager,
_currentProfile,
@@ -280,7 +315,8 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
effectiveSites,
scanOptions,
progress,
ct);
ct,
onAccessDenied);
// Update Results on the UI thread — clear + repopulate (not replace)
// so the CollectionViewSource bound to ResultsView stays connected.
@@ -307,6 +343,26 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
ExportHtmlCommand.NotifyCanExecuteChanged();
}
// ── Auto-ownership helpers ───────────────────────────────────────────────
private async Task<bool> IsAutoTakeOwnershipEnabled()
{
if (_settingsService == null) return false;
var settings = await _settingsService.GetSettingsAsync();
return settings.AutoTakeOwnership;
}
internal static string DeriveAdminUrl(string tenantUrl)
{
var uri = new Uri(tenantUrl.TrimEnd('/'));
var host = uri.Host;
if (host.Contains("-admin.sharepoint.com", StringComparison.OrdinalIgnoreCase))
return tenantUrl;
var adminHost = host.Replace(".sharepoint.com", "-admin.sharepoint.com",
StringComparison.OrdinalIgnoreCase);
return $"{uri.Scheme}://{adminHost}";
}
// ── Tenant switching ─────────────────────────────────────────────────────
protected override void OnTenantSwitched(TenantProfile profile)