create-pmos 0.195.0 → 0.197.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-core-docs-test.js +18 -0
- package/workspace/c3-ib-items-test.js +16 -8
- package/workspace/c3-ib-more-test.js +3 -2
- package/workspace/c3-ib-triage-backlog-test.js +1 -1
- package/workspace/c3-quiet-test.js +4 -3
- package/workspace/guide/core-documents.md +8 -0
- package/workspace/guide/initiative-boards.md +11 -6
- package/workspace/lib/core-docs.js +24 -5
- package/workspace/lib/coredoc-cli.js +1 -1
- package/workspace/lib/routes-core-docs.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-core-docs.js +5 -3
- package/workspace/public/pm/c3-demo-coredocs.js +12 -2
- package/workspace/public/pm/c3-ib-items.js +26 -17
- package/workspace/public/pm/c3-ib-releases.js +4 -7
- package/workspace/public/pm/c3-ib-sprints.js +5 -8
- package/workspace/public/pm/c3-ib-triage.js +4 -9
- package/workspace/public/pm/c3-ini-life.css +2 -0
- package/workspace/public/pm/c3-initiatives.js +3 -1
- package/workspace/public/pm/c3-quiet.css +1 -1
- package/workspace/public/pm/c3-quiet.js +4 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.197.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T09:43:42Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T09:43:42Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T09:43:42Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -49,6 +49,24 @@ const { harness } = require('./c3-ini-harness');
|
|
|
49
49
|
ok('R3 Issue it says nothing was sent and closes the sheet', await until(async () => /Issued\. Nothing was sent/.test(await toast()) && !(await sheetOpen())));
|
|
50
50
|
ok('R4 the row is up to date and the draft is gone from the waiting list', await until(async () => (await rowState('invest')) === 'ok' && (await st(() => document.querySelectorAll('[data-cd="drafts"] [data-cd-wait]').length)) === 1));
|
|
51
51
|
|
|
52
|
+
/* ── a next step on every row (PRD amendment 2026-10-13): the OKRs, the scan and the status report not started ── */
|
|
53
|
+
await st(() => Promise.all(['okrs', 'scan', 'status'].map((key) => fetch('/api/core-docs/drop', { method: 'POST', body: JSON.stringify({ scope: 'portfolio', key, slot: 'x' }) }))));
|
|
54
|
+
await page.click('[data-ini="pf-tabs"] [data-ini-tab="overview"]');
|
|
55
|
+
await until(() => st(() => !!document.querySelector('[data-ini="boards"]')));
|
|
56
|
+
await page.click('[data-ini="pf-tabs"] [data-ini-tab="docs"]');
|
|
57
|
+
await settle('[data-cd-row="okrs"][data-cd-st="missing"]');
|
|
58
|
+
const btn = (sel) => st((x) => { const n = document.querySelector(x); return n ? n.textContent.trim() : ''; }, sel);
|
|
59
|
+
ok('X1 the OKRs offer Propose them, the scan Run a scan, the status report Set it up', (await btn('[data-cd-draft="okrs"]')) === 'Propose them' && (await btn('[data-cd-go="scan"]')) === 'Run a scan' && (await btn('[data-cd-go="status"]')) === 'Set it up');
|
|
60
|
+
await page.click('[data-cd-draft="okrs"]');
|
|
61
|
+
ok('X2 Propose them starts a draft of the OKRs', await waitToast(/PMOS is drafting the okrs and health budgets/));
|
|
62
|
+
ok('X3 the OKRs row shows "Agent drafting"', await until(async () => (await rowState('okrs')) === 'running'));
|
|
63
|
+
await page.click('[data-cd-go="scan"]');
|
|
64
|
+
ok('X4 Run a scan opens the Portfolio Overview, where Horizon scan lives', await until(() => st(() => !!document.querySelector('[data-ini="boards"]') && !document.querySelector('[data-cd="core"]'))));
|
|
65
|
+
await page.click('[data-ini="pf-tabs"] [data-ini-tab="docs"]');
|
|
66
|
+
await settle('[data-cd-go="status"]');
|
|
67
|
+
await page.click('[data-cd-go="status"]');
|
|
68
|
+
ok('X5 Set it up opens Reports', await until(() => st(() => { const t = document.querySelector('[data-c3-tab="rep"]'); return !!t && t.getAttribute('aria-selected') === 'true'; })));
|
|
69
|
+
|
|
52
70
|
/* ── a board: IDPF ── */
|
|
53
71
|
await open('board:idpf:docs');
|
|
54
72
|
await settle('[data-cd="core"]');
|
|
@@ -50,20 +50,26 @@ const { harness } = require('./c3-ini-harness');
|
|
|
50
50
|
ok('K6 a Jira item dropped on a column stays put and the toast says it changes in Jira', !!t6 && (await colOf('IDPF-1201')) === 'To do', t6);
|
|
51
51
|
|
|
52
52
|
/* ── shared filters ── */
|
|
53
|
+
ok('F0 the filters are one row: search, the initiative picker and Filters, no initiative or source chips', await st(() => { const bar = document.querySelector('.wk-fbar'); return !!bar && !!bar.querySelector('[data-wk="q"]') && !!bar.querySelector('[data-wk="ini"]') && !!bar.querySelector('[data-wk="filters"]') && !document.querySelector('[data-wk="inis"],[data-wk="srcs"],[data-wk="count"]'); }));
|
|
54
|
+
await page.click('[data-wk="ini"]');
|
|
53
55
|
await page.click('[data-wk-ini="IDPF-21"]');
|
|
54
|
-
await until(() => st(() => document.querySelector('[data-wk
|
|
56
|
+
await until(() => st(() => /IDPF-21/.test(document.querySelector('[data-wk="ini"]').textContent)));
|
|
55
57
|
const only21 = await st(() => [...document.querySelectorAll('[data-ini-card]')].map((c) => c.dataset.iniCard).join(','));
|
|
56
58
|
ok('F1 the Initiative filter keeps that initiative\'s items', only21 === 't-idpf-3' || /^(t-idpf-3,?)+$/.test(only21) || (only21.length > 0 && await st(() => [...document.querySelectorAll('[data-ini-card] [data-ini-open]')].every((n) => n.dataset.iniOpen === 'IDPF-21'))), only21);
|
|
59
|
+
await page.click('[data-wk="ini"]');
|
|
57
60
|
await page.click('[data-wk-ini=""]');
|
|
58
|
-
await
|
|
59
|
-
await
|
|
61
|
+
await until(() => st(() => /All initiatives/.test(document.querySelector('[data-wk="ini"]').textContent)));
|
|
62
|
+
await page.click('[data-wk="filters"]');
|
|
63
|
+
await page.selectOption('[data-wk="filter-panel"] [data-wk="src"]', 'pmos');
|
|
64
|
+
await until(() => st(() => !!document.querySelector('[data-wk-unset="src"]')));
|
|
65
|
+
await page.keyboard.press('Escape');
|
|
60
66
|
ok('F2 the Source filter keeps PMOS tasks only', await st(() => { const s = [...document.querySelectorAll('[data-ini-card]')].map((c) => c.dataset.src); return s.length > 0 && s.every((x) => x === 'pmos'); }));
|
|
61
|
-
await page.click('[data-ini="board-tabs"] .qt-more'); // Backlog sits behind More (c3-
|
|
62
|
-
await page.click('[data-
|
|
67
|
+
await page.click('[data-ini="board-tabs"] .qt-more'); // Backlog sits behind More, a menu (c3-board-declutter)
|
|
68
|
+
await page.click('[data-qt-more="backlog"]');
|
|
63
69
|
await until(() => st(() => !!document.querySelector('[data-wk-tab="backlog"]')));
|
|
64
|
-
ok('F3 the filters stay on when the tab changes to Backlog', await st(() => document.querySelector('[data-wk-
|
|
70
|
+
ok('F3 the filters stay on when the tab changes to Backlog', await st(() => /Source: PMOS tasks/.test(document.querySelector('[data-wk-unset="src"]').textContent) && document.querySelector('[data-wk="filters"] .bdg').textContent === '1' && [...document.querySelectorAll('[data-ini-item]')].every((n) => n.dataset.src === 'pmos')));
|
|
65
71
|
await page.click('[data-wk="clear"]');
|
|
66
|
-
await until(() => st(() => document.querySelector('[data-wk
|
|
72
|
+
ok('F4 Clear turns every filter off', await until(() => st(() => !document.querySelector('[data-wk="chips"]') && !document.querySelector('[data-wk="filters"] .bdg'))));
|
|
67
73
|
|
|
68
74
|
/* ── Backlog: add a PMOS task ── */
|
|
69
75
|
const groups = await st(() => [...document.querySelectorAll('[data-wk-group]')].map((g) => g.dataset.wkGroup).join(','));
|
|
@@ -100,7 +106,9 @@ const { harness } = require('./c3-ini-harness');
|
|
|
100
106
|
/* ── a board with Build and deliver off ── */
|
|
101
107
|
await open('board:office:backlog');
|
|
102
108
|
await until(() => st(() => !!document.querySelector('[data-wk-tab="backlog"]')));
|
|
103
|
-
|
|
109
|
+
await page.click('[data-wk="filters"]');
|
|
110
|
+
ok('O1 with Build and deliver off the Filters panel has no Source and says why', await until(() => st(() => !!document.querySelector('[data-wk="filter-panel"]') && !document.querySelector('[data-wk="filter-panel"] [data-wk="src"]') && /Build and deliver is off/.test(document.querySelector('[data-wk="no-jira"]').textContent))));
|
|
111
|
+
await page.keyboard.press('Escape');
|
|
104
112
|
ok('O2 every item there is a PMOS task', await st(() => { const s = [...document.querySelectorAll('[data-ini-item]')].map((n) => n.dataset.src); return s.length > 0 && s.every((x) => x === 'pmos'); }));
|
|
105
113
|
await H.finish();
|
|
106
114
|
})();
|
|
@@ -24,6 +24,7 @@ const { harness } = require('./c3-ini-harness');
|
|
|
24
24
|
await until(() => st(() => !!document.querySelector('[data-inb="release-next"]')));
|
|
25
25
|
const when = await st(() => [...document.querySelectorAll('[data-inb-release]')].map((n) => n.dataset.inbRelease + ':' + n.dataset.inbWhen).join(','));
|
|
26
26
|
ok('R1 Releases shows the next release first, then a later one and the past ones', /^rel-idpf-31:next,rel-idpf-32:later,rel-idpf-30:past$/.test(when), when);
|
|
27
|
+
ok('R1b the Unreleased header links to the Calendar and there is no side card', await st(() => !!document.querySelector('[data-inb="release-next"] [data-inb="open-calendar"]') && !document.querySelector('.bcol.r')));
|
|
27
28
|
ok('R2 each release lists its scope initiatives as links', await st(() => !!document.querySelector('[data-inb-release="rel-idpf-31"] [data-inb-scope="IDPF-12"]')));
|
|
28
29
|
await page.click('[data-inb-release="rel-idpf-31"] [data-inb-scope="IDPF-12"]').catch(async () => { await page.click('[data-inb-release="rel-idpf-31"] .tt'); await page.click('[data-inb-release="rel-idpf-31"] [data-inb-scope="IDPF-12"]'); });
|
|
29
30
|
ok('R3 a scope link opens the initiative', await until(() => st(() => /ini:idpf:IDPF-12/.test(decodeURIComponent(location.search)))));
|
|
@@ -41,8 +42,8 @@ const { harness } = require('./c3-ini-harness');
|
|
|
41
42
|
/* ── Sprints, with the burndown card ── */
|
|
42
43
|
await open('board:idpf:sprints');
|
|
43
44
|
await until(() => st(() => !!document.querySelector('[data-inb="sprints-source"]')));
|
|
44
|
-
ok('SP1
|
|
45
|
-
ok('SP2 the active sprint is drawn with its counts; future and closed sprints are listed', await st(() => !!document.querySelector('[data-inb-state="active"]') && !!document.querySelector('[data-inb-state="future"]') && !!document.querySelector('[data-inb-state="closed"]') && /
|
|
45
|
+
ok('SP1 the active sprint header says Jira, read-only, and there is no banner', /Jira, read-only/.test(await text('[data-inb="sprints-source"]')) && await st(() => !!document.querySelector('[data-inb-state="active"] [data-inb="sprints-source"]') && !document.querySelector('.fold [data-inb="sprints-source"], .fold[data-inb="sprints-source"]')));
|
|
46
|
+
ok('SP2 the active sprint is drawn with its counts; future and closed sprints are listed', await st(() => !!document.querySelector('[data-inb-state="active"]') && !!document.querySelector('[data-inb-state="future"]') && !!document.querySelector('[data-inb-state="closed"]') && ((m) => !!m && Number(m[1]) + Number(m[2]) + Number(m[3]) === 12)(/Done(\d+)Inprogress(\d+)Todo(\d+)/.exec(document.querySelector('[data-inb="sprint-counts"]').textContent.replace(/\s+/g, '')))));
|
|
46
47
|
ok('SP3 the sprint burndown card draws the ideal and actual lines', await until(() => st(() => !!document.querySelector('[data-ini-tool-card="burndown"] svg.ini-burn path.ideal') && !!document.querySelector('[data-ini-tool-card="burndown"] svg.ini-burn path.act'))));
|
|
47
48
|
|
|
48
49
|
/* ── an initiative's six tabs ── */
|
|
@@ -23,7 +23,7 @@ const { harness } = require('./c3-ini-harness');
|
|
|
23
23
|
ok('T1 the open asks of the board, newest first', asks === 'ask-1,ask-3', asks);
|
|
24
24
|
ok('T2 an ask that was asked for more says so', await st(() => !!document.querySelector('[data-ibt-ask="ask-3"] [data-ibt="need-more"]')));
|
|
25
25
|
const intro = await st(() => document.querySelector('[data-ibt="intro"]').textContent);
|
|
26
|
-
ok('T3 the
|
|
26
|
+
ok('T3 the New asks header names the default workflow and its first stage; there is no side card', /Product, in Discover/.test(intro) && await st(() => !document.querySelector('.bcol.r') && !/How triage works/.test(document.body.textContent)), intro);
|
|
27
27
|
await page.fill('[data-ibt="line"]', 'Audit log export to S3 for enterprise admins');
|
|
28
28
|
await page.click('[data-ibt="add"]');
|
|
29
29
|
const added = await until(() => st(() => { const r = document.querySelector('[data-ibt="asks"] [data-ibt-ask]'); return r && /Audit log export/.test(r.textContent) && r.dataset.ibtAsk; }));
|
|
@@ -93,10 +93,11 @@ const shown = (s) => { const n = document.querySelector(s); return !!n && getCom
|
|
|
93
93
|
ok('M1 Backlog, Releases, Docs and Sprints are behind More', hidden.length > 0 && hidden.every(Boolean), JSON.stringify(hidden));
|
|
94
94
|
ok('M2 Summary still shows on the row', await tabVis('summary'));
|
|
95
95
|
await page.click('[data-ini="board-tabs"] .qt-more');
|
|
96
|
-
ok('M3 More
|
|
97
|
-
await page.click('[data-
|
|
96
|
+
ok('M3 More opens a menu of them and the row does not grow', await waitFor(page, () => document.querySelectorAll('[data-qt-more]').length === 4) && !(await tabVis('backlog')) && await page.getAttribute('[data-ini="board-tabs"] .qt-more', 'aria-expanded') === 'true');
|
|
97
|
+
await page.click('[data-qt-more="backlog"]');
|
|
98
98
|
ok('M4 Backlog opens and stays shown while it is the open tab',
|
|
99
|
-
await waitFor(page, () => document.querySelector(
|
|
99
|
+
await waitFor(page, () => { const vis = (k) => { const n = document.querySelector(`[data-ini="board-tabs"] [data-ini-tab="${k}"]`); return !!n && getComputedStyle(n).display !== 'none'; };
|
|
100
|
+
return document.querySelector('[data-ini="board-tabs"] [data-ini-tab="backlog"]')?.getAttribute('aria-pressed') === 'true' && vis('backlog') && !vis('docs'); })); // the quiet layer marks the redrawn tabs a moment after they draw
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
/* the freshness dot */
|
|
@@ -29,11 +29,19 @@ Risks are written once, on the initiative. Board risks and cross-board risks col
|
|
|
29
29
|
| Due | The cadence passed, or the current stage needs the document and it is not there. |
|
|
30
30
|
| Draft ready | PMOS drafted it. It waits for you. |
|
|
31
31
|
| Not started | Nothing is there, and the level needs it now. |
|
|
32
|
+
| None yet | Nothing is there yet, and this fills as the work goes: the decision log, kill conditions, market and user context. |
|
|
32
33
|
| Comes later | The initiative has not reached the stage that needs it. |
|
|
33
34
|
| Stale | It was issued before a change it depends on: a stage move (initiative risks, launch checklist, status update), a newer risk list or outcome review (rolled-up documents), or a change to the board settings (board charter). |
|
|
34
35
|
|
|
35
36
|
A row with no source and no document is "Not started" and has no date. PMOS does not guess a date or a warning.
|
|
36
37
|
|
|
38
|
+
A row that is not started always has a next step:
|
|
39
|
+
|
|
40
|
+
- **Draft it**: PMOS drafts a document it keeps itself.
|
|
41
|
+
- **Propose them** (OKRs and health budgets): with no OKR files, PMOS drafts proposed key results and health budgets for you to review and issue. Once OKR files exist, they are the source.
|
|
42
|
+
- **Run a scan** (Horizon scan reports): opens the Portfolio Overview at Horizon scan, where Scan now is.
|
|
43
|
+
- **Set it up** (status reports): opens Reports, where you set up the report.
|
|
44
|
+
|
|
37
45
|
## How PMOS drafts a document
|
|
38
46
|
|
|
39
47
|
- **When you ask.** Press **Draft it** on a row, or **Ask PMOS to change it** on a draft. PMOS starts one background run. The run reads the sources of that document and writes one draft. It has no tool that sends, posts or edits.
|
|
@@ -24,18 +24,22 @@ Under the rows are a timeline of all boards (board › initiative › epic) and
|
|
|
24
24
|
|
|
25
25
|
The tabs are **Summary**, **Initiatives**, **Triage**, **Backlog**, **Board**, **Timeline**, **Releases**, **Docs** and **Sprints**. A board with Build and deliver off has no Releases and no Sprints tab.
|
|
26
26
|
|
|
27
|
-
On a screen wider than 600 pixels, the breadcrumb, the board status, the tabs and the Board settings gear are on one row. **Backlog**, **Releases**, **Docs** and **Sprints** are behind **More**. Press **More** to
|
|
27
|
+
On a screen wider than 600 pixels, the breadcrumb, the board status, the tabs and the Board settings gear are on one row. **Backlog**, **Releases**, **Docs** and **Sprints** are behind **More**. Press **More** to open a menu of them. When one of them is the open tab, it shows next to the other five tabs.
|
|
28
28
|
|
|
29
29
|
- **Summary** shows the initiatives at a glance and one bar per category. At a glance lists up to six open initiatives. Initiatives with work, and Now initiatives that are not on track, come first. The others follow by end date. Press a bar to open the Initiatives tab with that category.
|
|
30
30
|
- **Initiatives** lists the initiatives of the board, 25 on each page. See [Initiatives: status, hold, cancel, merge and archive](#initiatives-status-hold-cancel-merge-and-archive).
|
|
31
|
-
- **Triage** lists the new asks. Add an ask, then open it to see its draft. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). On a board, **New initiative** lists the anchors that `planning/okrs/` declares. Pick one from the list. Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
32
|
-
- **Backlog**, **Board** and **Timeline** show the epics and items. Jira items and PMOS tasks are mixed. The
|
|
31
|
+
- **Triage** lists the new asks. Add an ask, then open it to see its draft. The New asks heading names the workflow and the first stage that **Accept** uses. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). On a board, **New initiative** lists the anchors that `planning/okrs/` declares. Pick one from the list. Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
32
|
+
- **Backlog**, **Board** and **Timeline** show the epics and items. Jira items and PMOS tasks are mixed. The filters are on one row and are the same on the three tabs:
|
|
33
|
+
- Type in the search box to find an item by key, title or owner.
|
|
34
|
+
- Press **All initiatives** to choose one initiative. Its ID then shows on the button.
|
|
35
|
+
- Press the filter icon to set **Type**, **Source** (Jira or PMOS tasks) and **Done** (Show or Hide). A board with Build and deliver off has no Source. The badge on the icon counts the filters that are on.
|
|
36
|
+
- A filter that is on shows next to the icon. Press its x to remove it, or press **Clear** to remove all.
|
|
33
37
|
- In Backlog, add a PMOS task to an initiative.
|
|
34
38
|
- In Board, drag a PMOS task to a column, or use its menu. A Jira item has a lock: change it in Jira.
|
|
35
39
|
- In Timeline, use **Week**, **Month** and **Quarter** to zoom, and drag the empty chart to pan. Drag a PMOS bar to reschedule the task, or focus the bar and press the Left or Right arrow key. The toast has **Undo**. A Jira bar is locked.
|
|
36
|
-
- **Releases** shows the next release, the later ones and the past ones, with their scope initiatives.
|
|
40
|
+
- **Releases** shows the next release, the later ones and the past ones, with their scope initiatives. Release dates live on the Calendar. Press **On the Calendar** in the Unreleased heading to open it.
|
|
37
41
|
- **Docs** lists the documents of the board by initiative.
|
|
38
|
-
- **Sprints** shows the Jira sprints and the sprint burndown. They are read-only.
|
|
42
|
+
- **Sprints** shows the Jira sprints and the sprint burndown. They are read-only. The heading of the active sprint shows when PMOS last read them from Jira.
|
|
39
43
|
|
|
40
44
|
### Initiatives: status, hold, cancel, merge and archive
|
|
41
45
|
|
|
@@ -105,9 +109,10 @@ Each level has a set of core documents. A level answers one question for one rea
|
|
|
105
109
|
- **Board:** the **Docs** tab starts with the core documents. The sprint review and the release notes show only when Build and deliver is on. The documents linked to the board follow under **Other documents**.
|
|
106
110
|
- **Initiative:** the **Docs** tab has one section for each of Discover, Plan, Do and Learn. The current section lists its documents. A finished section, or one not reached yet, folds to one line.
|
|
107
111
|
|
|
108
|
-
Each row shows what the document is for, its state, how often it changes, who reads it and who writes it. The states are Up to date, Due, Draft ready, Not started, Out of date and Comes later. Press a row to open the document, the tab, the stage tool or Reports that holds it.
|
|
112
|
+
Each row shows what the document is for, its state, how often it changes, who reads it and who writes it. The states are Up to date, Due, Draft ready, Not started, None yet, Out of date and Comes later. Press a row to open the document, the tab, the stage tool or Reports that holds it.
|
|
109
113
|
|
|
110
114
|
- **Draft it:** on a document that is not started, PMOS drafts it. The row shows "Agent drafting" until the draft is ready.
|
|
115
|
+
- **Propose them, Run a scan, Set it up:** a row read from its own source offers the step that makes that source: OKRs are proposed as a draft, Horizon scan opens Scan now, and a status report opens Reports.
|
|
111
116
|
- **Review the draft:** opens the draft in the document sheet. Above the draft, **Issue it** makes the draft the current document. **Ask PMOS to change it** asks for a new draft. PMOS sends nothing to anyone.
|
|
112
117
|
- The Briefing shows one row for each level that has drafts waiting. Press the row to open that Docs view.
|
|
113
118
|
|
|
@@ -46,11 +46,11 @@ const LEVEL = Object.freeze({
|
|
|
46
46
|
initiative: Object.freeze({ question: 'What are we building, why, and did it work?', reader: 'the PM, the builders and the reviewers', rhythm: 'at each stage' }),
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
const e = (level, o) => Object.freeze({ level, reader: null, cadence: null, build: false, cat: null, isNew: false, days: null, rollup: null, tool: null, ...o });
|
|
49
|
+
const e = (level, o) => Object.freeze({ level, reader: null, cadence: null, build: false, cat: null, isNew: false, days: null, rollup: null, tool: null, store: false, ...o });
|
|
50
50
|
const SETS = Object.freeze({
|
|
51
51
|
portfolio: Object.freeze([
|
|
52
52
|
e('portfolio', { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'you', kind: 'doc', days: 91, isNew: true }),
|
|
53
|
-
e('portfolio', { key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', kind: 'derived' }),
|
|
53
|
+
e('portfolio', { key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', kind: 'derived', store: true, days: 91 }),
|
|
54
54
|
e('portfolio', { key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', kind: 'doc', days: 30 }),
|
|
55
55
|
e('portfolio', { key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', kind: 'doc', days: 91, isNew: true }),
|
|
56
56
|
e('portfolio', { key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', kind: 'derived' }),
|
|
@@ -120,6 +120,10 @@ const levelOfScope = (scope) => { const p = parseScope(scope); return p ? p.leve
|
|
|
120
120
|
const scopeDir = (scope) => String(scope).replace(':', '-');
|
|
121
121
|
/** A doc entry PMOS may draft: kind doc, and not one the PM writes. */
|
|
122
122
|
const draftable = (entry) => !!entry && entry.kind === 'doc' && entry.by !== 'you';
|
|
123
|
+
/** A derived entry with `store` (the OKRs) is read from its own source, and PMOS may propose it as a core document while that source is absent. */
|
|
124
|
+
const storable = (entry) => !!entry && (entry.kind === 'doc' || !!entry.store);
|
|
125
|
+
/** A derived entry that fills as the work goes: with no source yet it is "none", not missing (portfolio and board only). */
|
|
126
|
+
const FILLS = Object.freeze(['files', 'watches', 'keeps']);
|
|
123
127
|
|
|
124
128
|
/** The entries of one level. `build`: the board has Build and deliver; `wfTpl`: the initiative's workflow template; `has(key)`: the
|
|
125
129
|
* initiative already has that document (a build-only entry it has stays). */
|
|
@@ -189,6 +193,8 @@ function sourceState(entry, facts) {
|
|
|
189
193
|
return { ...out, st: 'ok', when, due: src.next || null, open };
|
|
190
194
|
}
|
|
191
195
|
// derived
|
|
196
|
+
if (!src && entry.store) return docState(entry, facts);
|
|
197
|
+
if (!src && entry.level !== 'initiative' && FILLS.includes(entry.by)) return { ...out, st: 'none' };
|
|
192
198
|
if (!src) return { ...out, st: absent(entry, facts), open: entry.tool && entry.level === 'initiative' && catState(entry, facts) !== 'later' ? { tool: entry.tool } : null };
|
|
193
199
|
const res = { ...out, st: src.st || 'ok', when: src.when || null, due: src.due || null, flag: src.flag || null, open: src.open || null };
|
|
194
200
|
if (entry.level === 'initiative' && STALE_ON_MOVE.includes(entry.key) && src.atMs != null && facts.movedAt != null && facts.movedAt > src.atMs) {
|
|
@@ -254,7 +260,7 @@ function checkEntry(scope, key) {
|
|
|
254
260
|
if (!p) return refuse(400, 'scope must be portfolio, board:<id> or initiative:<id>');
|
|
255
261
|
const entry = entryOf(p.level, key);
|
|
256
262
|
if (!entry) return refuse(400, `key must be one of ${SETS[p.level].map((x) => x.key).join(', ')} for the ${p.level} level`);
|
|
257
|
-
if (entry
|
|
263
|
+
if (!storable(entry)) return refuse(400, `${entry.title} is not a document PMOS keeps here: it is read from its own source`);
|
|
258
264
|
return { ok: true, entry, level: p.level, id: p.id };
|
|
259
265
|
}
|
|
260
266
|
const scopeLabel = (scope, label) => { const p = parseScope(scope); return !p ? String(scope) : p.level === 'portfolio' ? 'Portfolio' : (label || p.id); };
|
|
@@ -562,6 +568,17 @@ function gather(ctx, w, { level, board: boardId, id, now = Date.now() } = {}) {
|
|
|
562
568
|
return { ok: true, facts: { level, scope: `initiative:${id}`, now, store, row, board: { id: board.id, name: board.name, build: !!board.build }, list } };
|
|
563
569
|
}
|
|
564
570
|
|
|
571
|
+
/** The next step of a row that is not started: draft it, propose it, or go where its source is made. Null otherwise. */
|
|
572
|
+
function actOf(entry, st) {
|
|
573
|
+
if (st === 'stale' && draftable(entry)) return { label: 'Draft it', do: 'draft' };
|
|
574
|
+
if (st !== 'missing') return null;
|
|
575
|
+
if (draftable(entry)) return { label: 'Draft it', do: 'draft' };
|
|
576
|
+
if (entry.store && entry.by !== 'you') return { label: 'Propose them', do: 'draft' };
|
|
577
|
+
if (entry.key === 'scan') return { label: 'Run a scan', go: 'scan' };
|
|
578
|
+
if (entry.kind === 'report' && entry.level !== 'initiative') return { label: 'Set it up', go: 'reports' };
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
565
582
|
/** The GET body. `running(scope, key)` → true while a core-doc run drafts that entry. */
|
|
566
583
|
function answer(facts, { running = () => false } = {}) {
|
|
567
584
|
const L = LEVEL[facts.level];
|
|
@@ -569,7 +586,7 @@ function answer(facts, { running = () => false } = {}) {
|
|
|
569
586
|
const docs = list.map(({ entry: x, st, when, due, flag, staleWhy, open, draft }) => ({
|
|
570
587
|
key: x.key, title: x.title, ic: x.ic, for: x.for, reader: x.reader, cadence: x.cadence, by: x.by, kind: x.kind, cat: x.cat, isNew: !!x.isNew,
|
|
571
588
|
st, when: when || null, due: due || null, flag: flag || null, staleWhy: staleWhy || null, open: open || null, draft: draft || null,
|
|
572
|
-
running: !!running(facts.scope, x.key),
|
|
589
|
+
act: actOf(x, st), running: !!running(facts.scope, x.key),
|
|
573
590
|
}));
|
|
574
591
|
const ini = facts.level === 'initiative';
|
|
575
592
|
return {
|
|
@@ -628,6 +645,8 @@ const GUIDE = Object.freeze({
|
|
|
628
645
|
sections: ['Headline (points committed and finished, as a percentage)', 'Finished', 'Carried over (a table: Item, Points, Why)', 'For the next sprint'] },
|
|
629
646
|
retro: { sources: ['The learnings of the closed initiatives on this board (the stage tools)', 'Their issued outcome reviews: <state>/core-docs/initiative-<id>/outcome/', 'The issued sprint reviews: <state>/core-docs/board-<id>/sprint/'],
|
|
630
647
|
sections: ['Patterns (each with the initiatives it comes from)', 'Keep, change, try (a table)'] },
|
|
648
|
+
okrs: { sources: ['The product strategy, if one is issued: <state>/core-docs/portfolio/strategy/', 'The boards and their initiatives with their anchors (`pmos board`)', 'planning/okrs/health-budgets.md, if any', 'The KR metric records and their readings (planning/records/kr-metrics.jsonl, `pmos measure`), if any'],
|
|
649
|
+
sections: ['A short line: these are proposals for the PM to change, keep or drop', 'Objectives, each with two to four key results (a table: Key result, Metric, Now, Target, Initiatives that serve it); write "not known" where no reading exists', 'Health budgets for upkeep work (a table: Budget, What it covers, Limit)', 'Initiatives with no key result to anchor to, if any'] },
|
|
631
650
|
risks: { sources: ['The initiative record and every file it names (PRD, design, rubric)', 'The Jira snapshot of the initiative, if any (blocked and late tickets)', 'Meeting notes and requirement notes that name the initiative'],
|
|
632
651
|
sections: ['A table: Risk, Impact (High, Medium, Low), Likelihood, Next step, Owner, Since', 'Waits on other boards, if any'] },
|
|
633
652
|
outcome: { sources: ['The key result readings since the release (`pmos measure` records)', 'The release that shipped it (the releases ledger)', 'The initiative bet, its kill conditions and its learnings (the stage tools)', 'Support tickets and notes that name it, if any'],
|
|
@@ -669,7 +688,7 @@ function runPrompt(entry, scope, { today, label } = {}) {
|
|
|
669
688
|
|
|
670
689
|
module.exports = {
|
|
671
690
|
LEDGER, DIR, LEVELS, LEVEL, SETS, CATS, KINDS, EVENTS, BY, RESEARCH_KEEP, BUILD_ONLY_INI, STALE_ON_MOVE, ID_RE, SLOT_RE, GUIDE,
|
|
672
|
-
entryOf, parseScope, levelOfScope, scopeDir, scopeLabel, draftable, setFor, stateOf, summaryOf, localDay, addDays,
|
|
691
|
+
entryOf, parseScope, levelOfScope, scopeDir, scopeLabel, draftable, storable, actOf, FILLS, setFor, stateOf, summaryOf, localDay, addDays,
|
|
673
692
|
ledgerFile, readLines, append, fold, storeOf, checkEntry, writeDraft, issue, drop, frontmatterOf,
|
|
674
693
|
gather, answer, initiativeDocs, initiativeSet, driverLists, runPrompt,
|
|
675
694
|
};
|
|
@@ -22,7 +22,7 @@ const USAGE = [
|
|
|
22
22
|
'usage: pmos coredoc write — the core documents of a portfolio, a board or an initiative',
|
|
23
23
|
' pmos coredoc write --scope <portfolio|board:<id>|initiative:<id>> --key <key> --file <path|->',
|
|
24
24
|
' Write ONE draft of a core document (the markdown from the file, or standard input with -). It waits for the PM, who issues it.',
|
|
25
|
-
' Keys: ' + CD.LEVELS.map((l) => `${l}: ${CD.SETS[l].filter(
|
|
25
|
+
' Keys: ' + CD.LEVELS.map((l) => `${l}: ${CD.SETS[l].filter(CD.storable).map((x) => x.key).join(', ')}`).join(' · '),
|
|
26
26
|
' every command: [--json] [--root <dir>]. Nothing is sent.',
|
|
27
27
|
];
|
|
28
28
|
const BOOL = new Set(['json', 'help']);
|
|
@@ -151,7 +151,7 @@ async function handle(req, res, url, ctx) {
|
|
|
151
151
|
if (sub === 'draft') {
|
|
152
152
|
const r = resolve(ctx, w, scope, key, now);
|
|
153
153
|
if (!r.ok) return send(bad(r.status || 400, r.error));
|
|
154
|
-
if (r.entry
|
|
154
|
+
if (!CD.storable(r.entry)) return send(bad(400, `${r.entry.title} is read from its own source; there is nothing to draft here`));
|
|
155
155
|
if (r.entry.by === 'you') return send(bad(400, `You write the ${r.entry.title.toLowerCase()}. PMOS only checks it`));
|
|
156
156
|
if (r.level === 'board' && r.entry.build && !r.facts.board.build) return send(bad(400, `${r.entry.title} is only for a board with Build and deliver`));
|
|
157
157
|
if (r.level === 'initiative' && !r.facts.list.some((x) => x.entry.key === key)) return send(bad(400, `${r.entry.title} is not in this initiative's set`));
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.197.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.197.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* The server holds the sets, the states and the drafts; this file draws them and holds nothing.
|
|
8
8
|
*
|
|
9
9
|
* coreDocs(env) -> { left, right, dispose } two hosts to put in the columns of cols(): the sections and the cards. env =
|
|
10
|
-
* { api, level, board?, id?, ws, today, toast, nav: { tab(tab), tool(tool), reports() }, boardRow?, iniDocs?: { docs, initiatives } }
|
|
10
|
+
* { api, level, board?, id?, ws, today, toast, nav: { tab(tab), tool(tool), reports(), scan()? }, boardRow?, iniDocs?: { docs, initiatives } }
|
|
11
11
|
* needsLoc(row) -> the `init=` address of the Docs view a Briefing row (kind core-doc-drafts) opens
|
|
12
12
|
* segBadge(n) the count badge of the Portfolio's Documents segment
|
|
13
|
-
* A press on a row opens `open`: { path } the document sheet, { tab } the board tab, { tool } the initiative's Overview tool, { reports } Reports.
|
|
13
|
+
* A press on a row opens `open`: { path } the document sheet, { tab } the board tab, { tool } the initiative's Overview tool, { reports } Reports. A row not started carries `act`: { do: 'draft' } (Draft it, Propose them) or { go: 'scan' | 'reports' } (Run a scan, Set it up).
|
|
14
14
|
* Review the draft opens the draft in the document sheet. The sheet holds the actions: a panel above the draft (docAbove, the sheet's own hook)
|
|
15
15
|
* with Issue it and Ask PMOS to change it, which asks for a new draft. Nothing is sent to anyone; the PM's press issues it. */
|
|
16
16
|
import { el } from './dom.js';
|
|
@@ -91,6 +91,7 @@ function stChip(d) {
|
|
|
91
91
|
case 'draft': return el('span', { class: 'st cd-ag', ...a }, icon('spark', 'sm'), 'Draft ready' + (d.due ? ', due ' + day(d.due) : ''));
|
|
92
92
|
case 'missing': return st('bad', 'alert', 'Not started', a);
|
|
93
93
|
case 'stale': return st('warn', 'clock', 'Out of date', a);
|
|
94
|
+
case 'none': return el('span', { class: 'chip2 dim', ...a }, 'None yet');
|
|
94
95
|
default: return el('span', { class: 'chip2 dim', ...a }, 'Comes later');
|
|
95
96
|
}
|
|
96
97
|
}
|
|
@@ -140,7 +141,8 @@ function coreRow(env, d, redraw) {
|
|
|
140
141
|
d.flag ? st('warn', 'alert', d.flag) : null, d.staleWhy && d.st === 'stale' ? chip(d.staleWhy) : null];
|
|
141
142
|
let action = null;
|
|
142
143
|
if (!d.running && d.st === 'draft') action = pbtn('primary', 'Review the draft', 'spark', { 'data-cd-review': d.key, onclick: review(env, d, redraw) });
|
|
143
|
-
else if (!d.running && d.
|
|
144
|
+
else if (!d.running && d.act && d.act.do === 'draft') action = pbtn('outline', d.act.label, 'spark', { 'data-cd-draft': d.key, onclick: () => draftIt(env, d, redraw) });
|
|
145
|
+
else if (!d.running && d.act && d.act.go && env.nav[d.act.go]) action = pbtn('outline', d.act.label, d.act.go === 'scan' ? 'search' : 'report', { 'data-cd-go': d.key, onclick: () => env.nav[d.act.go]() });
|
|
144
146
|
return nrow({ ic: d.ic || 'doc', hot: isAgent(d) || d.st === 'missing', title: [d.title, d.isNew ? [' ', tag('new')] : null], onOpen: open || undefined, openLabel: open ? 'Open ' + d.title : undefined,
|
|
145
147
|
titleAttrs: { 'data-cd-open': d.key }, sub: d['for'] || '',
|
|
146
148
|
chips: [stChip(d), d.cadence ? chip(icon('cal', 'sm'), d.cadence) : null, d.reader ? chip(icon('user', 'sm'), d.reader) : null, byChip(d.by), ...meta],
|
|
@@ -20,7 +20,7 @@ const RHYTHM = { portfolio: 'each quarter, with a monthly pulse', board: 'every
|
|
|
20
20
|
/* the core sets: key, title, ic, for, reader, cadence, by, kind (doc | live | report), cat, isNew, state, when, due, flag, home */
|
|
21
21
|
const PF = [
|
|
22
22
|
{ key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'you', st: 'ok', w: -9, flag: '2 key results do not trace to it', isNew: true },
|
|
23
|
-
{ key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', st: 'ok', w: -12 },
|
|
23
|
+
{ key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', st: 'ok', w: -12, kind: 'derived', store: true },
|
|
24
24
|
{ key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', st: 'draft', w: 0, due: 4 },
|
|
25
25
|
{ key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'draft', w: 0, due: 4, isNew: true },
|
|
26
26
|
{ key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', st: 'ok', w: -1, kind: 'report' },
|
|
@@ -99,6 +99,16 @@ function iniInfo(id) {
|
|
|
99
99
|
return { id, board: r.board, stage: r.stage, cat: stage.cat || 'Plan', wfTpl: r.template || (b && b.template) || 'product' };
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** The next step of a row not started, as lib/core-docs.js actOf. */
|
|
103
|
+
function actOf(e, stt) {
|
|
104
|
+
const kind = e.kind || 'doc';
|
|
105
|
+
if (kind === 'doc' && e.by !== 'you' && (stt === 'missing' || stt === 'stale')) return { label: 'Draft it', do: 'draft' };
|
|
106
|
+
if (stt !== 'missing') return null;
|
|
107
|
+
if (e.store && e.by !== 'you') return { label: 'Propose them', do: 'draft' };
|
|
108
|
+
if (e.key === 'scan') return { label: 'Run a scan', go: 'scan' };
|
|
109
|
+
if (kind === 'report' && !e.cat) return { label: 'Set it up', go: 'reports' };
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
102
112
|
function entryOf(scope, e, base) {
|
|
103
113
|
const o = OV.get(ovKey(scope, e.key)) || {};
|
|
104
114
|
const stt = o.st || base.st;
|
|
@@ -109,7 +119,7 @@ function entryOf(scope, e, base) {
|
|
|
109
119
|
else if (stt === 'draft') open = { path: draft.path };
|
|
110
120
|
else if (stt === 'ok' || stt === 'due' || stt === 'stale') open = when ? { path: pathOf(scope, e.key, when) } : null;
|
|
111
121
|
return { key: e.key, title: e.title, ic: e.ic, for: e.for, reader: e.reader || null, cadence: e.cadence || null, by: e.by, kind: e.kind || 'doc', cat: e.cat || null, isNew: !!e.isNew,
|
|
112
|
-
st: stt, when: when || null, due: base.due != null ? (typeof base.due === 'number' ? ymd(base.due) : base.due) : null, flag: o.st ? null : base.flag || null, staleWhy: null, open, draft, running: !!o.running };
|
|
122
|
+
st: stt, when: when || null, due: base.due != null ? (typeof base.due === 'number' ? ymd(base.due) : base.due) : null, flag: o.st ? null : base.flag || null, staleWhy: null, open, draft, act: actOf(e, stt), running: !!o.running };
|
|
113
123
|
}
|
|
114
124
|
const summaryOf = (docs) => { const n = (s) => docs.filter((d) => d.st === s).length; return { draft: n('draft'), due: n('due'), missing: n('missing'), stale: n('stale') }; };
|
|
115
125
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* pm/c3-ib-items.js — the Backlog, Board and Timeline tabs of a board (mockup v5, 34-initiatives.js tabItems / backlogBody / kanbanBody /
|
|
2
2
|
* timelineBody). The three show the same work items, Jira items and PMOS tasks mixed (GET /api/boards/items through loadItems), under ONE
|
|
3
|
-
* filter set per board that the three tabs share: search,
|
|
3
|
+
* filter set per board that the three tabs share, on one row (c3-board-declutter): search, an initiative picker, and a Filters panel
|
|
4
|
+
* with Type, Source and Done; a filter that is on shows as a chip with an x. ctx.filter.ini (a link "this initiative
|
|
4
5
|
* only" from an initiative page) sets the Initiative filter on mount. Source shows only when the board has Build and deliver on: with it
|
|
5
6
|
* off there is no Jira, and every item is a PMOS task.
|
|
6
7
|
* registerBoardTab('backlog' | 'board' | 'timeline', (host, ctx) => ...)
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* Every control is a button; the drag handlers are drag events on the cards and columns (pointer handlers stay on the document). */
|
|
15
16
|
import { el } from './dom.js';
|
|
16
17
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
17
|
-
import { icon, pill, lk, ib, pbtn, note,
|
|
18
|
+
import { icon, pill, lk, ib, pbtn, note, sect, card, list, empty, stepsMini, jiraLabel, plural, trunc, openMenu, mono, put } from './c3-ini-ui.js';
|
|
18
19
|
import { STATUSES, wfOf, tplName, loadItems, dropItems, workRow, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
|
|
19
20
|
import { timeline, tlInit, tlRange, tlStill } from './c3-ini-timeline.js';
|
|
20
21
|
|
|
@@ -52,25 +53,33 @@ function mountItems(tab) {
|
|
|
52
53
|
const withWork = [...new Set(all.map((w) => w.ini))].map((id) => byId.get(id)).filter(Boolean).sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || a.id.localeCompare(c.id));
|
|
53
54
|
if (st.ini && !withWork.some((i) => i.id === st.ini) && byId.get(st.ini)) withWork.push(byId.get(st.ini));
|
|
54
55
|
const others = R.initiatives.length - withWork.length;
|
|
55
|
-
const n = shown.filter((w) => w.kind !== 'Epic').length; const ne = shown.filter((w) => w.kind === 'Epic').length;
|
|
56
56
|
const search = el('input', { type: 'text', value: st.q, placeholder: 'Search ' + b.name + ' work items', 'aria-label': 'Search work items', 'data-wk': 'q', autocomplete: 'off' });
|
|
57
57
|
let deb = null;
|
|
58
58
|
search.addEventListener('input', () => { clearTimeout(deb); deb = setTimeout(() => { st.q = search.value; draw('[data-wk="q"]'); }, 200); });
|
|
59
|
-
const
|
|
59
|
+
const tick = () => el('span', { class: 'il-tick' }, icon('check', 'sm'));
|
|
60
|
+
const SRCS = [['', 'All sources'], ['jira', 'Jira ' + jiraLabel(b)], ['pmos', 'PMOS tasks']];
|
|
61
|
+
const nf = (st.kind ? 1 : 0) + (st.src ? 1 : 0) + (st.hideDone ? 1 : 0);
|
|
62
|
+
const iniMenu = (anchor) => openMenu(anchor, { label: 'Initiative', items: [{ group: 'Initiative' },
|
|
63
|
+
{ kids: [tick(), 'All initiatives'], checked: !st.ini, attrs: { 'data-wk-ini': '' }, onPick: () => { st.ini = ''; draw('[data-wk="ini"]'); } },
|
|
64
|
+
...withWork.map((i) => ({ kids: [tick(), el('span', { class: 'mono' }, i.id), ' ' + trunc(i.title, 30)], checked: st.ini === i.id, attrs: { 'data-wk-ini': i.id, title: i.id + ' ' + i.title }, onPick: () => { st.ini = i.id; draw('[data-wk="ini"]'); } })),
|
|
65
|
+
...(others > 0 ? [{ group: plural(others, 'more has', 'more have') + ' no work yet' }] : [])] });
|
|
66
|
+
const filterMenu = (anchor) => {
|
|
67
|
+
const sel = (k, label, opts) => el('label', { class: 'il-f' }, el('span', null, label), el('select', { 'aria-label': label, 'data-wk': k === 'kind' ? 'kind' : 'src', onchange: (e) => { st[k] = e.target.value; draw('[data-wk="filters"]'); } }, ...opts.map(([v, l]) => el('option', { value: v, selected: v === st[k] }, l))));
|
|
68
|
+
openMenu(anchor, { label: 'Filters', body: el('div', { 'data-wk': 'filter-panel' }, el('div', { class: 'ini-mlbl' }, 'Filter work items'),
|
|
69
|
+
sel('kind', 'Type', KINDS), b.build ? sel('src', 'Source', SRCS) : null,
|
|
70
|
+
el('label', { class: 'il-f' }, el('span', null, 'Done'), el('select', { 'aria-label': 'Done items', 'data-wk': 'done', onchange: (e) => { st.hideDone = e.target.value === 'hide'; draw('[data-wk="filters"]'); } }, el('option', { value: 'show', selected: !st.hideDone }, 'Show'), el('option', { value: 'hide', selected: st.hideDone }, 'Hide'))),
|
|
71
|
+
b.build ? null : el('div', { class: 'dim il-fnote', 'data-wk': 'no-jira' }, 'Build and deliver is off, so every item is a PMOS task. ', lk({ onclick: () => ctx.openBoardSettings() }, 'Board settings')),
|
|
72
|
+
el('div', { class: 'chips2 il-fend' }, lk({ 'data-wk': 'clear-in-menu', onclick: () => { Object.assign(st, { q: '', ini: '', src: '', kind: '', hideDone: false }); draw(); } }, 'Clear filters'))) });
|
|
73
|
+
};
|
|
74
|
+
const on = [st.kind ? ['kind', 'Type: ' + st.kind] : null, st.src ? ['src', 'Source: ' + (st.src === 'jira' ? 'Jira' : 'PMOS tasks')] : null, st.hideDone ? ['hideDone', 'Done hidden'] : null].filter(Boolean);
|
|
75
|
+
const any = st.q || st.ini || on.length;
|
|
60
76
|
return [
|
|
61
|
-
el('div', { class: 'calbar' }, el('div', { class: 'form
|
|
62
|
-
el('
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
others > 0 ? el('span', { class: 'dim ini-note' }, plural(others, 'more initiative has', 'more initiatives have') + ' no epics or tasks yet') : null),
|
|
68
|
-
b.build
|
|
69
|
-
? el('div', { class: 'chips2', role: 'group', 'aria-label': 'Source filter', 'data-wk': 'srcs' }, lbl('SOURCE'),
|
|
70
|
-
...[['', 'All sources'], ['jira', [icon('jira', 'sm'), 'Jira ' + jiraLabel(b)]], ['pmos', 'PMOS tasks']].map(([v, l]) => pill({ 'data-wk-src': v, 'aria-pressed': String(st.src === v), onclick: () => { st.src = v; draw('[data-wk-src="' + v + '"]'); } }, ...[].concat(l))))
|
|
71
|
-
: el('div', { class: 'dim ini-note', 'data-wk': 'no-jira' }, icon('info', 'sm'), ' Build and deliver is off for ' + b.name + ', so there is no Jira here: every item is a PMOS task. ', lk({ onclick: () => ctx.openBoardSettings() }, 'Board settings')),
|
|
72
|
-
el('div', { class: 'dim ini-note', 'data-wk': 'count' }, plural(n, 'item') + (ne ? ' in ' + plural(ne, 'epic') : '') + (st.ini ? ' · ' + st.ini + ' only' : '') + (st.src ? ' · ' + (st.src === 'jira' ? 'Jira only' : 'PMOS only') : '') + ' · ',
|
|
73
|
-
any ? lk({ 'data-wk': 'clear', onclick: () => { Object.assign(st, { q: '', ini: '', src: '', kind: '', hideDone: false }); draw(); } }, 'Clear filters') : null, any ? ' · ' : null, 'same filters on Backlog, Board and Timeline'),
|
|
77
|
+
el('div', { class: 'calbar wk-fbar' }, el('div', { class: 'form il-search' }, icon('search', 'sm'), search),
|
|
78
|
+
pbtn('ghost', [st.ini ? el('span', { class: 'mono' }, st.ini) : 'All initiatives', el('span', { class: 'il-chev' }, icon('next', 'sm'))], null, { 'data-wk': 'ini', 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Initiative', onclick: (e) => iniMenu(e.currentTarget) }),
|
|
79
|
+
ib(nf ? 'Filters, ' + nf + ' on' : 'Filters', [icon('filter', 'sm'), nf ? el('span', { class: 'bdg' }, String(nf)) : null], { class: 'il-ib', 'data-wk': 'filters', 'aria-haspopup': 'dialog', 'aria-expanded': 'false', title: 'Filters', onclick: (e) => filterMenu(e.currentTarget) }),
|
|
80
|
+
any ? el('div', { class: 'chips2 il-chips', 'data-wk': 'chips' },
|
|
81
|
+
...on.map(([k, l]) => lk({ 'data-wk-unset': k, title: 'Remove this filter', onclick: () => { st[k] = k === 'hideDone' ? false : ''; draw('[data-wk="filters"]'); } }, l + ' ', icon('x', 'sm'))),
|
|
82
|
+
lk({ 'data-wk': 'clear', onclick: () => { Object.assign(st, { q: '', ini: '', src: '', kind: '', hideDone: false }); draw(); } }, 'Clear')) : null),
|
|
74
83
|
];
|
|
75
84
|
}
|
|
76
85
|
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* and `status` when a release carries them) and keeps the releases whose initiative_ids meet the board's initiatives (loadItems). A
|
|
5
5
|
* release that names another board in `board` is left out. Read-only: releases mirror Jira fix versions; their dates live on the Calendar.
|
|
6
6
|
* Left: Unreleased (the nearest first), each row with its status, date and days to go, the share of its scope done, and in place its
|
|
7
|
-
* scope as links; then Released.
|
|
7
|
+
* scope as links; then Released. The Unreleased header links to the Calendar (c3-board-declutter: no side card). Nothing is invented: a release with no name shows
|
|
8
8
|
* its id, one with no date says "No date yet". */
|
|
9
9
|
import { el } from './dom.js';
|
|
10
10
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
11
|
-
import { icon,
|
|
11
|
+
import { icon, lk, chip, tag, st, note, sect, list, empty, nrow, hbs, hbr, statusChip, jiraLabel, plural, day, daysBetween, put } from './c3-ini-ui.js';
|
|
12
12
|
import { loadItems } from './c3-ini-work.js';
|
|
13
13
|
|
|
14
14
|
const whenOf = (r) => String(r.release_at || r.deploy_at || r.date || '').slice(0, 10);
|
|
@@ -42,10 +42,7 @@ registerBoardTab('releases', async (host, ctx) => {
|
|
|
42
42
|
: [el('span', { class: 'dim' }, 'No initiatives in scope')]))],
|
|
43
43
|
attrs: { 'data-inb-release': r.id, 'data-inb-when': isPast ? 'past' : r === up[0] ? 'next' : 'later' } });
|
|
44
44
|
};
|
|
45
|
-
put(host,
|
|
46
|
-
|
|
47
|
-
past.length ? sect({ ic: 'check', title: 'Released', count: past.length, attrs: { 'data-inb': 'release-past' } }, list(past.map((r) => row(r, true)))) : null],
|
|
48
|
-
[card({ ic: 'out', title: 'Where releases live' }, note('Releases mirror Jira ' + jiraLabel(b) + ' fix versions, because this board builds and delivers. Release dates are references: their home is the Calendar.'),
|
|
49
|
-
el('div', { class: 'chips2' }, pbtn('outline', 'Open the Calendar', 'cal', { 'data-inb': 'open-calendar', onclick: () => ctx.showTab('cal') })))]));
|
|
45
|
+
put(host, ...[sect({ ic: 'out', title: 'Unreleased', count: up.length, right: lk({ 'data-inb': 'open-calendar', title: 'Releases mirror Jira ' + jiraLabel(b) + ' fix versions. Their dates live on the Calendar.', onclick: () => ctx.showTab('cal') }, icon('cal', 'sm'), 'On the Calendar'), attrs: { 'data-inb': 'release-next' } }, up.length ? list(up.map((r) => row(r, false))) : empty('No release planned for ' + b.name + '.', { 'data-inb': 'release-none-next' })),
|
|
46
|
+
past.length ? sect({ ic: 'check', title: 'Released', count: past.length, attrs: { 'data-inb': 'release-past' } }, list(past.map((r) => row(r, true)))) : null].filter(Boolean));
|
|
50
47
|
return () => { dead = true; };
|
|
51
48
|
});
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* registerBoardTab('sprints', (host, ctx) => ...)
|
|
3
3
|
* Reads GET /api/boards/sprints { source: jira|none, readAt, sprints: [{ name, state: active|future|closed, issues: [{ key, summary, status,
|
|
4
4
|
* category: todo|doing|done, initiative }], counts }] }. READ-ONLY: the sprints mirror Jira, so there is no create, edit or move control.
|
|
5
|
-
* Left: the
|
|
6
|
-
* future and closed sprints in place. Right: the active sprint's
|
|
5
|
+
* Left: the active sprint's items, its header chip saying Jira, read-only and the read time (c3-board-declutter: no banner), as rows (a linked initiative opens its page), then the
|
|
6
|
+
* future and closed sprints in place. Right: the active sprint's bars, and its burndown from GET /api/stage-tools/burndown (built by
|
|
7
7
|
* the stage-records change; when that route is not on this server yet, the card says so quietly). source 'none' says there is no tracker. */
|
|
8
8
|
import { el } from './dom.js';
|
|
9
9
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
10
|
-
import { icon,
|
|
10
|
+
import { icon, lk, chip, tag, st, note, sect, card, list, empty, nrow, cols, hbs, hbr, jiraLabel, day, put } from './c3-ini-ui.js';
|
|
11
11
|
import { loadItems } from './c3-ini-work.js';
|
|
12
12
|
import { recordCards } from './c3-ini-records.js';
|
|
13
13
|
|
|
@@ -32,8 +32,7 @@ registerBoardTab('sprints', async (host, ctx) => {
|
|
|
32
32
|
right: [el('span', { class: 'chip2', title: 'Status lives in Jira' }, icon('lock', 'sm'))], attrs: { 'data-inb-issue': i.key } });
|
|
33
33
|
const t = readTime(r.readAt);
|
|
34
34
|
const left = [
|
|
35
|
-
el('
|
|
36
|
-
active ? sect({ ic: 'check', title: active.name + ' items', count: (active.issues || []).length, right: tag('read-only'), attrs: { 'data-inb-sprint': active.name, 'data-inb-state': 'active' } },
|
|
35
|
+
active ? sect({ ic: 'check', title: active.name + ' items', count: (active.issues || []).length, right: el('span', { class: 'chip2', 'data-inb': 'sprints-source', title: 'Mirrored from Jira ' + jiraLabel(b) + '. Change sprints in Jira.' }, icon('lock', 'sm'), 'Jira, read-only' + (t ? ' · read ' + t : '')), attrs: { 'data-inb-sprint': active.name, 'data-inb-state': 'active' } },
|
|
37
36
|
(active.issues || []).length ? list(active.issues.map(issueRow)) : empty('No items in this sprint yet.', { 'data-inb': 'sprint-no-issues' }))
|
|
38
37
|
: empty(sprints.length ? 'No sprint is active.' : 'Jira has no sprints for this board.', { 'data-inb': sprints.length ? 'sprints-no-active' : 'sprints-empty' }),
|
|
39
38
|
...others.map((s) => { const c = countsOf(s); return sect({ ic: s.state === 'closed' ? 'check' : 'clock', title: s.name, count: (s.issues || []).length, right: tag(s.state || 'sprint'), attrs: { 'data-inb-sprint': s.name, 'data-inb-state': s.state } },
|
|
@@ -42,13 +41,11 @@ registerBoardTab('sprints', async (host, ctx) => {
|
|
|
42
41
|
const right = [];
|
|
43
42
|
if (active) {
|
|
44
43
|
const c = countsOf(active); const tot = Math.max(1, c.todo + c.doing + c.done);
|
|
45
|
-
right.push(card({ ic: 'clock', title: active.name, attrs: { 'data-inb': 'sprint-counts' } },
|
|
46
|
-
hbs([hbr('Done', c.done, c.done / tot, 'ok'), hbr('In progress', c.doing, c.doing / tot, 'acc'), hbr('To do', c.todo, c.todo / tot, 'pm')])));
|
|
44
|
+
right.push(card({ ic: 'clock', title: active.name, attrs: { 'data-inb': 'sprint-counts' } }, hbs([hbr('Done', c.done, c.done / tot, 'ok'), hbr('In progress', c.doing, c.doing / tot, 'acc'), hbr('To do', c.todo, c.todo / tot, 'pm')])));
|
|
47
45
|
const ids = items.ok ? items.initiatives.map((i) => i.id) : [];
|
|
48
46
|
const pseudo = { ...ctx, row: { id: ids.join(','), title: '' } };
|
|
49
47
|
right.push(...await recordCards(pseudo, ['burndown']));
|
|
50
48
|
}
|
|
51
|
-
right.push(card({ ic: 'out', title: 'In Jira' }, note('Sprints, their dates and their items are Jira\'s. PMOS mirrors them after each sync.'), el('div', { class: 'chips2' }, pbtn('outline', 'Open the Board', 'cols', { onclick: () => ctx.openBoard(b.id, 'board') }))));
|
|
52
49
|
if (dead) return () => {};
|
|
53
50
|
put(host, cols(left, right));
|
|
54
51
|
return () => { dead = true; };
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* opens it.
|
|
9
9
|
* Ask for more a sheet for the question to the asker (POST /api/boards/asks/more). The ask stays open, marked "asked for more".
|
|
10
10
|
* Decline a sheet that asks for the reason (POST /api/boards/asks/decline).
|
|
11
|
-
*
|
|
11
|
+
* The New asks header names the default workflow and its first stage (c3-board-declutter: no side card, the asks take the width).
|
|
12
12
|
* Contract fields relied on: an ask is { id, line, source?, at?, by?, needMore?, moreNote?, draft?: { title?, lane, confidence,
|
|
13
13
|
* anchor_suggestion, prior_rejections: [{ id, title, why }] }, initiative? (accepted), why? (declined) }. Nothing is invented. */
|
|
14
14
|
import { el } from './dom.js';
|
|
15
15
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
16
|
-
import { icon, pbtn, lk, chip, tag, st, note, noteA, lbl, sect, card, list, empty, nrow,
|
|
16
|
+
import { icon, pbtn, lk, chip, tag, st, note, noteA, lbl, sect, card, list, empty, nrow, ago, put } from './c3-ini-ui.js';
|
|
17
17
|
import { TEMPLATES } from './c3-ini-work.js';
|
|
18
18
|
import { isScanAsk, scanAskParts } from './c3-scan.js';
|
|
19
19
|
|
|
@@ -34,12 +34,7 @@ registerBoardTab('triage', async (host, ctx) => {
|
|
|
34
34
|
const addBtn = pbtn('outline', 'Add', 'plus', { 'data-ibt': 'add', onclick: () => add() });
|
|
35
35
|
const msg = errLine({ 'data-ibt': 'error' });
|
|
36
36
|
const left = el('div', { class: 'ini-col' });
|
|
37
|
-
|
|
38
|
-
note('New asks land here from Slack, email, Jira Service Desk and meeting notes. Each gets a sharpness check and a prior-ask check. Accepting starts an initiative with this board\'s default workflow, ' + def.name + ', in ' + first.name + '. You can switch the initiative to another workflow afterwards.'),
|
|
39
|
-
chgl([el('li', null, icon('check', 'sm'), el('span', null, 'Accept: starts an initiative (' + def.name + ', ' + first.name + ')'), el('span')),
|
|
40
|
-
el('li', null, icon('reply', 'sm'), el('span', null, 'Ask for more: the asker is prompted for detail'), el('span')),
|
|
41
|
-
el('li', null, icon('x', 'sm'), el('span', null, 'Decline: the asker is told why'), el('span'))]));
|
|
42
|
-
put(host, cols(left, right));
|
|
37
|
+
put(host, left);
|
|
43
38
|
const setErr = (t) => { msg.textContent = t || ''; msg.hidden = !t; };
|
|
44
39
|
|
|
45
40
|
function draftNodes(a) {
|
|
@@ -74,7 +69,7 @@ registerBoardTab('triage', async (host, ctx) => {
|
|
|
74
69
|
function draw() {
|
|
75
70
|
put(left,
|
|
76
71
|
card({ ic: 'plus', title: 'Add an ask' }, el('div', { class: 'form', 'data-ibt': 'add-form' }, input, addBtn), msg),
|
|
77
|
-
sect({ ic: 'inbox', title: 'New asks', count: S.open.length }, S.open.length ? list(S.open.map((a) => row(a, 'open')), { 'data-ibt': 'asks', 'aria-label': 'Open asks' }) : empty('Nothing new to triage on this board.', { 'data-ibt': 'empty' })),
|
|
72
|
+
sect({ ic: 'inbox', title: 'New asks', count: S.open.length, right: el('span', { class: 'dim', 'data-ibt': 'intro', title: 'Accept starts an initiative on this board\'s default workflow. Ask for more prompts the asker. Decline tells them why.' }, 'Accept starts ' + def.name + ', in ' + first.name) }, S.open.length ? list(S.open.map((a) => row(a, 'open')), { 'data-ibt': 'asks', 'aria-label': 'Open asks' }) : empty('Nothing new to triage on this board.', { 'data-ibt': 'empty' })),
|
|
78
73
|
S.other.length ? sect({ ic: 'check', title: 'Decided', count: S.other.length, attrs: { 'data-ibt': 'closed' } }, list(S.other.map((x) => row(x, x.state)))) : null);
|
|
79
74
|
if (S.justAdded) {
|
|
80
75
|
const li = left.querySelector('[data-ibt-ask="' + CSS.escape(S.justAdded) + '"] .tt'); S.justAdded = '';
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
.c3 .ini-mpanel .il-f{display:grid;grid-template-columns:76px 1fr;align-items:center;gap:8px;padding:4px 8px;font-size:var(--fs-2);color:var(--muted-foreground)}
|
|
25
25
|
.c3 .ini-mpanel .il-f select{width:100%}
|
|
26
26
|
.c3 .ini-mpanel .il-fend{justify-content:flex-end;padding:6px 8px 2px}
|
|
27
|
+
.c3 .ini-mpanel .il-fnote{padding:4px 8px;font-size:var(--fs-2);max-width:280px}
|
|
28
|
+
.c3 .wk-fbar .il-chips{margin-left:auto}
|
|
27
29
|
.c3 .ini-mi .il-hint{color:var(--muted-foreground)}
|
|
28
30
|
.c3 .ini-mi[aria-checked="false"] .il-tick{visibility:hidden}
|
|
29
31
|
/* The board's Initiatives list shows every chip of a row, as the mockup draws it (the quiet pages keep two elsewhere). */
|
|
@@ -248,7 +248,9 @@ export function mountInitiatives(pane, opts) {
|
|
|
248
248
|
const total = boards.reduce((n, b) => n + ((b.counts || {}).total || 0), 0);
|
|
249
249
|
const badge = segBadge(0);
|
|
250
250
|
const lhHost = el('div', { class: 'cd-host' });
|
|
251
|
-
const cd = coreDocs({ api: pageApi, level: 'portfolio', ws: opts.ws || (boards[0] && boards[0].ws) || '', today: P.today, toast, nav: { reports: () => (opts.showTab || (() => {}))('rep'), tab: () => {}, tool: () => {}
|
|
251
|
+
const cd = coreDocs({ api: pageApi, level: 'portfolio', ws: opts.ws || (boards[0] && boards[0].ws) || '', today: P.today, toast, nav: { reports: () => (opts.showTab || (() => {}))('rep'), tab: () => {}, tool: () => {},
|
|
252
|
+
// Run a scan: the Horizon scan section, with Scan now, lives on the Portfolio Overview.
|
|
253
|
+
scan: () => { go({ view: 'portfolio' }); setTimeout(() => { const h = root.querySelector('[data-scan-setup]'); if (h && h.scrollIntoView) h.scrollIntoView({ block: 'start' }); }, 400); } },
|
|
252
254
|
onData: (r) => { setBadge(badge, r); put(lhHost, lh('doc', 'Portfolio documents', PORTFOLIO + ': the documents leadership reads to judge the bets. ' + (r.docs.filter((d) => d.st === 'draft').length ? plural(r.docs.filter((d) => d.st === 'draft').length, 'draft') + ' to review, ' : '') + r.docs.filter((d) => d.st === 'due').length + ' due.')); } });
|
|
253
255
|
P.dispose = () => cd.dispose();
|
|
254
256
|
put(root, pathCrumbs(null, null, el('div', { class: 'chips2' }, mono(plural(boards.length, 'board') + ' · ' + plural(total, 'initiative') + ' · ' + plural(asks, 'new ask')), pbtn('primary', 'New board', 'plus', { 'data-ini': 'new-board', onclick: () => openNewBoard() }))),
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
.c3 [data-ini-view="board"] > .calbody > .lh{display:none !important} /* board name repeats the breadcrumb */
|
|
44
44
|
.c3 [data-ini="board-tabs"] .bdg{display:none !important}
|
|
45
45
|
.c3 [data-ini="board-tabs"] .qt-more-tab{display:none}
|
|
46
|
-
.c3 [data-ini="board-tabs"]
|
|
46
|
+
.c3 [data-ini="board-tabs"] .qt-more-tab[aria-pressed="true"]{display:inline-flex}
|
|
47
47
|
.c3 .qt-status{color:var(--muted-foreground);font-size:var(--fs-2);margin-left:8px}
|
|
48
48
|
.c3 .qt-status.warn{color:var(--warning)}
|
|
49
49
|
.c3 [data-ini="glance"] .chip2.mono{display:none !important}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* mountQuiet(root) links c3-quiet.css once and keeps the page in its quiet layout as the views draw:
|
|
5
5
|
* - the freshness line folds into one dot next to the date ("4 sources not read"); pressing the dot shows the full line;
|
|
6
6
|
* - a board: its status joins the breadcrumb, the board tabs join that row, Backlog, Releases, Docs and Sprints go
|
|
7
|
-
* behind More, Dashboard and the all-zero Work items card are not shown;
|
|
7
|
+
* behind More (a menu; the open one shows beside the five, c3-board-declutter), Dashboard and the all-zero Work items card are not shown;
|
|
8
8
|
* - the portfolio: the headline joins the breadcrumb, an empty triage card is not shown, the timeline folds;
|
|
9
9
|
* - empty chips ("No owner", "0 epics", ...) are not drawn;
|
|
10
10
|
* - Calendar: the layer chips sit behind one Layers button, the Detail card shows once a meeting is picked;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* It runs again, debounced, whenever the views redraw (one MutationObserver on the root). */
|
|
15
15
|
|
|
16
16
|
import { button } from './primitives.js';
|
|
17
|
+
import { openMenu } from './c3-ini-ui.js';
|
|
17
18
|
|
|
18
19
|
const EMPTY_CHIP = /^(No owner|No dates yet|0 epics|0%|No release planned|Build and deliver)$/;
|
|
19
20
|
const MORE_TABS = /^(backlog|releases|docs|sprints)$/;
|
|
@@ -76,12 +77,10 @@ function board(v) {
|
|
|
76
77
|
if (tabs) {
|
|
77
78
|
for (const t of tabs.querySelectorAll('[data-ini-tab]')) if (MORE_TABS.test(t.dataset.iniTab)) t.classList.add('qt-more-tab');
|
|
78
79
|
if (!tabs.querySelector('.qt-more')) {
|
|
79
|
-
const m = button({ label: 'More', variant: 'ghost', size: 'sm', attrs: { class: 'qt-more', 'aria-expanded': 'false' },
|
|
80
|
-
onClick: () => {
|
|
80
|
+
const m = button({ label: 'More', variant: 'ghost', size: 'sm', attrs: { class: 'qt-more', 'aria-haspopup': 'menu', 'aria-expanded': 'false' },
|
|
81
|
+
onClick: () => openMenu(m, { label: 'More board views', items: [...tabs.querySelectorAll('.qt-more-tab')].map((t) => ({ kids: [...t.childNodes].map((n) => n.cloneNode(true)), checked: t.getAttribute('aria-pressed') === 'true', attrs: { 'data-qt-more': t.dataset.iniTab }, onPick: () => t.click() })) }) });
|
|
81
82
|
tabs.appendChild(m);
|
|
82
83
|
}
|
|
83
|
-
// a tab behind More that is the open one keeps the row shown, so the pressed tab never disappears
|
|
84
|
-
if (tabs.querySelector('.qt-more-tab[aria-pressed="true"]')) tabs.classList.add('qt-show');
|
|
85
84
|
const row = crumbs && crumbs.parentElement;
|
|
86
85
|
if (row && tabs.parentElement !== row) { row.classList.add('qt-ctx'); row.insertBefore(tabs, row.querySelector(':scope > .chips2:not([data-ini="crumbs"])')); }
|
|
87
86
|
}
|