improve Frontend

This commit is contained in:
Mika Kuns
2026-04-22 17:09:00 +02:00
parent 7de5510735
commit a4e313dbad
12 changed files with 437 additions and 12 deletions

View File

@@ -51,6 +51,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
builder.Property(t => t.Notes).HasColumnName("notes");
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
builder.HasOne(t => t.List)
.WithMany(l => l.Tasks)
@@ -74,5 +75,6 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
builder.HasIndex(t => t.ListId).HasDatabaseName("idx_tasks_list_id");
builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status");
builder.HasIndex(t => new { t.ListId, t.SortOrder }).HasDatabaseName("idx_tasks_list_sort");
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddTaskSortOrder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "sort_order",
table: "tasks",
type: "INTEGER",
nullable: false,
defaultValue: 0);
// Backfill existing rows with a per-list dense order (0..N-1) by creation time
// so today's UI order is preserved after the migration.
migrationBuilder.Sql("""
WITH ordered AS (
SELECT id, (row_number() OVER (PARTITION BY list_id ORDER BY created_at) - 1) AS rn
FROM tasks
)
UPDATE tasks SET sort_order = (SELECT rn FROM ordered WHERE ordered.id = tasks.id);
""");
migrationBuilder.CreateIndex(
name: "idx_tasks_list_sort",
table: "tasks",
columns: new[] { "list_id", "sort_order" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "idx_tasks_list_sort",
table: "tasks");
migrationBuilder.DropColumn(
name: "sort_order",
table: "tasks");
}
}
}

View File

@@ -281,6 +281,12 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
@@ -307,6 +313,9 @@ namespace ClaudeDo.Data.Migrations
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});

View File

@@ -29,6 +29,7 @@ public sealed class TaskEntity
public bool IsStarred { get; set; }
public bool IsMyDay { get; set; }
public string? Notes { get; set; }
public int SortOrder { get; set; }
// Navigation properties
public ListEntity List { get; set; } = null!;

View File

@@ -14,6 +14,13 @@ public sealed class TaskRepository
public async Task AddAsync(TaskEntity entity, CancellationToken ct = default)
{
// Append at bottom of the list by default: SortOrder = max(listId) + 1.
var maxSort = await _context.Tasks
.Where(t => t.ListId == entity.ListId)
.Select(t => (int?)t.SortOrder)
.MaxAsync(ct);
entity.SortOrder = (maxSort ?? -1) + 1;
_context.Tasks.Add(entity);
await _context.SaveChangesAsync(ct);
}
@@ -38,10 +45,32 @@ public sealed class TaskRepository
{
return await _context.Tasks
.Where(t => t.ListId == listId)
.OrderBy(t => t.CreatedAt)
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
.ToListAsync(ct);
}
/// <summary>
/// Renumbers tasks in a list to 0..N-1 according to <paramref name="orderedTaskIds"/>.
/// Ids not belonging to the list are ignored; ids missing from the list are untouched.
/// </summary>
public async Task ReorderAsync(string listId, IReadOnlyList<string> orderedTaskIds, CancellationToken ct = default)
{
if (orderedTaskIds.Count == 0) return;
var idSet = orderedTaskIds.ToHashSet();
var tasks = await _context.Tasks
.Where(t => t.ListId == listId && idSet.Contains(t.Id))
.ToListAsync(ct);
for (int i = 0; i < orderedTaskIds.Count; i++)
{
var task = tasks.FirstOrDefault(t => t.Id == orderedTaskIds[i]);
if (task is not null) task.SortOrder = i;
}
await _context.SaveChangesAsync(ct);
}
// Kept for backwards-compatibility with callers using the old name.
public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
=> GetByListIdAsync(listId, ct);
@@ -205,7 +234,7 @@ public sealed class TaskRepository
WHERE lt.list_id = t.list_id AND tg.name = 'agent'
)
)
ORDER BY t.created_at ASC
ORDER BY t.sort_order ASC, t.created_at ASC
LIMIT 1
)
RETURNING *