feat(HAI-100): add columnMovedAt timestamp and sort board tasks by move order
- Add columnMovedAt field to task types - Set columnMovedAt on task creation and column moves in store - Sort tasks by columnMovedAt descending in Board component - Add store tests for columnMovedAt behavior
This commit is contained in:
@@ -611,4 +611,42 @@ describe("TaskStore", () => {
|
||||
expect(logs[4].text).toBe("chunk 4");
|
||||
});
|
||||
});
|
||||
|
||||
describe("columnMovedAt", () => {
|
||||
it("createTask sets columnMovedAt", async () => {
|
||||
const before = new Date().toISOString();
|
||||
const task = await store.createTask({ description: "test columnMovedAt" });
|
||||
const after = new Date().toISOString();
|
||||
expect(task.columnMovedAt).toBeDefined();
|
||||
expect(task.columnMovedAt! >= before).toBe(true);
|
||||
expect(task.columnMovedAt! <= after).toBe(true);
|
||||
});
|
||||
|
||||
it("moveTask sets columnMovedAt to a recent ISO timestamp", async () => {
|
||||
const task = await store.createTask({ description: "move test", column: "triage" });
|
||||
const originalMovedAt = task.columnMovedAt;
|
||||
|
||||
// Small delay to ensure timestamp differs
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
const before = new Date().toISOString();
|
||||
const moved = await store.moveTask(task.id, "todo");
|
||||
const after = new Date().toISOString();
|
||||
|
||||
expect(moved.columnMovedAt).toBeDefined();
|
||||
expect(moved.columnMovedAt! >= before).toBe(true);
|
||||
expect(moved.columnMovedAt! <= after).toBe(true);
|
||||
expect(moved.columnMovedAt).not.toBe(originalMovedAt);
|
||||
});
|
||||
|
||||
it("updateTask does NOT change columnMovedAt", async () => {
|
||||
const task = await store.createTask({ description: "no change test" });
|
||||
const originalMovedAt = task.columnMovedAt;
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
const updated = await store.updateTask(task.id, { title: "new title" });
|
||||
expect(updated.columnMovedAt).toBe(originalMovedAt);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,6 +188,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [{ timestamp: now, action: "Task created" }],
|
||||
columnMovedAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@@ -263,7 +264,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
const fromColumn = task.column;
|
||||
task.column = toColumn;
|
||||
task.updatedAt = new Date().toISOString();
|
||||
task.columnMovedAt = new Date().toISOString();
|
||||
task.updatedAt = task.columnMovedAt;
|
||||
|
||||
// Clear transient fields when moving to done (matches moveToDone behavior)
|
||||
if (toColumn === "done") {
|
||||
@@ -605,7 +607,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.worktree = undefined;
|
||||
task.status = undefined;
|
||||
task.blockedBy = undefined;
|
||||
task.updatedAt = new Date().toISOString();
|
||||
task.columnMovedAt = new Date().toISOString();
|
||||
task.updatedAt = task.columnMovedAt;
|
||||
|
||||
await this.atomicWriteTaskJson(dir, task);
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@ export interface Task {
|
||||
log: TaskLogEntry[];
|
||||
size?: "S" | "M" | "L";
|
||||
reviewLevel?: number;
|
||||
/** ISO-8601 timestamp of when the task last entered its current column.
|
||||
* Used to sort cards within a column so that recently-moved cards appear at the top. */
|
||||
columnMovedAt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,18 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
|
||||
<Column
|
||||
key={col}
|
||||
column={col}
|
||||
tasks={tasks.filter((t) => t.column === col)}
|
||||
tasks={tasks
|
||||
.filter((t) => t.column === col)
|
||||
.sort((a, b) => {
|
||||
// Tasks with columnMovedAt sort descending (most recent first)
|
||||
// Tasks without it (legacy) fall to the bottom, sorted by createdAt ascending
|
||||
if (a.columnMovedAt && b.columnMovedAt) {
|
||||
return b.columnMovedAt.localeCompare(a.columnMovedAt);
|
||||
}
|
||||
if (a.columnMovedAt && !b.columnMovedAt) return -1;
|
||||
if (!a.columnMovedAt && b.columnMovedAt) return 1;
|
||||
return a.createdAt.localeCompare(b.createdAt);
|
||||
})}
|
||||
allTasks={tasks}
|
||||
maxConcurrent={maxConcurrent}
|
||||
onMoveTask={onMoveTask}
|
||||
|
||||
Reference in New Issue
Block a user