docs(06): create phase plan for global site selection (5 plans, 3 waves)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,13 @@
|
|||||||
2. Selecting sites in the toolbar causes all feature tabs to default to those sites when an operation is run
|
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
|
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
|
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
|
### Phase 7: User Access Audit
|
||||||
**Goal**: Administrators can audit every permission a specific user holds across selected sites and export the results
|
**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 |
|
| 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 |
|
| 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 |
|
| 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 | - |
|
| 7. User Access Audit | v1.1 | 0/? | Not started | - |
|
||||||
| 8. Simplified Permissions | v1.1 | 0/? | Not started | - |
|
| 8. Simplified Permissions | v1.1 | 0/? | Not started | - |
|
||||||
| 9. Storage Visualization | v1.1 | 0/? | Not started | - |
|
| 9. Storage Visualization | v1.1 | 0/? | Not started | - |
|
||||||
|
|||||||
187
.planning/phases/06-global-site-selection/06-01-PLAN.md
Normal file
187
.planning/phases/06-global-site-selection/06-01-PLAN.md
Normal file
@@ -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<GlobalSitesChangedMessage> in OnActivated"
|
||||||
|
pattern: "Register<GlobalSitesChangedMessage>"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/06-global-site-selection/06-CONTEXT.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Existing message pattern to follow exactly -->
|
||||||
|
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<TenantProfile>
|
||||||
|
{
|
||||||
|
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<TenantSwitchedMessage>(this, (r, m) => ((FeatureViewModelBase)r).OnTenantSwitched(m.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnTenantSwitched(TenantProfile profile)
|
||||||
|
{
|
||||||
|
// Derived classes override to reset their state
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create GlobalSitesChangedMessage</name>
|
||||||
|
<files>SharepointToolbox/Core/Messages/GlobalSitesChangedMessage.cs</files>
|
||||||
|
<action>
|
||||||
|
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<IReadOnlyList<SiteInfo>>
|
||||||
|
{
|
||||||
|
public GlobalSitesChangedMessage(IReadOnlyList<SiteInfo> sites) : base(sites) { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The value type is `IReadOnlyList<SiteInfo>` (not ObservableCollection) because the message carries a snapshot of the current selection — receivers should not mutate the sender's collection.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>GlobalSitesChangedMessage.cs exists in Core/Messages/, compiles without errors, follows the ValueChangedMessage pattern.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Extend FeatureViewModelBase with GlobalSites support</name>
|
||||||
|
<files>SharepointToolbox/ViewModels/FeatureViewModelBase.cs</files>
|
||||||
|
<action>
|
||||||
|
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
|
||||||
|
/// <summary>
|
||||||
|
/// Sites selected in the global toolbar picker. Updated via GlobalSitesChangedMessage.
|
||||||
|
/// Derived VMs check this in RunOperationAsync before falling back to per-tab SiteUrl.
|
||||||
|
/// </summary>
|
||||||
|
protected IReadOnlyList<SiteInfo> GlobalSites { get; private set; } = Array.Empty<SiteInfo>();
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In `OnActivated()`, add a second Messenger.Register call for GlobalSitesChangedMessage, right after the existing TenantSwitchedMessage registration:
|
||||||
|
```csharp
|
||||||
|
protected override void OnActivated()
|
||||||
|
{
|
||||||
|
Messenger.Register<TenantSwitchedMessage>(this, (r, m) => ((FeatureViewModelBase)r).OnTenantSwitched(m.Value));
|
||||||
|
Messenger.Register<GlobalSitesChangedMessage>(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<SiteInfo> sites)
|
||||||
|
{
|
||||||
|
GlobalSites = sites;
|
||||||
|
OnGlobalSitesChanged(sites);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Add a protected virtual method for derived classes to override:
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the global site selection changes. Override in derived VMs
|
||||||
|
/// to update UI state (e.g., pre-fill SiteUrl from first global site).
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> 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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>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</automated>
|
||||||
|
</verify>
|
||||||
|
<done>FeatureViewModelBase compiles with GlobalSites property, OnGlobalSitesChanged virtual method, and GlobalSitesChangedMessage registration in OnActivated. All existing tests still pass (no regression).</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
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.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-global-site-selection/06-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
210
.planning/phases/06-global-site-selection/06-02-PLAN.md
Normal file
210
.planning/phases/06-global-site-selection/06-02-PLAN.md
Normal file
@@ -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<SiteInfo> 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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/06-global-site-selection/06-CONTEXT.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- MainWindowViewModel current structure (add to, do not replace) -->
|
||||||
|
From SharepointToolbox/ViewModels/MainWindowViewModel.cs:
|
||||||
|
```csharp
|
||||||
|
public partial class MainWindowViewModel : ObservableRecipient
|
||||||
|
{
|
||||||
|
// Existing — DO NOT MODIFY
|
||||||
|
public Func<Window>? OpenProfileManagementDialog { get; set; }
|
||||||
|
public ObservableCollection<TenantProfile> 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);
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Dialog factory pattern used by PermissionsView — same pattern for MainWindow -->
|
||||||
|
From SharepointToolbox/Views/Tabs/PermissionsView.xaml.cs:
|
||||||
|
```csharp
|
||||||
|
vm.OpenSitePickerDialog = () =>
|
||||||
|
{
|
||||||
|
var factory = serviceProvider.GetRequiredService<Func<TenantProfile, SitePickerDialog>>();
|
||||||
|
return factory(vm.CurrentProfile ?? new TenantProfile());
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
From SharepointToolbox/Views/Dialogs/SitePickerDialog.xaml.cs:
|
||||||
|
```csharp
|
||||||
|
public IReadOnlyList<SiteInfo> SelectedUrls =>
|
||||||
|
_allItems.Where(i => i.IsSelected).Select(i => new SiteInfo(i.Url, i.Title)).ToList();
|
||||||
|
// DialogResult = true on OK click
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add global site selection state, command, and broadcast to MainWindowViewModel</name>
|
||||||
|
<files>SharepointToolbox/ViewModels/MainWindowViewModel.cs</files>
|
||||||
|
<action>
|
||||||
|
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
|
||||||
|
/// <summary>
|
||||||
|
/// Factory set by MainWindow.xaml.cs to open the SitePickerDialog for global site selection.
|
||||||
|
/// Returns the opened Window; ViewModel calls ShowDialog() on it.
|
||||||
|
/// </summary>
|
||||||
|
public Func<Window>? OpenGlobalSitePickerDialog { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add the global site selection collection and label. Place after existing observable properties:
|
||||||
|
```csharp
|
||||||
|
public ObservableCollection<SiteInfo> GlobalSelectedSites { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Label for toolbar display: "3 site(s) selected" or "No sites selected".
|
||||||
|
/// </summary>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>MainWindowViewModel compiles with GlobalSelectedSites collection, OpenGlobalSitePickerCommand (disabled when no profile), GlobalSitesSelectedLabel, broadcast on collection change, and clear on tenant switch + session clear.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
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.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-global-site-selection/06-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
254
.planning/phases/06-global-site-selection/06-03-PLAN.md
Normal file
254
.planning/phases/06-global-site-selection/06-03-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.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
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- MainWindowViewModel properties to bind to (from plan 06-02) -->
|
||||||
|
```csharp
|
||||||
|
public ObservableCollection<SiteInfo> GlobalSelectedSites { get; }
|
||||||
|
public string GlobalSitesSelectedLabel { get; } // "3 site(s) selected" or "No sites selected"
|
||||||
|
public RelayCommand OpenGlobalSitePickerCommand { get; }
|
||||||
|
public Func<Window>? OpenGlobalSitePickerDialog { get; set; } // Factory set by code-behind
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Existing toolbar XAML structure -->
|
||||||
|
From SharepointToolbox/MainWindow.xaml (ToolBar section):
|
||||||
|
```xml
|
||||||
|
<ToolBar DockPanel.Dock="Top">
|
||||||
|
<ComboBox Width="220" ... />
|
||||||
|
<Button Content="..." Command="{Binding ConnectCommand}" />
|
||||||
|
<Button Content="..." Command="{Binding ManageProfilesCommand}" />
|
||||||
|
<Separator />
|
||||||
|
<Button Content="..." Command="{Binding ClearSessionCommand}" />
|
||||||
|
<!-- NEW: Separator + Select Sites button + count label go HERE -->
|
||||||
|
</ToolBar>
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Dialog factory pattern from PermissionsView (replicate for MainWindow) -->
|
||||||
|
From SharepointToolbox/Views/Tabs/PermissionsView.xaml.cs:
|
||||||
|
```csharp
|
||||||
|
vm.OpenSitePickerDialog = () =>
|
||||||
|
{
|
||||||
|
var factory = serviceProvider.GetRequiredService<Func<TenantProfile, SitePickerDialog>>();
|
||||||
|
return factory(vm.CurrentProfile ?? new TenantProfile());
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Localization binding pattern used throughout the app -->
|
||||||
|
```xml
|
||||||
|
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[toolbar.connect]}"
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- TranslationSource pattern for code-behind label -->
|
||||||
|
```csharp
|
||||||
|
Localization.TranslationSource.Instance["key"]
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add localization keys for global site picker (EN + FR)</name>
|
||||||
|
<files>SharepointToolbox/Localization/Strings.resx, SharepointToolbox/Localization/Strings.fr.resx</files>
|
||||||
|
<action>
|
||||||
|
Add the following localization keys to both resource files.
|
||||||
|
|
||||||
|
In `Strings.resx` (English), add these data entries (maintain alphabetical ordering with existing keys if the file is sorted, otherwise append at the end before the closing `</root>` tag):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<data name="toolbar.selectSites" xml:space="preserve">
|
||||||
|
<value>Select Sites</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.selectSites.tooltip" xml:space="preserve">
|
||||||
|
<value>Select target sites for all tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.selectSites.tooltipDisabled" xml:space="preserve">
|
||||||
|
<value>Connect to a tenant first</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.globalSites.count" xml:space="preserve">
|
||||||
|
<value>{0} site(s) selected</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.globalSites.none" xml:space="preserve">
|
||||||
|
<value>No sites selected</value>
|
||||||
|
</data>
|
||||||
|
```
|
||||||
|
|
||||||
|
In `Strings.fr.resx` (French), add the matching entries:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<data name="toolbar.selectSites" xml:space="preserve">
|
||||||
|
<value>Choisir les sites</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.selectSites.tooltip" xml:space="preserve">
|
||||||
|
<value>Choisir les sites cibles pour tous les onglets</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.selectSites.tooltipDisabled" xml:space="preserve">
|
||||||
|
<value>Connectez-vous d'abord</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.globalSites.count" xml:space="preserve">
|
||||||
|
<value>{0} site(s) selectionne(s)</value>
|
||||||
|
</data>
|
||||||
|
<data name="toolbar.globalSites.none" xml:space="preserve">
|
||||||
|
<value>Aucun site selectionne</value>
|
||||||
|
</data>
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the resx files are well-formed XML after editing.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Both Strings.resx and Strings.fr.resx contain the 5 new keys each. Build succeeds (resx compiles).</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Update MainWindowViewModel label to use localized strings</name>
|
||||||
|
<files>SharepointToolbox/ViewModels/MainWindowViewModel.cs</files>
|
||||||
|
<action>
|
||||||
|
Update the GlobalSitesSelectedLabel property (added in plan 06-02) to use the new localization keys instead of hardcoded strings.
|
||||||
|
|
||||||
|
Replace the GlobalSitesSelectedLabel property with:
|
||||||
|
```csharp
|
||||||
|
public string GlobalSitesSelectedLabel =>
|
||||||
|
GlobalSelectedSites.Count > 0
|
||||||
|
? string.Format(Localization.TranslationSource.Instance["toolbar.globalSites.count"], GlobalSelectedSites.Count)
|
||||||
|
: Localization.TranslationSource.Instance["toolbar.globalSites.none"];
|
||||||
|
```
|
||||||
|
|
||||||
|
This follows the same pattern used by PermissionsViewModel.SitesSelectedLabel.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>GlobalSitesSelectedLabel uses TranslationSource localized keys instead of hardcoded strings.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Add toolbar UI controls and wire SitePickerDialog factory</name>
|
||||||
|
<files>SharepointToolbox/MainWindow.xaml, SharepointToolbox/MainWindow.xaml.cs</files>
|
||||||
|
<action>
|
||||||
|
**MainWindow.xaml** — Add a Separator, "Select Sites" button, and count label to the ToolBar, after the existing Clear Session button:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Separator />
|
||||||
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[toolbar.selectSites]}"
|
||||||
|
Command="{Binding OpenGlobalSitePickerCommand}"
|
||||||
|
ToolTip="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[toolbar.selectSites.tooltip]}" />
|
||||||
|
<TextBlock Text="{Binding GlobalSitesSelectedLabel}"
|
||||||
|
VerticalAlignment="Center" Margin="6,0,0,0"
|
||||||
|
Foreground="Gray" />
|
||||||
|
```
|
||||||
|
|
||||||
|
Place these three elements immediately after the existing `<Button Content="..." Command="{Binding ClearSessionCommand}" />` line, before the closing `</ToolBar>` tag.
|
||||||
|
|
||||||
|
Note: The button is automatically disabled when SelectedProfile is null because OpenGlobalSitePickerCommand's CanExecute checks `SelectedProfile != null`. A disabled tooltip would require a style trigger — defer that (per context, it's Claude's discretion for exact XAML layout).
|
||||||
|
|
||||||
|
**MainWindow.xaml.cs** — Wire the SitePickerDialog factory for the global site picker. In the constructor, after the existing line that wires `OpenProfileManagementDialog`, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Wire global site picker dialog factory (same pattern as PermissionsView)
|
||||||
|
viewModel.OpenGlobalSitePickerDialog = () =>
|
||||||
|
{
|
||||||
|
var factory = serviceProvider.GetRequiredService<Func<TenantProfile, SitePickerDialog>>();
|
||||||
|
return factory(viewModel.SelectedProfile ?? new TenantProfile());
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires adding a using directive for SitePickerDialog if not already present:
|
||||||
|
```csharp
|
||||||
|
using SharepointToolbox.Views.Dialogs; // already imported for ProfileManagementDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
Also add using for TenantProfile if not already present:
|
||||||
|
```csharp
|
||||||
|
using SharepointToolbox.Core.Models; // already imported
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>MainWindow.xaml shows "Select Sites" button + count label in toolbar. MainWindow.xaml.cs wires the SitePickerDialog factory to MainWindowViewModel.OpenGlobalSitePickerDialog. Build succeeds.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors
|
||||||
|
- MainWindow.xaml ToolBar contains the Select Sites button bound to OpenGlobalSitePickerCommand
|
||||||
|
- MainWindow.xaml ToolBar contains a TextBlock bound to GlobalSitesSelectedLabel
|
||||||
|
- MainWindow.xaml.cs sets viewModel.OpenGlobalSitePickerDialog factory
|
||||||
|
- Strings.resx contains 5 new toolbar.* keys
|
||||||
|
- Strings.fr.resx contains 5 matching FR translations
|
||||||
|
- MainWindowViewModel.GlobalSitesSelectedLabel uses localized strings
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
The toolbar displays a "Select Sites" button and a site count label. Clicking the button opens SitePickerDialog (when connected to a tenant). The label updates to show the count of selected sites. All strings are localized in EN and FR.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-global-site-selection/06-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
321
.planning/phases/06-global-site-selection/06-04-PLAN.md
Normal file
321
.planning/phases/06-global-site-selection/06-04-PLAN.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
---
|
||||||
|
phase: 06-global-site-selection
|
||||||
|
plan: 04
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: [06-01]
|
||||||
|
files_modified:
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/FolderStructureViewModel.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs
|
||||||
|
- SharepointToolbox/ViewModels/Tabs/BulkMembersViewModel.cs
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- SITE-01
|
||||||
|
- SITE-02
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Permissions tab pre-populates SelectedSites from global sites when no local override exists"
|
||||||
|
- "Storage, Search, Duplicates, FolderStructure tabs pre-fill SiteUrl from first global site URL"
|
||||||
|
- "Transfer tab pre-fills SourceSiteUrl from first global site URL"
|
||||||
|
- "BulkMembers tab does not consume global sites (CSV-driven, no SiteUrl field)"
|
||||||
|
- "Settings, BulkSites, Templates tabs do not consume global sites (per CONTEXT decisions)"
|
||||||
|
- "A user can type into a tab's SiteUrl field (local override) without clearing the global state"
|
||||||
|
- "Global site selection changes update all consuming tabs automatically"
|
||||||
|
artifacts:
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
|
||||||
|
provides: "Multi-site global consumption — pre-populates SelectedSites"
|
||||||
|
contains: "OnGlobalSitesChanged"
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs"
|
||||||
|
provides: "Single-site global consumption — pre-fills SiteUrl"
|
||||||
|
contains: "OnGlobalSitesChanged"
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs"
|
||||||
|
provides: "Single-site global consumption — pre-fills SiteUrl"
|
||||||
|
contains: "OnGlobalSitesChanged"
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs"
|
||||||
|
provides: "Single-site global consumption — pre-fills SiteUrl"
|
||||||
|
contains: "OnGlobalSitesChanged"
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/FolderStructureViewModel.cs"
|
||||||
|
provides: "Single-site global consumption — pre-fills SiteUrl"
|
||||||
|
contains: "OnGlobalSitesChanged"
|
||||||
|
- path: "SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs"
|
||||||
|
provides: "Single-site global consumption — pre-fills SourceSiteUrl"
|
||||||
|
contains: "OnGlobalSitesChanged"
|
||||||
|
key_links:
|
||||||
|
- from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
|
||||||
|
to: "SharepointToolbox/ViewModels/FeatureViewModelBase.cs"
|
||||||
|
via: "Override of OnGlobalSitesChanged virtual method"
|
||||||
|
pattern: "override.*OnGlobalSitesChanged"
|
||||||
|
- from: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs"
|
||||||
|
to: "SharepointToolbox/ViewModels/FeatureViewModelBase.cs"
|
||||||
|
via: "Override of OnGlobalSitesChanged virtual method"
|
||||||
|
pattern: "override.*OnGlobalSitesChanged"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Update all consuming tab ViewModels to react to global site selection changes. Multi-site tabs (Permissions) pre-populate their site list; single-site tabs pre-fill their SiteUrl from the first global site. Local overrides take priority at run time.
|
||||||
|
|
||||||
|
Purpose: Fulfills SITE-01 (all tabs consume global selection) and SITE-02 (per-tab override without clearing global state).
|
||||||
|
Output: 6 updated tab ViewModels with OnGlobalSitesChanged overrides.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.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-01-SUMMARY.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Base class contract from plan 06-01 -->
|
||||||
|
From SharepointToolbox/ViewModels/FeatureViewModelBase.cs:
|
||||||
|
```csharp
|
||||||
|
protected IReadOnlyList<SiteInfo> GlobalSites { get; private set; }
|
||||||
|
|
||||||
|
protected virtual void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
||||||
|
{
|
||||||
|
// Derived classes override to react to global site changes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- PermissionsViewModel — multi-site pattern (has SelectedSites collection) -->
|
||||||
|
From SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs:
|
||||||
|
```csharp
|
||||||
|
public ObservableCollection<SiteInfo> SelectedSites { get; } = new();
|
||||||
|
[ObservableProperty] private string _siteUrl = string.Empty;
|
||||||
|
|
||||||
|
// RunOperationAsync uses SelectedSites.Count > 0 ? SelectedSites : SiteUrl
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Single-site tab pattern (Storage, Search, Duplicates, FolderStructure) -->
|
||||||
|
From SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs:
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private string _siteUrl = string.Empty;
|
||||||
|
|
||||||
|
// RunOperationAsync checks string.IsNullOrWhiteSpace(SiteUrl)
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Transfer tab pattern (has SourceSiteUrl, not SiteUrl) -->
|
||||||
|
From SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs:
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private string _sourceSiteUrl = string.Empty;
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Tabs that do NOT consume global sites (no changes needed): -->
|
||||||
|
<!-- SettingsViewModel — no SiteUrl -->
|
||||||
|
<!-- BulkSitesViewModel — creates sites from CSV -->
|
||||||
|
<!-- TemplatesViewModel — creates new sites -->
|
||||||
|
<!-- BulkMembersViewModel — CSV-driven, no SiteUrl field -->
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Update PermissionsViewModel for multi-site global consumption</name>
|
||||||
|
<files>SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs</files>
|
||||||
|
<action>
|
||||||
|
PermissionsViewModel already supports multi-site via its SelectedSites collection. The global sites should pre-populate SelectedSites when the user has not made a local override.
|
||||||
|
|
||||||
|
Add a private field to track whether the user has made a local site selection on this tab:
|
||||||
|
```csharp
|
||||||
|
private bool _hasLocalSiteOverride;
|
||||||
|
```
|
||||||
|
|
||||||
|
Override OnGlobalSitesChanged to pre-populate SelectedSites when no local override exists:
|
||||||
|
```csharp
|
||||||
|
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
||||||
|
{
|
||||||
|
if (_hasLocalSiteOverride) return;
|
||||||
|
|
||||||
|
SelectedSites.Clear();
|
||||||
|
foreach (var site in sites)
|
||||||
|
SelectedSites.Add(site);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the existing `ExecuteOpenSitePicker` method, set `_hasLocalSiteOverride = true;` after the user picks sites locally. Add this line right before `SelectedSites.Clear()`:
|
||||||
|
```csharp
|
||||||
|
private void ExecuteOpenSitePicker()
|
||||||
|
{
|
||||||
|
if (OpenSitePickerDialog == null) return;
|
||||||
|
var dialog = OpenSitePickerDialog.Invoke();
|
||||||
|
if (dialog?.ShowDialog() == true && dialog is Views.Dialogs.SitePickerDialog picker)
|
||||||
|
{
|
||||||
|
_hasLocalSiteOverride = true; // <-- ADD THIS LINE
|
||||||
|
SelectedSites.Clear();
|
||||||
|
foreach (var site in picker.SelectedUrls)
|
||||||
|
SelectedSites.Add(site);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the existing `OnTenantSwitched` method, reset the local override flag:
|
||||||
|
```csharp
|
||||||
|
protected override void OnTenantSwitched(TenantProfile profile)
|
||||||
|
{
|
||||||
|
_currentProfile = profile;
|
||||||
|
_hasLocalSiteOverride = false; // <-- ADD THIS LINE
|
||||||
|
Results = new ObservableCollection<PermissionEntry>();
|
||||||
|
SiteUrl = string.Empty;
|
||||||
|
SelectedSites.Clear();
|
||||||
|
// ... rest unchanged
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do NOT modify RunOperationAsync — its existing logic already handles the correct priority: `SelectedSites.Count > 0 ? SelectedSites : SiteUrl`. When global sites are active, SelectedSites will be populated, so it naturally uses global sites. When user picks locally, SelectedSites has the local override.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>PermissionsViewModel overrides OnGlobalSitesChanged to pre-populate SelectedSites. Local site picker sets _hasLocalSiteOverride=true to prevent global from overwriting. Tenant switch resets the flag.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Update single-site tab VMs (Storage, Search, Duplicates, FolderStructure) for global consumption</name>
|
||||||
|
<files>
|
||||||
|
SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs,
|
||||||
|
SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs,
|
||||||
|
SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs,
|
||||||
|
SharepointToolbox/ViewModels/Tabs/FolderStructureViewModel.cs
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
All four single-site tabs follow the identical pattern: pre-fill SiteUrl from the first global site when the user has not typed a local URL.
|
||||||
|
|
||||||
|
For EACH of these four ViewModels, apply the same changes:
|
||||||
|
|
||||||
|
1. Add a using directive if not present:
|
||||||
|
```csharp
|
||||||
|
using SharepointToolbox.Core.Models; // for SiteInfo — likely already imported for TenantProfile
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add a private tracking field (place near other private fields):
|
||||||
|
```csharp
|
||||||
|
private bool _hasLocalSiteOverride;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Override OnGlobalSitesChanged:
|
||||||
|
```csharp
|
||||||
|
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
||||||
|
{
|
||||||
|
if (_hasLocalSiteOverride) return;
|
||||||
|
SiteUrl = sites.Count > 0 ? sites[0].Url : string.Empty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Detect local override when user modifies SiteUrl. Add a partial method for the [ObservableProperty] SiteUrl change notification:
|
||||||
|
```csharp
|
||||||
|
partial void OnSiteUrlChanged(string value)
|
||||||
|
{
|
||||||
|
// If the user typed something different from the global site, mark as local override.
|
||||||
|
// Empty string means user cleared it — revert to global.
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
_hasLocalSiteOverride = false;
|
||||||
|
// Re-apply global sites if available
|
||||||
|
if (GlobalSites.Count > 0)
|
||||||
|
SiteUrl = GlobalSites[0].Url;
|
||||||
|
}
|
||||||
|
else if (GlobalSites.Count == 0 || value != GlobalSites[0].Url)
|
||||||
|
{
|
||||||
|
_hasLocalSiteOverride = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
IMPORTANT: Check if any of these VMs already has a `partial void OnSiteUrlChanged` method. If so, merge the logic into the existing method rather than creating a duplicate. Currently:
|
||||||
|
- StorageViewModel: no OnSiteUrlChanged — add it
|
||||||
|
- SearchViewModel: no OnSiteUrlChanged — add it
|
||||||
|
- DuplicatesViewModel: no OnSiteUrlChanged — add it
|
||||||
|
- FolderStructureViewModel: no OnSiteUrlChanged — add it
|
||||||
|
|
||||||
|
5. In the existing `OnTenantSwitched` method of each VM, add `_hasLocalSiteOverride = false;` at the beginning of the method body (after `_currentProfile = profile;`).
|
||||||
|
|
||||||
|
Do NOT modify RunOperationAsync in any of these VMs — they already check `string.IsNullOrWhiteSpace(SiteUrl)` and use the value directly. When global sites are active, SiteUrl will be pre-filled, so the existing logic works.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>StorageViewModel, SearchViewModel, DuplicatesViewModel, and FolderStructureViewModel all override OnGlobalSitesChanged to pre-fill SiteUrl from first global site. Local typing sets _hasLocalSiteOverride=true. Tenant switch resets the flag. Build succeeds.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Update TransferViewModel and verify BulkMembersViewModel excluded</name>
|
||||||
|
<files>
|
||||||
|
SharepointToolbox/ViewModels/Tabs/TransferViewModel.cs,
|
||||||
|
SharepointToolbox/ViewModels/Tabs/BulkMembersViewModel.cs
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**TransferViewModel** — Pre-fill SourceSiteUrl from first global site (same pattern as single-site tabs, but the field is SourceSiteUrl not SiteUrl).
|
||||||
|
|
||||||
|
1. Add tracking field:
|
||||||
|
```csharp
|
||||||
|
private bool _hasLocalSourceSiteOverride;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Override OnGlobalSitesChanged:
|
||||||
|
```csharp
|
||||||
|
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
||||||
|
{
|
||||||
|
if (_hasLocalSourceSiteOverride) return;
|
||||||
|
SourceSiteUrl = sites.Count > 0 ? sites[0].Url : string.Empty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add partial method for SourceSiteUrl change notification:
|
||||||
|
```csharp
|
||||||
|
partial void OnSourceSiteUrlChanged(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
_hasLocalSourceSiteOverride = false;
|
||||||
|
if (GlobalSites.Count > 0)
|
||||||
|
SourceSiteUrl = GlobalSites[0].Url;
|
||||||
|
}
|
||||||
|
else if (GlobalSites.Count == 0 || value != GlobalSites[0].Url)
|
||||||
|
{
|
||||||
|
_hasLocalSourceSiteOverride = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. In the existing `OnTenantSwitched` method, add `_hasLocalSourceSiteOverride = false;` at the beginning.
|
||||||
|
|
||||||
|
**BulkMembersViewModel** — Verify it does NOT need changes. BulkMembersViewModel has no SiteUrl field (it reads site URLs from CSV rows). Confirm this by checking: it should NOT have an OnGlobalSitesChanged override. Do NOT modify this file — only verify it has no SiteUrl property.
|
||||||
|
|
||||||
|
Note: SettingsViewModel, BulkSitesViewModel, and TemplatesViewModel also do NOT consume global sites per the CONTEXT decisions. Do NOT modify them.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>TransferViewModel overrides OnGlobalSitesChanged to pre-fill SourceSiteUrl. BulkMembersViewModel is confirmed excluded (no SiteUrl, no override). Build succeeds.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors
|
||||||
|
- `dotnet test` shows no new failures
|
||||||
|
- PermissionsViewModel has OnGlobalSitesChanged override populating SelectedSites
|
||||||
|
- StorageViewModel, SearchViewModel, DuplicatesViewModel, FolderStructureViewModel have OnGlobalSitesChanged override setting SiteUrl
|
||||||
|
- TransferViewModel has OnGlobalSitesChanged override setting SourceSiteUrl
|
||||||
|
- BulkMembersViewModel, SettingsViewModel, BulkSitesViewModel, TemplatesViewModel are NOT modified
|
||||||
|
- All consuming VMs have _hasLocalSiteOverride tracking
|
||||||
|
- All consuming VMs reset the override flag on tenant switch
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
Every tab that should consume global sites does so automatically. Multi-site tab (Permissions) pre-populates its SelectedSites collection. Single-site tabs pre-fill their SiteUrl/SourceSiteUrl from the first global site. Users can type a different URL on any tab without clearing the global state. Tabs that don't apply (Settings, BulkSites, Templates, BulkMembers) are unaffected.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-global-site-selection/06-04-SUMMARY.md`
|
||||||
|
</output>
|
||||||
206
.planning/phases/06-global-site-selection/06-05-PLAN.md
Normal file
206
.planning/phases/06-global-site-selection/06-05-PLAN.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
---
|
||||||
|
phase: 06-global-site-selection
|
||||||
|
plan: 05
|
||||||
|
type: execute
|
||||||
|
wave: 3
|
||||||
|
depends_on: [06-01, 06-02, 06-04]
|
||||||
|
files_modified:
|
||||||
|
- SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- SITE-01
|
||||||
|
- SITE-02
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Unit tests verify GlobalSitesChangedMessage broadcasts when MainWindowViewModel global sites change"
|
||||||
|
- "Unit tests verify FeatureViewModelBase receives global sites and updates GlobalSites property"
|
||||||
|
- "Unit tests verify single-site tab VMs pre-fill SiteUrl from first global site"
|
||||||
|
- "Unit tests verify PermissionsViewModel pre-populates SelectedSites from global sites"
|
||||||
|
- "Unit tests verify local override prevents global sites from overwriting tab state"
|
||||||
|
- "Unit tests verify tenant switch clears global site selection"
|
||||||
|
- "All tests pass with dotnet test"
|
||||||
|
artifacts:
|
||||||
|
- path: "SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs"
|
||||||
|
provides: "Comprehensive unit tests for global site selection flow"
|
||||||
|
contains: "GlobalSiteSelectionTests"
|
||||||
|
key_links:
|
||||||
|
- from: "SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs"
|
||||||
|
to: "SharepointToolbox/ViewModels/MainWindowViewModel.cs"
|
||||||
|
via: "Tests broadcast and clear behavior"
|
||||||
|
pattern: "GlobalSelectedSites"
|
||||||
|
- from: "SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs"
|
||||||
|
to: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs"
|
||||||
|
via: "Tests single-site consumption and local override"
|
||||||
|
pattern: "OnGlobalSitesChanged"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create unit tests covering the full global site selection flow: message broadcast, base class reception, tab VM consumption, local override behavior, and tenant switch clearing.
|
||||||
|
|
||||||
|
Purpose: Verify the contracts established in plans 06-01 through 06-04 work correctly end-to-end without requiring a live SharePoint tenant.
|
||||||
|
Output: GlobalSiteSelectionTests.cs with passing tests covering all critical paths.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.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-01-SUMMARY.md
|
||||||
|
@.planning/phases/06-global-site-selection/06-02-SUMMARY.md
|
||||||
|
@.planning/phases/06-global-site-selection/06-04-SUMMARY.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- From plan 06-01: Base class contract -->
|
||||||
|
```csharp
|
||||||
|
// FeatureViewModelBase
|
||||||
|
protected IReadOnlyList<SiteInfo> GlobalSites { get; private set; }
|
||||||
|
protected virtual void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites) { }
|
||||||
|
// Registers for GlobalSitesChangedMessage in OnActivated()
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- From plan 06-02: MainWindowViewModel -->
|
||||||
|
```csharp
|
||||||
|
public ObservableCollection<SiteInfo> GlobalSelectedSites { get; }
|
||||||
|
public RelayCommand OpenGlobalSitePickerCommand { get; }
|
||||||
|
public string GlobalSitesSelectedLabel { get; }
|
||||||
|
// CollectionChanged on GlobalSelectedSites sends GlobalSitesChangedMessage
|
||||||
|
// OnSelectedProfileChanged clears GlobalSelectedSites
|
||||||
|
// ClearSessionAsync clears GlobalSelectedSites
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- From plan 06-04: Tab VM overrides -->
|
||||||
|
```csharp
|
||||||
|
// StorageViewModel (and Search, Duplicates, FolderStructure)
|
||||||
|
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
||||||
|
{
|
||||||
|
if (_hasLocalSiteOverride) return;
|
||||||
|
SiteUrl = sites.Count > 0 ? sites[0].Url : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermissionsViewModel
|
||||||
|
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
||||||
|
{
|
||||||
|
if (_hasLocalSiteOverride) return;
|
||||||
|
SelectedSites.Clear();
|
||||||
|
foreach (var site in sites) SelectedSites.Add(site);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Existing test patterns (from v1.0) -->
|
||||||
|
```csharp
|
||||||
|
// Tests use Moq for service interfaces, internal constructors for VMs
|
||||||
|
// InternalsVisibleTo is already configured
|
||||||
|
// WeakReferenceMessenger.Default for message sending in tests
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Create GlobalSiteSelectionTests with comprehensive test coverage</name>
|
||||||
|
<files>SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs</files>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: GlobalSitesChangedMessage carries site list — send message, verify receiver gets the sites
|
||||||
|
- Test 2: FeatureViewModelBase updates GlobalSites on message receive — send message to a derived VM, check GlobalSites property
|
||||||
|
- Test 3: StorageViewModel pre-fills SiteUrl from first global site — send global sites message, verify SiteUrl equals first site URL
|
||||||
|
- Test 4: StorageViewModel local override prevents global update — set SiteUrl manually, then send global sites, verify SiteUrl unchanged
|
||||||
|
- Test 5: StorageViewModel clearing SiteUrl reverts to global — set local override, clear SiteUrl, verify it reverts to global site
|
||||||
|
- Test 6: PermissionsViewModel pre-populates SelectedSites from global sites — send global sites, verify SelectedSites matches
|
||||||
|
- Test 7: PermissionsViewModel local picker override prevents global update — mark local override, send global sites, verify SelectedSites unchanged
|
||||||
|
- Test 8: Tenant switch clears global sites on StorageViewModel — send global sites, then send TenantSwitchedMessage, verify SiteUrl cleared and override reset
|
||||||
|
- Test 9: TransferViewModel pre-fills SourceSiteUrl from first global site
|
||||||
|
- Test 10: MainWindowViewModel GlobalSitesSelectedLabel updates with count — add sites to GlobalSelectedSites, verify label text
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Create `SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs` with the tests described above.
|
||||||
|
|
||||||
|
Use the existing test patterns from the project:
|
||||||
|
- Moq for `IStorageService`, `ISessionManager`, `IPermissionsService`, `ISiteListService`, `ILogger<FeatureViewModelBase>`
|
||||||
|
- Internal test constructors for ViewModels (already available via InternalsVisibleTo)
|
||||||
|
- `WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(...))` to simulate the toolbar broadcasting
|
||||||
|
|
||||||
|
Key implementation notes:
|
||||||
|
|
||||||
|
1. For tests that need to verify GlobalSites property on FeatureViewModelBase: Create a minimal concrete subclass in the test file:
|
||||||
|
```csharp
|
||||||
|
private class TestFeatureViewModel : FeatureViewModelBase
|
||||||
|
{
|
||||||
|
public TestFeatureViewModel(ILogger<FeatureViewModelBase> logger) : base(logger) { }
|
||||||
|
protected override Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
// Expose protected property for test assertions
|
||||||
|
public IReadOnlyList<SiteInfo> TestGlobalSites => GlobalSites;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. For StorageViewModel tests: use the internal test constructor `StorageViewModel(IStorageService, ISessionManager, ILogger<FeatureViewModelBase>)`.
|
||||||
|
|
||||||
|
3. For PermissionsViewModel tests: use the internal test constructor `PermissionsViewModel(IPermissionsService, ISiteListService, ISessionManager, ILogger<FeatureViewModelBase>)`.
|
||||||
|
|
||||||
|
4. For TransferViewModel tests: use the production constructor with mocked dependencies. Check if TransferViewModel has an internal test constructor — if not, mock all constructor parameters.
|
||||||
|
|
||||||
|
5. For MainWindowViewModel label test: use the production constructor with mocked ProfileService, SessionManager, ILogger. Add SiteInfo items to GlobalSelectedSites and assert the label property.
|
||||||
|
|
||||||
|
6. Reset WeakReferenceMessenger.Default between tests to avoid cross-test contamination:
|
||||||
|
```csharp
|
||||||
|
public GlobalSiteSelectionTests()
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Reset();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Each test should be a `[Fact]` with a descriptive name following the pattern: `MethodOrScenario_Condition_ExpectedResult`.
|
||||||
|
|
||||||
|
Example test structure:
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void OnGlobalSitesChanged_WithSites_PreFillsSiteUrlOnStorageTab()
|
||||||
|
{
|
||||||
|
var logger = Mock.Of<ILogger<FeatureViewModelBase>>();
|
||||||
|
var vm = new StorageViewModel(
|
||||||
|
Mock.Of<IStorageService>(),
|
||||||
|
Mock.Of<ISessionManager>(),
|
||||||
|
logger);
|
||||||
|
|
||||||
|
var sites = new List<SiteInfo>
|
||||||
|
{
|
||||||
|
new("https://contoso.sharepoint.com/sites/hr", "HR"),
|
||||||
|
new("https://contoso.sharepoint.com/sites/finance", "Finance")
|
||||||
|
};
|
||||||
|
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites.AsReadOnly()));
|
||||||
|
|
||||||
|
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Write all 10 tests. Ensure every test has clear Arrange/Act/Assert sections.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~GlobalSiteSelection" --verbosity normal 2>&1 | tail -20</automated>
|
||||||
|
</verify>
|
||||||
|
<done>All 10 tests in GlobalSiteSelectionTests pass. Tests cover message broadcast, base class reception, single-site pre-fill, multi-site pre-populate, local override, override reset, tenant switch clear, and label update.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `dotnet test SharepointToolbox.Tests --filter "GlobalSiteSelection"` shows 10 passed, 0 failed
|
||||||
|
- `dotnet test SharepointToolbox.Tests` shows no regressions in existing tests
|
||||||
|
- Test file exists at SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs
|
||||||
|
- Tests cover both SITE-01 (global consumption) and SITE-02 (local override) requirements
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
All 10 unit tests pass, validating the full global site selection contract: message creation, base class plumbing, tab VM consumption (multi-site and single-site), local override behavior, and tenant switch clearing. No regressions in existing test suite.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-global-site-selection/06-05-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user