mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 05:01:05 -04:00
feat(desktop): release several sets, merge into drafts, and undo releases
The release dialog now lists the sets to release instead of one set with a multiplier. More are added by typing a name (the matching active sets fill the list as you type) and each set has its own kit count. Its groups span all the sets, and each group can make a new Draft nest or merge into a Draft nest of the same material, grade and thickness, listed with its sheet count. The status column says that a merge discards the draft's layout and that Undo Release restores it. A draft open in a tab of this desktop is refused, because nest tabs do not track unsaved edits and the merge would leave them behind. The main window tells the dialog which server nests are open. The release history shows what each release did to each nest (created, or merged from one revision to another, and what undo did) and gains Undo Release. When nests changed after the release, it names them and offers to undo the others, leaving those as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74203483c5
commit
f6781d27c3
13 files changed
+985
-151
No files matched your search
@@ -75,6 +75,25 @@ public static class SetReleaseText
|
||||
+ "\n\nUndo the rest and leave these as they are, or restore them from their history.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What the release did to one of its nests, and what undoing it did: "Created",
|
||||
/// "Merged (revision 3 → 4)", "Created; undo deleted it".
|
||||
/// </summary>
|
||||
public static string Change(SetReleaseNest nest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
var text = nest.Kind == SetReleaseNestKind.Merged
|
||||
? $"Merged (revision {nest.RevisionBefore} → {nest.RevisionAfter})"
|
||||
: "Created";
|
||||
return nest.Undo switch
|
||||
{
|
||||
SetReleaseNestUndo.Deleted => text + "; undo deleted it",
|
||||
SetReleaseNestUndo.Restored => text + $"; undo restored revision {nest.RevisionBefore}",
|
||||
SetReleaseNestUndo.Skipped => text + "; undo left it as it was",
|
||||
_ => text,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>The state of one nest of a release: its status name, or "Deleted".</summary>
|
||||
public static string NestState(SetReleaseNest nest)
|
||||
{
|
||||
|
||||
@@ -165,8 +165,19 @@ internal sealed class InMemoryDrawingSets : IDrawingSetStore
|
||||
});
|
||||
}
|
||||
|
||||
public Task<SetReleaseRecord> UndoReleaseAsync(Guid id, SetReleaseUndoRequest request, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException("The in-memory store does not hold nests, so it cannot undo a release.");
|
||||
/// <summary>Every undo request, in order.</summary>
|
||||
public List<(Guid Id, SetReleaseUndoRequest Request)> UndoRequests { get; } = new();
|
||||
|
||||
/// <summary>Answers an undo; this store holds no nests, so a test decides what the server says.</summary>
|
||||
public Func<Guid, SetReleaseUndoRequest, SetReleaseRecord>? UndoHandler { get; set; }
|
||||
|
||||
public Task<SetReleaseRecord> UndoReleaseAsync(Guid id, SetReleaseUndoRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
UndoRequests.Add((id, new SetReleaseUndoRequest { UndoneBy = request.UndoneBy, SkipNests = request.SkipNests.ToList() }));
|
||||
return UndoHandler is { } handler
|
||||
? Task.FromResult(handler(id, request))
|
||||
: throw new NotSupportedException("The in-memory store does not hold nests, so it cannot undo a release.");
|
||||
}
|
||||
|
||||
private Task<DrawingSetRecord> SetArchived(Guid id, int expectedRevision, bool archived)
|
||||
{
|
||||
|
||||
@@ -53,6 +53,25 @@ public class SetReleaseTextTests
|
||||
Assert.Equal("Released from set 'Synthetic kit' revision 3 ×2 by Synthetic operator", text);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SetReleaseNestKind.Created, null, "Created")]
|
||||
[InlineData(SetReleaseNestKind.Created, SetReleaseNestUndo.Deleted, "Created; undo deleted it")]
|
||||
[InlineData(SetReleaseNestKind.Merged, null, "Merged (revision 3 → 4)")]
|
||||
[InlineData(SetReleaseNestKind.Merged, SetReleaseNestUndo.Restored, "Merged (revision 3 → 4); undo restored revision 3")]
|
||||
[InlineData(SetReleaseNestKind.Merged, SetReleaseNestUndo.Skipped, "Merged (revision 3 → 4); undo left it as it was")]
|
||||
public void Change_SaysWhatTheReleaseAndItsUndoDid(SetReleaseNestKind kind, SetReleaseNestUndo? undo, string expected)
|
||||
{
|
||||
var nest = new SetReleaseNest
|
||||
{
|
||||
Kind = kind,
|
||||
RevisionBefore = kind == SetReleaseNestKind.Merged ? 3 : null,
|
||||
RevisionAfter = kind == SetReleaseNestKind.Merged ? 4 : 1,
|
||||
Undo = undo,
|
||||
};
|
||||
|
||||
Assert.Equal(expected, SetReleaseText.Change(nest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UndoRefusal_NamesEachChangedNest_OrSaysItWasUndoneBefore()
|
||||
{
|
||||
|
||||
@@ -43,13 +43,13 @@ public class SetReleaseFormsTests
|
||||
Assert.Equal(2, grid.Rows.Count);
|
||||
Assert.True(skipped.Visible);
|
||||
Assert.Contains("'SYN-OLD'", skipped.Text);
|
||||
Field<NumericUpDown>(form, "multiplierBox").Value = 3;
|
||||
Field<DataGridView>(form, "setGrid").Rows[0].Cells["kitsColumn"].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.Contains("Release set 'Synthetic kit' ×3 into 2 Draft nests", 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));
|
||||
@@ -64,7 +64,8 @@ public class SetReleaseFormsTests
|
||||
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);
|
||||
Assert.True(Field<DataGridView>(form, "setGrid").ReadOnly);
|
||||
Assert.False(Field<ComboBox>(form, "addSetCombo").Enabled);
|
||||
},
|
||||
TestTimeout,
|
||||
"The release test did not complete.");
|
||||
@@ -188,6 +189,147 @@ public class SetReleaseFormsTests
|
||||
"The releases view test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Release_OfATypedInSecondSet_MergesOneGroupIntoADraft_UnlessItIsOpen()
|
||||
{
|
||||
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 sets = new InMemoryDrawingSets();
|
||||
var first = CreateSet(sets, "First kit", (a, 1), (c, 1));
|
||||
CreateSet(sets, "Second kit", (b, 2));
|
||||
var nests = new MemoryNests();
|
||||
var draft = nests.AddDraft("N26-0007", "Synthetic Steel", "Grade A", 0.25);
|
||||
var opened = new List<NestRecord>();
|
||||
var messages = new List<string>();
|
||||
|
||||
using var form = new ReleaseSetForm(sets, library, nests, Server, first, (_, record) => opened.Add(record));
|
||||
form.ShowMessage = (text, _, _) => { messages.Add(text); return DialogResult.OK; };
|
||||
form.CreateNest = (group, _) => Task.FromResult(new Nest("N26-0100") { Units = group.Units });
|
||||
var openTabs = new HashSet<Guid> { draft.Id };
|
||||
form.IsNestOpen = openTabs.Contains;
|
||||
form.Show();
|
||||
|
||||
Field<ComboBox>(form, "addSetCombo").Text = "second KIT";
|
||||
Run(form.AddTypedSetAsync());
|
||||
Assert.Equal(new[] { ("First kit", 1), ("Second kit", 1) }, form.Lines().Select(l => (l.Set.Name, l.Quantity)));
|
||||
Assert.False(form.AddLine(first, 2));
|
||||
Field<DataGridView>(form, "setGrid").Rows[1].Cells["kitsColumn"].Value = "2";
|
||||
|
||||
var grid = Field<DataGridView>(form, "groupGrid");
|
||||
DataGridViewRow Steel() => grid.Rows.Cast<DataGridViewRow>().Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Steel");
|
||||
PumpUntil(() => ((DataGridViewComboBoxCell)Steel().Cells["targetColumn"]).Items.Count == 2, "the drafts to merge into");
|
||||
Assert.Equal(new object[] { 1, 4 }, new[] { a, b }.Select(d => (object)((SetReleaseGroup)Steel().Tag!).Items.Single(i => i.Drawing.Id == d.Id).Quantity));
|
||||
var mergeChoice = (string)((DataGridViewComboBoxCell)Steel().Cells["targetColumn"]).Items[1]!;
|
||||
Assert.StartsWith("Merge into N26-0007 (1 sheet", mergeChoice);
|
||||
|
||||
// An open draft is refused and the group stays a new nest.
|
||||
Steel().Cells["targetColumn"].Value = mergeChoice;
|
||||
Assert.Contains("is open in a tab here", Assert.Single(messages));
|
||||
Assert.Equal("New Draft nest", Steel().Cells["targetColumn"].Value);
|
||||
Assert.Empty(form.ChosenMerges());
|
||||
|
||||
openTabs.Clear();
|
||||
Steel().Cells["targetColumn"].Value = mergeChoice;
|
||||
Assert.Same(draft, form.ChosenMerges().Values.Single());
|
||||
Assert.Contains("Merging discards the layout of 'N26-0007'", (string)Steel().Cells["statusColumn"].Value);
|
||||
Assert.Contains("1 new Draft nest, 1 merge, 6 parts", Field<Label>(form, "summaryLabel").Text);
|
||||
|
||||
Run(form.ReleaseAsync());
|
||||
|
||||
Assert.Contains("1 new and 1 merged into existing drafts", messages[^1]);
|
||||
var release = Assert.Single(sets.Releases);
|
||||
Assert.Equal(new[] { ("First kit", 1), ("Second kit", 2) }, release.Sets.Select(l => (l.SetName, l.Quantity)));
|
||||
var merged = release.Nests.Single(n => n.Kind == SetReleaseNestKind.Merged);
|
||||
Assert.Equal((draft.Id, (int?)draft.Revision, draft.Revision + 1), (merged.NestId, merged.RevisionBefore, merged.RevisionAfter));
|
||||
Assert.Equal(SetReleaseNestKind.Created, release.Nests.Single(n => n.NestId != draft.Id).Kind);
|
||||
Assert.Equal(2, opened.Count);
|
||||
Assert.StartsWith("Merged into N26-0007", (string)Steel().Cells["statusColumn"].Value);
|
||||
},
|
||||
TestTimeout,
|
||||
"The multi-set merge release test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Releases_UndoOffersToLeaveChangedNests_AndShowsWhatTheReleaseDid()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets);
|
||||
var created = new SetReleaseNest { NestId = Guid.NewGuid(), MaterialName = "Synthetic Steel", Thickness = 0.25, RevisionAfter = 1, Nest = new NestRecord { Name = "N1", Status = NestStatus.Draft } };
|
||||
var merged = new SetReleaseNest
|
||||
{
|
||||
NestId = Guid.NewGuid(),
|
||||
Kind = SetReleaseNestKind.Merged,
|
||||
RevisionBefore = 3,
|
||||
RevisionAfter = 4,
|
||||
MaterialName = "Synthetic Aluminum",
|
||||
Thickness = 0.25,
|
||||
Nest = new NestRecord { Name = "N2", Status = NestStatus.Draft },
|
||||
};
|
||||
var release = new SetReleaseRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Sets = new[]
|
||||
{
|
||||
new SetReleaseLine { SetId = set.Id, SetRevision = 1, SetName = set.Name, Quantity = 2 },
|
||||
new SetReleaseLine { SetId = Guid.NewGuid(), SetRevision = 1, SetName = "Other kit", Quantity = 1 },
|
||||
},
|
||||
Nests = new[] { created, merged },
|
||||
};
|
||||
sets.Releases.Add(release);
|
||||
sets.UndoHandler = (_, request) =>
|
||||
{
|
||||
if (request.SkipNests.Count == 0)
|
||||
throw new SetReleaseUndoConflictException(new SetReleaseUndoConflict
|
||||
{
|
||||
Reason = SetReleaseUndoRefusal.NestsChanged,
|
||||
Release = release,
|
||||
ChangedNests = new[] { new NestRecord { Id = merged.NestId, Name = "N2", Revision = 6, Status = NestStatus.ToBeCut } },
|
||||
});
|
||||
release.UndoneAt = new DateTime(2026, 10, 21, 12, 0, 0, DateTimeKind.Utc);
|
||||
created.Undo = SetReleaseNestUndo.Deleted;
|
||||
merged.Undo = SetReleaseNestUndo.Skipped;
|
||||
return release;
|
||||
};
|
||||
var questions = new List<string>();
|
||||
|
||||
using var form = new SetReleasesForm(sets, set);
|
||||
form.ShowMessage = (text, buttons, _) =>
|
||||
{
|
||||
questions.Add(text);
|
||||
return buttons == MessageBoxButtons.YesNo ? DialogResult.Yes : DialogResult.OK;
|
||||
};
|
||||
form.Show();
|
||||
var releases = Field<DataGridView>(form, "releaseGrid");
|
||||
var nests = Field<DataGridView>(form, "nestGrid");
|
||||
PumpUntil(() => releases.Rows.Count == 1 && nests.Rows.Count == 2, "the releases");
|
||||
Assert.Equal("×2 (+1 other set)", releases.Rows[0].Cells["multiplierColumn"].Value);
|
||||
Assert.Equal(new object[] { "Created", "Merged (revision 3 → 4)" }, nests.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["changeColumn"].Value));
|
||||
Assert.True(Field<Button>(form, "undoButton").Enabled);
|
||||
|
||||
Run(form.UndoSelectedAsync());
|
||||
|
||||
Assert.Contains("1 nest it created is deleted", questions[0]);
|
||||
Assert.Contains("1 draft it merged into goes back", questions[0]);
|
||||
Assert.Contains("- N2: saved since the release (now revision 6, To Be Cut)", questions[1]);
|
||||
Assert.Equal(new[] { 0, 1 }, sets.UndoRequests.Select(r => r.Request.SkipNests.Count));
|
||||
Assert.Equal(merged.NestId, sets.UndoRequests[1].Request.SkipNests.Single());
|
||||
PumpUntil(() => (string)releases.Rows[0].Cells["progressColumn"].Value == "Undone", "the release to read as undone");
|
||||
Assert.Equal(new object[] { "Created; undo deleted it", "Merged (revision 3 → 4); undo left it as it was" },
|
||||
nests.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["changeColumn"].Value));
|
||||
Assert.False(Field<Button>(form, "undoButton").Enabled);
|
||||
},
|
||||
TestTimeout,
|
||||
"The release undo test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Browser_Release_ReadsTheSetWithItsMembers_OnlyWhenEnabled()
|
||||
{
|
||||
@@ -222,12 +364,15 @@ public class SetReleaseFormsTests
|
||||
"The browser release test did not complete.");
|
||||
}
|
||||
|
||||
private static DrawingSetRecord CreateSet(InMemoryDrawingSets sets, params (DrawingLibraryRecord Drawing, int Quantity)[] members)
|
||||
private static DrawingSetRecord CreateSet(InMemoryDrawingSets sets, params (DrawingLibraryRecord Drawing, int Quantity)[] members) =>
|
||||
CreateSet(sets, "Synthetic kit", members);
|
||||
|
||||
private static DrawingSetRecord CreateSet(InMemoryDrawingSets sets, string name, params (DrawingLibraryRecord Drawing, int Quantity)[] members)
|
||||
{
|
||||
var task = sets.CreateAsync(new DrawingSetRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Synthetic kit",
|
||||
Name = name,
|
||||
Customer = "Synthetic customer",
|
||||
Members = members.Select(m => new DrawingSetMember { DrawingId = m.Drawing.Id, Quantity = m.Quantity, Drawing = m.Drawing }).ToList(),
|
||||
});
|
||||
@@ -301,10 +446,57 @@ public class SetReleaseFormsTests
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>Stores uploads in memory under their client-allocated ids.</summary>
|
||||
/// <summary>Stores uploads in memory under their client-allocated ids, and drafts a release can merge into.</summary>
|
||||
private sealed class MemoryNests : INestRepository
|
||||
{
|
||||
private readonly Dictionary<(Guid, int), byte[]> _revisions = new();
|
||||
|
||||
public List<NestRecord> Uploaded { get; } = new();
|
||||
public List<NestRecord> Drafts { get; } = new();
|
||||
|
||||
/// <summary>A Draft nest on one empty sheet, stored as a real archive at revision 3.</summary>
|
||||
public NestRecord AddDraft(string name, string material, string grade, double thickness)
|
||||
{
|
||||
var nest = new Nest(name) { Units = Units.Inches, Material = new Material(material, grade), Thickness = thickness, Status = NestStatus.Draft };
|
||||
nest.CreatePlate();
|
||||
using var stream = new MemoryStream();
|
||||
new NestWriter(nest).Write(stream);
|
||||
var record = new NestRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Revision = 3,
|
||||
Name = name,
|
||||
Material = material,
|
||||
MaterialGrade = grade,
|
||||
Thickness = thickness,
|
||||
Status = NestStatus.Draft,
|
||||
PlateCount = 1,
|
||||
SavedAt = new DateTime(2026, 10, 21, 9, 0, 0, DateTimeKind.Utc),
|
||||
};
|
||||
_revisions[(record.Id, record.Revision)] = stream.ToArray();
|
||||
Drafts.Add(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
public Task<NestPage> QueryAsync(NestQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var found = Drafts.Where(d => query.Status == d.Status && query.Material == d.Material
|
||||
&& query.MaterialGrade == d.MaterialGrade && query.Thickness == d.Thickness).ToList();
|
||||
return Task.FromResult(new NestPage { Items = found, Total = found.Count, Limit = query.Limit });
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetRevisionFileAsync(Guid id, int revisionNumber, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(_revisions.TryGetValue((id, revisionNumber), out var bytes) ? bytes : null);
|
||||
|
||||
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] nestFile, NestRecord record, int expectedRevision,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var current = Drafts.Single(d => d.Id == id);
|
||||
Assert.Equal(current.Revision, expectedRevision);
|
||||
var saved = new NestRecord { Id = id, Revision = expectedRevision + 1, Name = record.Name, Status = record.Status };
|
||||
_revisions[(id, saved.Revision)] = nestFile;
|
||||
return Task.FromResult(saved);
|
||||
}
|
||||
|
||||
public Task<NestRecord> UploadAsync(byte[] file, NestRecord record, CancellationToken ct = default)
|
||||
{
|
||||
@@ -318,7 +510,6 @@ public class SetReleaseFormsTests
|
||||
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();
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace OpenNest.Controls
|
||||
private string serverUrl;
|
||||
private INestRepository nests;
|
||||
private Action<Nest, NestRecord> openNest;
|
||||
private Func<Guid, bool> isNestOpen = _ => false;
|
||||
private DrawingSetBrowseSession session;
|
||||
private bool pick;
|
||||
private bool busy;
|
||||
@@ -101,7 +102,7 @@ namespace OpenNest.Controls
|
||||
};
|
||||
ShowRelease = set =>
|
||||
{
|
||||
using var form = new ReleaseSetForm(sets, library, nests, this.serverUrl, set, openNest);
|
||||
using var form = new ReleaseSetForm(sets, library, nests, this.serverUrl, set, openNest) { IsNestOpen = isNestOpen };
|
||||
form.ShowDialog(FindForm());
|
||||
};
|
||||
|
||||
@@ -120,11 +121,14 @@ namespace OpenNest.Controls
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows Release, which makes nests from the selected set: uploads them through
|
||||
/// <paramref name="nests"/> and opens each with <paramref name="openNest"/>.
|
||||
/// Shows Release, which makes nests from the selected set and any others the operator
|
||||
/// adds: saves them through <paramref name="nests"/> and opens each with
|
||||
/// <paramref name="openNest"/>. <paramref name="isNestOpen"/> says whether a server nest
|
||||
/// is open in a tab, which keeps a release from merging into it.
|
||||
/// </summary>
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest)
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest, Func<Guid, bool> isNestOpen = null)
|
||||
{
|
||||
this.isNestOpen = isNestOpen ?? (_ => false);
|
||||
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));
|
||||
|
||||
@@ -160,13 +160,14 @@ namespace OpenNest.Forms
|
||||
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"/>.
|
||||
/// Lets the Sets tab release sets: nests are saved through <paramref name="nests"/>
|
||||
/// and each is opened with <paramref name="openNest"/>; a nest <paramref name="isNestOpen"/>
|
||||
/// reports as open in a tab is never merged into.
|
||||
/// </summary>
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest)
|
||||
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest, Func<Guid, bool> isNestOpen = null)
|
||||
{
|
||||
if (setBrowser.IsInitialized)
|
||||
setBrowser.EnableRelease(nests, openNest);
|
||||
setBrowser.EnableRelease(nests, openNest, isNestOpen);
|
||||
}
|
||||
|
||||
private async void TabControl_SelectedIndexChanged(object sender, EventArgs e)
|
||||
|
||||
@@ -1268,7 +1268,7 @@ 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.EnableRelease(NestStorage.CreateRepository(), main.OpenServerNest, main.IsServerNestOpen);
|
||||
form.ShowDialog(this);
|
||||
if (form.AddedDrawings.Count > 0)
|
||||
UpdateDrawingList();
|
||||
|
||||
@@ -45,6 +45,10 @@ namespace OpenNest.Forms
|
||||
LoadNest(nest, windowState, record.Id, NestStorage.Settings.ServerUrl, record.Revision);
|
||||
}
|
||||
|
||||
/// <summary>True when a tab holds the server nest <paramref name="id"/>.</summary>
|
||||
internal bool IsServerNestOpen(Guid id) =>
|
||||
id != Guid.Empty && MdiChildren.OfType<EditNestForm>().Any(form => form.Document.RemoteId == id);
|
||||
|
||||
#region File Menu Events
|
||||
|
||||
private async void New_Click(object sender, EventArgs e)
|
||||
|
||||
+182
-55
@@ -9,6 +9,7 @@ namespace OpenNest.Forms
|
||||
if (disposing)
|
||||
{
|
||||
cancellation?.Dispose();
|
||||
searchCancellation?.Dispose();
|
||||
if (components != null)
|
||||
components.Dispose();
|
||||
}
|
||||
@@ -19,12 +20,21 @@ namespace OpenNest.Forms
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
System.Windows.Forms.DataGridViewCellStyle rightAligned = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
layoutTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
setLabel = new System.Windows.Forms.Label();
|
||||
addSetTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
addSetLabel = new System.Windows.Forms.Label();
|
||||
addSetCombo = new System.Windows.Forms.ComboBox();
|
||||
addSetButton = new System.Windows.Forms.Button();
|
||||
removeSetButton = new System.Windows.Forms.Button();
|
||||
setGrid = new System.Windows.Forms.DataGridView();
|
||||
setNameColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
setCustomerColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
setRevisionColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
kitsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
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();
|
||||
@@ -37,6 +47,7 @@ namespace OpenNest.Forms
|
||||
cutTypeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
drawingsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
partsColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
targetColumn = new System.Windows.Forms.DataGridViewComboBoxColumn();
|
||||
statusColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
skippedLabel = new System.Windows.Forms.Label();
|
||||
bottomTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
@@ -44,9 +55,11 @@ namespace OpenNest.Forms
|
||||
releaseButton = new System.Windows.Forms.Button();
|
||||
closeButton = new System.Windows.Forms.Button();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
searchTimer = new System.Windows.Forms.Timer(components);
|
||||
layoutTable.SuspendLayout();
|
||||
addSetTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)setGrid).BeginInit();
|
||||
optionsTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)multiplierBox).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)groupGrid).BeginInit();
|
||||
bottomTable.SuspendLayout();
|
||||
SuspendLayout();
|
||||
@@ -56,23 +69,27 @@ namespace OpenNest.Forms
|
||||
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.Controls.Add(addSetTable, 0, 1);
|
||||
layoutTable.Controls.Add(setGrid, 0, 2);
|
||||
layoutTable.Controls.Add(optionsTable, 0, 3);
|
||||
layoutTable.Controls.Add(groupGrid, 0, 4);
|
||||
layoutTable.Controls.Add(skippedLabel, 0, 5);
|
||||
layoutTable.Controls.Add(bottomTable, 0, 6);
|
||||
layoutTable.Controls.Add(statusLabel, 0, 7);
|
||||
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.RowCount = 8;
|
||||
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.Absolute, 130F));
|
||||
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.Size = new System.Drawing.Size(1104, 661);
|
||||
layoutTable.TabIndex = 0;
|
||||
//
|
||||
// setLabel
|
||||
@@ -84,55 +101,136 @@ namespace OpenNest.Forms
|
||||
setLabel.Name = "setLabel";
|
||||
setLabel.Size = new System.Drawing.Size(100, 20);
|
||||
setLabel.TabIndex = 0;
|
||||
setLabel.Text = "Sets to release";
|
||||
//
|
||||
// addSetTable
|
||||
//
|
||||
addSetTable.AutoSize = true;
|
||||
addSetTable.ColumnCount = 4;
|
||||
addSetTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
addSetTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 360F));
|
||||
addSetTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
addSetTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
addSetTable.Controls.Add(addSetLabel, 0, 0);
|
||||
addSetTable.Controls.Add(addSetCombo, 1, 0);
|
||||
addSetTable.Controls.Add(addSetButton, 2, 0);
|
||||
addSetTable.Controls.Add(removeSetButton, 3, 0);
|
||||
addSetTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
addSetTable.Margin = new System.Windows.Forms.Padding(0, 0, 0, 3);
|
||||
addSetTable.Name = "addSetTable";
|
||||
addSetTable.RowCount = 1;
|
||||
addSetTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
addSetTable.TabIndex = 1;
|
||||
//
|
||||
// addSetLabel
|
||||
//
|
||||
addSetLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
addSetLabel.AutoSize = true;
|
||||
addSetLabel.Name = "addSetLabel";
|
||||
addSetLabel.TabIndex = 0;
|
||||
addSetLabel.Text = "Add a set:";
|
||||
//
|
||||
// addSetCombo
|
||||
//
|
||||
addSetCombo.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
addSetCombo.Name = "addSetCombo";
|
||||
addSetCombo.TabIndex = 1;
|
||||
addSetCombo.TextUpdate += AddSetCombo_TextUpdate;
|
||||
addSetCombo.KeyDown += AddSetCombo_KeyDown;
|
||||
//
|
||||
// addSetButton
|
||||
//
|
||||
addSetButton.AutoSize = true;
|
||||
addSetButton.Name = "addSetButton";
|
||||
addSetButton.TabIndex = 2;
|
||||
addSetButton.Text = "Add";
|
||||
addSetButton.UseVisualStyleBackColor = true;
|
||||
addSetButton.Click += AddSetButton_Click;
|
||||
//
|
||||
// removeSetButton
|
||||
//
|
||||
removeSetButton.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
removeSetButton.AutoSize = true;
|
||||
removeSetButton.Name = "removeSetButton";
|
||||
removeSetButton.TabIndex = 3;
|
||||
removeSetButton.Text = "Remove Set";
|
||||
removeSetButton.UseVisualStyleBackColor = true;
|
||||
removeSetButton.Click += RemoveSetButton_Click;
|
||||
//
|
||||
// setGrid
|
||||
//
|
||||
setGrid.AllowUserToAddRows = false;
|
||||
setGrid.AllowUserToDeleteRows = false;
|
||||
setGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { setNameColumn, setCustomerColumn, setRevisionColumn, kitsColumn });
|
||||
setGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
setGrid.Margin = new System.Windows.Forms.Padding(3, 3, 3, 6);
|
||||
setGrid.MultiSelect = false;
|
||||
setGrid.Name = "setGrid";
|
||||
setGrid.RowHeadersVisible = false;
|
||||
setGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||
setGrid.StandardTab = true;
|
||||
setGrid.TabIndex = 2;
|
||||
setGrid.CellValidating += SetGrid_CellValidating;
|
||||
setGrid.CellValueChanged += SetGrid_CellValueChanged;
|
||||
setGrid.SelectionChanged += SetGrid_SelectionChanged;
|
||||
//
|
||||
// setNameColumn
|
||||
//
|
||||
setNameColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
|
||||
setNameColumn.HeaderText = "Set";
|
||||
setNameColumn.Name = "setNameColumn";
|
||||
setNameColumn.ReadOnly = true;
|
||||
setNameColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
//
|
||||
// setCustomerColumn
|
||||
//
|
||||
setCustomerColumn.HeaderText = "Customer";
|
||||
setCustomerColumn.Name = "setCustomerColumn";
|
||||
setCustomerColumn.ReadOnly = true;
|
||||
setCustomerColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
setCustomerColumn.Width = 200;
|
||||
//
|
||||
// setRevisionColumn
|
||||
//
|
||||
setRevisionColumn.DefaultCellStyle = rightAligned;
|
||||
setRevisionColumn.HeaderText = "Revision";
|
||||
setRevisionColumn.Name = "setRevisionColumn";
|
||||
setRevisionColumn.ReadOnly = true;
|
||||
setRevisionColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
setRevisionColumn.Width = 75;
|
||||
//
|
||||
// kitsColumn
|
||||
//
|
||||
kitsColumn.DefaultCellStyle = rightAligned;
|
||||
kitsColumn.HeaderText = "Kits";
|
||||
kitsColumn.Name = "kitsColumn";
|
||||
kitsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
kitsColumn.ToolTipText = "How many of this set to release: every quantity in the set is multiplied by it.";
|
||||
kitsColumn.Width = 70;
|
||||
//
|
||||
// optionsTable
|
||||
//
|
||||
optionsTable.AutoSize = true;
|
||||
optionsTable.ColumnCount = 5;
|
||||
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
optionsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
optionsTable.ColumnCount = 3;
|
||||
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.Controls.Add(includeArchivedCheckBox, 0, 0);
|
||||
optionsTable.Controls.Add(notesLabel, 1, 0);
|
||||
optionsTable.Controls.Add(notesBox, 2, 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;
|
||||
optionsTable.TabIndex = 3;
|
||||
//
|
||||
// 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.TabIndex = 0;
|
||||
includeArchivedCheckBox.Text = "Include archived drawings";
|
||||
includeArchivedCheckBox.UseVisualStyleBackColor = true;
|
||||
includeArchivedCheckBox.CheckedChanged += Options_Changed;
|
||||
@@ -143,7 +241,7 @@ namespace OpenNest.Forms
|
||||
notesLabel.AutoSize = true;
|
||||
notesLabel.Margin = new System.Windows.Forms.Padding(12, 0, 3, 0);
|
||||
notesLabel.Name = "notesLabel";
|
||||
notesLabel.TabIndex = 3;
|
||||
notesLabel.TabIndex = 1;
|
||||
notesLabel.Text = "Release notes:";
|
||||
//
|
||||
// notesBox
|
||||
@@ -151,22 +249,24 @@ namespace OpenNest.Forms
|
||||
notesBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
notesBox.MaxLength = 4000;
|
||||
notesBox.Name = "notesBox";
|
||||
notesBox.TabIndex = 4;
|
||||
notesBox.TabIndex = 2;
|
||||
//
|
||||
// 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.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { includeColumn, materialColumn, gradeColumn, thicknessColumn, unitsColumn, cutTypeColumn, drawingsColumn, partsColumn, targetColumn, statusColumn });
|
||||
groupGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
groupGrid.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
|
||||
groupGrid.MultiSelect = false;
|
||||
groupGrid.Name = "groupGrid";
|
||||
groupGrid.RowHeadersVisible = false;
|
||||
groupGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||
groupGrid.StandardTab = true;
|
||||
groupGrid.TabIndex = 2;
|
||||
groupGrid.TabIndex = 4;
|
||||
groupGrid.CellValueChanged += GroupGrid_CellValueChanged;
|
||||
groupGrid.CurrentCellDirtyStateChanged += GroupGrid_CurrentCellDirtyStateChanged;
|
||||
groupGrid.DataError += GroupGrid_DataError;
|
||||
//
|
||||
// includeColumn
|
||||
//
|
||||
@@ -234,6 +334,16 @@ namespace OpenNest.Forms
|
||||
partsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
partsColumn.Width = 65;
|
||||
//
|
||||
// targetColumn
|
||||
//
|
||||
targetColumn.DisplayStyle = System.Windows.Forms.DataGridViewComboBoxDisplayStyle.DropDownButton;
|
||||
targetColumn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
targetColumn.HeaderText = "Nest";
|
||||
targetColumn.Name = "targetColumn";
|
||||
targetColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
targetColumn.ToolTipText = "Make a new Draft nest, or merge into a Draft nest of the same material and renest it.";
|
||||
targetColumn.Width = 260;
|
||||
//
|
||||
// statusColumn
|
||||
//
|
||||
statusColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
|
||||
@@ -247,9 +357,9 @@ namespace OpenNest.Forms
|
||||
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.MaximumSize = new System.Drawing.Size(1080, 0);
|
||||
skippedLabel.Name = "skippedLabel";
|
||||
skippedLabel.TabIndex = 3;
|
||||
skippedLabel.TabIndex = 5;
|
||||
skippedLabel.Visible = false;
|
||||
//
|
||||
// bottomTable
|
||||
@@ -267,7 +377,7 @@ namespace OpenNest.Forms
|
||||
bottomTable.Name = "bottomTable";
|
||||
bottomTable.RowCount = 1;
|
||||
bottomTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
bottomTable.TabIndex = 4;
|
||||
bottomTable.TabIndex = 6;
|
||||
//
|
||||
// summaryLabel
|
||||
//
|
||||
@@ -302,27 +412,34 @@ namespace OpenNest.Forms
|
||||
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;
|
||||
statusLabel.TabIndex = 7;
|
||||
//
|
||||
// searchTimer
|
||||
//
|
||||
searchTimer.Interval = 250;
|
||||
searchTimer.Tick += SearchTimer_Tick;
|
||||
//
|
||||
// ReleaseSetForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
ClientSize = new System.Drawing.Size(944, 521);
|
||||
ClientSize = new System.Drawing.Size(1104, 661);
|
||||
Controls.Add(layoutTable);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||
MinimizeBox = false;
|
||||
MinimumSize = new System.Drawing.Size(760, 400);
|
||||
MinimumSize = new System.Drawing.Size(860, 520);
|
||||
Name = "ReleaseSetForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Release Drawing Set";
|
||||
Text = "Release Drawing Sets";
|
||||
FormClosing += ReleaseSetForm_FormClosing;
|
||||
layoutTable.ResumeLayout(false);
|
||||
layoutTable.PerformLayout();
|
||||
addSetTable.ResumeLayout(false);
|
||||
addSetTable.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)setGrid).EndInit();
|
||||
optionsTable.ResumeLayout(false);
|
||||
optionsTable.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)multiplierBox).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)groupGrid).EndInit();
|
||||
bottomTable.ResumeLayout(false);
|
||||
bottomTable.PerformLayout();
|
||||
@@ -333,9 +450,17 @@ namespace OpenNest.Forms
|
||||
|
||||
private System.Windows.Forms.TableLayoutPanel layoutTable;
|
||||
private System.Windows.Forms.Label setLabel;
|
||||
private System.Windows.Forms.TableLayoutPanel addSetTable;
|
||||
private System.Windows.Forms.Label addSetLabel;
|
||||
private System.Windows.Forms.ComboBox addSetCombo;
|
||||
private System.Windows.Forms.Button addSetButton;
|
||||
private System.Windows.Forms.Button removeSetButton;
|
||||
private System.Windows.Forms.DataGridView setGrid;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn setNameColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn setCustomerColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn setRevisionColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn kitsColumn;
|
||||
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;
|
||||
@@ -348,6 +473,7 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn cutTypeColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn drawingsColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn partsColumn;
|
||||
private System.Windows.Forms.DataGridViewComboBoxColumn targetColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn;
|
||||
private System.Windows.Forms.Label skippedLabel;
|
||||
private System.Windows.Forms.TableLayoutPanel bottomTable;
|
||||
@@ -355,5 +481,6 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.Button releaseButton;
|
||||
private System.Windows.Forms.Button closeButton;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
private System.Windows.Forms.Timer searchTimer;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
@@ -14,27 +15,36 @@ 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
|
||||
/// Drawing Sets > Release: the operator lists the sets to release, typed in or added from
|
||||
/// the browser, each with a number of kits, and the dialog previews how they split into one
|
||||
/// nest per material group (<see cref="SetReleasePlan"/>), with an archived-drawing toggle
|
||||
/// and a tick per group. Each group makes a new Draft nest or merges into a Draft nest of
|
||||
/// the same material, which is renested. <see cref="SetReleaseSession"/> then builds and
|
||||
/// saves those nests, opens each in its own tab, and records the release 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 const string NewNestChoice = "New Draft nest";
|
||||
|
||||
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);
|
||||
// The draft each group merges into, by group key, so the choice survives plan rebuilds.
|
||||
private readonly Dictionary<string, NestRecord> targets = new(StringComparer.Ordinal);
|
||||
// The drafts each group could merge into, by group key; absent while still loading.
|
||||
private readonly Dictionary<string, IReadOnlyList<NestRecord>> drafts = new(StringComparer.Ordinal);
|
||||
private SetReleasePlan plan;
|
||||
private SetReleaseSession session;
|
||||
private CancellationTokenSource cancellation;
|
||||
private CancellationTokenSource searchCancellation;
|
||||
private bool populating;
|
||||
private bool running;
|
||||
private bool loadingDrafts;
|
||||
|
||||
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
@@ -42,11 +52,17 @@ namespace OpenNest.Forms
|
||||
/// <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>
|
||||
/// True when a server nest is open in a tab of this desktop. Such a draft is not merged
|
||||
/// into: the tab could hold edits the merge would leave behind.
|
||||
/// </summary>
|
||||
public Func<Guid, bool> IsNestOpen { get; set; } = _ => false;
|
||||
|
||||
/// <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>
|
||||
/// <param name="set">The first set to release, read with <see cref="IDrawingSetStore.GetAsync"/>, members joined.</param>
|
||||
/// <param name="openNest">Opens a created or merged 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)
|
||||
{
|
||||
@@ -55,47 +71,253 @@ namespace OpenNest.Forms
|
||||
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));
|
||||
ArgumentNullException.ThrowIfNull(set);
|
||||
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
|
||||
CreateNest = (group, _) => NewNestFactory.CreateAsync(this, group.Units);
|
||||
|
||||
GridStyle.Apply(setGrid);
|
||||
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();
|
||||
AddLine(set, 1);
|
||||
}
|
||||
|
||||
/// <summary>The sets listed, in order, with their kits.</summary>
|
||||
internal IReadOnlyList<SetReleasePlanLine> Lines() =>
|
||||
setGrid.Rows.Cast<DataGridViewRow>()
|
||||
.Select(r => new SetReleasePlanLine((DrawingSetRecord)r.Tag, Kits(r)))
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a set read from the server (members joined) with <paramref name="kits"/>; false,
|
||||
/// with the reason shown, when it is archived or already listed.
|
||||
/// </summary>
|
||||
internal bool AddLine(DrawingSetRecord set, int kits)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(set);
|
||||
var listed = setGrid.Rows.Cast<DataGridViewRow>().FirstOrDefault(r => ((DrawingSetRecord)r.Tag).Id == set.Id);
|
||||
if (listed != null)
|
||||
{
|
||||
listed.Selected = true;
|
||||
statusLabel.Text = $"'{set.Name}' is already listed. Change its kits instead.";
|
||||
return false;
|
||||
}
|
||||
if (set.Archived)
|
||||
{
|
||||
statusLabel.Text = $"'{set.Name}' is archived. Restore it before releasing it.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var row = new DataGridViewRow { Tag = set };
|
||||
row.CreateCells(setGrid, set.Name, set.Customer, set.Revision, kits.ToString(CultureInfo.CurrentCulture));
|
||||
setGrid.Rows.Add(row);
|
||||
statusLabel.Text = "";
|
||||
BuildPlan();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int Kits(DataGridViewRow row) =>
|
||||
int.TryParse(Convert.ToString(row.Cells["kitsColumn"].Value, CultureInfo.CurrentCulture), NumberStyles.Integer,
|
||||
CultureInfo.CurrentCulture, out var kits) ? kits : 1;
|
||||
|
||||
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 SetGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
|
||||
{
|
||||
if (e.ColumnIndex != kitsColumn.Index)
|
||||
return;
|
||||
var valid = int.TryParse(Convert.ToString(e.FormattedValue, CultureInfo.CurrentCulture), NumberStyles.Integer,
|
||||
CultureInfo.CurrentCulture, out var kits) && kits >= 1 && kits <= SetReleaseRules.MaxQuantity;
|
||||
setGrid.Rows[e.RowIndex].ErrorText = valid ? "" : $"Kits must be a whole number from 1 to {SetReleaseRules.MaxQuantity}.";
|
||||
e.Cancel = !valid;
|
||||
}
|
||||
|
||||
private void SetGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex >= 0 && e.ColumnIndex == kitsColumn.Index && !running && Result == null)
|
||||
BuildPlan();
|
||||
}
|
||||
|
||||
private void SetGrid_SelectionChanged(object sender, EventArgs e) => UpdateCommands();
|
||||
|
||||
private void RemoveSetButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (setGrid.Rows.Count < 2 || setGrid.CurrentRow == null)
|
||||
return;
|
||||
setGrid.Rows.Remove(setGrid.CurrentRow);
|
||||
BuildPlan();
|
||||
}
|
||||
|
||||
// Type-ahead: the set list is queried once typing pauses.
|
||||
private void AddSetCombo_TextUpdate(object sender, EventArgs e)
|
||||
{
|
||||
searchTimer.Stop();
|
||||
searchTimer.Start();
|
||||
}
|
||||
|
||||
private async void SearchTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
searchTimer.Stop();
|
||||
await SearchSetsAsync(addSetCombo.Text);
|
||||
}
|
||||
|
||||
private async void AddSetCombo_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode != Keys.Enter)
|
||||
return;
|
||||
e.Handled = e.SuppressKeyPress = true;
|
||||
await AddTypedSetAsync();
|
||||
}
|
||||
|
||||
private async void AddSetButton_Click(object sender, EventArgs e) => await AddTypedSetAsync();
|
||||
|
||||
/// <summary>Lists active sets whose name, customer or notes contain <paramref name="text"/>.</summary>
|
||||
internal async Task SearchSetsAsync(string text)
|
||||
{
|
||||
searchCancellation?.Cancel();
|
||||
searchCancellation?.Dispose();
|
||||
searchCancellation = new CancellationTokenSource();
|
||||
var token = searchCancellation.Token;
|
||||
DrawingSetPage page;
|
||||
try
|
||||
{
|
||||
page = await sets.QueryAsync(new DrawingSetQuery { Search = text ?? "", Limit = 20, Sort = DrawingSetSortField.Name, Descending = false }, token);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or HttpRequestException or TaskCanceledException or System.Text.Json.JsonException)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
statusLabel.Text = $"Could not search the sets: {ex.Message}";
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDisposed || token.IsCancellationRequested)
|
||||
return;
|
||||
// Refilling the list resets the text, so the operator's typing is put back.
|
||||
var typed = addSetCombo.Text;
|
||||
var caret = addSetCombo.SelectionStart;
|
||||
addSetCombo.BeginUpdate();
|
||||
addSetCombo.Items.Clear();
|
||||
foreach (var found in page.Items)
|
||||
addSetCombo.Items.Add(new SetChoice(found));
|
||||
addSetCombo.EndUpdate();
|
||||
addSetCombo.Text = typed;
|
||||
addSetCombo.SelectionStart = System.Math.Min(caret, typed.Length);
|
||||
if (addSetCombo.Focused && page.Items.Count > 0)
|
||||
addSetCombo.DroppedDown = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the set picked in the list, or the one active set whose name is exactly what was
|
||||
/// typed, read with its members.
|
||||
/// </summary>
|
||||
internal async Task AddTypedSetAsync()
|
||||
{
|
||||
if (running || Result != null)
|
||||
return;
|
||||
var typed = (addSetCombo.Text ?? "").Trim();
|
||||
try
|
||||
{
|
||||
Guid id;
|
||||
if (addSetCombo.SelectedItem is SetChoice choice && choice.ToString() == addSetCombo.Text)
|
||||
{
|
||||
id = choice.Set.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (typed.Length == 0)
|
||||
return;
|
||||
var page = await sets.QueryAsync(new DrawingSetQuery { Search = typed, Limit = 50 });
|
||||
var exact = page.Items.Where(s => string.Equals(s.Name.Trim(), typed, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (exact.Count != 1)
|
||||
{
|
||||
statusLabel.Text = exact.Count == 0
|
||||
? $"No active set is named '{typed}'. Pick one from the list."
|
||||
: $"{exact.Count} sets are named '{typed}'. Pick one from the list.";
|
||||
return;
|
||||
}
|
||||
id = exact[0].Id;
|
||||
}
|
||||
|
||||
var read = await sets.GetAsync(id);
|
||||
if (IsDisposed)
|
||||
return;
|
||||
if (read == null)
|
||||
{
|
||||
statusLabel.Text = "That set no longer exists on the server.";
|
||||
return;
|
||||
}
|
||||
if (AddLine(read, 1))
|
||||
addSetCombo.Text = "";
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or HttpRequestException or TaskCanceledException or System.Text.Json.JsonException)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
statusLabel.Text = $"Could not read the set: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// Commit a checkbox click or a target choice at once so the summary follows it.
|
||||
private void GroupGrid_CurrentCellDirtyStateChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (groupGrid.IsCurrentCellDirty && groupGrid.CurrentCell?.OwningColumn == includeColumn)
|
||||
if (groupGrid.IsCurrentCellDirty
|
||||
&& (groupGrid.CurrentCell?.OwningColumn == includeColumn || groupGrid.CurrentCell?.OwningColumn == targetColumn))
|
||||
groupGrid.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||||
}
|
||||
|
||||
private void GroupGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (populating || e.RowIndex < 0 || e.ColumnIndex != includeColumn.Index)
|
||||
if (populating || e.RowIndex < 0)
|
||||
return;
|
||||
var row = groupGrid.Rows[e.RowIndex];
|
||||
if (row.Tag is SetReleaseGroup group)
|
||||
if (row.Tag is not SetReleaseGroup group)
|
||||
return;
|
||||
if (e.ColumnIndex == includeColumn.Index)
|
||||
{
|
||||
if (row.Cells[includeColumn.Index].Value is true)
|
||||
excluded.Remove(group.Key);
|
||||
else
|
||||
excluded.Add(group.Key);
|
||||
}
|
||||
else if (e.ColumnIndex == targetColumn.Index)
|
||||
{
|
||||
ChooseTarget(row, group, row.Cells[targetColumn.Index].Value as string);
|
||||
}
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
// A combo value that is not in a rebuilt list is reset rather than reported.
|
||||
private void GroupGrid_DataError(object sender, DataGridViewDataErrorEventArgs e) => e.ThrowException = false;
|
||||
|
||||
/// <summary>Makes <paramref name="group"/> merge into the draft named by <paramref name="choice"/>, or a new nest.</summary>
|
||||
internal void ChooseTarget(DataGridViewRow row, SetReleaseGroup group, string choice)
|
||||
{
|
||||
var draft = drafts.TryGetValue(group.Key, out var candidates)
|
||||
? candidates.FirstOrDefault(d => DraftChoice(d) == choice)
|
||||
: null;
|
||||
if (draft != null && IsNestOpen(draft.Id))
|
||||
{
|
||||
ShowMessage($"'{draft.Name}' is open in a tab here. Close it first, so no edits there are left behind by the merge.",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
draft = null;
|
||||
}
|
||||
|
||||
if (draft == null)
|
||||
targets.Remove(group.Key);
|
||||
else
|
||||
targets[group.Key] = draft;
|
||||
populating = true;
|
||||
row.Cells[targetColumn.Index].Value = draft == null ? NewNestChoice : DraftChoice(draft);
|
||||
row.Cells[statusColumn.Index].Value = GroupNote(group);
|
||||
populating = false;
|
||||
}
|
||||
|
||||
private async void ReleaseButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (Result is { Release: null, ReleaseError.Length: > 0 })
|
||||
@@ -109,7 +331,7 @@ namespace OpenNest.Forms
|
||||
if (running)
|
||||
{
|
||||
cancellation?.Cancel();
|
||||
statusLabel.Text = "Stopping after the nest being uploaded...";
|
||||
statusLabel.Text = "Stopping after the nest being saved...";
|
||||
closeButton.Enabled = false;
|
||||
return;
|
||||
}
|
||||
@@ -123,39 +345,57 @@ namespace OpenNest.Forms
|
||||
if (!running)
|
||||
return;
|
||||
e.Cancel = true;
|
||||
cancellation?.Cancel();
|
||||
CloseButton_Click(sender, e);
|
||||
}
|
||||
|
||||
/// <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>The chosen groups that merge into a draft, with that draft.</summary>
|
||||
internal IReadOnlyDictionary<SetReleaseGroup, NestRecord> ChosenMerges() =>
|
||||
ChosenGroups().Where(g => targets.ContainsKey(g.Key)).ToDictionary(g => g, g => targets[g.Key]);
|
||||
|
||||
/// <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.
|
||||
/// Confirms, then makes or merges 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())
|
||||
if (running || Result != null || !setGrid.EndEdit() || !groupGrid.EndEdit())
|
||||
return;
|
||||
var chosen = ChosenGroups();
|
||||
if (chosen.Count == 0)
|
||||
return;
|
||||
var multiplier = plan.Lines[0].Quantity;
|
||||
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)
|
||||
var merges = ChosenMerges();
|
||||
if (merges.Values.FirstOrDefault(d => IsNestOpen(d.Id)) is { } open)
|
||||
{
|
||||
ShowMessage($"'{open.Name}' is open in a tab here. Close it, or choose a new nest for its group.",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = plan.Lines;
|
||||
var setsText = lines.Count == 1
|
||||
? $"set '{lines[0].Set.Name}'" + (lines[0].Quantity == 1 ? "" : $" ×{lines[0].Quantity}")
|
||||
: $"{lines.Count} sets";
|
||||
var created = chosen.Count - merges.Count;
|
||||
var text = $"Release {setsText} into {chosen.Count} Draft nest{(chosen.Count == 1 ? "" : "s")}";
|
||||
if (merges.Count > 0)
|
||||
text += $": {created} new and {merges.Count} merged into existing drafts. A merged draft loses its "
|
||||
+ "current layout and is nested again; Undo Release restores it";
|
||||
if (ShowMessage(text + ". 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);
|
||||
session = new SetReleaseSession(sets, library, nests, serverUrl, ReadPackage, WriteNest, CreateNest, ReadNest);
|
||||
var progress = new Progress<SetReleaseProgress>(ShowProgress);
|
||||
try
|
||||
{
|
||||
Result = await session.RunAsync(plan, chosen, Environment.UserName, notesBox.Text, progress, cancellation.Token);
|
||||
Result = await session.RunAsync(plan, chosen, Environment.UserName, notesBox.Text, progress, cancellation.Token, merges);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -167,14 +407,14 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var created in Result.Created)
|
||||
openNest(created.Nest, created.Record);
|
||||
foreach (var done in Result.Created)
|
||||
openNest(done.Nest, done.Record);
|
||||
if (IsDisposed)
|
||||
return;
|
||||
ShowOutcome();
|
||||
}
|
||||
|
||||
/// <summary>Posts the release again after every nest was created but recording it failed.</summary>
|
||||
/// <summary>Posts the release again after every nest was saved but recording it failed.</summary>
|
||||
internal async Task RetryRecordAsync()
|
||||
{
|
||||
if (running || session == null || !session.HasPendingRelease)
|
||||
@@ -187,7 +427,7 @@ namespace OpenNest.Forms
|
||||
var release = await session.RetryReleaseAsync();
|
||||
Result = Result.WithRelease(release);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is System.Net.Http.HttpRequestException
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException
|
||||
|| ex is TaskCanceledException || ex is System.Text.Json.JsonException)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
@@ -204,7 +444,8 @@ namespace OpenNest.Forms
|
||||
|
||||
private void BuildPlan()
|
||||
{
|
||||
plan = SetReleasePlan.Build(set, (int)multiplierBox.Value, includeArchivedCheckBox.Checked);
|
||||
plan = SetReleasePlan.Build(Lines(), includeArchivedCheckBox.Checked);
|
||||
Text = plan.Lines.Count == 1 ? $"Release '{plan.Lines[0].Set.Name}'" : $"Release {plan.Lines.Count} Drawing Sets";
|
||||
populating = true;
|
||||
groupGrid.Rows.Clear();
|
||||
foreach (var group in plan.Groups)
|
||||
@@ -219,10 +460,12 @@ namespace OpenNest.Forms
|
||||
group.IsUnassigned ? "" : group.CutType.ToString(),
|
||||
group.Items.Count,
|
||||
group.TotalQuantity,
|
||||
group.CanRelease ? string.Join(" ", group.Warnings) : group.Refusal);
|
||||
null,
|
||||
GroupNote(group));
|
||||
// A refused group cannot be ticked.
|
||||
row.Cells[includeColumn.Index].ReadOnly = !group.CanRelease;
|
||||
groupGrid.Rows.Add(row);
|
||||
FillTargets(row, group);
|
||||
}
|
||||
populating = false;
|
||||
|
||||
@@ -234,8 +477,82 @@ namespace OpenNest.Forms
|
||||
+ (skipped.Count > 5 ? $" and {skipped.Count - 5} more" : "")
|
||||
+ ". Tick Include archived drawings to release them.";
|
||||
UpdateCommands();
|
||||
_ = LoadDraftsAsync();
|
||||
}
|
||||
|
||||
// The target list of one group: a new nest, then each draft it could merge into.
|
||||
private void FillTargets(DataGridViewRow row, SetReleaseGroup group)
|
||||
{
|
||||
var cell = (DataGridViewComboBoxCell)row.Cells[targetColumn.Index];
|
||||
cell.Items.Clear();
|
||||
cell.Items.Add(NewNestChoice);
|
||||
if (group.CanRelease && drafts.TryGetValue(group.Key, out var candidates))
|
||||
{
|
||||
foreach (var draft in candidates)
|
||||
cell.Items.Add(DraftChoice(draft));
|
||||
}
|
||||
if (targets.TryGetValue(group.Key, out var target) && !cell.Items.Contains(DraftChoice(target)))
|
||||
targets.Remove(group.Key);
|
||||
cell.Value = targets.TryGetValue(group.Key, out target) ? DraftChoice(target) : NewNestChoice;
|
||||
cell.ReadOnly = !group.CanRelease;
|
||||
}
|
||||
|
||||
/// <summary>Looks up, once per group, the Draft nests it could merge into, then fills the lists.</summary>
|
||||
internal async Task LoadDraftsAsync()
|
||||
{
|
||||
if (loadingDrafts || plan == null)
|
||||
return;
|
||||
loadingDrafts = true;
|
||||
try
|
||||
{
|
||||
while (plan.Groups.FirstOrDefault(g => g.CanRelease && !drafts.ContainsKey(g.Key)) is { } group)
|
||||
{
|
||||
try
|
||||
{
|
||||
drafts[group.Key] = await SetReleasePlan.FindDraftsAsync(nests, group);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or HttpRequestException or TaskCanceledException
|
||||
or System.Text.Json.JsonException or NotSupportedException)
|
||||
{
|
||||
drafts[group.Key] = Array.Empty<NestRecord>();
|
||||
if (!IsDisposed)
|
||||
statusLabel.Text = $"Could not list the draft nests to merge into: {ex.Message}";
|
||||
}
|
||||
if (IsDisposed)
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingDrafts = false;
|
||||
}
|
||||
|
||||
if (IsDisposed || running || Result != null)
|
||||
return;
|
||||
populating = true;
|
||||
foreach (DataGridViewRow row in groupGrid.Rows)
|
||||
{
|
||||
if (row.Tag is SetReleaseGroup group)
|
||||
FillTargets(row, group);
|
||||
}
|
||||
populating = false;
|
||||
}
|
||||
|
||||
private string GroupNote(SetReleaseGroup group)
|
||||
{
|
||||
if (!group.CanRelease)
|
||||
return group.Refusal;
|
||||
var notes = group.Warnings.ToList();
|
||||
if (targets.TryGetValue(group.Key, out var draft))
|
||||
notes.Insert(0, $"Merging discards the layout of '{draft.Name}' ({draft.PlateCount} sheet"
|
||||
+ $"{(draft.PlateCount == 1 ? "" : "s")}) and nests everything again; Undo Release restores it.");
|
||||
return string.Join(" ", notes);
|
||||
}
|
||||
|
||||
private static string DraftChoice(NestRecord draft) =>
|
||||
$"Merge into {draft.Name} ({draft.PlateCount} sheet{(draft.PlateCount == 1 ? "" : "s")}, saved "
|
||||
+ $"{draft.SavedAt.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)})";
|
||||
|
||||
private void ShowProgress(SetReleaseProgress progress)
|
||||
{
|
||||
// Progress is posted, so a report can arrive after the run ended; the outcome wins.
|
||||
@@ -244,7 +561,7 @@ namespace OpenNest.Forms
|
||||
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)}...",
|
||||
SetReleaseStage.Uploading => $"Saving nest {progress.Index + 1} of {progress.Count}: {Describe(progress.Group)}...",
|
||||
_ => "Recording the release...",
|
||||
};
|
||||
}
|
||||
@@ -256,28 +573,29 @@ namespace OpenNest.Forms
|
||||
var outcome = Result.Groups.FirstOrDefault(g => ReferenceEquals(g.Group, row.Tag));
|
||||
if (outcome == null)
|
||||
continue;
|
||||
var notes = outcome.Warnings.Count == 0 ? "" : " — " + string.Join(" ", outcome.Warnings);
|
||||
row.Cells[statusColumn.Index].Value = outcome.Outcome switch
|
||||
{
|
||||
SetReleaseGroupOutcome.Created => $"Created {outcome.Record!.Name}"
|
||||
+ (outcome.Warnings.Count == 0 ? "" : " — " + string.Join(" ", outcome.Warnings)),
|
||||
SetReleaseGroupOutcome.Created when outcome.MergedInto != null => $"Merged into {outcome.Record!.Name}" + notes,
|
||||
SetReleaseGroupOutcome.Created => $"Created {outcome.Record!.Name}" + notes,
|
||||
SetReleaseGroupOutcome.Refused => "Refused: " + outcome.Error,
|
||||
SetReleaseGroupOutcome.Failed => "Failed: " + outcome.Error,
|
||||
_ => "Not started",
|
||||
};
|
||||
}
|
||||
|
||||
var created = Result.Created.Count;
|
||||
var nestsText = $"{created} nest{(created == 1 ? "" : "s")}";
|
||||
var done = Result.Created.Count;
|
||||
var nestsText = $"{done} nest{(done == 1 ? "" : "s")}";
|
||||
if (Result.Release != null)
|
||||
statusLabel.Text = $"Released: {nestsText} created and opened, and the release recorded.";
|
||||
statusLabel.Text = $"Released: {nestsText} saved 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.";
|
||||
statusLabel.Text = $"Canceled: {nestsText} saved; 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.";
|
||||
statusLabel.Text = done == 0
|
||||
? "Nothing was saved. See the Status column."
|
||||
: $"Stopped: {nestsText} saved; no release recorded. See the Status column.";
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
@@ -285,24 +603,35 @@ namespace OpenNest.Forms
|
||||
{
|
||||
var chosen = plan == null ? Array.Empty<SetReleaseGroup>() : ChosenGroups();
|
||||
var editable = !running && Result == null;
|
||||
multiplierBox.Enabled = editable;
|
||||
setGrid.ReadOnly = !editable;
|
||||
addSetCombo.Enabled = editable;
|
||||
addSetButton.Enabled = editable;
|
||||
removeSetButton.Enabled = editable && setGrid.Rows.Count > 1 && setGrid.CurrentRow != null;
|
||||
includeArchivedCheckBox.Enabled = editable;
|
||||
notesBox.ReadOnly = !editable;
|
||||
includeColumn.ReadOnly = !editable;
|
||||
targetColumn.ReadOnly = !editable;
|
||||
if (editable)
|
||||
{
|
||||
foreach (DataGridViewRow row in groupGrid.Rows)
|
||||
row.Cells[includeColumn.Index].ReadOnly = row.Tag is SetReleaseGroup { CanRelease: false };
|
||||
{
|
||||
var refused = row.Tag is SetReleaseGroup { CanRelease: false };
|
||||
row.Cells[includeColumn.Index].ReadOnly = refused;
|
||||
row.Cells[targetColumn.Index].ReadOnly = refused;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
var merged = chosen.Count(g => targets.ContainsKey(g.Key));
|
||||
summaryLabel.Text = plan == null || plan.Groups.Count == 0
|
||||
? "This set has nothing to release."
|
||||
? "These sets have 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")}";
|
||||
+ $"{chosen.Count - merged} new Draft nest{(chosen.Count - merged == 1 ? "" : "s")}"
|
||||
+ (merged == 0 ? "" : $", {merged} merge{(merged == 1 ? "" : "s")}")
|
||||
+ $", {parts:N0} part{(parts == 1 ? "" : "s")}";
|
||||
}
|
||||
|
||||
private static string Describe(SetReleaseGroup group) =>
|
||||
@@ -319,11 +648,24 @@ namespace OpenNest.Forms
|
||||
return new LibraryPackage(bytes, contents.Drawing, contents.Units, contents.Warnings);
|
||||
}
|
||||
|
||||
private static Nest ReadNest(byte[] bytes)
|
||||
{
|
||||
using var stream = new MemoryStream(bytes, writable: false);
|
||||
return new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared }.Read();
|
||||
}
|
||||
|
||||
private static byte[] WriteNest(Nest nest)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared }.Write(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>A set in the type-ahead list, shown by name and customer.</summary>
|
||||
private sealed record SetChoice(DrawingSetRecord Set)
|
||||
{
|
||||
public override string ToString() =>
|
||||
string.IsNullOrWhiteSpace(Set.Customer) ? Set.Name : $"{Set.Name} ({Set.Customer})";
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-5
@@ -34,10 +34,12 @@ namespace OpenNest.Forms
|
||||
statusColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
nestCustomerColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
modifiedColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
changeColumn = 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();
|
||||
undoButton = new System.Windows.Forms.Button();
|
||||
closeButton = new System.Windows.Forms.Button();
|
||||
layoutTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)releaseGrid).BeginInit();
|
||||
@@ -146,7 +148,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
nestGrid.AllowUserToAddRows = false;
|
||||
nestGrid.AllowUserToDeleteRows = false;
|
||||
nestGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { nestNameColumn, groupColumn, statusColumn, nestCustomerColumn, modifiedColumn });
|
||||
nestGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { nestNameColumn, groupColumn, changeColumn, statusColumn, nestCustomerColumn, modifiedColumn });
|
||||
nestGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
nestGrid.MultiSelect = false;
|
||||
nestGrid.Name = "nestGrid";
|
||||
@@ -170,6 +172,14 @@ namespace OpenNest.Forms
|
||||
groupColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
groupColumn.Width = 260;
|
||||
//
|
||||
// changeColumn
|
||||
//
|
||||
changeColumn.HeaderText = "Release did";
|
||||
changeColumn.Name = "changeColumn";
|
||||
changeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
|
||||
changeColumn.ToolTipText = "Whether the release created the nest or merged into it, and what undoing it did.";
|
||||
changeColumn.Width = 190;
|
||||
//
|
||||
// statusColumn
|
||||
//
|
||||
statusColumn.HeaderText = "Status";
|
||||
@@ -194,15 +204,17 @@ namespace OpenNest.Forms
|
||||
// bottomTable
|
||||
//
|
||||
bottomTable.AutoSize = true;
|
||||
bottomTable.ColumnCount = 4;
|
||||
bottomTable.ColumnCount = 5;
|
||||
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.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.Controls.Add(undoButton, 3, 0);
|
||||
bottomTable.Controls.Add(closeButton, 4, 0);
|
||||
bottomTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
bottomTable.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
bottomTable.Name = "bottomTable";
|
||||
@@ -237,12 +249,22 @@ namespace OpenNest.Forms
|
||||
summaryLabel.Name = "summaryLabel";
|
||||
summaryLabel.TabIndex = 2;
|
||||
//
|
||||
// undoButton
|
||||
//
|
||||
undoButton.AutoSize = true;
|
||||
undoButton.Enabled = false;
|
||||
undoButton.Name = "undoButton";
|
||||
undoButton.TabIndex = 3;
|
||||
undoButton.Text = "Undo Release...";
|
||||
undoButton.UseVisualStyleBackColor = true;
|
||||
undoButton.Click += UndoButton_Click;
|
||||
//
|
||||
// closeButton
|
||||
//
|
||||
closeButton.AutoSize = true;
|
||||
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
closeButton.Name = "closeButton";
|
||||
closeButton.TabIndex = 3;
|
||||
closeButton.TabIndex = 4;
|
||||
closeButton.Text = "Close";
|
||||
closeButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
@@ -250,7 +272,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
CancelButton = closeButton;
|
||||
ClientSize = new System.Drawing.Size(824, 541);
|
||||
ClientSize = new System.Drawing.Size(1004, 541);
|
||||
Controls.Add(layoutTable);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||
MinimizeBox = false;
|
||||
@@ -286,6 +308,7 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.DataGridView nestGrid;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn nestNameColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn groupColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn changeColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn nestCustomerColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn modifiedColumn;
|
||||
@@ -293,6 +316,7 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.Button previousButton;
|
||||
private System.Windows.Forms.Button nextButton;
|
||||
private System.Windows.Forms.Label summaryLabel;
|
||||
private System.Windows.Forms.Button undoButton;
|
||||
private System.Windows.Forms.Button closeButton;
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,12 @@ 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.
|
||||
/// Drawing Sets > Releases: the releases that include one set, newest first, a bounded
|
||||
/// page at a time, each with its progress ("2 of 3 cut"), and the nests of the selected
|
||||
/// release with what the release did to each and their current status, or Deleted. The
|
||||
/// server joins every nest's current metadata on each read. Undo Release deletes the nests
|
||||
/// a release created and restores the drafts it merged into, all or nothing; nests changed
|
||||
/// since can be left as they are.
|
||||
/// </summary>
|
||||
public partial class SetReleasesForm : Form
|
||||
{
|
||||
@@ -24,6 +27,10 @@ namespace OpenNest.Forms
|
||||
private SetReleasePage page;
|
||||
private int offset;
|
||||
private int pageRequest;
|
||||
private bool undoing;
|
||||
|
||||
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
|
||||
public SetReleasesForm(IDrawingSetStore sets, DrawingSetRecord set)
|
||||
{
|
||||
@@ -33,6 +40,7 @@ namespace OpenNest.Forms
|
||||
GridStyle.Apply(releaseGrid);
|
||||
GridStyle.Apply(nestGrid);
|
||||
Text = $"Releases of '{set.Name}'";
|
||||
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
|
||||
}
|
||||
|
||||
private async void SetReleasesForm_Shown(object sender, EventArgs e) => await LoadPageAsync(0);
|
||||
@@ -42,8 +50,72 @@ namespace OpenNest.Forms
|
||||
|
||||
private async void NextButton_Click(object sender, EventArgs e) => await LoadPageAsync(offset + PageSize);
|
||||
|
||||
private void ReleaseGrid_CurrentCellChanged(object sender, EventArgs e) =>
|
||||
private void ReleaseGrid_CurrentCellChanged(object sender, EventArgs e)
|
||||
{
|
||||
ShowNests(releaseGrid.CurrentRow?.Tag as SetReleaseRecord);
|
||||
UpdateUndo();
|
||||
}
|
||||
|
||||
private async void UndoButton_Click(object sender, EventArgs e) => await UndoSelectedAsync();
|
||||
|
||||
private SetReleaseRecord SelectedRelease => releaseGrid.CurrentRow?.Tag as SetReleaseRecord;
|
||||
|
||||
private void UpdateUndo() => undoButton.Enabled = !undoing && SelectedRelease is { IsUndone: false };
|
||||
|
||||
/// <summary>
|
||||
/// Confirms, then undoes the selected release. When nests changed since it, lists them and
|
||||
/// offers to undo the others, leaving those as they are.
|
||||
/// </summary>
|
||||
internal async Task UndoSelectedAsync()
|
||||
{
|
||||
if (undoing || SelectedRelease is not { IsUndone: false } release)
|
||||
return;
|
||||
var created = release.Nests.Count(n => n.Kind == SetReleaseNestKind.Created);
|
||||
var merged = release.Nests.Count - created;
|
||||
var question = "Undo this release?"
|
||||
+ (created == 0 ? "" : $"\n\n{created} nest{(created == 1 ? " it created is" : "s it created are")} deleted (they can be recovered).")
|
||||
+ (merged == 0 ? "" : $"\n\n{merged} draft{(merged == 1 ? " it merged into goes" : "s it merged into go")} back to how {(merged == 1 ? "it was" : "they were")} before.");
|
||||
if (ShowMessage(question, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
undoing = true;
|
||||
UpdateUndo();
|
||||
try
|
||||
{
|
||||
var request = new SetReleaseUndoRequest { UndoneBy = Environment.UserName };
|
||||
try
|
||||
{
|
||||
await sets.UndoReleaseAsync(release.Id, request);
|
||||
}
|
||||
catch (SetReleaseUndoConflictException conflict) when (conflict.Conflict.Reason == SetReleaseUndoRefusal.NestsChanged
|
||||
&& conflict.Conflict.ChangedNests.Count < release.Nests.Count)
|
||||
{
|
||||
if (IsDisposed || ShowMessage(SetReleaseText.UndoRefusal(conflict.Conflict) + "\n\nUndo the others now?",
|
||||
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
|
||||
return;
|
||||
request.SkipNests = conflict.Conflict.ChangedNests.Select(n => n.Id).ToList();
|
||||
await sets.UndoReleaseAsync(release.Id, request);
|
||||
}
|
||||
}
|
||||
catch (SetReleaseUndoConflictException conflict)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
ShowMessage(SetReleaseText.UndoRefusal(conflict.Conflict), MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException
|
||||
|| ex is System.Text.Json.JsonException)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
ShowMessage($"Could not undo the release: {ex.Message}", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
undoing = false;
|
||||
}
|
||||
|
||||
if (!IsDisposed)
|
||||
await LoadPageAsync(offset);
|
||||
}
|
||||
|
||||
/// <summary>Reads one page of releases; a later request discards an earlier one's result.</summary>
|
||||
internal async Task LoadPageAsync(int requestedOffset)
|
||||
@@ -97,6 +169,7 @@ namespace OpenNest.Forms
|
||||
if (!IsDisposed && request == pageRequest)
|
||||
{
|
||||
previousButton.Enabled = offset > 0;
|
||||
UpdateUndo();
|
||||
nextButton.Enabled = page != null && offset + page.Items.Count < page.Total;
|
||||
}
|
||||
}
|
||||
@@ -117,6 +190,7 @@ namespace OpenNest.Forms
|
||||
nestGrid.Rows.Add(
|
||||
record?.Name ?? nest.NestId.ToString(),
|
||||
SetReleaseText.Group(nest),
|
||||
SetReleaseText.Change(nest),
|
||||
SetReleaseText.NestState(nest),
|
||||
record?.Customer ?? "",
|
||||
record == null || record.DateModified == default
|
||||
|
||||
+39
-21
@@ -389,31 +389,49 @@ newer there than in the nest is listed first, then the set editor opens for
|
||||
review and Save; nothing is stored before then.
|
||||
|
||||
**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.
|
||||
selected active set with its members and opens the release dialog with it listed.
|
||||
More sets are added by typing in **Add a set** (the list of matching active sets
|
||||
fills as you type; Enter or **Add** takes the picked set, or the one set whose
|
||||
name is exactly what was typed) and removed with **Remove Set**; each set's
|
||||
**Kits** (1 to 1000) multiplies its quantities, and a set is listed once. The
|
||||
grid below has one row per material group across all the sets
|
||||
(`OpenNest.Data.SetReleasePlan`): material, grade, thickness, units, cut type,
|
||||
drawings, parts, **Nest** and status. **Include archived drawings** puts archived
|
||||
members back in, otherwise they are named below the grid as left out. Each group
|
||||
has a **Release** tick; refused groups (two drawings with one name, or drawings
|
||||
without material or thickness) show why and cannot be ticked, and a tick
|
||||
survives changing the sets, kits or the archived toggle.
|
||||
|
||||
**Nest** is **New Draft nest** or **Merge into** one of the Draft nests of the
|
||||
group's material, grade and thickness, listed with their sheet count and when
|
||||
they were saved. A merge discards that draft's layout and nests the whole demand
|
||||
again (the status column says so); **Undo Release** restores it. A draft open in
|
||||
a tab of this desktop is refused, since that tab could hold edits the merge would
|
||||
leave behind: close it first.
|
||||
|
||||
**Release** confirms, then runs `OpenNest.Data.SetReleaseSession`: every ticked
|
||||
group's new nest is made as File > New would (the server's next nest name, the
|
||||
saved plate defaults and clamps), in the group's units, with the saved plate size
|
||||
and spacings converted (25.4 mm per inch) when those differ; the nest takes the
|
||||
group's material, grade and thickness, the sets' customer when they share one,
|
||||
and the **Draft** status. A merged draft is saved only while nobody else has
|
||||
saved it since it was listed. Each created or merged 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 or Merged into with
|
||||
the nest name, Refused or Failed with the reason, or Not started. Cancel stops
|
||||
between saves, never during one. When every nest was saved but recording failed,
|
||||
the button becomes **Record Release** and posts the same release again. A dialog
|
||||
runs one release.
|
||||
|
||||
**Releases...** lists the releases that include the selected set, newest first:
|
||||
when, by whom, the set revision, the kits of this set (and how many other sets
|
||||
the release had), the number of nests, progress such as "2 of 3 cut" (deleted
|
||||
nests are not counted) or "Undone", and the notes. The selected
|
||||
release's nests show their material group and current status, or Deleted.
|
||||
nests are not counted) or "Undone", and the notes. The selected release's nests
|
||||
show their material group, what the release did ("Created", "Merged (revision
|
||||
3 → 4)", and what undo did) and current status, or Deleted. **Undo Release...**
|
||||
deletes the nests the release created (they can be recovered) and restores the
|
||||
drafts it merged into. When some nests were changed after the release, it lists
|
||||
them and offers to undo the others, leaving those as they are.
|
||||
|
||||
**Nest > Edit** (Nest Info) shows, beside its buttons, the newest release that
|
||||
made or merged into a server nest ("Released from set 'Kit A' revision 3 ×2 on
|
||||
|
||||
Reference in new issue
Block a user