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>
219 lines
7.8 KiB
C#
219 lines
7.8 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Windows;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.Extensions.Logging;
|
|
using Serilog;
|
|
using SharepointToolbox.Core.Models;
|
|
using SharepointToolbox.Infrastructure.Persistence;
|
|
using SharepointToolbox.Services;
|
|
|
|
namespace SharepointToolbox.ViewModels.Tabs;
|
|
|
|
public partial class TemplatesViewModel : FeatureViewModelBase
|
|
{
|
|
private readonly ITemplateService _templateService;
|
|
private readonly TemplateRepository _templateRepo;
|
|
private readonly ISessionManager _sessionManager;
|
|
private readonly ILogger<FeatureViewModelBase> _logger;
|
|
private TenantProfile? _currentProfile;
|
|
|
|
// Template list
|
|
private ObservableCollection<SiteTemplate> _templates = new();
|
|
public ObservableCollection<SiteTemplate> Templates
|
|
{
|
|
get => _templates;
|
|
private set { _templates = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
[ObservableProperty] private SiteTemplate? _selectedTemplate;
|
|
|
|
// Capture options
|
|
[ObservableProperty] private string _templateName = string.Empty;
|
|
[ObservableProperty] private bool _captureLibraries = true;
|
|
[ObservableProperty] private bool _captureFolders = true;
|
|
[ObservableProperty] private bool _capturePermissions = true;
|
|
[ObservableProperty] private bool _captureLogo = true;
|
|
[ObservableProperty] private bool _captureSettings = true;
|
|
|
|
// Apply options
|
|
[ObservableProperty] private string _newSiteTitle = string.Empty;
|
|
[ObservableProperty] private string _newSiteAlias = string.Empty;
|
|
|
|
public IAsyncRelayCommand CaptureCommand { get; }
|
|
public IAsyncRelayCommand ApplyCommand { get; }
|
|
public IAsyncRelayCommand RenameCommand { get; }
|
|
public IAsyncRelayCommand DeleteCommand { get; }
|
|
public IAsyncRelayCommand RefreshCommand { get; }
|
|
|
|
public TenantProfile? CurrentProfile => _currentProfile;
|
|
|
|
// Factory for rename dialog — set by View code-behind
|
|
public Func<string, string?>? RenameDialogFactory { get; set; }
|
|
|
|
public TemplatesViewModel(
|
|
ITemplateService templateService,
|
|
TemplateRepository templateRepo,
|
|
ISessionManager sessionManager,
|
|
ILogger<FeatureViewModelBase> logger)
|
|
: base(logger)
|
|
{
|
|
_templateService = templateService;
|
|
_templateRepo = templateRepo;
|
|
_sessionManager = sessionManager;
|
|
_logger = logger;
|
|
|
|
CaptureCommand = new AsyncRelayCommand(CaptureAsync, () => !IsRunning);
|
|
ApplyCommand = new AsyncRelayCommand(ApplyAsync, () => !IsRunning && SelectedTemplate != null);
|
|
RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedTemplate != null);
|
|
DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedTemplate != null);
|
|
RefreshCommand = new AsyncRelayCommand(RefreshListAsync);
|
|
}
|
|
|
|
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
|
{
|
|
// Not used directly — Capture and Apply have their own async commands
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private async Task CaptureAsync()
|
|
{
|
|
if (_currentProfile == null)
|
|
throw new InvalidOperationException("No tenant connected.");
|
|
if (string.IsNullOrWhiteSpace(TemplateName))
|
|
throw new InvalidOperationException("Template name is required.");
|
|
|
|
var captureSiteUrl = GlobalSites.Select(s => s.Url).FirstOrDefault(u => !string.IsNullOrWhiteSpace(u));
|
|
if (string.IsNullOrWhiteSpace(captureSiteUrl))
|
|
throw new InvalidOperationException("Select at least one site from the toolbar.");
|
|
|
|
try
|
|
{
|
|
IsRunning = true;
|
|
StatusMessage = "Capturing template...";
|
|
|
|
var profile = new TenantProfile
|
|
{
|
|
Name = _currentProfile.Name,
|
|
TenantUrl = captureSiteUrl,
|
|
ClientId = _currentProfile.ClientId,
|
|
};
|
|
var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None);
|
|
|
|
var options = new SiteTemplateOptions
|
|
{
|
|
CaptureLibraries = CaptureLibraries,
|
|
CaptureFolders = CaptureFolders,
|
|
CapturePermissionGroups = CapturePermissions,
|
|
CaptureLogo = CaptureLogo,
|
|
CaptureSettings = CaptureSettings,
|
|
};
|
|
|
|
var progress = new Progress<OperationProgress>(p => StatusMessage = p.Message);
|
|
var template = await _templateService.CaptureTemplateAsync(ctx, options, progress, CancellationToken.None);
|
|
template.Name = TemplateName;
|
|
|
|
await _templateRepo.SaveAsync(template);
|
|
Log.Information("Template captured: {Name} from {Url}", template.Name, captureSiteUrl);
|
|
|
|
await RefreshListAsync();
|
|
StatusMessage = $"Template captured successfully.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Capture failed: {ex.Message}";
|
|
Log.Error(ex, "Template capture failed");
|
|
}
|
|
finally
|
|
{
|
|
IsRunning = false;
|
|
}
|
|
}
|
|
|
|
private async Task ApplyAsync()
|
|
{
|
|
if (_currentProfile == null || SelectedTemplate == null) return;
|
|
if (string.IsNullOrWhiteSpace(NewSiteTitle))
|
|
throw new InvalidOperationException("New site title is required.");
|
|
if (string.IsNullOrWhiteSpace(NewSiteAlias))
|
|
throw new InvalidOperationException("New site alias is required.");
|
|
|
|
try
|
|
{
|
|
IsRunning = true;
|
|
StatusMessage = $"Applying template...";
|
|
|
|
var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, CancellationToken.None);
|
|
var progress = new Progress<OperationProgress>(p => StatusMessage = p.Message);
|
|
|
|
var siteUrl = await _templateService.ApplyTemplateAsync(
|
|
ctx, SelectedTemplate, NewSiteTitle, NewSiteAlias,
|
|
progress, CancellationToken.None);
|
|
|
|
StatusMessage = $"Template applied. Site created at: {siteUrl}";
|
|
Log.Information("Template applied. New site: {Url}", siteUrl);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Apply failed: {ex.Message}";
|
|
Log.Error(ex, "Template apply failed");
|
|
}
|
|
finally
|
|
{
|
|
IsRunning = false;
|
|
}
|
|
}
|
|
|
|
private async Task RenameAsync()
|
|
{
|
|
if (SelectedTemplate == null) return;
|
|
|
|
if (RenameDialogFactory != null)
|
|
{
|
|
var newName = RenameDialogFactory(SelectedTemplate.Name);
|
|
if (!string.IsNullOrWhiteSpace(newName))
|
|
{
|
|
await _templateRepo.RenameAsync(SelectedTemplate.Id, newName);
|
|
await RefreshListAsync();
|
|
Log.Information("Template renamed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task DeleteAsync()
|
|
{
|
|
if (SelectedTemplate == null) return;
|
|
|
|
await _templateRepo.DeleteAsync(SelectedTemplate.Id);
|
|
await RefreshListAsync();
|
|
Log.Information("Template deleted.");
|
|
}
|
|
|
|
private async Task RefreshListAsync()
|
|
{
|
|
var templates = await _templateRepo.GetAllAsync();
|
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
{
|
|
Templates = new ObservableCollection<SiteTemplate>(templates);
|
|
});
|
|
}
|
|
|
|
protected override void OnTenantSwitched(TenantProfile profile)
|
|
{
|
|
_currentProfile = profile;
|
|
TemplateName = string.Empty;
|
|
NewSiteTitle = string.Empty;
|
|
NewSiteAlias = string.Empty;
|
|
StatusMessage = string.Empty;
|
|
|
|
_ = RefreshListAsync();
|
|
}
|
|
|
|
partial void OnSelectedTemplateChanged(SiteTemplate? value)
|
|
{
|
|
ApplyCommand.NotifyCanExecuteChanged();
|
|
RenameCommand.NotifyCanExecuteChanged();
|
|
DeleteCommand.NotifyCanExecuteChanged();
|
|
}
|
|
}
|