Add toggle (check/uncheck) to checklist items + "Open in rTasks" button
- checkAC → toggleAC: clicking a checked item unchecks it - Tokens generated for all items (checked and unchecked) - Checked items now clickable with green checkmark links - Uncheck shows amber banner; check shows green banner - "Open in rTasks" button links to kanban board Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0728c9e516
commit
cf296a3bb3
|
|
@ -7,7 +7,7 @@
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { checklistConfig } from "./lib/checklist-config";
|
import { checklistConfig } from "./lib/checklist-config";
|
||||||
import { createToken, verifyToken } from "./lib/token";
|
import { createToken, verifyToken } from "./lib/token";
|
||||||
import { readTask, checkAC } from "./lib/backlog";
|
import { readTask, toggleAC } from "./lib/backlog";
|
||||||
import { renderEmailHTML, renderWebPage, renderError } from "./lib/render";
|
import { renderEmailHTML, renderWebPage, renderError } from "./lib/render";
|
||||||
|
|
||||||
export const checklistCheckRoutes = new Hono();
|
export const checklistCheckRoutes = new Hono();
|
||||||
|
|
@ -30,17 +30,14 @@ checklistCheckRoutes.get("/:token", async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await checkAC(verified.repo, verified.taskId, verified.acIndex);
|
const result = await toggleAC(verified.repo, verified.taskId, verified.acIndex);
|
||||||
const task = await readTask(verified.repo, verified.taskId);
|
|
||||||
|
|
||||||
const tokens = new Map<number, string>();
|
const tokens = new Map<number, string>();
|
||||||
for (const ac of task.criteria) {
|
for (const ac of result.criteria) {
|
||||||
if (!ac.checked) {
|
|
||||||
tokens.set(ac.index, await createToken(verified.repo, verified.taskId, ac.index));
|
tokens.set(ac.index, await createToken(verified.repo, verified.taskId, ac.index));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return c.html(renderWebPage(task.title, verified.taskId, task.criteria, tokens, verified.acIndex));
|
return c.html(renderWebPage(result.title, verified.taskId, result.criteria, tokens, verified.acIndex, result.toggled));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Tasks/checklist] Check error:", err);
|
console.error("[Tasks/checklist] Check error:", err);
|
||||||
return c.html(renderError("Error", "Could not update task. Please try again."), 500);
|
return c.html(renderError("Error", "Could not update task. Please try again."), 500);
|
||||||
|
|
|
||||||
|
|
@ -87,16 +87,23 @@ export async function readTask(repo: string, taskId: string): Promise<TaskInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle an AC item to checked. Idempotent: if already checked, no-op.
|
* Toggle an AC item: unchecked → checked, checked → unchecked.
|
||||||
|
* Returns the new checked state.
|
||||||
*/
|
*/
|
||||||
export async function checkAC(repo: string, taskId: string, acIndex: number): Promise<TaskInfo> {
|
export async function toggleAC(repo: string, taskId: string, acIndex: number): Promise<TaskInfo & { toggled: boolean }> {
|
||||||
const filePath = await findTaskFile(repo, taskId);
|
const filePath = await findTaskFile(repo, taskId);
|
||||||
let content = await Bun.file(filePath).text();
|
let content = await Bun.file(filePath).text();
|
||||||
|
|
||||||
const unchecked = `- [ ] #${acIndex} `;
|
const unchecked = `- [ ] #${acIndex} `;
|
||||||
const checked = `- [x] #${acIndex} `;
|
const checked = `- [x] #${acIndex} `;
|
||||||
|
let nowChecked = false;
|
||||||
if (content.includes(unchecked)) {
|
if (content.includes(unchecked)) {
|
||||||
content = content.replace(unchecked, checked);
|
content = content.replace(unchecked, checked);
|
||||||
|
nowChecked = true;
|
||||||
|
await Bun.write(filePath, content);
|
||||||
|
} else if (content.includes(checked)) {
|
||||||
|
content = content.replace(checked, unchecked);
|
||||||
|
nowChecked = false;
|
||||||
await Bun.write(filePath, content);
|
await Bun.write(filePath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,5 +112,6 @@ export async function checkAC(repo: string, taskId: string, acIndex: number): Pr
|
||||||
status: parseStatus(content),
|
status: parseStatus(content),
|
||||||
criteria: parseAC(content),
|
criteria: parseAC(content),
|
||||||
filePath,
|
filePath,
|
||||||
|
toggled: nowChecked,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,15 +36,15 @@ function progress(criteria: AcceptanceCriterion[]): string {
|
||||||
return `<div class="pb"><div class="pf" style="width:${pct}%"></div></div><div class="pt">${done} of ${total} complete (${pct}%)</div>`;
|
return `<div class="pb"><div class="pf" style="width:${pct}%"></div></div><div class="pt">${done} of ${total} complete (${pct}%)</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checklist(criteria: AcceptanceCriterion[], tokens: Map<number, string>, justChecked?: number): string {
|
function checklist(criteria: AcceptanceCriterion[], tokens: Map<number, string>, justToggled?: number): string {
|
||||||
return criteria
|
return criteria
|
||||||
.map((ac) => {
|
.map((ac) => {
|
||||||
const hi = ac.index === justChecked ? " hi" : "";
|
const hi = ac.index === justToggled ? " hi" : "";
|
||||||
if (ac.checked) {
|
|
||||||
return `<div class="row${hi}"><span class="cb ck">✓</span><span class="txt done">#${ac.index} ${esc(ac.text)}</span></div>`;
|
|
||||||
}
|
|
||||||
const token = tokens.get(ac.index);
|
const token = tokens.get(ac.index);
|
||||||
const url = token ? `${checklistConfig.baseUrl}/rtasks/check/${token}` : "#";
|
const url = token ? `${checklistConfig.baseUrl}/rtasks/check/${token}` : "#";
|
||||||
|
if (ac.checked) {
|
||||||
|
return `<div class="row${hi}"><a href="${url}" class="cb ck" title="Click to uncheck">✓</a><span class="txt done">#${ac.index} ${esc(ac.text)}</span></div>`;
|
||||||
|
}
|
||||||
return `<div class="row${hi}"><a href="${url}" class="cb uc" title="Click to check off"> </a><span class="txt">#${ac.index} ${esc(ac.text)}</span></div>`;
|
return `<div class="row${hi}"><a href="${url}" class="cb uc" title="Click to check off"> </a><span class="txt">#${ac.index} ${esc(ac.text)}</span></div>`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
@ -71,23 +71,33 @@ export function renderWebPage(
|
||||||
taskId: string,
|
taskId: string,
|
||||||
criteria: AcceptanceCriterion[],
|
criteria: AcceptanceCriterion[],
|
||||||
tokens: Map<number, string>,
|
tokens: Map<number, string>,
|
||||||
justChecked?: number,
|
justToggled?: number,
|
||||||
|
wasChecked?: boolean,
|
||||||
): string {
|
): string {
|
||||||
const justAC = justChecked !== undefined ? criteria.find((c) => c.index === justChecked) : undefined;
|
const justAC = justToggled !== undefined ? criteria.find((c) => c.index === justToggled) : undefined;
|
||||||
const banner = justAC
|
let banner = "";
|
||||||
? `<div style="background:${SUCCESS_COLOR};color:#fff;padding:12px;border-radius:8px;margin-bottom:20px;font-size:14px;">✓ Checked off: <strong>#${justAC.index} ${esc(justAC.text)}</strong></div>`
|
if (justAC && wasChecked) {
|
||||||
: "";
|
banner = `<div style="background:${SUCCESS_COLOR};color:#fff;padding:12px;border-radius:8px;margin-bottom:20px;font-size:14px;">✓ Checked off: <strong>#${justAC.index} ${esc(justAC.text)}</strong></div>`;
|
||||||
|
} else if (justAC && !wasChecked) {
|
||||||
|
banner = `<div style="background:#f59e0b;color:#fff;padding:12px;border-radius:8px;margin-bottom:20px;font-size:14px;">↻ Unchecked: <strong>#${justAC.index} ${esc(justAC.text)}</strong></div>`;
|
||||||
|
}
|
||||||
const allDone = criteria.every((c) => c.checked);
|
const allDone = criteria.every((c) => c.checked);
|
||||||
const doneMsg = allDone ? `<div style="text-align:center;padding:24px;font-size:18px;color:${SUCCESS_COLOR};">🎉 All items complete!</div>` : "";
|
const doneMsg = allDone ? `<div style="text-align:center;padding:24px;font-size:18px;color:${SUCCESS_COLOR};">🎉 All items complete!</div>` : "";
|
||||||
|
|
||||||
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)} - rTasks</title><style>${styles}</style></head>
|
const rtasksUrl = `${checklistConfig.baseUrl}/demo/rtasks`;
|
||||||
|
|
||||||
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)} - rTasks</title><style>${styles}
|
||||||
|
.btn{display:inline-block;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:600;text-decoration:none;margin-top:16px}
|
||||||
|
.btn-primary{background:#6366f1;color:#fff}.btn-primary:hover{background:#4f46e5}
|
||||||
|
</style></head>
|
||||||
<body><div class="card">
|
<body><div class="card">
|
||||||
${banner}
|
${banner}
|
||||||
<h1>${esc(title)}</h1>
|
<h1>${esc(title)}</h1>
|
||||||
<div class="sub">${esc(taskId)}</div>
|
<div class="sub">${esc(taskId)}</div>
|
||||||
${progress(criteria)}
|
${progress(criteria)}
|
||||||
${checklist(criteria, tokens, justChecked)}
|
${checklist(criteria, tokens, justToggled)}
|
||||||
${doneMsg}
|
${doneMsg}
|
||||||
|
<div style="text-align:center;margin-top:20px"><a href="${rtasksUrl}" class="btn btn-primary">Open in rTasks</a></div>
|
||||||
<div class="ft">rTasks · rspace.online</div>
|
<div class="ft">rTasks · rspace.online</div>
|
||||||
</div></body></html>`;
|
</div></body></html>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue