mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-13 10:31:54 -04:00
feat(ui): show nest versions in database details tab
This commit is contained in:
1 parent
fae37dd1d0
commit
09dd19ca66
4 files changed
+105
-27
No files matched your search
@@ -49,6 +49,39 @@ public class NestHistoryFormTests
|
||||
}, TimeSpan.FromSeconds(30), "Recovery form did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SavedNests_VersionsTabFollowsSelectedNestWithoutWriting()
|
||||
{
|
||||
StaTestThread.Run(() =>
|
||||
{
|
||||
var repository = new Repository();
|
||||
using var browser = new SavedNestsForm(repository, "http://shop");
|
||||
browser.Show();
|
||||
Application.DoEvents();
|
||||
var tabs = Field<TabControl>(browser, "detailsTabs");
|
||||
Assert.Equal(new[] { "Plates", "Drawings", "Versions" },
|
||||
tabs.TabPages.Cast<TabPage>().Select(page => page.Text));
|
||||
tabs.SelectedIndex = 2;
|
||||
Application.DoEvents();
|
||||
var first = Field<NestHistoryForm>(browser, "historyView");
|
||||
Assert.NotNull(first);
|
||||
Assert.Same(tabs.SelectedTab, first.Parent);
|
||||
Assert.True(first.Visible);
|
||||
Assert.Equal(repository.Id, ((NestRecord)Field<DataGridView>(first, "revisions").Rows[0].Tag!).Id);
|
||||
Assert.False(Field<Button>(first, "restore").Enabled);
|
||||
|
||||
var nests = Field<DataGridView>(browser, "nestGrid");
|
||||
nests.CurrentCell = nests.Rows[1].Cells[0];
|
||||
Application.DoEvents();
|
||||
var second = Field<NestHistoryForm>(browser, "historyView");
|
||||
Assert.True(first.IsDisposed);
|
||||
Assert.NotSame(first, second);
|
||||
Assert.Equal(repository.SecondId, ((NestRecord)Field<DataGridView>(second, "revisions").Rows[0].Tag!).Id);
|
||||
Assert.Equal(0, repository.Writes);
|
||||
browser.Close();
|
||||
}, TimeSpan.FromSeconds(30), "Embedded history did not follow the selected nest.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Document_BindsOpenedRevisionWithoutLegacyFallback()
|
||||
{
|
||||
@@ -67,16 +100,17 @@ public class NestHistoryFormTests
|
||||
private sealed class Repository : INestRepository
|
||||
{
|
||||
public Guid Id = Guid.NewGuid();
|
||||
public Guid SecondId = Guid.NewGuid();
|
||||
public bool Deleted;
|
||||
public int Writes;
|
||||
private NestRecord Record(int revision) => new() { Id = Id, Revision = revision, Deleted = Deleted, Name = "Synthetic" };
|
||||
public Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken ct = default) => Task.FromResult<NestRecord?>(Record(5));
|
||||
private NestRecord Record(int revision, Guid? id = null) => new() { Id = id ?? Id, Revision = revision, Deleted = Deleted, Name = "Synthetic" };
|
||||
public Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken ct = default) => Task.FromResult<NestRecord?>(Record(5, id));
|
||||
public Task<NestRecord?> GetDeletedMetadataAsync(Guid id, CancellationToken ct = default) => GetMetadataAsync(id, ct);
|
||||
public Task<NestPage> QueryDeletedAsync(NestQuery query, CancellationToken ct = default) => Task.FromResult(new NestPage { Items = [Record(5)], Total = 1, Limit = query.Limit });
|
||||
public Task<NestRevisionPage> GetRevisionsAsync(Guid id, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) => Task.FromResult(new NestRevisionPage { Items = [Record(5), Record(2)], Total = 2, Limit = limit });
|
||||
public Task<NestRevisionPage> GetRevisionsAsync(Guid id, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) => Task.FromResult(new NestRevisionPage { Items = [Record(5, id), Record(2, id)], Total = 2, Limit = limit });
|
||||
public Task<NestRevisionPage> GetDeletedRevisionsAsync(Guid id, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) => GetRevisionsAsync(id, offset, limit, cancellationToken);
|
||||
public Task<IReadOnlyList<NestRecord>> ListAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<NestPage> QueryAsync(NestQuery query, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<NestPage> QueryAsync(NestQuery query, CancellationToken ct = default) => Task.FromResult(new NestPage { Items = [Record(5), Record(5, SecondId)], Total = 2, Limit = query.Limit });
|
||||
public Task<byte[]?> GetFileAsync(Guid id, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<NestRecord> UploadAsync(byte[] file, NestRecord record, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] file, NestRecord record, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
|
||||
@@ -35,8 +35,9 @@ public sealed class NestHistoryForm : Form
|
||||
private bool populating;
|
||||
private bool busy;
|
||||
public bool Changed { get; private set; }
|
||||
public event EventHandler HistoryChanged;
|
||||
|
||||
public NestHistoryForm(INestRepository repository, string serverUrl, Guid id = default)
|
||||
public NestHistoryForm(INestRepository repository, string serverUrl, Guid id = default, bool embedded = false)
|
||||
{
|
||||
this.repository = repository;
|
||||
this.serverUrl = serverUrl;
|
||||
@@ -48,6 +49,14 @@ public sealed class NestHistoryForm : Form
|
||||
Size = new Size(1050, 650);
|
||||
MinimumSize = new Size(800, 480);
|
||||
ShowInTaskbar = false;
|
||||
if (embedded)
|
||||
{
|
||||
TopLevel = false;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
Dock = DockStyle.Fill;
|
||||
MinimumSize = Size.Empty;
|
||||
CancelButton = null;
|
||||
}
|
||||
records.Columns.Add("Name", "Deleted nest");
|
||||
records.Columns.Add("Version", "Current version");
|
||||
records.Columns.Add("Saved", "Saved");
|
||||
@@ -77,9 +86,10 @@ public sealed class NestHistoryForm : Form
|
||||
var find = new Button { Text = "Find", AutoSize = true };
|
||||
find.Click += async (_, _) => { deletedOffset = 0; await ReadDeletedAsync(); };
|
||||
var close = new Button { Text = "Close", AutoSize = true, DialogResult = DialogResult.Cancel };
|
||||
CancelButton = close;
|
||||
if (!embedded) CancelButton = close;
|
||||
var bar = new FlowLayoutPanel { Dock = DockStyle.Bottom, AutoSize = true };
|
||||
bar.Controls.AddRange(new Control[] { refresh, previous, next, inspect, download, restore, recover, close });
|
||||
bar.Controls.AddRange(new Control[] { refresh, previous, next, inspect, download, restore, recover });
|
||||
if (!embedded) bar.Controls.Add(close);
|
||||
recover.Visible = recovery;
|
||||
restore.Visible = !recovery;
|
||||
var heading = new Label
|
||||
@@ -253,6 +263,7 @@ public sealed class NestHistoryForm : Form
|
||||
if (recoveryAction) await session.RecoverAsync(serverUrl, lifetime.Token);
|
||||
else await session.RestoreAsync(revision, serverUrl, lifetime.Token);
|
||||
Changed = true;
|
||||
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||
busy = false;
|
||||
if (!IsDisposed) { if (recoveryAction) await ReadDeletedAsync(); else await ReadAsync(0); }
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ namespace OpenNest.Forms;
|
||||
/// <summary>
|
||||
/// File > Open (Database mode). The upper list browses saved nests on the shared nest
|
||||
/// server one bounded page at a time; filtering, sorting and paging run on the server
|
||||
/// through <see cref="NestBrowseSession"/>. The lower tabs show the plates and drawings of
|
||||
/// the highlighted nest, read from its archive through <see cref="NestDetailsSession"/>,
|
||||
/// beside a preview of one of its plates. This form only renders their state.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class SavedNestsForm : Form
|
||||
{
|
||||
@@ -45,6 +45,9 @@ public sealed class SavedNestsForm : Form
|
||||
private readonly DataGridView platesGrid;
|
||||
private readonly DataGridView drawingsGrid;
|
||||
private readonly TabControl detailsTabs;
|
||||
private readonly TabPage versionsTab;
|
||||
private NestHistoryForm historyView;
|
||||
private Guid? historyId;
|
||||
private readonly SplitContainer detailsSplit;
|
||||
private readonly PlateView platePreview;
|
||||
private readonly Button previousPlateButton;
|
||||
@@ -141,7 +144,6 @@ public sealed class SavedNestsForm : Form
|
||||
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 });
|
||||
menu.Items.Add(new ToolStripMenuItem("Version history...", null, async (_, _) => await ShowHistoryAsync(false)));
|
||||
nestGrid.ContextMenuStrip = menu;
|
||||
|
||||
// Details of the highlighted nest.
|
||||
@@ -173,6 +175,9 @@ public sealed class SavedNestsForm : Form
|
||||
detailsTabs = new TabControl { Dock = DockStyle.Fill };
|
||||
detailsTabs.TabPages.Add(CreateTab("Plates", platesGrid));
|
||||
detailsTabs.TabPages.Add(CreateTab("Drawings", drawingsGrid));
|
||||
versionsTab = new TabPage("Versions");
|
||||
detailsTabs.TabPages.Add(versionsTab);
|
||||
detailsTabs.SelectedIndexChanged += (_, _) => UpdateHistoryView();
|
||||
|
||||
// Preview of one plate beside the tables; the buttons below it step through the plates.
|
||||
platePreview = new PlateView
|
||||
@@ -274,11 +279,8 @@ public sealed class SavedNestsForm : Form
|
||||
findBar.Controls.Add(new Label { Text = "Find:", AutoSize = true, Margin = new Padding(3, 8, 0, 0) });
|
||||
findBar.Controls.Add(searchBox);
|
||||
findBar.Controls.Add(refreshButton);
|
||||
var historyButton = new Button { Text = "Version history...", AutoSize = true };
|
||||
historyButton.Click += async (_, _) => await ShowHistoryAsync(false);
|
||||
var recoveryButton = new Button { Text = "Deleted nests...", AutoSize = true };
|
||||
recoveryButton.Click += async (_, _) => await ShowHistoryAsync(true);
|
||||
findBar.Controls.Add(historyButton);
|
||||
recoveryButton.Click += async (_, _) => await ShowDeletedAsync();
|
||||
findBar.Controls.Add(recoveryButton);
|
||||
findBar.Controls.Add(previousButton);
|
||||
findBar.Controls.Add(nextButton);
|
||||
@@ -328,7 +330,7 @@ public sealed class SavedNestsForm : Form
|
||||
_ = RunAsync(() => session.SetSearchAsync(searchBox.Text));
|
||||
nestGrid.Focus();
|
||||
return true;
|
||||
case Keys.Enter:
|
||||
case Keys.Enter when nestGrid.Focused:
|
||||
OpenSelected();
|
||||
return true;
|
||||
case Keys.Escape:
|
||||
@@ -579,6 +581,13 @@ public sealed class SavedNestsForm : Form
|
||||
{
|
||||
detailsTimer.Stop();
|
||||
var id = SelectedNestId();
|
||||
if (historyId != id)
|
||||
{
|
||||
historyView?.Dispose();
|
||||
historyView = null;
|
||||
historyId = null;
|
||||
}
|
||||
UpdateHistoryView();
|
||||
if (id == null)
|
||||
{
|
||||
details.Clear();
|
||||
@@ -756,10 +765,32 @@ public sealed class SavedNestsForm : Form
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
private async Task ShowHistoryAsync(bool recovery)
|
||||
private void UpdateHistoryView()
|
||||
{
|
||||
if (!recovery && SelectedNestId() == null) return;
|
||||
using var dialog = new NestHistoryForm(repository, serverUrl, recovery ? Guid.Empty : SelectedNestId().Value);
|
||||
if (detailsTabs.SelectedTab != versionsTab || SelectedNestId() is not Guid id || historyId == id)
|
||||
return;
|
||||
historyView?.Dispose();
|
||||
historyView = new NestHistoryForm(repository, serverUrl, id, embedded: true);
|
||||
historyId = id;
|
||||
historyView.HistoryChanged += (_, _) =>
|
||||
{
|
||||
// Let the history operation finish before refreshing the browse selection.
|
||||
BeginInvoke(new Action(async () =>
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
details.Dispose();
|
||||
details = new NestDetailsSession(LoadDetailsAsync);
|
||||
ShowDetailsMessage("");
|
||||
await RunAsync(session.RefreshAsync);
|
||||
}));
|
||||
};
|
||||
versionsTab.Controls.Add(historyView);
|
||||
historyView.Show();
|
||||
}
|
||||
|
||||
private async Task ShowDeletedAsync()
|
||||
{
|
||||
using var dialog = new NestHistoryForm(repository, serverUrl);
|
||||
dialog.ShowDialog(this);
|
||||
if (dialog.Changed && !IsDisposed)
|
||||
{
|
||||
|
||||
+10
-8
@@ -14,18 +14,20 @@ The desktop app can save nests two ways:
|
||||
user when the browser closes, including across app restarts. Until a selection
|
||||
is saved, nests appear newest-saved first.
|
||||
Below the list, the **Details** tabs show the highlighted nest's plates
|
||||
(duplicates, size, parts, drawings, utilization) and drawings (required, nested,
|
||||
remaining, area); they are read from that nest's archive, which is downloaded
|
||||
shortly after the highlight stops moving. Beside the tabs, a preview draws one
|
||||
(duplicates, size, parts, drawings, utilization), drawings (required, nested,
|
||||
remaining, area), and **Versions** (bounded current and earlier snapshots).
|
||||
Plates and drawings come from the archive downloaded shortly after the
|
||||
highlight stops moving; Versions loads on selection and follows the selected
|
||||
nest without opening another window. Beside the tabs, a preview draws one
|
||||
plate of the highlighted nest; the arrow buttons below it step through the
|
||||
plates, and selecting a row on the Plates tab shows that plate. Enter or a
|
||||
double-click opens the highlighted nest and Esc closes. Save creates a record on
|
||||
plates, and selecting a row on the Plates tab shows that plate. Enter on the
|
||||
nest list or a double-click opens the highlighted nest; Esc closes. Save creates a record on
|
||||
first save and updates that same record afterward. Save As creates a new record
|
||||
(a copy). A failed save keeps the previous record association; switching server
|
||||
URLs creates a record on the new server rather than updating an id from the
|
||||
previous one. **Version history...** on a selected row lists bounded current and
|
||||
earlier snapshots; **Open read-only...** previews one without binding an editable
|
||||
document, and **Download selected version...** saves its exact archived bytes.
|
||||
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.
|
||||
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