mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-26 22:01:06 -04:00
feat(desktop): open Edit Drawing from everywhere and warn before duplicates
Builds on the library's Edit Drawing (details as a new revision): - warns before saving details that would duplicate another active library drawing (D4), naming them; a failed lookup also asks; - File > Drawing Library, a nest-less mode of the library browser that lists every active drawing and edits on double-click; - the set editor's member grid (double-click, right-click, button); - the release dialog: Fix Drawings on a group refused for missing material or thickness or duplicate names, so those refusals are no longer a dead end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48863d51c8
commit
0d1dade063
23 files changed
+1073
-104
No files matched your search
@@ -11,17 +11,17 @@ public sealed class DrawingLibraryBrowseSession : BrowseSession<DrawingLibraryPa
|
||||
{
|
||||
private readonly IDrawingLibrary _library;
|
||||
|
||||
public DrawingLibraryBrowseSession(IDrawingLibrary library, Nest nest, int pageSize = DrawingLibraryQuery.DefaultLimit)
|
||||
/// <param name="nest">The nest to filter to, or null to list every active drawing (File > Drawing Library).</param>
|
||||
public DrawingLibraryBrowseSession(IDrawingLibrary library, Nest? nest, int pageSize = DrawingLibraryQuery.DefaultLimit)
|
||||
: base(pageSize, DrawingLibraryQuery.MaxLimit, DrawingSortField.SavedAt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(library);
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
|
||||
_library = library;
|
||||
MaterialName = (nest.Material?.Name ?? "").Trim();
|
||||
MaterialGrade = (nest.Material?.Grade ?? "").Trim();
|
||||
MaterialName = (nest?.Material?.Name ?? "").Trim();
|
||||
MaterialGrade = (nest?.Material?.Grade ?? "").Trim();
|
||||
// A nest without a thickness has nothing to match; it filters by material only.
|
||||
Thickness = double.IsFinite(nest.Thickness) && nest.Thickness > 0
|
||||
Thickness = nest != null && double.IsFinite(nest.Thickness) && nest.Thickness > 0
|
||||
? DrawingLibraryRules.NormalizeThickness(nest.Thickness)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,32 @@ public sealed class DrawingLibraryEditSession
|
||||
SameDetails(Build(details, notes, settings), Listed)
|
||||
&& (notes ?? "") == Listed.Notes && Equals(settings, Listed.Settings);
|
||||
|
||||
/// <summary>
|
||||
/// True when the edit changes the details that identify a library drawing (D4: customer,
|
||||
/// name, material, grade, thickness and cut type, normalized as duplicates are matched).
|
||||
/// </summary>
|
||||
public bool ChangesIdentity(DrawingLibraryDetails details) =>
|
||||
!DrawingLibraryDuplicates.SameDuplicateKey(BuildDetails(details), Listed);
|
||||
|
||||
/// <summary>
|
||||
/// The other active library drawings the edited details would duplicate (D4), newest
|
||||
/// first. Empty when the edit keeps this drawing's identity, so drawings that already
|
||||
/// shared it are not reported on every save.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<DrawingLibraryRecord>> FindCollisionsAsync(DrawingLibraryDetails details,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var record = BuildDetails(details);
|
||||
if (!ChangesIdentity(details) || DrawingLibraryRules.GetMetadataError(record) != null)
|
||||
return Array.Empty<DrawingLibraryRecord>();
|
||||
var matches = await DrawingLibraryDuplicates.FindMatchesAsync(_library, record, cancellationToken);
|
||||
return matches.Where(m => m.Id != Listed.Id).ToList();
|
||||
}
|
||||
|
||||
// The record with the edited details and the listed notes and settings, for identity checks.
|
||||
private DrawingLibraryRecord BuildDetails(DrawingLibraryDetails details) =>
|
||||
Build(details, Listed.Notes, Listed.Settings ?? new DrawingNestingSettings());
|
||||
|
||||
/// <summary>Saves the edit as the next revision; returns the stored record.</summary>
|
||||
public async Task<DrawingLibraryRecord> SaveAsync(DrawingLibraryDetails details, string notes,
|
||||
DrawingNestingSettings settings, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -85,6 +85,51 @@ public sealed class DrawingLibraryDuplicates
|
||||
/// <summary>True when these duplicates were found for exactly <paramref name="metadata"/>'s matching details.</summary>
|
||||
public bool IsFor(DrawingLibraryRecord metadata) => SameDuplicateKey(Metadata, metadata);
|
||||
|
||||
/// <summary>
|
||||
/// The active library drawings whose duplicate key equals <paramref name="metadata"/>'s,
|
||||
/// newest first, reading every page the server filters down to; none when it is invalid.
|
||||
/// </summary>
|
||||
internal static async Task<List<DrawingLibraryRecord>> FindMatchesAsync(IDrawingLibrary library,
|
||||
DrawingLibraryRecord metadata, CancellationToken cancellationToken)
|
||||
{
|
||||
var matches = new List<DrawingLibraryRecord>();
|
||||
if (string.IsNullOrWhiteSpace(metadata.Name) || DrawingLibraryRules.GetMetadataError(metadata) != null)
|
||||
return matches;
|
||||
|
||||
// The server matches customer, material and grade by the same normalized key. A
|
||||
// blank one means "any" there, so every key is checked again here; the name search
|
||||
// narrows by its longest word because LIKE does not collapse whitespace.
|
||||
var offset = 0;
|
||||
while (true)
|
||||
{
|
||||
var page = await library.QueryAsync(new DrawingLibraryQuery
|
||||
{
|
||||
Search = LongestWord(metadata.Name),
|
||||
Customer = metadata.Customer,
|
||||
MaterialName = metadata.MaterialName,
|
||||
MaterialGrade = metadata.MaterialGrade,
|
||||
Thickness = metadata.Thickness,
|
||||
CutType = metadata.CutType,
|
||||
Sort = DrawingSortField.SavedAt,
|
||||
Descending = true,
|
||||
Offset = offset,
|
||||
Limit = DrawingLibraryQuery.MaxLimit,
|
||||
}, cancellationToken);
|
||||
matches.AddRange(page.Items.Where(r => !r.Archived && SameDuplicateKey(r, metadata)
|
||||
&& matches.All(m => m.Id != r.Id)));
|
||||
offset += page.Items.Count;
|
||||
if (page.Items.Count == 0 || offset >= page.Total)
|
||||
return matches;
|
||||
if (offset >= DrawingLibrarySaveSession.MaxScannedDrawings)
|
||||
throw new IOException($"More than {DrawingLibrarySaveSession.MaxScannedDrawings} library drawings share this name's details. Narrow the name or customer before saving.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string LongestWord(string name) =>
|
||||
name.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)
|
||||
.OrderByDescending(word => word.Length)
|
||||
.FirstOrDefault() ?? "";
|
||||
|
||||
internal static bool SameDuplicateKey(DrawingLibraryRecord a, DrawingLibraryRecord b) =>
|
||||
SharedListNames.Key(a.Customer) == SharedListNames.Key(b.Customer)
|
||||
&& SharedListNames.Key(a.Name) == SharedListNames.Key(b.Name)
|
||||
@@ -169,37 +214,7 @@ public sealed class DrawingLibrarySaveSession
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(metadata);
|
||||
var snapshot = Copy(metadata);
|
||||
var matches = new List<DrawingLibraryRecord>();
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.Name) && DrawingLibraryRules.GetMetadataError(snapshot) == null)
|
||||
{
|
||||
// The server matches customer, material and grade by the same normalized key. A
|
||||
// blank one means "any" there, so every key is checked again here; the name search
|
||||
// narrows by its longest word because LIKE does not collapse whitespace.
|
||||
var offset = 0;
|
||||
while (true)
|
||||
{
|
||||
var page = await _library.QueryAsync(new DrawingLibraryQuery
|
||||
{
|
||||
Search = LongestWord(snapshot.Name),
|
||||
Customer = snapshot.Customer,
|
||||
MaterialName = snapshot.MaterialName,
|
||||
MaterialGrade = snapshot.MaterialGrade,
|
||||
Thickness = snapshot.Thickness,
|
||||
CutType = snapshot.CutType,
|
||||
Sort = DrawingSortField.SavedAt,
|
||||
Descending = true,
|
||||
Offset = offset,
|
||||
Limit = DrawingLibraryQuery.MaxLimit,
|
||||
}, cancellationToken);
|
||||
matches.AddRange(page.Items.Where(r => !r.Archived && DrawingLibraryDuplicates.SameDuplicateKey(r, snapshot)
|
||||
&& matches.All(m => m.Id != r.Id)));
|
||||
offset += page.Items.Count;
|
||||
if (page.Items.Count == 0 || offset >= page.Total)
|
||||
break;
|
||||
if (offset >= MaxScannedDrawings)
|
||||
throw new IOException($"More than {MaxScannedDrawings} library drawings share this name's details. Narrow the name or customer before saving.");
|
||||
}
|
||||
}
|
||||
var matches = await DrawingLibraryDuplicates.FindMatchesAsync(_library, snapshot, cancellationToken);
|
||||
|
||||
DrawingLibraryRecord? linked = null;
|
||||
if (Drawing.Library is { } link && link.ServerKey == ServerKey
|
||||
@@ -336,11 +351,6 @@ public sealed class DrawingLibrarySaveSession
|
||||
return new DrawingLibrarySaveResult(saved, outcome, recoveredEarlierSave);
|
||||
}
|
||||
|
||||
private static string LongestWord(string name) =>
|
||||
name.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)
|
||||
.OrderByDescending(word => word.Length)
|
||||
.FirstOrDefault() ?? "";
|
||||
|
||||
private static DrawingLibraryRecord Copy(DrawingLibraryRecord record) => new()
|
||||
{
|
||||
Id = record.Id,
|
||||
|
||||
@@ -131,6 +131,21 @@ public sealed class DrawingSetEditSession
|
||||
_members[index].Quantity = quantity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows <paramref name="drawing"/>, a newer revision of a member's library drawing (for
|
||||
/// example one just edited), on that member. Members are saved by id, so the set itself is
|
||||
/// not changed; false when no member lists the drawing or it already shows that revision.
|
||||
/// </summary>
|
||||
public bool UpdateDrawing(DrawingLibraryRecord drawing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawing);
|
||||
var member = _members.FirstOrDefault(m => m.DrawingId == drawing.Id);
|
||||
if (member == null || (member.Drawing != null && member.Drawing.Revision >= drawing.Revision))
|
||||
return false;
|
||||
member.Drawing = drawing;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>The record a save sends.</summary>
|
||||
public DrawingSetRecord Build() => new()
|
||||
{
|
||||
|
||||
@@ -64,7 +64,8 @@ public sealed class SetReleaseItem
|
||||
public sealed class SetReleaseGroup
|
||||
{
|
||||
internal SetReleaseGroup(string key, DrawingLibraryRecord first, bool unassigned,
|
||||
IReadOnlyList<SetReleaseItem> items, IReadOnlyList<string> warnings, string refusal)
|
||||
IReadOnlyList<SetReleaseItem> items, IReadOnlyList<string> warnings, string refusal,
|
||||
IReadOnlyList<SetReleaseItem> refusedItems)
|
||||
{
|
||||
Key = key;
|
||||
MaterialName = (first.MaterialName ?? "").Trim();
|
||||
@@ -76,6 +77,7 @@ public sealed class SetReleaseGroup
|
||||
Items = items;
|
||||
Warnings = warnings;
|
||||
Refusal = refusal;
|
||||
RefusedItems = refusedItems;
|
||||
}
|
||||
|
||||
/// <summary>Identifies the group across rebuilds of the plan (for example after the multiplier changes).</summary>
|
||||
@@ -112,6 +114,12 @@ public sealed class SetReleaseGroup
|
||||
public string Refusal { get; }
|
||||
|
||||
public bool CanRelease => Refusal.Length == 0;
|
||||
|
||||
/// <summary>
|
||||
/// The items whose library drawings must be edited before the group can be released (all
|
||||
/// of an unassigned group, or those sharing a name), in set order; empty when it can be.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SetReleaseItem> RefusedItems { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -312,21 +320,26 @@ public sealed class SetReleasePlan
|
||||
.ToList();
|
||||
|
||||
var refusal = "";
|
||||
IReadOnlyList<SetReleaseItem> refused = Array.Empty<SetReleaseItem>();
|
||||
if (unassigned)
|
||||
{
|
||||
refusal = $"{Names(items)} {(items.Count == 1 ? "has" : "have")} no material or thickness. "
|
||||
+ "Set them on the library drawings, then release again.";
|
||||
+ "Set them on the library drawings (Edit Drawing), then release again.";
|
||||
refused = items;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Matches LibraryDrawingInsert, which refuses a second drawing with the same name.
|
||||
var duplicate = items.GroupBy(i => i.Drawing.Name, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1);
|
||||
if (duplicate != null)
|
||||
{
|
||||
refusal = $"{duplicate.Count()} drawings in this group are named '{duplicate.Key}'. "
|
||||
+ "A nest keys drawings by name, so rename all but one in the library first.";
|
||||
+ "A nest keys drawings by name, so rename all but one in the library (Edit Drawing) first.";
|
||||
refused = duplicate.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return new SetReleaseGroup(key, items[0].Drawing, unassigned, items, warnings, refusal);
|
||||
return new SetReleaseGroup(key, items[0].Drawing, unassigned, items, warnings, refusal, refused);
|
||||
}
|
||||
|
||||
private static string Names(IReadOnlyList<SetReleaseItem> items)
|
||||
|
||||
@@ -180,6 +180,50 @@ public sealed class DrawingLibraryEndToEndTests
|
||||
Assert.Equal(moved, await library.GetPackageAsync(current.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditSession_ChangesTheDetails_KeepsThePackage_AndSetsReleaseTheNewRevision()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var client = factory.CreateClient();
|
||||
using var library = new RemoteDrawingLibrary(client, client.BaseAddress!.ToString());
|
||||
using var sets = new RemoteDrawingSetStore(client, client.BaseAddress!.ToString());
|
||||
var bracket = SyntheticDrawing("Synthetic bracket", 2);
|
||||
var missing = Metadata(bracket, CutType.Laser, 0);
|
||||
missing.MaterialName = "";
|
||||
var package = DrawingPackage.Write(bracket, Units.Inches);
|
||||
var stored = await library.UploadAsync(missing, package);
|
||||
var twin = Metadata(SyntheticDrawing("Synthetic plate", 5), CutType.Laser, 0.25);
|
||||
await library.UploadAsync(twin, DrawingPackage.Write(SyntheticDrawing("Synthetic plate", 5), Units.Inches));
|
||||
var set = await sets.CreateAsync(new DrawingSetRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Synthetic kit",
|
||||
Members = new[] { new DrawingSetMember { DrawingId = stored.Id, Quantity = 2 } },
|
||||
});
|
||||
var before = SetReleasePlan.Build(set, 1, includeArchived: false);
|
||||
|
||||
var session = new DrawingLibraryEditSession(library, stored, "Synthetic operator");
|
||||
var fixedDetails = session.Details with { MaterialName = "Synthetic Steel", Thickness = 0.25 };
|
||||
var colliding = session.Details with { Name = "Synthetic plate", MaterialName = "Synthetic Steel", Thickness = 0.25 };
|
||||
var none = await session.FindCollisionsAsync(fixedDetails);
|
||||
var collisions = await session.FindCollisionsAsync(colliding);
|
||||
var settings = stored.Settings ?? new DrawingNestingSettings();
|
||||
var saved = await session.SaveAsync(fixedDetails, stored.Notes, settings);
|
||||
var stale = await Assert.ThrowsAsync<DrawingLibraryConflictException>(() => session.SaveAsync(fixedDetails, "Late", settings));
|
||||
var after = SetReleasePlan.Build((await sets.GetAsync(set.Id))!, 1, includeArchived: false);
|
||||
|
||||
Assert.Empty(none);
|
||||
Assert.Equal(twin.Id, Assert.Single(collisions).Id);
|
||||
Assert.Equal((2, "Synthetic Steel", 0.25, Units.Inches), (saved.Revision, saved.MaterialName, saved.Thickness, saved.Units));
|
||||
Assert.Equal(stored.PackageSha256, saved.PackageSha256);
|
||||
Assert.Equal(package, await library.GetRevisionPackageAsync(stored.Id, 2));
|
||||
Assert.Equal(2, stale.Current.Revision);
|
||||
Assert.True(Assert.Single(before.Groups).IsUnassigned);
|
||||
var group = Assert.Single(after.Groups);
|
||||
Assert.True(group.CanRelease);
|
||||
Assert.Equal(2, Assert.Single(group.Items).Drawing.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ArchiveRestoreAndMetadata_ThroughTheClient()
|
||||
{
|
||||
|
||||
@@ -59,6 +59,19 @@ public class DrawingLibraryBrowseSessionTests
|
||||
Assert.False(session.IsFilteredToNest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithoutANest_ListsEveryDrawing()
|
||||
{
|
||||
var library = new PagedLibrary(3);
|
||||
using var session = new DrawingLibraryBrowseSession(library, null);
|
||||
|
||||
await session.RefreshAsync();
|
||||
|
||||
var query = Assert.Single(library.Queries);
|
||||
Assert.Equal(("", "", (double?)null), (query.MaterialName, query.MaterialGrade, query.Thickness));
|
||||
Assert.False(session.IsFilteredToNest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchSortAndPaging_FollowTheNestBrowseRules()
|
||||
{
|
||||
|
||||
@@ -117,6 +117,44 @@ public class DrawingLibraryEditSessionTests
|
||||
Assert.Throws<ArgumentException>(() => new DrawingLibraryEditSession(new RecordingStore(), Listed(archived: true), ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangesIdentity_FollowsTheDuplicateKey_NotNotesOrSettings()
|
||||
{
|
||||
var session = new DrawingLibraryEditSession(new RecordingStore(), Listed(), "");
|
||||
|
||||
Assert.False(session.ChangesIdentity(session.Details));
|
||||
// Customer, name, material and grade compare by normalized key, as duplicates are matched.
|
||||
Assert.False(session.ChangesIdentity(session.Details with { Name = "test SQUARE", MaterialName = "synthetic steel" }));
|
||||
Assert.True(session.ChangesIdentity(session.Details with { Thickness = 0.1875 }));
|
||||
Assert.True(session.ChangesIdentity(session.Details with { CutType = CutType.Plasma }));
|
||||
Assert.True(session.ChangesIdentity(session.Details with { Customer = "" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindCollisionsAsync_ListsOtherActiveDrawingsWithTheNewDetails_AndSkipsTheQueryWhenUnchanged()
|
||||
{
|
||||
var listed = Listed();
|
||||
var store = new RecordingStore();
|
||||
var twin = Listed();
|
||||
twin.Name = "Test square B";
|
||||
var archived = Listed(archived: true);
|
||||
archived.Name = "Test square B";
|
||||
var near = Listed();
|
||||
near.Name = "Test square B";
|
||||
near.Thickness = 0.5;
|
||||
store.Library.AddRange(new[] { listed, twin, archived, near });
|
||||
var session = new DrawingLibraryEditSession(store, listed, "");
|
||||
|
||||
Assert.Empty(await session.FindCollisionsAsync(session.Details));
|
||||
Assert.Empty(store.Queries);
|
||||
|
||||
var collisions = await session.FindCollisionsAsync(session.Details with { Name = "Test square B" });
|
||||
|
||||
Assert.Equal(twin.Id, Assert.Single(collisions).Id);
|
||||
var query = Assert.Single(store.Queries);
|
||||
Assert.Equal(("square", "Synthetic Steel", (double?)0.25), (query.Search, query.MaterialName, query.Thickness));
|
||||
}
|
||||
|
||||
private sealed class RecordingStore : IDrawingLibraryStore
|
||||
{
|
||||
public List<(Guid Id, int Expected, DrawingLibraryRecord Record)> Updates { get; } = new();
|
||||
@@ -145,8 +183,16 @@ public class DrawingLibraryEditSessionTests
|
||||
});
|
||||
}
|
||||
|
||||
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
public List<DrawingLibraryRecord> Library { get; } = new();
|
||||
|
||||
public List<DrawingLibraryQuery> Queries { get; } = new();
|
||||
|
||||
// Every record, as a server that ignored the filters would; the session checks the key itself.
|
||||
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Queries.Add(query);
|
||||
return Task.FromResult(new DrawingLibraryPage { Items = Library.ToArray(), Total = Library.Count, Offset = query.Offset, Limit = query.Limit });
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
@@ -31,6 +31,23 @@ public class DrawingSetEditSessionTests
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => session.AddDrawings(new[] { Drawing("SYN-D") }, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateDrawing_ShowsANewerRevisionOnItsMember_WithoutChangingTheSet()
|
||||
{
|
||||
var a = Drawing("SYN-A");
|
||||
var session = new DrawingSetEditSession(new InMemoryDrawingSets(), null, "");
|
||||
session.AddDrawings(new[] { a, Drawing("SYN-B") }, 2);
|
||||
var before = session.Build();
|
||||
var edited = new DrawingLibraryRecord { Id = a.Id, Revision = 2, Name = "SYN-A2", MaterialName = "Synthetic Aluminum", Thickness = 0.125 };
|
||||
|
||||
Assert.True(session.UpdateDrawing(edited));
|
||||
Assert.False(session.UpdateDrawing(a));
|
||||
Assert.False(session.UpdateDrawing(Drawing("SYN-C")));
|
||||
|
||||
Assert.Same(edited, session.Members[0].Drawing);
|
||||
Assert.Equal(before.Members.Select(m => (m.DrawingId, m.Quantity)), session.Build().Members.Select(m => (m.DrawingId, m.Quantity)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveRemoveAndQuantity_ChangeTheMemberList()
|
||||
{
|
||||
|
||||
@@ -219,6 +219,8 @@ public class SetReleasePlanTests
|
||||
var laser = plan.Groups.Single(g => g.CutType == CutType.Laser);
|
||||
Assert.False(laser.CanRelease);
|
||||
Assert.Contains("2 drawings in this group are named 'SYN-A'", laser.Refusal);
|
||||
Assert.Equal(new[] { "SYN-A", "SYN-A" }, laser.RefusedItems.Select(i => i.Drawing.Name));
|
||||
Assert.Empty(plan.Groups.Single(g => g.CutType == CutType.Plasma).RefusedItems);
|
||||
// The same name in another group, or differently cased, nests separately.
|
||||
Assert.True(plan.Groups.Single(g => g.CutType == CutType.Plasma).CanRelease);
|
||||
}
|
||||
@@ -238,6 +240,7 @@ public class SetReleasePlanTests
|
||||
Assert.Equal(new[] { "SYN-NOMAT", "SYN-NOTHICK" }, unassigned.Items.Select(i => i.Drawing.Name));
|
||||
Assert.False(unassigned.CanRelease);
|
||||
Assert.Contains("no material or thickness", unassigned.Refusal);
|
||||
Assert.Equal(unassigned.Items, unassigned.RefusedItems);
|
||||
Assert.False(plan.Groups[0].IsUnassigned);
|
||||
}
|
||||
|
||||
|
||||
@@ -318,6 +318,92 @@ public class DrawingLibraryFormTests
|
||||
"The drawing library details edit test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EditDrawing_WarnsBeforeDuplicatingAnotherDrawing_AndCancelSavesNothing()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new FakeLibrary();
|
||||
var record = library.Add("SYN-606", "", "", 0, Units.Millimeters, "No material yet");
|
||||
var twin = library.Add("SYN-707", "Synthetic Steel", "Grade A", 6.35, Units.Millimeters, "");
|
||||
var warnings = new List<string>();
|
||||
var answer = DialogResult.Cancel;
|
||||
|
||||
using var editor = new EditLibraryDrawingForm(new DrawingLibraryEditSession(library, record, "Synthetic operator"));
|
||||
editor.ShowMessage = (text, _, _) => { warnings.Add(text); return answer; };
|
||||
editor.Show();
|
||||
Field<TextBox>(editor, "nameBox").Text = "SYN-707";
|
||||
Field<TextBox>(editor, "materialBox").Text = "Synthetic Steel";
|
||||
Field<TextBox>(editor, "gradeBox").Text = "Grade A";
|
||||
Field<NumericUpDown>(editor, "thicknessBox").Value = 6.35m;
|
||||
|
||||
// The new details duplicate SYN-707: cancelling the warning saves nothing.
|
||||
Run(editor.SaveAsync());
|
||||
Assert.Contains("'SYN-707'", Assert.Single(warnings));
|
||||
Assert.Empty(library.MetadataUpdates);
|
||||
Assert.Null(editor.Saved);
|
||||
|
||||
Field<TextBox>(editor, "nameBox").Text = "SYN-606";
|
||||
Field<ComboBox>(editor, "cutTypeBox").SelectedItem = "Plasma";
|
||||
answer = DialogResult.OK;
|
||||
Run(editor.SaveAsync());
|
||||
|
||||
Assert.Single(warnings);
|
||||
var update = Assert.Single(library.MetadataUpdates);
|
||||
Assert.Equal(("SYN-606", "Synthetic Steel", "Grade A", 6.35, Units.Millimeters, CutType.Plasma),
|
||||
(update.Record.Name, update.Record.MaterialName, update.Record.MaterialGrade, update.Record.Thickness,
|
||||
update.Record.Units, update.Record.CutType));
|
||||
Assert.NotEqual(twin.Id, editor.Saved!.Id);
|
||||
},
|
||||
TestTimeout,
|
||||
"The edit drawing duplicate warning test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithoutANest_ListsEveryDrawing_HasNoAdd_AndEditsOnDoubleClick()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new FakeLibrary();
|
||||
var record = library.Add("SYN-909", "", "", 0, Units.Inches, "");
|
||||
var edited = new List<DrawingLibraryEditSession>();
|
||||
|
||||
using var form = new DrawingLibraryForm(library, Server, null);
|
||||
form.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
|
||||
form.LoadPreview = (_, _) => Task.FromResult<Drawing>(null!);
|
||||
form.EditDrawing = edit =>
|
||||
{
|
||||
edited.Add(edit);
|
||||
var save = edit.SaveAsync(edit.Details with { MaterialName = "Synthetic Steel", Thickness = 0.25 },
|
||||
edit.Listed.Notes, edit.Listed.Settings ?? new DrawingNestingSettings());
|
||||
return Run(save);
|
||||
};
|
||||
form.Show();
|
||||
var grid = Field<DataGridView>(form, "drawingGrid");
|
||||
PumpUntil(() => grid.Rows.Count == 1, "the library page");
|
||||
|
||||
var query = Assert.Single(library.Queries);
|
||||
Assert.Equal(("", "", (double?)null), (query.MaterialName, query.MaterialGrade, query.Thickness));
|
||||
Assert.StartsWith("Drawing Library", form.Text);
|
||||
Assert.DoesNotContain(Field<TabPage>(form, "nestTab"), Field<TabControl>(form, "tabControl").TabPages.Cast<TabPage>());
|
||||
Assert.False(Field<Button>(form, "addButton").Visible);
|
||||
Assert.Equal("Edit Drawing...", Field<Button>(form, "editButton").Text);
|
||||
Run(form.AddSelectedAsync());
|
||||
Assert.Empty(library.Downloads);
|
||||
|
||||
typeof(DataGridView).GetMethod("OnCellDoubleClick", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(grid, new object[] { new DataGridViewCellEventArgs(0, 0) });
|
||||
PumpUntil(() => Equals(grid.Rows[0].Cells["revisionColumn"].Value, record.Revision + 1), "the reloaded page");
|
||||
|
||||
Assert.Equal(record.Id, Assert.Single(edited).Listed.Id);
|
||||
Assert.Equal("Synthetic Steel", grid.Rows[0].Cells["materialColumn"].Value);
|
||||
},
|
||||
TestTimeout,
|
||||
"The library-only dialog test did not complete.");
|
||||
}
|
||||
|
||||
private static Nest MatchingNest() => new("Synthetic job")
|
||||
{
|
||||
Units = Units.Inches,
|
||||
@@ -327,6 +413,12 @@ public class DrawingLibraryFormTests
|
||||
|
||||
private static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the add to finish");
|
||||
|
||||
private static T Run<T>(Task<T> task)
|
||||
{
|
||||
Run((Task)task);
|
||||
return task.Result;
|
||||
}
|
||||
|
||||
private static void PumpUntil(Func<bool> condition, string what)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddSeconds(15);
|
||||
|
||||
@@ -341,6 +341,53 @@ public class DrawingSetFormsTests
|
||||
"The set editor test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Editor_EditDrawing_ReadsTheCurrentRecord_AndShowsWhatWasSaved_WithoutChangingTheSet()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new FakeLibrary();
|
||||
var a = library.Add("SYN-A", "", 0);
|
||||
var b = library.Add("SYN-B", "Synthetic Steel", 0.25);
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var created = Run(sets.CreateAsync(new DrawingSetRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Synthetic kit",
|
||||
Members = new[] { a, b }.Select(d => new DrawingSetMember { DrawingId = d.Id, Quantity = 2, Drawing = d }).ToList(),
|
||||
}));
|
||||
// The library moved on since the set was read: the edit starts from the current record.
|
||||
library.Replace(a.Id, r => r.Notes = "Newer notes");
|
||||
var edits = new List<DrawingLibraryEditSession>();
|
||||
|
||||
using var editor = new EditDrawingSetForm(new DrawingSetEditSession(sets, created, "Synthetic operator"), library, Server);
|
||||
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
|
||||
editor.EditDrawing = edit =>
|
||||
{
|
||||
edits.Add(edit);
|
||||
return Run(edit.SaveAsync(edit.Details with { MaterialName = "Synthetic Aluminum", Thickness = 0.125 }, edit.Listed.Notes, edit.Listed.Settings ?? new DrawingNestingSettings()));
|
||||
};
|
||||
editor.Show();
|
||||
var grid = Field<DataGridView>(editor, "memberGrid");
|
||||
Assert.Contains("double-click", (string)grid.Rows[0].Cells["warningColumn"].Value);
|
||||
grid.CurrentCell = grid.Rows[0].Cells["drawingColumn"];
|
||||
Assert.True(Field<Button>(editor, "editDrawingButton").Enabled);
|
||||
|
||||
Run(editor.EditSelectedDrawingAsync());
|
||||
|
||||
var edit = Assert.Single(edits);
|
||||
Assert.Equal((a.Id, 2, "Newer notes"), (edit.Listed.Id, edit.Listed.Revision, edit.Listed.Notes));
|
||||
Assert.Equal("Synthetic Aluminum", grid.Rows[0].Cells["materialColumn"].Value);
|
||||
Assert.Equal("", grid.Rows[0].Cells["warningColumn"].Value);
|
||||
Assert.Contains("revision 3", Field<Label>(editor, "statusLabel").Text);
|
||||
// Members are listed by id, so the set has nothing new to save.
|
||||
Assert.False(Field<Button>(editor, "saveButton").Enabled);
|
||||
},
|
||||
TestTimeout,
|
||||
"The set editor edit drawing test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void History_ListsRevisionsNewestFirst_AndTheSelectedRevisionsMembers()
|
||||
{
|
||||
@@ -564,8 +611,45 @@ public class DrawingSetFormsTests
|
||||
int expectedRevision, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
/// <summary>Stores the next revision of a drawing, as another operator's save would.</summary>
|
||||
public void Replace(Guid id, Action<DrawingLibraryRecord> edit)
|
||||
{
|
||||
var index = _records.FindIndex(r => r.Id == id);
|
||||
var next = Copy(_records[index]);
|
||||
next.Revision++;
|
||||
edit(next);
|
||||
_records[index] = next;
|
||||
}
|
||||
|
||||
public Task<DrawingLibraryRecord> UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var index = _records.FindIndex(r => r.Id == id);
|
||||
if (_records[index].Revision != expectedRevision)
|
||||
throw new DrawingLibraryConflictException(_records[index]);
|
||||
var saved = Copy(record);
|
||||
saved.Revision = expectedRevision + 1;
|
||||
saved.Units = _records[index].Units;
|
||||
saved.PackageSha256 = _records[index].PackageSha256;
|
||||
_records[index] = saved;
|
||||
return Task.FromResult(saved);
|
||||
}
|
||||
|
||||
private static DrawingLibraryRecord Copy(DrawingLibraryRecord r) => new()
|
||||
{
|
||||
Id = r.Id,
|
||||
Revision = r.Revision,
|
||||
Name = r.Name,
|
||||
Customer = r.Customer,
|
||||
MaterialName = r.MaterialName,
|
||||
MaterialGrade = r.MaterialGrade,
|
||||
Thickness = r.Thickness,
|
||||
Units = r.Units,
|
||||
CutType = r.CutType,
|
||||
Notes = r.Notes,
|
||||
Settings = r.Settings,
|
||||
PackageSha256 = r.PackageSha256,
|
||||
SavedAt = r.SavedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,56 @@ public class SetReleaseFormsTests
|
||||
"The release choice test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixDrawings_EditsEachDrawingTheGroupIsRefusedFor_ThenTheGroupCanBeReleased()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new PackageLibrary();
|
||||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||||
var noMaterial = library.Add("SYN-B", "");
|
||||
var noThickness = library.Add("SYN-C", "Synthetic Steel", thickness: 0);
|
||||
var sets = new InMemoryDrawingSets();
|
||||
var set = CreateSet(sets, (a, 1), (noMaterial, 2), (noThickness, 3));
|
||||
var nests = new MemoryNests();
|
||||
var edits = new List<string>();
|
||||
|
||||
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.EditDrawing = edit =>
|
||||
{
|
||||
edits.Add(edit.Listed.Name);
|
||||
return Run(edit.SaveAsync(edit.Details with { MaterialName = "Synthetic Steel", Thickness = 0.25 }, edit.Listed.Notes, edit.Listed.Settings ?? new DrawingNestingSettings()));
|
||||
};
|
||||
Field<CheckBox>(form, "nestAfterReleaseCheckBox").Checked = false;
|
||||
form.Show();
|
||||
var grid = Field<DataGridView>(form, "groupGrid");
|
||||
var fix = Field<Button>(form, "fixDrawingsButton");
|
||||
var refused = grid.Rows.Cast<DataGridViewRow>().Single(r => ((SetReleaseGroup)r.Tag!).IsUnassigned);
|
||||
Assert.Contains("Edit Drawing", (string)refused.Cells["statusColumn"].Value);
|
||||
grid.CurrentCell = grid.Rows[0].Cells["materialColumn"];
|
||||
Assert.False(fix.Enabled);
|
||||
grid.CurrentCell = refused.Cells["materialColumn"];
|
||||
Assert.True(fix.Enabled);
|
||||
|
||||
Run(form.FixSelectedGroupAsync());
|
||||
|
||||
Assert.Equal(new[] { "SYN-B", "SYN-C" }, edits);
|
||||
var group = Assert.IsType<SetReleaseGroup>(Assert.Single(grid.Rows.Cast<DataGridViewRow>()).Tag);
|
||||
Assert.True(group.CanRelease);
|
||||
Assert.Equal(new[] { ("SYN-A", 1), ("SYN-B", 2), ("SYN-C", 2) }, group.Items.Select(i => (i.Drawing.Name, i.Drawing.Revision)));
|
||||
|
||||
Run(form.ReleaseAsync());
|
||||
|
||||
Assert.Single(nests.Uploaded);
|
||||
Assert.Equal("Synthetic Steel", Assert.Single(Assert.Single(sets.Releases).Nests).MaterialName);
|
||||
},
|
||||
TestTimeout,
|
||||
"The fix drawings test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleaseNotRecorded_CanBeRecordedAgain()
|
||||
{
|
||||
@@ -720,6 +770,12 @@ public class SetReleaseFormsTests
|
||||
|
||||
private static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the operation to finish");
|
||||
|
||||
private static T Run<T>(Task<T> task)
|
||||
{
|
||||
Run((Task)task);
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void PumpUntil(Func<bool> condition, string what)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddSeconds(15);
|
||||
@@ -741,7 +797,7 @@ public class SetReleaseFormsTests
|
||||
private readonly List<DrawingLibraryRecord> _records = new();
|
||||
private readonly Dictionary<(Guid, int), byte[]> _packages = new();
|
||||
|
||||
public DrawingLibraryRecord Add(string name, string material, bool archived = false)
|
||||
public DrawingLibraryRecord Add(string name, string material, bool archived = false, double thickness = 0.25)
|
||||
{
|
||||
var package = DrawingPackage.Write(new RectangleShape { Name = name, Length = 4, Width = 2 }.GetDrawing(), Units.Inches);
|
||||
var record = new DrawingLibraryRecord
|
||||
@@ -751,7 +807,7 @@ public class SetReleaseFormsTests
|
||||
Name = name,
|
||||
MaterialName = material,
|
||||
MaterialGrade = "Grade A",
|
||||
Thickness = 0.25,
|
||||
Thickness = thickness,
|
||||
Units = Units.Inches,
|
||||
CutType = CutType.Laser,
|
||||
Archived = archived,
|
||||
@@ -769,7 +825,7 @@ public class SetReleaseFormsTests
|
||||
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();
|
||||
Task.FromResult(_records.FirstOrDefault(r => r.Id == id));
|
||||
|
||||
public Task<DrawingLibraryPage?> GetRevisionsAsync(Guid id, int offset = 0,
|
||||
int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) =>
|
||||
@@ -781,8 +837,33 @@ public class SetReleaseFormsTests
|
||||
public Task<DrawingLibraryRecord> UpdatePackageAsync(Guid id, DrawingLibraryRecord record, byte[] package,
|
||||
int expectedRevision, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>Stores the next revision of the sent details with the same package.</summary>
|
||||
public Task<DrawingLibraryRecord> UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var index = _records.FindIndex(r => r.Id == id);
|
||||
var current = _records[index];
|
||||
if (current.Revision != expectedRevision)
|
||||
throw new DrawingLibraryConflictException(current);
|
||||
var saved = new DrawingLibraryRecord
|
||||
{
|
||||
Id = id,
|
||||
Revision = expectedRevision + 1,
|
||||
Name = record.Name,
|
||||
Customer = record.Customer,
|
||||
MaterialName = record.MaterialName,
|
||||
MaterialGrade = record.MaterialGrade,
|
||||
Thickness = record.Thickness,
|
||||
Units = current.Units,
|
||||
CutType = record.CutType,
|
||||
Notes = record.Notes,
|
||||
Settings = record.Settings,
|
||||
PackageSha256 = current.PackageSha256,
|
||||
};
|
||||
_records[index] = saved;
|
||||
_packages[(id, saved.Revision)] = _packages[(id, current.Revision)];
|
||||
return Task.FromResult(saved);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stores uploads in memory under their client-allocated ids, and drafts a release can merge into.</summary>
|
||||
|
||||
+15
-1
@@ -87,6 +87,8 @@ namespace OpenNest.Forms
|
||||
searchTimer = new System.Windows.Forms.Timer(components);
|
||||
previewTimer = new System.Windows.Forms.Timer(components);
|
||||
drawingMenu = new System.Windows.Forms.ContextMenuStrip(components);
|
||||
editMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
editSeparator = new System.Windows.Forms.ToolStripSeparator();
|
||||
usedInSetsMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
addToSetMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
releaseSeparator = new System.Windows.Forms.ToolStripSeparator();
|
||||
@@ -768,10 +770,20 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// drawingMenu
|
||||
//
|
||||
drawingMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { usedInSetsMenuItem, addToSetMenuItem, releaseSeparator, releaseMenuItem, releasesMenuItem });
|
||||
drawingMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { editMenuItem, editSeparator, usedInSetsMenuItem, addToSetMenuItem, releaseSeparator, releaseMenuItem, releasesMenuItem });
|
||||
drawingMenu.Name = "drawingMenu";
|
||||
drawingMenu.Opening += DrawingMenu_Opening;
|
||||
//
|
||||
// editMenuItem
|
||||
//
|
||||
editMenuItem.Name = "editMenuItem";
|
||||
editMenuItem.Text = "Edit Drawing...";
|
||||
editMenuItem.Click += EditButton_Click;
|
||||
//
|
||||
// editSeparator
|
||||
//
|
||||
editSeparator.Name = "editSeparator";
|
||||
//
|
||||
// usedInSetsMenuItem
|
||||
//
|
||||
usedInSetsMenuItem.Name = "usedInSetsMenuItem";
|
||||
@@ -921,6 +933,8 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.Timer searchTimer;
|
||||
private System.Windows.Forms.Timer previewTimer;
|
||||
private System.Windows.Forms.ContextMenuStrip drawingMenu;
|
||||
private System.Windows.Forms.ToolStripMenuItem editMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator editSeparator;
|
||||
private System.Windows.Forms.ToolStripMenuItem usedInSetsMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem addToSetMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator releaseSeparator;
|
||||
|
||||
@@ -14,13 +14,15 @@ using OpenNest.IO;
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Nest > Add from Drawing Library (Database mode). Browses the server drawing library
|
||||
/// one bounded page at a time through <see cref="DrawingLibraryBrowseSession"/>, filtered
|
||||
/// to the nest's material and thickness unless Show all is ticked. Add downloads the
|
||||
/// listed revision's exact package, plans it with <see cref="LibraryDrawingInsert"/>,
|
||||
/// shows any warning or refusal first and only then changes the nest. Edit Drawing
|
||||
/// changes the selected drawing's details (name, customer, material, grade, thickness,
|
||||
/// cut type), notes and nesting settings (Cut first, priority, rotation, color) on the server as a new revision with the same package. With a set
|
||||
/// Nest > Add from Drawing Library and File > Drawing Library (Database mode). Browses
|
||||
/// the server drawing library one bounded page at a time through
|
||||
/// <see cref="DrawingLibraryBrowseSession"/>, filtered to the nest's material and thickness
|
||||
/// unless Show all is ticked. Add downloads the listed revision's exact package, plans it
|
||||
/// with <see cref="LibraryDrawingInsert"/>, shows any warning or refusal first and only then
|
||||
/// changes the nest. Opened without a nest, it lists every active drawing and has no Add or
|
||||
/// In This Nest tab; a double-click edits the drawing instead. Edit Drawing changes the
|
||||
/// selected drawing's details, notes and nesting settings (Cut first, priority, rotation,
|
||||
/// color) on the server as a new revision with the same package. With a set
|
||||
/// store, the drawing grid's menu lists the sets a drawing is in, adds it to a set, lists
|
||||
/// the releases that had it on its own and, once release is enabled, releases it; sets
|
||||
/// themselves are browsed and released in <see cref="DrawingSetsForm"/>.
|
||||
@@ -56,7 +58,7 @@ namespace OpenNest.Forms
|
||||
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
|
||||
/// <summary>Shows the settings editor and returns the saved record, or null; replaced by tests.</summary>
|
||||
/// <summary>Shows Edit Drawing and returns the saved record, or null; replaced by tests.</summary>
|
||||
internal Func<DrawingLibraryEditSession, DrawingLibraryRecord> EditDrawing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -89,6 +91,7 @@ namespace OpenNest.Forms
|
||||
/// </summary>
|
||||
public bool QuantitiesChanged { get; private set; }
|
||||
|
||||
/// <param name="nest">The nest to add drawings to, or null to browse and edit the library alone.</param>
|
||||
/// <param name="sets">Enables Used in Sets and Add to Set; null hides them.</param>
|
||||
public DrawingLibraryForm(IDrawingLibraryStore library, string serverUrl, Nest nest, IDrawingSetStore sets = null)
|
||||
{
|
||||
@@ -100,7 +103,7 @@ namespace OpenNest.Forms
|
||||
};
|
||||
this.library = library ?? throw new ArgumentNullException(nameof(library));
|
||||
this.serverUrl = serverUrl ?? "";
|
||||
this.nest = nest ?? throw new ArgumentNullException(nameof(nest));
|
||||
this.nest = nest;
|
||||
this.sets = sets;
|
||||
session = new DrawingLibraryBrowseSession(library, nest);
|
||||
Disposed += (_, _) => session.Dispose();
|
||||
@@ -148,10 +151,19 @@ namespace OpenNest.Forms
|
||||
revisionColumn.Tag = DrawingSortField.Revision;
|
||||
savedColumn.Tag = DrawingSortField.SavedAt;
|
||||
|
||||
Text = $"Add from Drawing Library — {this.serverUrl}";
|
||||
Text = nest == null ? $"Drawing Library — {this.serverUrl}" : $"Add from Drawing Library — {this.serverUrl}";
|
||||
if (nest == null)
|
||||
{
|
||||
tabControl.TabPages.Remove(nestTab);
|
||||
addButton.Visible = false;
|
||||
showAllCheckBox.Visible = false;
|
||||
// The quantity is only for adding to a set or releasing.
|
||||
quantityLabel.Visible = quantityBox.Visible = sets != null;
|
||||
}
|
||||
|
||||
if (sets != null)
|
||||
drawingGrid.ContextMenuStrip = drawingMenu;
|
||||
drawingGrid.ContextMenuStrip = drawingMenu;
|
||||
editSeparator.Visible = sets != null;
|
||||
usedInSetsMenuItem.Visible = addToSetMenuItem.Visible = releasesMenuItem.Visible = sets != null;
|
||||
showAllCheckBox.Enabled = session.IsFilteredToNest;
|
||||
UpdateFilterLabel();
|
||||
pager.UpdateNavigation();
|
||||
@@ -354,7 +366,11 @@ namespace OpenNest.Forms
|
||||
|
||||
private async void DrawingGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex >= 0)
|
||||
if (e.RowIndex < 0)
|
||||
return;
|
||||
if (nest == null)
|
||||
await EditSelectedAsync();
|
||||
else
|
||||
await AddSelectedAsync();
|
||||
}
|
||||
|
||||
@@ -392,6 +408,7 @@ namespace OpenNest.Forms
|
||||
private void DrawingMenu_Opening(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
var record = SelectedRecord();
|
||||
editMenuItem.Enabled = record is { Archived: false } && !adding;
|
||||
usedInSetsMenuItem.Enabled = record != null && !adding;
|
||||
addToSetMenuItem.Enabled = record != null && !adding;
|
||||
releaseMenuItem.Enabled = record is { Archived: false } && !adding && ShowRelease != null;
|
||||
@@ -565,8 +582,8 @@ namespace OpenNest.Forms
|
||||
private async void EditButton_Click(object sender, EventArgs e) => await EditSelectedAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Edits the selected drawing's details, notes and nesting settings as a new library revision,
|
||||
/// then reloads the page so the list shows the saved revision.
|
||||
/// Edits the selected drawing's details, notes and nesting settings as a new library
|
||||
/// revision, then reloads the page so the list shows the saved revision.
|
||||
/// </summary>
|
||||
internal async Task EditSelectedAsync()
|
||||
{
|
||||
@@ -586,7 +603,7 @@ namespace OpenNest.Forms
|
||||
{
|
||||
if (keyData == Keys.Enter && drawingGrid.ContainsFocus && SelectedRecord() != null)
|
||||
{
|
||||
_ = AddSelectedAsync();
|
||||
_ = nest == null ? EditSelectedAsync() : AddSelectedAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -601,7 +618,7 @@ namespace OpenNest.Forms
|
||||
internal async Task AddSelectedAsync()
|
||||
{
|
||||
var record = SelectedRecord();
|
||||
if (record == null || adding)
|
||||
if (record == null || adding || nest == null)
|
||||
return;
|
||||
|
||||
adding = true;
|
||||
@@ -706,12 +723,18 @@ namespace OpenNest.Forms
|
||||
|
||||
private void UpdateCommands()
|
||||
{
|
||||
addButton.Enabled = !adding && SelectedRecord() != null;
|
||||
addButton.Enabled = !adding && nest != null && SelectedRecord() != null;
|
||||
editButton.Enabled = !adding && SelectedRecord() is { Archived: false };
|
||||
}
|
||||
|
||||
private void UpdateFilterLabel(bool showAll = false)
|
||||
{
|
||||
if (nest == null)
|
||||
{
|
||||
filterLabel.Text = "Showing every active drawing. Double-click one to edit it.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.IsFilteredToNest && !showAll)
|
||||
{
|
||||
filterLabel.Text = "This nest has no material or thickness; showing every drawing.";
|
||||
@@ -738,6 +761,8 @@ namespace OpenNest.Forms
|
||||
/// </summary>
|
||||
private void RefreshNestTab()
|
||||
{
|
||||
if (nest == null)
|
||||
return;
|
||||
if (nestGrid.IsCurrentCellInEditMode)
|
||||
nestGrid.CancelEdit();
|
||||
|
||||
|
||||
+44
-3
@@ -59,6 +59,7 @@ namespace OpenNest.Forms
|
||||
quantityColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
warningColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
memberButtons = new System.Windows.Forms.FlowLayoutPanel();
|
||||
editDrawingButton = new System.Windows.Forms.Button();
|
||||
removeButton = new System.Windows.Forms.Button();
|
||||
upButton = new System.Windows.Forms.Button();
|
||||
downButton = new System.Windows.Forms.Button();
|
||||
@@ -68,6 +69,9 @@ namespace OpenNest.Forms
|
||||
cancelButton = new System.Windows.Forms.Button();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
searchTimer = new System.Windows.Forms.Timer(components);
|
||||
memberMenu = new System.Windows.Forms.ContextMenuStrip(components);
|
||||
editDrawingMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
removeMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
layoutTable.SuspendLayout();
|
||||
detailsTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
|
||||
@@ -418,6 +422,7 @@ namespace OpenNest.Forms
|
||||
memberGrid.AllowUserToAddRows = false;
|
||||
memberGrid.AllowUserToDeleteRows = false;
|
||||
memberGrid.AllowUserToOrderColumns = false;
|
||||
memberGrid.ContextMenuStrip = memberMenu;
|
||||
memberGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { drawingColumn, materialColumn, gradeColumn, thicknessColumn, unitsColumn, cutTypeColumn, quantityColumn, warningColumn });
|
||||
memberGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
memberGrid.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnKeystrokeOrF2;
|
||||
@@ -432,6 +437,8 @@ namespace OpenNest.Forms
|
||||
memberGrid.DataError += MemberGrid_DataError;
|
||||
memberGrid.CellEndEdit += MemberGrid_CellEndEdit;
|
||||
memberGrid.CurrentCellChanged += MemberGrid_CurrentCellChanged;
|
||||
memberGrid.CellDoubleClick += MemberGrid_CellDoubleClick;
|
||||
memberGrid.CellMouseDown += MemberGrid_CellMouseDown;
|
||||
//
|
||||
// drawingColumn
|
||||
//
|
||||
@@ -507,6 +514,7 @@ namespace OpenNest.Forms
|
||||
// memberButtons
|
||||
//
|
||||
memberButtons.AutoSize = true;
|
||||
memberButtons.Controls.Add(editDrawingButton);
|
||||
memberButtons.Controls.Add(removeButton);
|
||||
memberButtons.Controls.Add(upButton);
|
||||
memberButtons.Controls.Add(downButton);
|
||||
@@ -517,13 +525,24 @@ namespace OpenNest.Forms
|
||||
memberButtons.TabIndex = 1;
|
||||
memberButtons.WrapContents = false;
|
||||
//
|
||||
// editDrawingButton
|
||||
//
|
||||
editDrawingButton.Enabled = false;
|
||||
editDrawingButton.Margin = new System.Windows.Forms.Padding(0, 0, 0, 12);
|
||||
editDrawingButton.Name = "editDrawingButton";
|
||||
editDrawingButton.Size = new System.Drawing.Size(100, 28);
|
||||
editDrawingButton.TabIndex = 0;
|
||||
editDrawingButton.Text = "Edit Drawing...";
|
||||
editDrawingButton.UseVisualStyleBackColor = true;
|
||||
editDrawingButton.Click += EditDrawingButton_Click;
|
||||
//
|
||||
// removeButton
|
||||
//
|
||||
removeButton.Enabled = false;
|
||||
removeButton.Margin = new System.Windows.Forms.Padding(0, 0, 0, 3);
|
||||
removeButton.Name = "removeButton";
|
||||
removeButton.Size = new System.Drawing.Size(100, 28);
|
||||
removeButton.TabIndex = 0;
|
||||
removeButton.TabIndex = 1;
|
||||
removeButton.Text = "Remove";
|
||||
removeButton.UseVisualStyleBackColor = true;
|
||||
removeButton.Click += RemoveButton_Click;
|
||||
@@ -534,7 +553,7 @@ namespace OpenNest.Forms
|
||||
upButton.Margin = new System.Windows.Forms.Padding(0, 12, 0, 3);
|
||||
upButton.Name = "upButton";
|
||||
upButton.Size = new System.Drawing.Size(100, 28);
|
||||
upButton.TabIndex = 1;
|
||||
upButton.TabIndex = 2;
|
||||
upButton.Text = "Move Up";
|
||||
upButton.UseVisualStyleBackColor = true;
|
||||
upButton.Click += UpButton_Click;
|
||||
@@ -545,7 +564,7 @@ namespace OpenNest.Forms
|
||||
downButton.Margin = new System.Windows.Forms.Padding(0, 0, 0, 3);
|
||||
downButton.Name = "downButton";
|
||||
downButton.Size = new System.Drawing.Size(100, 28);
|
||||
downButton.TabIndex = 2;
|
||||
downButton.TabIndex = 3;
|
||||
downButton.Text = "Move Down";
|
||||
downButton.UseVisualStyleBackColor = true;
|
||||
downButton.Click += DownButton_Click;
|
||||
@@ -607,6 +626,24 @@ namespace OpenNest.Forms
|
||||
searchTimer.Interval = 300;
|
||||
searchTimer.Tick += SearchTimer_Tick;
|
||||
//
|
||||
// memberMenu
|
||||
//
|
||||
memberMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { editDrawingMenuItem, removeMenuItem });
|
||||
memberMenu.Name = "memberMenu";
|
||||
memberMenu.Opening += MemberMenu_Opening;
|
||||
//
|
||||
// editDrawingMenuItem
|
||||
//
|
||||
editDrawingMenuItem.Name = "editDrawingMenuItem";
|
||||
editDrawingMenuItem.Text = "Edit Drawing...";
|
||||
editDrawingMenuItem.Click += EditDrawingButton_Click;
|
||||
//
|
||||
// removeMenuItem
|
||||
//
|
||||
removeMenuItem.Name = "removeMenuItem";
|
||||
removeMenuItem.Text = "Remove";
|
||||
removeMenuItem.Click += RemoveButton_Click;
|
||||
//
|
||||
// EditDrawingSetForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
@@ -698,5 +735,9 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.Button cancelButton;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
private System.Windows.Forms.Timer searchTimer;
|
||||
private System.Windows.Forms.Button editDrawingButton;
|
||||
private System.Windows.Forms.ContextMenuStrip memberMenu;
|
||||
private System.Windows.Forms.ToolStripMenuItem editDrawingMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem removeMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -18,20 +18,27 @@ namespace OpenNest.Forms
|
||||
/// browsed in the left pane (every active drawing, a page at a time through
|
||||
/// <see cref="DrawingLibraryBrowseSession"/>); Add to Set, a double-click or Enter adds the
|
||||
/// selected drawings at the entered quantity, and a drawing already in the set is greyed
|
||||
/// and not added twice. Saving adds a revision on the server, guarded by the revision the
|
||||
/// set was read at.
|
||||
/// and not added twice. Edit Drawing (a double-click on a member, its right-click menu or
|
||||
/// the button) reads the member's current library record and edits its details there, so
|
||||
/// a drawing without a material or thickness can be fixed in place. Saving adds a revision
|
||||
/// on the server, guarded by the revision the set was read at.
|
||||
/// </summary>
|
||||
public partial class EditDrawingSetForm : Form
|
||||
{
|
||||
private readonly DrawingSetEditSession session;
|
||||
private readonly IDrawingLibraryStore library;
|
||||
private readonly DrawingLibraryBrowseSession librarySession;
|
||||
private readonly BrowsePager<DrawingLibraryPage, DrawingSortField> libraryPager;
|
||||
private bool loading;
|
||||
private bool saving;
|
||||
private bool editing;
|
||||
|
||||
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
|
||||
/// <summary>Shows Edit Drawing and returns the saved record, or null; replaced by tests.</summary>
|
||||
internal Func<DrawingLibraryEditSession, DrawingLibraryRecord> EditDrawing { get; set; }
|
||||
|
||||
/// <summary>The saved revision once the dialog closes with OK; otherwise null.</summary>
|
||||
public DrawingSetRecord Saved { get; private set; }
|
||||
|
||||
@@ -39,9 +46,9 @@ namespace OpenNest.Forms
|
||||
{
|
||||
InitializeComponent();
|
||||
this.session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
ArgumentNullException.ThrowIfNull(library);
|
||||
this.library = library ?? throw new ArgumentNullException(nameof(library));
|
||||
// A set mixes materials and thicknesses, so the library is never filtered to a nest.
|
||||
librarySession = new DrawingLibraryBrowseSession(library, new Nest());
|
||||
librarySession = new DrawingLibraryBrowseSession(library, null);
|
||||
Disposed += (_, _) => librarySession.Dispose();
|
||||
libraryPager = new BrowsePager<DrawingLibraryPage, DrawingSortField>(
|
||||
librarySession, libraryGrid, librarySummaryLabel, previousButton, nextButton, "the drawing library")
|
||||
@@ -50,6 +57,11 @@ namespace OpenNest.Forms
|
||||
Settled = UpdateAddCommand,
|
||||
};
|
||||
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
|
||||
EditDrawing = edit =>
|
||||
{
|
||||
using var form = new EditLibraryDrawingForm(edit);
|
||||
return form.ShowDialog(this) == DialogResult.OK ? form.Saved : null;
|
||||
};
|
||||
|
||||
GridStyle.Apply(memberGrid);
|
||||
GridStyle.Apply(libraryGrid);
|
||||
@@ -185,6 +197,101 @@ namespace OpenNest.Forms
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
// The quantity cell edits in place; a double-click anywhere else edits the drawing.
|
||||
private async void MemberGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex >= 0 && e.ColumnIndex != quantityColumn.Index)
|
||||
await EditSelectedDrawingAsync();
|
||||
}
|
||||
|
||||
// A right-click selects the row under the pointer, so the menu acts on what was clicked.
|
||||
private void MemberGrid_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Right && e.RowIndex >= 0 && SelectedIndex() != e.RowIndex && memberGrid.EndEdit())
|
||||
memberGrid.CurrentCell = memberGrid.Rows[e.RowIndex].Cells[drawingColumn.Index];
|
||||
}
|
||||
|
||||
private void MemberMenu_Opening(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
var index = SelectedIndex();
|
||||
editDrawingMenuItem.Enabled = !saving && !editing && index >= 0;
|
||||
removeMenuItem.Enabled = !saving && !editing && index >= 0;
|
||||
e.Cancel = index < 0;
|
||||
}
|
||||
|
||||
private async void EditDrawingButton_Click(object sender, EventArgs e) => await EditSelectedDrawingAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Reads the selected member's current library record and opens Edit Drawing on it. The
|
||||
/// member then shows what was read or saved; the set itself lists drawings by id, so it
|
||||
/// is not changed and the next release takes the new revision.
|
||||
/// </summary>
|
||||
internal async Task EditSelectedDrawingAsync()
|
||||
{
|
||||
var index = SelectedIndex();
|
||||
if (index < 0 || saving || editing || !memberGrid.EndEdit())
|
||||
return;
|
||||
|
||||
var member = session.Members[index];
|
||||
var name = member.Drawing?.Name ?? member.DrawingId.ToString();
|
||||
DrawingLibraryRecord current;
|
||||
editing = true;
|
||||
UpdateCommands();
|
||||
statusLabel.Text = $"Reading '{name}'...";
|
||||
try
|
||||
{
|
||||
current = await library.GetAsync(member.DrawingId);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
statusLabel.Text = "";
|
||||
ShowMessage($"Could not read '{name}': {ex.Message}", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
editing = false;
|
||||
if (!IsDisposed)
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
if (IsDisposed)
|
||||
return;
|
||||
statusLabel.Text = "";
|
||||
if (current == null)
|
||||
{
|
||||
ShowMessage($"'{name}' is no longer in the library.", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
ShowMemberDrawing(current);
|
||||
if (current.Archived)
|
||||
{
|
||||
ShowMessage($"'{current.Name}' is archived in the library. Restore it there before editing it.",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var saved = EditDrawing(new DrawingLibraryEditSession(library, current, Environment.UserName));
|
||||
if (saved == null || IsDisposed)
|
||||
return;
|
||||
ShowMemberDrawing(saved);
|
||||
// The library pane may list the old details.
|
||||
await RunLibraryAsync(librarySession.RefreshAsync);
|
||||
if (!IsDisposed)
|
||||
statusLabel.Text = $"Saved '{saved.Name}' revision {saved.Revision} to the library.";
|
||||
}
|
||||
|
||||
private void ShowMemberDrawing(DrawingLibraryRecord drawing)
|
||||
{
|
||||
var index = SelectedIndex();
|
||||
if (session.UpdateDrawing(drawing))
|
||||
Populate(index);
|
||||
}
|
||||
|
||||
private void UpButton_Click(object sender, EventArgs e) => MoveSelected(-1);
|
||||
|
||||
private void DownButton_Click(object sender, EventArgs e) => MoveSelected(1);
|
||||
@@ -356,7 +463,7 @@ namespace OpenNest.Forms
|
||||
if (drawing.Archived)
|
||||
return "Archived in the library: skipped on release unless included";
|
||||
if (string.IsNullOrWhiteSpace(drawing.MaterialName) || !(drawing.Thickness > 0))
|
||||
return "No material or thickness: cannot be released until set in the library";
|
||||
return "No material or thickness: double-click to set them before releasing";
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -367,6 +474,7 @@ namespace OpenNest.Forms
|
||||
var index = SelectedIndex();
|
||||
var error = session.GetError();
|
||||
UpdateAddCommand();
|
||||
editDrawingButton.Enabled = !saving && !editing && index >= 0;
|
||||
removeButton.Enabled = !saving && index >= 0;
|
||||
upButton.Enabled = !saving && index > 0;
|
||||
downButton.Enabled = !saving && index >= 0 && index < session.Members.Count - 1;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
@@ -15,10 +17,14 @@ namespace OpenNest.Forms
|
||||
/// material, grade, thickness, cut type; units are shown, not edited), notes and nesting
|
||||
/// settings (Cut first, priority, rotation constraints, color) through
|
||||
/// <see cref="DrawingLibraryEditSession"/>. Saving adds a revision on the server that
|
||||
/// keeps the same package; nests that already hold the drawing keep their revision.
|
||||
/// keeps the same package; nests that already hold the drawing keep their revision. When
|
||||
/// the new details would duplicate another active library drawing (D4), the operator is
|
||||
/// warned before saving.
|
||||
/// </summary>
|
||||
public partial class EditLibraryDrawingForm : Form
|
||||
{
|
||||
private const int CollisionNames = 5;
|
||||
|
||||
private readonly DrawingLibraryEditSession session;
|
||||
private readonly decimal initialStep;
|
||||
private readonly decimal initialStart;
|
||||
@@ -111,10 +117,20 @@ namespace OpenNest.Forms
|
||||
|
||||
saving = true;
|
||||
UpdateCommands();
|
||||
statusLabel.Text = "Saving...";
|
||||
var details = ReadDetails();
|
||||
try
|
||||
{
|
||||
Saved = await session.SaveAsync(ReadDetails(), notesBox.Text, ReadSettings());
|
||||
if (session.ChangesIdentity(details) && !await ConfirmIdentityAsync(details))
|
||||
{
|
||||
if (!IsDisposed)
|
||||
statusLabel.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDisposed)
|
||||
return;
|
||||
statusLabel.Text = "Saving...";
|
||||
Saved = await session.SaveAsync(details, notesBox.Text, ReadSettings());
|
||||
if (IsDisposed)
|
||||
return;
|
||||
DialogResult = DialogResult.OK;
|
||||
@@ -139,6 +155,50 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the active library drawings the new details would duplicate (D4) and, when
|
||||
/// there are any or the lookup fails, asks whether to save anyway.
|
||||
/// </summary>
|
||||
private async Task<bool> ConfirmIdentityAsync(DrawingLibraryDetails details)
|
||||
{
|
||||
statusLabel.Text = "Checking the library for drawings with these details...";
|
||||
IReadOnlyList<DrawingLibraryRecord> collisions;
|
||||
try
|
||||
{
|
||||
collisions = await session.FindCollisionsAsync(details);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return false;
|
||||
return ShowMessage($"Could not check the library for drawings with these details: {ex.Message}"
|
||||
+ Environment.NewLine + Environment.NewLine + "Save anyway?",
|
||||
MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) == DialogResult.OK;
|
||||
}
|
||||
|
||||
if (IsDisposed)
|
||||
return false;
|
||||
if (collisions.Count == 0)
|
||||
return true;
|
||||
return ShowMessage(DescribeCollisions(collisions), MessageBoxButtons.OKCancel, MessageBoxIcon.Warning)
|
||||
== DialogResult.OK;
|
||||
}
|
||||
|
||||
internal static string DescribeCollisions(IReadOnlyList<DrawingLibraryRecord> collisions)
|
||||
{
|
||||
var lines = collisions.Take(CollisionNames).Select(r =>
|
||||
$"- '{r.Name}'" + (string.IsNullOrWhiteSpace(r.Customer) ? "" : $" ({r.Customer})")
|
||||
+ $", {$"{r.MaterialName} {r.MaterialGrade}".Trim()} "
|
||||
+ $"{r.Thickness.ToString("0.####", CultureInfo.CurrentCulture)} {UnitsHelper.GetShortString(r.Units)} {r.CutType}"
|
||||
+ $", revision {r.Revision.ToString(CultureInfo.CurrentCulture)}");
|
||||
var more = collisions.Count > CollisionNames ? $"{Environment.NewLine}...and {collisions.Count - CollisionNames} more." : "";
|
||||
var count = collisions.Count == 1 ? "Another library drawing already has" : $"{collisions.Count} other library drawings already have";
|
||||
return $"{count} this name, customer, material, grade, thickness and cut type:"
|
||||
+ Environment.NewLine + string.Join(Environment.NewLine, lines) + more
|
||||
+ Environment.NewLine + Environment.NewLine
|
||||
+ "Saving makes them hard to tell apart, and Save to Drawing Library will offer to revise either. Save anyway?";
|
||||
}
|
||||
|
||||
private bool CanSave()
|
||||
{
|
||||
var details = ReadDetails();
|
||||
|
||||
Generated
+10
-1
@@ -38,6 +38,7 @@ namespace OpenNest.Forms
|
||||
mnuFileNew = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileOpen = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileImportBom = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileDrawingLibrary = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileDrawingSets = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileReleases = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
@@ -207,7 +208,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// mnuFile
|
||||
//
|
||||
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileExportNestReport, toolStripMenuItem3, mnuFileExit });
|
||||
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingLibrary, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileExportNestReport, toolStripMenuItem3, mnuFileExit });
|
||||
mnuFile.Name = "mnuFile";
|
||||
mnuFile.DropDownOpening += FileMenu_Opening;
|
||||
mnuFile.Size = new System.Drawing.Size(37, 20);
|
||||
@@ -238,6 +239,13 @@ namespace OpenNest.Forms
|
||||
mnuFileImportBom.Text = "Import BOM...";
|
||||
mnuFileImportBom.Click += ImportBom_Click;
|
||||
//
|
||||
// mnuFileDrawingLibrary
|
||||
//
|
||||
mnuFileDrawingLibrary.Name = "mnuFileDrawingLibrary";
|
||||
mnuFileDrawingLibrary.Size = new System.Drawing.Size(180, 22);
|
||||
mnuFileDrawingLibrary.Text = "Drawing Library...";
|
||||
mnuFileDrawingLibrary.Click += FileDrawingLibrary_Click;
|
||||
//
|
||||
// mnuFileDrawingSets
|
||||
//
|
||||
mnuFileDrawingSets.Name = "mnuFileDrawingSets";
|
||||
@@ -1356,6 +1364,7 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileNew;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileOpen;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileImportBom;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileDrawingLibrary;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileDrawingSets;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileReleases;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1;
|
||||
|
||||
@@ -98,15 +98,46 @@ namespace OpenNest.Forms
|
||||
|
||||
private void FileMenu_Opening(object sender, EventArgs e)
|
||||
{
|
||||
// Drawing sets live on the nest server; File mode has nothing to browse.
|
||||
// The drawing library and sets live on the nest server; File mode has nothing to browse.
|
||||
var databaseMode = NestStorage.Settings.IsDatabaseMode;
|
||||
mnuFile.DropDown.ShowItemToolTips = true;
|
||||
mnuFileDrawingLibrary.Enabled = databaseMode;
|
||||
mnuFileDrawingLibrary.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly;
|
||||
mnuFileDrawingSets.Enabled = databaseMode;
|
||||
mnuFileDrawingSets.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly;
|
||||
mnuFileReleases.Enabled = databaseMode;
|
||||
mnuFileReleases.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Browses the drawing library and edits its drawings' details and settings; needs no
|
||||
/// open nest. Drawings are added to sets or released from it, never to a nest.
|
||||
/// </summary>
|
||||
private void FileDrawingLibrary_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!NestStorage.Settings.IsDatabaseMode)
|
||||
return;
|
||||
|
||||
IDrawingLibraryStore library;
|
||||
IDrawingSetStore sets;
|
||||
INestRepository nests;
|
||||
try
|
||||
{
|
||||
library = NestStorage.CreateDrawingLibrary();
|
||||
sets = NestStorage.CreateDrawingSetStore();
|
||||
nests = NestStorage.CreateRepository();
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
MessageBox.Show(this, ex.Message, "Drawing Library", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
using var form = new DrawingLibraryForm(library, NestStorage.Settings.ServerUrl, null, sets);
|
||||
form.EnableRelease(nests, OpenServerNest, IsServerNestOpen);
|
||||
form.ShowDialog(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Browses, edits and releases drawing sets; needs no open nest, since a release
|
||||
/// creates or merges into nests and opens each in its own tab.
|
||||
|
||||
+31
-5
@@ -59,6 +59,8 @@ namespace OpenNest.Forms
|
||||
closeButton = new System.Windows.Forms.Button();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
searchTimer = new System.Windows.Forms.Timer(components);
|
||||
fixDrawingsButton = new System.Windows.Forms.Button();
|
||||
groupMenu = new System.Windows.Forms.ContextMenuStrip(components);
|
||||
layoutTable.SuspendLayout();
|
||||
addSetTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)setGrid).BeginInit();
|
||||
@@ -295,6 +297,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
groupGrid.AllowUserToAddRows = false;
|
||||
groupGrid.AllowUserToDeleteRows = false;
|
||||
groupGrid.ContextMenuStrip = groupMenu;
|
||||
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;
|
||||
@@ -307,6 +310,9 @@ namespace OpenNest.Forms
|
||||
groupGrid.CellValueChanged += GroupGrid_CellValueChanged;
|
||||
groupGrid.CurrentCellDirtyStateChanged += GroupGrid_CurrentCellDirtyStateChanged;
|
||||
groupGrid.DataError += GroupGrid_DataError;
|
||||
groupGrid.CellDoubleClick += GroupGrid_CellDoubleClick;
|
||||
groupGrid.CellMouseDown += GroupGrid_CellMouseDown;
|
||||
groupGrid.CurrentCellChanged += GroupGrid_CurrentCellChanged;
|
||||
//
|
||||
// includeColumn
|
||||
//
|
||||
@@ -405,13 +411,15 @@ namespace OpenNest.Forms
|
||||
// bottomTable
|
||||
//
|
||||
bottomTable.AutoSize = true;
|
||||
bottomTable.ColumnCount = 3;
|
||||
bottomTable.ColumnCount = 4;
|
||||
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.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.Controls.Add(fixDrawingsButton, 1, 0);
|
||||
bottomTable.Controls.Add(releaseButton, 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";
|
||||
@@ -426,12 +434,23 @@ namespace OpenNest.Forms
|
||||
summaryLabel.Name = "summaryLabel";
|
||||
summaryLabel.TabIndex = 0;
|
||||
//
|
||||
// fixDrawingsButton
|
||||
//
|
||||
fixDrawingsButton.AutoSize = true;
|
||||
fixDrawingsButton.Enabled = false;
|
||||
fixDrawingsButton.Margin = new System.Windows.Forms.Padding(3, 3, 12, 3);
|
||||
fixDrawingsButton.Name = "fixDrawingsButton";
|
||||
fixDrawingsButton.TabIndex = 1;
|
||||
fixDrawingsButton.Text = "Fix Drawings...";
|
||||
fixDrawingsButton.UseVisualStyleBackColor = true;
|
||||
fixDrawingsButton.Click += FixDrawingsButton_Click;
|
||||
//
|
||||
// releaseButton
|
||||
//
|
||||
releaseButton.AutoSize = true;
|
||||
releaseButton.Enabled = false;
|
||||
releaseButton.Name = "releaseButton";
|
||||
releaseButton.TabIndex = 1;
|
||||
releaseButton.TabIndex = 2;
|
||||
releaseButton.Text = "Release";
|
||||
releaseButton.UseVisualStyleBackColor = true;
|
||||
releaseButton.Click += ReleaseButton_Click;
|
||||
@@ -440,7 +459,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
closeButton.AutoSize = true;
|
||||
closeButton.Name = "closeButton";
|
||||
closeButton.TabIndex = 2;
|
||||
closeButton.TabIndex = 3;
|
||||
closeButton.Text = "Close";
|
||||
closeButton.UseVisualStyleBackColor = true;
|
||||
closeButton.Click += CloseButton_Click;
|
||||
@@ -459,6 +478,11 @@ namespace OpenNest.Forms
|
||||
searchTimer.Interval = 250;
|
||||
searchTimer.Tick += SearchTimer_Tick;
|
||||
//
|
||||
// groupMenu
|
||||
//
|
||||
groupMenu.Name = "groupMenu";
|
||||
groupMenu.Opening += GroupMenu_Opening;
|
||||
//
|
||||
// ReleaseSetForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
@@ -519,6 +543,8 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.DataGridViewComboBoxColumn targetColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn statusColumn;
|
||||
private System.Windows.Forms.Label skippedLabel;
|
||||
private System.Windows.Forms.Button fixDrawingsButton;
|
||||
private System.Windows.Forms.ContextMenuStrip groupMenu;
|
||||
private System.Windows.Forms.TableLayoutPanel bottomTable;
|
||||
private System.Windows.Forms.Label summaryLabel;
|
||||
private System.Windows.Forms.Button releaseButton;
|
||||
|
||||
@@ -24,7 +24,10 @@ namespace OpenNest.Forms
|
||||
/// the same material, whose layout is discarded. <see cref="SetReleaseSession"/> then builds
|
||||
/// those nests, nests each one with Auto Nest when Nest after release is ticked, saves them,
|
||||
/// 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.
|
||||
/// dialog shows every group's outcome; a release runs once per dialog. A group refused for
|
||||
/// its drawings (no material or thickness, or two with one name) is not a dead end: Fix
|
||||
/// Drawings, a double-click or the group's right-click menu opens Edit Drawing on the
|
||||
/// current library record, and the plan is rebuilt with what was saved.
|
||||
/// </summary>
|
||||
public partial class ReleaseSetForm : Form
|
||||
{
|
||||
@@ -32,6 +35,8 @@ namespace OpenNest.Forms
|
||||
|
||||
private readonly IDrawingSetStore sets;
|
||||
private readonly IDrawingLibrary library;
|
||||
// Edit Drawing needs a writable library; null hides it.
|
||||
private readonly IDrawingLibraryStore libraryStore;
|
||||
private readonly INestRepository nests;
|
||||
private readonly string serverUrl;
|
||||
private readonly Action<Nest, NestRecord> openNest;
|
||||
@@ -47,6 +52,7 @@ namespace OpenNest.Forms
|
||||
private bool populating;
|
||||
private bool running;
|
||||
private bool loadingDrafts;
|
||||
private bool editing;
|
||||
|
||||
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
@@ -72,6 +78,9 @@ namespace OpenNest.Forms
|
||||
/// </summary>
|
||||
internal Action ShowAddDrawings { get; set; }
|
||||
|
||||
/// <summary>Shows Edit Drawing and returns the saved record, or null; replaced by tests.</summary>
|
||||
internal Func<DrawingLibraryEditSession, DrawingLibraryRecord> EditDrawing { get; set; }
|
||||
|
||||
/// <summary>The finished release, once one has run; otherwise null.</summary>
|
||||
public SetReleaseResult Result { get; private set; }
|
||||
|
||||
@@ -98,6 +107,7 @@ namespace OpenNest.Forms
|
||||
InitializeComponent();
|
||||
this.sets = sets ?? throw new ArgumentNullException(nameof(sets));
|
||||
this.library = library ?? throw new ArgumentNullException(nameof(library));
|
||||
libraryStore = library as IDrawingLibraryStore;
|
||||
this.nests = nests ?? throw new ArgumentNullException(nameof(nests));
|
||||
this.serverUrl = serverUrl ?? "";
|
||||
this.openNest = openNest ?? throw new ArgumentNullException(nameof(openNest));
|
||||
@@ -111,6 +121,12 @@ namespace OpenNest.Forms
|
||||
id => DrawingLines().Any(l => l.Drawing.Id == id));
|
||||
form.ShowDialog(this);
|
||||
};
|
||||
EditDrawing = edit =>
|
||||
{
|
||||
using var form = new EditLibraryDrawingForm(edit);
|
||||
return form.ShowDialog(this) == DialogResult.OK ? form.Saved : null;
|
||||
};
|
||||
fixDrawingsButton.Visible = libraryStore != null;
|
||||
|
||||
GridStyle.Apply(setGrid);
|
||||
GridStyle.Apply(groupGrid);
|
||||
@@ -462,6 +478,163 @@ namespace OpenNest.Forms
|
||||
populating = false;
|
||||
}
|
||||
|
||||
private void GroupGrid_CurrentCellChanged(object sender, EventArgs e) => UpdateCommands();
|
||||
|
||||
// A right-click selects the row under the pointer, so the menu acts on what was clicked.
|
||||
private void GroupGrid_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Right && e.RowIndex >= 0 && groupGrid.CurrentRow?.Index != e.RowIndex)
|
||||
groupGrid.CurrentCell = groupGrid.Rows[e.RowIndex].Cells[materialColumn.Index];
|
||||
}
|
||||
|
||||
private async void GroupGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex >= 0 && groupGrid.Rows[e.RowIndex].Tag is SetReleaseGroup { CanRelease: false })
|
||||
await FixSelectedGroupAsync();
|
||||
}
|
||||
|
||||
private async void FixDrawingsButton_Click(object sender, EventArgs e) => await FixSelectedGroupAsync();
|
||||
|
||||
// One Edit Drawing item per drawing of the group, the ones it is refused for first.
|
||||
private void GroupMenu_Opening(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
groupMenu.Items.Clear();
|
||||
if (libraryStore == null || groupGrid.CurrentRow?.Tag is not SetReleaseGroup group)
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const int listed = 15;
|
||||
var items = group.RefusedItems.Concat(group.Items.Where(i => !group.RefusedItems.Contains(i))).ToList();
|
||||
var enabled = CanEditDrawings();
|
||||
foreach (var item in items.Take(listed))
|
||||
{
|
||||
var menuItem = new ToolStripMenuItem($"Edit Drawing '{item.Drawing.Name}'...") { Enabled = enabled };
|
||||
menuItem.Click += async (_, _) => await EditItemDrawingAsync(item);
|
||||
groupMenu.Items.Add(menuItem);
|
||||
}
|
||||
if (items.Count > listed)
|
||||
{
|
||||
groupMenu.Items.Add(new ToolStripMenuItem(
|
||||
$"...and {items.Count - listed} more: edit them in the set or the Drawing Library")
|
||||
{ Enabled = false });
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanEditDrawings() => libraryStore != null && !running && !editing && Result == null;
|
||||
|
||||
/// <summary>
|
||||
/// Opens Edit Drawing, one after another, on each drawing the selected group is refused
|
||||
/// for; cancelling one stops. The plan is rebuilt after each save.
|
||||
/// </summary>
|
||||
internal async Task FixSelectedGroupAsync()
|
||||
{
|
||||
if (!CanEditDrawings() || groupGrid.CurrentRow?.Tag is not SetReleaseGroup { CanRelease: false } group)
|
||||
return;
|
||||
foreach (var item in group.RefusedItems.ToList())
|
||||
{
|
||||
if (!await EditItemDrawingAsync(item) || IsDisposed)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the item's current library record and opens Edit Drawing on it; true when it
|
||||
/// was saved. What was read or saved replaces the drawing in the plan.
|
||||
/// </summary>
|
||||
internal async Task<bool> EditItemDrawingAsync(SetReleaseItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
if (!CanEditDrawings())
|
||||
return false;
|
||||
|
||||
var name = item.Drawing.Name;
|
||||
DrawingLibraryRecord current;
|
||||
editing = true;
|
||||
UpdateCommands();
|
||||
statusLabel.Text = $"Reading '{name}'...";
|
||||
try
|
||||
{
|
||||
current = await libraryStore.GetAsync(item.Drawing.Id);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
statusLabel.Text = "";
|
||||
ShowMessage($"Could not read '{name}': {ex.Message}", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
editing = false;
|
||||
if (!IsDisposed)
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
if (IsDisposed)
|
||||
return false;
|
||||
statusLabel.Text = "";
|
||||
if (current == null)
|
||||
{
|
||||
ShowMessage($"'{name}' is no longer in the library.", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return false;
|
||||
}
|
||||
if (current.Archived)
|
||||
{
|
||||
ShowMessage($"'{current.Name}' is archived in the library. Restore it there before editing it.",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return false;
|
||||
}
|
||||
|
||||
var saved = EditDrawing(new DrawingLibraryEditSession(libraryStore, current, Environment.UserName));
|
||||
if (IsDisposed)
|
||||
return false;
|
||||
ApplyDrawing(saved ?? current);
|
||||
if (saved == null)
|
||||
return false;
|
||||
statusLabel.Text = $"Saved '{saved.Name}' revision {saved.Revision} to the library.";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a newer library record of a listed drawing, in the sets that list it (their
|
||||
/// joined members) and on its own line, and rebuilds the plan. Sets list drawings by id,
|
||||
/// so the release takes the drawing as it now is.
|
||||
/// </summary>
|
||||
internal void ApplyDrawing(DrawingLibraryRecord drawing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawing);
|
||||
var changed = false;
|
||||
foreach (DataGridViewRow row in setGrid.Rows)
|
||||
{
|
||||
if (row.Tag is DrawingSetRecord set)
|
||||
{
|
||||
foreach (var member in set.Members ?? new List<DrawingSetMember>())
|
||||
{
|
||||
if (member.DrawingId == drawing.Id && (member.Drawing == null || member.Drawing.Revision < drawing.Revision))
|
||||
{
|
||||
member.Drawing = drawing;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (row.Tag is DrawingLibraryRecord listed && listed.Id == drawing.Id && listed.Revision < drawing.Revision)
|
||||
{
|
||||
row.Tag = drawing;
|
||||
row.Cells[setNameColumn.Index].Value = drawing.Name;
|
||||
row.Cells[setCustomerColumn.Index].Value = drawing.Customer;
|
||||
row.Cells[setRevisionColumn.Index].Value = drawing.Revision;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
BuildPlan();
|
||||
}
|
||||
|
||||
private async void ReleaseButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (Result is { Release: null, ReleaseError.Length: > 0 })
|
||||
@@ -790,6 +963,8 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
fixDrawingsButton.Enabled = CanEditDrawings() && groupGrid.CurrentRow?.Tag is SetReleaseGroup { CanRelease: false };
|
||||
|
||||
var retry = Result is { Release: null, ReleaseError.Length: > 0 };
|
||||
releaseButton.Text = retry ? "Record Release" : "Release";
|
||||
releaseButton.Enabled = !running && (retry || (editable && chosen.Count > 0));
|
||||
|
||||
+49
-13
@@ -321,17 +321,50 @@ is edited in place (0 to 99,999; one click starts editing, a bad number keeps
|
||||
the editor open with an error) and applies to the nest at once, like an add;
|
||||
the main window's drawing list refreshes when the dialog closes.
|
||||
|
||||
The browser's **Cut first** column shows the setting. **Edit Drawing...**
|
||||
(`OpenNest.Data.DrawingLibraryEditSession`) edits the selected active drawing's
|
||||
details (name, customer, material, grade, thickness and cut type; text is
|
||||
trimmed, the name is required, units are shown but not editable since they give
|
||||
the geometry its meaning) and its Cut first flag, priority, rotation
|
||||
step/start/end (degrees), 180° equivalent, color and notes, and saves them
|
||||
through the metadata endpoint guarded by the listed revision: a new revision
|
||||
with the same package. A material or thickness change is an edit of the same
|
||||
drawing, not a new one. A 409 is shown and the operator reloads. Nests that
|
||||
already hold the drawing keep their copy and revision; sets list the drawing,
|
||||
so their next release uses the new details.
|
||||
The browser's **Cut first** column shows the setting.
|
||||
|
||||
**File > Drawing Library...** (Database mode only, like Drawing Sets) opens the
|
||||
same browser without a nest: it lists every active drawing, has no Add to Nest
|
||||
or In This Nest tab, and a double-click or Enter edits the selected drawing. Its
|
||||
right-click menu has Edit Drawing and the set and release items below.
|
||||
|
||||
### Edit Drawing (desktop)
|
||||
|
||||
**Edit Drawing...** (`OpenNest.Data.DrawingLibraryEditSession`) edits one active
|
||||
library drawing. Its **Details** are the name, customer, material, grade,
|
||||
thickness and cut type; **Units** are shown read-only, since changing them would
|
||||
change what the geometry means. Its **Nesting settings** are Cut first, priority,
|
||||
rotation step/start/end (degrees), 180° equivalent and color, then the notes.
|
||||
Text is trimmed and thickness rounded as the server stores it; Save stays
|
||||
disabled until something changed and every value passes the library rules.
|
||||
|
||||
When the new details would give the drawing another identity (D4: normalized
|
||||
customer, name, material and grade, exact thickness, units and cut type), Save
|
||||
first looks up active library drawings that already have them and, if any do,
|
||||
names them and asks before saving; a failed lookup also asks. Editing only notes
|
||||
or settings never asks.
|
||||
|
||||
Save goes through `PUT /api/drawings/{id}/metadata`, guarded by the revision
|
||||
the dialog opened with: a new revision with the same package. A material or
|
||||
thickness change is an edit of the same drawing, not a new one. A 409 (someone
|
||||
else saved first) is shown, nothing is saved and the dialog stays open; close it
|
||||
and edit again. Nests that already hold the drawing keep their copy and
|
||||
revision. Sets list drawings by id, so they release the new revision next time.
|
||||
|
||||
Edit Drawing opens from:
|
||||
|
||||
- the Drawing Library dialog (the **Edit Drawing...** button or the drawing's
|
||||
right-click menu), which reloads the page after a save;
|
||||
- the set editor's member grid: double-click a member (outside its quantity),
|
||||
right-click it, or **Edit Drawing...** beside the grid. It first reads the
|
||||
member's current record, so a set read earlier does not cause a conflict; the
|
||||
row then shows what was saved. The set itself is unchanged and needs no save;
|
||||
- the release dialog: a group refused for its drawings (no material or
|
||||
thickness, or two drawings with one name) has **Fix Drawings...**, which opens
|
||||
Edit Drawing on each drawing it is refused for in turn (Cancel stops), as does
|
||||
a double-click on the group. Its right-click menu lists Edit Drawing for each of
|
||||
the group's drawings. Each edit reads the current record first, and the plan
|
||||
is rebuilt with what was saved, so a fixed drawing joins its material group.
|
||||
|
||||
### Save to Drawing Library (desktop)
|
||||
|
||||
@@ -387,7 +420,8 @@ a set shows its notes.
|
||||
in the grid (1 to 100,000); **Remove**, **Move Up** and **Move Down** change
|
||||
the list. The note column flags drawings archived in the library (skipped on
|
||||
release unless included) and drawings with no material or thickness (cannot be
|
||||
released until set in the library).
|
||||
released until set; double-click to open
|
||||
[Edit Drawing](#edit-drawing-desktop)).
|
||||
- **Save** adds a revision guarded by the revision the set was read at; a 409 is
|
||||
shown and the operator cancels and reloads. A new set's id is allocated
|
||||
before its first save, and a save that fails without a definite answer stays
|
||||
@@ -441,7 +475,9 @@ drawings, parts, **Nest** and status. **Include archived drawings** puts archive
|
||||
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.
|
||||
survives changing the sets, kits or the archived toggle. **Fix Drawings...**
|
||||
opens [Edit Drawing](#edit-drawing-desktop) on a refused group's drawings and
|
||||
rebuilds the plan with what was saved.
|
||||
|
||||
**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
|
||||
|
||||
Reference in new issue
Block a user