mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-29 06:51:06 -04:00
fix(browser): integrate metadata-safe nest information edits
This commit is contained in:
commit
697b6492c7
5 files changed
+348
-8
No files matched your search
@@ -9,6 +9,22 @@ namespace OpenNest.Data;
|
||||
/// </summary>
|
||||
public static class NestRecordFactory
|
||||
{
|
||||
/// <summary>Overlays authoritative current metadata on an archived nest before editing.</summary>
|
||||
public static void ApplyMetadata(Nest nest, NestRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
nest.Name = record.Name;
|
||||
nest.Customer = record.Customer;
|
||||
nest.Notes = record.Comments;
|
||||
nest.Status = record.Status;
|
||||
nest.MadeBy = record.MadeBy;
|
||||
nest.DateLastModified = record.DateModified;
|
||||
nest.DateCreated = record.DateCreated;
|
||||
nest.Material = NestMaterialSelection.Apply(nest.Material, record.Material);
|
||||
nest.Thickness = record.Thickness;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a record for uploading <paramref name="nest"/>. <paramref name="id"/> is the
|
||||
/// client-tracked id: <see cref="System.Guid.Empty"/> for a first save (the server
|
||||
|
||||
@@ -6,6 +6,52 @@ namespace OpenNest.Tests.Data;
|
||||
|
||||
public class NestRecordFactoryTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Steel", "Aluminum", false)]
|
||||
[InlineData("Aluminum", "Aluminum", true)]
|
||||
public void ApplyMetadata_UsesAllCurrentFields_AndPreservesMatchingMaterialProperties(
|
||||
string archiveMaterial, string currentMaterial, bool sameMaterial)
|
||||
{
|
||||
var nest = new Nest("Old archive")
|
||||
{
|
||||
Material = new Material(archiveMaterial, "Test grade", 0.1),
|
||||
Thickness = 0.125,
|
||||
DateCreated = new DateTime(2025, 1, 1),
|
||||
};
|
||||
var plate = nest.CreatePlate();
|
||||
var current = new NestRecord
|
||||
{
|
||||
Name = "Current name",
|
||||
Customer = "Current customer",
|
||||
Comments = "Current notes",
|
||||
Material = currentMaterial,
|
||||
Thickness = 0.375,
|
||||
DateCreated = new DateTime(2026, 1, 1),
|
||||
DateModified = new DateTime(2026, 2, 1),
|
||||
Status = NestStatus.HasBeenCut,
|
||||
MadeBy = "Operator",
|
||||
};
|
||||
NestRecordFactory.ApplyMetadata(nest, current);
|
||||
var roundTrip = NestRecordFactory.FromNest(nest, Guid.Empty, 0);
|
||||
Assert.Equal(current.Material, roundTrip.Material);
|
||||
Assert.Equal(current.Thickness, roundTrip.Thickness);
|
||||
Assert.Equal(current.DateCreated, roundTrip.DateCreated);
|
||||
Assert.Equal(current.DateModified, roundTrip.DateModified);
|
||||
Assert.Equal(current.Name, roundTrip.Name);
|
||||
Assert.Equal(current.Customer, roundTrip.Customer);
|
||||
Assert.Equal(current.Comments, roundTrip.Comments);
|
||||
Assert.Equal(current.Status, roundTrip.Status);
|
||||
Assert.Equal(current.MadeBy, roundTrip.MadeBy);
|
||||
if (sameMaterial)
|
||||
{
|
||||
Assert.Equal("Test grade", nest.Material.Grade);
|
||||
Assert.Equal(0.1, nest.Material.Density);
|
||||
}
|
||||
else
|
||||
Assert.NotEqual("Test grade", nest.Material.Grade);
|
||||
Assert.Same(plate, Assert.Single(nest.Plates));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromNest_CountsPlatesAndNonCutoffParts()
|
||||
{
|
||||
|
||||
@@ -145,6 +145,140 @@ public class SavedNestsFormTests
|
||||
"The saved-nest dialog test did not complete.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(true, true)]
|
||||
public void NestInformation_SavesOnlyOnAccept_AndRefusesStaleWrites(bool accept, bool conflict)
|
||||
{
|
||||
StaTestThread.Run(() =>
|
||||
{
|
||||
// PerformClick outside Application.Run needs the real UI dispatcher installed.
|
||||
using var context = new WindowsFormsSynchronizationContext();
|
||||
var record = Record("Current name");
|
||||
record.Revision = 3;
|
||||
record.Comments = "Current metadata notes";
|
||||
record.Status = NestStatus.ToBeCut;
|
||||
record.Material = "Aluminum";
|
||||
record.Thickness = 0.375;
|
||||
record.DateCreated = new DateTime(2026, 1, 1);
|
||||
var repository = new FakeRepository(record) { Conflict = conflict };
|
||||
repository.Files[record.Id] = NestFile((Quantity: 2, Parts: 3));
|
||||
record.FileSize = repository.Files[record.Id].Length;
|
||||
var original = repository.Files[record.Id].ToArray();
|
||||
using var form = new SavedNestsForm(repository, "http://nest-server:5000");
|
||||
form.Show();
|
||||
PumpUntil(() => Field<DataGridView>(form, "nestGrid").Rows.Count == 1, "the nest page");
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
var shown = false;
|
||||
form.ShowNestInfoDialog = dialog =>
|
||||
{
|
||||
shown = true;
|
||||
Assert.Equal(record.Name, dialog.NestName);
|
||||
Assert.Equal(record.Comments, dialog.Notes);
|
||||
Assert.Equal(NestStatus.ToBeCut, dialog.Status);
|
||||
Assert.Equal("Aluminum", dialog.MaterialName);
|
||||
Assert.Equal(0.375, dialog.Thickness);
|
||||
Assert.Equal(new DateTime(2026, 1, 1), dialog.DateCreated);
|
||||
dialog.Customer = "Updated customer";
|
||||
dialog.Notes = "Updated notes";
|
||||
dialog.Status = NestStatus.HasBeenCut;
|
||||
dialog.MadeBy = "Operator";
|
||||
return accept ? DialogResult.OK : DialogResult.Cancel;
|
||||
};
|
||||
var button = Field<Button>(form, "infoButton");
|
||||
Assert.True(button.Enabled);
|
||||
Assert.Contains(Field<DataGridView>(form, "nestGrid").ContextMenuStrip!.Items.Cast<ToolStripItem>(),
|
||||
item => item.Text == "Nest Information...");
|
||||
button.PerformClick();
|
||||
PumpUntil(() => shown && form.Enabled, "the information edit");
|
||||
Assert.True(shown);
|
||||
Assert.True(form.Enabled);
|
||||
Assert.Equal(DialogResult.None, form.DialogResult);
|
||||
Assert.Equal(Guid.Empty, form.SelectedId);
|
||||
Assert.Equal(accept ? 1 : 0, repository.UpdateAttempts);
|
||||
if (accept && !conflict)
|
||||
{
|
||||
Assert.Equal(3, repository.ExpectedRevision);
|
||||
Assert.Equal(4, record.Revision);
|
||||
Assert.Equal("Updated customer", record.Customer);
|
||||
Assert.Equal("Updated notes", record.Comments);
|
||||
using var stream = new MemoryStream(repository.Files[record.Id]);
|
||||
var nest = new NestReader(stream).Read();
|
||||
Assert.Equal("Updated customer", nest.Customer);
|
||||
Assert.Equal("Updated notes", nest.Notes);
|
||||
Assert.Equal(NestStatus.HasBeenCut, nest.Status);
|
||||
Assert.Equal("Operator", nest.MadeBy);
|
||||
Assert.Equal("Aluminum", nest.Material.Name);
|
||||
Assert.Equal(0.375, nest.Thickness);
|
||||
Assert.Equal(new DateTime(2026, 1, 1), nest.DateCreated);
|
||||
Assert.Equal("Aluminum", record.Material);
|
||||
Assert.Equal(0.375, record.Thickness);
|
||||
Assert.Equal(new DateTime(2026, 1, 1), record.DateCreated);
|
||||
Assert.Single(nest.Plates);
|
||||
Assert.Equal(2, nest.Plates[0].Quantity);
|
||||
Assert.Equal(3, nest.Plates[0].Parts.Count);
|
||||
Assert.Equal("Updated customer", Field<DataGridView>(form, "nestGrid").Rows[0].Cells["Customer"].Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(original, repository.Files[record.Id]);
|
||||
Assert.Equal(3, record.Revision);
|
||||
if (conflict)
|
||||
Assert.Contains("409", Field<Label>(form, "summaryLabel").Text);
|
||||
}
|
||||
}, TestTimeout, "The information edit did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestInformation_PendingOperationBlocksReentryAndClose_AndRefreshesWarmedDetails()
|
||||
{
|
||||
StaTestThread.Run(() =>
|
||||
{
|
||||
// PerformClick outside Application.Run needs the real UI dispatcher installed.
|
||||
using var context = new WindowsFormsSynchronizationContext();
|
||||
var record = Record("Busy job");
|
||||
record.Revision = 1;
|
||||
var repository = new FakeRepository(record);
|
||||
repository.Files[record.Id] = NestFile((Quantity: 1, Parts: 2));
|
||||
record.FileSize = repository.Files[record.Id].Length;
|
||||
using var form = new SavedNestsForm(repository, "http://nest-server:5000");
|
||||
form.Show();
|
||||
PumpUntil(() => Field<DataGridView>(form, "platesGrid").Rows.Count == 1, "warmed details");
|
||||
var oldDetails = Field<NestDetailsSession>(form, "details");
|
||||
repository.ReadGate = new TaskCompletionSource<bool>();
|
||||
repository.SaveGate = new TaskCompletionSource<bool>();
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
var shown = false;
|
||||
form.ShowNestInfoDialog = dialog =>
|
||||
{
|
||||
shown = true;
|
||||
dialog.Notes = "Saved after wait";
|
||||
return DialogResult.OK;
|
||||
};
|
||||
var operation = form.EditSelectedInfoAsync();
|
||||
Assert.False(operation.IsCompleted);
|
||||
Assert.False(form.Enabled);
|
||||
Assert.False(Field<ToolStripMenuItem>(form, "infoMenuItem").Enabled);
|
||||
form.Close();
|
||||
Assert.False(form.IsDisposed);
|
||||
Assert.True(form.EditSelectedInfoAsync().IsCompletedSuccessfully);
|
||||
Assert.False(shown);
|
||||
repository.ReadGate.SetResult(true);
|
||||
PumpUntil(() => repository.UpdateAttempts == 1, "pending save");
|
||||
Assert.True(shown);
|
||||
Assert.False(operation.IsCompleted);
|
||||
Assert.False(form.Enabled);
|
||||
repository.SaveGate.SetResult(true);
|
||||
PumpUntil(() => operation.IsCompleted, "completed save");
|
||||
Assert.Null(operation.Exception);
|
||||
Assert.True(form.Enabled);
|
||||
Assert.NotSame(oldDetails, Field<NestDetailsSession>(form, "details"));
|
||||
PumpUntil(() => Field<DataGridView>(form, "platesGrid").Rows.Count == 1, "reloaded details");
|
||||
Assert.Equal("Saved after wait", record.Comments);
|
||||
}, TestTimeout, "The pending information edit did not complete.");
|
||||
}
|
||||
|
||||
private static NestRecord Record(string name) =>
|
||||
new() { Id = Guid.NewGuid(), Name = name, SavedAt = DateTime.Now };
|
||||
|
||||
@@ -203,8 +337,43 @@ public class SavedNestsFormTests
|
||||
|
||||
public FakeRepository(params NestRecord[] records) => _records = records;
|
||||
|
||||
public TaskCompletionSource<bool>? ReadGate { get; set; }
|
||||
public TaskCompletionSource<bool>? SaveGate { get; set; }
|
||||
public bool Conflict { get; set; }
|
||||
public int UpdateAttempts { get; private set; }
|
||||
public int ExpectedRevision { get; private set; }
|
||||
public Dictionary<Guid, byte[]> Files { get; } = new();
|
||||
|
||||
public Task<byte[]?> GetRevisionFileAsync(Guid id, int revisionNumber, CancellationToken cancellationToken = default) =>
|
||||
GetFileAsync(id, cancellationToken);
|
||||
|
||||
public Task<NestRevisionPage> GetRevisionsAsync(Guid id, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new NestRevisionPage());
|
||||
|
||||
public async Task<NestRecord> UpdateFileAsync(Guid id, byte[] bytes, NestRecord metadata, int expectedRevision,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
UpdateAttempts++;
|
||||
ExpectedRevision = expectedRevision;
|
||||
if (SaveGate != null) await SaveGate.Task;
|
||||
if (Conflict) throw new IOException("409: changed on another PC");
|
||||
var current = _records.Single(r => r.Id == id);
|
||||
Assert.Equal(current.Revision, expectedRevision);
|
||||
current.Revision++;
|
||||
current.Name = metadata.Name;
|
||||
current.Customer = metadata.Customer;
|
||||
current.Comments = metadata.Comments;
|
||||
current.Material = metadata.Material;
|
||||
current.Thickness = metadata.Thickness;
|
||||
current.Status = metadata.Status;
|
||||
current.MadeBy = metadata.MadeBy;
|
||||
current.DateModified = metadata.DateModified;
|
||||
current.DateCreated = metadata.DateCreated;
|
||||
current.FileSize = bytes.Length;
|
||||
Files[id] = bytes;
|
||||
return current;
|
||||
}
|
||||
|
||||
public Task<NestPage> QueryAsync(NestQuery query, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new NestPage
|
||||
{
|
||||
@@ -220,8 +389,11 @@ public class SavedNestsFormTests
|
||||
public Task<IReadOnlyList<NestRecord>> ListAsync(CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
public async Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (ReadGate != null) await ReadGate.Task;
|
||||
return _records.SingleOrDefault(r => r.Id == id);
|
||||
}
|
||||
|
||||
public Task<NestRecord> UploadAsync(byte[] nestFile, NestRecord record, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace OpenNest.Forms;
|
||||
/// server one bounded page at a time; filtering, sorting and paging run on the server
|
||||
/// through <see cref="NestBrowseSession"/>. The lower tabs show plates and drawings
|
||||
/// from <see cref="NestDetailsSession"/> and on-demand version history beside a plate
|
||||
/// preview. This form renders the selected nest without editing its archive.
|
||||
/// preview. Nest Information edits a detached snapshot through guarded database saves.
|
||||
/// </summary>
|
||||
public sealed class SavedNestsForm : Form
|
||||
{
|
||||
@@ -63,6 +63,11 @@ public sealed class SavedNestsForm : Form
|
||||
private readonly ToolStripMenuItem openMenuItem;
|
||||
private readonly ToolStripMenuItem deleteMenuItem;
|
||||
private bool deleting;
|
||||
private bool editingInfo;
|
||||
private readonly Button infoButton;
|
||||
private readonly ToolStripMenuItem infoMenuItem;
|
||||
|
||||
internal Func<EditNestInfoForm, DialogResult> ShowNestInfoDialog { get; set; }
|
||||
private IReadOnlyList<Plate> previewPlates = Array.Empty<Plate>();
|
||||
private int previewIndex = -1;
|
||||
|
||||
@@ -143,7 +148,8 @@ public sealed class SavedNestsForm : Form
|
||||
openMenuItem = new ToolStripMenuItem("Open", null, (_, _) => OpenSelected()) { ShortcutKeyDisplayString = "Enter" };
|
||||
deleteMenuItem = new ToolStripMenuItem("Delete...", null, async (_, _) => await DeleteSelectedAsync());
|
||||
var refreshMenuItem = new ToolStripMenuItem("Refresh", null, async (_, _) => await RunAsync(session.RefreshAsync));
|
||||
menu.Items.AddRange(new ToolStripItem[] { openMenuItem, deleteMenuItem, new ToolStripSeparator(), refreshMenuItem });
|
||||
infoMenuItem = new ToolStripMenuItem("Nest Information...", null, async (_, _) => await EditSelectedInfoAsync());
|
||||
menu.Items.AddRange(new ToolStripItem[] { openMenuItem, infoMenuItem, deleteMenuItem, new ToolStripSeparator(), refreshMenuItem });
|
||||
nestGrid.ContextMenuStrip = menu;
|
||||
|
||||
// Details of the highlighted nest.
|
||||
@@ -237,7 +243,14 @@ public sealed class SavedNestsForm : Form
|
||||
SplitterWidth = 6,
|
||||
};
|
||||
split.Panel1.Controls.Add(nestGrid);
|
||||
split.Panel1.Controls.Add(SectionLabel("Nests", DockStyle.Top));
|
||||
infoButton = new Button { Text = "Nest Information...", AutoSize = true, Dock = DockStyle.Right };
|
||||
infoButton.Click += async (_, _) => await EditSelectedInfoAsync();
|
||||
var nestsHeader = new Panel { Dock = DockStyle.Top, Height = 36 };
|
||||
nestsHeader.Controls.Add(infoButton);
|
||||
nestsHeader.Controls.Add(SectionLabel("Nests", DockStyle.Left));
|
||||
split.Panel1.Controls.Add(nestsHeader);
|
||||
ShowNestInfoDialog = dialog => dialog.ShowDialog(this);
|
||||
FormClosing += (_, e) => e.Cancel = editingInfo;
|
||||
split.Panel2.Controls.Add(detailsSplit);
|
||||
split.Panel2.Controls.Add(detailsHeader);
|
||||
|
||||
@@ -322,6 +335,8 @@ public sealed class SavedNestsForm : Form
|
||||
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
if (editingInfo)
|
||||
return true;
|
||||
switch (keyData)
|
||||
{
|
||||
case Keys.Enter when searchBox.Focused:
|
||||
@@ -492,9 +507,11 @@ public sealed class SavedNestsForm : Form
|
||||
|
||||
private void UpdateCommands()
|
||||
{
|
||||
var hasSelection = SelectedNestId() != null;
|
||||
var hasSelection = SelectedNestId() != null && !editingInfo && !deleting;
|
||||
openMenuItem.Enabled = hasSelection;
|
||||
deleteMenuItem.Enabled = hasSelection && !deleting;
|
||||
infoButton.Enabled = hasSelection;
|
||||
infoMenuItem.Enabled = hasSelection;
|
||||
deleteMenuItem.Enabled = hasSelection;
|
||||
}
|
||||
|
||||
private void UpdateTitle(string summary)
|
||||
@@ -759,7 +776,7 @@ public sealed class SavedNestsForm : Form
|
||||
|
||||
private void OpenSelected()
|
||||
{
|
||||
if (SelectedNestId() is not Guid id)
|
||||
if (editingInfo || SelectedNestId() is not Guid id)
|
||||
return;
|
||||
SelectedId = id;
|
||||
DialogResult = DialogResult.OK;
|
||||
@@ -801,6 +818,84 @@ public sealed class SavedNestsForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Edits a detached current snapshot without opening an MDI document.</summary>
|
||||
internal async Task EditSelectedInfoAsync()
|
||||
{
|
||||
if (editingInfo || deleting || SelectedNestId() is not Guid id)
|
||||
return;
|
||||
summaryLabel.Text = "Loading nest information...";
|
||||
editingInfo = true;
|
||||
Enabled = false;
|
||||
UpdateCommands();
|
||||
try
|
||||
{
|
||||
var snapshot = await NestHistorySession.ReadCurrentAsync(repository, id, serverUrl);
|
||||
if (IsDisposed) return;
|
||||
if (snapshot.Metadata.Revision < 1)
|
||||
throw new NotSupportedException("Upgrade the nest server and refresh before editing nest information.");
|
||||
var nest = await Task.Run(() =>
|
||||
{
|
||||
using var source = new MemoryStream(snapshot.Archive);
|
||||
var loaded = new NestReader(source).Read();
|
||||
// Metadata-only revisions can be newer than the archived fields.
|
||||
NestRecordFactory.ApplyMetadata(loaded, snapshot.Metadata);
|
||||
return loaded;
|
||||
});
|
||||
if (IsDisposed) return;
|
||||
using var dialog = new EditNestInfoForm();
|
||||
dialog.LoadNestInfo(nest);
|
||||
if (ShowNestInfoDialog(dialog) != DialogResult.OK)
|
||||
return;
|
||||
// Same default propagation as Nest > Nest Info in the open editor.
|
||||
using var manager = new PlateManager(nest);
|
||||
manager.ChangePlateDefaults(() => dialog.SaveNestInfo(nest));
|
||||
nest.DateLastModified = DateTime.Now;
|
||||
summaryLabel.Text = "Saving nest information...";
|
||||
var bytes = await Task.Run(() =>
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
new NestWriter(nest).Write(output);
|
||||
return output.ToArray();
|
||||
});
|
||||
var save = new NestSaveSession();
|
||||
save.Bind(id, snapshot.ServerUrl, snapshot.Metadata.Revision);
|
||||
summaryLabel.Text = "Sending nest information...";
|
||||
var saved = await save.SaveAsync(repository, snapshot.ServerUrl, nest, bytes);
|
||||
var verified = await NestHistorySession.ReadCurrentAsync(repository, id, snapshot.ServerUrl);
|
||||
if (verified.Metadata.Revision != saved.Revision
|
||||
|| !System.Linq.Enumerable.SequenceEqual(bytes, verified.Archive))
|
||||
throw new InvalidDataException("Could not verify the saved nest information.");
|
||||
if (IsDisposed) return;
|
||||
// Drop archive previews and embedded history, including prefetched snapshots.
|
||||
details.Dispose();
|
||||
details = new NestDetailsSession(LoadDetailsAsync);
|
||||
historyView?.Dispose();
|
||||
historyView = null;
|
||||
historyId = null;
|
||||
ShowDetailsMessage("");
|
||||
await RunAsync(session.RefreshAsync);
|
||||
foreach (DataGridViewRow row in nestGrid.Rows)
|
||||
if (row.Cells[IdColumn].Value is Guid rowId && rowId == id)
|
||||
nestGrid.CurrentCell = row.Cells[0];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
summaryLabel.Text = $"Could not edit nest information: {ex.Message} Refresh and reopen Nest Information before trying again; the save may already have reached the server.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
editingInfo = false;
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Enabled = true;
|
||||
if (summaryLabel.Text == "Loading nest information...")
|
||||
summaryLabel.Text = session.Summary;
|
||||
UpdateCommands();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSelectedAsync()
|
||||
{
|
||||
if (deleting || SelectedNestId() is not Guid id)
|
||||
|
||||
@@ -28,6 +28,17 @@ The desktop app can save nests two ways:
|
||||
previous one. On the Versions tab, **Open read-only...** previews one snapshot
|
||||
without binding an editable document, and **Download selected version...** saves
|
||||
its exact archived bytes.
|
||||
Select a nest and click **Nest Information...** above the list (or use the
|
||||
row's right-click menu) to edit the same information as **Nest > Nest Info**
|
||||
without opening the nest in the main editor. **OK** saves immediately to the
|
||||
same database ID as a new version, then refreshes the list, preview and history;
|
||||
**Cancel** makes no change. The dialog starts with the latest saved metadata.
|
||||
Plate defaults follow the same rules as the open editor: populated sheets are
|
||||
not resized or re-nested. This action requires a version-aware server and
|
||||
refuses to overwrite a newer save from another PC. After a conflict or network
|
||||
error, refresh and reopen the information dialog before trying again; a lost
|
||||
reply can mean the save already reached the server. An already-open editor is
|
||||
not modified; reload it before saving further changes to that same nest.
|
||||
A separate confirmed **Restore selected version...** creates a new current
|
||||
revision of the same ID and keeps all intervening versions. **Delete...** hides
|
||||
the record from ordinary browsing but retains every archive; **Deleted nests...**
|
||||
|
||||
Reference in new issue
Block a user