diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 47a0ebd..b33dd47 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -36,7 +36,13 @@ 2. Selecting sites in the toolbar causes all feature tabs to default to those sites when an operation is run 3. A user can override the global selection on any individual tab without clearing the global state 4. The global site selection persists across tab switches within the same session -**Plans**: TBD +**Plans:** 5 plans +Plans: +- [ ] 06-01-PLAN.md — GlobalSitesChangedMessage + FeatureViewModelBase extension +- [ ] 06-02-PLAN.md — MainWindowViewModel global selection state + command +- [ ] 06-03-PLAN.md — Toolbar UI, dialog wiring, and localization keys +- [ ] 06-04-PLAN.md — Tab VM updates for global site consumption +- [ ] 06-05-PLAN.md — Unit tests for global site selection flow ### Phase 7: User Access Audit **Goal**: Administrators can audit every permission a specific user holds across selected sites and export the results @@ -80,7 +86,7 @@ | 3. Storage and File Operations | v1.0 | 8/8 | Complete | 2026-04-02 | | 4. Bulk Operations and Provisioning | v1.0 | 10/10 | Complete | 2026-04-03 | | 5. Distribution and Hardening | v1.0 | 3/3 | Complete | 2026-04-03 | -| 6. Global Site Selection | v1.1 | 0/? | Not started | - | +| 6. Global Site Selection | v1.1 | 0/5 | Planned | - | | 7. User Access Audit | v1.1 | 0/? | Not started | - | | 8. Simplified Permissions | v1.1 | 0/? | Not started | - | | 9. Storage Visualization | v1.1 | 0/? | Not started | - | diff --git a/.planning/phases/06-global-site-selection/06-01-PLAN.md b/.planning/phases/06-global-site-selection/06-01-PLAN.md new file mode 100644 index 0000000..fb4a51b --- /dev/null +++ b/.planning/phases/06-global-site-selection/06-01-PLAN.md @@ -0,0 +1,187 @@ +--- +phase: 06-global-site-selection +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - SharepointToolbox/Core/Messages/GlobalSitesChangedMessage.cs + - SharepointToolbox/ViewModels/FeatureViewModelBase.cs +autonomous: true +requirements: + - SITE-01 +must_haves: + truths: + - "GlobalSitesChangedMessage exists and follows the same ValueChangedMessage pattern as TenantSwitchedMessage" + - "FeatureViewModelBase registers for GlobalSitesChangedMessage in OnActivated and exposes a protected GlobalSites property" + - "Derived tab VMs can override OnGlobalSitesChanged to react to global site selection changes" + - "Existing TenantSwitchedMessage registration still works (no regression)" + artifacts: + - path: "SharepointToolbox/Core/Messages/GlobalSitesChangedMessage.cs" + provides: "Messenger message for global site selection changes" + contains: "GlobalSitesChangedMessage" + - path: "SharepointToolbox/ViewModels/FeatureViewModelBase.cs" + provides: "Base class with GlobalSites property and OnGlobalSitesChanged virtual method" + contains: "GlobalSites" + key_links: + - from: "SharepointToolbox/ViewModels/FeatureViewModelBase.cs" + to: "SharepointToolbox/Core/Messages/GlobalSitesChangedMessage.cs" + via: "Messenger.Register in OnActivated" + pattern: "Register" +--- + + +Create the GlobalSitesChangedMessage and extend FeatureViewModelBase to receive and store global site selections. This establishes the messaging contract that all tab VMs and MainWindowViewModel depend on. + +Purpose: Foundation contract — every other plan in this phase builds on this message class and base class extension. +Output: GlobalSitesChangedMessage.cs, updated FeatureViewModelBase.cs + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/06-global-site-selection/06-CONTEXT.md + + + +From SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs: +```csharp +using CommunityToolkit.Mvvm.Messaging.Messages; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Core.Messages; + +public sealed class TenantSwitchedMessage : ValueChangedMessage +{ + public TenantSwitchedMessage(TenantProfile profile) : base(profile) { } +} +``` + +From SharepointToolbox/Core/Models/SiteInfo.cs: +```csharp +namespace SharepointToolbox.Core.Models; +public record SiteInfo(string Url, string Title); +``` + +From SharepointToolbox/ViewModels/FeatureViewModelBase.cs (OnActivated — extend this): +```csharp +protected override void OnActivated() +{ + Messenger.Register(this, (r, m) => ((FeatureViewModelBase)r).OnTenantSwitched(m.Value)); +} + +protected virtual void OnTenantSwitched(TenantProfile profile) +{ + // Derived classes override to reset their state +} +``` + + + + + + + Task 1: Create GlobalSitesChangedMessage + SharepointToolbox/Core/Messages/GlobalSitesChangedMessage.cs + + Create a new message class following the exact same pattern as TenantSwitchedMessage. + + File: `SharepointToolbox/Core/Messages/GlobalSitesChangedMessage.cs` + ```csharp + using CommunityToolkit.Mvvm.Messaging.Messages; + using SharepointToolbox.Core.Models; + + namespace SharepointToolbox.Core.Messages; + + public sealed class GlobalSitesChangedMessage : ValueChangedMessage> + { + public GlobalSitesChangedMessage(IReadOnlyList sites) : base(sites) { } + } + ``` + + The value type is `IReadOnlyList` (not ObservableCollection) because the message carries a snapshot of the current selection — receivers should not mutate the sender's collection. + + + cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 + + GlobalSitesChangedMessage.cs exists in Core/Messages/, compiles without errors, follows the ValueChangedMessage pattern. + + + + Task 2: Extend FeatureViewModelBase with GlobalSites support + SharepointToolbox/ViewModels/FeatureViewModelBase.cs + + Modify FeatureViewModelBase to register for GlobalSitesChangedMessage and store the global sites. + + 1. Add using directive: `using SharepointToolbox.Core.Models;` (SiteInfo is in Core.Models). + + 2. Add a protected property to store the global sites (after the existing fields, before RunCommand): + ```csharp + /// + /// Sites selected in the global toolbar picker. Updated via GlobalSitesChangedMessage. + /// Derived VMs check this in RunOperationAsync before falling back to per-tab SiteUrl. + /// + protected IReadOnlyList GlobalSites { get; private set; } = Array.Empty(); + ``` + + 3. In `OnActivated()`, add a second Messenger.Register call for GlobalSitesChangedMessage, right after the existing TenantSwitchedMessage registration: + ```csharp + protected override void OnActivated() + { + Messenger.Register(this, (r, m) => ((FeatureViewModelBase)r).OnTenantSwitched(m.Value)); + Messenger.Register(this, (r, m) => ((FeatureViewModelBase)r).OnGlobalSitesReceived(m.Value)); + } + ``` + + 4. Add a private method that updates the property and calls the virtual hook: + ```csharp + private void OnGlobalSitesReceived(IReadOnlyList sites) + { + GlobalSites = sites; + OnGlobalSitesChanged(sites); + } + ``` + + 5. Add a protected virtual method for derived classes to override: + ```csharp + /// + /// Called when the global site selection changes. Override in derived VMs + /// to update UI state (e.g., pre-fill SiteUrl from first global site). + /// + protected virtual void OnGlobalSitesChanged(IReadOnlyList sites) + { + // Derived classes override to react to global site changes + } + ``` + + Do NOT modify anything in the ExecuteAsync, RunCommand, CancelCommand, or OnTenantSwitched areas. Only add the new GlobalSites infrastructure. + + + cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --no-build 2>&1 | tail -5 + + FeatureViewModelBase compiles with GlobalSites property, OnGlobalSitesChanged virtual method, and GlobalSitesChangedMessage registration in OnActivated. All existing tests still pass (no regression). + + + + + +- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors +- `dotnet test` shows no new failures (existing tests unaffected) +- GlobalSitesChangedMessage.cs exists in Core/Messages/ +- FeatureViewModelBase.cs contains `GlobalSites` property and `OnGlobalSitesChanged` virtual method +- OnActivated registers for both TenantSwitchedMessage and GlobalSitesChangedMessage + + + +The messaging contract is established: GlobalSitesChangedMessage can be sent by any publisher and received by all FeatureViewModelBase subclasses. The protected GlobalSites property and virtual OnGlobalSitesChanged hook are available for tab VMs to override in plan 06-04. + + + +After completion, create `.planning/phases/06-global-site-selection/06-01-SUMMARY.md` + diff --git a/.planning/phases/06-global-site-selection/06-02-PLAN.md b/.planning/phases/06-global-site-selection/06-02-PLAN.md new file mode 100644 index 0000000..ff83fe1 --- /dev/null +++ b/.planning/phases/06-global-site-selection/06-02-PLAN.md @@ -0,0 +1,210 @@ +--- +phase: 06-global-site-selection +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - SharepointToolbox/ViewModels/MainWindowViewModel.cs +autonomous: true +requirements: + - SITE-01 +must_haves: + truths: + - "MainWindowViewModel has an ObservableCollection GlobalSelectedSites property" + - "OpenGlobalSitePickerCommand opens the site picker dialog and populates GlobalSelectedSites from the result" + - "Changing GlobalSelectedSites broadcasts GlobalSitesChangedMessage via WeakReferenceMessenger" + - "Switching tenant profiles clears GlobalSelectedSites" + - "Clearing session clears GlobalSelectedSites" + - "OpenGlobalSitePickerCommand is disabled when no profile is selected" + artifacts: + - path: "SharepointToolbox/ViewModels/MainWindowViewModel.cs" + provides: "Global site selection state, command, and message broadcast" + contains: "GlobalSelectedSites" + key_links: + - from: "SharepointToolbox/ViewModels/MainWindowViewModel.cs" + to: "SharepointToolbox/Core/Messages/GlobalSitesChangedMessage.cs" + via: "WeakReferenceMessenger.Default.Send in GlobalSelectedSites setter" + pattern: "Send.*GlobalSitesChangedMessage" +--- + + +Add global site selection state and command to MainWindowViewModel. This VM owns the global site list, broadcasts changes via GlobalSitesChangedMessage, and clears the selection on tenant switch and session clear. + +Purpose: Central state management for global site selection — the toolbar UI (plan 06-03) binds to these properties. +Output: Updated MainWindowViewModel.cs with GlobalSelectedSites, OpenGlobalSitePickerCommand, GlobalSitesSelectedLabel, and broadcast logic. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/06-global-site-selection/06-CONTEXT.md + + + +From SharepointToolbox/ViewModels/MainWindowViewModel.cs: +```csharp +public partial class MainWindowViewModel : ObservableRecipient +{ + // Existing — DO NOT MODIFY + public Func? OpenProfileManagementDialog { get; set; } + public ObservableCollection TenantProfiles { get; } + public IAsyncRelayCommand ConnectCommand { get; } + public IAsyncRelayCommand ClearSessionCommand { get; } + public RelayCommand ManageProfilesCommand { get; } + + // OnSelectedProfileChanged sends TenantSwitchedMessage + // ClearSessionAsync clears session +} +``` + +From SharepointToolbox/Core/Models/SiteInfo.cs: +```csharp +public record SiteInfo(string Url, string Title); +``` + + +From SharepointToolbox/Views/Tabs/PermissionsView.xaml.cs: +```csharp +vm.OpenSitePickerDialog = () => +{ + var factory = serviceProvider.GetRequiredService>(); + return factory(vm.CurrentProfile ?? new TenantProfile()); +}; +``` + +From SharepointToolbox/Views/Dialogs/SitePickerDialog.xaml.cs: +```csharp +public IReadOnlyList SelectedUrls => + _allItems.Where(i => i.IsSelected).Select(i => new SiteInfo(i.Url, i.Title)).ToList(); +// DialogResult = true on OK click +``` + + + + + + + Task 1: Add global site selection state, command, and broadcast to MainWindowViewModel + SharepointToolbox/ViewModels/MainWindowViewModel.cs + + Modify MainWindowViewModel to add global site selection support. All changes are additive — do not remove or modify any existing properties/methods except where noted. + + 1. Add using directives at the top (if not already present): + ```csharp + using SharepointToolbox.Core.Models; // for SiteInfo — may already be there for TenantProfile + ``` + + 2. Add a dialog factory property (same pattern as OpenProfileManagementDialog). Place it near the other dialog factory: + ```csharp + /// + /// Factory set by MainWindow.xaml.cs to open the SitePickerDialog for global site selection. + /// Returns the opened Window; ViewModel calls ShowDialog() on it. + /// + public Func? OpenGlobalSitePickerDialog { get; set; } + ``` + + 3. Add the global site selection collection and label. Place after existing observable properties: + ```csharp + public ObservableCollection GlobalSelectedSites { get; } = new(); + + /// + /// Label for toolbar display: "3 site(s) selected" or "No sites selected". + /// + public string GlobalSitesSelectedLabel => + GlobalSelectedSites.Count > 0 + ? $"{GlobalSelectedSites.Count} site(s) selected" + : "No sites selected"; + ``` + + Note: The label uses a hardcoded string for now. Plan 06-03 will replace it with a localized string once the localization keys are added. + + 4. Add the command. Declare it near the other commands: + ```csharp + public RelayCommand OpenGlobalSitePickerCommand { get; } + ``` + + 5. In the constructor, initialize the command (after ManageProfilesCommand initialization): + ```csharp + OpenGlobalSitePickerCommand = new RelayCommand(ExecuteOpenGlobalSitePicker, () => SelectedProfile != null); + GlobalSelectedSites.CollectionChanged += (_, _) => + { + OnPropertyChanged(nameof(GlobalSitesSelectedLabel)); + BroadcastGlobalSites(); + }; + ``` + + 6. Add the command implementation method: + ```csharp + private void ExecuteOpenGlobalSitePicker() + { + if (OpenGlobalSitePickerDialog == null) return; + var dialog = OpenGlobalSitePickerDialog.Invoke(); + if (dialog?.ShowDialog() == true && dialog is Views.Dialogs.SitePickerDialog picker) + { + GlobalSelectedSites.Clear(); + foreach (var site in picker.SelectedUrls) + GlobalSelectedSites.Add(site); + } + } + ``` + + 7. Add the broadcast helper method: + ```csharp + private void BroadcastGlobalSites() + { + WeakReferenceMessenger.Default.Send( + new GlobalSitesChangedMessage(GlobalSelectedSites.ToList().AsReadOnly())); + } + ``` + + 8. In `OnSelectedProfileChanged`, add after the existing body: + ```csharp + // Clear global site selection on tenant switch (sites belong to a tenant) + GlobalSelectedSites.Clear(); + OpenGlobalSitePickerCommand.NotifyCanExecuteChanged(); + ``` + + 9. In `ClearSessionAsync`, add at the END of the try block (before ConnectionStatus = "Not connected"): + ```csharp + GlobalSelectedSites.Clear(); + ``` + + 10. Add required using for the message (if not already imported): + ```csharp + using SharepointToolbox.Core.Messages; // already present for TenantSwitchedMessage + ``` + + IMPORTANT: The `using SharepointToolbox.Views.Dialogs;` namespace is needed for the `SitePickerDialog` cast in ExecuteOpenGlobalSitePicker. Add it if not present. This is acceptable since MainWindowViewModel already references `System.Windows.Window` (a View-layer type) via the dialog factory pattern. + + + cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 + + MainWindowViewModel compiles with GlobalSelectedSites collection, OpenGlobalSitePickerCommand (disabled when no profile), GlobalSitesSelectedLabel, broadcast on collection change, and clear on tenant switch + session clear. + + + + + +- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors +- MainWindowViewModel.cs contains GlobalSelectedSites ObservableCollection +- MainWindowViewModel.cs contains OpenGlobalSitePickerCommand +- MainWindowViewModel.cs contains GlobalSitesSelectedLabel property +- MainWindowViewModel.cs sends GlobalSitesChangedMessage when collection changes +- OnSelectedProfileChanged clears GlobalSelectedSites +- ClearSessionAsync clears GlobalSelectedSites + + + +MainWindowViewModel owns the global site selection state, can open the site picker dialog, broadcasts changes to all tab VMs, and clears the selection on tenant switch and session clear. The toolbar UI (plan 06-03) can bind directly to these properties and commands. + + + +After completion, create `.planning/phases/06-global-site-selection/06-02-SUMMARY.md` + diff --git a/.planning/phases/06-global-site-selection/06-03-PLAN.md b/.planning/phases/06-global-site-selection/06-03-PLAN.md new file mode 100644 index 0000000..804bf44 --- /dev/null +++ b/.planning/phases/06-global-site-selection/06-03-PLAN.md @@ -0,0 +1,254 @@ +--- +phase: 06-global-site-selection +plan: 03 +type: execute +wave: 2 +depends_on: [06-02] +files_modified: + - SharepointToolbox/MainWindow.xaml + - SharepointToolbox/MainWindow.xaml.cs + - SharepointToolbox/Localization/Strings.resx + - SharepointToolbox/Localization/Strings.fr.resx + - SharepointToolbox/ViewModels/MainWindowViewModel.cs +autonomous: true +requirements: + - SITE-01 +must_haves: + truths: + - "A 'Select Sites' button is visible in the toolbar after the Clear Session button" + - "A label next to the button shows the count of selected sites (e.g., '3 site(s) selected') or 'No sites selected'" + - "Clicking the button opens SitePickerDialog and updates the global selection" + - "The button is disabled when no tenant profile is connected" + - "The button and label use localized strings (EN + FR)" + - "The global site selection persists across tab switches (lives on MainWindowViewModel)" + artifacts: + - path: "SharepointToolbox/MainWindow.xaml" + provides: "Toolbar with global site picker button and count label" + contains: "OpenGlobalSitePickerCommand" + - path: "SharepointToolbox/MainWindow.xaml.cs" + provides: "SitePickerDialog factory wiring for toolbar" + contains: "OpenGlobalSitePickerDialog" + - path: "SharepointToolbox/Localization/Strings.resx" + provides: "EN localization keys for global site picker" + contains: "toolbar.selectSites" + - path: "SharepointToolbox/Localization/Strings.fr.resx" + provides: "FR localization keys for global site picker" + contains: "toolbar.selectSites" + key_links: + - from: "SharepointToolbox/MainWindow.xaml" + to: "SharepointToolbox/ViewModels/MainWindowViewModel.cs" + via: "Command binding for OpenGlobalSitePickerCommand" + pattern: "OpenGlobalSitePickerCommand" + - from: "SharepointToolbox/MainWindow.xaml.cs" + to: "SharepointToolbox/Views/Dialogs/SitePickerDialog.xaml.cs" + via: "Dialog factory lambda using DI" + pattern: "OpenGlobalSitePickerDialog" +--- + + +Add the global site picker button and count label to the main toolbar, wire the SitePickerDialog factory from code-behind, add localization keys for all new toolbar strings, and update MainWindowViewModel to use localized label text. + +Purpose: Makes the global site selection visible and interactive in the UI. Users see the button at all times regardless of active tab. +Output: Updated MainWindow.xaml with toolbar controls, MainWindow.xaml.cs with dialog wiring, localization files with new EN/FR keys, MainWindowViewModel using localized label. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/06-global-site-selection/06-CONTEXT.md +@.planning/phases/06-global-site-selection/06-02-SUMMARY.md + + + +```csharp +public ObservableCollection GlobalSelectedSites { get; } +public string GlobalSitesSelectedLabel { get; } // "3 site(s) selected" or "No sites selected" +public RelayCommand OpenGlobalSitePickerCommand { get; } +public Func? OpenGlobalSitePickerDialog { get; set; } // Factory set by code-behind +``` + + +From SharepointToolbox/MainWindow.xaml (ToolBar section): +```xml + + +