diff --git a/OpenNest.Data/DrawingLibraryEditSession.cs b/OpenNest.Data/DrawingLibraryEditSession.cs
index 07b66ba8..990a066f 100644
--- a/OpenNest.Data/DrawingLibraryEditSession.cs
+++ b/OpenNest.Data/DrawingLibraryEditSession.cs
@@ -1,10 +1,23 @@
namespace OpenNest.Data;
///
-/// Edits one listed library drawing's notes and nesting settings in place: the save is a
-/// metadata-only revision guarded by the revision it was listed with, so the package (and
-/// its hash) stays the same. A stale revision raises
-/// and changes nothing.
+/// A library drawing's editable details. Units are not among them: they give the stored
+/// geometry its meaning, so changing them would rescale the part.
+///
+public sealed record DrawingLibraryDetails(
+ string Name,
+ string Customer,
+ string MaterialName,
+ string MaterialGrade,
+ double Thickness,
+ CutType CutType);
+
+///
+/// Edits one listed library drawing's details (name, customer, material, grade, thickness,
+/// cut type), notes and nesting settings in place: the save is a metadata-only revision
+/// guarded by the revision it was listed with, so the package (and its hash) stays the same
+/// and nests keep the revision they hold. A stale revision raises
+/// and changes nothing.
///
public sealed class DrawingLibraryEditSession
{
@@ -26,42 +39,64 @@ public sealed class DrawingLibraryEditSession
public string SavedBy { get; }
- /// The record to save: the listed details with new notes and settings.
- public DrawingLibraryRecord Build(string notes, DrawingNestingSettings settings) => new()
+ /// The listed record's details, the starting point of an edit.
+ public DrawingLibraryDetails Details => new(Listed.Name, Listed.Customer, Listed.MaterialName,
+ Listed.MaterialGrade, Listed.Thickness, Listed.CutType);
+
+ ///
+ /// The record to save: the edited details (text trimmed, thickness rounded as stored),
+ /// the listed units, and the new notes and settings.
+ ///
+ public DrawingLibraryRecord Build(DrawingLibraryDetails details, string notes, DrawingNestingSettings settings)
{
- Id = Listed.Id,
- Name = Listed.Name,
- Customer = Listed.Customer,
- MaterialName = Listed.MaterialName,
- MaterialGrade = Listed.MaterialGrade,
- Thickness = Listed.Thickness,
- Units = Listed.Units,
- CutType = Listed.CutType,
- Notes = notes ?? "",
- Settings = settings ?? throw new ArgumentNullException(nameof(settings)),
- SavedBy = SavedBy,
- };
+ ArgumentNullException.ThrowIfNull(details);
+ return new DrawingLibraryRecord
+ {
+ Id = Listed.Id,
+ Name = (details.Name ?? "").Trim(),
+ Customer = (details.Customer ?? "").Trim(),
+ MaterialName = (details.MaterialName ?? "").Trim(),
+ MaterialGrade = (details.MaterialGrade ?? "").Trim(),
+ Thickness = double.IsFinite(details.Thickness)
+ ? DrawingLibraryRules.NormalizeThickness(details.Thickness)
+ : details.Thickness,
+ Units = Listed.Units,
+ CutType = details.CutType,
+ Notes = notes ?? "",
+ Settings = settings ?? throw new ArgumentNullException(nameof(settings)),
+ SavedBy = SavedBy,
+ };
+ }
/// The first rule the edit breaks, or null when it may be saved.
- public string? GetError(string notes, DrawingNestingSettings settings) =>
- DrawingLibraryRules.GetMetadataError(Build(notes, settings));
+ public string? GetError(DrawingLibraryDetails details, string notes, DrawingNestingSettings settings) =>
+ DrawingLibraryRules.GetMetadataError(Build(details, notes, settings));
/// True when saving would change nothing.
- public bool IsUnchanged(string notes, DrawingNestingSettings settings) =>
- (notes ?? "") == Listed.Notes && Equals(settings, Listed.Settings);
+ public bool IsUnchanged(DrawingLibraryDetails details, string notes, DrawingNestingSettings settings) =>
+ SameDetails(Build(details, notes, settings), Listed)
+ && (notes ?? "") == Listed.Notes && Equals(settings, Listed.Settings);
/// Saves the edit as the next revision; returns the stored record.
- public async Task SaveAsync(string notes, DrawingNestingSettings settings,
- CancellationToken cancellationToken = default)
+ public async Task SaveAsync(DrawingLibraryDetails details, string notes,
+ DrawingNestingSettings settings, CancellationToken cancellationToken = default)
{
- var record = Build(notes, settings);
+ var record = Build(details, notes, settings);
var error = DrawingLibraryRules.GetMetadataError(record);
if (error != null)
- throw new ArgumentException(error, nameof(settings));
+ throw new ArgumentException(error, nameof(details));
var saved = await _library.UpdateMetadataAsync(Listed.Id, record, Listed.Revision, cancellationToken);
if (!string.Equals(saved.PackageSha256, Listed.PackageSha256, StringComparison.OrdinalIgnoreCase)
+ || !SameDetails(saved, record) || saved.Units != record.Units
|| !Equals(saved.Settings, record.Settings) || saved.Notes != record.Notes)
throw new IOException("The nest server stored a different library drawing than was edited. Reload the library.");
return saved;
}
+
+ // Compares a built (trimmed, rounded) record with another, as stored.
+ private static bool SameDetails(DrawingLibraryRecord built, DrawingLibraryRecord other) =>
+ built.Name == other.Name.Trim() && built.Customer == other.Customer.Trim()
+ && built.MaterialName == other.MaterialName.Trim() && built.MaterialGrade == other.MaterialGrade.Trim()
+ && built.Thickness == DrawingLibraryRules.NormalizeThickness(other.Thickness)
+ && built.CutType == other.CutType;
}
diff --git a/OpenNest.Tests/Data/DrawingLibraryEditSessionTests.cs b/OpenNest.Tests/Data/DrawingLibraryEditSessionTests.cs
index 33ab7c92..20ca6b79 100644
--- a/OpenNest.Tests/Data/DrawingLibraryEditSessionTests.cs
+++ b/OpenNest.Tests/Data/DrawingLibraryEditSessionTests.cs
@@ -29,9 +29,9 @@ public class DrawingLibraryEditSessionTests
var session = new DrawingLibraryEditSession(store, listed, "Synthetic operator");
var settings = new DrawingNestingSettings { CutFirst = true };
- Assert.True(session.IsUnchanged("Calibration", new DrawingNestingSettings()));
- Assert.False(session.IsUnchanged("Calibration", settings));
- var saved = await session.SaveAsync("Calibration", settings);
+ Assert.True(session.IsUnchanged(session.Details, "Calibration", new DrawingNestingSettings()));
+ Assert.False(session.IsUnchanged(session.Details, "Calibration", settings));
+ var saved = await session.SaveAsync(session.Details, "Calibration", settings);
var (id, expected, record) = Assert.Single(store.Updates);
Assert.Equal((listed.Id, 4), (id, expected));
@@ -49,11 +49,68 @@ public class DrawingLibraryEditSessionTests
var session = new DrawingLibraryEditSession(store, Listed(), "");
var invalid = new DrawingNestingSettings { StepAngle = -1 };
- Assert.NotNull(session.GetError("", invalid));
- await Assert.ThrowsAsync(() => session.SaveAsync("", invalid));
+ Assert.NotNull(session.GetError(session.Details, "", invalid));
+ await Assert.ThrowsAsync(() => session.SaveAsync(session.Details, "", invalid));
Assert.Empty(store.Updates);
}
+ [Fact]
+ public async Task SaveAsync_SendsEditedDetailsTrimmedAsTheNextRevisionOfTheSamePackage()
+ {
+ var listed = Listed();
+ var store = new RecordingStore();
+ var session = new DrawingLibraryEditSession(store, listed, "Synthetic operator");
+ var details = new DrawingLibraryDetails(" Test square B ", "Other customer", " Synthetic Stainless ", "304 ",
+ 0.1875, CutType.Waterjet);
+
+ Assert.False(session.IsUnchanged(details, listed.Notes, listed.Settings));
+ var saved = await session.SaveAsync(details, listed.Notes, listed.Settings);
+
+ var (id, expected, record) = Assert.Single(store.Updates);
+ Assert.Equal((listed.Id, 4), (id, expected));
+ Assert.Equal(("Test square B", "Other customer", "Synthetic Stainless", "304", 0.1875, CutType.Waterjet),
+ (record.Name, record.Customer, record.MaterialName, record.MaterialGrade, record.Thickness, record.CutType));
+ // Units are never edited here: they give the stored geometry its meaning.
+ Assert.Equal(listed.Units, record.Units);
+ Assert.Equal(("Test square B", 0.1875), (saved.Name, saved.Thickness));
+ }
+
+ [Fact]
+ public void Details_StartFromTheListedRecordAndOnlyRealChangesCount()
+ {
+ var listed = Listed();
+ var session = new DrawingLibraryEditSession(new RecordingStore(), listed, "");
+
+ Assert.Equal(new DrawingLibraryDetails(listed.Name, listed.Customer, listed.MaterialName, listed.MaterialGrade,
+ listed.Thickness, listed.CutType), session.Details);
+ // Surrounding spaces and sub-rounding thickness noise are not edits.
+ Assert.True(session.IsUnchanged(session.Details with { Name = " Test square ", Thickness = 0.250001 },
+ listed.Notes, listed.Settings));
+ Assert.False(session.IsUnchanged(session.Details with { MaterialGrade = "Grade B" }, listed.Notes, listed.Settings));
+ }
+
+ [Fact]
+ public async Task SaveAsync_RefusesABlankNameWithoutSending()
+ {
+ var store = new RecordingStore();
+ var session = new DrawingLibraryEditSession(store, Listed(), "");
+ var blank = session.Details with { Name = " " };
+
+ Assert.Equal("name is required.", session.GetError(blank, "", new DrawingNestingSettings()));
+ await Assert.ThrowsAsync(() => session.SaveAsync(blank, "", new DrawingNestingSettings()));
+ Assert.Empty(store.Updates);
+ }
+
+ [Fact]
+ public async Task SaveAsync_RefusesAReplyWithOtherDetailsThanSent()
+ {
+ var store = new RecordingStore { DropDetails = true };
+ var session = new DrawingLibraryEditSession(store, Listed(), "");
+
+ await Assert.ThrowsAsync(() =>
+ session.SaveAsync(session.Details with { MaterialName = "Other" }, "", new DrawingNestingSettings()));
+ }
+
[Fact]
public void ArchivedDrawings_CannotBeEdited()
{
@@ -64,6 +121,9 @@ public class DrawingLibraryEditSessionTests
{
public List<(Guid Id, int Expected, DrawingLibraryRecord Record)> Updates { get; } = new();
+ /// Replies with the listed details instead of the sent ones.
+ public bool DropDetails { get; init; }
+
public Task UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
CancellationToken cancellationToken = default)
{
@@ -72,7 +132,13 @@ public class DrawingLibraryEditSessionTests
{
Id = id,
Revision = expectedRevision + 1,
- Name = record.Name,
+ Name = DropDetails ? "Test square" : record.Name,
+ Customer = DropDetails ? "Synthetic customer" : record.Customer,
+ MaterialName = DropDetails ? "Synthetic Steel" : record.MaterialName,
+ MaterialGrade = DropDetails ? "Grade A" : record.MaterialGrade,
+ Thickness = DropDetails ? 0.25 : record.Thickness,
+ Units = record.Units,
+ CutType = DropDetails ? CutType.Laser : record.CutType,
Notes = record.Notes,
Settings = record.Settings,
PackageSha256 = "ABC123",
diff --git a/OpenNest.WinForms.Tests/Forms/DrawingLibraryFormTests.cs b/OpenNest.WinForms.Tests/Forms/DrawingLibraryFormTests.cs
index e4ddec9f..f6e13d6b 100644
--- a/OpenNest.WinForms.Tests/Forms/DrawingLibraryFormTests.cs
+++ b/OpenNest.WinForms.Tests/Forms/DrawingLibraryFormTests.cs
@@ -267,6 +267,57 @@ public class DrawingLibraryFormTests
"The drawing library edit test did not complete.");
}
+ [Fact]
+ public void EditDrawing_SavesNewDetailsAsANewRevision_KeepingUnits()
+ {
+ StaTestThread.Run(
+ () =>
+ {
+ var library = new FakeLibrary();
+ var record = library.Add("SYN-405", "", "", 0, Units.Inches, "Test square");
+ var nest = MatchingNest();
+
+ using var form = new DrawingLibraryForm(library, Server, nest);
+ form.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
+ form.EditDrawing = edit =>
+ {
+ using var editor = new EditLibraryDrawingForm(edit);
+ editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
+ editor.Show();
+ var save = Field