improve Frontend
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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 *
|
||||
|
||||
Reference in New Issue
Block a user