generated from coulomb/repo-seed
Implement dashboard UI improvements
This commit is contained in:
@@ -401,12 +401,18 @@ export function buildEntityTable(rows, columns, onRowClick) {
|
||||
for (const col of columns) {
|
||||
const td = document.createElement("td");
|
||||
const raw = col.key ? row[col.key] : null;
|
||||
const text = col.render ? col.render(row) : (raw ?? "—");
|
||||
const textStr = String(text ?? "—");
|
||||
td.textContent = textStr;
|
||||
const rendered = col.render ? col.render(row) : (raw ?? "—");
|
||||
let textStr = "";
|
||||
if (rendered instanceof Node) {
|
||||
td.append(rendered);
|
||||
textStr = td.textContent ?? "";
|
||||
} else {
|
||||
textStr = String(rendered ?? "—");
|
||||
td.textContent = textStr;
|
||||
}
|
||||
if (col.cls) td.className = col.cls;
|
||||
// Native tooltip so full value shows on hover (skip placeholder "—")
|
||||
if (textStr && textStr !== "—") td.title = textStr;
|
||||
if (!(rendered instanceof Node) && textStr && textStr !== "—") td.title = textStr;
|
||||
tr.append(td);
|
||||
}
|
||||
tbody.append(tr);
|
||||
|
||||
160
dashboard/src/components/status-control.js
Normal file
160
dashboard/src/components/status-control.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import {apiFetch} from "./config.js";
|
||||
import {WORKSTREAM_STATUSES} from "./workplan-status.js";
|
||||
|
||||
const STYLE_ID = "status-control-styles";
|
||||
|
||||
export const TASK_STATUSES = ["todo", "in_progress", "blocked", "done", "cancelled"];
|
||||
export {WORKSTREAM_STATUSES};
|
||||
|
||||
function ensureStyles() {
|
||||
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = `
|
||||
.status-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
.status-control-select {
|
||||
min-width: 8.5rem;
|
||||
max-width: 100%;
|
||||
height: 1.85rem;
|
||||
border: 1px solid var(--theme-foreground-faint, #d1d5db);
|
||||
border-radius: 6px;
|
||||
background: var(--theme-background, #fff);
|
||||
color: var(--theme-foreground, #111);
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
padding: 0.18rem 0.45rem;
|
||||
}
|
||||
.status-control-select:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.status-control-message {
|
||||
min-width: 3.5rem;
|
||||
font-size: 0.72rem;
|
||||
color: var(--theme-foreground-muted, #6b7280);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-control-message.status-control-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
.status-control-message.status-control-ok {
|
||||
color: #16a34a;
|
||||
}
|
||||
`;
|
||||
document.head.append(style);
|
||||
}
|
||||
|
||||
function labelForStatus(status) {
|
||||
return String(status ?? "").replace(/_/g, " ");
|
||||
}
|
||||
|
||||
function endpointFor(type, id) {
|
||||
if (type === "task") return `/tasks/${id}`;
|
||||
if (type === "workstream") return `/workstreams/${id}`;
|
||||
throw new Error(`Unsupported status-control type: ${type}`);
|
||||
}
|
||||
|
||||
async function readError(response) {
|
||||
try {
|
||||
const body = await response.json();
|
||||
if (Array.isArray(body?.detail)) return body.detail.map(d => d.msg ?? String(d)).join("; ");
|
||||
return body?.detail ?? `HTTP ${response.status}`;
|
||||
} catch {
|
||||
return `HTTP ${response.status}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function statusControl({
|
||||
entity,
|
||||
type,
|
||||
statuses = type === "task" ? TASK_STATUSES : WORKSTREAM_STATUSES,
|
||||
onSaved = null,
|
||||
} = {}) {
|
||||
ensureStyles();
|
||||
|
||||
let currentStatus = entity?.status ?? "";
|
||||
const root = document.createElement("span");
|
||||
root.className = "status-control";
|
||||
root.addEventListener("click", event => event.stopPropagation());
|
||||
root.addEventListener("mousedown", event => event.stopPropagation());
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.className = "status-control-select";
|
||||
select.setAttribute("aria-label", "Status");
|
||||
for (const status of statuses) {
|
||||
const option = document.createElement("option");
|
||||
option.value = status;
|
||||
option.textContent = labelForStatus(status);
|
||||
select.append(option);
|
||||
}
|
||||
select.value = currentStatus;
|
||||
|
||||
const message = document.createElement("span");
|
||||
message.className = "status-control-message";
|
||||
|
||||
function setMessage(text, kind = "") {
|
||||
message.textContent = text;
|
||||
message.classList.toggle("status-control-error", kind === "error");
|
||||
message.classList.toggle("status-control-ok", kind === "ok");
|
||||
}
|
||||
|
||||
select.addEventListener("change", async () => {
|
||||
const nextStatus = select.value;
|
||||
if (nextStatus === currentStatus) return;
|
||||
|
||||
const payload = {status: nextStatus};
|
||||
if (type === "task" && nextStatus === "blocked") {
|
||||
const existingReason = entity?.blocking_reason ?? "";
|
||||
const reason = existingReason || window.prompt("Blocking reason required for blocked tasks:");
|
||||
if (!reason) {
|
||||
select.value = currentStatus;
|
||||
setMessage("unchanged");
|
||||
return;
|
||||
}
|
||||
payload.blocking_reason = reason;
|
||||
}
|
||||
|
||||
if (type === "task" && nextStatus === "done" && currentStatus !== "done") {
|
||||
const ok = window.confirm(
|
||||
"Mark this task done? Without token fields, State Hub records a heuristic token event."
|
||||
);
|
||||
if (!ok) {
|
||||
select.value = currentStatus;
|
||||
setMessage("unchanged");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
select.disabled = true;
|
||||
setMessage("saving");
|
||||
try {
|
||||
const response = await apiFetch(endpointFor(type, entity.id), {
|
||||
method: "PATCH",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error(await readError(response));
|
||||
const updated = await response.json();
|
||||
Object.assign(entity, updated);
|
||||
currentStatus = updated.status;
|
||||
select.value = currentStatus;
|
||||
setMessage("saved", "ok");
|
||||
if (typeof onSaved === "function") onSaved(updated);
|
||||
setTimeout(() => {
|
||||
if (message.textContent === "saved") setMessage("");
|
||||
}, 2200);
|
||||
} catch (error) {
|
||||
select.value = currentStatus;
|
||||
setMessage(error?.message ?? "save failed", "error");
|
||||
} finally {
|
||||
select.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
root.append(select, message);
|
||||
return root;
|
||||
}
|
||||
@@ -92,6 +92,7 @@ const filtered = data.filter(t =>
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
import {openEntityModal, buildEntityTable} from "./components/entity-modal.js";
|
||||
import {statusControl, TASK_STATUSES} from "./components/status-control.js";
|
||||
|
||||
// ── KPI sidebar card ─────────────────────────────────────────────────────────
|
||||
const _open = data.filter(t => ["todo", "in_progress", "blocked"].includes(t.status));
|
||||
@@ -219,7 +220,7 @@ const sorted = [...filtered].sort((a, b) => {
|
||||
display(buildEntityTable(
|
||||
sorted,
|
||||
[
|
||||
{label: "Status", key: "status"},
|
||||
{label: "Status", render: t => statusControl({entity: t, type: "task", statuses: TASK_STATUSES})},
|
||||
{label: "Priority", key: "priority"},
|
||||
{label: "Title", key: "title", cls: "et-title-col et-title-cell"},
|
||||
{label: "Domain", key: "domain"},
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Task
|
||||
```js
|
||||
import {API} from "../components/config.js";
|
||||
import {fieldRow} from "../components/field-help.js";
|
||||
import {statusControl, TASK_STATUSES} from "../components/status-control.js";
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -22,16 +23,40 @@ if (raw.error) {
|
||||
const shortName = name.length > 60 ? name.slice(0, 60) + "…" : name;
|
||||
display(html`<h1 style="font-size:1.1rem;margin-bottom:0.25rem">Task · <em>${shortName}</em></h1>`);
|
||||
display(html`<p style="margin-top:0"><a href="/tasks">← Tasks</a> | <a href="/token-cost">← Token Cost</a></p>`);
|
||||
display(html`<div class="task-summary">
|
||||
<div>
|
||||
<span>Status</span>
|
||||
${statusControl({
|
||||
entity: raw,
|
||||
type: "task",
|
||||
statuses: TASK_STATUSES,
|
||||
onSaved: () => setTimeout(() => location.reload(), 450),
|
||||
})}
|
||||
</div>
|
||||
<div><span>Priority</span><strong>${raw.priority ?? "—"}</strong></div>
|
||||
<div><span>Assignee</span><strong>${raw.assignee ?? "—"}</strong></div>
|
||||
</div>`);
|
||||
|
||||
const FIELD_ORDER = [
|
||||
"id","title","status","priority","assignee",
|
||||
"workstream_id","due_date","needs_human","intervention_note",
|
||||
"created_at","updated_at",
|
||||
];
|
||||
const HIDDEN_FIELDS = ["description"];
|
||||
|
||||
const taskContent = (raw.description ?? "").trim();
|
||||
if (taskContent) {
|
||||
display(html`<section class="task-content">
|
||||
<h2>Task Content</h2>
|
||||
<div class="task-content-body">${taskContent}</div>
|
||||
</section>`);
|
||||
} else {
|
||||
display(html`<p class="task-content-empty">No task content recorded.</p>`);
|
||||
}
|
||||
|
||||
const rows = FIELD_ORDER.map(k => fieldRow(k, raw[k] ?? null));
|
||||
for (const k of Object.keys(raw)) {
|
||||
if (!FIELD_ORDER.includes(k)) rows.push(fieldRow(k, raw[k]));
|
||||
if (!FIELD_ORDER.includes(k) && !HIDDEN_FIELDS.includes(k)) rows.push(fieldRow(k, raw[k]));
|
||||
}
|
||||
|
||||
display(html`<table style="border-collapse:collapse;width:100%;max-width:640px">
|
||||
@@ -40,3 +65,52 @@ if (raw.error) {
|
||||
</table>`);
|
||||
}
|
||||
```
|
||||
|
||||
<style>
|
||||
.task-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0 1.25rem;
|
||||
max-width: 860px;
|
||||
}
|
||||
.task-summary div {
|
||||
background: var(--theme-background-alt);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem 0.9rem;
|
||||
}
|
||||
.task-summary span {
|
||||
display: block;
|
||||
color: gray;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.task-summary strong {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.task-content {
|
||||
margin: 1rem 0 1.25rem;
|
||||
max-width: 860px;
|
||||
border: 1px solid var(--theme-foreground-faint, #e5e7eb);
|
||||
border-radius: 6px;
|
||||
background: var(--theme-background-alt, #f8fafc);
|
||||
}
|
||||
.task-content h2 {
|
||||
margin: 0;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border-bottom: 1px solid var(--theme-foreground-faint, #e5e7eb);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.task-content-body {
|
||||
padding: 0.85rem 0.9rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.55;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.task-content-empty {
|
||||
margin: 1rem 0;
|
||||
color: var(--theme-foreground-muted, #666);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,10 @@ const gdprReport = await fetch(`${API}/tpsc/report/gdpr`)
|
||||
const snapshots = await fetch(`${API}/tpsc/snapshots/`)
|
||||
.then(r => r.json())
|
||||
.catch(() => []);
|
||||
|
||||
const repos = await fetch(`${API}/repos/`)
|
||||
.then(r => r.ok ? r.json() : [])
|
||||
.catch(() => []);
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -150,12 +154,41 @@ if (gdprReport.warnings.length === 0) {
|
||||
## Per-Repo Breakdown
|
||||
|
||||
```js
|
||||
const repoById = Object.fromEntries(repos.map(r => [r.id, r]));
|
||||
const repoBySlug = Object.fromEntries(repos.map(r => [r.slug, r]));
|
||||
|
||||
function repoForSnapshotKey(repoKey) {
|
||||
return repoById[repoKey] ?? repoBySlug[repoKey] ?? null;
|
||||
}
|
||||
|
||||
function repoWebUrl(repo) {
|
||||
const url = repo?.remote_url ?? "";
|
||||
return /^https?:\/\//.test(url) ? url : null;
|
||||
}
|
||||
|
||||
function repoCell(repoKey) {
|
||||
const repo = repoForSnapshotKey(repoKey);
|
||||
const webUrl = repoWebUrl(repo);
|
||||
const label = repo?.name || repo?.slug || repoKey || "unknown repo";
|
||||
const slug = repo?.slug ?? repoKey ?? "unknown";
|
||||
const domain = repo?.domain_slug ?? "unknown";
|
||||
const content = html`<div class="repo-ref">
|
||||
<div class="repo-ref-name">
|
||||
${webUrl
|
||||
? html`<a href="${webUrl}" target="_blank" rel="noopener noreferrer">${label}</a>`
|
||||
: label}
|
||||
</div>
|
||||
<div class="repo-ref-meta">${domain} / ${slug}</div>
|
||||
</div>`;
|
||||
return content;
|
||||
}
|
||||
|
||||
// Build: latest snapshot per repo → service list
|
||||
const repoBreakdown = new Map();
|
||||
for (const snap of snapshots) {
|
||||
const repoSlug = snap.repo_id || "unknown";
|
||||
if (!repoBreakdown.has(repoSlug) || snap.snapshot_at > repoBreakdown.get(repoSlug).snapshot_at) {
|
||||
repoBreakdown.set(repoSlug, snap);
|
||||
const repoKey = snap.repo_id || snap.repo_slug || "unknown";
|
||||
if (!repoBreakdown.has(repoKey) || snap.snapshot_at > repoBreakdown.get(repoKey).snapshot_at) {
|
||||
repoBreakdown.set(repoKey, snap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +204,8 @@ const repoTable = html`<table style="width:100%; border-collapse:collapse; font-
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${[...repoBreakdown.entries()].map(([repoSlug, snap]) => html`<tr style="border-bottom:1px solid #f3f4f6;">
|
||||
<td style="padding:8px 12px; font-weight:500;">${repoSlug}</td>
|
||||
${[...repoBreakdown.entries()].map(([repoKey, snap]) => html`<tr style="border-bottom:1px solid #f3f4f6;">
|
||||
<td style="padding:8px 12px; font-weight:500;">${repoCell(repoKey)}</td>
|
||||
<td style="padding:8px 12px;">
|
||||
${snap.entries.map(e => {
|
||||
const cat = catalogBySlug[e.service_slug];
|
||||
@@ -191,3 +224,27 @@ const repoTable = html`<table style="width:100%; border-collapse:collapse; font-
|
||||
</table>`;
|
||||
display(repoTable);
|
||||
```
|
||||
|
||||
<style>
|
||||
.repo-ref {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.repo-ref-name {
|
||||
font-weight: 600;
|
||||
color: var(--theme-foreground, #111);
|
||||
}
|
||||
.repo-ref-name a {
|
||||
color: #1d4ed8;
|
||||
text-decoration: none;
|
||||
}
|
||||
.repo-ref-name a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.repo-ref-meta {
|
||||
font-size: 0.76rem;
|
||||
font-weight: 400;
|
||||
color: var(--theme-foreground-muted, #6b7280);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,8 @@ const STEP_LABEL = {
|
||||
test: "Test",
|
||||
review: "Review",
|
||||
finished: "Finished",
|
||||
addressed: "Addressed",
|
||||
resolved: "Resolved",
|
||||
wont_fix: "Won't Fix",
|
||||
};
|
||||
const STEP_COLOR = {
|
||||
@@ -27,6 +29,8 @@ const STEP_COLOR = {
|
||||
test: "#06b6d4",
|
||||
review: "#6366f1",
|
||||
finished: "#16a34a",
|
||||
addressed: "#16a34a",
|
||||
resolved: "#16a34a",
|
||||
wont_fix: "#9ca3af",
|
||||
};
|
||||
const STEP_HINT = {
|
||||
@@ -37,8 +41,11 @@ const STEP_HINT = {
|
||||
test: "Verifying the implementation works correctly.",
|
||||
review: "Awaiting review by the original suggester.",
|
||||
finished: "Shipped and confirmed.",
|
||||
addressed: "Implemented before the current workflow labels existed.",
|
||||
resolved: "Resolved before the current workflow labels existed.",
|
||||
wont_fix: "Decided not to implement.",
|
||||
};
|
||||
const CLOSED_STATUSES = new Set(["finished", "addressed", "resolved", "wont_fix"]);
|
||||
function nextStep(current) {
|
||||
const i = STEPS.indexOf(current);
|
||||
return i >= 0 && i < STEPS.length - 1 ? STEPS[i + 1] : null;
|
||||
@@ -58,7 +65,7 @@ const feedbackState = (async function*() {
|
||||
data = items
|
||||
.filter(t => t.debt_type === "dashboard-improvement")
|
||||
.sort((a, b) => {
|
||||
const st = {submitted:0, analyse:1, plan:2, implement:3, test:4, review:5, finished:6, wont_fix:7, open:8, in_progress:9, resolved:10};
|
||||
const st = {submitted:0, analyse:1, plan:2, implement:3, test:4, review:5, finished:6, addressed:7, resolved:8, wont_fix:9, open:10, in_progress:11};
|
||||
return (st[a.status] ?? 99) - (st[b.status] ?? 99);
|
||||
});
|
||||
}
|
||||
@@ -82,8 +89,8 @@ const _ts = feedbackState.ts;
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
|
||||
const _active = data.filter(t => t.status !== "finished" && t.status !== "wont_fix");
|
||||
const _finished = data.filter(t => t.status === "finished");
|
||||
const _active = data.filter(t => !CLOSED_STATUSES.has(t.status));
|
||||
const _finished = data.filter(t => ["finished", "addressed", "resolved"].includes(t.status));
|
||||
const _wontfix = data.filter(t => t.status === "wont_fix");
|
||||
|
||||
const _kpiBox = html`<div class="kpi-infobox">
|
||||
|
||||
@@ -141,6 +141,7 @@ import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
import "./components/help-tip.js";
|
||||
import {openEntityModal, buildEntityTable} from "./components/entity-modal.js";
|
||||
import {statusControl} from "./components/status-control.js";
|
||||
|
||||
// ── Live indicator ────────────────────────────────────────────────────────────
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
@@ -288,7 +289,7 @@ display(_filtersForm);
|
||||
{label: "Title", key: "title", cls: "et-title-col et-title-cell",
|
||||
render: w => w.title},
|
||||
{label: "Domain", key: "domain"},
|
||||
{label: "Status", key: "status"},
|
||||
{label: "Status", render: w => statusControl({entity: w, type: "workstream", statuses: WORKSTREAM_STATUSES})},
|
||||
{label: "Owner", render: w => w.owner ?? "—"},
|
||||
{label: "Due", render: w => w.due_date ?? "—"},
|
||||
{label: "Updated", render: w => new Date(w.updated_at).toLocaleDateString()},
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Workstream
|
||||
```js
|
||||
import {API} from "../components/config.js";
|
||||
import {fieldRow} from "../components/field-help.js";
|
||||
import {statusControl, TASK_STATUSES, WORKSTREAM_STATUSES} from "../components/status-control.js";
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -33,7 +34,12 @@ if (raw.error) {
|
||||
display(html`<p style="margin-top:0"><a href="/">← Overview</a> | <a href="/workstreams">← Workstreams</a> | <a href="/token-cost">← Token Cost</a></p>`);
|
||||
|
||||
display(html`<div class="ws-summary">
|
||||
<div><span>Status</span><strong>${raw.status ?? "—"}</strong></div>
|
||||
<div><span>Status</span>${statusControl({
|
||||
entity: raw,
|
||||
type: "workstream",
|
||||
statuses: WORKSTREAM_STATUSES,
|
||||
onSaved: () => setTimeout(() => location.reload(), 450),
|
||||
})}</div>
|
||||
<div><span>Workplan</span><strong>${workplan.filename ?? "not file-backed"}</strong></div>
|
||||
<div><span>Tasks</span><strong>${taskRows.length}</strong></div>
|
||||
</div>`);
|
||||
@@ -52,7 +58,12 @@ if (raw.error) {
|
||||
display(html`<table class="task-table">
|
||||
<thead><tr><th>Status</th><th>Priority</th><th>Task</th><th>Human</th></tr></thead>
|
||||
<tbody>${sortedTasks.map(t => html`<tr>
|
||||
<td><span class="task-status task-status-${t.status}">${t.status}</span></td>
|
||||
<td>${statusControl({
|
||||
entity: t,
|
||||
type: "task",
|
||||
statuses: TASK_STATUSES,
|
||||
onSaved: () => setTimeout(() => location.reload(), 450),
|
||||
})}</td>
|
||||
<td>${t.priority ?? "—"}</td>
|
||||
<td><a href="/tasks/${t.id}">${t.title ?? t.id}</a></td>
|
||||
<td>${t.needs_human ? "yes" : ""}</td>
|
||||
|
||||
@@ -61,7 +61,7 @@ Inspection notes:
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "2aaeb57e-26aa-438d-bfd8-4943c8b0b136"
|
||||
```
|
||||
@@ -87,7 +87,7 @@ active queue and the two live suggestions are clearly visible as planned work.
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b6fe5e8e-80f6-439f-8ddc-47e65218f041"
|
||||
```
|
||||
@@ -115,7 +115,7 @@ degrade gracefully.
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "11166491-2c7a-4007-9820-e0707e71556c"
|
||||
```
|
||||
@@ -146,7 +146,7 @@ side-effectful transitions are guarded.
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "34b29724-bd4b-416e-ab55-1d37132490dd"
|
||||
```
|
||||
@@ -171,7 +171,7 @@ Done when the active suggestion
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0562bd05-d67c-4fe2-b501-166650d7129a"
|
||||
```
|
||||
@@ -195,7 +195,7 @@ away and without breaking table scanning.
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "7b055f11-25e2-45cb-80ac-d2d175fb5a1f"
|
||||
```
|
||||
@@ -222,7 +222,7 @@ consistent spacing, labels, and failure states.
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0b182fff-c100-468c-afed-91918483638f"
|
||||
```
|
||||
@@ -244,7 +244,7 @@ Done when the dashboard suggestions page tells the same story as the shipped UI.
|
||||
|
||||
```task
|
||||
id: STATE-WP-0043-T08
|
||||
status: todo
|
||||
status: in_progress
|
||||
priority: high
|
||||
state_hub_task_id: "9d59cae4-c2a8-43e1-8a28-6344e22653b0"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user