diff --git a/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs b/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs index 4bca9c9..9a48ac7 100644 --- a/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs +++ b/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs @@ -42,6 +42,42 @@ public sealed class ProjectLifecycleTests Assert.Equal("2026/27", manifest.Metadata["school-year"]); } + [Fact] + public void PlatzhalterAnlegen_WaehltIhnAusUndUebernimmtBearbeiteteDefinition() + { + var viewModel = new DesignerViewModel(); + viewModel.Placeholders.Clear(); + + var placeholder = viewModel.AddPlaceholder(); + placeholder.Name = "Sprache"; + placeholder.Type = LehrerApp.Templating.PlaceholderType.Text; + placeholder.Required = true; + placeholder.Sample = "Deutsch"; + + Assert.Same(placeholder, viewModel.SelectedPlaceholder); + var definition = Assert.Single(viewModel.BuildManifest().Placeholders); + Assert.Equal("Sprache", definition.Name); + Assert.Equal(LehrerApp.Templating.PlaceholderType.Text, definition.Type); + Assert.True(definition.Required); + + viewModel.RemoveSelectedPlaceholder(); + Assert.Empty(viewModel.Placeholders); + Assert.Null(viewModel.SelectedPlaceholder); + } + + [Fact] + public void DoppeltePlatzhalternamen_WerdenVerstaendlichAbgelehnt() + { + var viewModel = new DesignerViewModel(); + viewModel.Placeholders.Clear(); + viewModel.Placeholders.Add(new("Name", LehrerApp.Templating.PlaceholderType.Text, false, "A")); + viewModel.Placeholders.Add(new("Name", LehrerApp.Templating.PlaceholderType.Text, false, "B")); + + var exception = Assert.Throws(() => viewModel.BuildManifest()); + + Assert.Contains("mehrfach", exception.Message); + } + private static readonly byte[] OnePixelPng = Convert.FromBase64String( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="); } diff --git a/LehrerApp.TemplateDesigner/DesignerViewModel.cs b/LehrerApp.TemplateDesigner/DesignerViewModel.cs index 6ca04ca..abe5d12 100644 --- a/LehrerApp.TemplateDesigner/DesignerViewModel.cs +++ b/LehrerApp.TemplateDesigner/DesignerViewModel.cs @@ -60,6 +60,8 @@ public partial class DesignerViewModel : ObservableObject public ObservableCollection AssetItems { get; } = []; public ObservableCollection StarterTemplates { get; } = []; + public DesignerViewModel() => SelectedPlaceholder = Placeholders.FirstOrDefault(); + public void Reset() { TemplateId = "neue-vorlage"; TemplateName = "Neue Vorlage"; Description = ""; @@ -83,7 +85,7 @@ public partial class DesignerViewModel : ObservableObject Id = TemplateId.Trim(), Name = TemplateName.Trim(), Description = Description.Trim(), PageSize = new((float)PageWidth, (float)PageHeight, Unit), LayoutFile = "layout.tpl", Metadata = BuildMetadata(), - Placeholders = Placeholders.Select(x => new PlaceholderDefinition(x.Name.Trim(), x.Type, x.Required)).ToList(), + Placeholders = BuildPlaceholders(), }; public LoadedTemplate BuildLoaded() @@ -106,6 +108,24 @@ public partial class DesignerViewModel : ObservableObject public ITemplateDataProvider BuildDataProvider() => new DesignerDataProvider(Placeholders.ToDictionary( x => x.Name.Trim(), x => x.ToValue(), StringComparer.Ordinal)); + public DesignerPlaceholder AddPlaceholder() + { + var existing = Placeholders.Select(x => x.Name).ToHashSet(StringComparer.Ordinal); + var index = Placeholders.Count + 1; + while (existing.Contains($"Feld{index}")) index++; + var placeholder = new DesignerPlaceholder($"Feld{index}", PlaceholderType.Text, false, "Beispiel"); + Placeholders.Add(placeholder); SelectedPlaceholder = placeholder; CanExport = false; + SetStatus($"Platzhalter „{placeholder.Name}“ angelegt. Details können jetzt bearbeitet werden.", false); + return placeholder; + } + + public void RemoveSelectedPlaceholder() + { + if (SelectedPlaceholder is not { } selected) return; + Placeholders.Remove(selected); SelectedPlaceholder = null; CanExport = false; + SetStatus($"Platzhalter „{selected.Name}“ entfernt.", false); + } + public void Load(LoadedTemplate template, string layoutSource) { TemplateId = template.Manifest.Id; TemplateName = template.Manifest.Name; Description = template.Manifest.Description; @@ -118,6 +138,7 @@ public partial class DesignerViewModel : ObservableObject Placeholders.Clear(); foreach (var placeholder in template.Manifest.Placeholders) Placeholders.Add(new(placeholder.Name, placeholder.Type, placeholder.Required, DesignerPlaceholder.SampleFor(placeholder.Type))); + SelectedPlaceholder = Placeholders.FirstOrDefault(); Assets.Clear(); AssetItems.Clear(); foreach (var asset in template.Assets) AddOrReplaceAsset(asset.Key, asset.Value, keepName: true); RefreshAssetUsage(); @@ -163,6 +184,20 @@ public partial class DesignerViewModel : ObservableObject return result; } + private List BuildPlaceholders() + { + var names = new HashSet(StringComparer.Ordinal); + var result = new List(Placeholders.Count); + foreach (var placeholder in Placeholders) + { + var name = placeholder.Name.Trim(); + if (name.Length == 0) throw new InvalidDataException("Ein Platzhaltername darf nicht leer sein."); + if (!names.Add(name)) throw new InvalidDataException($"Platzhalter „{name}“ ist mehrfach definiert."); + result.Add(new(name, placeholder.Type, placeholder.Required)); + } + return result; + } + private static ITemplateDataProvider BuildSampleDataProvider(TemplateManifest manifest) => new DesignerDataProvider(manifest.Placeholders.ToDictionary(x => x.Name, x => new DesignerPlaceholder(x.Name, x.Type, x.Required, DesignerPlaceholder.SampleFor(x.Type)).ToValue(), diff --git a/LehrerApp.TemplateDesigner/MainWindow.axaml b/LehrerApp.TemplateDesigner/MainWindow.axaml index 79abea1..a5b58fa 100644 --- a/LehrerApp.TemplateDesigner/MainWindow.axaml +++ b/LehrerApp.TemplateDesigner/MainWindow.axaml @@ -92,23 +92,34 @@ -