feat: drop created-at card sort and add card display ordering

This commit is contained in:
doyaGu committed 2026-10-03 02:52:28 -04:00
1 parent 27b507e439
commit bb4f96a9c5
4 files changed
+117 -5

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
using YKanBan.Models;
using YKanBan.Storage.AppData;
namespace YKanBan.Tests.Models;
/// <summary>
/// Tests for the in-column card display order: each sort option plus the id
/// ascending tie-breaker.
/// </summary>
[TestClass]
public class CardOrderingTests
{
private static CardModel Card(long id, string title = "", long updatedAt = 0) =>
new(id, 1, title, string.Empty, 0, updatedAt, []);
private static long[] Ids(IEnumerable<CardModel> cards, CardSortOption option) =>
CardOrdering.Sort(cards, option).Select(card => card.Id).ToArray();
[TestMethod]
public void IdSortsAscending()
{
CollectionAssert.AreEqual(new[] { 1L, 2L, 3L }, Ids([Card(3), Card(1), Card(2)], CardSortOption.Id));
}
[TestMethod]
public void UpdatedAtSortsMostRecentFirstThenById()
{
CardModel[] cards = [Card(1, updatedAt: 10), Card(2, updatedAt: 30), Card(3, updatedAt: 10), Card(4, updatedAt: 20)];
CollectionAssert.AreEqual(new[] { 2L, 4L, 1L, 3L }, Ids(cards, CardSortOption.UpdatedAt));
}
[TestMethod]
public void TitleSortsByCodePointWithEmptyFirstThenById()
{
CardModel[] cards = [Card(1, "b"), Card(2, "B"), Card(3, ""), Card(4, "a"), Card(5, "b")];
CollectionAssert.AreEqual(new[] { 3L, 2L, 4L, 1L, 5L }, Ids(cards, CardSortOption.Title));
}
[TestMethod]
public void TitleSortsSupplementaryCharactersAfterBmpPrivateUse()
{
// U+1F600 is a surrogate pair in UTF-16, so ordinal comparison would put it before U+E000.
CardModel[] cards = [Card(1, "\U0001F600"), Card(2, "")];
CollectionAssert.AreEqual(new[] { 2L, 1L }, Ids(cards, CardSortOption.Title));
}
}
@@ -145,7 +145,7 @@ public class AppConfigStoreTests
{ {
Language = "zh-Hans", Language = "zh-Hans",
Theme = ThemeOption.Light, Theme = ThemeOption.Light,
Sort = new SortSettings { Card = CardSortOption.CreatedAt }, Sort = new SortSettings { Card = CardSortOption.Title },
ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Wide }, ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Wide },
Confirmations = new ConfirmationSettings { DiscardEdit = false }, Confirmations = new ConfirmationSettings { DiscardEdit = false },
}; };
+70
View File
@@ -0,0 +1,70 @@
using System.Text;
using YKanBan.Storage.AppData;
namespace YKanBan.Models;
/// <summary>
/// Computes the in-column card display order for a <see cref="CardSortOption"/>.
/// Columns are not affected: they always stay in id order. Every option falls
/// back to id ascending as the final tie-breaker, so the order is total.
/// </summary>
public static class CardOrdering
{
/// <summary>
/// Orders cards for display.
/// </summary>
/// <param name="cards">The cards to order, typically those of one column.</param>
/// <param name="option">The selected sort option.</param>
/// <returns>The cards in display order.</returns>
public static IReadOnlyList<CardModel> Sort(IEnumerable<CardModel> cards, CardSortOption option)
{
IOrderedEnumerable<CardModel> ordered = option switch
{
CardSortOption.Id => cards.OrderBy(card => card.Id),
// Most recently modified first.
CardSortOption.UpdatedAt => cards.OrderByDescending(card => card.UpdatedAt).ThenBy(card => card.Id),
// Code point order, matching SQLite's BINARY collation; the empty title sorts first.
CardSortOption.Title => cards.OrderBy(card => card.Title, CodePointComparer.Instance).ThenBy(card => card.Id),
_ => throw new ArgumentOutOfRangeException(nameof(option), option, "Unknown card sort option."),
};
return ordered.ToList();
}
/// <summary>
/// Compares strings by Unicode code points. Unlike ordinal UTF-16 comparison
/// it orders supplementary characters after U+E000–U+FFFF, as BINARY does on UTF-8.
/// </summary>
private sealed class CodePointComparer : IComparer<string>
{
public static readonly CodePointComparer Instance = new();
public int Compare(string? x, string? y)
{
if (x is null || y is null)
{
return x is null ? (y is null ? 0 : -1) : 1;
}
StringRuneEnumerator left = x.EnumerateRunes();
StringRuneEnumerator right = y.EnumerateRunes();
while (true)
{
bool hasLeft = left.MoveNext();
bool hasRight = right.MoveNext();
if (!hasLeft || !hasRight)
{
return hasLeft.CompareTo(hasRight);
}
int result = left.Current.Value.CompareTo(right.Current.Value);
if (result != 0)
{
return result;
}
}
}
}
}
-4
View File
@@ -13,10 +13,6 @@ public enum CardSortOption
[EnumMember(Value = "id")] [EnumMember(Value = "id")]
Id, Id,
/// <summary>Creation time, oldest first.</summary>
[EnumMember(Value = "created-at")]
CreatedAt,
/// <summary>Last modification time, most recently modified first.</summary> /// <summary>Last modification time, most recently modified first.</summary>
[EnumMember(Value = "updated-at")] [EnumMember(Value = "updated-at")]
UpdatedAt, UpdatedAt,