Files
Sharepoint-Toolbox/SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs
T
Dev 12dd1de9f2 chore: release v2.4
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager)
- Add InputDialog, Spinner common view
- Add DuplicatesCsvExportService
- Refresh views, dialogs, and view models across tabs
- Update localization strings (en/fr)
- Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:50:03 +02:00

271 lines
9.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.Localization;
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;
private bool _aliasManuallyEdited;
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()
{
var T = TranslationSource.Instance;
if (_currentProfile == null)
throw new InvalidOperationException(T["err.no_tenant"]);
if (string.IsNullOrWhiteSpace(TemplateName))
throw new InvalidOperationException(T["err.template_name_required"]);
var captureSiteUrl = GlobalSites.Select(s => s.Url).FirstOrDefault(u => !string.IsNullOrWhiteSpace(u));
if (string.IsNullOrWhiteSpace(captureSiteUrl))
throw new InvalidOperationException(T["err.no_sites_selected"]);
try
{
IsRunning = true;
StatusMessage = T["templates.status.capturing"];
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 = T["templates.status.success"];
}
catch (Exception ex)
{
StatusMessage = string.Format(T["templates.status.capture_failed"], ex.Message);
Log.Error(ex, "Template capture failed");
}
finally
{
IsRunning = false;
}
}
private async Task ApplyAsync()
{
if (_currentProfile == null || SelectedTemplate == null) return;
var T = TranslationSource.Instance;
if (string.IsNullOrWhiteSpace(NewSiteTitle))
throw new InvalidOperationException(T["err.site_title_required"]);
// Auto-fill alias from title if user left it blank
if (string.IsNullOrWhiteSpace(NewSiteAlias))
{
var generated = GenerateAliasFromTitle(NewSiteTitle);
if (string.IsNullOrWhiteSpace(generated))
throw new InvalidOperationException(T["err.site_alias_required"]);
NewSiteAlias = generated;
}
try
{
IsRunning = true;
StatusMessage = T["templates.status.applying"];
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 = string.Format(T["templates.status.applied"], siteUrl);
Log.Information("Template applied. New site: {Url}", siteUrl);
}
catch (Exception ex)
{
StatusMessage = string.Format(T["templates.status.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;
_aliasManuallyEdited = false;
StatusMessage = string.Empty;
_ = RefreshListAsync();
}
partial void OnSelectedTemplateChanged(SiteTemplate? value)
{
ApplyCommand.NotifyCanExecuteChanged();
RenameCommand.NotifyCanExecuteChanged();
DeleteCommand.NotifyCanExecuteChanged();
}
partial void OnNewSiteTitleChanged(string value)
{
if (_aliasManuallyEdited) return;
var generated = GenerateAliasFromTitle(value);
if (NewSiteAlias != generated)
{
// Bypass user-edit flag while we sync alias to title
_suppressAliasEditedFlag = true;
NewSiteAlias = generated;
_suppressAliasEditedFlag = false;
}
}
private bool _suppressAliasEditedFlag;
partial void OnNewSiteAliasChanged(string value)
{
if (_suppressAliasEditedFlag) return;
_aliasManuallyEdited = !string.IsNullOrWhiteSpace(value)
&& value != GenerateAliasFromTitle(NewSiteTitle);
if (string.IsNullOrWhiteSpace(value)) _aliasManuallyEdited = false;
}
internal static string GenerateAliasFromTitle(string title)
{
if (string.IsNullOrWhiteSpace(title)) return string.Empty;
var normalized = title.Normalize(System.Text.NormalizationForm.FormD);
var sb = new System.Text.StringBuilder(normalized.Length);
foreach (var c in normalized)
{
var cat = System.Globalization.CharUnicodeInfo.GetUnicodeCategory(c);
if (cat == System.Globalization.UnicodeCategory.NonSpacingMark) continue;
if (char.IsLetterOrDigit(c)) sb.Append(c);
else if (c == ' ' || c == '-' || c == '_') sb.Append('-');
}
var collapsed = System.Text.RegularExpressions.Regex.Replace(sb.ToString(), "-+", "-");
return collapsed.Trim('-');
}
}