diff --git a/dashboard/src/components/workplan-search.js b/dashboard/src/components/workplan-search.js new file mode 100644 index 0000000..aa355e0 --- /dev/null +++ b/dashboard/src/components/workplan-search.js @@ -0,0 +1,22 @@ +/** + * Case-insensitive substring match for Overview "Workplans by Repository". + * Matches title, repo, domain, filename, owner, and id. + * + * @param {object} workplan Row from overview workplan_rows + * @param {string} query Free-text filter + * @returns {boolean} + */ +export function workplanMatchesSearch(workplan, query) { + const q = String(query ?? "").trim().toLowerCase(); + if (!q) return true; + if (!workplan || typeof workplan !== "object") return false; + const haystack = [ + workplan.title, + workplan.repo_label, + workplan.domain, + workplan.workplan_filename, + workplan.owner, + workplan.id, + ].filter(Boolean).join("\n").toLowerCase(); + return haystack.includes(q); +} diff --git a/dashboard/src/docs/overview.md b/dashboard/src/docs/overview.md index ce827ac..c25808d 100644 --- a/dashboard/src/docs/overview.md +++ b/dashboard/src/docs/overview.md @@ -24,6 +24,13 @@ by repository. Each bar is broken into four task-status segments: | orange | wait | | light grey | todo | +Above the chart: + +- **Mode selector** — lifecycle, health, or recently-changed windows (with + matching workplan counts in each option). +- **Text filter** — case-insensitive substring match on title, repository, + domain, workplan filename, and owner. Useful when many rows share a mode. + The left axis shows the `domain / repository` label once per repository group. The `done/total` count is printed to the right of each bar. Workplans with no tasks yet show a grey "— no tasks yet" label. diff --git a/dashboard/src/index.md b/dashboard/src/index.md index 3a8edc4..7deb975 100644 --- a/dashboard/src/index.md +++ b/dashboard/src/index.md @@ -151,6 +151,8 @@ display(html`
No workplans match “${_searchQ}” for the selected mode.
`); + } else { const _emptyMsg = { proposed: "No proposed workplans.", ready: "No ready workplans.", @@ -339,6 +375,7 @@ if (chartWs.length === 0) { month: "No workplans changed this month.", }; display(html`${_emptyMsg[_chartModeValue] ?? "No workplans."}
`); + } } else { display(Plot.plot({ y: { @@ -680,6 +717,7 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({ .live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; } .ws-mode-bar { margin-bottom: 0.75rem; } .ws-mode-select { min-width: 18rem; max-width: 100%; padding: 0.35rem 0.5rem; border-radius: 4px; border: 1px solid var(--theme-foreground-faint, #ddd); background: var(--theme-background); color: var(--theme-foreground); font: inherit; } +.ws-search-input { min-width: 14rem; max-width: 100%; height: 30px; font-size: 0.85rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--theme-foreground-faint, #ccc); background: var(--theme-background, #fff); font-family: inherit; color: inherit; } .card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; } .card.warn { border: 2px solid orange; } .card-link { cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; text-decoration: none; color: inherit; display: block; } diff --git a/dashboard/test/workplan-search.test.mjs b/dashboard/test/workplan-search.test.mjs new file mode 100644 index 0000000..33f5940 --- /dev/null +++ b/dashboard/test/workplan-search.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import {readFileSync} from "node:fs"; +import {dirname, join} from "node:path"; +import {fileURLToPath} from "node:url"; + +import {workplanMatchesSearch} from "../src/components/workplan-search.js"; + +const root = dirname(fileURLToPath(import.meta.url)); +const indexMd = readFileSync(join(root, "../src/index.md"), "utf-8"); + +const sample = { + id: "abc-123", + title: "Overview Workstream Stage Counts", + repo_label: "state-hub", + domain: "infotech", + workplan_filename: "STATE-WP-0057-overview-workstream-stage-counts.md", + owner: "codex", +}; + +test("empty query matches all workplans", () => { + assert.equal(workplanMatchesSearch(sample, ""), true); + assert.equal(workplanMatchesSearch(sample, " "), true); + assert.equal(workplanMatchesSearch(sample, null), true); +}); + +test("matches title, repo, domain, filename, owner, and id", () => { + assert.equal(workplanMatchesSearch(sample, "stage counts"), true); + assert.equal(workplanMatchesSearch(sample, "STATE-HUB"), true); + assert.equal(workplanMatchesSearch(sample, "Infotech"), true); + assert.equal(workplanMatchesSearch(sample, "wp-0057"), true); + assert.equal(workplanMatchesSearch(sample, "codex"), true); + assert.equal(workplanMatchesSearch(sample, "abc-123"), true); + assert.equal(workplanMatchesSearch(sample, "railiance"), false); +}); + +test("overview chart wires the text filter into Workplans by Repository", () => { + assert.match(indexMd, /workplanMatchesSearch/); + assert.match(indexMd, /ws-search-input/); + assert.match(indexMd, /Filter by title, repo, domain/); + assert.match(indexMd, /## Workplans by Repository/); +});