From 69304d385180b192d16cdd423b2ff28aa87bd052 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 9 Oct 2026 21:54:10 -0400 Subject: [PATCH] feat(settings): set nest server URL from Options --- OpenNest.Data/NestStorageSettings.cs | 12 +++++++ OpenNest.Tests/Data/NestStorageTests.cs | 25 ++++++++++++++ OpenNest/Forms/MainForm.cs | 6 ++++ OpenNest/Forms/OptionsForm.Designer.cs | 45 +++++++++++++++++++++++-- OpenNest/Forms/OptionsForm.cs | 25 ++++++++++++++ docs/nest-storage.md | 4 +++ 6 files changed, 114 insertions(+), 3 deletions(-) diff --git a/OpenNest.Data/NestStorageSettings.cs b/OpenNest.Data/NestStorageSettings.cs index f092c5d..f15cf19 100644 --- a/OpenNest.Data/NestStorageSettings.cs +++ b/OpenNest.Data/NestStorageSettings.cs @@ -32,6 +32,18 @@ public sealed class NestStorageSettings public bool IsDatabaseMode => Mode == NestStorageMode.Database && !string.IsNullOrWhiteSpace(ServerUrl); + /// Validate and normalize a nest server base URL without changing storage mode. + public static bool TryNormalizeServerUrl(string? url, out string normalized) + { + normalized = NormalizeUrl(url); + if (Uri.TryCreate(normalized, UriKind.Absolute, out var uri) + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + return true; + + normalized = ""; + return false; + } + /// %APPDATA%\OpenNest\storage.json. public static string DefaultPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), diff --git a/OpenNest.Tests/Data/NestStorageTests.cs b/OpenNest.Tests/Data/NestStorageTests.cs index 1a41586..b7a33f9 100644 --- a/OpenNest.Tests/Data/NestStorageTests.cs +++ b/OpenNest.Tests/Data/NestStorageTests.cs @@ -59,6 +59,31 @@ public class NestStorageSettingsTests : IDisposable Assert.False(settings.IsDatabaseMode); } + [Fact] + public void TryNormalizeServerUrl_AcceptsHttpsWithoutChangingMode() + { + var settings = new NestStorageSettings { Mode = NestStorageMode.File }; + + Assert.True(NestStorageSettings.TryNormalizeServerUrl(" https://shop.example:8090/ ", out var url)); + settings.ServerUrl = url; + settings.Save(FilePath); + + var loaded = NestStorageSettings.Load(FilePath); + Assert.Equal("https://shop.example:8090", loaded.ServerUrl); + Assert.Equal(NestStorageMode.File, loaded.Mode); + Assert.False(loaded.IsDatabaseMode); + } + + [Theory] + [InlineData("")] + [InlineData("ftp://shop.example")] + [InlineData("not-a-url")] + public void TryNormalizeServerUrl_RejectsInvalidAddress(string input) + { + Assert.False(NestStorageSettings.TryNormalizeServerUrl(input, out var url)); + Assert.Equal("", url); + } + public void Dispose() { if (Directory.Exists(_directory)) diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 938e7fc..f7bf2da 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -646,6 +646,7 @@ namespace OpenNest.Forms var serverUrl = NestStorage.Settings.ServerUrl; databaseOpenInProgress = true; storageModeMenu.Enabled = false; + mnuToolsOptions.Enabled = false; var file = await repository.GetFileAsync(dlg.SelectedId); if (file == null) throw new FileNotFoundException("This nest no longer exists on the server."); @@ -663,7 +664,10 @@ namespace OpenNest.Forms { databaseOpenInProgress = false; if (!IsDisposed) + { storageModeMenu.Enabled = !databaseSaveInProgress; + mnuToolsOptions.Enabled = !databaseSaveInProgress; + } } } @@ -707,6 +711,7 @@ namespace OpenNest.Forms { databaseSaveInProgress = true; storageModeMenu.Enabled = false; + mnuToolsOptions.Enabled = false; EnableCheck(); try { @@ -729,6 +734,7 @@ namespace OpenNest.Forms if (!IsDisposed) { storageModeMenu.Enabled = !databaseOpenInProgress; + mnuToolsOptions.Enabled = !databaseOpenInProgress; EnableCheck(); } } diff --git a/OpenNest/Forms/OptionsForm.Designer.cs b/OpenNest/Forms/OptionsForm.Designer.cs index 3425726..47e5dd0 100644 --- a/OpenNest/Forms/OptionsForm.Designer.cs +++ b/OpenNest/Forms/OptionsForm.Designer.cs @@ -41,9 +41,13 @@ this.strategyGroupBox = new System.Windows.Forms.GroupBox(); this.colorSchemeLabel = new System.Windows.Forms.Label(); this.colorSchemeCombo = new System.Windows.Forms.ComboBox(); + this.serverGroupBox = new System.Windows.Forms.GroupBox(); + this.serverUrlBox = new System.Windows.Forms.TextBox(); + this.setServerButton = new System.Windows.Forms.Button(); ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit(); this.tableLayoutPanel1.SuspendLayout(); this.bottomPanel1.SuspendLayout(); + this.serverGroupBox.SuspendLayout(); this.SuspendLayout(); // // checkBox1 @@ -135,7 +139,7 @@ this.bottomPanel1.Controls.Add(this.cancelButton); this.bottomPanel1.Controls.Add(this.saveButton); this.bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom; - this.bottomPanel1.Location = new System.Drawing.Point(0, 368); + this.bottomPanel1.Location = new System.Drawing.Point(0, 443); this.bottomPanel1.Name = "bottomPanel1"; this.bottomPanel1.Size = new System.Drawing.Size(708, 50); this.bottomPanel1.TabIndex = 1; @@ -158,7 +162,7 @@ // strategyGroupBox // this.strategyGroupBox.Controls.Add(this.strategyGrid); - this.strategyGroupBox.Location = new System.Drawing.Point(12, 178); + this.strategyGroupBox.Location = new System.Drawing.Point(12, 253); this.strategyGroupBox.Name = "strategyGroupBox"; this.strategyGroupBox.Size = new System.Drawing.Size(684, 180); this.strategyGroupBox.TabIndex = 2; @@ -184,13 +188,43 @@ this.colorSchemeCombo.Size = new System.Drawing.Size(130, 24); this.colorSchemeCombo.TabIndex = 11; // + // serverGroupBox + // + this.serverGroupBox.Controls.Add(this.serverUrlBox); + this.serverGroupBox.Controls.Add(this.setServerButton); + this.serverGroupBox.Location = new System.Drawing.Point(12, 178); + this.serverGroupBox.Name = "serverGroupBox"; + this.serverGroupBox.Size = new System.Drawing.Size(684, 69); + this.serverGroupBox.TabIndex = 1; + this.serverGroupBox.TabStop = false; + this.serverGroupBox.Text = "Nest server URL (File / Database mode is selected under File > Storage Mode)"; + // + // serverUrlBox + // + this.serverUrlBox.Location = new System.Drawing.Point(12, 29); + this.serverUrlBox.Name = "serverUrlBox"; + this.serverUrlBox.PlaceholderText = "http://server:8090"; + this.serverUrlBox.Size = new System.Drawing.Size(485, 22); + this.serverUrlBox.TabIndex = 0; + // + // setServerButton + // + this.setServerButton.Location = new System.Drawing.Point(504, 26); + this.setServerButton.Name = "setServerButton"; + this.setServerButton.Size = new System.Drawing.Size(168, 28); + this.setServerButton.TabIndex = 1; + this.setServerButton.Text = "Set / Change Server"; + this.setServerButton.UseVisualStyleBackColor = true; + this.setServerButton.Click += new System.EventHandler(this.SetServer_Click); + // // OptionsForm // this.AcceptButton = this.saveButton; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; this.CancelButton = this.cancelButton; - this.ClientSize = new System.Drawing.Size(708, 418); + this.ClientSize = new System.Drawing.Size(708, 493); this.Controls.Add(this.strategyGroupBox); + this.Controls.Add(this.serverGroupBox); this.Controls.Add(this.tableLayoutPanel1); this.Controls.Add(this.bottomPanel1); this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); @@ -206,6 +240,8 @@ this.tableLayoutPanel1.ResumeLayout(false); this.tableLayoutPanel1.PerformLayout(); this.bottomPanel1.ResumeLayout(false); + this.serverGroupBox.ResumeLayout(false); + this.serverGroupBox.PerformLayout(); this.ResumeLayout(false); } @@ -224,5 +260,8 @@ private System.Windows.Forms.GroupBox strategyGroupBox; private System.Windows.Forms.Label colorSchemeLabel; private System.Windows.Forms.ComboBox colorSchemeCombo; + private System.Windows.Forms.GroupBox serverGroupBox; + private System.Windows.Forms.TextBox serverUrlBox; + private System.Windows.Forms.Button setServerButton; } } \ No newline at end of file diff --git a/OpenNest/Forms/OptionsForm.cs b/OpenNest/Forms/OptionsForm.cs index 1ea4579..95fec44 100644 --- a/OpenNest/Forms/OptionsForm.cs +++ b/OpenNest/Forms/OptionsForm.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; +using OpenNest.Data; using OpenNest.Engine.Strategies; using OpenNest.Properties; @@ -72,6 +73,7 @@ namespace OpenNest.Forms private void LoadSettings() { + serverUrlBox.Text = NestStorage.Settings.ServerUrl; checkBox1.Checked = Settings.Default.CreateNewNestOnOpen; numericUpDown1.Value = (decimal)Settings.Default.AutoSizePlateFactor; @@ -142,5 +144,28 @@ namespace OpenNest.Forms { SaveSettings(); } + + private void SetServer_Click(object sender, EventArgs e) + { + if (!NestStorageSettings.TryNormalizeServerUrl(serverUrlBox.Text, out var url)) + { + MessageBox.Show(this, "Enter a valid http or https server URL.", + "Server URL", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + try + { + NestStorage.Save(NestStorage.Settings.Mode, url); + serverUrlBox.Text = NestStorage.Settings.ServerUrl; + MessageBox.Show(this, "Server URL saved. Storage mode has not changed.", + "Server URL", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show(this, $"Could not save the server URL: {ex.Message}", + "Server URL", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } } } diff --git a/docs/nest-storage.md b/docs/nest-storage.md index e30b588..e831cb9 100644 --- a/docs/nest-storage.md +++ b/docs/nest-storage.md @@ -241,6 +241,10 @@ curl --fail http://:/healthz # {"status":"ok"} In the desktop app choose **File > Storage Mode...**, Database, and enter the base URL `http://:`, without `/healthz` or `/api/nests`. +To set or change just the saved server address without changing File/Database mode, +use **Tools > Options > Nest server URL > Set / Change Server**. This saves the URL +immediately, independently of the Options Save button; it does not migrate nests +or enable Database mode. Canceling Options afterward does not undo that address. ### Backup, restore, and upgrade