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>
This commit is contained in:
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Infrastructure.Persistence;
|
||||
using SharepointToolbox.Localization;
|
||||
using SharepointToolbox.Services;
|
||||
|
||||
namespace SharepointToolbox.ViewModels.Tabs;
|
||||
@@ -39,6 +40,7 @@ public partial class TemplatesViewModel : FeatureViewModelBase
|
||||
// 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; }
|
||||
@@ -78,19 +80,20 @@ public partial class TemplatesViewModel : FeatureViewModelBase
|
||||
|
||||
private async Task CaptureAsync()
|
||||
{
|
||||
var T = TranslationSource.Instance;
|
||||
if (_currentProfile == null)
|
||||
throw new InvalidOperationException("No tenant connected.");
|
||||
throw new InvalidOperationException(T["err.no_tenant"]);
|
||||
if (string.IsNullOrWhiteSpace(TemplateName))
|
||||
throw new InvalidOperationException("Template name is required.");
|
||||
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("Select at least one site from the toolbar.");
|
||||
throw new InvalidOperationException(T["err.no_sites_selected"]);
|
||||
|
||||
try
|
||||
{
|
||||
IsRunning = true;
|
||||
StatusMessage = "Capturing template...";
|
||||
StatusMessage = T["templates.status.capturing"];
|
||||
|
||||
var profile = new TenantProfile
|
||||
{
|
||||
@@ -117,11 +120,11 @@ public partial class TemplatesViewModel : FeatureViewModelBase
|
||||
Log.Information("Template captured: {Name} from {Url}", template.Name, captureSiteUrl);
|
||||
|
||||
await RefreshListAsync();
|
||||
StatusMessage = $"Template captured successfully.";
|
||||
StatusMessage = T["templates.status.success"];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Capture failed: {ex.Message}";
|
||||
StatusMessage = string.Format(T["templates.status.capture_failed"], ex.Message);
|
||||
Log.Error(ex, "Template capture failed");
|
||||
}
|
||||
finally
|
||||
@@ -133,15 +136,23 @@ public partial class TemplatesViewModel : FeatureViewModelBase
|
||||
private async Task ApplyAsync()
|
||||
{
|
||||
if (_currentProfile == null || SelectedTemplate == null) return;
|
||||
var T = TranslationSource.Instance;
|
||||
if (string.IsNullOrWhiteSpace(NewSiteTitle))
|
||||
throw new InvalidOperationException("New site title is required.");
|
||||
throw new InvalidOperationException(T["err.site_title_required"]);
|
||||
|
||||
// Auto-fill alias from title if user left it blank
|
||||
if (string.IsNullOrWhiteSpace(NewSiteAlias))
|
||||
throw new InvalidOperationException("New site alias is required.");
|
||||
{
|
||||
var generated = GenerateAliasFromTitle(NewSiteTitle);
|
||||
if (string.IsNullOrWhiteSpace(generated))
|
||||
throw new InvalidOperationException(T["err.site_alias_required"]);
|
||||
NewSiteAlias = generated;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsRunning = true;
|
||||
StatusMessage = $"Applying template...";
|
||||
StatusMessage = T["templates.status.applying"];
|
||||
|
||||
var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, CancellationToken.None);
|
||||
var progress = new Progress<OperationProgress>(p => StatusMessage = p.Message);
|
||||
@@ -150,12 +161,12 @@ public partial class TemplatesViewModel : FeatureViewModelBase
|
||||
ctx, SelectedTemplate, NewSiteTitle, NewSiteAlias,
|
||||
progress, CancellationToken.None);
|
||||
|
||||
StatusMessage = $"Template applied. Site created at: {siteUrl}";
|
||||
StatusMessage = string.Format(T["templates.status.applied"], siteUrl);
|
||||
Log.Information("Template applied. New site: {Url}", siteUrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Apply failed: {ex.Message}";
|
||||
StatusMessage = string.Format(T["templates.status.apply_failed"], ex.Message);
|
||||
Log.Error(ex, "Template apply failed");
|
||||
}
|
||||
finally
|
||||
@@ -204,6 +215,7 @@ public partial class TemplatesViewModel : FeatureViewModelBase
|
||||
TemplateName = string.Empty;
|
||||
NewSiteTitle = string.Empty;
|
||||
NewSiteAlias = string.Empty;
|
||||
_aliasManuallyEdited = false;
|
||||
StatusMessage = string.Empty;
|
||||
|
||||
_ = RefreshListAsync();
|
||||
@@ -215,4 +227,44 @@ public partial class TemplatesViewModel : FeatureViewModelBase
|
||||
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('-');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user