diff --git a/SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs b/SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs new file mode 100644 index 0000000..8dd54cd --- /dev/null +++ b/SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs @@ -0,0 +1,218 @@ +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 _logger; + private TenantProfile? _currentProfile; + + // Template list + private ObservableCollection _templates = new(); + public ObservableCollection Templates + { + get => _templates; + private set { _templates = value; OnPropertyChanged(); } + } + + [ObservableProperty] private SiteTemplate? _selectedTemplate; + + // Capture options + [ObservableProperty] private string _captureSiteUrl = string.Empty; + [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? RenameDialogFactory { get; set; } + + public TemplatesViewModel( + ITemplateService templateService, + TemplateRepository templateRepo, + ISessionManager sessionManager, + ILogger 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 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(CaptureSiteUrl)) + throw new InvalidOperationException("Site URL is required."); + if (string.IsNullOrWhiteSpace(TemplateName)) + throw new InvalidOperationException("Template name is required."); + + 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(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(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(templates); + }); + } + + protected override void OnTenantSwitched(TenantProfile profile) + { + _currentProfile = profile; + CaptureSiteUrl = string.Empty; + TemplateName = string.Empty; + NewSiteTitle = string.Empty; + NewSiteAlias = string.Empty; + StatusMessage = string.Empty; + + _ = RefreshListAsync(); + } + + partial void OnSelectedTemplateChanged(SiteTemplate? value) + { + ApplyCommand.NotifyCanExecuteChanged(); + RenameCommand.NotifyCanExecuteChanged(); + DeleteCommand.NotifyCanExecuteChanged(); + } +} diff --git a/SharepointToolbox/Views/Tabs/TemplatesView.xaml b/SharepointToolbox/Views/Tabs/TemplatesView.xaml new file mode 100644 index 0000000..30e54a8 --- /dev/null +++ b/SharepointToolbox/Views/Tabs/TemplatesView.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + +