Fix Platzhalter Editer im TemplateDesigner
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-08-30 22:30:26 +02:00
parent 6468596bf3
commit 9f28081931
5 changed files with 94 additions and 17 deletions
@@ -42,6 +42,42 @@ public sealed class ProjectLifecycleTests
Assert.Equal("2026/27", manifest.Metadata["school-year"]); 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<InvalidDataException>(() => viewModel.BuildManifest());
Assert.Contains("mehrfach", exception.Message);
}
private static readonly byte[] OnePixelPng = Convert.FromBase64String( private static readonly byte[] OnePixelPng = Convert.FromBase64String(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="); "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=");
} }
@@ -60,6 +60,8 @@ public partial class DesignerViewModel : ObservableObject
public ObservableCollection<DesignerAsset> AssetItems { get; } = []; public ObservableCollection<DesignerAsset> AssetItems { get; } = [];
public ObservableCollection<StarterTemplateItem> StarterTemplates { get; } = []; public ObservableCollection<StarterTemplateItem> StarterTemplates { get; } = [];
public DesignerViewModel() => SelectedPlaceholder = Placeholders.FirstOrDefault();
public void Reset() public void Reset()
{ {
TemplateId = "neue-vorlage"; TemplateName = "Neue Vorlage"; Description = ""; 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(), Id = TemplateId.Trim(), Name = TemplateName.Trim(), Description = Description.Trim(),
PageSize = new((float)PageWidth, (float)PageHeight, Unit), LayoutFile = "layout.tpl", PageSize = new((float)PageWidth, (float)PageHeight, Unit), LayoutFile = "layout.tpl",
Metadata = BuildMetadata(), Metadata = BuildMetadata(),
Placeholders = Placeholders.Select(x => new PlaceholderDefinition(x.Name.Trim(), x.Type, x.Required)).ToList(), Placeholders = BuildPlaceholders(),
}; };
public LoadedTemplate BuildLoaded() public LoadedTemplate BuildLoaded()
@@ -106,6 +108,24 @@ public partial class DesignerViewModel : ObservableObject
public ITemplateDataProvider BuildDataProvider() => new DesignerDataProvider(Placeholders.ToDictionary( public ITemplateDataProvider BuildDataProvider() => new DesignerDataProvider(Placeholders.ToDictionary(
x => x.Name.Trim(), x => x.ToValue(), StringComparer.Ordinal)); 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) public void Load(LoadedTemplate template, string layoutSource)
{ {
TemplateId = template.Manifest.Id; TemplateName = template.Manifest.Name; Description = template.Manifest.Description; TemplateId = template.Manifest.Id; TemplateName = template.Manifest.Name; Description = template.Manifest.Description;
@@ -118,6 +138,7 @@ public partial class DesignerViewModel : ObservableObject
Placeholders.Clear(); Placeholders.Clear();
foreach (var placeholder in template.Manifest.Placeholders) foreach (var placeholder in template.Manifest.Placeholders)
Placeholders.Add(new(placeholder.Name, placeholder.Type, placeholder.Required, DesignerPlaceholder.SampleFor(placeholder.Type))); Placeholders.Add(new(placeholder.Name, placeholder.Type, placeholder.Required, DesignerPlaceholder.SampleFor(placeholder.Type)));
SelectedPlaceholder = Placeholders.FirstOrDefault();
Assets.Clear(); AssetItems.Clear(); Assets.Clear(); AssetItems.Clear();
foreach (var asset in template.Assets) AddOrReplaceAsset(asset.Key, asset.Value, keepName: true); foreach (var asset in template.Assets) AddOrReplaceAsset(asset.Key, asset.Value, keepName: true);
RefreshAssetUsage(); RefreshAssetUsage();
@@ -163,6 +184,20 @@ public partial class DesignerViewModel : ObservableObject
return result; return result;
} }
private List<PlaceholderDefinition> BuildPlaceholders()
{
var names = new HashSet<string>(StringComparer.Ordinal);
var result = new List<PlaceholderDefinition>(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) => private static ITemplateDataProvider BuildSampleDataProvider(TemplateManifest manifest) =>
new DesignerDataProvider(manifest.Placeholders.ToDictionary(x => x.Name, new DesignerDataProvider(manifest.Placeholders.ToDictionary(x => x.Name,
x => new DesignerPlaceholder(x.Name, x.Type, x.Required, DesignerPlaceholder.SampleFor(x.Type)).ToValue(), x => new DesignerPlaceholder(x.Name, x.Type, x.Required, DesignerPlaceholder.SampleFor(x.Type)).ToValue(),
+20 -9
View File
@@ -92,23 +92,34 @@
</Grid> </Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Platzhalter &amp; Beispieldaten" Classes="section"/> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Platzhalter &amp; Beispieldaten" Classes="section"/>
<Button Grid.Column="1" Content="" Click="OnAddPlaceholder"/></Grid> <Button Grid.Column="1" Content=" Platzhalter" Click="OnAddPlaceholder"/></Grid>
<DataGrid x:Name="PlaceholderGrid" ItemsSource="{Binding Placeholders}" SelectedItem="{Binding SelectedPlaceholder}" <DataGrid x:Name="PlaceholderGrid" ItemsSource="{Binding Placeholders}" SelectedItem="{Binding SelectedPlaceholder}"
AutoGenerateColumns="False" Height="210" CanUserResizeColumns="True"> AutoGenerateColumns="False" Height="180" CanUserResizeColumns="True" IsReadOnly="True">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*"/> <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="Typ" Binding="{Binding Type}" Width="105" IsReadOnly="True"/> <DataGridTextColumn Header="Typ" Binding="{Binding Type}" Width="105"/>
<DataGridCheckBoxColumn Header="Pflicht" Binding="{Binding Required}" Width="58"/> <DataGridCheckBoxColumn Header="Pflicht" Binding="{Binding Required}" Width="58"/>
<DataGridTextColumn Header="Beispiel" Binding="{Binding Sample}" Width="*"/> <DataGridTextColumn Header="Beispiel" Binding="{Binding Sample}" Width="*"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<Grid ColumnDefinitions="Auto,10,*"> <Border Padding="10" BorderBrush="#CBD5E1" BorderThickness="1" CornerRadius="4">
<Button Content="Markierten Platzhalter entfernen" Click="OnRemovePlaceholder"/> <StackPanel Spacing="8">
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center"> <TextBlock Text="Ausgewählten Platzhalter bearbeiten" FontWeight="SemiBold"/>
<TextBlock Text="Typ:" VerticalAlignment="Center"/> <Grid ColumnDefinitions="*,10,*">
<ComboBox Width="130" ItemsSource="{Binding PlaceholderTypes}" SelectedItem="{Binding SelectedPlaceholder.Type}"/> <StackPanel><TextBlock Text="Name" Classes="label"/>
</StackPanel> <TextBox Text="{Binding SelectedPlaceholder.Name, Mode=TwoWay}" PlaceholderText="z. B. Empfaenger"/></StackPanel>
<StackPanel Grid.Column="2"><TextBlock Text="Datentyp" Classes="label"/>
<ComboBox ItemsSource="{Binding PlaceholderTypes}"
SelectedItem="{Binding SelectedPlaceholder.Type, Mode=TwoWay}"/></StackPanel>
</Grid> </Grid>
<CheckBox Content="Pflichtwert" IsChecked="{Binding SelectedPlaceholder.Required, Mode=TwoWay}"/>
<StackPanel><TextBlock Text="Beispielwert für die Vorschau" Classes="label"/>
<TextBox Text="{Binding SelectedPlaceholder.Sample, Mode=TwoWay}" AcceptsReturn="True"
MinHeight="58" TextWrapping="Wrap" PlaceholderText="Beispieldaten eingeben"/></StackPanel>
<Button Content="Ausgewählten Platzhalter entfernen" HorizontalAlignment="Left"
Click="OnRemovePlaceholder"/>
</StackPanel>
</Border>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Paket-Assets" Classes="section"/> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Paket-Assets" Classes="section"/>
<Button Grid.Column="1" Content=" Bild importieren …" Click="OnImportAsset"/></Grid> <Button Grid.Column="1" Content=" Bild importieren …" Click="OnImportAsset"/></Grid>
@@ -27,8 +27,7 @@ public partial class MainWindow : Window
_viewModel.Reset(); _currentPackagePath = null; UpdateDocumentTitle(); _viewModel.Reset(); _currentPackagePath = null; UpdateDocumentTitle();
} }
private void OnExit(object? sender, RoutedEventArgs e) => Close(); private void OnExit(object? sender, RoutedEventArgs e) => Close();
private void OnAddPlaceholder(object? sender, RoutedEventArgs e) => private void OnAddPlaceholder(object? sender, RoutedEventArgs e) => _viewModel.AddPlaceholder();
_viewModel.Placeholders.Add(new($"Feld{_viewModel.Placeholders.Count + 1}", PlaceholderType.Text, false, "Beispiel"));
private void OnAddMetadata(object? sender, RoutedEventArgs e) private void OnAddMetadata(object? sender, RoutedEventArgs e)
{ {
var item = new DesignerMetadata($"custom-{_viewModel.MetadataItems.Count + 1}", ""); var item = new DesignerMetadata($"custom-{_viewModel.MetadataItems.Count + 1}", "");
@@ -39,11 +38,7 @@ public partial class MainWindow : Window
if (_viewModel.SelectedMetadata is not { } selected) return; if (_viewModel.SelectedMetadata is not { } selected) return;
_viewModel.MetadataItems.Remove(selected); _viewModel.SelectedMetadata = null; _viewModel.CanExport = false; _viewModel.MetadataItems.Remove(selected); _viewModel.SelectedMetadata = null; _viewModel.CanExport = false;
} }
private void OnRemovePlaceholder(object? sender, RoutedEventArgs e) private void OnRemovePlaceholder(object? sender, RoutedEventArgs e) => _viewModel.RemoveSelectedPlaceholder();
{
if (this.FindControl<DataGrid>("PlaceholderGrid")?.SelectedItem is DesignerPlaceholder selected)
_viewModel.Placeholders.Remove(selected);
}
private void OnAddElement(object? sender, RoutedEventArgs e) private void OnAddElement(object? sender, RoutedEventArgs e)
{ try { _viewModel.AddElement(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } } { try { _viewModel.AddElement(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
private void OnMeasureOverlayMode(object? sender, RoutedEventArgs e) private void OnMeasureOverlayMode(object? sender, RoutedEventArgs e)
Binary file not shown.