Public: anyone can read this; the crew can change it.

A card moved under a filter lands where the hidden cards are: a visible index passed as an absolute one #63

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/tempo:scout/33-a-card-moved-under-a-filter-lands-where → main 1 commit fork

  • Merged. scout/33-a-card-moved-under-a-filter-lands-where was merged into main as ec382a9.

Review

Sign in to review this change.
2 files changed +78 −19
src/keys.ts +36 −12
@@ -85,6 +85,20 @@ export function visibleCards(board: BoardStore, state: State, columnId: string):
8585 .filter((c) => q === "" || c.title.toLowerCase().includes(q));
8686 }
8787
88 /**
89 * Where `gap` — a position among a column's *visible* cards, 0 meaning
90 * before all of them — lands among every card in that column, the index
91 * `db.moveCard` expects. `all` and `visible` both exclude the card being
92 * moved, `visible` being `all` filtered the same way `visibleCards` does,
93 * so a hidden card between two visible ones never becomes the anchor.
94 */
95 function absoluteIndex(all: Card[], visible: Card[], gap: number): number {
96 const clamped = Math.max(0, Math.min(gap, visible.length));
97 if (clamped === 0) return visible.length === 0 ? 0 : all.findIndex((c) => c.id === visible[0].id);
98 const before = visible[clamped - 1];
99 return all.findIndex((c) => c.id === before.id) + 1;
100 }
101
88102 export function handleKey(state: State, key: string, board: BoardStore): Outcome {
89103 if (state.prompt) return handlePromptKey(state, key, board);
90104 if (state.mode === "boards") return handleBoardsKey(state, key, board);
@@ -196,24 +210,34 @@ function handleBoardKey(state: State, key: string, board: BoardStore): Outcome {
196210 next.prompt = { kind: "editCard", label: "Edit card", value: card.title, cardId: card.id };
197211 } else if (key === "d" && card) {
198212 board.deleteCard(state.boardId!, card.id);
199 } else if (key === "K" && card) {
200 board.moveCard(state.boardId!, card.id, column.id, Math.max(0, state.cardCursor - 1));
201 next.cardCursor = Math.max(0, state.cardCursor - 1);
202 } else if (key === "J" && card) {
203 board.moveCard(state.boardId!, card.id, column.id, state.cardCursor + 1);
204 next.cardCursor = Math.min(cards.length - 1, state.cardCursor + 1);
213 } else if (key === "K" && card && state.cardCursor > 0) {
214 const all = board.listCards(state.boardId!).filter((c) => c.column_id === column.id && c.id !== card.id);
215 const visible = cards.filter((c) => c.id !== card.id);
216 const gap = state.cardCursor - 1;
217 board.moveCard(state.boardId!, card.id, column.id, absoluteIndex(all, visible, gap));
218 next.cardCursor = gap;
219 } else if (key === "J" && card && state.cardCursor < cards.length - 1) {
220 const all = board.listCards(state.boardId!).filter((c) => c.column_id === column.id && c.id !== card.id);
221 const visible = cards.filter((c) => c.id !== card.id);
222 const gap = state.cardCursor + 1;
223 board.moveCard(state.boardId!, card.id, column.id, absoluteIndex(all, visible, gap));
224 next.cardCursor = gap;
205225 } else if (key === "H" && card && state.columnCursor > 0) {
206226 const target = columns[state.columnCursor - 1];
207 const index = Math.min(state.cardCursor, visibleCards(board, state, target.id).length);
208 board.moveCard(state.boardId!, card.id, target.id, index);
227 const all = board.listCards(state.boardId!).filter((c) => c.column_id === target.id);
228 const visible = visibleCards(board, state, target.id);
229 const gap = Math.min(state.cardCursor, visible.length);
230 board.moveCard(state.boardId!, card.id, target.id, absoluteIndex(all, visible, gap));
209231 next.columnCursor = state.columnCursor - 1;
210 next.cardCursor = index;
232 next.cardCursor = gap;
211233 } else if (key === "L" && card && state.columnCursor < columns.length - 1) {
212234 const target = columns[state.columnCursor + 1];
213 const index = Math.min(state.cardCursor, visibleCards(board, state, target.id).length);
214 board.moveCard(state.boardId!, card.id, target.id, index);
235 const all = board.listCards(state.boardId!).filter((c) => c.column_id === target.id);
236 const visible = visibleCards(board, state, target.id);
237 const gap = Math.min(state.cardCursor, visible.length);
238 board.moveCard(state.boardId!, card.id, target.id, absoluteIndex(all, visible, gap));
215239 next.columnCursor = state.columnCursor + 1;
216 next.cardCursor = index;
240 next.cardCursor = gap;
217241 } else if (key === "c") {
218242 next.prompt = { kind: "newColumn", label: "New column", value: "" };
219243 } else if (key === "r" && column) {
src/tty.test.ts +42 −7
@@ -327,12 +327,7 @@ test("f sets the filter and the card cursor stays inside the filtered list", ()
327327 expect(visibleCards(db, cleared, todo.id).map((c) => c.title)).toEqual(["alpha", "beta", "gamma"]);
328328 });
329329
330 test("J with a filter on counts the index among all the cards, not the visible ones", () => {
331 // Today's behaviour, recorded rather than fixed, and a bug: the cursor
332 // index comes from the filtered list but db.moveCard reads it as an index
333 // among every card in the column, so a hidden card absorbs the move and
334 // the visible order does not change. #33 fixes it and rewrites this test;
335 // until then this is a marker, not a contract.
330 test("J and K move a card past its visible neighbour, not the hidden ones in between", () => {
336331 const { state, boardId } = openBoard();
337332 const [todo] = db.listColumns(boardId);
338333 for (const title of ["x-one", "hidden", "x-two"]) db.createCard(boardId, todo.id, title);
@@ -340,10 +335,50 @@ test("J with a filter on counts the index among all the cards, not the visible o
340335 const filtered = { ...state, filter: "x-" };
341336 expect(visibleCards(db, filtered, todo.id).map((c) => c.title)).toEqual(["x-one", "x-two"]);
342337
338 // J on x-one lands it after the card visibly below it (x-two), and the
339 // hidden card in between does not move.
343340 const moved = press(filtered, "J");
344341 expect(moved.cardCursor).toBe(1);
342 expect(titlesIn(boardId, 0)).toEqual(["hidden", "x-two", "x-one"]);
343 expect(visibleCards(db, moved, todo.id).map((c) => c.title)).toEqual(["x-two", "x-one"]);
344
345 // K undoes it: x-one lands back before x-two, hidden still untouched.
346 const back = press(moved, "K");
347 expect(back.cardCursor).toBe(0);
345348 expect(titlesIn(boardId, 0)).toEqual(["hidden", "x-one", "x-two"]);
346 expect(visibleCards(db, moved, todo.id).map((c) => c.title)).toEqual(["x-one", "x-two"]);
349 expect(visibleCards(db, back, todo.id).map((c) => c.title)).toEqual(["x-one", "x-two"]);
350
351 // Clamped at the ends of the *visible* list: K at the top and J at the
352 // bottom leave the order, and the hidden card, alone.
353 expect(press(back, "K").cardCursor).toBe(0);
354 expect(titlesIn(boardId, 0)).toEqual(["hidden", "x-one", "x-two"]);
355 const atBottom = { ...back, cardCursor: 1 };
356 expect(press(atBottom, "J").cardCursor).toBe(1);
357 expect(titlesIn(boardId, 0)).toEqual(["hidden", "x-one", "x-two"]);
358 });
359
360 test("H and L land a card among the target's visible cards, not counting its hidden ones", () => {
361 const { state, boardId } = openBoard();
362 const [todo, doing] = db.listColumns(boardId);
363 for (const title of ["s-one", "s-two"]) db.createCard(boardId, todo.id, title);
364 for (const title of ["x-first", "hidden", "x-second"]) db.createCard(boardId, doing.id, title);
365
366 const filtered = { ...state, filter: "-", cardCursor: 1 };
367 expect(visibleCards(db, filtered, todo.id).map((c) => c.title)).toEqual(["s-one", "s-two"]);
368 expect(visibleCards(db, filtered, doing.id).map((c) => c.title)).toEqual(["x-first", "x-second"]);
369
370 // s-two, second in the visible source list, lands second in the visible
371 // target list too: after x-first, before x-second — the hidden card
372 // between them does not move and is not counted.
373 const right = press(filtered, "L");
374 expect(titlesIn(boardId, 1)).toEqual(["x-first", "s-two", "hidden", "x-second"]);
375 expect(visibleCards(db, right, doing.id).map((c) => c.title)).toEqual(["x-first", "s-two", "x-second"]);
376 expect([right.columnCursor, right.cardCursor]).toEqual([1, 1]);
377
378 const back = press(right, "H");
379 expect(titlesIn(boardId, 0)).toEqual(["s-one", "s-two"]);
380 expect(titlesIn(boardId, 1)).toEqual(["x-first", "hidden", "x-second"]);
381 expect([back.columnCursor, back.cardCursor]).toEqual([0, 1]);
347382 });
348383
349384 test("u undoes the last change", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs