5bd1c0323f
The lane strip's tiles are now drag-reorderable (Alt+←/→ is the keyboard
path), and a lane's name can be set in place from the detail header. The
"Lane 23 ·" prefix is gone from both — the bare `#23` stays on the tile,
because that number is how the CLI addresses a lane.
Card order is a per-viewer preference, not a lane fact: it lives in
localStorage (`agenthub.workspace.laneOrder`) beside the split-view layout
and writes nothing server-side, so `GET /api/lanes` and `agenthub lanes`
keep answering in id order. Renaming is the opposite — it is PATCH
/api/lanes/:id with {title}, the one lane column a user now types into
directly.
That made a patch value reachable by hand for the first time, so
`bindable` now rejects any non-scalar for every PATCHABLE column with
EBADFIELD, and the route answers 400 for that and for the
SQLITE_CONSTRAINT family, rather than handing better-sqlite3 a datatype
throw and reporting 500 on plain bad input.
The uncommitted-files tooltip became a real panel. `GET /api/lanes/:id/git`
now returns `files` as {path, status, added, deleted} records — deltas from
one `git diff --numstat HEAD`, null where git has none to report (untracked,
binary, renamed) — and the card renders them portalled to the body, so the
Workspace's own scroll box cannot clip the list.
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
/**
|
|
* @file laneOrderStorage.test.ts
|
|
* @description Tests for the Workspace lane strip's stored card order: the
|
|
* storage round-trip, and the two rules that keep a stale stored order from
|
|
* losing a lane — unknown ids land at the end, vanished ids are skipped.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { readLaneOrder, writeLaneOrder, sortByStoredOrder, moveLane } from "../laneOrderStorage";
|
|
|
|
const lanes = (...ids: number[]) => ids.map((id) => ({ id }));
|
|
|
|
describe("laneOrderStorage", () => {
|
|
beforeEach(() => localStorage.clear());
|
|
|
|
it("returns an empty order when nothing is stored", () => {
|
|
expect(readLaneOrder()).toEqual([]);
|
|
});
|
|
|
|
it("round-trips a written order", () => {
|
|
writeLaneOrder([3, 1, 2]);
|
|
expect(readLaneOrder()).toEqual([3, 1, 2]);
|
|
});
|
|
|
|
it("falls back to an empty order when stored JSON is malformed", () => {
|
|
localStorage.setItem("agenthub.workspace.laneOrder", "{not json");
|
|
expect(readLaneOrder()).toEqual([]);
|
|
});
|
|
|
|
it("drops non-numeric entries from a stored order", () => {
|
|
localStorage.setItem("agenthub.workspace.laneOrder", JSON.stringify([1, "2", null, 3]));
|
|
expect(readLaneOrder()).toEqual([1, 3]);
|
|
});
|
|
});
|
|
|
|
describe("sortByStoredOrder", () => {
|
|
it("keeps server order when nothing is stored", () => {
|
|
expect(sortByStoredOrder(lanes(1, 2, 3), [])).toEqual(lanes(1, 2, 3));
|
|
});
|
|
|
|
it("applies the stored order", () => {
|
|
expect(sortByStoredOrder(lanes(1, 2, 3), [3, 1, 2])).toEqual(lanes(3, 1, 2));
|
|
});
|
|
|
|
it("appends lanes the stored order has never seen, in server order", () => {
|
|
expect(sortByStoredOrder(lanes(1, 2, 3, 4), [3, 1])).toEqual(lanes(3, 1, 2, 4));
|
|
});
|
|
|
|
it("skips stored ids whose lane no longer exists", () => {
|
|
expect(sortByStoredOrder(lanes(1, 3), [3, 2, 1])).toEqual(lanes(3, 1));
|
|
});
|
|
|
|
it("does not mutate the input", () => {
|
|
const input = lanes(1, 2, 3);
|
|
sortByStoredOrder(input, [3, 2, 1]);
|
|
expect(input.map((l) => l.id)).toEqual([1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
describe("moveLane", () => {
|
|
it("moves a card forward", () => {
|
|
expect(moveLane([1, 2, 3], 0, 2)).toEqual([2, 3, 1]);
|
|
});
|
|
|
|
it("moves a card backward", () => {
|
|
expect(moveLane([1, 2, 3], 2, 0)).toEqual([3, 1, 2]);
|
|
});
|
|
|
|
it("returns the order unchanged for a no-op or out-of-range move", () => {
|
|
expect(moveLane([1, 2, 3], 1, 1)).toEqual([1, 2, 3]);
|
|
expect(moveLane([1, 2, 3], 0, 3)).toEqual([1, 2, 3]);
|
|
expect(moveLane([1, 2, 3], -1, 0)).toEqual([1, 2, 3]);
|
|
});
|
|
});
|