Phase 1: versioning, menu bar, About dialog, right-click context menu

- Directory.Build.props sets Version=0.1.0 (semver pre-1.0 = beta) plus
  Authors / Product / RepositoryUrl, picked up by all three projects.
- MainWindow gets a real menu bar (File / Server / Help) replacing the
  old toolbar. File: New endpoint / Import / Export / Backups (last
  three are stubs for the next phase) / Exit. Server: Settings /
  Restart service. Help: About.
- Drop the Refresh button - the 3 s polling loop covers it.
- DataGridRow gets a right-click context menu: Edit / Copy URL /
  toggle Enabled / Delete.
- New About dialog reads AssemblyInformationalVersion at runtime and
  links jpaul.me + the GitHub repo via clickable hyperlinks.
- Ctrl+N input binding for new-endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 09:39:18 -04:00
parent 28479272d5
commit a45d994c18
5 changed files with 155 additions and 15 deletions
+36 -15
View File
@@ -8,6 +8,9 @@
mc:Ignorable="d"
Title="Webhook Server" Height="600" Width="1000"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}">
<Window.InputBindings>
<KeyBinding Key="N" Modifiers="Control" Command="{Binding AddEndpointCommand}"/>
</Window.InputBindings>
<DockPanel LastChildFill="True">
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
@@ -19,21 +22,25 @@
</StatusBarItem>
</StatusBar>
<ToolBar DockPanel.Dock="Top">
<Button Content="Refresh" Command="{Binding RefreshCommand}"/>
<Separator/>
<Button Content="Add" Command="{Binding AddEndpointCommand}"/>
<Button Content="Edit" Command="{Binding EditEndpointCommand}"
IsEnabled="{Binding SelectedEndpoint, Converter={StaticResource NotNull}}"/>
<Button Content="Delete" Command="{Binding DeleteEndpointCommand}"
IsEnabled="{Binding SelectedEndpoint, Converter={StaticResource NotNull}}"/>
<Separator/>
<Button Content="Copy URL" Command="{Binding CopyEndpointUrlCommand}"
IsEnabled="{Binding SelectedEndpoint, Converter={StaticResource NotNull}}"
ToolTip="Copy the full webhook URL for the selected endpoint to the clipboard"/>
<Separator/>
<Button Content="Server Settings…" Command="{Binding EditServerSettingsCommand}"/>
</ToolBar>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New endpoint…" Command="{Binding AddEndpointCommand}" InputGestureText="Ctrl+N"/>
<Separator/>
<MenuItem Header="_Import config…" IsEnabled="False" ToolTip="Coming soon"/>
<MenuItem Header="_Export config…" IsEnabled="False" ToolTip="Coming soon"/>
<MenuItem Header="_Backups" IsEnabled="False" ToolTip="Coming soon"/>
<Separator/>
<MenuItem Header="E_xit" Command="{Binding ExitCommand}"/>
</MenuItem>
<MenuItem Header="_Server">
<MenuItem Header="_Settings…" Command="{Binding EditServerSettingsCommand}"/>
<Separator/>
<MenuItem Header="_Restart service" Command="{Binding RestartServiceCommand}"/>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About Webhook Server…" Command="{Binding ShowAboutCommand}"/>
</MenuItem>
</Menu>
<Grid>
<Grid.RowDefinitions>
@@ -53,6 +60,20 @@
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick" Handler="OnRowDoubleClick"/>
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="_Edit…" Command="{Binding DataContext.EditEndpointCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="_Copy URL" Command="{Binding DataContext.CopyEndpointUrlCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Separator/>
<MenuItem Header="Toggle _enabled"
Command="{Binding DataContext.ToggleEnabledCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"/>
<Separator/>
<MenuItem Header="_Delete…" Command="{Binding DataContext.DeleteEndpointCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
@@ -175,6 +175,41 @@ public sealed partial class MainViewModel : ObservableObject
}
}
[RelayCommand]
private async Task RestartServiceAsync()
{
var ok = MessageBox.Show(
"Restart the WebhookServer service? In-flight requests will be aborted.",
"Restart service",
MessageBoxButton.OKCancel,
MessageBoxImage.Warning);
if (ok != MessageBoxResult.OK) return;
try
{
await _client.RestartListenerAsync().ConfigureAwait(false);
await Task.Delay(2000).ConfigureAwait(false);
await RefreshAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
ShowError("Restart failed", ex);
}
}
[RelayCommand]
private void ShowAbout()
{
var dlg = new Views.AboutDialog { Owner = Application.Current.MainWindow };
dlg.ShowDialog();
}
[RelayCommand]
private void Exit()
{
Application.Current.Shutdown();
}
[RelayCommand]
private void CopyEndpointUrl()
{
@@ -0,0 +1,42 @@
<Window x:Class="WebhookServer.Gui.Views.AboutDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="About Webhook Server"
Height="320" Width="420"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Webhook Server" FontSize="22" FontWeight="Bold"/>
<TextBlock Grid.Row="1" x:Name="VersionText" Foreground="Gray" Margin="0,4,0,16"/>
<TextBlock Grid.Row="2" TextWrapping="Wrap">
A Windows-native webhook server that runs PowerShell, cmd, or arbitrary
executables in response to incoming HTTP requests.
</TextBlock>
<StackPanel Grid.Row="3" Margin="0,16,0,0">
<TextBlock>
<Run Text="Created by"/>
<Run Text="Justin Paul" FontWeight="SemiBold"/>
</TextBlock>
<TextBlock Margin="0,4,0,0">
<Hyperlink NavigateUri="https://jpaul.me" RequestNavigate="OnHyperlink">https://jpaul.me</Hyperlink>
</TextBlock>
<TextBlock Margin="0,4,0,0">
<Hyperlink NavigateUri="https://github.com/recklessop/webhook-server" RequestNavigate="OnHyperlink">github.com/recklessop/webhook-server</Hyperlink>
</TextBlock>
</StackPanel>
<Button Grid.Row="5" Content="OK" Width="80" HorizontalAlignment="Right" IsDefault="True" IsCancel="True" Click="OnOk"/>
</Grid>
</Window>
@@ -0,0 +1,28 @@
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Navigation;
namespace WebhookServer.Gui.Views;
public partial class AboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
var asm = Assembly.GetExecutingAssembly();
var info = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? asm.GetName().Version?.ToString()
?? "0.0.0";
VersionText.Text = $"Version {info}";
}
private void OnHyperlink(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });
e.Handled = true;
}
private void OnOk(object sender, RoutedEventArgs e) => Close();
}