diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a20b59..614823a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ broke. The heading is parsed by `scripts/release-notes.ps1`, so keep its shape; under it is ordinary Markdown, and the renderer handles paragraphs, lists, links, `code` and **bold**. +## 1.5.1 - 14 September 2026 + +### Imported containers lay out at the spacing you choose +**Preferences → Import** sets the **Horizontal spacing** between containers in a row and the +**Vertical spacing** between rows, from 0 to 500 px at 100% zoom. Both start at 32 px, the +gap an import has always used, and a change applies to the next file you import rather than +to containers already on the board. + ## 1.5.0 - 11 September 2026 ### The board you had open comes back diff --git a/Directory.Build.props b/Directory.Build.props index f5aa46b..674bd28 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change to this line rather than an edit in a pipeline variable group. --> - 1.5.0 + 1.5.1 latest enable enable diff --git a/docs/wimport.md b/docs/wimport.md index 8dedd90..0b4f0e2 100644 --- a/docs/wimport.md +++ b/docs/wimport.md @@ -86,6 +86,12 @@ Whiteboard measures each container, then packs them **left to right**. A themati starts a new row even if the current row is not full. Without a break, a row wraps when the next item would exceed about 2400 world units. +In Preferences → Import, **Horizontal spacing** sets the gap between side-by-side +containers, and **Vertical spacing** sets the gap below the tallest container when a new +row starts, whether from a break or automatic wrapping. Both default to **32 px** and range +from 0 to 500 px at 100% zoom. Changes apply to future imports, not containers already on +the board. + On drop, the group’s **top-left** is the pointer. On Open or File → Import, the group’s top-left is the top-left of the visible view. diff --git a/site/wimport.html b/site/wimport.html index 61a5a62..f8a84fb 100644 --- a/site/wimport.html +++ b/site/wimport.html @@ -95,6 +95,7 @@

Languages shipped today

Layout

Whiteboard measures each container, then packs them left to right. A thematic break starts a new row even if the current row is not full. Without a break, a row wraps when the next item would exceed about 2400 world units.

+

In Preferences → Import, set Horizontal spacing between containers and Vertical spacing between rows. Both default to 32 px and range from 0 to 500 px at 100% zoom. Changes apply to future imports only.

On drop, the group’s top-left is the pointer. On Open or toolbar Import, the group’s top-left is the top-left of the visible view. Do not put coordinates in the file. There is no pos:, no YAML front matter, and no HTML comment layout.

How the file is opened

diff --git a/src/SQLBI.Whiteboard.Core/Import/ImportLayout.cs b/src/SQLBI.Whiteboard.Core/Import/ImportLayout.cs index e049b9b..f080105 100644 --- a/src/SQLBI.Whiteboard.Core/Import/ImportLayout.cs +++ b/src/SQLBI.Whiteboard.Core/Import/ImportLayout.cs @@ -12,7 +12,9 @@ public static class ImportLayout public static IReadOnlyList Place( IReadOnlyList<(double Width, double Height, bool StartNewRow)> items, - PointD originTopLeft) + PointD originTopLeft, + double horizontalSpacing = Gap, + double verticalSpacing = Gap) { ArgumentNullException.ThrowIfNull(items); var placed = new RectD[items.Count]; @@ -30,13 +32,13 @@ public static IReadOnlyList Place( if (wrap) { x = originTopLeft.X; - y += rowHeight + Gap; + y += rowHeight + verticalSpacing; rowHeight = 0; rowOccupied = false; } placed[index] = new RectD(x, y, width, height); - x += width + Gap; + x += width + horizontalSpacing; rowHeight = Math.Max(rowHeight, height); rowOccupied = true; } diff --git a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs index 37ade84..f62c54a 100644 --- a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs +++ b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs @@ -150,6 +150,8 @@ public sealed class AppSettings public ExportSettings Export { get; set; } = new(); + public ImportSettings Import { get; set; } = new(); + /// /// Whether to say at startup that Windows reports nothing to draw with. The /// tablet list this reads is a list of digitizers rather than an answer @@ -180,7 +182,7 @@ public sealed class AppSettings public static class AppSettingsSerializer { - public const int CurrentVersion = 17; + public const int CurrentVersion = 18; /// /// The version that moved plain text to the end of the default snippet @@ -293,6 +295,7 @@ private static AppSettings Normalize(AppSettings? settings) settings.Laser = LaserSettings.Normalize(settings.Laser); settings.PenButtons = PenButtonSettings.Normalize(settings.PenButtons); settings.Export = ExportSettings.Normalize(settings.Export); + settings.Import = ImportSettings.Normalize(settings.Import); if (settings.Version < VersionWithPlainTextLast && TextLanguageIds.IsLegacyDefaultOrder(settings.SnippetFormatOrder)) { diff --git a/src/SQLBI.Whiteboard.Core/Settings/ImportSettings.cs b/src/SQLBI.Whiteboard.Core/Settings/ImportSettings.cs new file mode 100644 index 0000000..83b06fa --- /dev/null +++ b/src/SQLBI.Whiteboard.Core/Settings/ImportSettings.cs @@ -0,0 +1,26 @@ +using SQLBI.Whiteboard.Core.Import; + +namespace SQLBI.Whiteboard.Core.Settings; + +public sealed class ImportSettings +{ + public const double DefaultSpacing = ImportLayout.Gap; + public const double MinimumSpacing = 0; + public const double MaximumSpacing = 500; + + // Board pixels at 100% zoom, independent of the view used to import the file. + public double HorizontalSpacing { get; set; } = DefaultSpacing; + + public double VerticalSpacing { get; set; } = DefaultSpacing; + + public static ImportSettings Normalize(ImportSettings? settings) + { + var result = settings ?? new ImportSettings(); + result.HorizontalSpacing = Clamp(result.HorizontalSpacing); + result.VerticalSpacing = Clamp(result.VerticalSpacing); + return result; + } + + private static double Clamp(double value) => + double.IsFinite(value) ? Math.Clamp(value, MinimumSpacing, MaximumSpacing) : DefaultSpacing; +} diff --git a/src/SQLBI.Whiteboard/MainWindow.xaml.cs b/src/SQLBI.Whiteboard/MainWindow.xaml.cs index bdf1cba..d92752a 100644 --- a/src/SQLBI.Whiteboard/MainWindow.xaml.cs +++ b/src/SQLBI.Whiteboard/MainWindow.xaml.cs @@ -5402,7 +5402,11 @@ private void ApplyImport(ImportDocument imported, PointD originTopLeft, bool rec return; } - var rects = ImportLayout.Place(sizes, originTopLeft); + var rects = ImportLayout.Place( + sizes, + originTopLeft, + _settings.Import.HorizontalSpacing, + _settings.Import.VerticalSpacing); var objects = new List(decoded.Count); var assets = new List(); for (var index = 0; index < decoded.Count; index++) diff --git a/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs b/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs index 049582f..a82e8e0 100644 --- a/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs +++ b/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs @@ -265,9 +265,9 @@ private Border CreateRow(SettingDescriptor setting, string? query) var value = new TextBlock { Style = (Style)FindResource("SettingsValueLabel"), - Text = FormatSeconds(slider.Value), + Text = FormatRangeValue(setting, slider.Value), }; - slider.ValueChanged += (_, _) => value.Text = FormatSeconds(slider.Value); + slider.ValueChanged += (_, _) => value.Text = FormatRangeValue(setting, slider.Value); Grid.SetColumn(value, 1); Grid.SetRow(slider, 1); Grid.SetColumnSpan(slider, 2); @@ -908,20 +908,26 @@ private ToggleButton CreateSwitch(SettingDescriptor setting) private Slider CreateSlider(SettingDescriptor setting) { - var value = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds - ? _settings.Laser.FadeSeconds - : _settings.Laser.HoldSeconds; + var value = setting.Id switch + { + SettingsCatalog.Ids.LaserFadeSeconds => _settings.Laser.FadeSeconds, + SettingsCatalog.Ids.LaserHoldSeconds => _settings.Laser.HoldSeconds, + SettingsCatalog.Ids.ImportHorizontalSpacing => _settings.Import.HorizontalSpacing, + SettingsCatalog.Ids.ImportVerticalSpacing => _settings.Import.VerticalSpacing, + _ => throw new ArgumentException("Unknown range setting.", nameof(setting)), + }; var slider = new Slider { Style = (Style)FindResource("SettingsSlider"), Minimum = setting.Minimum, Maximum = setting.Maximum, - SmallChange = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds ? 0.05 : 0.25, - LargeChange = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds ? 0.5 : 1, - TickFrequency = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds ? 0.05 : 0.25, + SmallChange = setting.SmallChange, + LargeChange = setting.LargeChange, + TickFrequency = setting.SmallChange, IsSnapToTickEnabled = true, Value = value, }; + AutomationProperties.SetName(slider, setting.Title); slider.ValueChanged += (_, _) => SetRange(setting, slider.Value); return slider; } @@ -1109,16 +1115,26 @@ private void SetRange(SettingDescriptor setting, double value) return; } - if (setting.Id == SettingsCatalog.Ids.LaserFadeSeconds) - { - _settings.Laser.FadeSeconds = value; - } - else + switch (setting.Id) { - _settings.Laser.HoldSeconds = value; + case SettingsCatalog.Ids.LaserFadeSeconds: + _settings.Laser.FadeSeconds = value; + break; + case SettingsCatalog.Ids.LaserHoldSeconds: + _settings.Laser.HoldSeconds = value; + break; + case SettingsCatalog.Ids.ImportHorizontalSpacing: + _settings.Import.HorizontalSpacing = value; + break; + case SettingsCatalog.Ids.ImportVerticalSpacing: + _settings.Import.VerticalSpacing = value; + break; + default: + return; } _settings.Laser = LaserSettings.Normalize(_settings.Laser); + _settings.Import = ImportSettings.Normalize(_settings.Import); NotifyApplied(); } @@ -1195,11 +1211,11 @@ private static int DistinctCategoryCount(IReadOnlyList settin return seen.Count; } - private static string FormatSeconds(double value) + private static string FormatRangeValue(SettingDescriptor setting, double value) { var rounded = Math.Round(value, 2, MidpointRounding.AwayFromZero); var text = rounded.ToString(rounded == Math.Truncate(rounded) ? "0" : "0.##", CultureInfo.CurrentCulture); - return $"{text} s"; + return $"{text} {setting.Unit}"; } private sealed class MonitorChoiceItem diff --git a/src/SQLBI.Whiteboard/SettingsCatalog.cs b/src/SQLBI.Whiteboard/SettingsCatalog.cs index 6847273..fb04137 100644 --- a/src/SQLBI.Whiteboard/SettingsCatalog.cs +++ b/src/SQLBI.Whiteboard/SettingsCatalog.cs @@ -87,6 +87,12 @@ internal sealed class SettingDescriptor public double Maximum { get; init; } + public double SmallChange { get; init; } = 1; + + public double LargeChange { get; init; } = 10; + + public string Unit { get; init; } = string.Empty; + public bool HideInStore { get; init; } } @@ -96,6 +102,8 @@ public static class Ids { public const string StartupMonitor = "startup.monitor"; public const string StartFullScreen = "startup.fullscreen"; + public const string ImportHorizontalSpacing = "import.horizontalSpacing"; + public const string ImportVerticalSpacing = "import.verticalSpacing"; public const string LaserHoldSeconds = "laser.holdSeconds"; public const string LaserFadeSeconds = "laser.fadeSeconds"; public const string LaserHoldMode = "laser.holdMode"; @@ -127,12 +135,13 @@ public static class EraserButton public const string Startup = "Startup"; public const string Input = "Input"; + public const string Import = "Import"; public const string Laser = "Laser pointer"; public const string Toolbar = "Toolbar"; public const string Updates = "Updates"; public static IReadOnlyList Categories { get; } = - [Startup, Input, Laser, Toolbar, Updates]; + [Startup, Input, Import, Laser, Toolbar, Updates]; public static IReadOnlyList All { get; } = [ @@ -244,6 +253,32 @@ public static class EraserButton Editor = SettingEditorKind.OrderedList, }, new() + { + Id = Ids.ImportHorizontalSpacing, + Category = Import, + Title = "Horizontal spacing", + Summary = "Space between imported containers in a row", + Description = "Pixels at 100% zoom. Applies to the next .wimport file, without rearranging existing containers. The default is 32 px.", + Keywords = ["import", "wimport", "horizontal", "spacing", "gap", "pixels", "layout"], + Editor = SettingEditorKind.DoubleRange, + Minimum = ImportSettings.MinimumSpacing, + Maximum = ImportSettings.MaximumSpacing, + Unit = "px", + }, + new() + { + Id = Ids.ImportVerticalSpacing, + Category = Import, + Title = "Vertical spacing", + Summary = "Space between imported rows", + Description = "Pixels at 100% zoom, measured below the tallest item in the previous row. Applies to explicit line breaks and automatic wrapping in the next .wimport file. The default is 32 px.", + Keywords = ["import", "wimport", "vertical", "spacing", "gap", "pixels", "row", "line", "layout"], + Editor = SettingEditorKind.DoubleRange, + Minimum = ImportSettings.MinimumSpacing, + Maximum = ImportSettings.MaximumSpacing, + Unit = "px", + }, + new() { Id = Ids.LaserHoldSeconds, Category = Laser, @@ -253,6 +288,9 @@ public static class EraserButton Editor = SettingEditorKind.DoubleRange, Minimum = LaserSettings.MinimumHoldSeconds, Maximum = LaserSettings.MaximumHoldSeconds, + SmallChange = 0.25, + LargeChange = 1, + Unit = "s", }, new() { @@ -264,6 +302,9 @@ public static class EraserButton Editor = SettingEditorKind.DoubleRange, Minimum = LaserSettings.MinimumFadeSeconds, Maximum = LaserSettings.MaximumFadeSeconds, + SmallChange = 0.05, + LargeChange = 0.5, + Unit = "s", }, new() { diff --git a/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs b/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs index 965d5eb..44a889c 100644 --- a/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs +++ b/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs @@ -331,6 +331,42 @@ kqlFromFence.Items is placed[2].X == 10 && placed[2].Y > placed[0].Bottom && placed[4].Y > placed[3].Y, "Flow layout should pack left to right, honor a forced row, and wrap on max width."); +var spacedImport = ImportLayout.Place( + [(400, 200, false), (400, 300, false), (400, 200, true), (2400, 100, false), (500, 100, false)], + new PointD(10, 20), + horizontalSpacing: 80, + verticalSpacing: 120); +Assert( + spacedImport.SequenceEqual( + [ + new RectD(10, 20, 400, 200), + new RectD(490, 20, 400, 300), + new RectD(10, 440, 400, 200), + new RectD(10, 760, 2400, 100), + new RectD(10, 980, 500, 100), + ]), + "Custom spacing should separate columns horizontally and forced or wrapped rows vertically below the tallest item."); +var touchingImport = ImportLayout.Place( + [(1200, 200, false), (1200, 300, false), (400, 100, false), (200, 100, true)], + new PointD(10, 20), + horizontalSpacing: 0, + verticalSpacing: 0); +Assert( + touchingImport.SequenceEqual( + [ + new RectD(10, 20, 1200, 200), + new RectD(1210, 20, 1200, 300), + new RectD(10, 320, 400, 100), + new RectD(10, 420, 200, 100), + ]), + "Zero spacing should allow touching edges and an exactly full row without overlapping containers."); +Assert( + ImportLayout.Place( + [(1200, 200, false), (1200, 300, false)], + new PointD(10, 20), + horizontalSpacing: 1, + verticalSpacing: 7)[1] == new RectD(10, 227, 1200, 300), + "The configured horizontal gap must count toward the wrap threshold."); Assert( ImportLayout.ImageSize(1800, 1400) is { Width: 900, Height: 700 }, "Imported images should use the same 900 by 700 cap as a dropped image."); @@ -787,6 +823,28 @@ await BoardArchive.SaveAsync( "Unknown pen colors and sizes should snap to the pen default."); var defaultSettings = AppSettingsSerializer.Parse(string.Empty); +Assert( + defaultSettings.Import is { HorizontalSpacing: 32, VerticalSpacing: 32 } && + AppSettingsSerializer.Parse("{ \"version\": 17 }").Import is + { HorizontalSpacing: 32, VerticalSpacing: 32 } && + AppSettingsSerializer.Parse("{ \"import\": null }").Import is + { HorizontalSpacing: 32, VerticalSpacing: 32 }, + "New, older, and null import settings should preserve the existing 32 px gaps."); +Assert( + AppSettingsSerializer.Parse("{ \"import\": { \"horizontalSpacing\": 0 } }").Import is + { HorizontalSpacing: 0, VerticalSpacing: 32 }, + "A missing direction should default independently without replacing a saved zero gap."); +Assert( + AppSettingsSerializer.Parse( + "{ \"import\": { \"horizontalSpacing\": -10, \"verticalSpacing\": 10000 } }").Import is + { HorizontalSpacing: ImportSettings.MinimumSpacing, VerticalSpacing: ImportSettings.MaximumSpacing }, + "Import spacing should clamp invalid saved values to the slider range."); +Assert( + AppSettingsSerializer.Parse(AppSettingsSerializer.Format(new AppSettings + { + Import = new ImportSettings { HorizontalSpacing = double.NaN, VerticalSpacing = double.PositiveInfinity }, + })).Import is { HorizontalSpacing: 32, VerticalSpacing: 32 }, + "Non-finite spacing should fall back to the defaults before saving JSON."); Assert( defaultSettings.ToolbarPlacement == ToolbarPlacement.TopRight, "Missing settings should default the toolbar to top-right."); @@ -845,11 +903,15 @@ await BoardArchive.SaveAsync( ToolbarPlacement = ToolbarPlacement.BottomCenter, CalligraphyAccess = CalligraphyAccess.SizeRow, Highlighter = new InkToolSettings { Argb = 0xFFF472B6, Thickness = 10 }, + Import = new ImportSettings { HorizontalSpacing = 80, VerticalSpacing = 120 }, }); Assert( formattedSettings.Contains("BottomCenter", StringComparison.Ordinal), "Settings JSON should persist the toolbar placement name."); var roundTripped = AppSettingsSerializer.Parse(formattedSettings); +Assert( + roundTripped.Import is { HorizontalSpacing: 80, VerticalSpacing: 120 }, + "Horizontal and vertical import spacing should persist independently."); Assert( roundTripped.ToolbarPlacement == ToolbarPlacement.BottomCenter && roundTripped.CalligraphyAccess == CalligraphyAccess.SizeRow && diff --git a/tests/SQLBI.Whiteboard.SmokeTests/PreferencesSmokeTests.cs b/tests/SQLBI.Whiteboard.SmokeTests/PreferencesSmokeTests.cs new file mode 100644 index 0000000..b13f61e --- /dev/null +++ b/tests/SQLBI.Whiteboard.SmokeTests/PreferencesSmokeTests.cs @@ -0,0 +1,143 @@ +using System.Windows; +using System.Windows.Automation; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using SQLBI.Whiteboard.Core.Settings; + +namespace SQLBI.Whiteboard.SmokeTests; + +internal static class PreferencesSmokeTests +{ + public static void Run() + { + Exception? failure = null; + var thread = new Thread(() => + { + try + { + CheckImportSliders(); + } + catch (Exception exception) + { + failure = exception; + } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + if (failure is not null) + { + throw new InvalidOperationException("Preferences smoke tests failed.", failure); + } + } + + private static void CheckImportSliders() + { + // Load the real styles without starting the application or touching the user's settings. + var app = new App(); + app.InitializeComponent(); + var settings = new AppSettings(); + var saved = string.Empty; + var changes = 0; + var window = new PreferencesWindow(settings, () => + { + saved = AppSettingsSerializer.Format(settings); + changes++; + }); + try + { + var horizontal = FindSlider(window, "Horizontal spacing"); + var vertical = FindSlider(window, "Vertical spacing"); + Assert( + horizontal.Value == 32 && vertical.Value == 32 && changes == 0, + "Opening Preferences should show the old gaps without applying any changes."); + Assert( + horizontal.Orientation == Orientation.Horizontal && vertical.Orientation == Orientation.Horizontal && + horizontal.Minimum == 0 && horizontal.Maximum == 500 && + vertical.Minimum == 0 && vertical.Maximum == 500 && + horizontal.TickFrequency == 1 && vertical.TickFrequency == 1 && + horizontal.IsSnapToTickEnabled && vertical.IsSnapToTickEnabled, + "Import should offer horizontal sliders with whole-pixel steps, including zero."); + + horizontal.Value = 80; + Assert( + settings.Import is { HorizontalSpacing: 80, VerticalSpacing: 32 } && ValueLabel(horizontal) == "80 px", + "The horizontal slider should change only its gap and display pixels."); + vertical.Value = 120; + Assert( + changes == 2 && ValueLabel(vertical) == "120 px" && + AppSettingsSerializer.Parse(saved).Import is { HorizontalSpacing: 80, VerticalSpacing: 120 }, + "Both slider changes should reach the persistence callback with their independent values."); + + var hold = FindSlider(window, "Trail duration"); + var fade = FindSlider(window, "Fade duration"); + Assert( + hold.Value == LaserSettings.DefaultHoldSeconds && hold.TickFrequency == 0.25 && + hold.SmallChange == 0.25 && hold.LargeChange == 1 && + fade.Value == LaserSettings.DefaultFadeSeconds && fade.TickFrequency == 0.05 && + fade.SmallChange == 0.05 && fade.LargeChange == 0.5, + "Adding pixel sliders must not change the laser timing controls."); + hold.Value = 3; + fade.Value = 1; + Assert( + settings.Laser is { HoldSeconds: 3, FadeSeconds: 1 } && + ValueLabel(hold) == "3 s" && ValueLabel(fade) == "1 s" && + settings.Import is { HorizontalSpacing: 80, VerticalSpacing: 120 }, + "Laser sliders should still update seconds without changing import spacing."); + + var category = Descendants(window).OfType() + .Single(button => button.Content is "Import"); + category.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent)); + Assert( + Descendants(window).OfType().Count() == 2 && + FindSlider(window, "Horizontal spacing").Value == 80 && + FindSlider(window, "Vertical spacing").Value == 120 && changes == 4, + "The Import category should show only its sliders and retain values when rebuilt."); + + var reopened = new PreferencesWindow(AppSettingsSerializer.Parse(saved), () => + throw new InvalidOperationException("Reopening Preferences must not apply a change.")); + try + { + Assert( + FindSlider(reopened, "Horizontal spacing").Value == 80 && + FindSlider(reopened, "Vertical spacing").Value == 120, + "Saved spacing should be restored into the actual controls."); + } + finally + { + reopened.Close(); + } + } + finally + { + window.Close(); + app.Shutdown(); + } + } + + private static Slider FindSlider(DependencyObject root, string name) => + Descendants(root).OfType().Single(slider => AutomationProperties.GetName(slider) == name); + + private static string ValueLabel(Slider slider) => + ((Grid)slider.Parent).Children.OfType().Single().Text; + + private static IEnumerable Descendants(DependencyObject root) + { + foreach (var child in LogicalTreeHelper.GetChildren(root).OfType()) + { + yield return child; + foreach (var descendant in Descendants(child)) + { + yield return descendant; + } + } + } + + private static void Assert(bool condition, string message) + { + if (!condition) + { + throw new InvalidOperationException(message); + } + } +} diff --git a/tests/SQLBI.Whiteboard.SmokeTests/Program.cs b/tests/SQLBI.Whiteboard.SmokeTests/Program.cs index 2e1e1a0..efa9c5e 100644 --- a/tests/SQLBI.Whiteboard.SmokeTests/Program.cs +++ b/tests/SQLBI.Whiteboard.SmokeTests/Program.cs @@ -577,6 +577,8 @@ public function greet(string $name): string } } +SQLBI.Whiteboard.SmokeTests.PreferencesSmokeTests.Run(); + Console.WriteLine("SQLBI.Whiteboard smoke tests passed."); static string ShortPathOf(string path)