From f6781d27c3a651b6df8b2f1c5a8ed05ef281ff16 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 21 Oct 2026 09:41:32 -0400 Subject: [PATCH] feat(desktop): release several sets, merge into drafts, and undo releases The release dialog now lists the sets to release instead of one set with a multiplier. More are added by typing a name (the matching active sets fill the list as you type) and each set has its own kit count. Its groups span all the sets, and each group can make a new Draft nest or merge into a Draft nest of the same material, grade and thickness, listed with its sheet count. The status column says that a merge discards the draft's layout and that Undo Release restores it. A draft open in a tab of this desktop is refused, because nest tabs do not track unsaved edits and the merge would leave them behind. The main window tells the dialog which server nests are open. The release history shows what each release did to each nest (created, or merged from one revision to another, and what undo did) and gains Undo Release. When nests changed after the release, it names them and offers to undo the others, leaving those as they are. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Data/SetReleaseText.cs | 19 + OpenNest.Tests/Data/InMemoryDrawingSets.cs | 15 +- OpenNest.Tests/Data/SetReleaseTextTests.cs | 19 + .../Forms/SetReleaseFormsTests.cs | 205 +++++++- OpenNest/Controls/DrawingSetBrowser.cs | 12 +- OpenNest/Forms/DrawingLibraryForm.cs | 9 +- OpenNest/Forms/EditNestForm.cs | 2 +- OpenNest/Forms/MainForm.File.cs | 4 + OpenNest/Forms/ReleaseSetForm.Designer.cs | 237 +++++++--- OpenNest/Forms/ReleaseSetForm.cs | 438 ++++++++++++++++-- OpenNest/Forms/SetReleasesForm.Designer.cs | 34 +- OpenNest/Forms/SetReleasesForm.cs | 82 +++- docs/nest-storage.md | 60 ++- 13 files changed, 985 insertions(+), 151 deletions(-) diff --git a/OpenNest.Data/SetReleaseText.cs b/OpenNest.Data/SetReleaseText.cs index d95e4ccd..77232025 100644 --- a/OpenNest.Data/SetReleaseText.cs +++ b/OpenNest.Data/SetReleaseText.cs @@ -75,6 +75,25 @@ public static class SetReleaseText + "\n\nUndo the rest and leave these as they are, or restore them from their history."; } + /// + /// What the release did to one of its nests, and what undoing it did: "Created", + /// "Merged (revision 3 → 4)", "Created; undo deleted it". + /// + public static string Change(SetReleaseNest nest) + { + ArgumentNullException.ThrowIfNull(nest); + var text = nest.Kind == SetReleaseNestKind.Merged + ? $"Merged (revision {nest.RevisionBefore} → {nest.RevisionAfter})" + : "Created"; + return nest.Undo switch + { + SetReleaseNestUndo.Deleted => text + "; undo deleted it", + SetReleaseNestUndo.Restored => text + $"; undo restored revision {nest.RevisionBefore}", + SetReleaseNestUndo.Skipped => text + "; undo left it as it was", + _ => text, + }; + } + /// The state of one nest of a release: its status name, or "Deleted". public static string NestState(SetReleaseNest nest) { diff --git a/OpenNest.Tests/Data/InMemoryDrawingSets.cs b/OpenNest.Tests/Data/InMemoryDrawingSets.cs index fa3e8097..36cfb19e 100644 --- a/OpenNest.Tests/Data/InMemoryDrawingSets.cs +++ b/OpenNest.Tests/Data/InMemoryDrawingSets.cs @@ -165,8 +165,19 @@ internal sealed class InMemoryDrawingSets : IDrawingSetStore }); } - public Task UndoReleaseAsync(Guid id, SetReleaseUndoRequest request, CancellationToken cancellationToken = default) => - throw new NotSupportedException("The in-memory store does not hold nests, so it cannot undo a release."); + /// Every undo request, in order. + public List<(Guid Id, SetReleaseUndoRequest Request)> UndoRequests { get; } = new(); + + /// Answers an undo; this store holds no nests, so a test decides what the server says. + public Func? UndoHandler { get; set; } + + public Task UndoReleaseAsync(Guid id, SetReleaseUndoRequest request, CancellationToken cancellationToken = default) + { + UndoRequests.Add((id, new SetReleaseUndoRequest { UndoneBy = request.UndoneBy, SkipNests = request.SkipNests.ToList() })); + return UndoHandler is { } handler + ? Task.FromResult(handler(id, request)) + : throw new NotSupportedException("The in-memory store does not hold nests, so it cannot undo a release."); + } private Task SetArchived(Guid id, int expectedRevision, bool archived) { diff --git a/OpenNest.Tests/Data/SetReleaseTextTests.cs b/OpenNest.Tests/Data/SetReleaseTextTests.cs index cc0bf4d4..25c4f5ba 100644 --- a/OpenNest.Tests/Data/SetReleaseTextTests.cs +++ b/OpenNest.Tests/Data/SetReleaseTextTests.cs @@ -53,6 +53,25 @@ public class SetReleaseTextTests Assert.Equal("Released from set 'Synthetic kit' revision 3 ×2 by Synthetic operator", text); } + [Theory] + [InlineData(SetReleaseNestKind.Created, null, "Created")] + [InlineData(SetReleaseNestKind.Created, SetReleaseNestUndo.Deleted, "Created; undo deleted it")] + [InlineData(SetReleaseNestKind.Merged, null, "Merged (revision 3 → 4)")] + [InlineData(SetReleaseNestKind.Merged, SetReleaseNestUndo.Restored, "Merged (revision 3 → 4); undo restored revision 3")] + [InlineData(SetReleaseNestKind.Merged, SetReleaseNestUndo.Skipped, "Merged (revision 3 → 4); undo left it as it was")] + public void Change_SaysWhatTheReleaseAndItsUndoDid(SetReleaseNestKind kind, SetReleaseNestUndo? undo, string expected) + { + var nest = new SetReleaseNest + { + Kind = kind, + RevisionBefore = kind == SetReleaseNestKind.Merged ? 3 : null, + RevisionAfter = kind == SetReleaseNestKind.Merged ? 4 : 1, + Undo = undo, + }; + + Assert.Equal(expected, SetReleaseText.Change(nest)); + } + [Fact] public void UndoRefusal_NamesEachChangedNest_OrSaysItWasUndoneBefore() { diff --git a/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs b/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs index 1c4e4650..bab6d707 100644 --- a/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs +++ b/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs @@ -43,13 +43,13 @@ public class SetReleaseFormsTests Assert.Equal(2, grid.Rows.Count); Assert.True(skipped.Visible); Assert.Contains("'SYN-OLD'", skipped.Text); - Field(form, "multiplierBox").Value = 3; + Field(form, "setGrid").Rows[0].Cells["kitsColumn"].Value = "3"; Assert.Equal(new object[] { 12, 9 }, grid.Rows.Cast().Select(r => r.Cells["partsColumn"].Value)); Field(form, "notesBox").Text = "Synthetic run"; Run(form.ReleaseAsync()); - Assert.Contains("Create 2 Draft nests from set 'Synthetic kit' ×3", Assert.Single(questions)); + Assert.Contains("Release set 'Synthetic kit' ×3 into 2 Draft nests", Assert.Single(questions)); Assert.Equal(2, nests.Uploaded.Count); Assert.Equal(2, opened.Count); Assert.All(opened, o => Assert.Equal(NestStatus.Draft, o.Nest.Status)); @@ -64,7 +64,8 @@ public class SetReleaseFormsTests Assert.StartsWith("Released: 2 nests", Field