feat: implement board tab with dialogs, virtualized card lists and refresh-after-write
- board: columns in id order, per-column sort via config, column width presets (240/320/480) and custom 160-720 - column header new-card and edit/delete menu; card menu delete and move-to-column - card editor as one unit of work with dirty check and discard confirmation; nested tag picker writes new tags immediately - schema constraint violations mapped to typed exceptions and shown inside the modal - dialog service with owner stack so nested modals center on their parent and get FlowDirection - repository: filtered GetCards, CountCards; view-model tests
This commit is contained in:
1 parent
84d08c6768
commit
e3f58ce150
41 files changed
+3004
-26
No files matched your search
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using YKanBan.Models;
|
||||
using YKanBan.Search;
|
||||
using YKanBan.Storage;
|
||||
using YKanBan.Storage.Workspace;
|
||||
using YKanBan.Tests.TestUtilities;
|
||||
|
||||
@@ -253,8 +255,87 @@ public class WorkspaceRepositoryTests
|
||||
public void AddTagWithDuplicateNameIsRejected()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
Assert.ThrowsExactly<SqliteException>(() =>
|
||||
fixture.Repository.AddTag("bug", RgbColor.Parse("#123456"), ""));
|
||||
DuplicateNameException exception = Assert.ThrowsExactly<DuplicateNameException>(() =>
|
||||
fixture.Repository.AddTag("BUG", RgbColor.Parse("#123456"), ""));
|
||||
Assert.AreEqual(NamedEntity.Tag, exception.Entity);
|
||||
Assert.AreEqual("BUG", exception.Name);
|
||||
Assert.AreEqual(2, fixture.Repository.GetTags().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AddTagWithEmptyNameIsRejected()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
NameRequiredException exception = Assert.ThrowsExactly<NameRequiredException>(() =>
|
||||
fixture.Repository.AddTag("", RgbColor.Parse("#123456"), ""));
|
||||
Assert.AreEqual(NamedEntity.Tag, exception.Entity);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateTagToExistingNameIsRejectedAndKeepsTheRow()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
Assert.ThrowsExactly<DuplicateNameException>(() =>
|
||||
fixture.Repository.UpdateTag(fixture.UiTagId, "Bug", RgbColor.Parse("#00ff00"), ""));
|
||||
Assert.AreEqual("ui", fixture.Repository.GetTags().Single(tag => tag.Id == fixture.UiTagId).Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AddColumnWithDuplicateTitleIsRejectedCaseInsensitively()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
DuplicateNameException exception = Assert.ThrowsExactly<DuplicateNameException>(() =>
|
||||
fixture.Repository.AddColumn("COL", ""));
|
||||
Assert.AreEqual(NamedEntity.Column, exception.Entity);
|
||||
Assert.AreEqual(1, fixture.Repository.GetColumns().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AddColumnWithEmptyTitleIsRejected()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
NameRequiredException exception = Assert.ThrowsExactly<NameRequiredException>(() =>
|
||||
fixture.Repository.AddColumn("", "description"));
|
||||
Assert.AreEqual(NamedEntity.Column, exception.Entity);
|
||||
Assert.AreEqual(1, fixture.Repository.GetColumns().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateColumnConstraintViolationsAreTyped()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
ColumnModel other = fixture.Repository.AddColumn("Other", "");
|
||||
|
||||
Assert.ThrowsExactly<DuplicateNameException>(() => fixture.Repository.UpdateColumn(other.Id, "col", ""));
|
||||
Assert.ThrowsExactly<NameRequiredException>(() => fixture.Repository.UpdateColumn(other.Id, "", ""));
|
||||
Assert.AreEqual("Other", fixture.Repository.GetColumn(other.Id)!.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetCardsWithFilterReturnsMatchesWithAllOfTheirTags()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
fixture.Repository.AddCard(fixture.ColumnId, "Other", "", []);
|
||||
|
||||
// Filtering by one tag must still load both tags of the matching card.
|
||||
CompiledSearch filter = SearchSqlCompiler.Compile(SearchQueryParser.Parse("tag:ui")!);
|
||||
IReadOnlyList<CardModel> cards = fixture.Repository.GetCards(filter);
|
||||
|
||||
Assert.AreEqual(1, cards.Count);
|
||||
Assert.AreEqual(fixture.CardId, cards[0].Id);
|
||||
CollectionAssert.AreEqual(new[] { "bug", "ui" }, cards[0].Tags.Select(tag => tag.Name).ToArray());
|
||||
Assert.AreEqual(2, fixture.Repository.GetCards(null).Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CountCardsCountsPerColumn()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
ColumnModel empty = fixture.Repository.AddColumn("Empty", "");
|
||||
fixture.Repository.AddCard(fixture.ColumnId, "", "", []);
|
||||
|
||||
Assert.AreEqual(2L, fixture.Repository.CountCards(fixture.ColumnId));
|
||||
Assert.AreEqual(0L, fixture.Repository.CountCards(empty.Id));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
Reference in new issue
Block a user