mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-03 20:31:35 -05:00
feat(storage): record material grade on server nests and pick it in Nest Info
Nest records gain a materialGrade field (server schema v8, blank for existing rows) that is searched, sorted and versioned like the other metadata. The Nest Info dialog gets a grade picker listing the machine configs' grades for the chosen material; density survives only an unchanged name and grade. A blank recorded grade keeps the archive's grade while the name matches, so records saved before v8 do not lose it on open. Saved Nests shows a Grade column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
11b6032459
commit
7bf753ce17
21 files changed
+427
-61
No files matched your search
@@ -1,25 +1,48 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the nest material when the Nest Info dialog is accepted. The dialog
|
||||
/// edits only the material name, so grade and density must come from the
|
||||
/// material the nest already has.
|
||||
/// Chooses the nest material from a material name and grade, as edited in the
|
||||
/// Nest Info dialog or recorded on the nest server. Density is not edited, so it
|
||||
/// must come from the material the nest already has.
|
||||
/// </summary>
|
||||
public static class NestMaterialSelection
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a new material named <paramref name="selectedName"/>. When that
|
||||
/// name matches the current material's name by <see cref="SharedListNames.Key"/>,
|
||||
/// the current grade and density are kept; any other name gets a name-only
|
||||
/// material. The current material is never modified or returned.
|
||||
/// Returns a new material named <paramref name="selectedName"/> with grade
|
||||
/// <paramref name="selectedGrade"/>. When both match the current material by
|
||||
/// <see cref="SharedListNames.Key"/>, the current density is kept; any other
|
||||
/// name or grade gets no density. The current material is never modified or returned.
|
||||
/// </summary>
|
||||
public static Material Apply(Material? current, string? selectedName)
|
||||
public static Material Apply(Material? current, string? selectedName, string? selectedGrade)
|
||||
{
|
||||
var name = selectedName ?? "";
|
||||
var grade = selectedGrade ?? "";
|
||||
|
||||
if (current != null && SharedListNames.Key(current.Name) == SharedListNames.Key(name))
|
||||
return new Material(name, current.Grade, current.Density);
|
||||
if (
|
||||
current != null
|
||||
&& SharedListNames.Key(current.Name) == SharedListNames.Key(name)
|
||||
&& SharedListNames.Key(current.Grade) == SharedListNames.Key(grade)
|
||||
)
|
||||
return new Material(name, grade, current.Density);
|
||||
|
||||
return new Material(name);
|
||||
return new Material(name, grade);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies server metadata to an archived material. A blank recorded grade means
|
||||
/// the record predates grades, so the archive's grade stands while the name is
|
||||
/// unchanged; every edit rewrites the archive too, so a cleared grade is blank there as well.
|
||||
/// </summary>
|
||||
public static Material ApplyRecorded(Material? current, string? recordedName, string? recordedGrade)
|
||||
{
|
||||
var grade = recordedGrade ?? "";
|
||||
if (
|
||||
grade.Length == 0
|
||||
&& current != null
|
||||
&& SharedListNames.Key(current.Name) == SharedListNames.Key(recordedName)
|
||||
)
|
||||
grade = current.Grade ?? "";
|
||||
|
||||
return Apply(current, recordedName, grade);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public sealed class NestQuery
|
||||
public const int MaxSearchLength = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Case-insensitive substring matched against name, customer, material, made by,
|
||||
/// Case-insensitive substring matched against name, customer, material, grade, made by,
|
||||
/// comments and status (stored and display names). Blank means no filter.
|
||||
/// </summary>
|
||||
public string Search { get; init; } = "";
|
||||
|
||||
@@ -29,6 +29,9 @@ public sealed class NestRecord
|
||||
|
||||
public string Material { get; set; } = "";
|
||||
|
||||
/// <summary>Material grade; blank when none was chosen or the server predates grades.</summary>
|
||||
public string MaterialGrade { get; set; } = "";
|
||||
|
||||
public double Thickness { get; set; }
|
||||
|
||||
/// <summary>quote | toBeCut | hasBeenCut (camelCase on the wire, case-insensitive readers).</summary>
|
||||
|
||||
@@ -21,7 +21,7 @@ public static class NestRecordFactory
|
||||
nest.MadeBy = record.MadeBy;
|
||||
nest.DateLastModified = record.DateModified;
|
||||
nest.DateCreated = record.DateCreated;
|
||||
nest.Material = NestMaterialSelection.Apply(nest.Material, record.Material);
|
||||
nest.Material = NestMaterialSelection.ApplyRecorded(nest.Material, record.Material, record.MaterialGrade);
|
||||
nest.Thickness = record.Thickness;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ public static class NestRecordFactory
|
||||
DateCreated = nest.DateCreated,
|
||||
DateModified = nest.DateLastModified,
|
||||
Material = nest.Material?.Name ?? "",
|
||||
MaterialGrade = nest.Material?.Grade ?? "",
|
||||
Thickness = nest.Thickness,
|
||||
Status = nest.Status,
|
||||
// The trailing empty plate is the editor's new-plate slot, not a plate of the nest.
|
||||
|
||||
@@ -127,7 +127,7 @@ public sealed class NestSaveSession
|
||||
|
||||
private static bool SameEditableMetadata(NestRecord a, NestRecord b) =>
|
||||
a.Name == b.Name && a.Customer == b.Customer && a.DateCreated == b.DateCreated
|
||||
&& a.DateModified == b.DateModified && a.Material == b.Material && a.Thickness == b.Thickness
|
||||
&& a.Status == b.Status && a.PlateCount == b.PlateCount && a.PartCount == b.PartCount
|
||||
&& a.DateModified == b.DateModified && a.Material == b.Material && a.MaterialGrade == b.MaterialGrade
|
||||
&& a.Thickness == b.Thickness && a.Status == b.Status && a.PlateCount == b.PlateCount && a.PartCount == b.PartCount
|
||||
&& a.Comments == b.Comments && a.MadeBy == b.MadeBy;
|
||||
}
|
||||
@@ -16,4 +16,5 @@ public enum NestSortField
|
||||
MadeBy,
|
||||
Comments,
|
||||
FileSize,
|
||||
MaterialGrade,
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
Assert.Empty(database.QueryDrawings(new DrawingLibraryQuery { IncludeArchived = true }).Items);
|
||||
Assert.Equal(before, NestDigest(factory.DatabasePath));
|
||||
Assert.Equal(7L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(8L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
@@ -42,7 +42,7 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
{
|
||||
Assert.Equal(7L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(8L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(before, NestDigest(factory.DatabasePath));
|
||||
// Two distinct blobs across four revisions: the bracket's notes edit and the
|
||||
// plate both reuse the bracket's first package.
|
||||
@@ -79,7 +79,7 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using (new NestDatabase(factory.DatabasePath, pooling: false)) { }
|
||||
Execute(factory.DatabasePath, "PRAGMA user_version=8");
|
||||
Execute(factory.DatabasePath, "PRAGMA user_version=9");
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => new NestDatabase(factory.DatabasePath, pooling: false));
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class DrawingSetDatabaseTests
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
Assert.Empty(database.QueryDrawingSets(new DrawingSetQuery { IncludeArchived = true }).Items);
|
||||
Assert.Equal(before, Digest(factory.DatabasePath));
|
||||
Assert.Equal(7L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(8L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
@@ -47,7 +47,7 @@ public sealed class DrawingSetDatabaseTests
|
||||
Assert.Equal(before, Digest(factory.DatabasePath));
|
||||
Execute(factory.DatabasePath, "DROP TABLE ix_drawing_set_members_drawing");
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
Assert.Equal(7L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(8L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Server.Tests;
|
||||
|
||||
public sealed class NestMaterialGradeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Grade_RoundTripsThroughInsertUpdateAndHistory()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
var created = database.Insert(id, Record("Job", "Stainless", "304"), [1]);
|
||||
Assert.Equal("304", created.MaterialGrade);
|
||||
|
||||
var updated = database.UpdateMetadata(id, Record("Job", "Stainless", "316"), created.Revision)!;
|
||||
Assert.Equal("316", updated.MaterialGrade);
|
||||
Assert.Equal(created.Revision + 1, updated.Revision);
|
||||
|
||||
var history = database.History(id, 0, 10)!;
|
||||
Assert.Equal(["316", "304"], history.Items.Select(r => r.MaterialGrade));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GradeOnlyChange_IsNotANoOp()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var id = Guid.NewGuid();
|
||||
var created = database.Insert(id, Record("Job", "Stainless", "304"), [1]);
|
||||
|
||||
var unchanged = database.UpdateMetadata(id, Record("Job", "Stainless", "304"), created.Revision)!;
|
||||
var changed = database.UpdateMetadata(id, Record("Job", "Stainless", "316"), created.Revision)!;
|
||||
|
||||
Assert.Equal(created.Revision, unchanged.Revision);
|
||||
Assert.Equal(created.Revision + 1, changed.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_SearchesAndSortsByGrade()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
database.Insert(Guid.NewGuid(), Record("A", "Aluminum", "6061"), [1]);
|
||||
database.Insert(Guid.NewGuid(), Record("B", "Aluminum", "5052"), [1]);
|
||||
database.Insert(Guid.NewGuid(), Record("C", "Stainless", "304"), [1]);
|
||||
|
||||
var found = database.Query(new NestQuery { Search = "5052" });
|
||||
Assert.Equal("B", Assert.Single(found.Items).Name);
|
||||
|
||||
var sorted = database.Query(new NestQuery { Sort = NestSortField.MaterialGrade, Descending = false });
|
||||
Assert.Equal(["C", "B", "A"], sorted.Items.Select(r => r.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Version7Database_GainsBlankGradeAndKeepsRecords()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
var id = Guid.NewGuid();
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
database.Insert(id, Record("Legacy", "Stainless", "304"), [1]);
|
||||
Execute(factory.DatabasePath, """
|
||||
ALTER TABLE nests DROP COLUMN materialGrade;
|
||||
ALTER TABLE nest_revisions DROP COLUMN materialGrade;
|
||||
PRAGMA user_version=7;
|
||||
""");
|
||||
|
||||
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
|
||||
var record = upgraded.Get(id)!;
|
||||
Assert.Equal("Stainless", record.Material);
|
||||
Assert.Equal("", record.MaterialGrade);
|
||||
Assert.Equal("", Assert.Single(upgraded.History(id, 0, 10)!.Items).MaterialGrade);
|
||||
Assert.Equal(8L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
}
|
||||
|
||||
private static NestRecord Record(string name, string material, string grade) => new()
|
||||
{
|
||||
Name = name,
|
||||
Material = material,
|
||||
MaterialGrade = grade,
|
||||
Thickness = 0.25,
|
||||
DateCreated = new DateTime(2026, 1, 1),
|
||||
DateModified = new DateTime(2026, 1, 2),
|
||||
};
|
||||
|
||||
private static void Execute(string path, string sql)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={path};Pooling=False");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static object? Scalar(string path, string sql)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={path};Pooling=False");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return command.ExecuteScalar();
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,7 @@ public sealed class NestQueryTests : IDisposable
|
||||
DateCreated = new DateTime(2026, 1, 1 + index % 28, 8, 0, 0, DateTimeKind.Unspecified),
|
||||
DateModified = new DateTime(2026, 1, 2 + index % 27, 9, 0, 0, DateTimeKind.Unspecified),
|
||||
Material = index % 3 == 0 ? "Synthetic steel" : "Synthetic alloy",
|
||||
MaterialGrade = index % 4 == 0 ? "" : $"Grade {index % 5}",
|
||||
Thickness = 0.25 + index % 3 * 0.125,
|
||||
Status = statuses[index % statuses.Length],
|
||||
PlateCount = 1 + index % 4,
|
||||
@@ -362,6 +363,7 @@ public sealed class NestQueryTests : IDisposable
|
||||
NestSortField.MadeBy => Text(r => r.MadeBy),
|
||||
NestSortField.Comments => Text(r => r.Comments),
|
||||
NestSortField.FileSize => (a, b) => a.FileSize.CompareTo(b.FileSize),
|
||||
NestSortField.MaterialGrade => Text(r => r.MaterialGrade),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(field)),
|
||||
};
|
||||
var sorted = records.ToList();
|
||||
|
||||
@@ -149,11 +149,11 @@ public sealed partial class NestDatabase : IDisposable
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = """
|
||||
INSERT INTO nests (id, name, customer, dateCreated, dateModified, material,
|
||||
thickness, status, plateCount, partCount, comments, madeBy, fileSize,
|
||||
savedAt, file)
|
||||
materialGrade, thickness, status, plateCount, partCount, comments, madeBy,
|
||||
fileSize, savedAt, file)
|
||||
VALUES ($id, $name, $customer, $dateCreated, $dateModified, $material,
|
||||
$thickness, $status, $plateCount, $partCount, $comments, $madeBy,
|
||||
$fileSize, $savedAt, $file)
|
||||
$materialGrade, $thickness, $status, $plateCount, $partCount, $comments,
|
||||
$madeBy, $fileSize, $savedAt, $file)
|
||||
""";
|
||||
AddRecordParameters(command, id, record, file, updateFile: true);
|
||||
command.ExecuteNonQuery();
|
||||
@@ -199,7 +199,7 @@ public sealed partial class NestDatabase : IDisposable
|
||||
UPDATE nests SET
|
||||
name = $name, customer = $customer, dateCreated = $dateCreated,
|
||||
dateModified = $dateModified, material = $material,
|
||||
thickness = $thickness, status = $status, plateCount = $plateCount,
|
||||
materialGrade = $materialGrade, thickness = $thickness, status = $status, plateCount = $plateCount,
|
||||
partCount = $partCount, comments = $comments, madeBy = $madeBy,
|
||||
savedAt = $savedAt, revision = $revision, restoredFromRevision=$source{fileClause}
|
||||
WHERE id = $id
|
||||
@@ -297,8 +297,8 @@ public sealed partial class NestDatabase : IDisposable
|
||||
|
||||
private static bool SameMetadata(NestRecord a, NestRecord b) =>
|
||||
a.Name == b.Name && a.Customer == b.Customer && a.DateCreated == b.DateCreated &&
|
||||
a.DateModified == b.DateModified && a.Material == b.Material && a.Thickness == b.Thickness &&
|
||||
a.Status == b.Status && a.PlateCount == b.PlateCount && a.PartCount == b.PartCount &&
|
||||
a.DateModified == b.DateModified && a.Material == b.Material && a.MaterialGrade == b.MaterialGrade &&
|
||||
a.Thickness == b.Thickness && a.Status == b.Status && a.PlateCount == b.PlateCount && a.PartCount == b.PartCount &&
|
||||
a.Comments == b.Comments && a.MadeBy == b.MadeBy;
|
||||
|
||||
private void Snapshot(Guid id, SqliteTransaction transaction)
|
||||
@@ -319,7 +319,7 @@ public sealed partial class NestDatabase : IDisposable
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = "PRAGMA user_version";
|
||||
var version = (long)command.ExecuteScalar()!;
|
||||
if (version > 7) throw new InvalidOperationException("Database schema is newer than this server.");
|
||||
if (version > 8) throw new InvalidOperationException("Database schema is newer than this server.");
|
||||
if (version == 0)
|
||||
{
|
||||
command.CommandText = """
|
||||
@@ -371,6 +371,21 @@ public sealed partial class NestDatabase : IDisposable
|
||||
command.CommandText = DrawingSetSchema;
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
if (version < 8)
|
||||
{
|
||||
// Existing records stay blank until saved again; clients keep the archive's grade meanwhile.
|
||||
foreach (var table in new[] { "nests", "nest_revisions" })
|
||||
{
|
||||
command.CommandText = $"SELECT COUNT(*) FROM pragma_table_info('{table}') WHERE name='materialGrade'";
|
||||
if ((long)command.ExecuteScalar()! == 0)
|
||||
{
|
||||
command.CommandText = $"ALTER TABLE {table} ADD COLUMN materialGrade TEXT NOT NULL DEFAULT ''";
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
command.CommandText = "PRAGMA user_version=8";
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
@@ -408,7 +423,8 @@ public sealed partial class NestDatabase : IDisposable
|
||||
|
||||
private const string RecordColumns = """
|
||||
id, name, customer, dateCreated, dateModified, material, thickness,
|
||||
status, plateCount, partCount, comments, madeBy, fileSize, savedAt, revision, deleted, restoredFromRevision
|
||||
status, plateCount, partCount, comments, madeBy, fileSize, savedAt, revision, deleted, restoredFromRevision,
|
||||
materialGrade
|
||||
""";
|
||||
|
||||
// Case-insensitive (ASCII) substring over the text columns and the status's stored
|
||||
@@ -417,6 +433,7 @@ public sealed partial class NestDatabase : IDisposable
|
||||
name LIKE $pattern ESCAPE '\'
|
||||
OR customer LIKE $pattern ESCAPE '\'
|
||||
OR material LIKE $pattern ESCAPE '\'
|
||||
OR materialGrade LIKE $pattern ESCAPE '\'
|
||||
OR madeBy LIKE $pattern ESCAPE '\'
|
||||
OR comments LIKE $pattern ESCAPE '\'
|
||||
OR status LIKE $pattern ESCAPE '\'
|
||||
@@ -440,6 +457,7 @@ public sealed partial class NestDatabase : IDisposable
|
||||
NestSortField.MadeBy => "madeBy COLLATE NOCASE",
|
||||
NestSortField.Comments => "comments COLLATE NOCASE",
|
||||
NestSortField.FileSize => "fileSize",
|
||||
NestSortField.MaterialGrade => "materialGrade COLLATE NOCASE",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(field), field, "Unsupported sort column."),
|
||||
};
|
||||
|
||||
@@ -461,6 +479,7 @@ public sealed partial class NestDatabase : IDisposable
|
||||
command.Parameters.AddWithValue("$dateCreated", record.DateCreated.ToString("o"));
|
||||
command.Parameters.AddWithValue("$dateModified", record.DateModified.ToString("o"));
|
||||
command.Parameters.AddWithValue("$material", record.Material);
|
||||
command.Parameters.AddWithValue("$materialGrade", record.MaterialGrade);
|
||||
command.Parameters.AddWithValue("$thickness", record.Thickness);
|
||||
command.Parameters.AddWithValue("$status", record.Status.ToString());
|
||||
command.Parameters.AddWithValue("$plateCount", record.PlateCount);
|
||||
@@ -498,6 +517,7 @@ public sealed partial class NestDatabase : IDisposable
|
||||
Revision = reader.GetInt32(first + 14),
|
||||
Deleted = reader.GetInt32(first + 15) != 0,
|
||||
RestoredFromRevision = reader.IsDBNull(first + 16) ? null : reader.GetInt32(first + 16),
|
||||
MaterialGrade = reader.GetString(first + 17),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ internal static class NestMetadata
|
||||
if (record.Name is null) return "name must be a string.";
|
||||
if (record.Customer is null) return "customer must be a string.";
|
||||
if (record.Material is null) return "material must be a string.";
|
||||
if (record.MaterialGrade is null) return "materialGrade must be a string.";
|
||||
if (record.Comments is null) return "comments must be a string.";
|
||||
if (record.MadeBy is null) return "madeBy must be a string.";
|
||||
if (!Enum.IsDefined(record.Status)) return "status must be draft, quote, toBeCut or hasBeenCut.";
|
||||
|
||||
@@ -5,11 +5,11 @@ namespace OpenNest.Tests.Data;
|
||||
public class NestMaterialSelectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_UnchangedName_KeepsGradeAndDensity()
|
||||
public void Apply_UnchangedNameAndGrade_KeepsDensity()
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.Apply(current, "Stainless");
|
||||
var result = NestMaterialSelection.Apply(current, "Stainless", "304");
|
||||
|
||||
Assert.Equal("Stainless", result.Name);
|
||||
Assert.Equal("304", result.Grade);
|
||||
@@ -21,7 +21,7 @@ public class NestMaterialSelectionTests
|
||||
{
|
||||
var current = new Material("Cold Rolled Steel", "1008", 0.284);
|
||||
|
||||
var result = NestMaterialSelection.Apply(current, " cold rolled\tsteel ");
|
||||
var result = NestMaterialSelection.Apply(current, " cold rolled\tsteel ", "1008");
|
||||
|
||||
Assert.Equal(" cold rolled\tsteel ", result.Name);
|
||||
Assert.Equal("1008", result.Grade);
|
||||
@@ -33,7 +33,7 @@ public class NestMaterialSelectionTests
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.Apply(current, "Aluminum");
|
||||
var result = NestMaterialSelection.Apply(current, "Aluminum", "");
|
||||
|
||||
Assert.Equal("Aluminum", result.Name);
|
||||
Assert.True(string.IsNullOrEmpty(result.Grade));
|
||||
@@ -45,7 +45,7 @@ public class NestMaterialSelectionTests
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.Apply(current, "");
|
||||
var result = NestMaterialSelection.Apply(current, "", "");
|
||||
|
||||
Assert.Equal("", result.Name);
|
||||
Assert.True(string.IsNullOrEmpty(result.Grade));
|
||||
@@ -55,7 +55,7 @@ public class NestMaterialSelectionTests
|
||||
[Fact]
|
||||
public void Apply_NoCurrentMaterial_GetsNameOnly()
|
||||
{
|
||||
var result = NestMaterialSelection.Apply(null, "Stainless");
|
||||
var result = NestMaterialSelection.Apply(null, "Stainless", null);
|
||||
|
||||
Assert.Equal("Stainless", result.Name);
|
||||
Assert.True(string.IsNullOrEmpty(result.Grade));
|
||||
@@ -67,7 +67,7 @@ public class NestMaterialSelectionTests
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.Apply(current, "stainless");
|
||||
var result = NestMaterialSelection.Apply(current, "stainless", "304");
|
||||
result.Grade = "316";
|
||||
result.Density = 1;
|
||||
|
||||
@@ -77,6 +77,67 @@ public class NestMaterialSelectionTests
|
||||
Assert.Equal(0.289, current.Density);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_ChangedGrade_TakesGradeWithoutDensity()
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.Apply(current, "Stainless", "316");
|
||||
|
||||
Assert.Equal("Stainless", result.Name);
|
||||
Assert.Equal("316", result.Grade);
|
||||
Assert.Equal(0, result.Density);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_ChangedNameWithGrade_TakesNameAndGrade()
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.Apply(current, "Aluminum", "5052");
|
||||
|
||||
Assert.Equal("Aluminum", result.Name);
|
||||
Assert.Equal("5052", result.Grade);
|
||||
Assert.Equal(0, result.Density);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void ApplyRecorded_BlankGradeWithSameName_KeepsArchivedGradeAndDensity(string? recordedGrade)
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.ApplyRecorded(current, "stainless", recordedGrade);
|
||||
|
||||
Assert.Equal("stainless", result.Name);
|
||||
Assert.Equal("304", result.Grade);
|
||||
Assert.Equal(0.289, result.Density);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRecorded_BlankGradeWithChangedName_GetsNameOnly()
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.ApplyRecorded(current, "Aluminum", "");
|
||||
|
||||
Assert.Equal("Aluminum", result.Name);
|
||||
Assert.Equal("", result.Grade);
|
||||
Assert.Equal(0, result.Density);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRecorded_RecordedGrade_Wins()
|
||||
{
|
||||
var current = new Material("Stainless", "304", 0.289);
|
||||
|
||||
var result = NestMaterialSelection.ApplyRecorded(current, "Stainless", "316");
|
||||
|
||||
Assert.Equal("316", result.Grade);
|
||||
Assert.Equal(0, result.Density);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Carbon Steel", "CARBON STEEL")]
|
||||
[InlineData(" carbon \t steel\n", "CARBON STEEL")]
|
||||
|
||||
@@ -34,6 +34,7 @@ public class NestRecordFactoryTests
|
||||
NestRecordFactory.ApplyMetadata(nest, current);
|
||||
var roundTrip = NestRecordFactory.FromNest(nest, Guid.Empty, 0);
|
||||
Assert.Equal(current.Material, roundTrip.Material);
|
||||
Assert.Equal(sameMaterial ? "Test grade" : "", roundTrip.MaterialGrade);
|
||||
Assert.Equal(current.Thickness, roundTrip.Thickness);
|
||||
Assert.Equal(current.DateCreated, roundTrip.DateCreated);
|
||||
Assert.Equal(current.DateModified, roundTrip.DateModified);
|
||||
@@ -58,7 +59,7 @@ public class NestRecordFactoryTests
|
||||
var nest = new Nest("Job 42")
|
||||
{
|
||||
Customer = "Cozy Cat",
|
||||
Material = new Material("Aluminum"),
|
||||
Material = new Material("Aluminum", "5052"),
|
||||
Thickness = 0.125,
|
||||
Status = NestStatus.ToBeCut,
|
||||
MadeBy = "AJ",
|
||||
@@ -89,6 +90,7 @@ public class NestRecordFactoryTests
|
||||
Assert.Equal("Job 42", record.Name);
|
||||
Assert.Equal("Cozy Cat", record.Customer);
|
||||
Assert.Equal("Aluminum", record.Material);
|
||||
Assert.Equal("5052", record.MaterialGrade);
|
||||
Assert.Equal(0.125, record.Thickness);
|
||||
Assert.Equal(NestStatus.ToBeCut, record.Status);
|
||||
Assert.Equal("AJ", record.MadeBy);
|
||||
@@ -111,6 +113,7 @@ public class NestRecordFactoryTests
|
||||
Assert.Equal(0, record.PlateCount);
|
||||
Assert.Equal(0, record.PartCount);
|
||||
Assert.Equal("", record.Material);
|
||||
Assert.Equal("", record.MaterialGrade);
|
||||
Assert.Equal("", record.Comments);
|
||||
Assert.Equal("", record.MadeBy);
|
||||
}
|
||||
|
||||
@@ -216,6 +216,56 @@ public class EditNestInfoFormTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GradeList_FollowsMaterial_AndSavesChosenGrade()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest { Material = new Material("Stainless", "304", 0.289) };
|
||||
using var form = new EditNestInfoForm();
|
||||
form.SetMaterialCatalog(new[]
|
||||
{
|
||||
new Data.MaterialConfig { Name = "Stainless", Grade = "316" },
|
||||
new Data.MaterialConfig { Name = "Stainless", Grade = "304" },
|
||||
new Data.MaterialConfig { Name = "Aluminum", Grade = "5052" },
|
||||
});
|
||||
form.LoadNestInfo(nest);
|
||||
var gradeBox = Assert.IsType<ComboBox>(form.Controls.Find("gradeBox", true).Single());
|
||||
|
||||
Assert.Equal(new[] { "304", "316" }, gradeBox.Items.Cast<string>());
|
||||
Assert.Equal("304", form.MaterialGrade);
|
||||
|
||||
form.MaterialName = "aluminum";
|
||||
Assert.Equal(new[] { "5052" }, gradeBox.Items.Cast<string>());
|
||||
Assert.Equal("", form.MaterialGrade);
|
||||
|
||||
form.MaterialGrade = "5052";
|
||||
form.SaveNestInfo(nest);
|
||||
|
||||
Assert.Equal("Aluminum", nest.Material.Name);
|
||||
Assert.Equal("5052", nest.Material.Grade);
|
||||
Assert.Equal(0, nest.Material.Density);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveWithChangedGrade_KeepsNameAndDropsDensity()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = new Nest { Material = new Material("Stainless", "304", 0.289) };
|
||||
using var form = new EditNestInfoForm();
|
||||
form.LoadNestInfo(nest);
|
||||
form.MaterialGrade = "316";
|
||||
|
||||
form.SaveNestInfo(nest);
|
||||
|
||||
Assert.Equal("Stainless", nest.Material.Name);
|
||||
Assert.Equal("316", nest.Material.Grade);
|
||||
Assert.Equal(0, nest.Material.Density);
|
||||
});
|
||||
}
|
||||
|
||||
private static void RunSta(Action action) =>
|
||||
StaTestThread.Run(action, TimeSpan.FromSeconds(15), "The STA test did not complete.");
|
||||
}
|
||||
@@ -758,6 +758,7 @@ namespace OpenNest.Forms
|
||||
public async Task ShowNestInfoEditorAsync()
|
||||
{
|
||||
var form = new EditNestInfoForm { AllowRename = NestStorage.Settings.IsDatabaseMode };
|
||||
form.SetMaterialCatalog(MachineSettings.LoadMaterials());
|
||||
form.LoadNestInfo(Nest);
|
||||
_ = ShowReleaseOriginAsync(form);
|
||||
|
||||
|
||||
+42
-19
@@ -64,6 +64,8 @@
|
||||
this.label5 = new System.Windows.Forms.Label();
|
||||
this.labelMaterial = new System.Windows.Forms.Label();
|
||||
this.materialBox = new System.Windows.Forms.ComboBox();
|
||||
this.labelGrade = new System.Windows.Forms.Label();
|
||||
this.gradeBox = new System.Windows.Forms.ComboBox();
|
||||
this.labelStatus = new System.Windows.Forms.Label();
|
||||
this.statusBox = new System.Windows.Forms.ComboBox();
|
||||
this.labelMadeBy = new System.Windows.Forms.Label();
|
||||
@@ -408,37 +410,40 @@
|
||||
this.tableLayoutPanel3.ColumnCount = 2;
|
||||
this.tableLayoutPanel3.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
this.tableLayoutPanel3.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
this.tableLayoutPanel3.Controls.Add(this.tableLayoutPanel4, 1, 6);
|
||||
this.tableLayoutPanel3.Controls.Add(this.tableLayoutPanel4, 1, 7);
|
||||
this.tableLayoutPanel3.Controls.Add(this.label1, 0, 0);
|
||||
this.tableLayoutPanel3.Controls.Add(this.nameBox, 1, 0);
|
||||
this.tableLayoutPanel3.Controls.Add(this.label2, 0, 5);
|
||||
this.tableLayoutPanel3.Controls.Add(this.label2, 0, 6);
|
||||
this.tableLayoutPanel3.Controls.Add(this.labelThk, 0, 3);
|
||||
this.tableLayoutPanel3.Controls.Add(this.thicknessBox, 1, 3);
|
||||
this.tableLayoutPanel3.Controls.Add(this.labelMaterial, 0, 4);
|
||||
this.tableLayoutPanel3.Controls.Add(this.materialBox, 1, 4);
|
||||
this.tableLayoutPanel3.Controls.Add(this.labelGrade, 0, 5);
|
||||
this.tableLayoutPanel3.Controls.Add(this.gradeBox, 1, 5);
|
||||
this.tableLayoutPanel3.Controls.Add(this.label3, 0, 1);
|
||||
this.tableLayoutPanel3.Controls.Add(this.label4, 0, 2);
|
||||
this.tableLayoutPanel3.Controls.Add(this.customerBox, 1, 5);
|
||||
this.tableLayoutPanel3.Controls.Add(this.customerBox, 1, 6);
|
||||
this.tableLayoutPanel3.Controls.Add(this.textBox1, 1, 1);
|
||||
this.tableLayoutPanel3.Controls.Add(this.textBox2, 1, 2);
|
||||
this.tableLayoutPanel3.Controls.Add(this.label5, 0, 6);
|
||||
this.tableLayoutPanel3.Controls.Add(this.labelStatus, 0, 7);
|
||||
this.tableLayoutPanel3.Controls.Add(this.statusBox, 1, 7);
|
||||
this.tableLayoutPanel3.Controls.Add(this.labelMadeBy, 0, 8);
|
||||
this.tableLayoutPanel3.Controls.Add(this.madeByBox, 1, 8);
|
||||
this.tableLayoutPanel3.Controls.Add(this.label5, 0, 7);
|
||||
this.tableLayoutPanel3.Controls.Add(this.labelStatus, 0, 8);
|
||||
this.tableLayoutPanel3.Controls.Add(this.statusBox, 1, 8);
|
||||
this.tableLayoutPanel3.Controls.Add(this.labelMadeBy, 0, 9);
|
||||
this.tableLayoutPanel3.Controls.Add(this.madeByBox, 1, 9);
|
||||
this.tableLayoutPanel3.Location = new System.Drawing.Point(6, 6);
|
||||
this.tableLayoutPanel3.Name = "tableLayoutPanel3";
|
||||
this.tableLayoutPanel3.RowCount = 9;
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.11111F));
|
||||
this.tableLayoutPanel3.Size = new System.Drawing.Size(362, 280);
|
||||
this.tableLayoutPanel3.RowCount = 10;
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
|
||||
this.tableLayoutPanel3.Size = new System.Drawing.Size(362, 311);
|
||||
this.tableLayoutPanel3.TabIndex = 0;
|
||||
//
|
||||
// tableLayoutPanel4
|
||||
@@ -532,6 +537,22 @@
|
||||
this.materialBox.Name = "materialBox";
|
||||
this.materialBox.Size = new System.Drawing.Size(224, 24);
|
||||
this.materialBox.TabIndex = 11;
|
||||
//
|
||||
// labelGrade
|
||||
//
|
||||
this.labelGrade.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.labelGrade.AutoSize = true;
|
||||
this.labelGrade.Name = "labelGrade";
|
||||
this.labelGrade.TabIndex = 10;
|
||||
this.labelGrade.Text = "Grade :";
|
||||
//
|
||||
// gradeBox
|
||||
//
|
||||
this.gradeBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.gradeBox.FormattingEnabled = true;
|
||||
this.gradeBox.Name = "gradeBox";
|
||||
this.gradeBox.Size = new System.Drawing.Size(224, 24);
|
||||
this.gradeBox.TabIndex = 11;
|
||||
//
|
||||
// labelStatus
|
||||
//
|
||||
@@ -786,6 +807,8 @@
|
||||
private System.Windows.Forms.Label label5;
|
||||
private System.Windows.Forms.Label labelMaterial;
|
||||
private System.Windows.Forms.ComboBox materialBox;
|
||||
private System.Windows.Forms.Label labelGrade;
|
||||
private System.Windows.Forms.ComboBox gradeBox;
|
||||
private System.Windows.Forms.Label labelStatus;
|
||||
private System.Windows.Forms.ComboBox statusBox;
|
||||
private System.Windows.Forms.Label labelMadeBy;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Geometry;
|
||||
@@ -11,6 +13,8 @@ namespace OpenNest.Forms
|
||||
private readonly Timer timer;
|
||||
private DateTime dateCreated;
|
||||
private DateTime dateLastModified;
|
||||
private List<MaterialConfig> materialCatalog = new();
|
||||
private string gradeMaterialKey = "";
|
||||
|
||||
public EditNestInfoForm()
|
||||
{
|
||||
@@ -34,6 +38,7 @@ namespace OpenNest.Forms
|
||||
};
|
||||
Disposed += (sender, e) => timer.Dispose();
|
||||
nameBox.TextChanged += (sender, e) => EnableCheck();
|
||||
materialBox.TextChanged += (sender, e) => RefreshGrades();
|
||||
EnableCheck();
|
||||
}
|
||||
|
||||
@@ -142,6 +147,57 @@ namespace OpenNest.Forms
|
||||
set { materialBox.Text = value; }
|
||||
}
|
||||
|
||||
public string MaterialGrade
|
||||
{
|
||||
get { return gradeBox.Text; }
|
||||
set { gradeBox.Text = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offers the machine configurations' materials: their names join the material
|
||||
/// list, and the grade list shows the grades on file for the chosen material.
|
||||
/// </summary>
|
||||
public void SetMaterialCatalog(IEnumerable<MaterialConfig> materials)
|
||||
{
|
||||
materialCatalog = materials.ToList();
|
||||
|
||||
var text = materialBox.Text;
|
||||
var names = materialBox.Items.Cast<string>()
|
||||
.Concat(materialCatalog.Select(m => m.Name))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.GroupBy(SharedListNames.Key)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
materialBox.Items.Clear();
|
||||
materialBox.Items.AddRange(names);
|
||||
materialBox.Text = text;
|
||||
RefreshGrades();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists the grades on file for the typed material. A grade belongs to its
|
||||
/// material, so changing the material clears a grade the new one does not have.
|
||||
/// </summary>
|
||||
private void RefreshGrades()
|
||||
{
|
||||
var text = gradeBox.Text;
|
||||
var key = SharedListNames.Key(materialBox.Text);
|
||||
var grades = materialCatalog
|
||||
.Where(m => SharedListNames.Key(m.Name) == key && !string.IsNullOrWhiteSpace(m.Grade))
|
||||
.Select(m => m.Grade)
|
||||
.GroupBy(SharedListNames.Key)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(grade => grade, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
if (key != gradeMaterialKey && !grades.Any(grade => SharedListNames.Key(grade) == SharedListNames.Key(text)))
|
||||
text = "";
|
||||
gradeMaterialKey = key;
|
||||
gradeBox.Items.Clear();
|
||||
gradeBox.Items.AddRange(grades);
|
||||
gradeBox.Text = text;
|
||||
}
|
||||
|
||||
public NestStatus Status
|
||||
{
|
||||
get { return statusBox.SelectedItem is NestStatus status ? status : NestStatus.Quote; }
|
||||
@@ -245,6 +301,7 @@ namespace OpenNest.Forms
|
||||
DateLastModified = nest.DateLastModified;
|
||||
Thickness = nest.Thickness;
|
||||
MaterialName = nest.Material?.Name ?? "";
|
||||
MaterialGrade = nest.Material?.Grade ?? "";
|
||||
Status = nest.Status;
|
||||
MadeBy = nest.MadeBy ?? "";
|
||||
SizeString = nest.PlateDefaults.Size.ToString();
|
||||
@@ -267,7 +324,7 @@ namespace OpenNest.Forms
|
||||
nest.DateCreated = DateCreated;
|
||||
nest.DateLastModified = DateLastModified;
|
||||
nest.Thickness = Thickness;
|
||||
nest.Material = NestMaterialSelection.Apply(nest.Material, MaterialName);
|
||||
nest.Material = NestMaterialSelection.Apply(nest.Material, MaterialName, MaterialGrade);
|
||||
nest.Status = Status;
|
||||
nest.MadeBy = MadeBy;
|
||||
nest.PlateDefaults.Size = OpenNest.Geometry.Size.Parse(SizeString);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
|
||||
@@ -42,6 +43,10 @@ namespace OpenNest.Forms
|
||||
catch (UnauthorizedAccessException) { }
|
||||
return machines;
|
||||
}
|
||||
|
||||
/// <summary>Every material and grade on file across the readable machine configurations.</summary>
|
||||
public static IEnumerable<MaterialConfig> LoadMaterials() =>
|
||||
LoadMachines().SelectMany(machine => machine.Materials);
|
||||
private static bool defaultsWarned;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -111,6 +111,7 @@ public sealed class SavedNestsForm : Form
|
||||
AddNestColumn("Customer", NestSortField.Customer, 160);
|
||||
AddNestColumn("Made by", NestSortField.MadeBy, 90);
|
||||
AddNestColumn("Material", NestSortField.Material, 150);
|
||||
AddNestColumn("Grade", NestSortField.MaterialGrade, 80);
|
||||
AddNestColumn("Thickness", NestSortField.Thickness, 80, alignRight: true);
|
||||
AddNestColumn("Created", NestSortField.DateCreated, 140);
|
||||
AddNestColumn("Saved", NestSortField.SavedAt, 140);
|
||||
@@ -515,6 +516,7 @@ public sealed class SavedNestsForm : Form
|
||||
record.Customer,
|
||||
record.MadeBy,
|
||||
record.Material,
|
||||
record.MaterialGrade,
|
||||
record.Thickness.ToString("0.####", CultureInfo.CurrentCulture),
|
||||
FormatDate(record.DateCreated),
|
||||
FormatDate(record.SavedAt),
|
||||
@@ -816,6 +818,7 @@ public sealed class SavedNestsForm : Form
|
||||
});
|
||||
if (IsDisposed) return;
|
||||
using var dialog = new EditNestInfoForm { AllowRename = true };
|
||||
dialog.SetMaterialCatalog(MachineSettings.LoadMaterials());
|
||||
dialog.LoadNestInfo(nest);
|
||||
while (true)
|
||||
{
|
||||
|
||||
+10
-4
@@ -5,7 +5,7 @@ The desktop app can save nests two ways:
|
||||
- **File mode** (default, unchanged behavior): Save/Save As write a `.nest` ZIP
|
||||
archive to disk via the normal file dialog. See [nest-file-format.md](nest-file-format.md).
|
||||
- **Database mode**: Open browses server records 100 at a time. The Find box
|
||||
searches name, customer, material, made by, comments and status on the server
|
||||
searches name, customer, material, grade, made by, comments and status on the server
|
||||
after a short typing pause (Enter in the box searches at once); clicking a
|
||||
column header sorts every match on the server (click again to reverse); the
|
||||
arrow buttons move between pages and the title and find bar show the range and
|
||||
@@ -77,7 +77,8 @@ so upgrade the server before desktops that can set it.
|
||||
| `name` | string | |
|
||||
| `customer` | string | |
|
||||
| `dateCreated` / `dateModified` | datetime | Set by the client from the nest's own metadata. |
|
||||
| `material` | string | |
|
||||
| `material` | string | Material name (type), e.g. `Stainless`. |
|
||||
| `materialGrade` | string | Optional; default blank. Older servers drop it. A blank grade on a record saved before schema v8 is treated as unrecorded: opening keeps the archive's grade while the material name matches. |
|
||||
| `thickness` | number | |
|
||||
| `status` | `"draft"` \| `"quote"` \| `"toBeCut"` \| `"hasBeenCut"` | Default `quote`. |
|
||||
| `plateCount` / `partCount` | integer | Computed client-side at save time (non-cutoff parts only). |
|
||||
@@ -163,13 +164,13 @@ only the requested page of metadata (never archive bytes):
|
||||
- `search` (optional): trimmed, at most 200 characters, no NUL characters;
|
||||
blank means no filter.
|
||||
Case-insensitive substring of the whole text in `name`, `customer`, `material`,
|
||||
`madeBy`, `comments` or the status (`ToBeCut` or the display name `To Be Cut`).
|
||||
`materialGrade`, `madeBy`, `comments` or the status (`ToBeCut` or the display name `To Be Cut`).
|
||||
`%`, `_` and `\` are literal. Dates and numbers are not matched as text.
|
||||
Case folding is ASCII-only (SQLite `LIKE`).
|
||||
- `offset` (default 0, at least 0) and `limit` (default 100, 1 to 500).
|
||||
- `sort` (default `savedAt`): one of `savedAt`, `name`, `customer`, `status`,
|
||||
`material`, `dateCreated`, `dateModified`, `thickness`, `plateCount`,
|
||||
`partCount`, `madeBy`, `comments`, `fileSize` (names, case-insensitive; no
|
||||
`partCount`, `madeBy`, `comments`, `fileSize`, `materialGrade` (names, case-insensitive; no
|
||||
numbers). Text columns sort case-insensitively (ASCII); dates sort as their
|
||||
stored ISO text. `order` is `asc` or `desc` (default `desc`).
|
||||
- Ties are broken by `id` in the same direction, so pages partition the matches
|
||||
@@ -535,6 +536,11 @@ v7 refuses to open a v7 database, so rolling back needs the pre-upgrade backup.
|
||||
The stopped-volume `tar` backup includes the set and release tables; `nest_manifest.py` and
|
||||
`restore_check` do not verify them yet.
|
||||
|
||||
Schema v8 adds a `materialGrade` text column (default blank) to `nests` and
|
||||
`nest_revisions`; existing records keep a blank grade until they are next saved.
|
||||
A server older than v8 refuses to open a v8 database, so rolling back needs the
|
||||
pre-upgrade backup.
|
||||
|
||||
## Storage
|
||||
|
||||
`OpenNest.Server.NestDatabase` uses SQLite (`Microsoft.Data.Sqlite`) with
|
||||
|
||||
Reference in new issue
Block a user