create-pmos 0.123.0 → 0.125.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 +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/notes.md +1 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +100 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +415 -0
- package/workspace/public/pm/briefing-model.js +176 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +173 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +637 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +138 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1015 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1480 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-notes.js +25 -3
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +919 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +8 -0
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/* pm/releases-view.js — the Releases view of Initiatives (console3 wave A4, read side; R4b: on the console3 mockup's own markup and CSS).
|
|
2
|
+
*
|
|
3
|
+
* Lifted from console3.html (the Releases view, lines 4867 to 4945): the lens header `.lh` (iniInfo, line 4941), `.brief.sbb > .bcols`, the list
|
|
4
|
+
* `section.card.sect > .sh2 + ul.list` with a "New release" row (`.ki` with the soft accent and the plus) and one `li.nrow`
|
|
5
|
+
* per release (`.ki`, `.nb > .tt + .chips` with initiative chips, `.tag` tags, date chips and `.st` flags, `.xp > .xi` with the
|
|
6
|
+
* case file: `.case > .sit + .c3rel`), the create form as `.c3rel` (initiative pills, date and time `.c3dt` labels), the
|
|
7
|
+
* tag detection line, the gap flags as `.chgl` rows and the summary lines with their source `.tag`. Styles: pm/console3.css.
|
|
8
|
+
*
|
|
9
|
+
* What is real: GET /api/releases (releases, their tags, gap flags, summary lines, acceptance) and the writes through pmPost
|
|
10
|
+
* (/api/releases/create, /edit, /delete, /summary). A refused summary line is shown, not stored. There is no freeze warning here
|
|
11
|
+
* until a rule exists, and a release never creates a Jira tag: the PM's acceptance comes first, and that is recorded, not
|
|
12
|
+
* acted on. Not lifted: the mockup's invented releases and tags (C3.REL, C3.JV), the ticket table (the payload has no tickets),
|
|
13
|
+
* "shipped" (acceptance is not shipping), the ready-to-ship checks, the freeze chip and its example rule, the sync-from-Jira
|
|
14
|
+
* chips and demo buttons, the outward options (tag in Jira, update to a person, calendar hold: each needs the slide and the
|
|
15
|
+
* outbox, and none exists for a release), the promises card and the "Jira release tags" bars (no ticket counts per tag).
|
|
16
|
+
* Self-registers as the 'releases' Initiatives view, like Timeline. */
|
|
17
|
+
import { el } from './dom.js';
|
|
18
|
+
import { ensureComponentsCss } from './primitives.js';
|
|
19
|
+
import { pmGet, pmPost } from './http.js';
|
|
20
|
+
import { registerInitiativesView } from './registry.js';
|
|
21
|
+
import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
|
|
22
|
+
import { card, hBars } from './c3-init-extras.js';
|
|
23
|
+
|
|
24
|
+
const views = new WeakMap();
|
|
25
|
+
const TYPES = [{ value: 'deploy', label: 'Deploy' }, { value: 'customer', label: 'Customer' }, { value: 'both', label: 'Deploy and customer' }];
|
|
26
|
+
const SOURCES = ['fact', 'estimate', 'assumption', 'reported'];
|
|
27
|
+
const FLAG_TEXT = { initiative_ticket_without_tag: 'Ticket without a release tag', tagged_ticket_without_initiative: 'Tagged ticket with no initiative' };
|
|
28
|
+
const iso = (d, t) => (d ? new Date(d + 'T' + (t || '00:00')).toISOString() : null);
|
|
29
|
+
const dim = (t, extra) => el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' + (extra || '') }, t);
|
|
30
|
+
const when = (v) => (v ? v.replace('T', ' ').replace(/:\d\dZ$/, 'Z') : 'not set');
|
|
31
|
+
const DOW = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
32
|
+
/** The mockup's c3dfmt: "Thu 1 Oct, 12:00" in the viewer's time. */
|
|
33
|
+
const dfmt = (v) => { const d = v ? new Date(v) : null; if (!d || isNaN(d)) return 'no date'; return DOW[d.getDay()] + ' ' + d.getDate() + ' ' + MON[d.getMonth()] + ', ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); };
|
|
34
|
+
const nameOfRel = (V, r) => r.name || (r.initiative_ids.length ? r.initiative_ids.map((id) => titleOf(V, id)).join(' and ') : r.id);
|
|
35
|
+
|
|
36
|
+
function scopeIds(ctx) { return (ctx.rows || []).map((r) => r.id).filter(Boolean); }
|
|
37
|
+
const shown = (V, r) => !r.initiative_ids.length || r.initiative_ids.some((id) => V.ids.includes(id));
|
|
38
|
+
const titleOf = (V, id) => { const r = (V.ctx.rows || []).find((x) => x.id === id); return (r && r.title) || id; };
|
|
39
|
+
|
|
40
|
+
function summaryEditor(V, r) {
|
|
41
|
+
const lines = (r.summary && r.summary.length ? r.summary : [{ text: '', source: 'estimate' }]).map((l) => ({ ...l }));
|
|
42
|
+
const box = el('div', { class: 'c3rel', 'data-rel-summary': r.id });
|
|
43
|
+
const note = el('div', { class: 'fold w', 'data-rel-refused': '' });
|
|
44
|
+
note.hidden = true;
|
|
45
|
+
const draw = (refused = []) => {
|
|
46
|
+
box.replaceChildren(...lines.map((l, i) => {
|
|
47
|
+
const input = el('input', { type: 'text', value: l.text || '', 'aria-label': 'Line ' + (i + 1), autocomplete: 'off' });
|
|
48
|
+
input.addEventListener('change', () => { l.text = input.value; });
|
|
49
|
+
const sel = el('select', { 'aria-label': 'Source of line ' + (i + 1) },
|
|
50
|
+
[{ value: '', label: 'Source needed' }, ...SOURCES.map((s) => ({ value: s, label: s })), ...(l.source && !SOURCES.includes(l.source) ? [{ value: l.source, label: l.source }] : [])].map((o) => el('option', { value: o.value }, o.label)));
|
|
51
|
+
sel.value = l.source || '';
|
|
52
|
+
sel.addEventListener('change', () => { l.source = sel.value || undefined; });
|
|
53
|
+
return el('div', { class: 'form row', 'data-rel-line': i, 'data-refused': refused.includes(i) ? 'true' : null }, input, sel);
|
|
54
|
+
}));
|
|
55
|
+
note.hidden = !refused.length;
|
|
56
|
+
if (refused.length) note.replaceChildren(c3ic('alert', 'sm'), 'Not saved. Line ' + refused.map((i) => i + 1).join(', ') + ' needs a source: fact, estimate, assumption, reported or a ticket key.');
|
|
57
|
+
};
|
|
58
|
+
draw();
|
|
59
|
+
const save = async () => {
|
|
60
|
+
const a = await pmPost('/api/releases/summary', { id: r.id, lines });
|
|
61
|
+
if (a && a.ok) { await load(V); return; }
|
|
62
|
+
draw(a && a.refused ? a.refused : []);
|
|
63
|
+
if (!(a && a.refused)) { note.hidden = false; note.replaceChildren(c3ic('alert', 'sm'), (a && a.error) || 'Not saved.'); }
|
|
64
|
+
};
|
|
65
|
+
return el('div', { class: 'c3rel' },
|
|
66
|
+
el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'Summary: each line cites a source'), box, note,
|
|
67
|
+
el('div', { class: 'chips2' },
|
|
68
|
+
el('button', { type: 'button', class: 'pill', onClick: () => { lines.push({ text: '', source: 'estimate' }); draw(); } }, c3ic('plus', 'sm'), 'Add line'),
|
|
69
|
+
el('button', { type: 'button', class: 'pill', 'data-rel-save-summary': r.id, onClick: save }, c3ic('check', 'sm'), 'Save summary')));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The release's case file (the mockup's C3.cases.rl): what the release is, the tag, the gaps, the summary, and the presses. */
|
|
73
|
+
function caseFile(V, r) {
|
|
74
|
+
const flags = r.gapFlags || [];
|
|
75
|
+
const t = r.tags || { detected: [], reason: 'board not set' };
|
|
76
|
+
return el('div', { class: 'case', style: '--tc:var(--acc)' },
|
|
77
|
+
el('p', { class: 'sit' }, 'Deploy ' + when(r.deploy_at) + '. Release ' + when(r.release_at) + '. Initiatives: ' + (r.initiative_ids.map((id) => titleOf(V, id)).join(', ') || 'none') + '.'),
|
|
78
|
+
el('div', { class: 'c3rel' },
|
|
79
|
+
el('div', { class: 'chips2', 'data-rel-tags': r.id }, dim('Jira tag'), t.detected.length ? t.detected.map((x) => el('span', { class: 'tag', title: 'Jira release tag' }, x)) : dim(t.reason)),
|
|
80
|
+
el('div', { 'data-rel-flags': r.id }, flags.length
|
|
81
|
+
? el('ul', { class: 'chgl' }, flags.map((f) => el('li', { class: 'warn' }, c3ic('alert', 'sm'), el('span', null, (FLAG_TEXT[f.flag] || f.flag) + ': ' + f.ticket), el('span', { class: 'tag' }, 'gap'))))
|
|
82
|
+
: el('div', { class: 'fold' }, c3ic('check', 'sm'), 'No gaps between tickets and tags.')),
|
|
83
|
+
summaryEditor(V, r),
|
|
84
|
+
el('div', { class: 'chips2' },
|
|
85
|
+
el('button', { type: 'button', class: 'pill', 'data-rel-accept': r.id,
|
|
86
|
+
onClick: async () => { await pmPost('/api/releases/edit', { id: r.id, fields: { accepted: !r.accepted_at } }); await load(V); } }, c3ic(r.accepted_at ? 'undo' : 'check', 'sm'), r.accepted_at ? 'Withdraw acceptance' : 'Record acceptance'),
|
|
87
|
+
!r.initiative_ids.length ? el('button', { type: 'button', class: 'pill', 'data-rel-delete': r.id,
|
|
88
|
+
onClick: async () => { await pmPost('/api/releases/delete', { id: r.id }); await load(V); } }, c3ic('trash', 'sm'), 'Delete') : null)),
|
|
89
|
+
el('div', { class: 'cft' }, el('span', null, c3ic('shield', 'sm'), 'A release never creates a Jira tag. The PM’s acceptance comes first, and that is recorded, not acted on.')));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toggler(V, id, li, xp) {
|
|
93
|
+
const flip = () => { V.open[id] = !V.open[id]; li.setAttribute('aria-expanded', String(!!V.open[id])); xp.classList.toggle('open', !!V.open[id]); };
|
|
94
|
+
li.addEventListener('click', (e) => { if (!e.target.closest('.xi')) flip(); });
|
|
95
|
+
li.addEventListener('keydown', (e) => { if (e.target === li && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); flip(); } });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function relRow(V, r) {
|
|
99
|
+
const flags = r.gapFlags || [];
|
|
100
|
+
const t = r.tags || { detected: [] };
|
|
101
|
+
const op = !!V.open[r.id];
|
|
102
|
+
const li = el('li', { class: 'nrow', 'data-row': '', 'data-rel': r.id, tabindex: '0', 'aria-expanded': String(op) });
|
|
103
|
+
const xp = el('div', { class: 'xp' + (op ? ' open' : '') }, el('div', { class: 'xi' }, caseFile(V, r)));
|
|
104
|
+
toggler(V, r.id, li, xp);
|
|
105
|
+
li.append(
|
|
106
|
+
el('span', { class: 'ki', style: r.accepted_at ? 'color:var(--success)' : null }, c3ic(r.accepted_at ? 'check' : 'out')),
|
|
107
|
+
el('div', { class: 'nb' },
|
|
108
|
+
el('span', { class: 'tt' }, nameOfRel(V, r)),
|
|
109
|
+
el('div', { class: 'chips' },
|
|
110
|
+
r.initiative_ids.map((id) => el('span', { class: 'chip2' }, c3ic('init', 'sm'), titleOf(V, id))),
|
|
111
|
+
(t.detected || []).map((x) => el('span', { class: 'tag', title: 'Jira release tag' }, x)),
|
|
112
|
+
el('span', { class: 'chip2' }, c3ic('cal', 'sm'), dfmt(r.release_at)),
|
|
113
|
+
r.accepted_at ? el('span', { class: 'st ok' }, c3ic('check', 'sm'), 'accepted by the PM') : (t.detected || []).length ? null : el('span', { class: 'st warn' }, c3ic('alert', 'sm'), 'not accepted: no Jira tag yet'),
|
|
114
|
+
flags.length ? el('span', { class: 'st warn' }, c3ic('alert', 'sm'), flags.length + ' gap' + (flags.length === 1 ? '' : 's')) : null,
|
|
115
|
+
r.progress && r.progress.total ? [el('div', { class: 'pbar', style: 'width:70px' }, el('i', { style: 'width:' + Math.round(r.progress.done / r.progress.total * 100) + '%' })), el('span', { class: 'mono dim', style: 'font-size:var(--fs-2)' }, r.progress.done + ' of ' + r.progress.total)] : null),
|
|
116
|
+
xp));
|
|
117
|
+
return li;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The mockup's "New release" row and its wizard (c3wizard, line 4903) as a form: initiatives, type, deploy and release times. */
|
|
121
|
+
function newRow(V) {
|
|
122
|
+
const F = V.form;
|
|
123
|
+
const op = !!V.open.__new;
|
|
124
|
+
const li = el('li', { class: 'nrow', 'data-row': '', tabindex: '0', 'aria-expanded': String(op), 'data-rel-new': '' });
|
|
125
|
+
const err = el('div', { class: 'fold w', 'data-rel-error': '' });
|
|
126
|
+
err.hidden = true;
|
|
127
|
+
const pickRow = el('div', { class: 'chips2', role: 'group', 'aria-label': 'Initiatives' }, dim('initiatives'), V.ctx.rows.map((r) => {
|
|
128
|
+
const b = el('button', { type: 'button', class: 'pill', 'aria-pressed': String(F.picked.has(r.id)), 'data-rel-pick': r.id, onClick: () => { F.picked.has(r.id) ? F.picked.delete(r.id) : F.picked.add(r.id); b.setAttribute('aria-pressed', String(F.picked.has(r.id))); } }, r.title || r.id);
|
|
129
|
+
return b;
|
|
130
|
+
}));
|
|
131
|
+
const typeRow = el('div', { class: 'chips2', role: 'group', 'aria-label': 'Type' }, dim('type'), TYPES.map((t) => {
|
|
132
|
+
const b = el('button', { type: 'button', class: 'pill', 'aria-pressed': String(F.type === t.value), 'data-rel-type': t.value, onClick: () => { F.type = t.value; typeRow.querySelectorAll('button').forEach((x) => x.setAttribute('aria-pressed', String(x === b))); } }, t.label);
|
|
133
|
+
return b;
|
|
134
|
+
}));
|
|
135
|
+
const dt = (label, key, note) => {
|
|
136
|
+
const d = el('input', { type: 'date', value: F[key][0], 'aria-label': label + ' date' }); d.addEventListener('change', () => { F[key][0] = d.value; });
|
|
137
|
+
const t = el('input', { type: 'time', value: F[key][1], 'aria-label': label + ' time' }); t.addEventListener('change', () => { F[key][1] = t.value; });
|
|
138
|
+
return el('label', { class: 'c3dt' }, el('span', { class: 'dim mono' }, label), d, t, el('span', { class: 'dim mono' }, note));
|
|
139
|
+
};
|
|
140
|
+
const create = el('button', { type: 'button', class: 'pill', 'data-rel-create-go': '', onClick: async () => {
|
|
141
|
+
const a = await pmPost('/api/releases/create', { initiative_ids: [...F.picked], type: F.type, deploy_at: iso(F.deploy[0], F.deploy[1]), release_at: iso(F.rel[0], F.rel[1]) });
|
|
142
|
+
if (a && a.ok) { V.form = newForm(); V.open.__new = false; await load(V); } else { err.hidden = false; err.replaceChildren(c3ic('alert', 'sm'), (a && a.error) || 'Not created.'); }
|
|
143
|
+
} }, c3ic('plus', 'sm'), 'Create release');
|
|
144
|
+
const xp = el('div', { class: 'xp' + (op ? ' open' : '') }, el('div', { class: 'xi' }, el('div', { class: 'case', 'data-rel-create': '', style: '--tc:var(--acc)' },
|
|
145
|
+
el('p', { class: 'sit' }, 'Which initiatives ship together? Pick one, or several. The release is recorded here only; nothing changes in Jira.'),
|
|
146
|
+
el('div', { class: 'c3rel' }, pickRow, typeRow, dt('Deploy', 'deploy', 'engineering ships it'), dt('Release', 'rel', 'customers see it'), err, el('div', { class: 'chips2' }, create)))));
|
|
147
|
+
toggler(V, '__new', li, xp);
|
|
148
|
+
li.append(el('span', { class: 'ki', style: 'background:var(--accw);color:var(--acc)' }, c3ic('plus')),
|
|
149
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, 'New release'), el('div', { class: 'chips' }, dim('pick initiatives, a type and the deploy and release times')), xp));
|
|
150
|
+
return li;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const newForm = () => ({ picked: new Set(), type: 'deploy', deploy: ['', ''], rel: ['', ''] });
|
|
154
|
+
|
|
155
|
+
/** The mockup's Releases list (c3relList): the New release row and one row per release. */
|
|
156
|
+
function listCard(V) {
|
|
157
|
+
const list = V.releases.filter((r) => shown(V, r));
|
|
158
|
+
const rows = [newRow(V), ...(list.length ? list.map((r) => relRow(V, r)) : [el('li', { class: 'fold', 'data-rel-empty': '' }, c3ic('check', 'sm'), 'No releases yet.')])];
|
|
159
|
+
return el('section', { class: 'card sect', 'data-rel-list': '' },
|
|
160
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('out', 'sm'), 'Releases ', el('span', { class: 'bdg g' }, String(list.length))), el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'tied to initiatives and a Jira tag')),
|
|
161
|
+
el('ul', { class: 'list' }, rows));
|
|
162
|
+
}
|
|
163
|
+
const TAG_COLORS = ['var(--acc)', 'var(--success)', 'var(--purple)', 'var(--warning)'];
|
|
164
|
+
/** The mockup's "Jira release tags: tickets per tag". The ticket counts come from `tag_tickets` where the server sends them (the demo does); otherwise the tags
|
|
165
|
+
* the releases carry are listed with no count, and with no board set the card says so. Nothing is counted here. */
|
|
166
|
+
function tagsCard(V) {
|
|
167
|
+
const tt = V.tagTickets;
|
|
168
|
+
let body;
|
|
169
|
+
if (tt && tt.length) body = hBars(tt.map((v, i) => [v.tag, v.total ? v.done / v.total || 0.03 : 0.03, TAG_COLORS[i % 4], v.done + ' of ' + v.total + ' done' + (v.date ? ' · ' + v.date : '')]));
|
|
170
|
+
else {
|
|
171
|
+
const tags = [...new Set(V.releases.flatMap((r) => (r.tags && r.tags.detected) || []))];
|
|
172
|
+
body = tags.length ? el('div', { class: 'chips2', 'data-rel-tag-list': '' }, tags.map((t) => el('span', { class: 'tag', title: 'Jira release tag' }, t)))
|
|
173
|
+
: el('div', { class: 'fold', 'data-rel-tags-empty': '' }, c3ic('moon', 'sm'), V.board ? 'No release tag is detected on the board yet.' : 'Jira board: not set, so no release tag is read.');
|
|
174
|
+
}
|
|
175
|
+
const s = card('out', 'Jira release tags', 'tickets per tag', body, el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, tt && tt.length ? 'Read from Jira. Fact.' : 'Ticket counts per tag are not read here.'));
|
|
176
|
+
s.setAttribute('data-rel-tags-card', '');
|
|
177
|
+
return s;
|
|
178
|
+
}
|
|
179
|
+
function rulesCard(V) {
|
|
180
|
+
const demo = V.rules;
|
|
181
|
+
const rows = demo
|
|
182
|
+
? [['clock', 'Change freeze: ' + demo.freeze, 'example'], ['doc', 'Tag name: ' + demo.tag, 'example'], ['shield', 'Acceptance before a tag is created: not decided', 'open']]
|
|
183
|
+
: [['clock', 'Change freeze: no rule is set, so none is checked', 'open'], ['doc', 'Tag name: no rule is set, and no tag is created from here', 'open'], ['shield', 'Acceptance before a tag is created: the PM’s acceptance is recorded, and no tag is created from here', 'recorded']];
|
|
184
|
+
const s = card('lock', 'Open rules', demo ? 'example values, yours to set' : 'yours to set', el('ul', { class: 'chgl' }, rows.map((r) => el('li', { class: 'warn' }, c3ic(r[0], 'sm'), el('span', null, r[1]), el('span', { class: 'tag' }, r[2])))));
|
|
185
|
+
s.setAttribute('data-rel-rules', '');
|
|
186
|
+
return s;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function render(V) {
|
|
190
|
+
if (V.redraw) { V.redraw(); return; }
|
|
191
|
+
const list = V.releases.filter((r) => shown(V, r));
|
|
192
|
+
const accepted = list.filter((r) => r.accepted_at).length;
|
|
193
|
+
const head = el('div', { class: 'lh' },
|
|
194
|
+
el('span', { class: 'ki' }, c3ic('out')),
|
|
195
|
+
el('div', { style: 'min-width:0' },
|
|
196
|
+
el('b', null, list.length + ' release' + (list.length === 1 ? '' : 's') + ', ' + accepted + ' accepted by the PM'),
|
|
197
|
+
el('div', { class: 'dim', 'data-rel-board': '' }, V.board ? 'Jira board: ' + V.board.name : 'Jira board: not set')),
|
|
198
|
+
c3ring(accepted, Math.max(1, list.length), 44, accepted + '/' + list.length));
|
|
199
|
+
V.root.replaceChildren(el('div', { class: 'calwrap' }, el('div', { class: 'calbody' },
|
|
200
|
+
head,
|
|
201
|
+
V.error ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), V.error) : null,
|
|
202
|
+
el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, listCard(V)), el('div', { class: 'bcol r' }, tagsCard(V), rulesCard(V)))))));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function load(V) {
|
|
206
|
+
const fx = window.__c3demo && window.__c3planfx && window.__c3planfx.releases;
|
|
207
|
+
const a = fx ? JSON.parse(JSON.stringify(fx)) : await pmGet('/api/releases');
|
|
208
|
+
if (V.disposed) return;
|
|
209
|
+
if (a && a.ok) { V.releases = a.releases || []; V.board = a.board || null; V.tagTickets = a.tag_tickets || null; V.rules = fx ? fx.rules : null; V.error = null; V.ready = true; } else V.error = (a && a.error) || 'Releases could not be read.';
|
|
210
|
+
render(V);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The Lifecycle Deliver stage: the same list, tags and rules, drawn inside that lens (c3-lifecycle-panels.js). `host` is the lens's V; `scope` an initiative id or null.
|
|
214
|
+
* The state lives on host.__rel, so a write (create, summary, acceptance) reloads and asks the lens to draw again. */
|
|
215
|
+
export function releasePanels(host, scope) {
|
|
216
|
+
let V = host.__rel;
|
|
217
|
+
if (!V) {
|
|
218
|
+
V = host.__rel = { ctx: host.ctx, ids: scopeIds(host.ctx), releases: [], board: null, tagTickets: null, rules: null, error: null, ready: false, disposed: false, open: {}, form: newForm(), redraw: () => { if (!host.disposed && host.render) host.render(); } };
|
|
219
|
+
load(V);
|
|
220
|
+
}
|
|
221
|
+
V.ctx = host.ctx; V.ids = scope ? [scope] : scopeIds(host.ctx);
|
|
222
|
+
if (!V.ready && !V.error) return { left: [el('section', { class: 'card sect' }, el('div', { class: 'fold', 'data-rel-loading': '' }, c3ic('clock', 'sm'), 'Reading the releases…'))], right: [] };
|
|
223
|
+
return { left: [listCard(V)], right: [tagsCard(V), rulesCard(V)] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function mountReleases(host, ctx) {
|
|
227
|
+
let V = views.get(host);
|
|
228
|
+
if (V && V.ctx.ws !== ctx.ws) { disposeReleases(host); V = null; }
|
|
229
|
+
if (V) { V.ctx = ctx; V.ids = scopeIds(ctx); if (host.firstChild !== V.root) host.replaceChildren(V.root); return; }
|
|
230
|
+
ensureComponentsCss();
|
|
231
|
+
ensureC3Css();
|
|
232
|
+
V = { ctx, ids: scopeIds(ctx), root: el('div', { class: 'c3', 'data-in-view': 'releases' }), releases: [], board: null, error: null, disposed: false, open: {}, form: newForm() };
|
|
233
|
+
V.stopPhone = c3Phone(V.root);
|
|
234
|
+
views.set(host, V);
|
|
235
|
+
host.replaceChildren(V.root);
|
|
236
|
+
load(V);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function disposeReleases(host) {
|
|
240
|
+
const V = views.get(host);
|
|
241
|
+
if (!V) return;
|
|
242
|
+
V.disposed = true;
|
|
243
|
+
if (V.stopPhone) V.stopPhone();
|
|
244
|
+
views.delete(host);
|
|
245
|
+
if (V.root.parentNode === host) V.root.remove();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
registerInitiativesView('releases', { label: 'Releases', mount: mountReleases, dispose: disposeReleases });
|