mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-26 22:01:06 -04:00
feat(nests): add a Draft nest status
Nests released from drawing sets need a state before Quote: created but not yet nested or checked. Draft is appended to NestStatus (value 3) because the server still accepts integer statuses from existing callers, so existing numbers must not shift; NestStatuses.WorkflowOrder puts it first wherever the operator picks a status. Older desktops read Draft as Quote (the documented unknown-status fallback), and an older server refuses "draft", so the server must be upgraded before desktops that can set it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1b323e2380
commit
28ee04a480
9 files changed
+90
-6
No files matched your search
@@ -4,10 +4,33 @@ namespace OpenNest
|
||||
/// Shop-floor workflow state of a saved nest, persisted as a string in
|
||||
/// <c>nest.json</c> so unknown future values can fall back safely.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// New values go at the end: the nest server still accepts integer statuses from
|
||||
/// existing callers, so existing values must keep their numbers. Lists shown to the
|
||||
/// operator use <see cref="NestStatuses.WorkflowOrder"/>, not declaration order.
|
||||
/// </remarks>
|
||||
public enum NestStatus
|
||||
{
|
||||
Quote,
|
||||
ToBeCut,
|
||||
HasBeenCut,
|
||||
|
||||
/// <summary>
|
||||
/// Created but not yet ready to quote or cut, such as a nest released from a
|
||||
/// drawing set before it has been nested and checked. Comes before Quote.
|
||||
/// </summary>
|
||||
Draft,
|
||||
}
|
||||
|
||||
public static class NestStatuses
|
||||
{
|
||||
/// <summary>Every status in the order a nest moves through them.</summary>
|
||||
public static readonly NestStatus[] WorkflowOrder =
|
||||
{
|
||||
NestStatus.Draft,
|
||||
NestStatus.Quote,
|
||||
NestStatus.ToBeCut,
|
||||
NestStatus.HasBeenCut,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ public sealed class NestUploadValidationTests
|
||||
[InlineData("""{"name":"A","madeBy":null}""")]
|
||||
[InlineData("""{"name":"A","status":99}""")]
|
||||
[InlineData("""{"name":"A","status":"cancelled"}""")]
|
||||
[InlineData("""{"name":"A","status":4}""")]
|
||||
public async Task Create_WithInvalidMetadata_Returns400AndStoresNothing(string metadata)
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
@@ -80,6 +81,8 @@ public sealed class NestUploadValidationTests
|
||||
[Theory]
|
||||
[InlineData("""{"name":"A","status":"toBeCut"}""", NestStatus.ToBeCut)]
|
||||
[InlineData("""{"name":"A","status":2}""", NestStatus.HasBeenCut)]
|
||||
[InlineData("""{"name":"A","status":"draft"}""", NestStatus.Draft)]
|
||||
[InlineData("""{"name":"A","status":3}""", NestStatus.Draft)]
|
||||
public async Task Create_WithDefinedStatus_StoresIt(string metadata, NestStatus expected)
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
|
||||
@@ -53,7 +53,7 @@ internal static class NestMetadata
|
||||
if (record.Material is null) return "material 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 quote, toBeCut or hasBeenCut.";
|
||||
if (!Enum.IsDefined(record.Status)) return "status must be draft, quote, toBeCut or hasBeenCut.";
|
||||
if (!double.IsFinite(record.Thickness)) return "thickness must be a finite number.";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,41 @@ public class NestStatusSerializationTests
|
||||
Assert.Equal(NestStatus.HasBeenCut, loaded.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_Draft_PreservedAsName()
|
||||
{
|
||||
var nest = CreateMinimalNest();
|
||||
nest.Status = NestStatus.Draft;
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
new NestWriter(nest).Write(ms);
|
||||
using var doc = JsonDocument.Parse(ReadNestJson(ms));
|
||||
ms.Position = 0;
|
||||
var loaded = new NestReader(ms).Read();
|
||||
|
||||
Assert.Equal("Draft", doc.RootElement.GetProperty("status").GetString());
|
||||
Assert.Equal(NestStatus.Draft, loaded.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkflowOrder_StartsWithDraftAndListsEveryStatusOnce()
|
||||
{
|
||||
Assert.Equal(NestStatus.Draft, NestStatuses.WorkflowOrder[0]);
|
||||
Assert.Equal(
|
||||
Enum.GetValues<NestStatus>().OrderBy(s => s),
|
||||
NestStatuses.WorkflowOrder.OrderBy(s => s));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExistingStatusNumbers_AreUnchanged()
|
||||
{
|
||||
// The nest server still accepts integer statuses from existing callers.
|
||||
Assert.Equal(0, (int)NestStatus.Quote);
|
||||
Assert.Equal(1, (int)NestStatus.ToBeCut);
|
||||
Assert.Equal(2, (int)NestStatus.HasBeenCut);
|
||||
Assert.Equal(3, (int)NestStatus.Draft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Status_SerializedAsEnumString()
|
||||
{
|
||||
|
||||
@@ -143,6 +143,25 @@ public class EditNestInfoFormTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StatusList_FollowsWorkflowOrder_AndSavesDraft()
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
var nest = AcceptanceNest();
|
||||
using var form = new EditNestInfoForm();
|
||||
form.LoadNestInfo(nest);
|
||||
var statusBox = Assert.IsType<ComboBox>(form.Controls.Find("statusBox", true).Single());
|
||||
|
||||
Assert.Equal(NestStatuses.WorkflowOrder, statusBox.Items.Cast<NestStatus>());
|
||||
|
||||
form.Status = NestStatus.Draft;
|
||||
form.SaveNestInfo(nest);
|
||||
|
||||
Assert.Equal(NestStatus.Draft, nest.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlankName_DisablesOk()
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace OpenNest.Forms
|
||||
foreach (var name in PostProcessorMaterials.Names)
|
||||
materialBox.Items.Add(name);
|
||||
|
||||
foreach (var status in Enum.GetValues<NestStatus>())
|
||||
foreach (var status in NestStatuses.WorkflowOrder)
|
||||
statusBox.Items.Add(status);
|
||||
statusBox.SelectedItem = NestStatus.Quote;
|
||||
|
||||
|
||||
@@ -542,6 +542,7 @@ public sealed class SavedNestsForm : Form
|
||||
|
||||
private static string FormatStatus(NestStatus status) => status switch
|
||||
{
|
||||
NestStatus.Draft => "Draft",
|
||||
NestStatus.Quote => "Quote",
|
||||
NestStatus.ToBeCut => "To Be Cut",
|
||||
NestStatus.HasBeenCut => "Has Been Cut",
|
||||
|
||||
@@ -8,9 +8,10 @@ opening and saving with an older application loses that state.
|
||||
|
||||
`nest.json` carries the workflow fields shown in the nest info dialog:
|
||||
`name`, `customer`, `dateCreated`, `dateLastModified`, `notes`, `material`,
|
||||
`thickness`, plus `status` (`"Quote"`, `"ToBeCut"`, `"HasBeenCut"`) and `madeBy`.
|
||||
`thickness`, plus `status` (`"Draft"`, `"Quote"`, `"ToBeCut"`, `"HasBeenCut"`) and `madeBy`.
|
||||
Status and made-by are additive: files written before them load as Quote with an
|
||||
empty maker, and an unrecognized status value likewise falls back to Quote.
|
||||
empty maker, and an unrecognized status value likewise falls back to Quote. Builds
|
||||
before `Draft` therefore open a Draft nest as Quote, and saving it there keeps Quote.
|
||||
|
||||
## Programs and placements
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ with enums serialized as strings (`JsonSerializerDefaults.Web` +
|
||||
`JsonStringEnumConverter(JsonNamingPolicy.CamelCase)`), matching the rest of
|
||||
`OpenNest.Data`'s local JSON stores. Uploaded metadata (POST and both PUTs) must
|
||||
carry string text fields (`null` is 400) and a defined `status`: a name, or for
|
||||
existing callers its integer value 0–2; an undefined value is 400 and stores nothing.
|
||||
existing callers its integer value (0 quote, 1 toBeCut, 2 hasBeenCut, 3 draft); an
|
||||
undefined value is 400 and stores nothing. A server older than `draft` refuses it,
|
||||
so upgrade the server before desktops that can set it.
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
@@ -77,7 +79,7 @@ existing callers its integer value 0–2; an undefined value is 400 and stores n
|
||||
| `dateCreated` / `dateModified` | datetime | Set by the client from the nest's own metadata. |
|
||||
| `material` | string | |
|
||||
| `thickness` | number | |
|
||||
| `status` | `"quote"` \| `"toBeCut"` \| `"hasBeenCut"` | Default `quote`. |
|
||||
| `status` | `"draft"` \| `"quote"` \| `"toBeCut"` \| `"hasBeenCut"` | Default `quote`. |
|
||||
| `plateCount` / `partCount` | integer | Computed client-side at save time (non-cutoff parts only). |
|
||||
| `comments` | string | |
|
||||
| `madeBy` | string | |
|
||||
|
||||
Reference in new issue
Block a user