feat(desktop): say "set" and "quantity" in the set and release dialogs

Wording only, no behavior change: "kit" becomes "set" or "Quantity",
the release button reads Make Nests... (Try Again after a failed
record), the releases list shows Material and Action, and the release
dialog's status lines are shorter. Help in docs/nest-storage.md follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-24 15:33:12 -04:00
1 parent 0d1dade063
commit ee208b2766
10 files changed
+72 -71

No files matched your search

+5 -5
View File
@@ -196,11 +196,11 @@ public sealed class SetReleasePlan
var set = line?.Set ?? throw new ArgumentException("Every line needs a set.", nameof(lines));
if (line.Quantity < 1 || line.Quantity > SetReleaseRules.MaxQuantity)
throw new ArgumentOutOfRangeException(nameof(lines), line.Quantity,
$"The number of kits must be between 1 and {SetReleaseRules.MaxQuantity}.");
$"The quantity of a set must be between 1 and {SetReleaseRules.MaxQuantity}.");
if (set.Id == Guid.Empty || set.Revision < 1)
throw new ArgumentException("Only a set read from the server can be released.", nameof(lines));
if (!seenSets.Add(set.Id))
throw new ArgumentException($"Set '{set.Name}' is listed twice. Release it once with more kits.", nameof(lines));
throw new ArgumentException($"Set '{set.Name}' is listed twice. List it once with a larger quantity.", nameof(lines));
var members = set.Members ?? throw new ArgumentException("The set has no member list.", nameof(lines));
if (members.Any(m => m?.Drawing is null || m.Drawing.Id != m.DrawingId))
throw new ArgumentException("A set member is missing its library drawing. Reload the set.", nameof(lines));
@@ -216,7 +216,7 @@ public sealed class SetReleasePlan
if (drawing.Id == Guid.Empty || drawing.Revision < 1)
throw new ArgumentException("Only a drawing read from the library can be released.", nameof(drawings));
if (!seenDrawings.Add(drawing.Id))
throw new ArgumentException($"Drawing '{drawing.Name}' is listed twice. Release it once with a larger quantity.",
throw new ArgumentException($"Drawing '{drawing.Name}' is listed twice. List it once with a larger quantity.",
nameof(drawings));
}
@@ -324,7 +324,7 @@ public sealed class SetReleasePlan
if (unassigned)
{
refusal = $"{Names(items)} {(items.Count == 1 ? "has" : "have")} no material or thickness. "
+ "Set them on the library drawings (Edit Drawing), then release again.";
+ "Set them with Fix Drawings or Edit Drawing.";
refused = items;
}
else
@@ -334,7 +334,7 @@ public sealed class SetReleasePlan
if (duplicate != null)
{
refusal = $"{duplicate.Count()} drawings in this group are named '{duplicate.Key}'. "
+ "A nest keys drawings by name, so rename all but one in the library (Edit Drawing) first.";
+ "A nest needs unique names, so rename all but one (Fix Drawings or Edit Drawing).";
refused = duplicate.ToList();
}
}
@@ -49,7 +49,7 @@ public class SetReleaseFormsTests
Run(form.ReleaseAsync());
Assert.Contains("Release set 'Synthetic kit' ×3 into 2 Draft nests", Assert.Single(questions));
Assert.Contains("Make 2 Draft nests from set 'Synthetic kit' ×3?", 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));
@@ -61,7 +61,7 @@ public class SetReleaseFormsTests
var release = Assert.Single(sets.Releases);
Assert.Equal((3, "Synthetic run", 2), (release.Sets.Single().Quantity, release.Notes, release.Nests.Count));
Assert.Same(release, form.Result!.Release);
Assert.StartsWith("Released: 2 nests", Field<Label>(form, "statusLabel").Text);
Assert.StartsWith("Done: 2 nests", Field<Label>(form, "statusLabel").Text);
Assert.All(grid.Rows.Cast<DataGridViewRow>(), r => Assert.StartsWith("Created N26-", (string)r.Cells["statusColumn"].Value));
Assert.False(Field<Button>(form, "releaseButton").Enabled);
Assert.True(Field<DataGridView>(form, "setGrid").ReadOnly);
@@ -99,7 +99,7 @@ public class SetReleaseFormsTests
Assert.True(refused.Cells["includeColumn"].ReadOnly);
Assert.Contains("named 'SYN-A'", (string)refused.Cells["statusColumn"].Value);
copper.Cells["includeColumn"].Value = false;
Assert.Contains("1 of 3 groups chosen", Field<Label>(form, "summaryLabel").Text);
Assert.Contains("1 of 3 materials chosen", Field<Label>(form, "summaryLabel").Text);
// Rebuilding the plan keeps the operator's choice.
Field<CheckBox>(form, "includeArchivedCheckBox").Checked = true;
Assert.Equal(false, grid.Rows.Cast<DataGridViewRow>()
@@ -187,14 +187,14 @@ public class SetReleaseFormsTests
Assert.Equal(1, opened);
Assert.Empty(sets.Releases);
Assert.Equal(("Record Release", true), (release.Text, release.Enabled));
Assert.Equal(("Try Again", true), (release.Text, release.Enabled));
Assert.Contains("Synthetic release post failure.", Field<Label>(form, "statusLabel").Text);
Run(form.RetryRecordAsync());
Assert.Equal(2, sets.PostAttempts);
Assert.Same(Assert.Single(sets.Releases), form.Result!.Release);
Assert.Equal(("Release", false), (release.Text, release.Enabled));
Assert.Equal(("Make Nests...", false), (release.Text, release.Enabled));
},
TestTimeout,
"The release retry test did not complete.");
@@ -286,12 +286,12 @@ public class SetReleaseFormsTests
openTabs.Clear();
Steel().Cells["targetColumn"].Value = mergeChoice;
Assert.Same(draft, form.ChosenMerges().Values.Single());
Assert.Contains("Merging discards the layout of 'N26-0007'", (string)Steel().Cells["statusColumn"].Value);
Assert.Contains("Replaces the layout of 'N26-0007'", (string)Steel().Cells["statusColumn"].Value);
Assert.Contains("1 new Draft nest, 1 merge, 6 parts", Field<Label>(form, "summaryLabel").Text);
Run(form.ReleaseAsync());
Assert.Contains("1 new and 1 merged into existing drafts", messages[^1]);
Assert.Contains("1 new, 1 merged into existing drafts", messages[^1]);
var release = Assert.Single(sets.Releases);
Assert.Equal(new[] { ("First kit", 1), ("Second kit", 2) }, release.Sets.Select(l => (l.SetName, l.Quantity)));
var merged = release.Nests.Single(n => n.Kind == SetReleaseNestKind.Merged);
@@ -364,15 +364,15 @@ public class SetReleaseFormsTests
var row = Field<DataGridView>(form, "groupGrid").Rows[0];
PumpUntil(() => ((DataGridViewComboBoxCell)row.Cells["targetColumn"]).Items.Count == 2, "the drafts to merge into");
row.Cells["targetColumn"].Value = ((DataGridViewComboBoxCell)row.Cells["targetColumn"]).Items[1];
Assert.Contains("and nests everything again", (string)row.Cells["statusColumn"].Value);
Assert.Contains("and nests again", (string)row.Cells["statusColumn"].Value);
Field<CheckBox>(form, "nestAfterReleaseCheckBox").Checked = false;
Assert.Contains("and leaves everything for Auto Nest", (string)row.Cells["statusColumn"].Value);
Assert.Contains("and leaves it for Auto Nest", (string)row.Cells["statusColumn"].Value);
Run(form.ReleaseAsync());
Assert.Equal(0, renested);
Assert.Contains("current layout, left for Auto Nest", Assert.Single(questions));
Assert.Contains("layout is cleared for Auto Nest", Assert.Single(questions));
Assert.DoesNotContain("nested with Auto Nest", questions[0]);
var merged = Assert.Single(form.Result!.Created);
Assert.Same(draft, merged.MergedInto);
@@ -538,7 +538,7 @@ public class SetReleaseFormsTests
Run(form.ReleaseAsync());
Assert.Contains("Release set 'Synthetic kit' and 3 drawings into 2 Draft nests", Assert.Single(questions));
Assert.Contains("Make 2 Draft nests from set 'Synthetic kit' and 3 drawings?", Assert.Single(questions));
var release = Assert.Single(sets.Releases);
Assert.Equal(new[] { 5, 40, 1 }, release.Drawings.Select(d => d.Quantity));
var steel = release.Nests.Single(n => n.MaterialName == "Synthetic Steel");
@@ -575,7 +575,7 @@ public class SetReleaseFormsTests
Assert.Empty(nests.Uploaded);
Assert.Empty(sets.Releases);
Assert.Equal("Nothing was saved. See the Status column.", Field<Label>(form, "statusLabel").Text);
Assert.Equal("Nothing saved. See Status.", Field<Label>(form, "statusLabel").Text);
Assert.Contains("does not support releasing library drawings on their own",
(string)Field<DataGridView>(form, "groupGrid").Rows[0].Cells["statusColumn"].Value);
},
+1 -1
View File
@@ -322,7 +322,7 @@ namespace OpenNest.Controls
releaseButton.Margin = new System.Windows.Forms.Padding(12, 3, 3, 3);
releaseButton.Name = "releaseButton";
releaseButton.TabIndex = 8;
releaseButton.Text = "Release...";
releaseButton.Text = "Make Nests...";
releaseButton.UseVisualStyleBackColor = true;
releaseButton.Visible = false;
releaseButton.Click += ReleaseButton_Click;
+2 -2
View File
@@ -804,8 +804,8 @@ namespace OpenNest.Forms
// releaseMenuItem
//
releaseMenuItem.Name = "releaseMenuItem";
releaseMenuItem.Text = "Release...";
releaseMenuItem.ToolTipText = "Release this drawing on its own, at the entered quantity, with any other drawings or sets";
releaseMenuItem.Text = "Make Nests...";
releaseMenuItem.ToolTipText = "Make nests of this drawing at the entered quantity, with any other drawings or sets";
releaseMenuItem.Visible = false;
releaseMenuItem.Click += ReleaseMenuItem_Click;
//
+2 -2
View File
@@ -369,7 +369,7 @@ namespace OpenNest.Forms
quantityLabel.AutoSize = true;
quantityLabel.Name = "quantityLabel";
quantityLabel.TabIndex = 0;
quantityLabel.Text = "Qty per kit:";
quantityLabel.Text = "Qty per set:";
//
// quantityBox
//
@@ -498,7 +498,7 @@ namespace OpenNest.Forms
quantityColumn.HeaderText = "Qty";
quantityColumn.Name = "quantityColumn";
quantityColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
quantityColumn.ToolTipText = "Quantity per kit; type to change it";
quantityColumn.ToolTipText = "Quantity per set; type to change it";
quantityColumn.ValueType = typeof(int);
quantityColumn.Width = 50;
//
+1 -1
View File
@@ -453,7 +453,7 @@ namespace OpenNest.Forms
var parts = session.Members.Sum(m => (long)m.Quantity);
summaryLabel.Text = session.Members.Count == 0
? "No drawings yet. Select drawings in the library on the left and choose Add to Set."
: $"{session.Members.Count:N0} drawing{(session.Members.Count == 1 ? "" : "s")}, {parts:N0} part{(parts == 1 ? "" : "s")} per kit";
: $"{session.Members.Count:N0} drawing{(session.Members.Count == 1 ? "" : "s")}, {parts:N0} part{(parts == 1 ? "" : "s")} per set";
}
private static string Warning(DrawingLibraryRecord drawing)
+3 -3
View File
@@ -229,10 +229,10 @@ namespace OpenNest.Forms
// kitsColumn
//
kitsColumn.DefaultCellStyle = rightAligned;
kitsColumn.HeaderText = "Kits / Qty";
kitsColumn.HeaderText = "Quantity";
kitsColumn.Name = "kitsColumn";
kitsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
kitsColumn.ToolTipText = "For a set, how many kits: every quantity in the set is multiplied by it. For a drawing, how many to release.";
kitsColumn.ToolTipText = "How many to make. For a set, every quantity in the set is multiplied by it.";
kitsColumn.Width = 80;
//
// optionsTable
@@ -451,7 +451,7 @@ namespace OpenNest.Forms
releaseButton.Enabled = false;
releaseButton.Name = "releaseButton";
releaseButton.TabIndex = 2;
releaseButton.Text = "Release";
releaseButton.Text = "Make Nests...";
releaseButton.UseVisualStyleBackColor = true;
releaseButton.Click += ReleaseButton_Click;
//
+19 -19
View File
@@ -166,7 +166,7 @@ namespace OpenNest.Forms
if (listed != null)
{
listed.Selected = true;
statusLabel.Text = $"'{set.Name}' is already listed. Change its kits instead.";
statusLabel.Text = $"'{set.Name}' is already listed. Change its quantity instead.";
return false;
}
if (set.Archived)
@@ -280,7 +280,7 @@ namespace OpenNest.Forms
var valid = int.TryParse(Convert.ToString(e.FormattedValue, CultureInfo.CurrentCulture), NumberStyles.Integer,
CultureInfo.CurrentCulture, out var kits) && kits >= 1 && kits <= maximum;
setGrid.Rows[e.RowIndex].ErrorText = valid ? ""
: $"{(drawing ? "The quantity" : "Kits")} must be a whole number from 1 to {maximum:N0}.";
: $"Enter a whole number from 1 to {maximum:N0}.";
e.Cancel = !valid;
}
@@ -693,13 +693,13 @@ namespace OpenNest.Forms
}
var created = chosen.Count - merges.Count;
var text = $"Release {DescribeLines()} into {chosen.Count} Draft nest{(chosen.Count == 1 ? "" : "s")}";
var text = $"Make {chosen.Count} Draft nest{(chosen.Count == 1 ? "" : "s")} from {DescribeLines()}?";
var nestAfter = nestAfterReleaseCheckBox.Checked;
if (merges.Count > 0)
text += $": {created} new and {merges.Count} merged into existing drafts. A merged draft loses its "
+ (nestAfter ? "current layout and is nested again" : "current layout, left for Auto Nest")
+ "; Undo Release restores it";
text += nestAfter ? ". Each is nested with Auto Nest and opens in its own tab." : ". Each opens in its own tab.";
text += $" {created} new, {merges.Count} merged into existing drafts. A merged draft's layout is "
+ (nestAfter ? "replaced" : "cleared for Auto Nest")
+ "; Undo Release restores it.";
text += nestAfter ? " Each is nested with Auto Nest and opens in a tab." : " Each opens in a tab.";
if (ShowMessage(text, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
return;
@@ -797,7 +797,7 @@ namespace OpenNest.Forms
$"{skipped.Count} archived drawing{(skipped.Count == 1 ? " is" : "s are")} left out: "
+ string.Join(", ", skipped.Take(5).Select(m => $"'{m.Drawing!.Name}'"))
+ (skipped.Count > 5 ? $" and {skipped.Count - 5} more" : "")
+ ". Tick Include archived drawings to release them.";
+ ". Tick Include archived drawings to add them.";
UpdateCommands();
_ = LoadDraftsAsync();
}
@@ -881,9 +881,9 @@ namespace OpenNest.Forms
return group.Refusal;
var notes = group.Warnings.ToList();
if (targets.TryGetValue(group.Key, out var draft))
notes.Insert(0, $"Merging discards the layout of '{draft.Name}' ({draft.PlateCount} sheet"
+ $"{(draft.PlateCount == 1 ? "" : "s")}) and "
+ (nestAfterReleaseCheckBox.Checked ? "nests everything again" : "leaves everything for Auto Nest")
notes.Insert(0, $"Replaces the layout of '{draft.Name}' ({draft.PlateCount} sheet"
+ $"{(draft.PlateCount == 1 ? "" : "s")}) "
+ (nestAfterReleaseCheckBox.Checked ? "and nests again" : "and leaves it for Auto Nest")
+ "; Undo Release restores it.");
return string.Join(" ", notes);
}
@@ -927,15 +927,15 @@ namespace OpenNest.Forms
var done = Result.Created.Count;
var nestsText = $"{done} nest{(done == 1 ? "" : "s")}";
if (Result.Release != null)
statusLabel.Text = $"Released: {nestsText} saved and opened, and the release recorded.";
statusLabel.Text = $"Done: {nestsText} saved and opened.";
else if (Result.ReleaseError.Length > 0)
statusLabel.Text = Result.ReleaseError + " Choose Record Release to try again.";
statusLabel.Text = Result.ReleaseError + " Choose Try Again.";
else if (Result.Canceled)
statusLabel.Text = $"Canceled: {nestsText} saved; no release recorded.";
statusLabel.Text = $"Canceled: {nestsText} saved, release not recorded.";
else
statusLabel.Text = done == 0
? "Nothing was saved. See the Status column."
: $"Stopped: {nestsText} saved; no release recorded. See the Status column.";
? "Nothing saved. See Status."
: $"Stopped: {nestsText} saved, release not recorded. See Status.";
UpdateCommands();
}
@@ -966,13 +966,13 @@ namespace OpenNest.Forms
fixDrawingsButton.Enabled = CanEditDrawings() && groupGrid.CurrentRow?.Tag is SetReleaseGroup { CanRelease: false };
var retry = Result is { Release: null, ReleaseError.Length: > 0 };
releaseButton.Text = retry ? "Record Release" : "Release";
releaseButton.Text = retry ? "Try Again" : "Make Nests...";
releaseButton.Enabled = !running && (retry || (editable && chosen.Count > 0));
var parts = chosen.Sum(g => (long)g.TotalQuantity);
var merged = chosen.Count(g => targets.ContainsKey(g.Key));
summaryLabel.Text = plan == null || plan.Groups.Count == 0
? "Nothing listed can be released."
: $"{chosen.Count} of {plan.Groups.Count} group{(plan.Groups.Count == 1 ? "" : "s")} chosen: "
? "No nests to make."
: $"{chosen.Count} of {plan.Groups.Count} material{(plan.Groups.Count == 1 ? "" : "s")} chosen: "
+ $"{chosen.Count - merged} new Draft nest{(chosen.Count - merged == 1 ? "" : "s")}"
+ (merged == 0 ? "" : $", {merged} merge{(merged == 1 ? "" : "s")}")
+ $", {parts:N0} part{(parts == 1 ? "" : "s")}";
+6 -5
View File
@@ -86,7 +86,8 @@ namespace OpenNest.Forms
//
rightAligned.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
revisionColumn.DefaultCellStyle = rightAligned;
revisionColumn.HeaderText = "Set revision";
revisionColumn.HeaderText = "Revision";
revisionColumn.ToolTipText = "Which saved revision of the set was released; see the set's History.";
revisionColumn.Name = "revisionColumn";
revisionColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
revisionColumn.Width = 70;
@@ -107,7 +108,7 @@ namespace OpenNest.Forms
//
// multiplierColumn
//
multiplierColumn.HeaderText = "Kits";
multiplierColumn.HeaderText = "Quantity";
multiplierColumn.Name = "multiplierColumn";
multiplierColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
multiplierColumn.Width = 50;
@@ -168,17 +169,17 @@ namespace OpenNest.Forms
//
// groupColumn
//
groupColumn.HeaderText = "Material group";
groupColumn.HeaderText = "Material";
groupColumn.Name = "groupColumn";
groupColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
groupColumn.Width = 260;
//
// changeColumn
//
changeColumn.HeaderText = "Release did";
changeColumn.HeaderText = "Action";
changeColumn.Name = "changeColumn";
changeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
changeColumn.ToolTipText = "Whether the release created the nest or merged into it, and what undoing it did.";
changeColumn.ToolTipText = "Whether the release created this nest or merged into it, and what Undo did.";
changeColumn.Width = 190;
//
// statusColumn
+21 -21
View File
@@ -414,7 +414,7 @@ a set shows its notes.
The editor's left pane browses the library in place, with no second dialog:
every active drawing (never filtered to a nest), searched, sorted and paged
like the Drawing Library dialog. Select one or several and choose **Add to Set**,
double-click or press Enter to add them at the entered quantity per kit.
double-click or press Enter to add them at the entered **Qty per set**.
Drawings already in the set are greyed; one picked again is skipped and
named, so there is one member per drawing. Quantities are edited
in the grid (1 to 100,000); **Remove**, **Move Up** and **Move Down** change
@@ -437,7 +437,7 @@ whose current revision contains it) or **Add to Set...**: choose an active set
or **New Set**, and the set editor opens with the drawing added at the entered
quantity for review and Save. **Releases...** lists the releases that had the
drawing on its own (see below), with Undo Release. When the dialog was opened
from a nest, **Release...** reads the drawing's current record and opens the
from a nest, **Make Nests...** reads the drawing's current record and opens the
release dialog with it on its own at the entered quantity; more drawings and
sets are added there.
@@ -450,12 +450,12 @@ named; cutoffs are left out. If none remain it says so before anything else.
Otherwise choose an active set or **New Set** (named after the nest, with its
customer): the set's current library records are read, drawings the set already
lists **keep the set's quantity** (a released nest carries its multiplier, so
copying its quantities back would scale the kit), and members are added at the
copying its quantities back would multiply them again), and members are added at the
library's latest revision. Anything kept, skipped, archived in the library or
newer there than in the nest is listed first, then the set editor opens for
review and Save; nothing is stored before then.
**Release...** (in the drawing sets window) reads the
**Make Nests...** (in the drawing sets window) reads the
selected active set with its members and opens the release dialog with it listed.
More sets are added from **Add a set**: the list opens with the first active sets
by name and narrows as you type; Enter or **Add** takes the picked set, or the one
@@ -465,17 +465,17 @@ library pane does (search, server-side sort, pages, several selected at once),
and **Add to Release**, a double-click or Enter adds the selection at the
entered quantity. It stays open for more; a drawing already listed is greyed
and skipped, and an archived one is refused. **Remove** takes a set or drawing
out while another remains. The list shows each line's type; **Kits / Qty** is a
set's kits (1 to 1000, multiplying its quantities) or a drawing's quantity (1 to
out while another remains. The list shows each line's type; **Quantity** is how
many of a set (1 to 1000, multiplying its quantities) or of a drawing (1 to
100,000), and a set or drawing is listed once. A drawing that a set also lists
becomes one item with both quantities. The grid below has one row per material
group across all the sets and drawings
(name, grade, thickness, units and cut type) across all the sets and drawings
(`OpenNest.Data.SetReleasePlan`): material, grade, thickness, units, cut type,
drawings, parts, **Nest** and status. **Include archived drawings** puts archived
members back in, otherwise they are named below the grid as left out. Each group
has a **Release** tick; refused groups (two drawings with one name, or drawings
without material or thickness) show why and cannot be ticked, and a tick
survives changing the sets, kits or the archived toggle. **Fix Drawings...**
survives changing the sets, quantities or the archived toggle. **Fix Drawings...**
opens [Edit Drawing](#edit-drawing-desktop) on a refused group's drawings and
rebuilds the plan with what was saved.
@@ -497,7 +497,7 @@ Auto Nest". The status column lists each group's sheets and utilization, and any
copies left unplaced, which stay as remaining quantity. Unticked, new nests are
saved without sheets and merged drafts are left cleared for Auto Nest.
**Release** confirms, then runs `OpenNest.Data.SetReleaseSession`: every ticked
**Make Nests...** confirms, then runs `OpenNest.Data.SetReleaseSession`: every ticked
group's new nest is made as File > New would (the server's next nest name, the
saved plate defaults and clamps), in the group's units, with the saved plate size
and spacings converted (25.4 mm per inch) when those differ; the nest takes the
@@ -512,30 +512,30 @@ grid then shows every group's outcome: Created or Merged into with
the nest name, Refused or Failed with the reason, or Not started. Cancel during
preparing or nesting saves nothing; once saving starts it stops between saves,
never during one. When every nest was saved but recording failed,
the button becomes **Record Release** and posts the same release again. A dialog
the button becomes **Try Again** and posts the same release again. A dialog
runs one release.
**Releases...** lists the releases that include the selected set, newest first:
when, by whom, the set revision, the kits of this set (and how many other sets
when, by whom, the set's **Revision**, the **Quantity** of this set (and how many other sets
and drawings the release had), the number of nests, progress such as "2 of 3 cut" (deleted
nests are not counted) or "Undone", and the notes. The selected release's nests
show their material group, what the release did ("Created", "Merged (revision
show their **Material**, the **Action** ("Created", "Merged (revision
3 → 4)", and what undo did) and current status, or Deleted. **Undo Release...**
deletes the nests the release created (they can be recovered) and restores the
drafts it merged into. When some nests were changed after the release, it lists
them and offers to undo the others, leaving those as they are.
**File > Releases...** lists every release on the server the same way, newest
first, with a **Released** column naming what each one had ("Kit A ×2, Kit B;
Bracket ×4": sets with their kits past one, then drawings on their own with
their quantities) in place of the set revision and kits, and Undo Release.
first, with a **Released** column naming what each one had ("Frame A ×2, Frame B;
Bracket ×4": sets with their quantity past one, then drawings on their own with
their quantities) in place of the revision and quantity, and Undo Release.
**New Release...** opens the release dialog empty; sets are added by name and
drawings with Add Drawings, and once the dialog records a release the list
re-reads from the newest. Like Drawing Sets, it is enabled in database mode
only.
**Nest > Edit** (Nest Info) shows, beside its buttons, the newest release that
made or merged into a server nest ("Released from set 'Kit A' revision 3 ×2 on
made or merged into a server nest ("Released from set 'Frame A' revision 3 ×2 on
10/20/2026 by ...", "Released from drawing 'Bracket' ×4", or "Merged from sets
..."), with a count of earlier ones. It
is read in the background and left blank when the nest came from none or the
@@ -544,7 +544,7 @@ server cannot be reached.
## Drawing set endpoints
A **drawing set** (schema v7) is a named, versioned list of library drawings
with a quantity each: a kit, an assembly or a recurring order. Members name
with a quantity each: an assembly, a job or a recurring order. Members name
drawings by id and always mean each drawing's **current** revision; there is no
per-member revision pin. Sets travel as JSON (`application/json`, at most 1 MiB).
The desktop browses, edits and releases them in File > Drawing Sets
@@ -601,7 +601,7 @@ Since schema v9 a release covers one or more sets (`release_sets`), and a nest
may be one the release **created** or an existing draft it **merged** into
(`release_nests`, with the nest revision before and after). The drawing
quantities each set added to each nest are kept in `release_nest_items`, so a
kit can still be traced once kits share a nest. The v8 → v9 migration turns each
set can still be traced once sets share a nest. The v8 → v9 migration turns each
v7 release into one set line (its multiplier is the quantity) and created nests
at their first revision, with no items. Since schema v11 a release may also
name library drawings **on their own** (`release_drawings`), with or without
@@ -674,7 +674,7 @@ refused, and refuses replies whose id, revision, members, sets or nests differ
from what was sent.
`SetReleasePlan.Build(lines, drawings, includeArchived)` splits sets read from
the server, each with a number of kits (`SetReleasePlanLine`), and library
the server, each with a quantity (`SetReleasePlanLine`), and library
drawings on their own, each with a quantity (`SetReleaseDrawingLine`), into
groups without any I/O; either list may be empty, not both (`Build(lines,
includeArchived)` and `Build(set, multiplier, includeArchived)` are the
@@ -685,7 +685,7 @@ read with. A drawing listed twice on its own is refused (give it a larger
quantity). A group is the items whose drawings share material name and grade (by
`SharedListNames.Key`), thickness (rounded to 4 decimals), units and cut type.
The plan's customer is the one every set and drawing on its own names, or blank. A set listed twice is
refused (give it more kits instead). Archived drawings are skipped unless included. A group is refused when two of its drawings share a
refused (give it a larger quantity instead). Archived drawings are skipped unless included. A group is refused when two of its drawings share a
name (compared ordinally, as a nest keys drawings), and every drawing without a
material or a positive thickness goes to one refused "unassigned" group, listed last.
@@ -724,7 +724,7 @@ it, and nothing is built or saved.
otherwise says whether someone saved the draft meanwhile. The first real
failure stops the rest.
3. **Post the release** only when every chosen group was created or merged,
under a client-allocated id. It names every set with its kits, every drawing
under a client-allocated id. It names every set with its quantity, every drawing
on its own with its quantity, each nest as created or merged with the
revisions before and after, and the drawing revisions and quantities each set
(or no set, for a drawing on its own) put in each nest. When the post fails, the nests stay and