Commit Graph

103 Commits

Author SHA1 Message Date
Dev
3ba574612f feat(10-02): implement GraphUserDirectoryService with PageIterator and unit tests
- GraphUserDirectoryService uses PageIterator<User, UserCollectionResponse> for pagination
- Filter: accountEnabled eq true and userType eq 'Member' (no ConsistencyLevel header)
- Cancellation checked in PageIterator callback (return false stops iteration)
- Progress reported via IProgress<int> with running count per user
- MapUser extracted as internal static for direct unit test coverage
- Tests: 5 unit tests for MapUser field mapping and fallback logic
- Integration-level tests (pagination/cancellation) skipped with rationale documented
- Note: test project compilation blocked by pre-existing BrandingServiceTests.cs (10-01 artifact)
2026-04-08 12:32:04 +02:00
Dev
2280f12eab feat(10-01): create logo models, BrandingRepository, and repository tests
- Add LogoData record with Base64 and MimeType init properties
- Add BrandingSettings class with nullable MspLogo property
- Extend TenantProfile with nullable ClientLogo property (additive)
- Add BrandingRepository mirroring SettingsRepository pattern (write-then-replace)
- Add BrandingRepositoryTests: 5 tests covering load defaults, round-trip, dir creation, and TenantProfile serialization
2026-04-08 12:29:53 +02:00
Dev
5e56a96cd0 feat(10-02): add GraphDirectoryUser model and IGraphUserDirectoryService interface
- GraphDirectoryUser positional record with DisplayName, UPN, Mail, Department, JobTitle
- IGraphUserDirectoryService.GetUsersAsync with clientId, IProgress<int>?, CancellationToken
- Follows existing GraphUserSearchService namespace pattern
2026-04-08 12:29:19 +02:00
Dev
8447e78db9 docs: start milestone v2.2 Report Branding & User Directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:57:27 +02:00
Dev
fd442f3b4c 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>
2026-04-08 10:21:02 +02:00
Dev
a8d79a8241 feat(09-03): add chart panel to StorageView with toggle and localization
- Update StorageView.xaml: DataGrid top, GridSplitter, chart panel bottom
- Add PieChart and CartesianChart with MultiDataTrigger visibility
- Add radio buttons for donut/bar chart toggle in left panel
- Create BytesLabelConverter for chart tooltip formatting
- Add stor.chart.* localization keys in EN and FR resx files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:35:35 +02:00
Dev
70048ddcdf feat(09-03): extend StorageViewModel with chart data properties and toggle
- Add IsDonutChart toggle, FileTypeMetrics collection, PieChartSeries, BarChartSeries, BarXAxes, BarYAxes
- Add UpdateChartSeries method with top-10 + Other aggregation
- Call CollectFileTypeMetricsAsync after storage scan in RunOperationAsync
- Clear chart data on tenant switch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:27:54 +02:00
Dev
81e3dcac6d feat(09-02): implement CollectFileTypeMetricsAsync in StorageService
- CamlQuery with RecursiveAll scope enumerates files across all non-hidden document libraries
- Paginated 500-item batches avoid list view threshold issues
- Files grouped by extension (case-insensitive) with summed size and count
- Results returned as IReadOnlyList<FileTypeMetric> sorted by TotalSizeBytes descending
- Existing CollectStorageAsync, LoadFolderNodeAsync, CollectSubfoldersAsync unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:24:09 +02:00
Dev
39c31dadfa feat(09-01): extend IStorageService with CollectFileTypeMetricsAsync
- Add CollectFileTypeMetricsAsync method signature to IStorageService
- Returns IReadOnlyList<FileTypeMetric> for chart visualization data
- Existing CollectStorageAsync signature unchanged
- CS0535 expected until StorageService implements in Plan 09-02

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:21:02 +02:00
Dev
60cbb977bf feat(09-01): add LiveCharts2 NuGet and FileTypeMetric data model
- Add LiveChartsCore.SkiaSharpView.WPF 2.0.0-rc5.4 package reference
- Create FileTypeMetric record with Extension, TotalSizeBytes, FileCount
- Include DisplayLabel computed property for chart label binding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:20:38 +02:00
Dev
f503e6c0ca feat(08-05): wire export commands to use simplified overloads
- ExportCsvAsync branches on IsSimplifiedMode to call simplified WriteAsync overload
- ExportHtmlAsync branches on IsSimplifiedMode to call simplified WriteAsync overload
- Standard PermissionEntry export path unchanged when simplified mode is off

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:17:14 +02:00
Dev
60ddcd781f feat(08-05): add EN/FR localization keys for simplified permissions UI
- Add 6 keys to Strings.resx: chk.simplified.mode, grp.display.opts, lbl.detail.level, rad.detail.detailed, rad.detail.simple, lbl.summary.users
- Add matching French translations to Strings.fr.resx with proper XML entities for accented characters
- Wire hardcoded "user(s)" text in PermissionsView.xaml summary cards to lbl.summary.users localization key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:16:40 +02:00
Dev
899ab7d175 feat(08-04): add simplified export overloads to HtmlExportService
- Add RiskLevelColors helper for risk-level color coding
- Add BuildHtml(IReadOnlyList<SimplifiedPermissionEntry>) with risk summary cards, Simplified column, and color-coded Risk badges
- Add WriteAsync overload for simplified entries
- Original PermissionEntry methods unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:13:08 +02:00
Dev
163c506e0b feat(08-03): add simplified mode UI to PermissionsView
- Add Display Options GroupBox with Simplified Mode toggle and Simple/Detailed radio buttons
- Add summary panel with color-coded risk level cards bound to Summaries collection
- DataGrid binds to ActiveItemsSource, rows color-coded by RiskLevel via DataTriggers
- SimplifiedLabels column visible only in simplified mode via BooleanToVisibilityConverter
- DataGrid collapses in Simple mode via MultiDataTrigger on IsSimplifiedMode+IsDetailView
- Create InvertBoolConverter for radio button inverse binding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:12:57 +02:00
Dev
fe19249f82 feat(08-04): add simplified export overloads to CsvExportService
- Add BuildCsv(IReadOnlyList<SimplifiedPermissionEntry>) overload with SimplifiedLabels and RiskLevel columns
- Add WriteAsync overload for simplified entries
- Original PermissionEntry methods unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:12:18 +02:00
Dev
e2c94bf6d1 feat(08-02): add simplified mode properties to PermissionsViewModel
- IsSimplifiedMode toggle switches between raw and simplified labels
- IsDetailView toggle controls individual vs summary row display
- SimplifiedResults and Summaries computed from cached Results
- ActiveItemsSource provides correct collection for DataGrid binding
- Mode toggles rebuild from cache without re-running scan
- OnTenantSwitched resets simplified state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:09:57 +02:00
Dev
6609f2a70a feat(08-01): add SimplifiedPermissionEntry wrapper and PermissionSummary model
- SimplifiedPermissionEntry wraps PermissionEntry with computed labels and risk level
- Passthrough properties preserve DataGrid binding compatibility
- PermissionSummary record for grouped risk-level counts
- PermissionSummaryBuilder always returns all 4 risk levels for consistent UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:06:47 +02:00
Dev
f1390eaa1c feat(08-01): add RiskLevel enum and PermissionLevelMapping helper
- RiskLevel enum with High, Medium, Low, ReadOnly tiers
- PermissionLevelMapping maps 11 standard SharePoint roles to plain-language labels
- Case-insensitive lookup with Medium fallback for unknown roles
- GetHighestRisk and GetSimplifiedLabels for row-level formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:06:17 +02:00
Dev
00252fd137 fix(07): fix people picker selection and audit service authentication
People picker ListBox used MouseBinding which fires before SelectedItem
updates, causing null CommandParameter. Replaced with SelectionChanged
event handler in code-behind.

AuditUsersAsync created TenantProfile with empty ClientId, causing
ArgumentException in SessionManager. Added currentProfile parameter
to pass the authenticated tenant's ClientId through.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:44:53 +02:00
Dev
33833dce5d feat(07-09): add guest badge, warning icon, and ObjectType column to DataGrid
- Convert User column to DataGridTemplateColumn with orange 'Guest' pill badge on IsExternalUser=true
- Add ObjectType DataGridTextColumn between Object and Permission Level
- Convert Permission Level column to DataGridTemplateColumn with red warning icon on IsHighPrivilege=true
2026-04-07 13:14:29 +02:00
Dev
a2531ea33f feat(07-07): add localization keys for User Access Audit tab in English and French
- Add 17 audit.* keys and tab.userAccessAudit to Strings.resx (English)
- Add matching French translations with proper Unicode accented characters to Strings.fr.resx
2026-04-07 12:53:37 +02:00
Dev
df796ee956 feat(07-07): add UserAccessAuditTabItem to MainWindow and wire dialog factory
- Add UserAccessAuditTabItem to MainWindow.xaml TabControl before SettingsTabItem
- Wire UserAccessAuditView content and SitePickerDialog factory in MainWindow.xaml.cs
2026-04-07 12:53:04 +02:00
Dev
2ed8a0cb12 feat(07-07): add DI registrations for Phase 7 services and create UserAccessAuditView
- Register IUserAccessAuditService, IGraphUserSearchService, export services, ViewModel and View in App.xaml.cs
- Create UserAccessAuditView.xaml with two-panel layout: people picker, site picker, scan options, color-coded DataGrid with grouping, summary banner
- Create UserAccessAuditView.xaml.cs code-behind with ViewModel constructor injection
- [Rule 3] UserAccessAuditView was missing (07-05 not executed); created inline to unblock 07-07
2026-04-07 12:52:36 +02:00
Dev
975762dee4 feat(07-05): create UserAccessAuditView code-behind
- UserControl with UserAccessAuditViewModel constructor injection, sets DataContext
- Wires SearchResults.CollectionChanged to show/hide autocomplete ListBox
- OnSearchResultClicked handler invokes AddUserCommand for mouse-based user selection
2026-04-07 12:49:41 +02:00
Dev
bb9ba9d310 feat(07-05): create UserAccessAuditView XAML layout
- Two-panel layout (290px left + * right) following PermissionsView pattern
- Left panel: people picker with autocomplete list + removable user pills, site picker button, scan option checkboxes, run/cancel/export buttons
- Right panel: 3-card summary banner (TotalAccessCount, SitesCount, HighPrivilegeCount), filter TextBox, group-by ToggleButton, color-coded DataGrid
- DataGrid: color-coded rows by AccessType (Direct=blue, Group=green, Inherited=gray), warning icon for high privilege, Guest badge for external users, access type icons
- GroupStyle with Expander headers showing group name + item count
- Status bar with ProgressBar + StatusMessage
2026-04-07 12:49:37 +02:00
Dev
3de737ac3f feat(07-04): implement UserAccessAuditViewModel
- Extends FeatureViewModelBase with RunOperationAsync calling IUserAccessAuditService.AuditUsersAsync
- People picker with 300ms debounced Graph search via IGraphUserSearchService.SearchUsersAsync
- SelectedUsers ObservableCollection<GraphUserResult> with AddUserCommand/RemoveUserCommand
- Results ObservableCollection<UserAccessEntry> with CollectionViewSource grouping (by user/site) and FilterText predicate
- Summary banner properties: TotalAccessCount, SitesCount, HighPrivilegeCount (computed from Results)
- ExportCsvCommand/ExportHtmlCommand using UserAccessCsvExportService/UserAccessHtmlExportService
- Site selection with _hasLocalSiteOverride + OnGlobalSitesChanged pattern from PermissionsViewModel
- Dual constructors (DI + internal test constructor omitting export services)
- OnTenantSwitched resets all state (results, users, search, sites)
2026-04-07 12:44:02 +02:00
Dev
3146a04ad8 feat(07-06): implement UserAccessHtmlExportService
- BuildHtml produces self-contained HTML with inline CSS and JS
- Stats cards: Total Accesses, Users Audited, Sites Scanned, High Privilege, External Users
- Per-user summary cards with high-privilege border highlight and guest badge
- Dual-view toggle (By User / By Site) with JS toggleView()
- Collapsible group headers per user and per site via toggleGroup()
- Sortable columns via sortTable() within each group
- Text filter via filterTable() scoping to active view
- Color-coded access type badges: Direct (blue), Group (green), Inherited (gray)
- High-privilege rows with bold text and warning icon
- External user guest badge (orange pill)
- UTF-8 without BOM encoding (matching HtmlExportService pattern)
2026-04-07 12:40:51 +02:00
Dev
44b238e07a feat(07-02): implement UserAccessAuditService
- Scans permissions via IPermissionsService.ScanSiteAsync per site
- Filters PermissionEntry results to matching target user logins (case-insensitive contains)
- Splits semicolon-delimited users/logins/levels into per-user UserAccessEntry rows
- Classifies AccessType: Inherited (!HasUniquePermissions), Group (GrantedThrough), Direct
- Flags IsHighPrivilege (Full Control, Site Collection Administrator) and IsExternalUser (#EXT#)
2026-04-07 12:39:57 +02:00
Dev
9f891aa512 feat(07-06): implement UserAccessCsvExportService
- BuildCsv per-user CSV with summary section (user, totals, sites, high-privilege, date)
- WriteAsync groups entries by UserLogin, writes one file per user (audit_{email}_{date}.csv)
- WriteSingleFileAsync combines all users in one file for SaveFileDialog export
- RFC 4180 CSV escaping, UTF-8 with BOM for Excel compatibility
- SanitizeFileName strips invalid path chars from email addresses
2026-04-07 12:39:35 +02:00
Dev
026b8294de feat(07-03): implement GraphUserSearchService for people-picker autocomplete
- Queries Graph /users with startsWith filter on displayName, mail, UPN
- Requires minimum 2 chars to prevent overly broad queries
- Sets ConsistencyLevel=eventual + Count=true (required for advanced filter)
- Escapes single quotes to prevent OData injection
- Returns up to maxResults (default 10) GraphUserResult records
2026-04-07 12:39:22 +02:00
Dev
1a6989a9bb feat(07-01): add IUserAccessAuditService and IGraphUserSearchService interfaces
- IUserAccessAuditService.AuditUsersAsync: scan sites and filter by user logins
- IGraphUserSearchService.SearchUsersAsync: Graph API people-picker autocomplete
- GraphUserResult record: DisplayName, UserPrincipalName, Mail
2026-04-07 12:37:26 +02:00
Dev
e08df0f658 feat(07-01): add UserAccessEntry model and AccessType enum
- UserAccessEntry record with 12 fields for user-centric audit results
- AccessType enum: Direct, Group, Inherited
- Pre-computed IsHighPrivilege and IsExternalUser fields for grid display
2026-04-07 12:37:00 +02:00
Dev
4846915c80 fix(site-list): fix parsing error and double-auth in SiteListService
- Replace GetSitePropertiesFromSharePoint("", true) with modern
  GetSitePropertiesFromSharePointByFilters using null StartIndex
- Use ctx.Clone(adminUrl) instead of creating new AuthenticationManager
  for admin URL, eliminating second browser auth prompt

Resolves: UAT issue "Must specify valid information for parsing in the string"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:00:54 +02:00
Dev
5666565ac1 test(06): complete UAT - 0 passed, 3 issues, 7 skipped
Fix two pre-existing blockers found during UAT:
- ProfileManagementViewModel: add NotifyCanExecuteChanged on property changes
- SessionManager: open browser in openBrowserCallback (was no-op)

Remaining blocker: SitePickerDialog parsing error from PnP Framework.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:41:39 +02:00
Dev
0a91dd4ff3 feat(06-04): update TransferViewModel for global site consumption; confirm BulkMembers excluded
- TransferViewModel: add _hasLocalSourceSiteOverride field
- Override OnGlobalSitesChanged to pre-fill SourceSiteUrl from first global site
- Add OnSourceSiteUrlChanged partial to detect local user input
- Reset _hasLocalSourceSiteOverride on tenant switch
- BulkMembersViewModel confirmed excluded: no SiteUrl field, CSV-driven, no OnGlobalSitesChanged override added
2026-04-07 10:08:52 +02:00
Dev
6a2e4d1d89 feat(06-04): update single-site tab VMs for global site consumption
- StorageViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- SearchViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- DuplicatesViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- FolderStructureViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- All four VMs pre-fill SiteUrl from first global site; local typing sets override flag
- Tenant switch resets _hasLocalSiteOverride in all four VMs
2026-04-07 10:08:19 +02:00
Dev
45eb531128 feat(06-03): add global site picker button and count label to toolbar
- Add Separator + Select Sites button (bound to OpenGlobalSitePickerCommand) to ToolBar
- Add TextBlock bound to GlobalSitesSelectedLabel for site count display
- Wire viewModel.OpenGlobalSitePickerDialog factory in MainWindow.xaml.cs using DI
- Add using SharepointToolbox.Core.Models for TenantProfile in code-behind
2026-04-07 10:07:35 +02:00
Dev
467a940c6f feat(06-03): localize GlobalSitesSelectedLabel in MainWindowViewModel
- Replace hardcoded EN strings with TranslationSource.Instance lookups
- Uses toolbar.globalSites.count (formatted) and toolbar.globalSites.none keys
- Follows same pattern as PermissionsViewModel.SitesSelectedLabel
2026-04-07 10:06:57 +02:00
Dev
1bf47b5c4e feat(06-04): update PermissionsViewModel for multi-site global consumption
- Add _hasLocalSiteOverride field to track local user selection
- Override OnGlobalSitesChanged to pre-populate SelectedSites from global sites
- Set _hasLocalSiteOverride=true when user picks sites via site picker dialog
- Reset _hasLocalSiteOverride=false on tenant switch (OnTenantSwitched)
2026-04-07 10:06:57 +02:00
Dev
185642f4af feat(06-03): add EN/FR localization keys for global site picker toolbar
- Add toolbar.selectSites, toolbar.selectSites.tooltip, toolbar.selectSites.tooltipDisabled
- Add toolbar.globalSites.count and toolbar.globalSites.none to both Strings.resx and Strings.fr.resx
2026-04-07 10:06:40 +02:00
Dev
d4fe169bd8 feat(06-01): extend FeatureViewModelBase with GlobalSites support
- Add protected GlobalSites property (IReadOnlyList<SiteInfo>) initialized to Array.Empty
- Register GlobalSitesChangedMessage in OnActivated alongside TenantSwitchedMessage
- Add private OnGlobalSitesReceived to update GlobalSites and invoke virtual hook
- Add protected virtual OnGlobalSitesChanged for derived VMs to override
- [Rule 3 - Blocking] Fix MainWindowViewModel missing ExecuteOpenGlobalSitePicker and BroadcastGlobalSites stubs referenced in constructor (pre-existing partial state from earlier TODO commit)
2026-04-07 10:03:40 +02:00
Dev
a10f03edc8 feat(06-02): add global site selection state, command, and broadcast to MainWindowViewModel
- Add OpenGlobalSitePickerDialog factory property (dialog factory pattern)
- Add GlobalSelectedSites ObservableCollection<SiteInfo>
- Add GlobalSitesSelectedLabel computed property for toolbar display
- Add OpenGlobalSitePickerCommand (disabled when no profile selected)
- Broadcast GlobalSitesChangedMessage via WeakReferenceMessenger on collection change
- Clear GlobalSelectedSites on tenant switch (OnSelectedProfileChanged)
- Clear GlobalSelectedSites on session clear (ClearSessionAsync)
- Add using SharepointToolbox.Views.Dialogs for SitePickerDialog cast
2026-04-07 10:03:30 +02:00
Dev
7874fa8524 feat(06-01): create GlobalSitesChangedMessage
- New ValueChangedMessage<IReadOnlyList<SiteInfo>> following TenantSwitchedMessage pattern
- Carries snapshot of globally selected sites (IReadOnlyList — immutable by design)
2026-04-07 10:02:20 +02:00
Dev
724fdc550d chore: complete v1.0 milestone
Archive 5 phases (36 plans) to milestones/v1.0-phases/.
Archive roadmap, requirements, and audit to milestones/.
Evolve PROJECT.md with shipped state and validated requirements.
Collapse ROADMAP.md to one-line milestone summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:19:03 +02:00
Dev
b815c323d7 fix: resolve post-milestone tech debt items
- Add DataGrid RowStyle with red highlighting for invalid CSV rows
  in BulkMembersView, BulkSitesView, and FolderStructureView
- Fix cancel test locale mismatch by setting EN culture before assertion
- Remove dead FeatureTabBase placeholder (replaced by full tab views)
- Clean up unused xmlns:controls from MainWindow.xaml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:15:02 +02:00
Dev
39517d8956 feat(05-02): add self-contained single-file publish configuration
- Added conditional PropertyGroup for PublishSingleFile=true
- Sets SelfContained=true, RuntimeIdentifier=win-x64,
  IncludeNativeLibrariesForSelfExtract=true
- Conditional activation avoids affecting dotnet build and dotnet test
- Produces single SharepointToolbox.exe with zero loose DLL files
- PublishTrimmed remains false (required by PnP.Framework + MSAL)
2026-04-03 16:36:07 +02:00
Dev
f7829f0801 fix(05-02): correct French diacritics in Strings.fr.resx
- Fixed 27 strings with missing accents across Transfer, BulkMembers,
  BulkSites, FolderStruct, Templates, and shared bulk operation keys
- Corrected: Bibliothèque, Déplacer, Écraser, Démarrer, transférer,
  Aperçu, Créer, Propriétaires, Modèles, Sélectionner, Terminé, etc.
2026-04-03 16:35:34 +02:00
Dev
4d7e9ea02a feat(05-01): make helper methods internal and add unit tests
- Changed IsThrottleException to internal static in ExecuteQueryRetryHelper
- Changed BuildPagedViewXml to internal static in SharePointPaginationHelper
- Created ExecuteQueryRetryHelperTests: 5 tests (throttle true x3, non-throttle false, nested false)
- Created SharePointPaginationHelperTests: 5 tests (null, empty, whitespace, replace, append)
2026-04-03 16:34:54 +02:00
Dev
3d62b2c48b fix(04): resolve null-reference crashes in CsvValidationService and TransferView
- Add null-conditional on CsvReader.Context.Parser to fix CS8602 warnings
- Guard ConflictCombo_SelectionChanged against null ViewModel during XAML init

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:06:25 +02:00
Dev
988bca844b feat(04-10): register Phase 4 DI + wire MainWindow tabs + TemplatesView
- App.xaml.cs: register TemplateRepository, GraphClientFactory, ICsvValidationService, BulkResultCsvExportService
- App.xaml.cs: register BulkMemberService, BulkSiteService, ITemplateService, IFolderStructureService
- App.xaml.cs: register all 5 Phase 4 ViewModels and Views (Transfer, BulkMembers, BulkSites, FolderStructure, Templates)
- MainWindow.xaml: replace 3 FeatureTabBase stub tabs with 5 named TabItems (tab.transfer through tab.templates)
- MainWindow.xaml.cs: wire all 5 new TabItem.Content from DI-resolved Views
2026-04-03 10:24:32 +02:00