create-pmos 0.186.0 → 0.188.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/scripts/workflow-state.js +2 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-ib-triage-backlog-test.js +20 -13
- package/workspace/c3-ini-life-test.js +167 -0
- package/workspace/c3-initiatives-test.js +6 -6
- package/workspace/c3-phone-test.js +67 -16
- package/workspace/c3-quiet-test.js +2 -1
- package/workspace/guide/initiative-boards.md +37 -2
- package/workspace/guide/my-work.md +1 -0
- package/workspace/guide/phone-companion.md +19 -4
- package/workspace/lib/day-run.js +4 -3
- package/workspace/lib/engine/boards.js +73 -18
- package/workspace/lib/engine/workflow-state.js +2 -1
- package/workspace/lib/initiative-rows.js +2 -2
- package/workspace/lib/my-work.js +2 -1
- package/workspace/lib/routes-boards.js +119 -6
- package/workspace/lib/routes-meeting-focus.js +19 -7
- package/workspace/lib/vendor/scripts/workflow-state.js +2 -1
- package/workspace/lib/work-assigned.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +82 -17
- package/workspace/public/pm/c3-demo-focus.js +39 -1
- package/workspace/public/pm/c3-focus.css +53 -0
- package/workspace/public/pm/c3-focus.js +188 -44
- package/workspace/public/pm/c3-ib-initiatives.js +127 -60
- package/workspace/public/pm/c3-ib-items.js +2 -2
- package/workspace/public/pm/c3-ini-life.css +32 -0
- package/workspace/public/pm/c3-ini-life.js +379 -0
- package/workspace/public/pm/c3-ini-ui.js +12 -7
- package/workspace/public/pm/c3-initiatives.js +14 -4
- package/workspace/public/pm/c3-phone.css +3 -1
- package/workspace/public/pm/c3-phone.js +28 -8
- package/workspace/public/pm/console3.css +3 -0
- package/workspace/public/pm/pm-work-view.js +11 -5
- package/workspace/public/pm/run-card.js +12 -3
- package/workspace/public/pm/sessions-panel.js +2 -2
- package/workspace/server.js +25 -3
- package/workspace/test-lists/c3-ini-life.json +9 -0
|
@@ -1,96 +1,144 @@
|
|
|
1
|
-
/* pm/c3-ib-initiatives.js — the Initiatives tab of a board (mockup v5, 34-initiatives.js tabInitiatives / iniToolbar / iniRow
|
|
1
|
+
/* pm/c3-ib-initiatives.js — the Initiatives tab of a board (mockup v5, 34-initiatives.js tabInitiatives / iniToolbar / iniRow; its header and
|
|
2
|
+
* lifecycle from mockup v2 of c3-initiative-lifecycle, approved 2026-10-13).
|
|
2
3
|
* registerBoardTab('initiatives', (host, ctx) => ...) ctx is documented in the header of c3-initiatives.js.
|
|
3
|
-
* What it draws, all of it paged on the server (GET /api/boards/backlog; q, view, cat, stage, status, owner, horizon, sort, offset, limit):
|
|
4
|
-
* - a search box (debounced), the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
4
|
+
* What it draws, all of it paged on the server (GET /api/boards/backlog; q, view, cat, stage, status, owner, horizon, sort, offset, limit, archived):
|
|
5
|
+
* - one row of controls: a search box with an icon (debounced), the view menu (Now and next, Mine, At risk, No owner, All), a filter
|
|
6
|
+
* icon whose menu holds Status (grouped Active, Paused, Closed), Stage, Category, Owner and Horizon and whose badge counts the
|
|
7
|
+
* filters on, a sort icon whose menu holds the sort keys and the direction, and New initiative. "All active" and "At risk or late"
|
|
8
|
+
* are comma lists (status=on track,at risk,late), which the server reads as any of;
|
|
9
|
+
* - the filters on, as removable chips in the list header, and Clear;
|
|
10
|
+
* - one console3 row per initiative: id, its workflow template, the stage dots of its own workflow, status, then owner, progress,
|
|
11
|
+
* dates and warnings (or, when it is on hold or cancelled, since when, the check-back day and the reason), "Merged from", the
|
|
12
|
+
* duplicate hint; a "…" menu (Move to a stage, Status, Tidy up: c3-ini-life.js rowMenu) and Open;
|
|
13
|
+
* - a footer: the numbered pager of 25, "N cancelled hidden" (opens All with Status: Cancelled) and "N archived" (opens the archive);
|
|
14
|
+
* - the archive: a search box and Back to initiatives, each archived initiative with its reason or "Merged into <id>", Restore or
|
|
15
|
+
* Undo merge.
|
|
10
16
|
* ctx.filter (from the URL or a Summary tile) opens the tab on that view and those filters. One request is in flight at a time; a change
|
|
11
|
-
* made meanwhile is sent when it returns and the older answer is dropped.
|
|
12
|
-
*
|
|
17
|
+
* made meanwhile is sent when it returns and the older answer is dropped. After a hold, cancel or resume the list reads again until the
|
|
18
|
+
* row shows it (the engine derives a lifecycle change in a child process). host.dataset.ibbState is loading | ready | error and
|
|
19
|
+
* host.dataset.ibbSeq counts the answers drawn (the browser suites poll both). */
|
|
13
20
|
import { el } from './dom.js';
|
|
14
21
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
15
|
-
import { icon,
|
|
22
|
+
import { icon, lk, ib, pbtn, pill, chip, st, note, list, empty, nrow, sect, statusChip, ownerChip, stepsMini, progChip, dateChip, daysBetween, plural, openMenu, CATS, put } from './c3-ini-ui.js';
|
|
16
23
|
import { wfOf, catOf, tplName } from './c3-ini-work.js';
|
|
24
|
+
import { rowMenu, lifeChips, archivedChips, dupChip, mergedFromChip, restoreBtn, rowIcon, ensureLifeCss } from './c3-ini-life.js';
|
|
17
25
|
|
|
18
26
|
const PAGE = 25;
|
|
19
27
|
const VIEWS = [['now', 'Now and next'], ['mine', 'Mine'], ['risk', 'At risk'], ['noowner', 'No owner'], ['all', 'All']];
|
|
20
|
-
const
|
|
28
|
+
const STATUS_GROUPS = [['', [['', 'Any']]],
|
|
29
|
+
['Active', [['on track,at risk,late', 'All active'], ['on track', 'On track'], ['at risk,late', 'At risk or late'], ['at risk', 'At risk'], ['late', 'Late']]],
|
|
30
|
+
['Paused', [['on hold', 'On hold']]],
|
|
31
|
+
['Closed', [['done,cancelled', 'Done or cancelled'], ['done', 'Done'], ['cancelled', 'Cancelled']]]];
|
|
32
|
+
const STATUS = STATUS_GROUPS.flatMap((g) => g[1]);
|
|
21
33
|
const HORIZON = [['', 'Any'], ['now,next', 'Now and next'], ['now', 'Now'], ['next', 'Next'], ['later', 'Later']];
|
|
22
34
|
const SORTS = [['horizon', 'Horizon'], ['end', 'Ends'], ['title', 'Title'], ['stage', 'Stage'], ['progress', 'Progress'], ['status', 'Status'], ['owner', 'Owner'], ['id', 'Key']];
|
|
23
|
-
const FKEYS = ['
|
|
35
|
+
const FKEYS = ['status', 'stage', 'cat', 'owner', 'horizon'];
|
|
36
|
+
const FNAME = { status: 'Status', stage: 'Stage', cat: 'Category', owner: 'Owner', horizon: 'Horizon' };
|
|
24
37
|
const toneOf = (s) => (s === 'late' ? 'bad' : s === 'at risk' ? 'warn' : 'ok');
|
|
25
38
|
const warnChip = (w) => st('warn', 'alert', w, { title: 'An order rule, shown as a warning. It never blocks.', 'data-ini-warning': w });
|
|
39
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26
40
|
|
|
27
41
|
registerBoardTab('initiatives', (host, ctx) => {
|
|
42
|
+
ensureLifeCss();
|
|
28
43
|
const b = ctx.board;
|
|
29
44
|
const f0 = ctx.filter || {};
|
|
30
45
|
const sort0 = String(f0.sort || 'horizon');
|
|
31
46
|
const Q = { q: String(f0.q || ''), view: VIEWS.some((v) => v[0] === f0.view) ? f0.view : 'now', cat: CATS.includes(f0.cat) ? f0.cat : '', stage: f0.stage || '', status: f0.status || '', owner: f0.owner || '', horizon: f0.horizon || '',
|
|
32
|
-
sort: SORTS.some((s) => s[0] === sort0.replace(/^-/, '')) ? sort0.replace(/^-/, '') : 'horizon', dir: sort0.startsWith('-') ? -1 : 1, offset: Math.max(0, Number(f0.offset) || 0) };
|
|
47
|
+
sort: SORTS.some((s) => s[0] === sort0.replace(/^-/, '')) ? sort0.replace(/^-/, '') : 'horizon', dir: sort0.startsWith('-') ? -1 : 1, offset: Math.max(0, Number(f0.offset) || 0), archived: f0.archived === '1' };
|
|
33
48
|
const S = { data: null, flying: false, again: false, dead: false, seq: 0, err: '' };
|
|
34
49
|
|
|
35
|
-
const search = el('input', { type: 'text', 'data-ibb': 'q', 'aria-label': 'Search initiatives',
|
|
50
|
+
const search = el('input', { type: 'text', 'data-ibb': 'q', 'aria-label': 'Search initiatives', value: Q.q, autocomplete: 'off' });
|
|
36
51
|
let deb = null;
|
|
37
52
|
search.addEventListener('input', () => { clearTimeout(deb); deb = setTimeout(() => { Q.q = search.value.trim(); Q.offset = 0; request(); }, 250); });
|
|
38
53
|
search.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); clearTimeout(deb); Q.q = search.value.trim(); Q.offset = 0; request(); } });
|
|
39
|
-
const
|
|
40
|
-
const top = el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, search), views,
|
|
41
|
-
pbtn('primary', 'New initiative', 'plus', { 'data-ini': 'new-initiative', 'aria-haspopup': 'dialog', onclick: () => ctx.newInitiative() }));
|
|
42
|
-
const filters = el('div', { class: 'chips2', 'data-ibb': 'filters' });
|
|
43
|
-
const count = el('div', { class: 'dim ini-note', 'data-ibb': 'count', 'aria-live': 'polite' });
|
|
54
|
+
const top = el('div', { class: 'calbar il-bar', 'data-ibb': 'toolbar' });
|
|
44
55
|
const err = el('div', { class: 'fold w', role: 'alert', 'data-ibb': 'error', hidden: '' });
|
|
45
56
|
const body = el('div', { 'data-ibb': 'body' });
|
|
46
|
-
const
|
|
47
|
-
put(host, el('div', { class: 'calbody', 'data-ibb': 'initiatives' }, top,
|
|
57
|
+
const foot = el('div', { class: 'il-listfoot', 'data-ibb': 'foot' });
|
|
58
|
+
put(host, el('div', { class: 'calbody', 'data-ibb': 'initiatives' }, top, err, body, foot));
|
|
48
59
|
host.dataset.ibbState = 'loading'; host.dataset.ibbSeq = '0';
|
|
49
60
|
|
|
50
|
-
const
|
|
51
|
-
|
|
61
|
+
const on = () => FKEYS.filter((k) => Q[k]);
|
|
62
|
+
const labelOf = (k, v) => (k === 'status' ? (STATUS.find((x) => x[0] === v) || [null, v])[1] : k === 'horizon' ? (HORIZON.find((x) => x[0] === v) || [null, v])[1] : k === 'owner' && v === 'none' ? 'No owner' : v);
|
|
63
|
+
const set = (patch) => { Object.assign(Q, patch, { offset: 0 }); request(); };
|
|
64
|
+
function clearFilters() { for (const k of FKEYS) Q[k] = ''; Q.q = ''; search.value = ''; Q.offset = 0; request(); }
|
|
65
|
+
|
|
52
66
|
function drawControls() {
|
|
53
|
-
|
|
67
|
+
search.placeholder = Q.archived ? 'Search the ' + b.name + ' archive' : 'Search ' + b.name + ' initiatives';
|
|
68
|
+
search.setAttribute('aria-label', Q.archived ? 'Search the archive' : 'Search initiatives');
|
|
69
|
+
const box = el('div', { class: 'form il-search' }, icon('search', 'sm'), search);
|
|
70
|
+
if (Q.archived) {
|
|
71
|
+
put(top, box, pbtn('outline', [el('span', { class: 'il-back' }, icon('next', 'sm')), 'Back to initiatives'], null, { 'data-ibb': 'back', onclick: () => set({ archived: false }) }));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const n = on().length;
|
|
75
|
+
const view = (VIEWS.find((v) => v[0] === Q.view) || VIEWS[0])[1];
|
|
76
|
+
const sl = (SORTS.find((s) => s[0] === Q.sort) || SORTS[0])[1];
|
|
77
|
+
put(top, box,
|
|
78
|
+
pbtn('ghost', [view, el('span', { class: 'il-chev' }, icon('next', 'sm'))], null, { 'data-ibb': 'views', 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Saved views', onclick: (e) => viewsMenu(e.currentTarget) }),
|
|
79
|
+
ib(n ? 'Filters, ' + n + ' on' : 'Filters', [icon('filter', 'sm'), n ? el('span', { class: 'bdg' }, String(n)) : null], { class: 'il-ib', 'data-ibb': 'filters', 'aria-haspopup': 'dialog', 'aria-expanded': 'false', title: 'Filters', onclick: (e) => filterMenu(e.currentTarget) }),
|
|
80
|
+
ib('Sort: ' + sl + ', ' + (Q.dir > 0 ? 'ascending' : 'descending'), el('span', { class: Q.dir > 0 ? 'il-up' : 'il-down' }, icon('next', 'sm')), { 'data-ibb': 'sort', 'aria-haspopup': 'menu', 'aria-expanded': 'false', onclick: (e) => sortMenu(e.currentTarget) }),
|
|
81
|
+
pbtn('primary', 'New initiative', 'plus', { 'data-ini': 'new-initiative', 'aria-haspopup': 'dialog', onclick: () => ctx.newInitiative() }));
|
|
82
|
+
}
|
|
83
|
+
const tick = () => el('span', { class: 'il-tick' }, icon('check', 'sm'));
|
|
84
|
+
function viewsMenu(anchor) {
|
|
85
|
+
openMenu(anchor, { label: 'Saved views', items: [{ group: 'Views' }, ...VIEWS.map(([k, l]) => ({ kids: [tick(), l], checked: Q.view === k, attrs: { 'data-ibb-view': k }, onPick: () => set({ view: k }) }))] });
|
|
86
|
+
}
|
|
87
|
+
function sortMenu(anchor) {
|
|
88
|
+
openMenu(anchor, { label: 'Sort', items: [{ group: 'Sort by' }, ...SORTS.map(([k, l]) => ({ kids: [tick(), l], checked: Q.sort === k, attrs: { 'data-ibb-sort': k }, onPick: () => set({ sort: k }) })),
|
|
89
|
+
{ group: 'Order' }, { kids: [el('span', { class: Q.dir > 0 ? 'il-up' : 'il-down' }, icon('next', 'sm')), Q.dir > 0 ? 'Ascending, press to flip' : 'Descending, press to flip'], attrs: { 'data-ibb': 'dir' }, onPick: () => set({ dir: -Q.dir }) }] });
|
|
90
|
+
}
|
|
91
|
+
function filterMenu(anchor) {
|
|
54
92
|
const fa = (S.data && S.data.facets) || {};
|
|
55
93
|
const keep = (k, keys) => (Q[k] && !keys.includes(Q[k]) ? [Q[k], ...keys] : keys);
|
|
56
94
|
const order = (b.workflow || []).map((s) => s.name);
|
|
57
95
|
const stages = keep('stage', Object.keys(fa.stage || {}).sort((x, y) => ((order.indexOf(x) + 1 || 99) - (order.indexOf(y) + 1 || 99)) || x.localeCompare(y)));
|
|
58
96
|
const owners = keep('owner', Object.keys(fa.owner || {}).filter((o) => o !== 'none').sort((x, y) => (x === 'You' ? -1 : y === 'You' ? 1 : x.localeCompare(y))));
|
|
59
97
|
const n = (k, v) => (fa[k] && fa[k][v] != null ? ' (' + fa[k][v] + ')' : '');
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
sel('status', 'Status',
|
|
64
|
-
sel('
|
|
65
|
-
sel('
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
const opt = (k, [v, l]) => el('option', { value: v, selected: v === Q[k] }, l);
|
|
99
|
+
const sel = (k, label, kids) => el('label', { class: 'il-f' }, el('span', null, label), el('select', { 'data-ibb-filter': k, 'aria-label': label, onchange: (e) => { Q[k] = e.target.value; Q.offset = 0; request(); } }, ...kids));
|
|
100
|
+
const panel = el('div', { 'data-ibb': 'filter-panel' }, el('div', { class: 'ini-mlbl' }, 'Filter initiatives'),
|
|
101
|
+
sel('status', 'Status', STATUS_GROUPS.map(([g, opts]) => (g ? el('optgroup', { label: g }, ...opts.map((o) => opt('status', o))) : opts.map((o) => opt('status', o))))),
|
|
102
|
+
sel('stage', 'Stage', [['', 'Any'], ...stages.map((s) => [s, s + n('stage', s)])].map((o) => opt('stage', o))),
|
|
103
|
+
sel('cat', 'Category', [['', 'Any'], ...CATS.map((c) => [c, c + n('cat', c)])].map((o) => opt('cat', o))),
|
|
104
|
+
sel('owner', 'Owner', [['', 'Anyone'], ['none', 'No owner' + n('owner', 'none')], ...owners.map((o) => [o, o + n('owner', o)])].map((o) => opt('owner', o))),
|
|
105
|
+
sel('horizon', 'Horizon', HORIZON.map((o) => opt('horizon', o))),
|
|
106
|
+
el('div', { class: 'chips2 il-fend' }, lk({ 'data-ibb': 'clear-in-menu', onclick: () => { clearFilters(); anchor.focus(); } }, 'Clear filters')));
|
|
107
|
+
openMenu(anchor, { label: 'Filters', body: panel });
|
|
68
108
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
onPick: async () => { const m = await ctx.moveTo(r.id, s.name); if (m && m.ok) request(); } }))] });
|
|
109
|
+
function chipsOf() {
|
|
110
|
+
const ks = on();
|
|
111
|
+
if (!ks.length) return null;
|
|
112
|
+
return el('div', { class: 'chips2 il-chips', 'data-ibb': 'chips' },
|
|
113
|
+
...ks.map((k) => lk({ 'data-ibb-unset': k, title: 'Remove this filter', onclick: () => set({ [k]: '' }) }, FNAME[k] + ': ' + labelOf(k, Q[k]) + ' ', icon('x', 'sm'))),
|
|
114
|
+
lk({ 'data-ibb': 'clear', onclick: clearFilters }, 'Clear'));
|
|
76
115
|
}
|
|
116
|
+
|
|
117
|
+
const L = { ctx, done: (change) => settle(change) };
|
|
77
118
|
function rowOf(r) {
|
|
119
|
+
if (r.archived) {
|
|
120
|
+
return nrow({ ic: rowIcon(r), title: r.title || r.id, onOpen: () => ctx.openInitiative(r.id), openLabel: 'Open ' + r.id + ' ' + (r.title || ''), titleAttrs: { 'data-ibb-open': r.id },
|
|
121
|
+
chips: archivedChips(r).flat(), right: [restoreBtn(L, r)], attrs: { 'data-ibb-row': r.id, 'data-il-archived': r.archived.into ? 'merged' : 'archived' } });
|
|
122
|
+
}
|
|
78
123
|
const wf = wfOf(r, b);
|
|
79
|
-
const
|
|
80
|
-
|
|
124
|
+
const stopped = r.status === 'on hold' || r.status === 'cancelled';
|
|
125
|
+
const late = !stopped && r.status !== 'done' && r.end && daysBetween(ctx.today, r.end) < 0;
|
|
126
|
+
return nrow({ ic: rowIcon(r), hot: r.status === 'late' || r.status === 'at risk', title: r.title || r.id,
|
|
81
127
|
onOpen: () => ctx.openInitiative(r.id), openLabel: 'Open ' + r.id + ' ' + (r.title || ''), titleAttrs: { 'data-ibb-open': r.id },
|
|
82
|
-
chips: [el('span', { class: 'chip2 mono' }, r.id), el('span', { class: 'tag', title: 'Workflow' }, tplName(r, b)), stepsMini(wf, r.stage, tplName(r, b)), statusChip(r.status),
|
|
83
|
-
progChip(r.progress || 0, toneOf(r.status)), dateChip(r.start, r.end, late), r.anchor ? null : st('warn', 'alert', 'No anchor'), ...(r.warnings || []).map(warnChip)],
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
chips: [el('span', { class: 'chip2 mono' }, r.id), el('span', { class: 'tag', title: 'Workflow' }, tplName(r, b)), stepsMini(wf, r.stage, tplName(r, b)), statusChip(r.status),
|
|
129
|
+
...(stopped ? lifeChips(r) : [ownerChip(r.owner), progChip(r.progress || 0, toneOf(r.status)), dateChip(r.start, r.end, late), b.build ? chip(icon('bolt', 'sm'), plural((r.epics || []).length, 'epic')) : null, r.anchor ? null : st('warn', 'alert', 'No anchor'), ...(r.warnings || []).map(warnChip)]),
|
|
130
|
+
mergedFromChip(r), dupChip(L, r)],
|
|
131
|
+
right: [ib('Actions for ' + r.id, 'more', { 'data-ibb-move': r.id, 'data-ibb-menu': r.id, 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Move, pause, merge or archive',
|
|
132
|
+
onclick: (e) => rowMenu(e.currentTarget, L, r, b.self ? null : async (stage) => { const m = await ctx.moveTo(r.id, stage); if (m && m.ok) request(); }) }),
|
|
133
|
+
ib('Open ' + r.id, 'next', { 'data-ini-open': r.id, onclick: () => ctx.openInitiative(r.id) })],
|
|
134
|
+
attrs: { 'data-ibb-row': r.id, 'data-cat': r.cat || catOf(wf, r.stage), 'data-status': r.status } });
|
|
87
135
|
}
|
|
88
|
-
function
|
|
136
|
+
function pagerOf(d) {
|
|
89
137
|
const pages = Math.max(1, Math.ceil(d.total / PAGE)); const cur = Math.floor(d.offset / PAGE);
|
|
90
|
-
if (pages < 2)
|
|
138
|
+
if (pages < 2) return null;
|
|
91
139
|
const nums = []; for (let p = 0; p < pages; p++) if (p < 2 || p >= pages - 1 || Math.abs(p - cur) <= 1) nums.push(p); else if (nums[nums.length - 1] !== '…') nums.push('…');
|
|
92
140
|
const to = (p) => { Q.offset = p * PAGE; request(true); };
|
|
93
|
-
|
|
141
|
+
return el('div', { class: 'chips2 ini-pager', 'data-ibb': 'pager' }, el('span', { class: 'dim mono ini-meta' }, (d.offset + 1) + '–' + Math.min(d.total, d.offset + d.rows.length) + ' of ' + d.total),
|
|
94
142
|
pill({ 'data-ibb': 'prev', disabled: cur === 0, onclick: () => to(cur - 1) }, 'Previous'),
|
|
95
143
|
...nums.map((p) => (p === '…' ? el('span', { class: 'dim' }, '…') : pill({ 'data-ibb-page': String(p + 1), 'aria-pressed': String(p === cur), 'aria-label': 'Page ' + (p + 1), onclick: () => to(p) }, String(p + 1)))),
|
|
96
144
|
pill({ 'data-ibb': 'next', disabled: cur >= pages - 1, onclick: () => to(cur + 1) }, 'Next'));
|
|
@@ -100,11 +148,18 @@ registerBoardTab('initiatives', (host, ctx) => {
|
|
|
100
148
|
drawControls();
|
|
101
149
|
const d = S.data;
|
|
102
150
|
if (!d) return;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
if (Q.archived) {
|
|
152
|
+
put(body, sect({ ic: 'clip', title: 'Archive', count: d.total, right: el('span', { class: 'dim il-hint' }, 'Kept whole. Restore puts it back as it was.'), attrs: { 'data-ibb': 'archive' } },
|
|
153
|
+
d.rows.length ? list(d.rows.map(rowOf), { 'data-ibb': 'rows' }) : empty(Q.q ? 'Nothing in the archive matches.' : 'Nothing archived on this board.', { 'data-ibb': 'empty' })));
|
|
154
|
+
put(foot, pagerOf(d));
|
|
155
|
+
} else {
|
|
156
|
+
const filtered = on().length || Q.q;
|
|
157
|
+
put(body, sect({ ic: 'init', title: 'Initiatives', count: d.total, right: chipsOf() },
|
|
158
|
+
d.rows.length ? list(d.rows.map(rowOf), { 'data-ibb': 'rows' }) : empty(filtered || Q.view !== 'all' ? 'No initiatives match. Clear the filters or pick All.' : b.name + ' has no initiatives yet. Press New initiative, or accept an ask in Triage.', { 'data-ibb': 'empty' })));
|
|
159
|
+
put(foot, pagerOf(d), d.cancelledHidden || d.archived ? el('span', { class: 'chips2' },
|
|
160
|
+
d.cancelledHidden ? lk({ 'data-ibb': 'show-cancelled', title: 'Cancelled initiatives are hidden from every view', onclick: () => set({ view: 'all', status: 'cancelled' }) }, icon('x', 'sm'), d.cancelledHidden + ' cancelled hidden') : null,
|
|
161
|
+
d.archived ? lk({ 'data-ibb': 'show-archive', onclick: () => set({ archived: true }) }, icon('clip', 'sm'), d.archived + ' archived') : null) : null);
|
|
162
|
+
}
|
|
108
163
|
host.dataset.ibbSeq = String(++S.seq);
|
|
109
164
|
}
|
|
110
165
|
|
|
@@ -116,16 +171,28 @@ registerBoardTab('initiatives', (host, ctx) => {
|
|
|
116
171
|
for (;;) {
|
|
117
172
|
S.again = false;
|
|
118
173
|
const q = { ...Q };
|
|
119
|
-
r = await ctx.api.get('/api/boards/backlog', { board: b.id, q: q.q, view: q.view, cat: q.cat, stage: q.stage, status: q.status, owner: q.owner, horizon: q.horizon, sort: (q.dir < 0 ? '-' : '') + q.sort, offset: q.offset, limit: PAGE });
|
|
120
|
-
if (S.dead) return;
|
|
174
|
+
r = await ctx.api.get('/api/boards/backlog', { board: b.id, q: q.q, view: q.view, cat: q.cat, stage: q.stage, status: q.status, owner: q.owner, horizon: q.horizon, sort: (q.dir < 0 ? '-' : '') + q.sort, offset: q.offset, limit: PAGE, archived: q.archived ? '1' : '' });
|
|
175
|
+
if (S.dead) return null;
|
|
121
176
|
if (!S.again) break;
|
|
122
177
|
}
|
|
123
178
|
S.flying = false; host.removeAttribute('aria-busy');
|
|
124
|
-
if (!r || !r.ok || r.pending) { S.err = r && r.pending ? 'The repository is still being read. Try again in a moment.' : 'The initiatives could not be read: ' + ((r && r.error) || 'no answer') + '.'; host.dataset.ibbState = 'error'; draw(); return; }
|
|
125
|
-
S.err = ''; S.data = { total: r.total || 0, offset: r.offset || 0, limit: r.limit || PAGE, rows: r.rows || [], facets: r.facets || {} };
|
|
179
|
+
if (!r || !r.ok || r.pending) { S.err = r && r.pending ? 'The repository is still being read. Try again in a moment.' : 'The initiatives could not be read: ' + ((r && r.error) || 'no answer') + '.'; host.dataset.ibbState = 'error'; draw(); return null; }
|
|
180
|
+
S.err = ''; S.data = { total: r.total || 0, offset: r.offset || 0, limit: r.limit || PAGE, rows: r.rows || [], facets: r.facets || {}, cancelledHidden: r.cancelledHidden || 0, archived: r.archived || 0 };
|
|
126
181
|
Q.offset = S.data.offset;
|
|
127
182
|
draw(); host.dataset.ibbState = 'ready';
|
|
128
|
-
if (keepFocus) { const f =
|
|
183
|
+
if (keepFocus) { const f = foot.querySelector('[aria-pressed="true"]'); if (f) f.focus(); }
|
|
184
|
+
return S.data;
|
|
185
|
+
}
|
|
186
|
+
/* After a write: read again. A lifecycle line is derived in a child process, so the list may show the old status for a moment;
|
|
187
|
+
* read again until the row shows the new one, or it left the list (a paused initiative leaves Now and next), up to ten times. */
|
|
188
|
+
async function settle(change) {
|
|
189
|
+
for (let k = 0; k < 10 && !S.dead; k++) {
|
|
190
|
+
const d = await request();
|
|
191
|
+
if (!d || !change || change.kind !== 'status') return;
|
|
192
|
+
const row = d.rows.find((x) => x.id === change.id);
|
|
193
|
+
if (change.status ? (!row || row.status === change.status) : (!row || (row.status !== 'on hold' && row.status !== 'cancelled'))) return;
|
|
194
|
+
await sleep(800);
|
|
195
|
+
}
|
|
129
196
|
}
|
|
130
197
|
|
|
131
198
|
drawControls();
|
|
@@ -76,7 +76,7 @@ function mountItems(tab) {
|
|
|
76
76
|
|
|
77
77
|
/* ── Backlog ── */
|
|
78
78
|
function addForm() {
|
|
79
|
-
const inis = R.initiatives.filter((i) => i.status !== 'done').sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || a.id.localeCompare(c.id)).slice(0, 60);
|
|
79
|
+
const inis = R.initiatives.filter((i) => i.status !== 'done' && i.status !== 'cancelled').sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || a.id.localeCompare(c.id)).slice(0, 60);
|
|
80
80
|
const F = { ini: st.ini && inis.some((i) => i.id === st.ini) ? st.ini : (inis[0] || {}).id || '', busy: false };
|
|
81
81
|
const input = el('input', { type: 'text', placeholder: 'Add a PMOS task', 'aria-label': 'Add a PMOS task', 'data-wk': 'add-title' });
|
|
82
82
|
const msg = el('div', { class: 'fold w', role: 'alert', hidden: '', 'data-wk': 'add-error' });
|
|
@@ -163,7 +163,7 @@ function mountItems(tab) {
|
|
|
163
163
|
let hidden = 0;
|
|
164
164
|
if (!st.ini) {
|
|
165
165
|
const pick = inis.filter((i) => withItems.has(i.id)).sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || String(a.start || '9').localeCompare(String(c.start || '9')));
|
|
166
|
-
const rest = (st.src || st.q || st.kind) ? [] : inis.filter((i) => !withItems.has(i.id) &&
|
|
166
|
+
const rest = (st.src || st.q || st.kind) ? [] : inis.filter((i) => !withItems.has(i.id) && ['on track', 'at risk', 'late'].includes(i.status) && i.start && i.end && (i.horizon === 'now' || i.featured)).sort((a, c) => String(a.start).localeCompare(String(c.start)));
|
|
167
167
|
const cap = st.tlAll ? 1e9 : TL_CAP; inis = [...pick, ...rest].slice(0, Math.max(cap, pick.length));
|
|
168
168
|
hidden = pick.length + rest.length - inis.length;
|
|
169
169
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* pm/c3-ini-life.css — the board Initiatives header in two rows and the lifecycle sheets (c3-initiative-lifecycle; mockup v2,
|
|
2
|
+
* 34-initiatives.css "lifecycle + quieter Initiatives header"). Linked once by c3-ini-life.js. Every rule is under an `il-` class. */
|
|
3
|
+
.c3 .il-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
4
|
+
.c3 .il-search{position:relative;flex:1 1 260px}
|
|
5
|
+
.c3 .il-search > .ic{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--muted-foreground);pointer-events:none}
|
|
6
|
+
.c3 .il-search input[type="text"]{padding-left:30px}
|
|
7
|
+
.c3 .il-chev{display:inline-flex;transform:rotate(90deg);margin-left:2px;color:var(--muted-foreground)}
|
|
8
|
+
.c3 .il-back{display:inline-flex;transform:rotate(180deg)}
|
|
9
|
+
.c3 .il-ib{position:relative}
|
|
10
|
+
.c3 .il-ib .bdg{position:absolute;top:-6px;right:-6px}
|
|
11
|
+
.c3 .il-up,.c3 .il-down{display:inline-flex}
|
|
12
|
+
.c3 .il-up{transform:rotate(-90deg)}
|
|
13
|
+
.c3 .il-down{transform:rotate(90deg)}
|
|
14
|
+
.c3 .il-chips{justify-content:flex-end}
|
|
15
|
+
.c3 .il-foot{justify-content:flex-end;margin-top:4px}
|
|
16
|
+
.c3 .il-listfoot{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
|
|
17
|
+
.c3 .il-listfoot > :only-child{margin-left:auto}
|
|
18
|
+
.c3 .il-hint{font-size:var(--fs-2)}
|
|
19
|
+
.c3 .il-sheet{display:grid;gap:14px}
|
|
20
|
+
.c3 .il-sheet textarea{width:100%;resize:vertical}
|
|
21
|
+
.c3 .il-move{gap:8px;align-items:flex-start}
|
|
22
|
+
.c3 .il-banner{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
23
|
+
.c3 .il-banner > span:first-of-type{flex:1 1 240px}
|
|
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
|
+
.c3 .ini-mpanel .il-f select{width:100%}
|
|
26
|
+
.c3 .ini-mpanel .il-fend{justify-content:flex-end;padding:6px 8px 2px}
|
|
27
|
+
.c3 .ini-mi .il-hint{color:var(--muted-foreground)}
|
|
28
|
+
.c3 .ini-mi[aria-checked="false"] .il-tick{visibility:hidden}
|
|
29
|
+
/* The board's Initiatives list shows every chip of a row, as the mockup draws it (the quiet pages keep two elsewhere). */
|
|
30
|
+
.c3 [data-ibb="rows"] .nrow .nb > .chips > :nth-child(n+3){display:inline-flex !important}
|
|
31
|
+
.c3 [data-ibb="rows"] .nrow .nb > .chips > .qt-hide{display:none !important}
|
|
32
|
+
.c3 .il-note{font-size:var(--fs-2);line-height:1.45}
|