mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 02:41:06 -04:00
feat(desktop): release drawing sets into draft nests and track them
A set is only useful on the floor once it becomes nests. Release on the
Sets tab previews one row per material group with a kit multiplier, an
archived-drawing toggle and a tick per group, then builds each ticked
group's nest as File > New would, in the group's units, uploads it as a
Draft, opens it in its own tab bound to the server record and records the
release. Every group's outcome stays on screen, and a release whose
nests exist but whose record failed can be recorded again.
Releases shows each release's nests and how far they have got ("2 of 3
cut"), and Nest Info names the release a nest came from, so a kit run can
be followed across its nests. Plate defaults convert between inches and
millimeters so a group drawn in other units still gets sensible plates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ccba9b910
commit
cc731ff3d6
22 files changed
+1915
-21
No files matched your search
@@ -32,5 +32,15 @@ namespace OpenNest
|
||||
NestStatus.ToBeCut,
|
||||
NestStatus.HasBeenCut,
|
||||
};
|
||||
|
||||
/// <summary>The name shown to the operator, such as "To Be Cut".</summary>
|
||||
public static string DisplayName(NestStatus status) => status switch
|
||||
{
|
||||
NestStatus.Draft => "Draft",
|
||||
NestStatus.Quote => "Quote",
|
||||
NestStatus.ToBeCut => "To Be Cut",
|
||||
NestStatus.HasBeenCut => "Has Been Cut",
|
||||
_ => status.ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,28 @@ public sealed class NestDefaults
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// These defaults in <paramref name="units"/>: plate size and spacings are converted
|
||||
/// (25.4 mm per inch) and rounded to four decimals; the quadrant is kept. Returns a copy
|
||||
/// even when the units already match.
|
||||
/// </summary>
|
||||
public NestDefaults ConvertedTo(Units units)
|
||||
{
|
||||
var factor = units == Units ? 1.0 : units == Units.Millimeters ? MillimetersPerInch : 1 / MillimetersPerInch;
|
||||
double Convert(double value) => System.Math.Round(value * factor, 4, MidpointRounding.AwayFromZero);
|
||||
return new NestDefaults
|
||||
{
|
||||
Units = units,
|
||||
Size = new Size(Convert(Size.Width), Convert(Size.Length)),
|
||||
Quadrant = Quadrant,
|
||||
PartSpacing = Convert(PartSpacing),
|
||||
EdgeSpacing = new Spacing(Convert(EdgeSpacing.Left), Convert(EdgeSpacing.Bottom),
|
||||
Convert(EdgeSpacing.Right), Convert(EdgeSpacing.Top)),
|
||||
};
|
||||
}
|
||||
|
||||
private const double MillimetersPerInch = 25.4;
|
||||
|
||||
public void ApplyTo(Nest nest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
|
||||
@@ -108,6 +108,10 @@ public sealed class SetReleaseResult
|
||||
/// <see cref="SetReleaseSession.RetryReleaseAsync"/> posts the same release again.
|
||||
/// </summary>
|
||||
public string ReleaseError { get; }
|
||||
|
||||
/// <summary>This result once <see cref="SetReleaseSession.RetryReleaseAsync"/> has recorded <paramref name="release"/>.</summary>
|
||||
public SetReleaseResult WithRelease(SetReleaseRecord release) =>
|
||||
new(Groups, Canceled, release ?? throw new ArgumentNullException(nameof(release)), "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Globalization;
|
||||
using OpenNest;
|
||||
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>Operator-facing wording for set releases, shared by the release views and Nest Info.</summary>
|
||||
public static class SetReleaseText
|
||||
{
|
||||
/// <summary>Where a nest came from, such as "Released from set 'Kit A' revision 3 ×2 by J. Smith".</summary>
|
||||
public static string Origin(SetReleaseRecord release)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(release);
|
||||
var text = $"Released from set '{release.SetName}' revision {release.SetRevision} ×{release.Multiplier}";
|
||||
if (release.ReleasedAt != default)
|
||||
text += $" on {release.ReleasedAt.ToLocalTime().ToString("d", CultureInfo.CurrentCulture)}";
|
||||
return string.IsNullOrWhiteSpace(release.ReleasedBy) ? text : $"{text} by {release.ReleasedBy}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How far the release's nests have got, such as "2 of 3 cut" or "1 of 2 cut, 1 deleted".
|
||||
/// Deleted nests are left out of the count.
|
||||
/// </summary>
|
||||
public static string Progress(SetReleaseRecord release)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(release);
|
||||
var nests = release.Nests ?? Array.Empty<SetReleaseNest>();
|
||||
var deleted = nests.Count(IsDeleted);
|
||||
var active = nests.Count - deleted;
|
||||
if (active == 0)
|
||||
return nests.Count == 1 ? "Its nest was deleted" : "All nests deleted";
|
||||
|
||||
var cut = nests.Count(n => !IsDeleted(n) && n.Nest?.Status == NestStatus.HasBeenCut);
|
||||
var text = $"{cut} of {active} cut";
|
||||
return deleted == 0 ? text : $"{text}, {deleted} deleted";
|
||||
}
|
||||
|
||||
/// <summary>The state of one nest of a release: its status name, or "Deleted".</summary>
|
||||
public static string NestState(SetReleaseNest nest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
if (IsDeleted(nest))
|
||||
return "Deleted";
|
||||
return nest.Nest is { } record ? NestStatuses.DisplayName(record.Status) : "";
|
||||
}
|
||||
|
||||
/// <summary>The material group a release nest was made for, such as "Carbon Steel A36 0.25 in laser".</summary>
|
||||
public static string Group(SetReleaseNest nest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
var parts = new[]
|
||||
{
|
||||
nest.MaterialName,
|
||||
nest.MaterialGrade,
|
||||
$"{nest.Thickness.ToString("0.####", CultureInfo.CurrentCulture)} {UnitsHelper.GetShortString(nest.Units)}",
|
||||
nest.CutType.ToString().ToLowerInvariant(),
|
||||
};
|
||||
return string.Join(" ", parts.Where(p => !string.IsNullOrWhiteSpace(p)).Select(p => p.Trim()));
|
||||
}
|
||||
|
||||
// A nest the server no longer joins is as gone as a deleted one.
|
||||
private static bool IsDeleted(SetReleaseNest nest) => nest.Nest is null || nest.Nest.Deleted;
|
||||
}
|
||||
@@ -111,14 +111,54 @@ internal sealed class InMemoryDrawingSets : IDrawingSetStore
|
||||
});
|
||||
}
|
||||
|
||||
public Task<SetReleaseRecord> PostReleaseAsync(Guid setId, SetReleaseRecord release, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
/// <summary>Stored releases, oldest first; tests may add ones with joined nest metadata.</summary>
|
||||
public List<SetReleaseRecord> Releases { get; } = new();
|
||||
public int PostAttempts { get; private set; }
|
||||
public bool FailNextPost { get; set; }
|
||||
|
||||
public Task<SetReleasePage?> GetReleasesAsync(Guid setId, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
public Task<SetReleaseRecord> PostReleaseAsync(Guid setId, SetReleaseRecord release, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PostAttempts++;
|
||||
if (FailNextPost)
|
||||
{
|
||||
FailNextPost = false;
|
||||
throw new IOException("Synthetic release post failure.");
|
||||
}
|
||||
if (Releases.FirstOrDefault(r => r.Id == release.Id) is { } existing)
|
||||
throw new SetReleasePostConflictException(existing);
|
||||
|
||||
var stored = new SetReleaseRecord
|
||||
{
|
||||
Id = release.Id,
|
||||
SetId = setId,
|
||||
SetRevision = release.SetRevision,
|
||||
SetName = _revisions[setId].First(r => r.Revision == release.SetRevision).Name,
|
||||
Multiplier = release.Multiplier,
|
||||
Notes = release.Notes,
|
||||
ReleasedBy = release.ReleasedBy,
|
||||
ReleasedAt = new DateTime(2026, 10, 20, 15, 0, 0, DateTimeKind.Utc),
|
||||
Nests = release.Nests.ToList(),
|
||||
};
|
||||
Releases.Add(stored);
|
||||
return Task.FromResult(stored);
|
||||
}
|
||||
|
||||
public Task<SetReleasePage?> GetReleasesAsync(Guid setId, int offset = 0, int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_revisions.ContainsKey(setId))
|
||||
return Task.FromResult<SetReleasePage?>(null);
|
||||
var newest = Enumerable.Reverse(Releases.Where(r => r.SetId == setId).ToList()).ToList();
|
||||
return Task.FromResult<SetReleasePage?>(new SetReleasePage
|
||||
{
|
||||
Items = newest.Skip(offset).Take(limit).ToList(),
|
||||
Total = newest.Count,
|
||||
Offset = offset,
|
||||
Limit = limit,
|
||||
});
|
||||
}
|
||||
|
||||
public Task<SetReleaseRecord?> GetReleaseForNestAsync(Guid nestId, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
Task.FromResult(Releases.FirstOrDefault(r => r.Nests.Any(n => n.NestId == nestId)));
|
||||
|
||||
private Task<DrawingSetRecord> SetArchived(Guid id, int expectedRevision, bool archived)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
public class SetReleaseTextTests
|
||||
{
|
||||
private static SetReleaseNest Nest(NestStatus status, bool deleted = false, bool joined = true) => new()
|
||||
{
|
||||
NestId = Guid.NewGuid(),
|
||||
MaterialName = "Synthetic Steel",
|
||||
MaterialGrade = "Grade A",
|
||||
Thickness = 0.25,
|
||||
Units = Units.Inches,
|
||||
CutType = CutType.Waterjet,
|
||||
Nest = joined ? new NestRecord { Name = "N1", Status = status, Deleted = deleted } : null,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Progress_CountsCutNestsAndLeavesDeletedOnesOut()
|
||||
{
|
||||
Assert.Equal("2 of 3 cut", SetReleaseText.Progress(new SetReleaseRecord
|
||||
{
|
||||
Nests = new[] { Nest(NestStatus.HasBeenCut), Nest(NestStatus.HasBeenCut), Nest(NestStatus.Draft) },
|
||||
}));
|
||||
Assert.Equal("0 of 1 cut, 2 deleted", SetReleaseText.Progress(new SetReleaseRecord
|
||||
{
|
||||
Nests = new[] { Nest(NestStatus.ToBeCut), Nest(NestStatus.HasBeenCut, deleted: true), Nest(NestStatus.Quote, joined: false) },
|
||||
}));
|
||||
Assert.Equal("All nests deleted", SetReleaseText.Progress(new SetReleaseRecord
|
||||
{
|
||||
Nests = new[] { Nest(NestStatus.Draft, deleted: true), Nest(NestStatus.Draft, deleted: true) },
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestState_AndGroup_DescribeOneNest()
|
||||
{
|
||||
Assert.Equal("To Be Cut", SetReleaseText.NestState(Nest(NestStatus.ToBeCut)));
|
||||
Assert.Equal("Deleted", SetReleaseText.NestState(Nest(NestStatus.ToBeCut, deleted: true)));
|
||||
Assert.Equal("Synthetic Steel Grade A 0.25 in waterjet", SetReleaseText.Group(Nest(NestStatus.Draft)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Origin_NamesTheSetRevisionMultiplierAndWho()
|
||||
{
|
||||
var text = SetReleaseText.Origin(new SetReleaseRecord
|
||||
{
|
||||
SetName = "Synthetic kit",
|
||||
SetRevision = 3,
|
||||
Multiplier = 2,
|
||||
ReleasedBy = "Synthetic operator",
|
||||
});
|
||||
|
||||
Assert.Equal("Released from set 'Synthetic kit' revision 3 ×2 by Synthetic operator", text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestDefaults_ConvertedTo_ScalesPlateDefaultsBetweenUnits()
|
||||
{
|
||||
var inches = new NestDefaults
|
||||
{
|
||||
Units = Units.Inches,
|
||||
Size = new Size(60, 120),
|
||||
Quadrant = 3,
|
||||
PartSpacing = 0.125,
|
||||
EdgeSpacing = new Spacing(0.5, 0.25, 0.5, 0.25),
|
||||
};
|
||||
|
||||
var millimeters = inches.ConvertedTo(Units.Millimeters);
|
||||
var back = millimeters.ConvertedTo(Units.Inches);
|
||||
|
||||
Assert.Equal((Units.Millimeters, 1524.0, 3048.0, 3, 3.175), (millimeters.Units, millimeters.Size.Width,
|
||||
millimeters.Size.Length, millimeters.Quadrant, millimeters.PartSpacing));
|
||||
Assert.Equal((12.7, 6.35, 12.7, 6.35), (millimeters.EdgeSpacing.Left, millimeters.EdgeSpacing.Bottom,
|
||||
millimeters.EdgeSpacing.Right, millimeters.EdgeSpacing.Top));
|
||||
Assert.Equal((60.0, 120.0, 0.125), (back.Size.Width, back.Size.Length, back.PartSpacing));
|
||||
Assert.NotSame(inches, inches.ConvertedTo(Units.Inches));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Shapes;
|
||||
using OpenNest.Tests.Data;
|
||||
using DrawingSetBrowser = OpenNest.Controls.DrawingSetBrowser;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class SetReleaseFormsTests
|
||||
{
|
||||
private const string Server = "http://library.test:8090";
|
||||
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
|
||||
|
||||
[Fact]
|
||||
public void Release_CreatesOneDraftNestPerTickedGroup_OpensEach_AndRecordsTheRelease()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var b = library.Add("SYN-B", "Synthetic Steel");
|
||||
var c = library.Add("SYN-C", "Synthetic Aluminum");
|
||||
var old = library.Add("SYN-OLD", "Synthetic Steel", archived: true);
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets, (a, 2), (b, 1), (c, 4), (old, 1));
|
||||
var nests = new MemoryNests();
|
||||
var opened = new List<(Nest Nest, NestRecord Record)>();
|
||||
var questions = new List<string>();
|
||||
|
||||
using var form = new ReleaseSetForm(sets, library, nests, Server, set, (nest, record) => opened.Add((nest, record)));
|
||||
form.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.OK; };
|
||||
var created = 0;
|
||||
form.CreateNest = (group, _) => Task.FromResult(new Nest($"N26-{++created:D4}") { Units = group.Units });
|
||||
form.Show();
|
||||
var grid = Field<DataGridView>(form, "groupGrid");
|
||||
var skipped = Field<Label>(form, "skippedLabel");
|
||||
|
||||
Assert.Equal(2, grid.Rows.Count);
|
||||
Assert.True(skipped.Visible);
|
||||
Assert.Contains("'SYN-OLD'", skipped.Text);
|
||||
Field<NumericUpDown>(form, "multiplierBox").Value = 3;
|
||||
Assert.Equal(new object[] { 12, 9 }, grid.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["partsColumn"].Value));
|
||||
Field<TextBox>(form, "notesBox").Text = "Synthetic run";
|
||||
|
||||
Run(form.ReleaseAsync());
|
||||
|
||||
Assert.Contains("Create 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));
|
||||
var steel = opened.Single(o => o.Nest.Material.Name == "Synthetic Steel");
|
||||
Assert.Equal(new[] { ("SYN-A", 6), ("SYN-B", 3) },
|
||||
steel.Nest.Drawings.OrderBy(d => d.Name).Select(d => (d.Name, d.Quantity.Required)));
|
||||
Assert.Equal(steel.Record.Id, nests.Uploaded.Single(u => u.Name == steel.Nest.Name).Id);
|
||||
|
||||
var release = Assert.Single(sets.Releases);
|
||||
Assert.Equal((3, "Synthetic run", 2), (release.Multiplier, release.Notes, release.Nests.Count));
|
||||
Assert.Same(release, form.Result!.Release);
|
||||
Assert.StartsWith("Released: 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.False(Field<NumericUpDown>(form, "multiplierBox").Enabled);
|
||||
},
|
||||
TestTimeout,
|
||||
"The release test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UntickedAndRefusedGroups_AreNotReleased()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var twin = library.Add("SYN-A", "Synthetic Steel");
|
||||
var c = library.Add("SYN-C", "Synthetic Aluminum");
|
||||
var d = library.Add("SYN-D", "Synthetic Copper");
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets, (a, 1), (twin, 1), (c, 1), (d, 1));
|
||||
var nests = new MemoryNests();
|
||||
|
||||
using var form = new ReleaseSetForm(sets, library, nests, Server, set, (_, _) => { });
|
||||
form.ShowMessage = (_, _, _) => DialogResult.OK;
|
||||
form.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
|
||||
form.Show();
|
||||
var grid = Field<DataGridView>(form, "groupGrid");
|
||||
var rows = grid.Rows.Cast<DataGridViewRow>().ToList();
|
||||
var refused = rows.Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Steel");
|
||||
var copper = rows.Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Copper");
|
||||
|
||||
Assert.Equal(false, refused.Cells["includeColumn"].Value);
|
||||
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);
|
||||
// Rebuilding the plan keeps the operator's choice.
|
||||
Field<CheckBox>(form, "includeArchivedCheckBox").Checked = true;
|
||||
Assert.Equal(false, grid.Rows.Cast<DataGridViewRow>()
|
||||
.Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Copper").Cells["includeColumn"].Value);
|
||||
|
||||
Run(form.ReleaseAsync());
|
||||
|
||||
Assert.Equal("Synthetic Aluminum", Assert.Single(Assert.Single(sets.Releases).Nests).MaterialName);
|
||||
Assert.Single(nests.Uploaded);
|
||||
},
|
||||
TestTimeout,
|
||||
"The release choice test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleaseNotRecorded_CanBeRecordedAgain()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets, (a, 1));
|
||||
sets.FailNextPost = true;
|
||||
var opened = 0;
|
||||
|
||||
using var form = new ReleaseSetForm(sets, library, new MemoryNests(), Server, set, (_, _) => opened++);
|
||||
form.ShowMessage = (_, _, _) => DialogResult.OK;
|
||||
form.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
|
||||
form.Show();
|
||||
var release = Field<Button>(form, "releaseButton");
|
||||
|
||||
Run(form.ReleaseAsync());
|
||||
|
||||
Assert.Equal(1, opened);
|
||||
Assert.Empty(sets.Releases);
|
||||
Assert.Equal(("Record Release", 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));
|
||||
},
|
||||
TestTimeout,
|
||||
"The release retry test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Releases_ListsEachReleaseWithProgress_AndItsNestsStates()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets);
|
||||
SetReleaseNest Nest(string name, NestStatus status, bool deleted = false) => new()
|
||||
{
|
||||
NestId = Guid.NewGuid(),
|
||||
MaterialName = "Synthetic Steel",
|
||||
Thickness = 0.25,
|
||||
Nest = new NestRecord { Name = name, Status = status, Deleted = deleted },
|
||||
};
|
||||
sets.Releases.Add(new SetReleaseRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SetId = set.Id,
|
||||
SetRevision = 1,
|
||||
SetName = set.Name,
|
||||
Multiplier = 2,
|
||||
ReleasedBy = "Synthetic operator",
|
||||
ReleasedAt = new DateTime(2026, 10, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
Nests = new[] { Nest("N1", NestStatus.HasBeenCut), Nest("N2", NestStatus.ToBeCut), Nest("N3", NestStatus.Draft, deleted: true) },
|
||||
});
|
||||
|
||||
using var form = new SetReleasesForm(sets, set);
|
||||
form.Show();
|
||||
var releases = Field<DataGridView>(form, "releaseGrid");
|
||||
var nests = Field<DataGridView>(form, "nestGrid");
|
||||
PumpUntil(() => releases.Rows.Count == 1 && nests.Rows.Count == 3, "the releases");
|
||||
|
||||
Assert.Equal("1 of 2 cut, 1 deleted", releases.Rows[0].Cells["progressColumn"].Value);
|
||||
Assert.Equal("×2", releases.Rows[0].Cells["multiplierColumn"].Value);
|
||||
Assert.Equal(new object[] { "Has Been Cut", "To Be Cut", "Deleted" },
|
||||
nests.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["statusColumn"].Value));
|
||||
},
|
||||
TestTimeout,
|
||||
"The releases view test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Browser_Release_ReadsTheSetWithItsMembers_OnlyWhenEnabled()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var sets = new InMemoryDrawingSets();
|
||||
CreateSet(sets, (a, 2));
|
||||
var released = new List<DrawingSetRecord>();
|
||||
|
||||
using var host = new Form();
|
||||
var browser = new DrawingSetBrowser { Dock = DockStyle.Fill };
|
||||
host.Controls.Add(browser);
|
||||
browser.Initialize(sets, library, Server);
|
||||
browser.ShowRelease = released.Add;
|
||||
host.Show();
|
||||
Run(browser.RefreshAsync());
|
||||
var releaseButton = Field<Button>(browser, "releaseButton");
|
||||
Assert.False(releaseButton.Visible);
|
||||
|
||||
browser.EnableRelease(new MemoryNests(), (_, _) => { });
|
||||
Assert.True(releaseButton.Visible);
|
||||
Assert.True(releaseButton.Enabled);
|
||||
Run(browser.ReleaseSelectedAsync());
|
||||
|
||||
var set = Assert.Single(released);
|
||||
Assert.Equal("SYN-A", Assert.Single(set.Members).Drawing!.Name);
|
||||
},
|
||||
TestTimeout,
|
||||
"The browser release test did not complete.");
|
||||
}
|
||||
|
||||
private static DrawingSetRecord CreateSet(InMemoryDrawingSets sets, params (DrawingLibraryRecord Drawing, int Quantity)[] members)
|
||||
{
|
||||
var task = sets.CreateAsync(new DrawingSetRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Synthetic kit",
|
||||
Customer = "Synthetic customer",
|
||||
Members = members.Select(m => new DrawingSetMember { DrawingId = m.Drawing.Id, Quantity = m.Quantity, Drawing = m.Drawing }).ToList(),
|
||||
});
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the operation to finish");
|
||||
|
||||
private static void PumpUntil(Func<bool> condition, string what)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddSeconds(15);
|
||||
while (!condition() && DateTime.UtcNow < until)
|
||||
{
|
||||
Application.DoEvents();
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
Assert.True(condition(), $"Timed out waiting for {what}.");
|
||||
}
|
||||
|
||||
private static T Field<T>(object owner, string name) =>
|
||||
(T)owner.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
|
||||
|
||||
/// <summary>Real drawing packages by revision, so the release reads them as the desktop does.</summary>
|
||||
private sealed class PackageLibrary : IDrawingLibraryStore
|
||||
{
|
||||
private readonly List<DrawingLibraryRecord> _records = new();
|
||||
private readonly Dictionary<(Guid, int), byte[]> _packages = new();
|
||||
|
||||
public DrawingLibraryRecord Add(string name, string material, bool archived = false)
|
||||
{
|
||||
var package = DrawingPackage.Write(new RectangleShape { Name = name, Length = 4, Width = 2 }.GetDrawing(), Units.Inches);
|
||||
var record = new DrawingLibraryRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Revision = 1,
|
||||
Name = name,
|
||||
MaterialName = material,
|
||||
MaterialGrade = "Grade A",
|
||||
Thickness = 0.25,
|
||||
Units = Units.Inches,
|
||||
CutType = CutType.Laser,
|
||||
Archived = archived,
|
||||
PackageSha256 = Convert.ToHexString(SHA256.HashData(package)).ToLowerInvariant(),
|
||||
};
|
||||
_records.Add(record);
|
||||
_packages[(record.Id, record.Revision)] = package;
|
||||
return record;
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(_packages.TryGetValue((id, revision), out var package) ? package : null);
|
||||
|
||||
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new DrawingLibraryPage { Items = _records.ToArray(), Total = _records.Count, Offset = query.Offset, Limit = query.Limit });
|
||||
|
||||
public Task<DrawingLibraryRecord?> GetAsync(Guid id, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryPage?> GetRevisionsAsync(Guid id, int offset = 0,
|
||||
int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord> UploadAsync(DrawingLibraryRecord record, byte[] package,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord> UpdatePackageAsync(Guid id, DrawingLibraryRecord record, byte[] package,
|
||||
int expectedRevision, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord> UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>Stores uploads in memory under their client-allocated ids.</summary>
|
||||
private sealed class MemoryNests : INestRepository
|
||||
{
|
||||
public List<NestRecord> Uploaded { get; } = new();
|
||||
|
||||
public Task<NestRecord> UploadAsync(byte[] file, NestRecord record, CancellationToken ct = default)
|
||||
{
|
||||
Assert.NotEmpty(file);
|
||||
var stored = new NestRecord { Id = record.Id, Revision = 1, Name = record.Name, Status = record.Status };
|
||||
Uploaded.Add(stored);
|
||||
return Task.FromResult(stored);
|
||||
}
|
||||
|
||||
public Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken ct = default) =>
|
||||
Task.FromResult(Uploaded.FirstOrDefault(r => r.Id == id));
|
||||
|
||||
public Task<IReadOnlyList<NestRecord>> ListAsync(CancellationToken ct = default) => throw new NotSupportedException();
|
||||
public Task<NestPage> QueryAsync(NestQuery query, CancellationToken ct = default) => throw new NotSupportedException();
|
||||
public Task<byte[]?> GetFileAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException();
|
||||
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] file, NestRecord record, CancellationToken ct = default) => throw new NotSupportedException();
|
||||
public Task<NestRecord> UpdateMetadataAsync(Guid id, NestRecord record, CancellationToken ct = default) => throw new NotSupportedException();
|
||||
public Task DeleteAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
+31
-1
@@ -44,6 +44,8 @@ namespace OpenNest.Controls
|
||||
editButton = new System.Windows.Forms.Button();
|
||||
archiveButton = new System.Windows.Forms.Button();
|
||||
historyButton = new System.Windows.Forms.Button();
|
||||
releasesButton = new System.Windows.Forms.Button();
|
||||
releaseButton = new System.Windows.Forms.Button();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
searchTimer = new System.Windows.Forms.Timer(components);
|
||||
layoutTable.SuspendLayout();
|
||||
@@ -212,7 +214,9 @@ namespace OpenNest.Controls
|
||||
// bottomTable
|
||||
//
|
||||
bottomTable.AutoSize = true;
|
||||
bottomTable.ColumnCount = 7;
|
||||
bottomTable.ColumnCount = 9;
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
@@ -227,6 +231,8 @@ namespace OpenNest.Controls
|
||||
bottomTable.Controls.Add(editButton, 4, 0);
|
||||
bottomTable.Controls.Add(archiveButton, 5, 0);
|
||||
bottomTable.Controls.Add(historyButton, 6, 0);
|
||||
bottomTable.Controls.Add(releasesButton, 7, 0);
|
||||
bottomTable.Controls.Add(releaseButton, 8, 0);
|
||||
bottomTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
bottomTable.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
bottomTable.Name = "bottomTable";
|
||||
@@ -299,6 +305,28 @@ namespace OpenNest.Controls
|
||||
historyButton.UseVisualStyleBackColor = true;
|
||||
historyButton.Click += HistoryButton_Click;
|
||||
//
|
||||
// releasesButton
|
||||
//
|
||||
releasesButton.AutoSize = true;
|
||||
releasesButton.Enabled = false;
|
||||
releasesButton.Name = "releasesButton";
|
||||
releasesButton.TabIndex = 7;
|
||||
releasesButton.Text = "Releases...";
|
||||
releasesButton.UseVisualStyleBackColor = true;
|
||||
releasesButton.Click += ReleasesButton_Click;
|
||||
//
|
||||
// releaseButton
|
||||
//
|
||||
releaseButton.AutoSize = true;
|
||||
releaseButton.Enabled = false;
|
||||
releaseButton.Margin = new System.Windows.Forms.Padding(12, 3, 3, 3);
|
||||
releaseButton.Name = "releaseButton";
|
||||
releaseButton.TabIndex = 8;
|
||||
releaseButton.Text = "Release...";
|
||||
releaseButton.UseVisualStyleBackColor = true;
|
||||
releaseButton.Visible = false;
|
||||
releaseButton.Click += ReleaseButton_Click;
|
||||
//
|
||||
// statusLabel
|
||||
//
|
||||
statusLabel.AutoEllipsis = true;
|
||||
@@ -357,6 +385,8 @@ namespace OpenNest.Controls
|
||||
private System.Windows.Forms.Button editButton;
|
||||
private System.Windows.Forms.Button archiveButton;
|
||||
private System.Windows.Forms.Button historyButton;
|
||||
private System.Windows.Forms.Button releasesButton;
|
||||
private System.Windows.Forms.Button releaseButton;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
private System.Windows.Forms.Timer searchTimer;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ namespace OpenNest.Controls
|
||||
private const string LoadingText = "Loading...";
|
||||
|
||||
private IDrawingSetStore sets;
|
||||
private IDrawingLibraryStore library;
|
||||
private string serverUrl;
|
||||
private INestRepository nests;
|
||||
private Action<Nest, NestRecord> openNest;
|
||||
private DrawingSetBrowseSession session;
|
||||
private bool pick;
|
||||
private bool busy;
|
||||
@@ -34,6 +38,12 @@ namespace OpenNest.Controls
|
||||
/// <summary>Shows a set's revision history; replaced by tests.</summary>
|
||||
internal Action<DrawingSetRecord> ShowHistory { get; set; }
|
||||
|
||||
/// <summary>Shows a set's releases; replaced by tests.</summary>
|
||||
internal Action<DrawingSetRecord> ShowReleases { get; set; }
|
||||
|
||||
/// <summary>Shows the release dialog for a set read with its members; replaced by tests.</summary>
|
||||
internal Action<DrawingSetRecord> ShowRelease { get; set; }
|
||||
|
||||
/// <summary>Pick mode: an active set was double-clicked (or chosen with Enter).</summary>
|
||||
public event EventHandler SetActivated;
|
||||
|
||||
@@ -69,7 +79,8 @@ namespace OpenNest.Controls
|
||||
if (session != null)
|
||||
throw new InvalidOperationException("The set browser is already initialized.");
|
||||
this.sets = sets ?? throw new ArgumentNullException(nameof(sets));
|
||||
ArgumentNullException.ThrowIfNull(library);
|
||||
this.library = library ?? throw new ArgumentNullException(nameof(library));
|
||||
this.serverUrl = serverUrl ?? "";
|
||||
this.pick = pick;
|
||||
session = new DrawingSetBrowseSession(sets);
|
||||
ShowMessage = (text, buttons, icon) => MessageBox.Show(FindForm(), text, FindForm()?.Text ?? "Drawing Sets", buttons, icon);
|
||||
@@ -83,6 +94,16 @@ namespace OpenNest.Controls
|
||||
using var form = new DrawingSetHistoryForm(sets, set);
|
||||
form.ShowDialog(FindForm());
|
||||
};
|
||||
ShowReleases = set =>
|
||||
{
|
||||
using var form = new SetReleasesForm(sets, set);
|
||||
form.ShowDialog(FindForm());
|
||||
};
|
||||
ShowRelease = set =>
|
||||
{
|
||||
using var form = new ReleaseSetForm(sets, library, nests, this.serverUrl, set, openNest);
|
||||
form.ShowDialog(FindForm());
|
||||
};
|
||||
|
||||
if (pick)
|
||||
{
|
||||
@@ -91,12 +112,27 @@ namespace OpenNest.Controls
|
||||
editButton.Visible = false;
|
||||
archiveButton.Visible = false;
|
||||
historyButton.Visible = false;
|
||||
releasesButton.Visible = false;
|
||||
}
|
||||
|
||||
UpdateNavigation();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows Release, which makes nests from the selected set: uploads them through
|
||||
/// <paramref name="nests"/> and opens each with <paramref name="openNest"/>.
|
||||
/// </summary>
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest)
|
||||
{
|
||||
if (session == null || pick)
|
||||
throw new InvalidOperationException("Release needs an initialized set browser that is not picking.");
|
||||
this.nests = nests ?? throw new ArgumentNullException(nameof(nests));
|
||||
this.openNest = openNest ?? throw new ArgumentNullException(nameof(openNest));
|
||||
releaseButton.Visible = true;
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
/// <summary>Re-reads the current page (the first call loads the list).</summary>
|
||||
public Task RefreshAsync()
|
||||
{
|
||||
@@ -179,6 +215,56 @@ namespace OpenNest.Controls
|
||||
ShowHistory(set);
|
||||
}
|
||||
|
||||
private void ReleasesButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (SelectedSet is { } set)
|
||||
ShowReleases(set);
|
||||
}
|
||||
|
||||
private async void ReleaseButton_Click(object sender, EventArgs e) => await ReleaseSelectedAsync();
|
||||
|
||||
/// <summary>Reads the selected active set with its members' current drawings and opens the release dialog.</summary>
|
||||
internal async Task ReleaseSelectedAsync()
|
||||
{
|
||||
var listed = SelectedSet;
|
||||
if (session == null || nests == null || listed == null || listed.Archived || busy)
|
||||
return;
|
||||
|
||||
DrawingSetRecord current;
|
||||
busy = true;
|
||||
UpdateCommands();
|
||||
statusLabel.Text = $"Reading set '{listed.Name}'...";
|
||||
try
|
||||
{
|
||||
current = await sets.GetAsync(listed.Id);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException)
|
||||
{
|
||||
Fail($"Could not read set '{listed.Name}': {ex.Message}");
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
busy = false;
|
||||
if (!IsDisposed)
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
if (IsDisposed)
|
||||
return;
|
||||
statusLabel.Text = "";
|
||||
if (current == null || current.Archived)
|
||||
{
|
||||
ShowMessage(current == null
|
||||
? $"Set '{listed.Name}' no longer exists on the server."
|
||||
: $"Set '{listed.Name}' was archived. Restore it to release it.", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
await RunAsync(session.RefreshAsync);
|
||||
return;
|
||||
}
|
||||
|
||||
ShowRelease(current);
|
||||
}
|
||||
|
||||
private void ActivateSelected()
|
||||
{
|
||||
if (SelectedSet is { Archived: false })
|
||||
@@ -358,6 +444,10 @@ namespace OpenNest.Controls
|
||||
setGrid.Rows.Add(row);
|
||||
}
|
||||
|
||||
// The grid only sometimes makes the first row current by itself (it depends on focus).
|
||||
if (setGrid.CurrentCell == null && setGrid.Rows.Count > 0)
|
||||
setGrid.CurrentCell = setGrid.Rows[0].Cells[0];
|
||||
|
||||
foreach (DataGridViewColumn column in setGrid.Columns)
|
||||
{
|
||||
column.HeaderCell.SortGlyphDirection = column.Tag is DrawingSetSortField field && field == session.Sort
|
||||
@@ -383,6 +473,8 @@ namespace OpenNest.Controls
|
||||
archiveButton.Enabled = ready && selected != null;
|
||||
archiveButton.Text = selected is { Archived: true } ? "Restore" : "Archive";
|
||||
historyButton.Enabled = ready && selected != null;
|
||||
releasesButton.Enabled = ready && selected != null;
|
||||
releaseButton.Enabled = ready && nests != null && selected is { Archived: false, MemberCount: > 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,16 @@ namespace OpenNest.Forms
|
||||
/// <summary>The Sets tab's browser; loaded the first time the tab is shown.</summary>
|
||||
internal OpenNest.Controls.DrawingSetBrowser SetBrowser => setBrowser;
|
||||
|
||||
/// <summary>
|
||||
/// Lets the Sets tab release sets: nests are uploaded through <paramref name="nests"/>
|
||||
/// and each is opened with <paramref name="openNest"/>.
|
||||
/// </summary>
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest)
|
||||
{
|
||||
if (setBrowser.IsInitialized)
|
||||
setBrowser.EnableRelease(nests, openNest);
|
||||
}
|
||||
|
||||
private async void TabControl_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (tabControl.SelectedTab == setsTab && setBrowser.IsInitialized && !setBrowser.HasLoaded)
|
||||
|
||||
@@ -84,6 +84,10 @@ namespace OpenNest.Forms
|
||||
revisionGrid.Rows.Add(row);
|
||||
}
|
||||
|
||||
// The grid only sometimes makes the first row current by itself (it depends on focus).
|
||||
if (revisionGrid.CurrentCell == null && revisionGrid.Rows.Count > 0)
|
||||
revisionGrid.CurrentCell = revisionGrid.Rows[0].Cells[0];
|
||||
|
||||
summaryLabel.Text = loaded.Items.Count == 0
|
||||
? $"No revisions on this page ({loaded.Total:N0} in total)."
|
||||
: $"Revisions {offset + 1:N0}-{offset + loaded.Items.Count:N0} of {loaded.Total:N0}";
|
||||
|
||||
@@ -757,6 +757,7 @@ namespace OpenNest.Forms
|
||||
{
|
||||
var form = new EditNestInfoForm { AllowRename = NestStorage.Settings.IsDatabaseMode };
|
||||
form.LoadNestInfo(Nest);
|
||||
_ = ShowReleaseOriginAsync(form);
|
||||
|
||||
while (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
@@ -772,6 +773,30 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows which drawing set release made this nest, when it is a server nest that came
|
||||
/// from one. Best effort and in the background: Nest Info never waits for the server.
|
||||
/// </summary>
|
||||
private async Task ShowReleaseOriginAsync(EditNestInfoForm form)
|
||||
{
|
||||
var serverUrl = NestStorage.Settings.ServerUrl;
|
||||
if (!NestStorage.Settings.IsDatabaseMode || Document.RemoteId == Guid.Empty || Document.IsFirstDatabaseSave(serverUrl))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
var release = await NestStorage.CreateDrawingSetStore().GetReleaseForNestAsync(Document.RemoteId, timeout.Token);
|
||||
if (release != null && !form.IsDisposed)
|
||||
form.ReleaseOrigin = SetReleaseText.Origin(release);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or System.Net.Http.HttpRequestException
|
||||
or OperationCanceledException or ArgumentException or System.Text.Json.JsonException)
|
||||
{
|
||||
// An older server, or none reachable: the dialog simply shows no origin.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Renames go up with the next Save; warn now about a duplicate name.</summary>
|
||||
private async Task<bool> ConfirmRenameAsync(string name)
|
||||
{
|
||||
@@ -1297,6 +1322,9 @@ namespace OpenNest.Forms
|
||||
}
|
||||
|
||||
using var form = new DrawingLibraryForm(library, NestStorage.Settings.ServerUrl, Nest, sets);
|
||||
// Released nests open in their own tabs beside this one.
|
||||
if (MdiParent is MainForm main)
|
||||
form.EnableRelease(NestStorage.CreateRepository(), main.OpenServerNest);
|
||||
form.ShowDialog(this);
|
||||
if (form.AddedDrawings.Count > 0)
|
||||
UpdateDrawingList();
|
||||
|
||||
+14
@@ -74,6 +74,7 @@
|
||||
this.cancelButton = new System.Windows.Forms.Button();
|
||||
this.bottomPanel1 = new OpenNest.Controls.BottomPanel();
|
||||
this.applyButton = new System.Windows.Forms.Button();
|
||||
this.releaseLabel = new System.Windows.Forms.Label();
|
||||
this.groupBox1.SuspendLayout();
|
||||
this.tableLayoutPanel2.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.leftSpacingBox)).BeginInit();
|
||||
@@ -667,6 +668,7 @@
|
||||
//
|
||||
// bottomPanel1
|
||||
//
|
||||
this.bottomPanel1.Controls.Add(this.releaseLabel);
|
||||
this.bottomPanel1.Controls.Add(this.applyButton);
|
||||
this.bottomPanel1.Controls.Add(this.cancelButton);
|
||||
this.bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
@@ -675,6 +677,17 @@
|
||||
this.bottomPanel1.Size = new System.Drawing.Size(659, 50);
|
||||
this.bottomPanel1.TabIndex = 1;
|
||||
//
|
||||
// releaseLabel
|
||||
//
|
||||
this.releaseLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.releaseLabel.AutoEllipsis = true;
|
||||
this.releaseLabel.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
this.releaseLabel.Location = new System.Drawing.Point(12, 16);
|
||||
this.releaseLabel.Name = "releaseLabel";
|
||||
this.releaseLabel.Size = new System.Drawing.Size(440, 18);
|
||||
this.releaseLabel.TabIndex = 2;
|
||||
//
|
||||
// applyButton
|
||||
//
|
||||
this.applyButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
@@ -753,6 +766,7 @@
|
||||
private System.Windows.Forms.GroupBox groupBox3;
|
||||
private Controls.QuadrantSelect quadrantSelect1;
|
||||
private System.Windows.Forms.Button applyButton;
|
||||
private System.Windows.Forms.Label releaseLabel;
|
||||
private System.Windows.Forms.Button cancelButton;
|
||||
private System.Windows.Forms.TabControl tabControl1;
|
||||
private System.Windows.Forms.TabPage tabPage1;
|
||||
|
||||
@@ -52,6 +52,16 @@ namespace OpenNest.Forms
|
||||
set { nameBox.ReadOnly = !value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The drawing set release this nest came from (Database mode), shown beside the
|
||||
/// buttons; blank for none.
|
||||
/// </summary>
|
||||
public string ReleaseOrigin
|
||||
{
|
||||
get { return releaseLabel.Text; }
|
||||
set { releaseLabel.Text = value ?? ""; }
|
||||
}
|
||||
|
||||
public string Customer
|
||||
{
|
||||
get { return customerBox.Text; }
|
||||
|
||||
@@ -33,6 +33,18 @@ namespace OpenNest.Forms
|
||||
editForm.PlateView.ZoomToFit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a nest that is already on the nest server (a drawing set release made it) in
|
||||
/// its own tab, bound to that record so the next Save updates it.
|
||||
/// </summary>
|
||||
internal void OpenServerNest(Nest nest, NestRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
var windowState = ActiveMdiChild?.WindowState ?? FormWindowState.Maximized;
|
||||
LoadNest(nest, windowState, record.Id, NestStorage.Settings.ServerUrl, record.Revision);
|
||||
}
|
||||
|
||||
#region File Menu Events
|
||||
|
||||
private async void New_Click(object sender, EventArgs e)
|
||||
|
||||
@@ -19,9 +19,13 @@ namespace OpenNest.Forms
|
||||
{
|
||||
private static readonly TimeSpan ServerTimeout = TimeSpan.FromSeconds(3);
|
||||
|
||||
public static async Task<Nest> CreateAsync(IWin32Window owner)
|
||||
/// <param name="units">
|
||||
/// The new nest's units, with the saved plate defaults converted to them (a drawing set
|
||||
/// release group drawn in other units); null uses the defaults as saved.
|
||||
/// </param>
|
||||
public static async Task<Nest> CreateAsync(IWin32Window owner, Units? units = null)
|
||||
{
|
||||
var nest = CreateDefaultNest(owner);
|
||||
var nest = CreateDefaultNest(owner, units);
|
||||
var now = DateTime.Now;
|
||||
|
||||
nest.DateCreated = now;
|
||||
@@ -30,10 +34,11 @@ namespace OpenNest.Forms
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static Nest CreateDefaultNest(IWin32Window owner)
|
||||
private static Nest CreateDefaultNest(IWin32Window owner, Units? units)
|
||||
{
|
||||
var nest = new Nest();
|
||||
MachineSettings.LoadNestDefaults().ApplyTo(nest);
|
||||
var defaults = MachineSettings.LoadNestDefaults();
|
||||
(units is { } converted ? defaults.ConvertedTo(converted) : defaults).ApplyTo(nest);
|
||||
// After the defaults so the rail is converted to the new nest's units.
|
||||
MachineSettings.LoadClampSettings(owner).ApplyToNewNest(nest);
|
||||
return nest;
|
||||
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class ReleaseSetForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
cancellation?.Dispose();
|
||||
if (components != null)
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.Windows.Forms.DataGridViewCellStyle rightAligned = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
layoutTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
setLabel = new System.Windows.Forms.Label();
|
||||
optionsTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
multiplierLabel = new System.Windows.Forms.Label();
|
||||
multiplierBox = new System.Windows.Forms.NumericUpDown();
|
||||
includeArchivedCheckBox = new System.Windows.Forms.CheckBox();
|
||||
notesLabel = new System.Windows.Forms.Label();
|
||||
notesBox = new System.Windows.Forms.TextBox();
|
||||
groupGrid = new System.Windows.Forms.DataGridView();
|
||||
includeColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
materialColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
gradeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
thicknessColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
unitsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
cutTypeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
drawingsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
partsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
statusColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
skippedLabel = new System.Windows.Forms.Label();
|
||||
bottomTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
summaryLabel = new System.Windows.Forms.Label();
|
||||
releaseButton = new System.Windows.Forms.Button();
|
||||
closeButton = new System.Windows.Forms.Button();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
layoutTable.SuspendLayout();
|
||||
optionsTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)multiplierBox).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)groupGrid).BeginInit();
|
||||
bottomTable.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// layoutTable
|
||||
//
|
||||
layoutTable.ColumnCount = 1;
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.Controls.Add(setLabel, 0, 0);
|
||||
layoutTable.Controls.Add(optionsTable, 0, 1);
|
||||
layoutTable.Controls.Add(groupGrid, 0, 2);
|
||||
layoutTable.Controls.Add(skippedLabel, 0, 3);
|
||||
layoutTable.Controls.Add(bottomTable, 0, 4);
|
||||
layoutTable.Controls.Add(statusLabel, 0, 5);
|
||||
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
layoutTable.Location = new System.Drawing.Point(0, 0);
|
||||
layoutTable.Name = "layoutTable";
|
||||
layoutTable.Padding = new System.Windows.Forms.Padding(8);
|
||||
layoutTable.RowCount = 6;
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.Size = new System.Drawing.Size(944, 521);
|
||||
layoutTable.TabIndex = 0;
|
||||
//
|
||||
// setLabel
|
||||
//
|
||||
setLabel.AutoEllipsis = true;
|
||||
setLabel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
setLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
|
||||
setLabel.Margin = new System.Windows.Forms.Padding(3, 0, 3, 6);
|
||||
setLabel.Name = "setLabel";
|
||||
setLabel.Size = new System.Drawing.Size(100, 20);
|
||||
setLabel.TabIndex = 0;
|
||||
//
|
||||
// optionsTable
|
||||
//
|
||||
optionsTable.AutoSize = true;
|
||||
optionsTable.ColumnCount = 5;
|
||||
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
optionsTable.Controls.Add(multiplierLabel, 0, 0);
|
||||
optionsTable.Controls.Add(multiplierBox, 1, 0);
|
||||
optionsTable.Controls.Add(includeArchivedCheckBox, 2, 0);
|
||||
optionsTable.Controls.Add(notesLabel, 3, 0);
|
||||
optionsTable.Controls.Add(notesBox, 4, 0);
|
||||
optionsTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
optionsTable.Margin = new System.Windows.Forms.Padding(0, 0, 0, 6);
|
||||
optionsTable.Name = "optionsTable";
|
||||
optionsTable.RowCount = 1;
|
||||
optionsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
optionsTable.TabIndex = 1;
|
||||
//
|
||||
// multiplierLabel
|
||||
//
|
||||
multiplierLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
multiplierLabel.AutoSize = true;
|
||||
multiplierLabel.Name = "multiplierLabel";
|
||||
multiplierLabel.TabIndex = 0;
|
||||
multiplierLabel.Text = "Kits (multiplier):";
|
||||
//
|
||||
// multiplierBox
|
||||
//
|
||||
multiplierBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
multiplierBox.Maximum = new decimal(new int[] { 1000, 0, 0, 0 });
|
||||
multiplierBox.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
multiplierBox.Name = "multiplierBox";
|
||||
multiplierBox.Size = new System.Drawing.Size(70, 25);
|
||||
multiplierBox.TabIndex = 1;
|
||||
multiplierBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
multiplierBox.Value = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
multiplierBox.ValueChanged += Options_Changed;
|
||||
//
|
||||
// includeArchivedCheckBox
|
||||
//
|
||||
includeArchivedCheckBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
includeArchivedCheckBox.AutoSize = true;
|
||||
includeArchivedCheckBox.Margin = new System.Windows.Forms.Padding(12, 3, 3, 3);
|
||||
includeArchivedCheckBox.Name = "includeArchivedCheckBox";
|
||||
includeArchivedCheckBox.TabIndex = 2;
|
||||
includeArchivedCheckBox.Text = "Include archived drawings";
|
||||
includeArchivedCheckBox.UseVisualStyleBackColor = true;
|
||||
includeArchivedCheckBox.CheckedChanged += Options_Changed;
|
||||
//
|
||||
// notesLabel
|
||||
//
|
||||
notesLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
notesLabel.AutoSize = true;
|
||||
notesLabel.Margin = new System.Windows.Forms.Padding(12, 0, 3, 0);
|
||||
notesLabel.Name = "notesLabel";
|
||||
notesLabel.TabIndex = 3;
|
||||
notesLabel.Text = "Release notes:";
|
||||
//
|
||||
// notesBox
|
||||
//
|
||||
notesBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
notesBox.MaxLength = 4000;
|
||||
notesBox.Name = "notesBox";
|
||||
notesBox.TabIndex = 4;
|
||||
//
|
||||
// groupGrid
|
||||
//
|
||||
groupGrid.AllowUserToAddRows = false;
|
||||
groupGrid.AllowUserToDeleteRows = false;
|
||||
groupGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { includeColumn, materialColumn, gradeColumn, thicknessColumn, unitsColumn, cutTypeColumn, drawingsColumn, partsColumn, statusColumn });
|
||||
groupGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
groupGrid.MultiSelect = false;
|
||||
groupGrid.Name = "groupGrid";
|
||||
groupGrid.RowHeadersVisible = false;
|
||||
groupGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||
groupGrid.StandardTab = true;
|
||||
groupGrid.TabIndex = 2;
|
||||
groupGrid.CellValueChanged += GroupGrid_CellValueChanged;
|
||||
groupGrid.CurrentCellDirtyStateChanged += GroupGrid_CurrentCellDirtyStateChanged;
|
||||
//
|
||||
// includeColumn
|
||||
//
|
||||
includeColumn.HeaderText = "Release";
|
||||
includeColumn.Name = "includeColumn";
|
||||
includeColumn.Width = 65;
|
||||
//
|
||||
// materialColumn
|
||||
//
|
||||
materialColumn.HeaderText = "Material";
|
||||
materialColumn.Name = "materialColumn";
|
||||
materialColumn.ReadOnly = true;
|
||||
materialColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
materialColumn.Width = 140;
|
||||
//
|
||||
// gradeColumn
|
||||
//
|
||||
gradeColumn.HeaderText = "Grade";
|
||||
gradeColumn.Name = "gradeColumn";
|
||||
gradeColumn.ReadOnly = true;
|
||||
gradeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
gradeColumn.Width = 90;
|
||||
//
|
||||
// thicknessColumn
|
||||
//
|
||||
rightAligned.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
|
||||
thicknessColumn.DefaultCellStyle = rightAligned;
|
||||
thicknessColumn.HeaderText = "Thickness";
|
||||
thicknessColumn.Name = "thicknessColumn";
|
||||
thicknessColumn.ReadOnly = true;
|
||||
thicknessColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
thicknessColumn.Width = 80;
|
||||
//
|
||||
// unitsColumn
|
||||
//
|
||||
unitsColumn.HeaderText = "Units";
|
||||
unitsColumn.Name = "unitsColumn";
|
||||
unitsColumn.ReadOnly = true;
|
||||
unitsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
unitsColumn.Width = 55;
|
||||
//
|
||||
// cutTypeColumn
|
||||
//
|
||||
cutTypeColumn.HeaderText = "Cut type";
|
||||
cutTypeColumn.Name = "cutTypeColumn";
|
||||
cutTypeColumn.ReadOnly = true;
|
||||
cutTypeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
cutTypeColumn.Width = 75;
|
||||
//
|
||||
// drawingsColumn
|
||||
//
|
||||
drawingsColumn.DefaultCellStyle = rightAligned;
|
||||
drawingsColumn.HeaderText = "Drawings";
|
||||
drawingsColumn.Name = "drawingsColumn";
|
||||
drawingsColumn.ReadOnly = true;
|
||||
drawingsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
drawingsColumn.Width = 70;
|
||||
//
|
||||
// partsColumn
|
||||
//
|
||||
partsColumn.DefaultCellStyle = rightAligned;
|
||||
partsColumn.HeaderText = "Parts";
|
||||
partsColumn.Name = "partsColumn";
|
||||
partsColumn.ReadOnly = true;
|
||||
partsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
partsColumn.Width = 65;
|
||||
//
|
||||
// statusColumn
|
||||
//
|
||||
statusColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
|
||||
statusColumn.HeaderText = "Status";
|
||||
statusColumn.Name = "statusColumn";
|
||||
statusColumn.ReadOnly = true;
|
||||
statusColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
//
|
||||
// skippedLabel
|
||||
//
|
||||
skippedLabel.AutoSize = true;
|
||||
skippedLabel.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
skippedLabel.Margin = new System.Windows.Forms.Padding(3, 6, 3, 0);
|
||||
skippedLabel.MaximumSize = new System.Drawing.Size(920, 0);
|
||||
skippedLabel.Name = "skippedLabel";
|
||||
skippedLabel.TabIndex = 3;
|
||||
skippedLabel.Visible = false;
|
||||
//
|
||||
// bottomTable
|
||||
//
|
||||
bottomTable.AutoSize = true;
|
||||
bottomTable.ColumnCount = 3;
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.Controls.Add(summaryLabel, 0, 0);
|
||||
bottomTable.Controls.Add(releaseButton, 1, 0);
|
||||
bottomTable.Controls.Add(closeButton, 2, 0);
|
||||
bottomTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
bottomTable.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
bottomTable.Name = "bottomTable";
|
||||
bottomTable.RowCount = 1;
|
||||
bottomTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
bottomTable.TabIndex = 4;
|
||||
//
|
||||
// summaryLabel
|
||||
//
|
||||
summaryLabel.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
summaryLabel.AutoEllipsis = true;
|
||||
summaryLabel.Name = "summaryLabel";
|
||||
summaryLabel.TabIndex = 0;
|
||||
//
|
||||
// releaseButton
|
||||
//
|
||||
releaseButton.AutoSize = true;
|
||||
releaseButton.Enabled = false;
|
||||
releaseButton.Name = "releaseButton";
|
||||
releaseButton.TabIndex = 1;
|
||||
releaseButton.Text = "Release";
|
||||
releaseButton.UseVisualStyleBackColor = true;
|
||||
releaseButton.Click += ReleaseButton_Click;
|
||||
//
|
||||
// closeButton
|
||||
//
|
||||
closeButton.AutoSize = true;
|
||||
closeButton.Name = "closeButton";
|
||||
closeButton.TabIndex = 2;
|
||||
closeButton.Text = "Close";
|
||||
closeButton.UseVisualStyleBackColor = true;
|
||||
closeButton.Click += CloseButton_Click;
|
||||
//
|
||||
// statusLabel
|
||||
//
|
||||
statusLabel.AutoEllipsis = true;
|
||||
statusLabel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
statusLabel.Margin = new System.Windows.Forms.Padding(3, 6, 3, 0);
|
||||
statusLabel.Name = "statusLabel";
|
||||
statusLabel.Size = new System.Drawing.Size(100, 20);
|
||||
statusLabel.TabIndex = 5;
|
||||
//
|
||||
// ReleaseSetForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
ClientSize = new System.Drawing.Size(944, 521);
|
||||
Controls.Add(layoutTable);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||
MinimizeBox = false;
|
||||
MinimumSize = new System.Drawing.Size(760, 400);
|
||||
Name = "ReleaseSetForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Release Drawing Set";
|
||||
FormClosing += ReleaseSetForm_FormClosing;
|
||||
layoutTable.ResumeLayout(false);
|
||||
layoutTable.PerformLayout();
|
||||
optionsTable.ResumeLayout(false);
|
||||
optionsTable.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)multiplierBox).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)groupGrid).EndInit();
|
||||
bottomTable.ResumeLayout(false);
|
||||
bottomTable.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.TableLayoutPanel layoutTable;
|
||||
private System.Windows.Forms.Label setLabel;
|
||||
private System.Windows.Forms.TableLayoutPanel optionsTable;
|
||||
private System.Windows.Forms.Label multiplierLabel;
|
||||
private System.Windows.Forms.NumericUpDown multiplierBox;
|
||||
private System.Windows.Forms.CheckBox includeArchivedCheckBox;
|
||||
private System.Windows.Forms.Label notesLabel;
|
||||
private System.Windows.Forms.TextBox notesBox;
|
||||
private System.Windows.Forms.DataGridView groupGrid;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn includeColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn materialColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn gradeColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn thicknessColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn unitsColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn cutTypeColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn drawingsColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn partsColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn;
|
||||
private System.Windows.Forms.Label skippedLabel;
|
||||
private System.Windows.Forms.TableLayoutPanel bottomTable;
|
||||
private System.Windows.Forms.Label summaryLabel;
|
||||
private System.Windows.Forms.Button releaseButton;
|
||||
private System.Windows.Forms.Button closeButton;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Drawing Sets > Release: previews how a set splits into one nest per material group
|
||||
/// (<see cref="SetReleasePlan"/>), with a kit multiplier, an archived-drawing toggle and a
|
||||
/// tick per group, then makes those nests through <see cref="SetReleaseSession"/>: each is
|
||||
/// uploaded as a Draft and opened in its own tab, and the release is recorded once every
|
||||
/// chosen nest exists. The dialog shows every group's outcome; a release runs once per
|
||||
/// dialog.
|
||||
/// </summary>
|
||||
public partial class ReleaseSetForm : Form
|
||||
{
|
||||
private readonly IDrawingSetStore sets;
|
||||
private readonly IDrawingLibrary library;
|
||||
private readonly INestRepository nests;
|
||||
private readonly string serverUrl;
|
||||
private readonly DrawingSetRecord set;
|
||||
private readonly Action<Nest, NestRecord> openNest;
|
||||
private readonly HashSet<string> excluded = new(StringComparer.Ordinal);
|
||||
private SetReleasePlan plan;
|
||||
private SetReleaseSession session;
|
||||
private CancellationTokenSource cancellation;
|
||||
private bool populating;
|
||||
private bool running;
|
||||
|
||||
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
|
||||
/// <summary>Makes the empty nest for a group, as File > New would in its units; replaced by tests.</summary>
|
||||
internal Func<SetReleaseGroup, CancellationToken, Task<Nest>> CreateNest { get; set; }
|
||||
|
||||
/// <summary>The finished release, once one has run; otherwise null.</summary>
|
||||
public SetReleaseResult Result { get; private set; }
|
||||
|
||||
/// <param name="set">The set read with <see cref="IDrawingSetStore.GetAsync"/>, members joined.</param>
|
||||
/// <param name="openNest">Opens a created nest in its own tab, bound to its server record.</param>
|
||||
public ReleaseSetForm(IDrawingSetStore sets, IDrawingLibrary library, INestRepository nests, string serverUrl,
|
||||
DrawingSetRecord set, Action<Nest, NestRecord> openNest)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.sets = sets ?? throw new ArgumentNullException(nameof(sets));
|
||||
this.library = library ?? throw new ArgumentNullException(nameof(library));
|
||||
this.nests = nests ?? throw new ArgumentNullException(nameof(nests));
|
||||
this.serverUrl = serverUrl ?? "";
|
||||
this.set = set ?? throw new ArgumentNullException(nameof(set));
|
||||
this.openNest = openNest ?? throw new ArgumentNullException(nameof(openNest));
|
||||
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
|
||||
CreateNest = (group, _) => NewNestFactory.CreateAsync(this, group.Units);
|
||||
|
||||
GridStyle.Apply(groupGrid);
|
||||
Text = $"Release '{set.Name}'";
|
||||
setLabel.Text = string.IsNullOrWhiteSpace(set.Customer)
|
||||
? $"{set.Name} — revision {set.Revision}"
|
||||
: $"{set.Name} ({set.Customer}) — revision {set.Revision}";
|
||||
BuildPlan();
|
||||
}
|
||||
|
||||
private void Options_Changed(object sender, EventArgs e)
|
||||
{
|
||||
if (!running && Result == null)
|
||||
BuildPlan();
|
||||
}
|
||||
|
||||
// Commit a checkbox click at once so the summary follows the tick.
|
||||
private void GroupGrid_CurrentCellDirtyStateChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (groupGrid.IsCurrentCellDirty && groupGrid.CurrentCell?.OwningColumn == includeColumn)
|
||||
groupGrid.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||||
}
|
||||
|
||||
private void GroupGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (populating || e.RowIndex < 0 || e.ColumnIndex != includeColumn.Index)
|
||||
return;
|
||||
var row = groupGrid.Rows[e.RowIndex];
|
||||
if (row.Tag is SetReleaseGroup group)
|
||||
{
|
||||
if (row.Cells[includeColumn.Index].Value is true)
|
||||
excluded.Remove(group.Key);
|
||||
else
|
||||
excluded.Add(group.Key);
|
||||
}
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private async void ReleaseButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (Result is { Release: null, ReleaseError.Length: > 0 })
|
||||
await RetryRecordAsync();
|
||||
else
|
||||
await ReleaseAsync();
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (running)
|
||||
{
|
||||
cancellation?.Cancel();
|
||||
statusLabel.Text = "Stopping after the nest being uploaded...";
|
||||
closeButton.Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ReleaseSetForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
// Nests may be on their way to the server; stop between uploads, never mid-way.
|
||||
if (!running)
|
||||
return;
|
||||
e.Cancel = true;
|
||||
cancellation?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>The groups ticked for release, in plan order.</summary>
|
||||
internal IReadOnlyList<SetReleaseGroup> ChosenGroups() =>
|
||||
plan.Groups.Where(g => g.CanRelease && !excluded.Contains(g.Key)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Confirms, then makes one Draft nest per ticked group, opens each in its own tab and
|
||||
/// records the release. Every group's outcome is shown in the grid.
|
||||
/// </summary>
|
||||
internal async Task ReleaseAsync()
|
||||
{
|
||||
if (running || Result != null || !groupGrid.EndEdit())
|
||||
return;
|
||||
var chosen = ChosenGroups();
|
||||
if (chosen.Count == 0)
|
||||
return;
|
||||
var multiplier = plan.Multiplier;
|
||||
if (ShowMessage($"Create {chosen.Count} Draft nest{(chosen.Count == 1 ? "" : "s")} from set '{set.Name}'"
|
||||
+ (multiplier == 1 ? "" : $" ×{multiplier}") + "? Each opens in its own tab.",
|
||||
MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
running = true;
|
||||
cancellation = new CancellationTokenSource();
|
||||
closeButton.Text = "Cancel";
|
||||
UpdateCommands();
|
||||
session = new SetReleaseSession(sets, library, nests, serverUrl, ReadPackage, WriteNest, CreateNest);
|
||||
var progress = new Progress<SetReleaseProgress>(ShowProgress);
|
||||
try
|
||||
{
|
||||
Result = await session.RunAsync(plan, chosen, Environment.UserName, notesBox.Text, progress, cancellation.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
if (!IsDisposed)
|
||||
{
|
||||
closeButton.Text = "Close";
|
||||
closeButton.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var created in Result.Created)
|
||||
openNest(created.Nest, created.Record);
|
||||
if (IsDisposed)
|
||||
return;
|
||||
ShowOutcome();
|
||||
}
|
||||
|
||||
/// <summary>Posts the release again after every nest was created but recording it failed.</summary>
|
||||
internal async Task RetryRecordAsync()
|
||||
{
|
||||
if (running || session == null || !session.HasPendingRelease)
|
||||
return;
|
||||
running = true;
|
||||
UpdateCommands();
|
||||
statusLabel.Text = "Recording the release...";
|
||||
try
|
||||
{
|
||||
var release = await session.RetryReleaseAsync();
|
||||
Result = Result.WithRelease(release);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is System.Net.Http.HttpRequestException
|
||||
|| ex is TaskCanceledException || ex is System.Text.Json.JsonException)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
ShowMessage($"The release is still not recorded: {ex.Message}", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
}
|
||||
|
||||
if (!IsDisposed)
|
||||
ShowOutcome();
|
||||
}
|
||||
|
||||
private void BuildPlan()
|
||||
{
|
||||
plan = SetReleasePlan.Build(set, (int)multiplierBox.Value, includeArchivedCheckBox.Checked);
|
||||
populating = true;
|
||||
groupGrid.Rows.Clear();
|
||||
foreach (var group in plan.Groups)
|
||||
{
|
||||
var row = new DataGridViewRow { Tag = group };
|
||||
row.CreateCells(groupGrid,
|
||||
group.CanRelease && !excluded.Contains(group.Key),
|
||||
group.IsUnassigned ? "(none)" : group.MaterialName,
|
||||
group.MaterialGrade,
|
||||
group.IsUnassigned ? "" : group.Thickness.ToString("0.####", CultureInfo.CurrentCulture),
|
||||
group.IsUnassigned ? "" : UnitsHelper.GetShortString(group.Units),
|
||||
group.IsUnassigned ? "" : group.CutType.ToString(),
|
||||
group.Items.Count,
|
||||
group.TotalQuantity,
|
||||
group.CanRelease ? string.Join(" ", group.Warnings) : group.Refusal);
|
||||
// A refused group cannot be ticked.
|
||||
row.Cells[includeColumn.Index].ReadOnly = !group.CanRelease;
|
||||
groupGrid.Rows.Add(row);
|
||||
}
|
||||
populating = false;
|
||||
|
||||
var skipped = plan.SkippedArchived;
|
||||
skippedLabel.Visible = skipped.Count > 0;
|
||||
skippedLabel.Text = skipped.Count == 0 ? "" :
|
||||
$"{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.";
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void ShowProgress(SetReleaseProgress progress)
|
||||
{
|
||||
// Progress is posted, so a report can arrive after the run ended; the outcome wins.
|
||||
if (IsDisposed || !running)
|
||||
return;
|
||||
statusLabel.Text = progress.Stage switch
|
||||
{
|
||||
SetReleaseStage.Preparing => $"Preparing nest {progress.Index + 1} of {progress.Count}: {Describe(progress.Group)}...",
|
||||
SetReleaseStage.Uploading => $"Uploading nest {progress.Index + 1} of {progress.Count}: {Describe(progress.Group)}...",
|
||||
_ => "Recording the release...",
|
||||
};
|
||||
}
|
||||
|
||||
private void ShowOutcome()
|
||||
{
|
||||
foreach (DataGridViewRow row in groupGrid.Rows)
|
||||
{
|
||||
var outcome = Result.Groups.FirstOrDefault(g => ReferenceEquals(g.Group, row.Tag));
|
||||
if (outcome == null)
|
||||
continue;
|
||||
row.Cells[statusColumn.Index].Value = outcome.Outcome switch
|
||||
{
|
||||
SetReleaseGroupOutcome.Created => $"Created {outcome.Record!.Name}"
|
||||
+ (outcome.Warnings.Count == 0 ? "" : " — " + string.Join(" ", outcome.Warnings)),
|
||||
SetReleaseGroupOutcome.Refused => "Refused: " + outcome.Error,
|
||||
SetReleaseGroupOutcome.Failed => "Failed: " + outcome.Error,
|
||||
_ => "Not started",
|
||||
};
|
||||
}
|
||||
|
||||
var created = Result.Created.Count;
|
||||
var nestsText = $"{created} nest{(created == 1 ? "" : "s")}";
|
||||
if (Result.Release != null)
|
||||
statusLabel.Text = $"Released: {nestsText} created and opened, and the release recorded.";
|
||||
else if (Result.ReleaseError.Length > 0)
|
||||
statusLabel.Text = Result.ReleaseError + " Choose Record Release to try again.";
|
||||
else if (Result.Canceled)
|
||||
statusLabel.Text = $"Canceled: {nestsText} created; no release recorded.";
|
||||
else
|
||||
statusLabel.Text = created == 0
|
||||
? "Nothing was created. See the Status column."
|
||||
: $"Stopped: {nestsText} created; no release recorded. See the Status column.";
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void UpdateCommands()
|
||||
{
|
||||
var chosen = plan == null ? Array.Empty<SetReleaseGroup>() : ChosenGroups();
|
||||
var editable = !running && Result == null;
|
||||
multiplierBox.Enabled = editable;
|
||||
includeArchivedCheckBox.Enabled = editable;
|
||||
notesBox.ReadOnly = !editable;
|
||||
includeColumn.ReadOnly = !editable;
|
||||
if (editable)
|
||||
{
|
||||
foreach (DataGridViewRow row in groupGrid.Rows)
|
||||
row.Cells[includeColumn.Index].ReadOnly = row.Tag is SetReleaseGroup { CanRelease: false };
|
||||
}
|
||||
|
||||
var retry = Result is { Release: null, ReleaseError.Length: > 0 };
|
||||
releaseButton.Text = retry ? "Record Release" : "Release";
|
||||
releaseButton.Enabled = !running && (retry || (editable && chosen.Count > 0));
|
||||
var parts = chosen.Sum(g => (long)g.TotalQuantity);
|
||||
summaryLabel.Text = plan == null || plan.Groups.Count == 0
|
||||
? "This set has nothing to release."
|
||||
: $"{chosen.Count} of {plan.Groups.Count} group{(plan.Groups.Count == 1 ? "" : "s")} chosen: "
|
||||
+ $"{chosen.Count} Draft nest{(chosen.Count == 1 ? "" : "s")}, {parts:N0} part{(parts == 1 ? "" : "s")}";
|
||||
}
|
||||
|
||||
private static string Describe(SetReleaseGroup group) =>
|
||||
group == null ? "" : string.Join(" ", new[]
|
||||
{
|
||||
group.MaterialName,
|
||||
group.MaterialGrade,
|
||||
$"{group.Thickness.ToString("0.####", CultureInfo.CurrentCulture)} {UnitsHelper.GetShortString(group.Units)}",
|
||||
}.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
|
||||
private static LibraryPackage ReadPackage(byte[] bytes)
|
||||
{
|
||||
var contents = DrawingPackage.Read(bytes);
|
||||
return new LibraryPackage(bytes, contents.Drawing, contents.Units, contents.Warnings);
|
||||
}
|
||||
|
||||
private static byte[] WriteNest(Nest nest)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared }.Write(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,14 +540,7 @@ public sealed class SavedNestsForm : Form
|
||||
UpdateTitle(session.Summary);
|
||||
}
|
||||
|
||||
private static string FormatStatus(NestStatus status) => status switch
|
||||
{
|
||||
NestStatus.Draft => "Draft",
|
||||
NestStatus.Quote => "Quote",
|
||||
NestStatus.ToBeCut => "To Be Cut",
|
||||
NestStatus.HasBeenCut => "Has Been Cut",
|
||||
_ => status.ToString(),
|
||||
};
|
||||
private static string FormatStatus(NestStatus status) => NestStatuses.DisplayName(status);
|
||||
|
||||
private static string FormatDate(DateTime value) =>
|
||||
value == default ? "" : value.ToString("g", CultureInfo.CurrentCulture);
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class SetReleasesForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.Windows.Forms.DataGridViewCellStyle rightAligned = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
layoutTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
releaseGrid = new System.Windows.Forms.DataGridView();
|
||||
revisionColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
releasedColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
releasedByColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
multiplierColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
nestsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
progressColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
releaseNotesColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
nestsGroup = new System.Windows.Forms.GroupBox();
|
||||
nestGrid = new System.Windows.Forms.DataGridView();
|
||||
nestNameColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
groupColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
statusColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
nestCustomerColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
modifiedColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
bottomTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
previousButton = new System.Windows.Forms.Button();
|
||||
nextButton = new System.Windows.Forms.Button();
|
||||
summaryLabel = new System.Windows.Forms.Label();
|
||||
closeButton = new System.Windows.Forms.Button();
|
||||
layoutTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)releaseGrid).BeginInit();
|
||||
nestsGroup.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nestGrid).BeginInit();
|
||||
bottomTable.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// layoutTable
|
||||
//
|
||||
layoutTable.ColumnCount = 1;
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.Controls.Add(releaseGrid, 0, 0);
|
||||
layoutTable.Controls.Add(nestsGroup, 0, 1);
|
||||
layoutTable.Controls.Add(bottomTable, 0, 2);
|
||||
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
layoutTable.Location = new System.Drawing.Point(0, 0);
|
||||
layoutTable.Name = "layoutTable";
|
||||
layoutTable.Padding = new System.Windows.Forms.Padding(8);
|
||||
layoutTable.RowCount = 3;
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.Size = new System.Drawing.Size(824, 541);
|
||||
layoutTable.TabIndex = 0;
|
||||
//
|
||||
// releaseGrid
|
||||
//
|
||||
releaseGrid.AllowUserToAddRows = false;
|
||||
releaseGrid.AllowUserToDeleteRows = false;
|
||||
releaseGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { releasedColumn, releasedByColumn, revisionColumn, multiplierColumn, nestsColumn, progressColumn, releaseNotesColumn });
|
||||
releaseGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
releaseGrid.MultiSelect = false;
|
||||
releaseGrid.Name = "releaseGrid";
|
||||
releaseGrid.ReadOnly = true;
|
||||
releaseGrid.RowHeadersVisible = false;
|
||||
releaseGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||
releaseGrid.StandardTab = true;
|
||||
releaseGrid.TabIndex = 0;
|
||||
releaseGrid.CurrentCellChanged += ReleaseGrid_CurrentCellChanged;
|
||||
//
|
||||
// revisionColumn
|
||||
//
|
||||
rightAligned.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
|
||||
revisionColumn.DefaultCellStyle = rightAligned;
|
||||
revisionColumn.HeaderText = "Set revision";
|
||||
revisionColumn.Name = "revisionColumn";
|
||||
revisionColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
revisionColumn.Width = 70;
|
||||
//
|
||||
// releasedColumn
|
||||
//
|
||||
releasedColumn.HeaderText = "Released";
|
||||
releasedColumn.Name = "releasedColumn";
|
||||
releasedColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
releasedColumn.Width = 130;
|
||||
//
|
||||
// releasedByColumn
|
||||
//
|
||||
releasedByColumn.HeaderText = "Released by";
|
||||
releasedByColumn.Name = "releasedByColumn";
|
||||
releasedByColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
releasedByColumn.Width = 110;
|
||||
//
|
||||
// multiplierColumn
|
||||
//
|
||||
multiplierColumn.HeaderText = "Kits";
|
||||
multiplierColumn.Name = "multiplierColumn";
|
||||
multiplierColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
multiplierColumn.Width = 50;
|
||||
multiplierColumn.DefaultCellStyle = rightAligned;
|
||||
//
|
||||
// nestsColumn
|
||||
//
|
||||
nestsColumn.HeaderText = "Nests";
|
||||
nestsColumn.Name = "nestsColumn";
|
||||
nestsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
nestsColumn.Width = 55;
|
||||
nestsColumn.DefaultCellStyle = rightAligned;
|
||||
//
|
||||
// progressColumn
|
||||
//
|
||||
progressColumn.HeaderText = "Progress";
|
||||
progressColumn.Name = "progressColumn";
|
||||
progressColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
progressColumn.Width = 160;
|
||||
//
|
||||
// releaseNotesColumn
|
||||
//
|
||||
releaseNotesColumn.HeaderText = "Notes";
|
||||
releaseNotesColumn.Name = "releaseNotesColumn";
|
||||
releaseNotesColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
releaseNotesColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
|
||||
//
|
||||
// nestsGroup
|
||||
//
|
||||
nestsGroup.Controls.Add(nestGrid);
|
||||
nestsGroup.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
nestsGroup.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
nestsGroup.Name = "nestsGroup";
|
||||
nestsGroup.TabIndex = 1;
|
||||
nestsGroup.TabStop = false;
|
||||
nestsGroup.Text = "Nests of the selected release";
|
||||
//
|
||||
// nestGrid
|
||||
//
|
||||
nestGrid.AllowUserToAddRows = false;
|
||||
nestGrid.AllowUserToDeleteRows = false;
|
||||
nestGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { nestNameColumn, groupColumn, statusColumn, nestCustomerColumn, modifiedColumn });
|
||||
nestGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
nestGrid.MultiSelect = false;
|
||||
nestGrid.Name = "nestGrid";
|
||||
nestGrid.ReadOnly = true;
|
||||
nestGrid.RowHeadersVisible = false;
|
||||
nestGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||
nestGrid.StandardTab = true;
|
||||
nestGrid.TabIndex = 0;
|
||||
//
|
||||
// nestNameColumn
|
||||
//
|
||||
nestNameColumn.HeaderText = "Nest";
|
||||
nestNameColumn.Name = "nestNameColumn";
|
||||
nestNameColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
nestNameColumn.Width = 160;
|
||||
//
|
||||
// groupColumn
|
||||
//
|
||||
groupColumn.HeaderText = "Material group";
|
||||
groupColumn.Name = "groupColumn";
|
||||
groupColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
groupColumn.Width = 260;
|
||||
//
|
||||
// statusColumn
|
||||
//
|
||||
statusColumn.HeaderText = "Status";
|
||||
statusColumn.Name = "statusColumn";
|
||||
statusColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
statusColumn.Width = 110;
|
||||
//
|
||||
// nestCustomerColumn
|
||||
//
|
||||
nestCustomerColumn.HeaderText = "Customer";
|
||||
nestCustomerColumn.Name = "nestCustomerColumn";
|
||||
nestCustomerColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
nestCustomerColumn.Width = 140;
|
||||
//
|
||||
// modifiedColumn
|
||||
//
|
||||
modifiedColumn.HeaderText = "Modified";
|
||||
modifiedColumn.Name = "modifiedColumn";
|
||||
modifiedColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
modifiedColumn.Width = 130;
|
||||
//
|
||||
// bottomTable
|
||||
//
|
||||
bottomTable.AutoSize = true;
|
||||
bottomTable.ColumnCount = 4;
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
bottomTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
bottomTable.Controls.Add(previousButton, 0, 0);
|
||||
bottomTable.Controls.Add(nextButton, 1, 0);
|
||||
bottomTable.Controls.Add(summaryLabel, 2, 0);
|
||||
bottomTable.Controls.Add(closeButton, 3, 0);
|
||||
bottomTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
bottomTable.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
bottomTable.Name = "bottomTable";
|
||||
bottomTable.RowCount = 1;
|
||||
bottomTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
bottomTable.TabIndex = 2;
|
||||
//
|
||||
// previousButton
|
||||
//
|
||||
previousButton.AutoSize = true;
|
||||
previousButton.Enabled = false;
|
||||
previousButton.Name = "previousButton";
|
||||
previousButton.TabIndex = 0;
|
||||
previousButton.Text = "< Newer";
|
||||
previousButton.UseVisualStyleBackColor = true;
|
||||
previousButton.Click += PreviousButton_Click;
|
||||
//
|
||||
// nextButton
|
||||
//
|
||||
nextButton.AutoSize = true;
|
||||
nextButton.Enabled = false;
|
||||
nextButton.Name = "nextButton";
|
||||
nextButton.TabIndex = 1;
|
||||
nextButton.Text = "Older >";
|
||||
nextButton.UseVisualStyleBackColor = true;
|
||||
nextButton.Click += NextButton_Click;
|
||||
//
|
||||
// summaryLabel
|
||||
//
|
||||
summaryLabel.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
summaryLabel.AutoEllipsis = true;
|
||||
summaryLabel.Name = "summaryLabel";
|
||||
summaryLabel.TabIndex = 2;
|
||||
//
|
||||
// closeButton
|
||||
//
|
||||
closeButton.AutoSize = true;
|
||||
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
closeButton.Name = "closeButton";
|
||||
closeButton.TabIndex = 3;
|
||||
closeButton.Text = "Close";
|
||||
closeButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// SetReleasesForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
CancelButton = closeButton;
|
||||
ClientSize = new System.Drawing.Size(824, 541);
|
||||
Controls.Add(layoutTable);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||
MinimizeBox = false;
|
||||
MinimumSize = new System.Drawing.Size(600, 400);
|
||||
Name = "SetReleasesForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Set Releases";
|
||||
Shown += SetReleasesForm_Shown;
|
||||
layoutTable.ResumeLayout(false);
|
||||
layoutTable.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)releaseGrid).EndInit();
|
||||
nestsGroup.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)nestGrid).EndInit();
|
||||
bottomTable.ResumeLayout(false);
|
||||
bottomTable.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.TableLayoutPanel layoutTable;
|
||||
private System.Windows.Forms.DataGridView releaseGrid;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn revisionColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn releasedColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn releasedByColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn multiplierColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn nestsColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn progressColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn releaseNotesColumn;
|
||||
private System.Windows.Forms.GroupBox nestsGroup;
|
||||
private System.Windows.Forms.DataGridView nestGrid;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn nestNameColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn groupColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn nestCustomerColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn modifiedColumn;
|
||||
private System.Windows.Forms.TableLayoutPanel bottomTable;
|
||||
private System.Windows.Forms.Button previousButton;
|
||||
private System.Windows.Forms.Button nextButton;
|
||||
private System.Windows.Forms.Label summaryLabel;
|
||||
private System.Windows.Forms.Button closeButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Drawing Sets > Releases: one set's releases, newest first, a bounded page at a time,
|
||||
/// each with its progress ("2 of 3 cut"), and the nests of the selected release with their
|
||||
/// current status, or Deleted. The server joins every nest's current metadata on each read.
|
||||
/// </summary>
|
||||
public partial class SetReleasesForm : Form
|
||||
{
|
||||
private const int PageSize = 100;
|
||||
|
||||
private readonly IDrawingSetStore sets;
|
||||
private readonly DrawingSetRecord set;
|
||||
private SetReleasePage page;
|
||||
private int offset;
|
||||
private int pageRequest;
|
||||
|
||||
public SetReleasesForm(IDrawingSetStore sets, DrawingSetRecord set)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.sets = sets ?? throw new ArgumentNullException(nameof(sets));
|
||||
this.set = set ?? throw new ArgumentNullException(nameof(set));
|
||||
GridStyle.Apply(releaseGrid);
|
||||
GridStyle.Apply(nestGrid);
|
||||
Text = $"Releases of '{set.Name}'";
|
||||
}
|
||||
|
||||
private async void SetReleasesForm_Shown(object sender, EventArgs e) => await LoadPageAsync(0);
|
||||
|
||||
private async void PreviousButton_Click(object sender, EventArgs e) =>
|
||||
await LoadPageAsync(System.Math.Max(0, offset - PageSize));
|
||||
|
||||
private async void NextButton_Click(object sender, EventArgs e) => await LoadPageAsync(offset + PageSize);
|
||||
|
||||
private void ReleaseGrid_CurrentCellChanged(object sender, EventArgs e) =>
|
||||
ShowNests(releaseGrid.CurrentRow?.Tag as SetReleaseRecord);
|
||||
|
||||
/// <summary>Reads one page of releases; a later request discards an earlier one's result.</summary>
|
||||
internal async Task LoadPageAsync(int requestedOffset)
|
||||
{
|
||||
var request = ++pageRequest;
|
||||
previousButton.Enabled = nextButton.Enabled = false;
|
||||
summaryLabel.Text = "Loading...";
|
||||
try
|
||||
{
|
||||
var loaded = await sets.GetReleasesAsync(set.Id, requestedOffset, PageSize);
|
||||
if (IsDisposed || request != pageRequest)
|
||||
return;
|
||||
if (loaded == null)
|
||||
{
|
||||
summaryLabel.Text = "This set no longer exists on the server.";
|
||||
return;
|
||||
}
|
||||
|
||||
page = loaded;
|
||||
offset = requestedOffset;
|
||||
releaseGrid.Rows.Clear();
|
||||
nestGrid.Rows.Clear();
|
||||
foreach (var release in loaded.Items)
|
||||
{
|
||||
// Tagged before it is added: adding the first row selects it, which shows its nests.
|
||||
var row = new DataGridViewRow { Tag = release };
|
||||
row.CreateCells(releaseGrid,
|
||||
release.ReleasedAt == default ? "" : release.ReleasedAt.ToLocalTime().ToString("g", CultureInfo.CurrentCulture),
|
||||
release.ReleasedBy,
|
||||
release.SetRevision,
|
||||
$"×{release.Multiplier}",
|
||||
release.Nests.Count,
|
||||
SetReleaseText.Progress(release),
|
||||
release.Notes);
|
||||
releaseGrid.Rows.Add(row);
|
||||
}
|
||||
|
||||
// The grid only sometimes makes the first row current by itself (it depends on focus).
|
||||
if (releaseGrid.CurrentCell == null && releaseGrid.Rows.Count > 0)
|
||||
releaseGrid.CurrentCell = releaseGrid.Rows[0].Cells[0];
|
||||
|
||||
summaryLabel.Text = loaded.Total == 0
|
||||
? "This set has not been released yet."
|
||||
: $"Releases {offset + 1:N0}-{offset + loaded.Items.Count:N0} of {loaded.Total:N0}";
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException)
|
||||
{
|
||||
if (!IsDisposed && request == pageRequest)
|
||||
summaryLabel.Text = $"Could not read the releases: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!IsDisposed && request == pageRequest)
|
||||
{
|
||||
previousButton.Enabled = offset > 0;
|
||||
nextButton.Enabled = page != null && offset + page.Items.Count < page.Total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNests(SetReleaseRecord release)
|
||||
{
|
||||
nestGrid.Rows.Clear();
|
||||
if (release == null)
|
||||
{
|
||||
nestsGroup.Text = "Nests of the selected release";
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var nest in release.Nests)
|
||||
{
|
||||
var record = nest.Nest;
|
||||
nestGrid.Rows.Add(
|
||||
record?.Name ?? nest.NestId.ToString(),
|
||||
SetReleaseText.Group(nest),
|
||||
SetReleaseText.NestState(nest),
|
||||
record?.Customer ?? "",
|
||||
record == null || record.DateModified == default
|
||||
? ""
|
||||
: record.DateModified.ToString("g", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
nestsGroup.Text = $"Nests of the release on {release.ReleasedAt.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)}: "
|
||||
+ SetReleaseText.Progress(release);
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-2
@@ -360,6 +360,37 @@ 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.
|
||||
|
||||
**Release...** (the Sets tab, when the dialog was opened from a nest) reads the
|
||||
selected active set with its members and opens the release dialog. It lists one
|
||||
row per material group (`OpenNest.Data.SetReleasePlan`): material, grade,
|
||||
thickness, units, cut type, drawings, parts and status. **Kits (multiplier)**
|
||||
multiplies every quantity (1 to 1000); **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 multiplier or the archived toggle. **Release**
|
||||
confirms, then runs `OpenNest.Data.SetReleaseSession`: every ticked group's 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
|
||||
group's material, grade and thickness, the set's customer and the **Draft**
|
||||
status. Each created nest opens in its own tab, bound to its server record, and
|
||||
the release (with the release notes) is recorded. The grid then shows every
|
||||
group's outcome: Created with the nest name, Refused or Failed with the reason,
|
||||
or Not started. Cancel stops between uploads, never during one. When every nest
|
||||
was created but recording failed, the button becomes **Record Release** and
|
||||
posts the same release again. A dialog runs one release.
|
||||
|
||||
**Releases...** lists the selected set's releases, newest first: when, by whom,
|
||||
the set revision, the kit multiplier, the number of nests, progress such as
|
||||
"2 of 3 cut" (deleted nests are not counted) and the notes. The selected
|
||||
release's nests show their material group and current status, or Deleted.
|
||||
|
||||
**Nest > Edit** (Nest Info) shows, beside its buttons, the release a server nest
|
||||
came from ("Released from set 'Kit A' revision 3 ×2 on 10/20/2026 by ..."). It is
|
||||
read in the background and left blank when the nest came from none or the
|
||||
server cannot be reached.
|
||||
|
||||
## Drawing set endpoints
|
||||
|
||||
A **drawing set** (schema v7) is a named, versioned list of library drawings
|
||||
@@ -367,8 +398,7 @@ with a quantity each: a kit, an assembly 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 and edits them on the Drawing Library dialog's Sets tab
|
||||
([Drawing sets (desktop)](#drawing-sets-desktop)); releasing a set is not in the
|
||||
desktop yet.
|
||||
([Drawing sets (desktop)](#drawing-sets-desktop)), where sets are also released.
|
||||
|
||||
`OpenNest.Data.DrawingSetRecord` fields: `id` (client-allocated before the
|
||||
first save), `revision`, `name`, `customer`, `notes`, `archived`, `savedBy`,
|
||||
|
||||
Reference in new issue
Block a user