mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 00:21:06 -04:00
feat(desktop): Save to Drawing Library dialog
Operators can now save a nest drawing to the shared drawing library. Right- click a drawing in the drawing list > Save to Drawing Library... opens a dialog with name, customer, material (post materials suggested), grade, thickness, cut type and notes, prefilled by the save model; units follow the nest. Whenever the details change, the dialog looks up library drawings with the same details and offers Save as: the next revision of the match (the default for one match or the linked drawing), a choice among several, or a separate drawing. Save stays disabled until the lookup for the current details has finished, so a choice can never apply to details it was not made for. A 409 is shown and the choices are refreshed; a save that could not be confirmed tells the operator that saving again checks it and never stores it twice. On success only the drawing's library link changes, and the chosen cut type is remembered per PC. There is no nest dirty flag to set, so the operator saves the nest to keep the new link. The item is Database mode only and is shown only where the drawing list's host handles it (not the best-fit viewer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f93b0d6273
commit
2aecafb356
9 files changed
+1001
-3
No files matched your search
@@ -0,0 +1,274 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class SaveToDrawingLibraryFormTests
|
||||
{
|
||||
private const string Server = "http://library.test:8090";
|
||||
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
|
||||
|
||||
[Fact]
|
||||
public void NoMatch_PrefillsTheDetails_DefaultsToASeparateDrawing_AndSavesAndLinks()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var store = new FakeStore();
|
||||
var (nest, drawing) = MatchingNest();
|
||||
|
||||
var form = new SaveToDrawingLibraryForm(store, Server, nest, drawing, CutType.Waterjet, "Synthetic operator");
|
||||
form.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException("Unexpected message: " + text);
|
||||
form.Show();
|
||||
var saveAs = Field<ComboBox>(form, "saveAsBox");
|
||||
var save = Field<Button>(form, "saveButton");
|
||||
|
||||
Assert.Equal("SYN-101", Field<TextBox>(form, "nameBox").Text);
|
||||
Assert.Equal("Synthetic Steel", Field<ComboBox>(form, "materialBox").Text);
|
||||
Assert.Equal("Grade A", Field<TextBox>(form, "gradeBox").Text);
|
||||
Assert.Equal(0.25m, Field<NumericUpDown>(form, "thicknessBox").Value);
|
||||
Assert.Equal(CutType.Waterjet, Field<ComboBox>(form, "cutTypeBox").SelectedItem);
|
||||
PumpUntil(() => save.Enabled, "the duplicate check");
|
||||
Assert.Equal("Create a separate library drawing", saveAs.Text);
|
||||
|
||||
Run(form.SaveAsync());
|
||||
|
||||
Assert.Equal(DialogResult.OK, form.DialogResult);
|
||||
Assert.Equal(DrawingLibrarySaveOutcome.Created, form.Result.Outcome);
|
||||
var stored = Assert.Single(store.Records);
|
||||
Assert.Equal((CutType.Waterjet, "Synthetic operator"), (stored.CutType, stored.SavedBy));
|
||||
Assert.Equal((Server, stored.Id, 1), (drawing.Library!.ServerKey, drawing.Library.Id, drawing.Library.Revision));
|
||||
form.Dispose();
|
||||
},
|
||||
TestTimeout,
|
||||
"The save to drawing library test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneMatch_DefaultsToItsNextRevision_AndEditedDetailsMustBeCheckedAgain()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var store = new FakeStore();
|
||||
var match = store.Seed("SYN-101", 0.25);
|
||||
var (nest, drawing) = MatchingNest();
|
||||
|
||||
using var form = new SaveToDrawingLibraryForm(store, Server, nest, drawing, CutType.Laser, "");
|
||||
form.Show();
|
||||
var saveAs = Field<ComboBox>(form, "saveAsBox");
|
||||
var save = Field<Button>(form, "saveButton");
|
||||
var thickness = Field<NumericUpDown>(form, "thicknessBox");
|
||||
|
||||
PumpUntil(() => save.Enabled, "the duplicate check");
|
||||
Assert.StartsWith("Revision 2 of 'SYN-101'", saveAs.Text);
|
||||
|
||||
thickness.Value = 0.1875m;
|
||||
Assert.False(save.Enabled);
|
||||
Assert.Equal(-1, saveAs.SelectedIndex);
|
||||
PumpUntil(() => save.Enabled, "the second duplicate check");
|
||||
Assert.Equal("Create a separate library drawing", saveAs.Text);
|
||||
|
||||
thickness.Value = 0.25m;
|
||||
PumpUntil(() => save.Enabled, "the third duplicate check");
|
||||
Run(form.SaveAsync());
|
||||
|
||||
Assert.Equal(DrawingLibrarySaveOutcome.Revised, form.Result.Outcome);
|
||||
Assert.Equal((match.Id, 2), (drawing.Library!.Id, drawing.Library.Revision));
|
||||
},
|
||||
TestTimeout,
|
||||
"The drawing library revision test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeveralMatches_LeaveSaveDisabledUntilOneIsChosen()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var store = new FakeStore();
|
||||
store.Seed("SYN-101", 0.25);
|
||||
store.Seed("SYN-101", 0.25);
|
||||
var (nest, drawing) = MatchingNest();
|
||||
|
||||
using var form = new SaveToDrawingLibraryForm(store, Server, nest, drawing, CutType.Laser, "");
|
||||
form.Show();
|
||||
var saveAs = Field<ComboBox>(form, "saveAsBox");
|
||||
var save = Field<Button>(form, "saveButton");
|
||||
|
||||
PumpUntil(() => saveAs.Items.Count == 3, "the duplicate check");
|
||||
Assert.Equal(-1, saveAs.SelectedIndex);
|
||||
Assert.False(save.Enabled);
|
||||
Assert.Contains("Choose one", Field<Label>(form, "duplicateLabel").Text);
|
||||
|
||||
saveAs.SelectedIndex = 2;
|
||||
|
||||
Assert.True(save.Enabled);
|
||||
},
|
||||
TestTimeout,
|
||||
"The drawing library choice test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conflict_IsShown_TheDialogStaysOpen_AndTheChoicesAreRefreshed()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var store = new FakeStore();
|
||||
var match = store.Seed("SYN-101", 0.25);
|
||||
var (nest, drawing) = MatchingNest();
|
||||
var messages = new List<string>();
|
||||
|
||||
using var form = new SaveToDrawingLibraryForm(store, Server, nest, drawing, CutType.Laser, "");
|
||||
form.ShowMessage = (text, _, _) => { messages.Add(text); return DialogResult.OK; };
|
||||
form.Show();
|
||||
var saveAs = Field<ComboBox>(form, "saveAsBox");
|
||||
var save = Field<Button>(form, "saveButton");
|
||||
PumpUntil(() => save.Enabled, "the duplicate check");
|
||||
store.Revise(match.Id);
|
||||
|
||||
Run(form.SaveAsync());
|
||||
|
||||
Assert.Contains("changed by someone else", Assert.Single(messages));
|
||||
Assert.Null(form.Result);
|
||||
Assert.Null(drawing.Library);
|
||||
Assert.True(form.Visible);
|
||||
PumpUntil(() => save.Enabled, "the refreshed duplicate check");
|
||||
Assert.StartsWith("Revision 3 of 'SYN-101'", saveAs.Text);
|
||||
},
|
||||
TestTimeout,
|
||||
"The drawing library conflict test did not complete.");
|
||||
}
|
||||
|
||||
private static (Nest Nest, Drawing Drawing) MatchingNest()
|
||||
{
|
||||
var nest = new Nest("Synthetic job")
|
||||
{
|
||||
Units = Units.Inches,
|
||||
Thickness = 0.25,
|
||||
Material = new Material("Synthetic Steel", "Grade A"),
|
||||
};
|
||||
var drawing = new RectangleShape { Name = "SYN-101", Length = 4, Width = 2 }.GetDrawing();
|
||||
drawing.Customer = "Synthetic customer";
|
||||
nest.Drawings.Add(drawing);
|
||||
return (nest, drawing);
|
||||
}
|
||||
|
||||
private static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the save to finish");
|
||||
|
||||
private static void PumpUntil(Func<bool> condition, string what)
|
||||
{
|
||||
var until = DateTime.UtcNow.AddSeconds(15);
|
||||
while (!condition() && DateTime.UtcNow < until)
|
||||
{
|
||||
Application.DoEvents();
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
Assert.True(condition(), $"Timed out waiting for {what}.");
|
||||
}
|
||||
|
||||
private static T Field<T>(object owner, string name) =>
|
||||
(T)owner.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
|
||||
|
||||
/// <summary>An in-memory library that keeps every revision and filters like the server.</summary>
|
||||
private sealed class FakeStore : IDrawingLibraryStore
|
||||
{
|
||||
private readonly Dictionary<Guid, List<DrawingLibraryRecord>> _revisions = new();
|
||||
private DateTime _clock = new(2026, 10, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public IEnumerable<DrawingLibraryRecord> Records => _revisions.Values.Select(r => r[^1]);
|
||||
|
||||
public DrawingLibraryRecord Seed(string name, double thickness)
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
_revisions[id] = new List<DrawingLibraryRecord>
|
||||
{
|
||||
Stored(new DrawingLibraryRecord
|
||||
{
|
||||
Name = name,
|
||||
Customer = "Synthetic customer",
|
||||
MaterialName = "Synthetic Steel",
|
||||
MaterialGrade = "Grade A",
|
||||
Thickness = thickness,
|
||||
Units = Units.Inches,
|
||||
CutType = CutType.Laser,
|
||||
}, id, 1, new byte[] { (byte)'P', (byte)'K', 1, 2 }),
|
||||
};
|
||||
return _revisions[id][^1];
|
||||
}
|
||||
|
||||
public void Revise(Guid id)
|
||||
{
|
||||
var current = _revisions[id][^1];
|
||||
_revisions[id].Add(Stored(current, id, current.Revision + 1, new byte[] { (byte)'P', (byte)'K', 9, (byte)current.Revision }));
|
||||
}
|
||||
|
||||
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = Records
|
||||
.Where(r => r.Name.Contains(query.NormalizedSearch, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(r => query.Thickness is not { } t || r.Thickness == DrawingLibraryRules.NormalizeThickness(t))
|
||||
.Where(r => query.CutType is not { } c || r.CutType == c)
|
||||
.OrderByDescending(r => r.SavedAt)
|
||||
.ToList();
|
||||
return Task.FromResult(new DrawingLibraryPage
|
||||
{ Items = items.Skip(query.Offset).Take(query.Limit).ToList(), Total = items.Count, Offset = query.Offset, Limit = query.Limit });
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord?> GetAsync(Guid id, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(_revisions.TryGetValue(id, out var list) ? list[^1] : null);
|
||||
|
||||
public Task<DrawingLibraryPage?> GetRevisionsAsync(Guid id, int offset = 0,
|
||||
int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<DrawingLibraryPage?>(_revisions.TryGetValue(id, out var list)
|
||||
? new DrawingLibraryPage { Items = Enumerable.Reverse(list).Skip(offset).Take(limit).ToList(), Total = list.Count, Offset = offset, Limit = limit }
|
||||
: null);
|
||||
|
||||
public Task<DrawingLibraryRecord> UploadAsync(DrawingLibraryRecord record, byte[] package,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_revisions[record.Id] = new List<DrawingLibraryRecord> { Stored(record, record.Id, 1, package) };
|
||||
return Task.FromResult(_revisions[record.Id][^1]);
|
||||
}
|
||||
|
||||
public Task<DrawingLibraryRecord> UpdatePackageAsync(Guid id, DrawingLibraryRecord record, byte[] package,
|
||||
int expectedRevision, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var current = _revisions[id][^1];
|
||||
if (current.Revision != expectedRevision)
|
||||
throw new DrawingLibraryConflictException(current);
|
||||
_revisions[id].Add(Stored(record, id, current.Revision + 1, package));
|
||||
return Task.FromResult(_revisions[id][^1]);
|
||||
}
|
||||
|
||||
private DrawingLibraryRecord Stored(DrawingLibraryRecord metadata, Guid id, int revision, byte[] package)
|
||||
{
|
||||
_clock = _clock.AddMinutes(1);
|
||||
return new DrawingLibraryRecord
|
||||
{
|
||||
Id = id,
|
||||
Revision = revision,
|
||||
Name = metadata.Name,
|
||||
Customer = metadata.Customer,
|
||||
MaterialName = metadata.MaterialName,
|
||||
MaterialGrade = metadata.MaterialGrade,
|
||||
Thickness = DrawingLibraryRules.NormalizeThickness(metadata.Thickness),
|
||||
Units = metadata.Units,
|
||||
CutType = metadata.CutType,
|
||||
Notes = metadata.Notes,
|
||||
SavedBy = metadata.SavedBy,
|
||||
PackageSha256 = Convert.ToHexString(SHA256.HashData(package)).ToLowerInvariant(),
|
||||
SavedAt = _clock,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,17 +29,41 @@ namespace OpenNest.Controls
|
||||
if (SelectedItem is Drawing drawing)
|
||||
DeleteRequested?.Invoke(this, drawing);
|
||||
};
|
||||
var saveToLibraryItem = new ToolStripMenuItem("Save to Drawing Library...");
|
||||
saveToLibraryItem.Click += (s, e) =>
|
||||
{
|
||||
if (SelectedItem is Drawing drawing)
|
||||
SaveToLibraryRequested?.Invoke(this, drawing);
|
||||
};
|
||||
var saveToLibrarySeparator = new ToolStripSeparator();
|
||||
menu.ShowItemToolTips = true;
|
||||
menu.Opening += (s, e) =>
|
||||
{
|
||||
if (SelectedItem == null)
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only hosts that handle the request offer it.
|
||||
saveToLibraryItem.Visible = saveToLibrarySeparator.Visible = SaveToLibraryRequested != null;
|
||||
saveToLibraryItem.Enabled = SaveToLibraryUnavailableReason == null;
|
||||
saveToLibraryItem.ToolTipText = SaveToLibraryUnavailableReason ?? "";
|
||||
};
|
||||
menu.Items.Add(saveToLibraryItem);
|
||||
menu.Items.Add(saveToLibrarySeparator);
|
||||
menu.Items.Add(deleteItem);
|
||||
ContextMenuStrip = menu;
|
||||
}
|
||||
|
||||
public event EventHandler<Drawing> DeleteRequested;
|
||||
|
||||
/// <summary>Raised when Save to Drawing Library is chosen for the selected drawing.</summary>
|
||||
public event EventHandler<Drawing> SaveToLibraryRequested;
|
||||
|
||||
/// <summary>Why Save to Drawing Library is disabled, shown as its tooltip; null enables it.</summary>
|
||||
public string SaveToLibraryUnavailableReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the left button is pressed on a drawing's row.
|
||||
/// </summary>
|
||||
|
||||
@@ -438,6 +438,7 @@ namespace OpenNest.Forms
|
||||
Text = Nest.Name;
|
||||
drawingListBox1.Units = Nest.Units;
|
||||
drawingListBox1.DeleteRequested += drawingListBox1_DeleteRequested;
|
||||
drawingListBox1.SaveToLibraryRequested += drawingListBox1_SaveToLibraryRequested;
|
||||
drawingListBox1.DrawingPickedUp += drawingListBox1_DrawingPickedUp;
|
||||
drawingListBox1.DrawingDropped += drawingListBox1_DrawingDropped;
|
||||
Activated += (_, _) => UpdateDrawingLibraryCommand();
|
||||
@@ -1200,6 +1201,7 @@ namespace OpenNest.Forms
|
||||
var enabled = NestStorage.Settings.IsDatabaseMode;
|
||||
drawingLibraryButton.Enabled = enabled;
|
||||
drawingLibraryButton.ToolTipText = enabled ? "Add from Drawing Library" : NestStorage.DatabaseModeOnly;
|
||||
drawingListBox1.SaveToLibraryUnavailableReason = enabled ? null : NestStorage.DatabaseModeOnly;
|
||||
}
|
||||
|
||||
/// <summary>Opens the drawing library browser and lists whatever it added.</summary>
|
||||
@@ -1225,6 +1227,43 @@ namespace OpenNest.Forms
|
||||
UpdateDrawingList();
|
||||
}
|
||||
|
||||
private void drawingListBox1_SaveToLibraryRequested(object sender, Drawing drawing)
|
||||
{
|
||||
SaveDrawingToLibrary(drawing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves one nest drawing to the drawing library as a new drawing or a revision of a
|
||||
/// matching one. On success only the drawing's library link changes.
|
||||
/// </summary>
|
||||
public void SaveDrawingToLibrary(Drawing drawing)
|
||||
{
|
||||
if (drawing == null || !NestStorage.Settings.IsDatabaseMode)
|
||||
return;
|
||||
|
||||
IDrawingLibraryStore library;
|
||||
try
|
||||
{
|
||||
library = NestStorage.CreateDrawingLibrary();
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
MessageBox.Show(this, ex.Message, "Drawing Library", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var lastCutType = Enum.TryParse<CutType>(Properties.Settings.Default.DrawingLibraryCutType, out var cutType)
|
||||
&& Enum.IsDefined(cutType) ? cutType : CutType.Laser;
|
||||
using var form = new SaveToDrawingLibraryForm(library, NestStorage.Settings.ServerUrl, Nest, drawing,
|
||||
lastCutType, Environment.UserName);
|
||||
if (form.ShowDialog(this) != DialogResult.OK || form.Result == null)
|
||||
return;
|
||||
|
||||
Properties.Settings.Default.DrawingLibraryCutType = form.Result.Saved.CutType.ToString();
|
||||
Properties.Settings.Default.Save();
|
||||
UpdateDrawingList();
|
||||
}
|
||||
|
||||
private void EditDrawingsInConverter_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (Nest.Drawings.Count == 0)
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace OpenNest.Forms
|
||||
/// The shared drawing library client for the current server URL, recreated only when
|
||||
/// the URL changes. Throws <see cref="ArgumentException"/> for an invalid URL.
|
||||
/// </summary>
|
||||
public static IDrawingLibrary CreateDrawingLibrary()
|
||||
public static IDrawingLibraryStore CreateDrawingLibrary()
|
||||
{
|
||||
if (cachedLibrary == null || cachedLibraryUrl != settings.ServerUrl)
|
||||
{
|
||||
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class SaveToDrawingLibraryForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
layoutTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
nameLabel = new System.Windows.Forms.Label();
|
||||
nameBox = new System.Windows.Forms.TextBox();
|
||||
customerLabel = new System.Windows.Forms.Label();
|
||||
customerBox = new System.Windows.Forms.TextBox();
|
||||
materialLabel = new System.Windows.Forms.Label();
|
||||
materialBox = new System.Windows.Forms.ComboBox();
|
||||
gradeLabel = new System.Windows.Forms.Label();
|
||||
gradeBox = new System.Windows.Forms.TextBox();
|
||||
thicknessLabel = new System.Windows.Forms.Label();
|
||||
thicknessBox = new System.Windows.Forms.NumericUpDown();
|
||||
cutTypeLabel = new System.Windows.Forms.Label();
|
||||
cutTypeBox = new System.Windows.Forms.ComboBox();
|
||||
notesLabel = new System.Windows.Forms.Label();
|
||||
notesBox = new System.Windows.Forms.TextBox();
|
||||
saveAsLabel = new System.Windows.Forms.Label();
|
||||
saveAsBox = new System.Windows.Forms.ComboBox();
|
||||
duplicateLabel = new System.Windows.Forms.Label();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
buttonPanel = new System.Windows.Forms.FlowLayoutPanel();
|
||||
cancelButton = new System.Windows.Forms.Button();
|
||||
saveButton = new System.Windows.Forms.Button();
|
||||
lookupTimer = new System.Windows.Forms.Timer(components);
|
||||
layoutTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)thicknessBox).BeginInit();
|
||||
buttonPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// layoutTable
|
||||
//
|
||||
layoutTable.ColumnCount = 2;
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.Controls.Add(nameLabel, 0, 0);
|
||||
layoutTable.Controls.Add(nameBox, 1, 0);
|
||||
layoutTable.Controls.Add(customerLabel, 0, 1);
|
||||
layoutTable.Controls.Add(customerBox, 1, 1);
|
||||
layoutTable.Controls.Add(materialLabel, 0, 2);
|
||||
layoutTable.Controls.Add(materialBox, 1, 2);
|
||||
layoutTable.Controls.Add(gradeLabel, 0, 3);
|
||||
layoutTable.Controls.Add(gradeBox, 1, 3);
|
||||
layoutTable.Controls.Add(thicknessLabel, 0, 4);
|
||||
layoutTable.Controls.Add(thicknessBox, 1, 4);
|
||||
layoutTable.Controls.Add(cutTypeLabel, 0, 5);
|
||||
layoutTable.Controls.Add(cutTypeBox, 1, 5);
|
||||
layoutTable.Controls.Add(notesLabel, 0, 6);
|
||||
layoutTable.Controls.Add(notesBox, 1, 6);
|
||||
layoutTable.Controls.Add(saveAsLabel, 0, 7);
|
||||
layoutTable.Controls.Add(saveAsBox, 1, 7);
|
||||
layoutTable.Controls.Add(duplicateLabel, 0, 8);
|
||||
layoutTable.Controls.Add(statusLabel, 0, 9);
|
||||
layoutTable.Controls.Add(buttonPanel, 0, 10);
|
||||
layoutTable.SetColumnSpan(duplicateLabel, 2);
|
||||
layoutTable.SetColumnSpan(statusLabel, 2);
|
||||
layoutTable.SetColumnSpan(buttonPanel, 2);
|
||||
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
layoutTable.Location = new System.Drawing.Point(0, 0);
|
||||
layoutTable.Name = "layoutTable";
|
||||
layoutTable.Padding = new System.Windows.Forms.Padding(8);
|
||||
layoutTable.RowCount = 11;
|
||||
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.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.Size = new System.Drawing.Size(584, 481);
|
||||
layoutTable.TabIndex = 0;
|
||||
//
|
||||
// nameLabel
|
||||
//
|
||||
nameLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
nameLabel.AutoSize = true;
|
||||
nameLabel.Name = "nameLabel";
|
||||
nameLabel.TabIndex = 0;
|
||||
nameLabel.Text = "Name:";
|
||||
//
|
||||
// nameBox
|
||||
//
|
||||
nameBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
nameBox.MaxLength = 200;
|
||||
nameBox.Name = "nameBox";
|
||||
nameBox.TabIndex = 1;
|
||||
nameBox.TextChanged += Details_Changed;
|
||||
//
|
||||
// customerLabel
|
||||
//
|
||||
customerLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
customerLabel.AutoSize = true;
|
||||
customerLabel.Name = "customerLabel";
|
||||
customerLabel.TabIndex = 2;
|
||||
customerLabel.Text = "Customer:";
|
||||
//
|
||||
// customerBox
|
||||
//
|
||||
customerBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
customerBox.MaxLength = 200;
|
||||
customerBox.Name = "customerBox";
|
||||
customerBox.TabIndex = 3;
|
||||
customerBox.TextChanged += Details_Changed;
|
||||
//
|
||||
// materialLabel
|
||||
//
|
||||
materialLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
materialLabel.AutoSize = true;
|
||||
materialLabel.Name = "materialLabel";
|
||||
materialLabel.TabIndex = 4;
|
||||
materialLabel.Text = "Material:";
|
||||
//
|
||||
// materialBox
|
||||
//
|
||||
materialBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
materialBox.MaxLength = 200;
|
||||
materialBox.Name = "materialBox";
|
||||
materialBox.TabIndex = 5;
|
||||
materialBox.TextChanged += Details_Changed;
|
||||
//
|
||||
// gradeLabel
|
||||
//
|
||||
gradeLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
gradeLabel.AutoSize = true;
|
||||
gradeLabel.Name = "gradeLabel";
|
||||
gradeLabel.TabIndex = 6;
|
||||
gradeLabel.Text = "Grade:";
|
||||
//
|
||||
// gradeBox
|
||||
//
|
||||
gradeBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
gradeBox.MaxLength = 200;
|
||||
gradeBox.Name = "gradeBox";
|
||||
gradeBox.TabIndex = 7;
|
||||
gradeBox.TextChanged += Details_Changed;
|
||||
//
|
||||
// thicknessLabel
|
||||
//
|
||||
thicknessLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
thicknessLabel.AutoSize = true;
|
||||
thicknessLabel.Name = "thicknessLabel";
|
||||
thicknessLabel.TabIndex = 8;
|
||||
thicknessLabel.Text = "Thickness:";
|
||||
//
|
||||
// thicknessBox
|
||||
//
|
||||
thicknessBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
thicknessBox.DecimalPlaces = 4;
|
||||
thicknessBox.Increment = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
thicknessBox.Maximum = new decimal(new int[] { 10000, 0, 0, 0 });
|
||||
thicknessBox.Name = "thicknessBox";
|
||||
thicknessBox.Size = new System.Drawing.Size(100, 25);
|
||||
thicknessBox.TabIndex = 9;
|
||||
thicknessBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
thicknessBox.ValueChanged += Details_Changed;
|
||||
//
|
||||
// cutTypeLabel
|
||||
//
|
||||
cutTypeLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
cutTypeLabel.AutoSize = true;
|
||||
cutTypeLabel.Name = "cutTypeLabel";
|
||||
cutTypeLabel.TabIndex = 10;
|
||||
cutTypeLabel.Text = "Cut type:";
|
||||
//
|
||||
// cutTypeBox
|
||||
//
|
||||
cutTypeBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
cutTypeBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
cutTypeBox.Name = "cutTypeBox";
|
||||
cutTypeBox.Size = new System.Drawing.Size(140, 25);
|
||||
cutTypeBox.TabIndex = 11;
|
||||
cutTypeBox.SelectedIndexChanged += Details_Changed;
|
||||
//
|
||||
// notesLabel
|
||||
//
|
||||
notesLabel.AutoSize = true;
|
||||
notesLabel.Margin = new System.Windows.Forms.Padding(3, 6, 3, 0);
|
||||
notesLabel.Name = "notesLabel";
|
||||
notesLabel.TabIndex = 12;
|
||||
notesLabel.Text = "Notes:";
|
||||
//
|
||||
// notesBox
|
||||
//
|
||||
notesBox.AcceptsReturn = true;
|
||||
notesBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
notesBox.MaxLength = 4000;
|
||||
notesBox.Multiline = true;
|
||||
notesBox.Name = "notesBox";
|
||||
notesBox.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
||||
notesBox.TabIndex = 13;
|
||||
notesBox.TextChanged += Notes_Changed;
|
||||
//
|
||||
// saveAsLabel
|
||||
//
|
||||
saveAsLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
saveAsLabel.AutoSize = true;
|
||||
saveAsLabel.Name = "saveAsLabel";
|
||||
saveAsLabel.TabIndex = 14;
|
||||
saveAsLabel.Text = "Save as:";
|
||||
//
|
||||
// saveAsBox
|
||||
//
|
||||
saveAsBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
saveAsBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
saveAsBox.Name = "saveAsBox";
|
||||
saveAsBox.TabIndex = 15;
|
||||
saveAsBox.SelectedIndexChanged += SaveAsBox_SelectedIndexChanged;
|
||||
//
|
||||
// duplicateLabel
|
||||
//
|
||||
duplicateLabel.AutoEllipsis = true;
|
||||
duplicateLabel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
duplicateLabel.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
duplicateLabel.Name = "duplicateLabel";
|
||||
duplicateLabel.Size = new System.Drawing.Size(100, 20);
|
||||
duplicateLabel.TabIndex = 16;
|
||||
duplicateLabel.Text = "Checking the library...";
|
||||
//
|
||||
// statusLabel
|
||||
//
|
||||
statusLabel.AutoEllipsis = true;
|
||||
statusLabel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
statusLabel.ForeColor = System.Drawing.Color.Firebrick;
|
||||
statusLabel.Name = "statusLabel";
|
||||
statusLabel.Size = new System.Drawing.Size(100, 20);
|
||||
statusLabel.TabIndex = 17;
|
||||
//
|
||||
// buttonPanel
|
||||
//
|
||||
buttonPanel.AutoSize = true;
|
||||
buttonPanel.Controls.Add(cancelButton);
|
||||
buttonPanel.Controls.Add(saveButton);
|
||||
buttonPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft;
|
||||
buttonPanel.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
buttonPanel.Name = "buttonPanel";
|
||||
buttonPanel.TabIndex = 18;
|
||||
//
|
||||
// cancelButton
|
||||
//
|
||||
cancelButton.AutoSize = true;
|
||||
cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
cancelButton.Name = "cancelButton";
|
||||
cancelButton.TabIndex = 1;
|
||||
cancelButton.Text = "Cancel";
|
||||
cancelButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// saveButton
|
||||
//
|
||||
saveButton.AutoSize = true;
|
||||
saveButton.Enabled = false;
|
||||
saveButton.Name = "saveButton";
|
||||
saveButton.TabIndex = 0;
|
||||
saveButton.Text = "Save to Library";
|
||||
saveButton.UseVisualStyleBackColor = true;
|
||||
saveButton.Click += SaveButton_Click;
|
||||
//
|
||||
// lookupTimer
|
||||
//
|
||||
lookupTimer.Interval = 400;
|
||||
lookupTimer.Tick += LookupTimer_Tick;
|
||||
//
|
||||
// SaveToDrawingLibraryForm
|
||||
//
|
||||
AcceptButton = saveButton;
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
CancelButton = cancelButton;
|
||||
ClientSize = new System.Drawing.Size(584, 481);
|
||||
Controls.Add(layoutTable);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
MinimumSize = new System.Drawing.Size(480, 420);
|
||||
Name = "SaveToDrawingLibraryForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Save to Drawing Library";
|
||||
Shown += SaveToDrawingLibraryForm_Shown;
|
||||
layoutTable.ResumeLayout(false);
|
||||
layoutTable.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)thicknessBox).EndInit();
|
||||
buttonPanel.ResumeLayout(false);
|
||||
buttonPanel.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.TableLayoutPanel layoutTable;
|
||||
private System.Windows.Forms.Label nameLabel;
|
||||
private System.Windows.Forms.TextBox nameBox;
|
||||
private System.Windows.Forms.Label customerLabel;
|
||||
private System.Windows.Forms.TextBox customerBox;
|
||||
private System.Windows.Forms.Label materialLabel;
|
||||
private System.Windows.Forms.ComboBox materialBox;
|
||||
private System.Windows.Forms.Label gradeLabel;
|
||||
private System.Windows.Forms.TextBox gradeBox;
|
||||
private System.Windows.Forms.Label thicknessLabel;
|
||||
private System.Windows.Forms.NumericUpDown thicknessBox;
|
||||
private System.Windows.Forms.Label cutTypeLabel;
|
||||
private System.Windows.Forms.ComboBox cutTypeBox;
|
||||
private System.Windows.Forms.Label notesLabel;
|
||||
private System.Windows.Forms.TextBox notesBox;
|
||||
private System.Windows.Forms.Label saveAsLabel;
|
||||
private System.Windows.Forms.ComboBox saveAsBox;
|
||||
private System.Windows.Forms.Label duplicateLabel;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
private System.Windows.Forms.FlowLayoutPanel buttonPanel;
|
||||
private System.Windows.Forms.Button cancelButton;
|
||||
private System.Windows.Forms.Button saveButton;
|
||||
private System.Windows.Forms.Timer lookupTimer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Drawing list > Save to Drawing Library (Database mode). Edits the library details,
|
||||
/// looks up library drawings with the same details (D4) whenever they change, and saves
|
||||
/// the nest drawing as the next revision of the chosen one or as a separate drawing
|
||||
/// through <see cref="DrawingLibrarySaveSession"/>. On success the drawing's library link
|
||||
/// points at the saved revision; the nest's copy is otherwise unchanged.
|
||||
/// </summary>
|
||||
public partial class SaveToDrawingLibraryForm : Form
|
||||
{
|
||||
private readonly DrawingLibrarySaveSession session;
|
||||
private readonly string savedBy;
|
||||
private DrawingLibraryDuplicates duplicates;
|
||||
private int lookupGeneration;
|
||||
private bool loading;
|
||||
private bool saving;
|
||||
|
||||
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
|
||||
/// <summary>The saved revision once the dialog closes with OK; otherwise null.</summary>
|
||||
public DrawingLibrarySaveResult Result { get; private set; }
|
||||
|
||||
public SaveToDrawingLibraryForm(IDrawingLibraryStore library, string serverUrl, Nest nest, Drawing drawing,
|
||||
CutType lastCutType, string savedBy)
|
||||
{
|
||||
InitializeComponent();
|
||||
session = new DrawingLibrarySaveSession(library, serverUrl, nest, drawing);
|
||||
this.savedBy = savedBy ?? "";
|
||||
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
|
||||
|
||||
foreach (var name in PostProcessorMaterials.Names)
|
||||
materialBox.Items.Add(name);
|
||||
foreach (var cutType in Enum.GetValues<CutType>())
|
||||
cutTypeBox.Items.Add(cutType);
|
||||
|
||||
var metadata = session.Prefill(lastCutType, this.savedBy);
|
||||
loading = true;
|
||||
nameBox.Text = metadata.Name;
|
||||
customerBox.Text = metadata.Customer;
|
||||
materialBox.Text = metadata.MaterialName;
|
||||
gradeBox.Text = metadata.MaterialGrade;
|
||||
thicknessBox.Value = System.Math.Min(thicknessBox.Maximum, (decimal)metadata.Thickness);
|
||||
thicknessLabel.Text = $"Thickness ({UnitsHelper.GetShortString(metadata.Units)}):";
|
||||
cutTypeBox.SelectedItem = metadata.CutType;
|
||||
notesBox.Text = metadata.Notes;
|
||||
loading = false;
|
||||
|
||||
Text = $"Save '{drawing.Name}' to Drawing Library — {serverUrl}";
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private async void SaveToDrawingLibraryForm_Shown(object sender, EventArgs e) => await LookUpAsync();
|
||||
|
||||
private void Details_Changed(object sender, EventArgs e)
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
|
||||
// The choice must be made against the matches for the details as they now are.
|
||||
if (duplicates != null && !duplicates.IsFor(ReadMetadata()))
|
||||
{
|
||||
duplicates = null;
|
||||
saveAsBox.Items.Clear();
|
||||
duplicateLabel.Text = "Checking the library...";
|
||||
}
|
||||
|
||||
lookupTimer.Stop();
|
||||
lookupTimer.Start();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void Notes_Changed(object sender, EventArgs e) => UpdateCommands();
|
||||
|
||||
private async void LookupTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
lookupTimer.Stop();
|
||||
await LookUpAsync();
|
||||
}
|
||||
|
||||
private void SaveAsBox_SelectedIndexChanged(object sender, EventArgs e) => UpdateCommands();
|
||||
|
||||
private async void SaveButton_Click(object sender, EventArgs e) => await SaveAsync();
|
||||
|
||||
/// <summary>Finds the library drawings the current details duplicate and offers them; latest lookup wins.</summary>
|
||||
internal async Task LookUpAsync()
|
||||
{
|
||||
var generation = ++lookupGeneration;
|
||||
var metadata = ReadMetadata();
|
||||
duplicateLabel.Text = "Checking the library...";
|
||||
UpdateCommands();
|
||||
try
|
||||
{
|
||||
var found = await session.FindDuplicatesAsync(metadata);
|
||||
if (IsDisposed || generation != lookupGeneration)
|
||||
return;
|
||||
duplicates = found;
|
||||
ShowChoices(found);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException
|
||||
|| ex is ArgumentException)
|
||||
{
|
||||
if (IsDisposed || generation != lookupGeneration)
|
||||
return;
|
||||
duplicates = null;
|
||||
saveAsBox.Items.Clear();
|
||||
duplicateLabel.Text = ex is ServerTooOldException
|
||||
? ex.Message
|
||||
: $"Could not check the library: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!IsDisposed)
|
||||
UpdateCommands();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Packages the drawing and saves it as the selected choice; closes with OK on success.</summary>
|
||||
internal async Task SaveAsync()
|
||||
{
|
||||
if (saving || !CanSave())
|
||||
return;
|
||||
|
||||
var metadata = ReadMetadata();
|
||||
var choice = (SaveChoice)saveAsBox.SelectedItem;
|
||||
saving = true;
|
||||
UpdateCommands();
|
||||
statusLabel.Text = "Saving...";
|
||||
try
|
||||
{
|
||||
// Modal, so the nest and drawing cannot change while they are packaged and sent.
|
||||
var package = DrawingPackage.Write(session.Drawing, session.Nest.Units);
|
||||
var result = await session.SaveAsync(metadata, package, choice.Revise);
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
if (result.RecoveredEarlierSave)
|
||||
{
|
||||
ShowMessage($"An earlier save that seemed to fail did reach the library as revision {result.Saved.Revision} "
|
||||
+ $"of '{result.Saved.Name}'. Your changes since then were not saved; save again to store them.",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
Result = result;
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
catch (DrawingLibraryConflictException ex)
|
||||
{
|
||||
Fail(ex.Message);
|
||||
await LookUpAsync();
|
||||
}
|
||||
catch (ServerTooOldException ex)
|
||||
{
|
||||
Fail(ex.Message);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException
|
||||
|| ex is ArgumentException)
|
||||
{
|
||||
Fail(session.HasPendingSave
|
||||
? $"Could not confirm the save: {ex.Message}{Environment.NewLine}{Environment.NewLine}"
|
||||
+ "Save again to check whether it reached the library; it will not be stored twice."
|
||||
: $"Could not save to the library: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
saving = false;
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (statusLabel.Text == "Saving...")
|
||||
statusLabel.Text = "";
|
||||
UpdateCommands();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Fail(string message)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
statusLabel.Text = "";
|
||||
ShowMessage(message, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
private void ShowChoices(DrawingLibraryDuplicates found)
|
||||
{
|
||||
saveAsBox.BeginUpdate();
|
||||
saveAsBox.Items.Clear();
|
||||
SaveChoice selected = null;
|
||||
foreach (var record in found.Choices)
|
||||
{
|
||||
var choice = new SaveChoice(record, Describe(record, linked: found.Linked?.Id == record.Id));
|
||||
saveAsBox.Items.Add(choice);
|
||||
if (found.Default?.Id == record.Id)
|
||||
selected = choice;
|
||||
}
|
||||
|
||||
var separate = new SaveChoice(null, "Create a separate library drawing");
|
||||
saveAsBox.Items.Add(separate);
|
||||
if (selected == null && !found.RequiresChoice && found.Matches.Count == 0)
|
||||
selected = separate;
|
||||
saveAsBox.SelectedItem = selected;
|
||||
saveAsBox.EndUpdate();
|
||||
|
||||
duplicateLabel.Text = found.Matches.Count switch
|
||||
{
|
||||
0 => "No library drawing has these details.",
|
||||
1 => "A library drawing has these details; saving adds a revision to it.",
|
||||
_ when found.RequiresChoice => $"{found.Matches.Count} library drawings have these details. Choose one, or a separate drawing.",
|
||||
_ => $"{found.Matches.Count} library drawings have these details.",
|
||||
};
|
||||
}
|
||||
|
||||
private static string Describe(DrawingLibraryRecord record, bool linked)
|
||||
{
|
||||
var customer = string.IsNullOrWhiteSpace(record.Customer) ? "" : $" ({record.Customer})";
|
||||
var saved = record.SavedAt == default ? "" : $", saved {record.SavedAt.ToLocalTime().ToString("d", CultureInfo.CurrentCulture)}";
|
||||
var link = linked ? " — this drawing's library copy" : "";
|
||||
return $"Revision {record.Revision + 1} of '{record.Name}'{customer}, {record.MaterialName} {record.MaterialGrade} "
|
||||
+ $"{record.Thickness.ToString("0.####", CultureInfo.CurrentCulture)} {record.CutType}{saved}{link}";
|
||||
}
|
||||
|
||||
private DrawingLibraryRecord ReadMetadata() => new()
|
||||
{
|
||||
Name = nameBox.Text.Trim(),
|
||||
Customer = customerBox.Text.Trim(),
|
||||
MaterialName = materialBox.Text.Trim(),
|
||||
MaterialGrade = gradeBox.Text.Trim(),
|
||||
Thickness = DrawingLibraryRules.NormalizeThickness((double)thicknessBox.Value),
|
||||
Units = session.Nest.Units,
|
||||
CutType = cutTypeBox.SelectedItem is CutType cutType ? cutType : CutType.Laser,
|
||||
Notes = notesBox.Text,
|
||||
SavedBy = savedBy,
|
||||
};
|
||||
|
||||
private bool CanSave()
|
||||
{
|
||||
var metadata = ReadMetadata();
|
||||
return duplicates != null && duplicates.IsFor(metadata) && saveAsBox.SelectedItem is SaveChoice
|
||||
&& DrawingLibraryRules.GetMetadataError(metadata) == null;
|
||||
}
|
||||
|
||||
private void UpdateCommands()
|
||||
{
|
||||
var error = DrawingLibraryRules.GetMetadataError(ReadMetadata());
|
||||
saveButton.Enabled = !saving && CanSave();
|
||||
if (!saving)
|
||||
statusLabel.Text = error == null ? "" : char.ToUpper(error[0], CultureInfo.CurrentCulture) + error[1..];
|
||||
}
|
||||
|
||||
/// <summary>One "Save as" option: a revision of a listed library drawing, or a separate drawing.</summary>
|
||||
private sealed class SaveChoice
|
||||
{
|
||||
public SaveChoice(DrawingLibraryRecord revise, string text)
|
||||
{
|
||||
Revise = revise;
|
||||
Text = text;
|
||||
}
|
||||
|
||||
/// <summary>The library drawing to revise, or null for a separate drawing.</summary>
|
||||
public DrawingLibraryRecord Revise { get; }
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+13
@@ -270,5 +270,18 @@ namespace OpenNest.Properties {
|
||||
this["UpdateFeedUrl"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("Laser")]
|
||||
// Cut type last chosen in Save to Drawing Library on this PC.
|
||||
public string DrawingLibraryCutType {
|
||||
get {
|
||||
return ((string)(this["DrawingLibraryCutType"]));
|
||||
}
|
||||
set {
|
||||
this["DrawingLibraryCutType"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,5 +65,8 @@
|
||||
<Setting Name="UpdateFeedUrl" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="DrawingLibraryCutType" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">Laser</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
+33
-2
@@ -144,8 +144,8 @@ only the requested page of metadata (never archive bytes):
|
||||
## Drawing library endpoints
|
||||
|
||||
The server also keeps a shared library of drawings (schema v4). The desktop
|
||||
can add library drawings to a nest (below); saving to the library is not built
|
||||
yet. A drawing's geometry travels as an opaque
|
||||
adds library drawings to a nest and saves nest drawings to the library (below).
|
||||
A drawing's geometry travels as an opaque
|
||||
**drawing package**: a `.nest` archive holding exactly one drawing and no plates,
|
||||
written and read on the client by `OpenNest.IO.DrawingPackage`. The server never
|
||||
parses it.
|
||||
@@ -223,6 +223,37 @@ id, revision and a snapshot of the library details) that is saved with the nest
|
||||
(see [the nest file format](nest-file-format.md#drawing-library-links)). Editing it
|
||||
in the nest never changes the library.
|
||||
|
||||
### Save to Drawing Library (desktop)
|
||||
|
||||
Right-click a drawing in the drawing list > **Save to Drawing Library...**
|
||||
(Database mode only; disabled with "Database mode only" in File mode). The
|
||||
dialog (`OpenNest.Data.DrawingLibrarySaveSession`) prefills name and customer
|
||||
from the drawing (the nest's customer when the drawing has none), material,
|
||||
grade, thickness and units from the nest (the drawing's material when the nest
|
||||
has none), and the cut type and notes of the linked revision when the drawing
|
||||
came from this server's library, otherwise the cut type last saved on this PC.
|
||||
Units follow the nest and cannot be changed.
|
||||
|
||||
Whenever the details change, the dialog looks up active library drawings with
|
||||
the same normalized customer, name, material and grade and the exact thickness,
|
||||
units and cut type, and offers **Save as**:
|
||||
|
||||
- No match: a separate (new) library drawing.
|
||||
- One match, or the drawing's own linked library drawing among several: the
|
||||
next revision of it, guarded by `If-Match` on the revision listed.
|
||||
- Several matches: no default; the operator chooses one or a separate drawing.
|
||||
- A linked drawing whose details no longer match is offered but never the
|
||||
default, so changing thickness cannot silently rewrite it.
|
||||
|
||||
Save stays disabled until the lookup for the current details has finished. A
|
||||
409 (someone else saved first) is shown and the choices are refreshed. A save
|
||||
that fails without a definite answer stays pending: the next Save first looks
|
||||
for the revision it would have produced and adopts it when the package hash
|
||||
and details match, so it is never stored twice and a retried new drawing keeps
|
||||
its client-allocated id. Saving identical content at the current revision is a
|
||||
no-op. On success only the nest drawing's library link changes, to the saved
|
||||
revision; save the nest to keep it.
|
||||
|
||||
## Storage
|
||||
|
||||
`OpenNest.Server.NestDatabase` uses SQLite (`Microsoft.Data.Sqlite`) with
|
||||
|
||||
Reference in new issue
Block a user