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.
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
/**
|
|
* @file laneOrderStorage.ts
|
|
* @description Persists the Workspace lane strip's user-chosen card order to
|
|
* localStorage, the same read/validate/fallback convention as
|
|
* splitViewStorage.ts.
|
|
*
|
|
* The order lives in the browser rather than the lanes table on purpose: it is
|
|
* a per-viewer preference, like the split-pane layout beside it, and nothing
|
|
* server-side (the CLI's `agenthub lanes`, the pipeline-stats picker) has an
|
|
* opinion about it. Ceiling: the order does not follow the user to another
|
|
* browser or to the CLI — move it to a `lanes.sort_order` column if it ever
|
|
* must.
|
|
*
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const STORAGE_KEY = "agenthub.workspace.laneOrder";
|
|
|
|
export function readLaneOrder(): number[] {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return [];
|
|
const parsed: unknown = JSON.parse(raw);
|
|
if (!Array.isArray(parsed)) return [];
|
|
return parsed.filter((id): id is number => typeof id === "number");
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function writeLaneOrder(ids: number[]): void {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
|
|
} catch {
|
|
/* ignore quota / disabled storage */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies a stored id order to a freshly fetched lane list.
|
|
*
|
|
* Ids the stored order never saw — a lane created since, or created in another
|
|
* tab — keep their server order (id ascending) and land at the END, so a new
|
|
* lane appears predictably rather than wherever a stale index puts it. Stored
|
|
* ids naming a lane that no longer exists are simply skipped.
|
|
*
|
|
* @param lanes Lanes in server order.
|
|
* @param order Stored id order.
|
|
* @returns A new array; the input is never mutated.
|
|
*/
|
|
export function sortByStoredOrder<T extends { id: number }>(lanes: T[], order: number[]): T[] {
|
|
const rank = new Map(order.map((id, i) => [id, i]));
|
|
const known = lanes
|
|
.filter((l) => rank.has(l.id))
|
|
.sort((a, b) => rank.get(a.id)! - rank.get(b.id)!);
|
|
const fresh = lanes.filter((l) => !rank.has(l.id));
|
|
return [...known, ...fresh];
|
|
}
|
|
|
|
/**
|
|
* Moves the lane at `from` to index `to`, returning the resulting id order.
|
|
* Out-of-range indices yield the unchanged order rather than a hole.
|
|
*/
|
|
export function moveLane(ids: number[], from: number, to: number): number[] {
|
|
if (from === to || from < 0 || to < 0 || from >= ids.length || to >= ids.length) return ids;
|
|
const next = [...ids];
|
|
next.splice(to, 0, ...next.splice(from, 1));
|
|
return next;
|
|
}
|