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

Undo says what it reversed: a label on the action row and on the Undo button #116

Closed opened this
Closed without merging

scout/tempo:scout/43-undo-says-what-it-reversed-a-label-on-th → main 1 commit fork

  • Closed without merging. Nothing was applied to main.

Review

Sign in to review this change.
4 files changed +50 −21
src/db.ts +29 −14
@@ -98,6 +98,11 @@ type Payload = {
9898 // Only the name — restoring the whole board row would also reverse a
9999 // claim, and ownership is not undoable.
100100 restoreBoardName?: { id: string; name: string };
101 // A short, human sentence describing the write that logged this row —
102 // built from data the write already has, never a re-read of the board.
103 // Missing on a row logged before labels existed; undo() falls back to
104 // "a change" for those.
105 label?: string;
101106 };
102107
103108 // Runs inside the same transaction as the write that logged the action, so
@@ -196,7 +201,7 @@ export function renameBoard(boardId: string, name: string): boolean {
196201 const before = getBoard(boardId);
197202 if (!before) return false;
198203 db.query("UPDATE board SET name = ?2 WHERE id = ?1").run(boardId, name);
199 logAction(boardId, { restoreBoardName: { id: boardId, name: before.name } });
204 logAction(boardId, { restoreBoardName: { id: boardId, name: before.name }, label: "renamed the board" });
200205 bumpRevision(boardId);
201206 bumpRevision(BOARDS_SCOPE);
202207 return true;
@@ -244,7 +249,7 @@ export function createColumn(boardId: string, name: string, explicitPosition?: n
244249 name,
245250 position,
246251 );
247 logAction(boardId, { deleteColumnIds: [id] });
252 logAction(boardId, { deleteColumnIds: [id], label: `added the column "${name}"` });
248253 bumpRevision(boardId);
249254 return { id, board_id: boardId, name, position };
250255 }).immediate();
@@ -261,7 +266,7 @@ export function renameColumn(boardId: string, columnId: string, name: string): b
261266 const before = getColumn(boardId, columnId);
262267 if (!before) return false;
263268 db.query("UPDATE board_column SET name = ?2 WHERE id = ?1").run(columnId, name);
264 logAction(boardId, { restoreColumns: [before] });
269 logAction(boardId, { restoreColumns: [before], label: `renamed the column "${name}"` });
265270 bumpRevision(boardId);
266271 return true;
267272 }).immediate();
@@ -277,7 +282,7 @@ export function moveColumn(boardId: string, columnId: string, direction: "left"
277282 const before = [columns[i], columns[j]];
278283 db.query("UPDATE board_column SET position = ?2 WHERE id = ?1").run(columns[i].id, columns[j].position);
279284 db.query("UPDATE board_column SET position = ?2 WHERE id = ?1").run(columns[j].id, columns[i].position);
280 logAction(boardId, { restoreColumns: before });
285 logAction(boardId, { restoreColumns: before, label: `moved the column "${columns[i].name}"` });
281286 bumpRevision(boardId);
282287 return true;
283288 }).immediate();
@@ -289,7 +294,7 @@ export function deleteColumn(boardId: string, columnId: string): boolean {
289294 if (!before) return false;
290295 const cards = listCards(boardId).filter((c) => c.column_id === columnId);
291296 db.query("DELETE FROM board_column WHERE id = ?1").run(columnId);
292 logAction(boardId, { restoreColumns: [before], restoreCards: cards });
297 logAction(boardId, { restoreColumns: [before], restoreCards: cards, label: `deleted the column "${before.name}"` });
293298 bumpRevision(boardId);
294299 return true;
295300 }).immediate();
@@ -309,7 +314,7 @@ export function createCard(boardId: string, columnId: string, title: string): Ca
309314 title,
310315 position,
311316 );
312 logAction(boardId, { deleteCardIds: [id] });
317 logAction(boardId, { deleteCardIds: [id], label: `added the card "${title}"` });
313318 bumpRevision(boardId);
314319 return getCard(boardId, id)!;
315320 }).immediate();
@@ -329,7 +334,7 @@ export function editCard(boardId: string, cardId: string, title: string): boolea
329334 const before = getCard(boardId, cardId);
330335 if (!before) return false;
331336 db.query("UPDATE card SET title = ?2, updated_at = datetime('now') WHERE id = ?1").run(cardId, title);
332 logAction(boardId, { restoreCards: [before] });
337 logAction(boardId, { restoreCards: [before], label: `edited the card "${title}"` });
333338 bumpRevision(boardId);
334339 return true;
335340 }).immediate();
@@ -359,7 +364,7 @@ export function moveCard(boardId: string, cardId: string, columnId: string, inde
359364 columnId,
360365 position,
361366 );
362 logAction(boardId, { restoreCards: [before] });
367 logAction(boardId, { restoreCards: [before], label: `moved "${before.title}"` });
363368 bumpRevision(boardId);
364369 return true;
365370 }
@@ -377,7 +382,7 @@ export function moveCard(boardId: string, cardId: string, columnId: string, inde
377382 columnId,
378383 at + 1,
379384 );
380 logAction(boardId, { restoreCards });
385 logAction(boardId, { restoreCards, label: `moved "${before.title}"` });
381386 bumpRevision(boardId);
382387 return true;
383388 }).immediate();
@@ -388,18 +393,28 @@ export function deleteCard(boardId: string, cardId: string): boolean {
388393 const before = getCard(boardId, cardId);
389394 if (!before) return false;
390395 db.query("DELETE FROM card WHERE id = ?1").run(cardId);
391 logAction(boardId, { restoreCards: [before] });
396 logAction(boardId, { restoreCards: [before], label: `deleted the card "${before.title}"` });
392397 bumpRevision(boardId);
393398 return true;
394399 }).immediate();
395400 }
396401
397 export function undo(boardId: string): boolean {
398 return db.transaction((): boolean => {
402 /** The label of what Undo would reverse right now, without consuming it. */
403 export function peekUndo(boardId: string): string | null {
404 const row = db
405 .query<{ payload: string }, [string]>("SELECT payload FROM action WHERE board_id = ?1 ORDER BY id DESC LIMIT 1")
406 .get(boardId);
407 if (!row) return null;
408 return (JSON.parse(row.payload) as Payload).label ?? "a change";
409 }
410
411 /** Reverses the last logged change and reports its label, or null with nothing to undo. */
412 export function undo(boardId: string): string | null {
413 return db.transaction((): string | null => {
399414 const row = db
400415 .query<{ id: number; payload: string }, [string]>("SELECT id, payload FROM action WHERE board_id = ?1 ORDER BY id DESC LIMIT 1")
401416 .get(boardId);
402 if (!row) return false;
417 if (!row) return null;
403418 const payload = JSON.parse(row.payload) as Payload;
404419 const restoreColumn = db.query(
405420 "INSERT OR REPLACE INTO board_column (id, board_id, name, position) VALUES (?1, ?2, ?3, ?4)",
@@ -417,6 +432,6 @@ export function undo(boardId: string): boolean {
417432 }
418433 db.query("DELETE FROM action WHERE id = ?1").run(row.id);
419434 bumpRevision(boardId);
420 return true;
435 return payload.label ?? "a change";
421436 }).immediate();
422437 }
src/keys.ts +1 −1
@@ -30,7 +30,7 @@ export type BoardStore = {
3030 editCard(boardId: string, cardId: string, title: string): boolean;
3131 deleteCard(boardId: string, cardId: string): boolean;
3232 moveCard(boardId: string, cardId: string, columnId: string, index: number): boolean;
33 undo(boardId: string): boolean;
33 undo(boardId: string): string | null;
3434 boardRevision(boardId: string): number;
3535 boardsRevision(): number;
3636 };
src/pages/board.ts +8 −3
@@ -168,7 +168,7 @@ ${list}
168168 );
169169 }
170170
171 export type BoardData = { board: Board; columns: Column[]; cards: Card[]; query: string; who: Who; error?: string };
171 export type BoardData = { board: Board; columns: Column[]; cards: Card[]; query: string; who: Who; error?: string; undo?: string | null };
172172
173173 // A guest's board belongs to no one until someone signed in keeps it
174174 // (POST /b/:id/claim — see routes.ts and db.ts's claimBoard).
@@ -178,11 +178,16 @@ function keepHtml(board: Board, who: Who): string {
178178 return `<form class="keep" method="post" action="/b/${board.id}/claim"><button class="ghost">Keep this board</button></form>`;
179179 }
180180
181 export function boardPage({ board, columns, cards, query, who, error }: BoardData): string {
181 export function boardPage({ board, columns, cards, query, who, error, undo }: BoardData): string {
182182 const q = query.trim().toLowerCase();
183183 const matches = (c: Card) => q === "" || c.title.toLowerCase().includes(q);
184184 const cardsFor = (columnId: string) => cards.filter((c) => c.column_id === columnId);
185185
186 const undoButton =
187 undo == null
188 ? `<button class="ghost" disabled title="Nothing to undo" aria-label="Nothing to undo">Undo</button>`
189 : `<button class="ghost" title="Undo: ${escape(undo)}" aria-label="Undo: ${escape(undo)}">Undo</button>`;
190
186191 const columnOptions = (currentId: string) =>
187192 columns
188193 .map((c) => `<option value="${escape(c.id)}"${c.id === currentId ? " selected" : ""}>${escape(c.name)}</option>`)
@@ -260,7 +265,7 @@ export function boardPage({ board, columns, cards, query, who, error }: BoardDat
260265 <button>Filter</button>
261266 </form>
262267 <form method="post" action="/b/${board.id}/undo">
263 <button class="ghost">Undo</button>
268 ${undoButton}
264269 </form>
265270 </div>
266271 <div class="board">
src/routes.ts +12 −3
@@ -113,7 +113,15 @@ function boardBad(
113113 const board = db.getBoard(boardId);
114114 if (!board) return new Response(message, { status, headers: extraHeaders });
115115 return html(
116 boardPage({ board, columns: db.listColumns(boardId), cards: db.listCards(boardId), query: "", who, error: message }),
116 boardPage({
117 board,
118 columns: db.listColumns(boardId),
119 cards: db.listCards(boardId),
120 query: "",
121 who,
122 error: message,
123 undo: db.peekUndo(boardId),
124 }),
117125 status,
118126 extraHeaders,
119127 );
@@ -256,7 +264,7 @@ async function dispatch(request: Request, url: URL, env: string, who: Who, ip: s
256264 const columns = db.listColumns(boardId);
257265 const cards = db.listCards(boardId);
258266 const query = url.searchParams.get("q") ?? "";
259 return html(boardPage({ board, columns, cards, query: query.slice(0, 200), who }));
267 return html(boardPage({ board, columns, cards, query: query.slice(0, 200), who, undo: db.peekUndo(boardId) }));
260268 }
261269
262270 if (method === "GET" && parts.length === 3 && parts[2] === "events") {
@@ -285,7 +293,8 @@ async function dispatch(request: Request, url: URL, env: string, who: Who, ip: s
285293
286294 if (method === "POST" && parts.length === 3 && parts[2] === "undo") {
287295 if (!writeBucket.take(writeKey)) return writeRefused(request, who, boardId, writeKey);
288 return donePublish(request, who, boardId, db.undo(boardId), "undo");
296 const undid = db.undo(boardId);
297 return donePublish(request, who, boardId, undid !== null, "undo", {}, { undid });
289298 }
290299
291300 if (method === "POST" && parts.length === 3 && parts[2] === "columns") {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs