From 5bd52250a0c5eb7942cfb68ef90a5e69c551cfbd Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Mon, 4 May 2026 10:51:29 -0500 Subject: [PATCH] authoring app launch and toml file creation fixes --- authoring-ui.Tests/MainViewModelTests.cs | 26 +++++ authoring-ui/MainViewModel.cs | 118 +++++++++++++++++++---- 2 files changed, 125 insertions(+), 19 deletions(-) diff --git a/authoring-ui.Tests/MainViewModelTests.cs b/authoring-ui.Tests/MainViewModelTests.cs index 1354751..0a1ed47 100644 --- a/authoring-ui.Tests/MainViewModelTests.cs +++ b/authoring-ui.Tests/MainViewModelTests.cs @@ -48,6 +48,32 @@ public class MainViewModelTests Assert.Contains(validation.Warnings, warning => warning.Contains("require elevation", StringComparison.OrdinalIgnoreCase)); } + [Fact] + public void ApplyDefaults_replaces_previous_suggested_entries_without_removing_manual_entries() + { + var viewModel = new MainViewModel(() => null); + viewModel.AddDirectory(@"{LocalAppData}\Manual"); + viewModel.AddFile(@"payload\manual.dll", @"{LocalAppData}\Manual\manual.dll"); + viewModel.AddPurgePath(@"{LocalAppData}\Manual"); + + viewModel.AppName = "Renamed App"; + viewModel.ApplicationFolder = "RenamedApp"; + viewModel.PrimaryPayload = @"payload\renamed.exe"; + viewModel.ApplyDefaults(resetCollections: false); + + Assert.DoesNotContain(viewModel.Directories, directory => directory.Path == @"{LocalAppData}\CovenantSetupSample"); + Assert.DoesNotContain(viewModel.Files, file => file.Source == @"payload\sample_app.cmd"); + Assert.DoesNotContain(viewModel.PurgePaths, path => path == @"{LocalAppData}\CovenantSetupSample"); + Assert.Contains(viewModel.Directories, directory => directory.Path == @"{LocalAppData}\RenamedApp"); + Assert.Contains(viewModel.Files, file => file.Source == @"payload\renamed.exe"); + Assert.Contains(viewModel.PurgePaths, path => path == @"{LocalAppData}\RenamedApp"); + Assert.Contains(viewModel.Directories, directory => directory.Path == @"{LocalAppData}\Manual"); + Assert.Contains(viewModel.Files, file => file.Source == @"payload\manual.dll"); + Assert.Contains(viewModel.PurgePaths, path => path == @"{LocalAppData}\Manual"); + Assert.DoesNotContain("CovenantSetupSample", viewModel.TomlPreview, StringComparison.Ordinal); + Assert.DoesNotContain("Covenant-Setup Sample App", viewModel.TomlPreview, StringComparison.Ordinal); + } + [Fact] public void Packaging_is_disabled_when_covenant_setup_is_missing() { diff --git a/authoring-ui/MainViewModel.cs b/authoring-ui/MainViewModel.cs index 29d6736..10c4209 100644 --- a/authoring-ui/MainViewModel.cs +++ b/authoring-ui/MainViewModel.cs @@ -18,6 +18,8 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged private string _tomlPreview = string.Empty; private string _validationSummary = string.Empty; private bool _hasValidationErrors; + private bool _suppressPreviewRefresh; + private SuggestedManifestEntries _suggestedEntries = SuggestedManifestEntries.Empty; private readonly Func _locateCovenantSetupTool; public MainViewModel() @@ -131,30 +133,52 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged public void ApplyDefaults(bool resetCollections) { - if (resetCollections) + _suppressPreviewRefresh = true; + try { - Directories.Clear(); - Files.Clear(); - Registry.Clear(); - Shortcuts.Clear(); - Scripts.Clear(); - PurgePaths.Clear(); - PurgeRegistryBranches.Clear(); + if (resetCollections) + { + Directories.Clear(); + Files.Clear(); + Registry.Clear(); + Shortcuts.Clear(); + Scripts.Clear(); + PurgePaths.Clear(); + PurgeRegistryBranches.Clear(); + } + else + { + RemoveSuggestedEntries(_suggestedEntries); + } + + var suggestedEntries = BuildSuggestedEntries(); + AddSuggestedEntries(suggestedEntries); + _suggestedEntries = suggestedEntries; + } + finally + { + _suppressPreviewRefresh = false; } + RefreshPreview(); + } + + private SuggestedManifestEntries BuildSuggestedEntries() + { var folder = string.IsNullOrWhiteSpace(ApplicationFolder) ? SanitizeIdentifier(AppName) : SanitizeIdentifier(ApplicationFolder); - ApplicationFolder = folder; + SetProperty(ref _applicationFolder, folder, nameof(ApplicationFolder), refreshPreview: false); var root = InstallRootToken.TrimEnd('\\') + "\\" + folder; - AddUnique(Directories, new DirectorySpec(root)); - AddUnique(Directories, new DirectorySpec(root + @"\bin")); - AddUnique(PurgePaths, root); - var registryBranch = @"HKCU\Software\" + folder; - AddUnique(PurgeRegistryBranches, registryBranch); - AddUnique(Registry, new RegistrySpec(registryBranch, "InstallRoot", root)); + var directories = new List + { + new(root), + new(root + @"\bin") + }; + var files = new List(); + var shortcuts = new List(); if (!string.IsNullOrWhiteSpace(PrimaryPayload)) { @@ -162,8 +186,8 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged if (!string.IsNullOrWhiteSpace(fileName)) { var destination = root + @"\bin\" + fileName; - AddUnique(Files, new FileSpec(PrimaryPayload.Trim(), destination)); - AddUnique(Shortcuts, new ShortcutSpec( + files.Add(new FileSpec(PrimaryPayload.Trim(), destination)); + shortcuts.Add(new ShortcutSpec( @"{Desktop}\" + AppName.Trim() + ".lnk", destination, null, @@ -172,7 +196,13 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged } } - RefreshPreview(); + return new SuggestedManifestEntries( + directories, + files, + [new RegistrySpec(registryBranch, "InstallRoot", root)], + shortcuts, + [root], + [registryBranch]); } public void RefreshCovenantSetupTool() @@ -351,7 +381,30 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged private void CollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) { - RefreshPreview(); + if (!_suppressPreviewRefresh) + { + RefreshPreview(); + } + } + + private void AddSuggestedEntries(SuggestedManifestEntries entries) + { + AddRangeUnique(Directories, entries.Directories); + AddRangeUnique(Files, entries.Files); + AddRangeUnique(Registry, entries.Registry); + AddRangeUnique(Shortcuts, entries.Shortcuts); + AddRangeUnique(PurgePaths, entries.PurgePaths); + AddRangeUnique(PurgeRegistryBranches, entries.PurgeRegistryBranches); + } + + private void RemoveSuggestedEntries(SuggestedManifestEntries entries) + { + RemoveRange(Directories, entries.Directories); + RemoveRange(Files, entries.Files); + RemoveRange(Registry, entries.Registry); + RemoveRange(Shortcuts, entries.Shortcuts); + RemoveRange(PurgePaths, entries.PurgePaths); + RemoveRange(PurgeRegistryBranches, entries.PurgeRegistryBranches); } private static void AddTrimmed( @@ -373,6 +426,22 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged } } + private static void AddRangeUnique(ObservableCollection collection, IEnumerable items) + { + foreach (var item in items) + { + AddUnique(collection, item); + } + } + + private static void RemoveRange(ICollection collection, IEnumerable items) + { + foreach (var item in items) + { + collection.Remove(item); + } + } + private static string? NullIfWhiteSpace(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); @@ -394,4 +463,15 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged [GeneratedRegex(@"[^A-Za-z0-9_-]+")] private static partial Regex IdentifierRegex(); + + private sealed record SuggestedManifestEntries( + IReadOnlyList Directories, + IReadOnlyList Files, + IReadOnlyList Registry, + IReadOnlyList Shortcuts, + IReadOnlyList PurgePaths, + IReadOnlyList PurgeRegistryBranches) + { + public static SuggestedManifestEntries Empty { get; } = new([], [], [], [], [], []); + } }