@bongos/core 1.21.38 → 1.21.40
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/.bongos-core.json +33 -33
- package/docs/copy-inventory.md +5 -7
- package/docs/copy-registry.json +7 -25
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +14 -2
- package/docs/page-readings.json +387 -348
- package/modules/government/docket.js +14 -0
- package/modules/hall-ui/public/street-card.css +21 -2
- package/modules/hall-ui/public/street-card.js +198 -64
- package/modules/hall-ui/public/street-card.states.json +495 -7
- package/modules/hall-ui/public/street-desk.js +47 -2
- package/modules/hall-ui/public/street-inside.js +3 -13
- package/modules/hall-ui/public/street.css +3 -9
- package/modules/hall-ui/public/street.html +0 -5
- package/modules/hall-ui/public/street.js +44 -32
- package/modules/hall-ui/records/governor-street.md +2 -2
- package/modules/ideas/governor-docket.js +2 -0
- package/modules/lifecycle/governor-docket.js +4 -0
- package/modules/onboarding/routes/access-requests.js +2 -0
- package/modules/security/routes/reports.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/module-rechecks.js +12 -1
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +20 -0
- package/tests/hall_matter_card.mjs +143 -18
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_street_inside.mjs +18 -16
|
@@ -183,9 +183,23 @@ function normalizeMatter(raw, { source, building }) {
|
|
|
183
183
|
href: raw.href ? String(raw.href) : null,
|
|
184
184
|
ref: raw.ref && typeof raw.ref === 'object' ? { type: String(raw.ref.type), id: String(raw.ref.id) } : null,
|
|
185
185
|
...actsField(raw.acts),
|
|
186
|
+
...detailField(raw.detail),
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
// THE CARD'S "MORE" (task 1004566). A matter may carry `detail`: the full text
|
|
191
|
+
// its own list route already shows the same viewer (a blocker's body, an
|
|
192
|
+
// override's reason, a report's description), so the street's card can open it
|
|
193
|
+
// in place instead of linking to that route's page. The contributor reads it
|
|
194
|
+
// under the SAME gate as its matters, so it is never more than that route would
|
|
195
|
+
// answer. Bounded; absent when empty.
|
|
196
|
+
const MAX_DETAIL = 4000;
|
|
197
|
+
function detailField(raw) {
|
|
198
|
+
if (typeof raw !== 'string') return {};
|
|
199
|
+
const d = raw.trim();
|
|
200
|
+
return d ? { detail: d.slice(0, MAX_DETAIL) } : {};
|
|
201
|
+
}
|
|
202
|
+
|
|
189
203
|
// WHAT A VIEWER MAY DO, FROM THE SERVER (task 1004559). A matter or a desk row may
|
|
190
204
|
// carry `acts`: the acts THIS viewer may take on it, decided by the owning module
|
|
191
205
|
// from ctx.has() with the same atom its own write route checks. The floor desk
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
/* the full card holds more than the short one did: it may stand taller over
|
|
13
13
|
the street before it scrolls, so the acts are in view on a laptop */
|
|
14
14
|
.street-card { max-height: min(480px, calc(100vh - 120px)); }
|
|
15
|
+
/* "more" open (task 1004566): the same card grows in place and scrolls inside */
|
|
16
|
+
.street-card[data-more="open"] { max-height: min(640px, calc(100vh - 96px)); }
|
|
15
17
|
body[data-page="street"] {
|
|
16
18
|
/* an objecting seat: the terracotta, deepened so its white initial clears AA */
|
|
17
19
|
--mcard-against: color-mix(in srgb, var(--terracotta) 82%, #000000);
|
|
@@ -59,7 +61,24 @@ body[data-page="street"] {
|
|
|
59
61
|
.mcard-textbtn { align-self: flex-start; background: none; border: 0; padding: 6px 0; min-height: 24px; text-decoration: underline; text-underline-offset: 4px; font-family: var(--font-body); font-size: 14px; color: var(--ink-soft); cursor: pointer; text-align: left; }
|
|
60
62
|
.mcard-textbtn--accent { color: var(--street-accent-text); }
|
|
61
63
|
|
|
62
|
-
/*
|
|
64
|
+
/* an act that asks: its one-tap chips (the desk row's), or one GitHub name */
|
|
65
|
+
.mcard-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 12px; border-radius: 10px; background: var(--street-wash); }
|
|
66
|
+
.mcard-chips__ask { flex-basis: 100%; font-size: 12.5px; color: var(--ink-faint); }
|
|
67
|
+
.mcard-chip { height: 32px; padding: 0 14px; border-radius: 999px; font-family: var(--font-body); font-size: 13.5px; font-weight: 500; cursor: pointer; white-space: nowrap; text-transform: lowercase; background: var(--bg-card); border: 1px solid var(--street-rule-strong); color: var(--ink); }
|
|
68
|
+
.mcard-chip:hover { filter: brightness(1.06); }
|
|
69
|
+
.mcard-chip[disabled] { opacity: .55; cursor: progress; }
|
|
70
|
+
.mcard-chips__field { flex: 1 1 140px; min-width: 0; height: 32px; box-sizing: border-box; padding: 0 10px; font-family: var(--font-body); font-size: 14px; color: var(--ink); background: var(--bg); border: 1px solid var(--street-rule-strong); border-radius: 8px; }
|
|
71
|
+
|
|
72
|
+
/* more: the detail, in place */
|
|
73
|
+
.mcard-more__toggle { margin-top: -2px; }
|
|
74
|
+
.mcard-more { display: flex; flex-direction: column; gap: 6px; padding-top: 10px; border-top: 1px solid var(--street-rule); }
|
|
75
|
+
.mcard-more__text { margin: 0 0 4px; font-size: 14px; line-height: 1.5; white-space: pre-line; overflow-wrap: anywhere; }
|
|
76
|
+
.mcard-more__meta { margin: 0; font-size: 13px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
|
|
77
|
+
.mcard-more__history { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
|
|
78
|
+
.mcard-more__history li { padding: 6px 0; border-top: 1px solid var(--street-rule); font-size: 13.5px; line-height: 1.4; color: var(--ink-soft); overflow-wrap: anywhere; }
|
|
79
|
+
.mcard-more__queue { display: flex; flex-direction: column; align-items: flex-start; }
|
|
80
|
+
|
|
81
|
+
/* the revised words */
|
|
63
82
|
.mcard-form { display: flex; flex-direction: column; gap: 6px; }
|
|
64
83
|
.mcard-form__label { font-size: 12.5px; color: var(--ink-faint); }
|
|
65
84
|
.mcard-form__text, .mcard-form__select { font-family: var(--font-body); font-size: 14px; line-height: 1.45; color: var(--ink); background: var(--bg); border: 1px solid var(--street-rule-strong); border-radius: 10px; padding: 8px 10px; box-sizing: border-box; width: 100%; }
|
|
@@ -68,7 +87,7 @@ body[data-page="street"] {
|
|
|
68
87
|
.mcard-form .mcard-actions { margin-top: 4px; }
|
|
69
88
|
.mcard-error { margin: 0; font-size: 13.5px; line-height: 1.4; color: var(--street-accent-text); }
|
|
70
89
|
|
|
71
|
-
.mcard-btn:focus-visible, .mcard-textbtn:focus-visible, .mcard-form__text:focus-visible, .mcard-form__select:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
90
|
+
.mcard-btn:focus-visible, .mcard-textbtn:focus-visible, .mcard-form__text:focus-visible, .mcard-form__select:focus-visible, .mcard-chip:focus-visible, .mcard-chips__field:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
72
91
|
|
|
73
92
|
/* the toast: one short line over the street */
|
|
74
93
|
.street-toast { position: absolute; left: 50%; top: 192px; transform: translateX(-50%); max-width: calc(100% - 32px); box-sizing: border-box; font-size: 15px; line-height: 1.35; color: var(--ink); padding: 11px 20px; border-radius: 999px; background: var(--bg-card); border: 1px solid var(--street-rule-strong); z-index: 10; pointer-events: none; text-align: center; }
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
//
|
|
26
26
|
// WHERE THE REAL DATA BENDS THE DESIGN (said once here and in the ship notes):
|
|
27
27
|
// * Objection sections. The server keeps one closed vocabulary of sections an
|
|
28
|
-
// objection may name (board.js FULL_IDEA_SECTION_KEYS, a DB CHECK).
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
28
|
+
// objection may name (board.js FULL_IDEA_SECTION_KEYS, a DB CHECK). The card
|
|
29
|
+
// offers five of those keys as one-tap chips, the desk row's (GovDesk.CHIPS).
|
|
30
|
+
// A Full Idea is voted on here too: its sections open under "more", in front
|
|
31
|
+
// of the voter (ADR 0175 §8), so nobody is sent to the Board Room to vote.
|
|
32
32
|
// * Abstain. The ballot has yes and no and nothing else, so abstaining records
|
|
33
33
|
// nothing: the card says so and the matter still waits on you.
|
|
34
34
|
// * Counsel. There is no model behind it: the note is written from the sitting's
|
|
@@ -40,9 +40,17 @@
|
|
|
40
40
|
// its rows, a building's matter with its revised detail. The constitution's own
|
|
41
41
|
// fields are changed in the Board Room's amendment form, not here.
|
|
42
42
|
// * Release. An override and a PR held at the gate are released from the card
|
|
43
|
-
// through their own decide routes. A newer CORE
|
|
44
|
-
//
|
|
45
|
-
//
|
|
43
|
+
// through their own decide routes. A newer CORE keeps its check-then-take:
|
|
44
|
+
// the card draws the Release desk's core row (check, then take it), the same
|
|
45
|
+
// as the floor desk; only a hall without that row links to the pin's page.
|
|
46
|
+
//
|
|
47
|
+
// ONE ACTION MODEL (task 1004566). The card's buttons and chips are the desk
|
|
48
|
+
// row's for the same item: GovDesk.cardFor() (street-desk.js) puts that row on
|
|
49
|
+
// the model as `row`, and render() draws row.actions and row.choices, sending
|
|
50
|
+
// each act through request() with the row's own card. "more" opens the detail
|
|
51
|
+
// in place (moreOf): the full text, who and when, and the record. Nothing on
|
|
52
|
+
// the card links to an older page to decide; the one link left is a sign-off
|
|
53
|
+
// that needs a recording ('record it'), which only the goal's page can take.
|
|
46
54
|
|
|
47
55
|
(function (root) {
|
|
48
56
|
'use strict';
|
|
@@ -342,9 +350,12 @@
|
|
|
342
350
|
} else cv.result = 'You objected to ' + sectionWords(b.mine.section_key) + '. It is on the record.';
|
|
343
351
|
return cv;
|
|
344
352
|
}
|
|
345
|
-
|
|
353
|
+
// A Full Idea is voted on here too (task 1004566): its sections open under
|
|
354
|
+
// "more", in front of the voter. Its author cannot vote on it (the server's
|
|
355
|
+
// anti-self rule) and does not withdraw it from here.
|
|
356
|
+
if (kind === 'idea' && !b.youOpen) {
|
|
346
357
|
cv.readOnly = true;
|
|
347
|
-
cv.result =
|
|
358
|
+
cv.result = b.isAuthor ? 'Yours. It is with the sitting now.' : 'Not yours to vote on.';
|
|
348
359
|
return cv;
|
|
349
360
|
}
|
|
350
361
|
// task 1004564: the proposer may withdraw their own sitting until anyone has
|
|
@@ -358,10 +369,10 @@
|
|
|
358
369
|
return cv;
|
|
359
370
|
}
|
|
360
371
|
if (b.youOpen) {
|
|
372
|
+
// the reason for an objection is one tap: the desk row's chips (GovDesk.CHIPS)
|
|
361
373
|
cv.actions = [act('for', 'vote for', 'primary'), act('object', 'object', 'quiet')];
|
|
362
374
|
cv.textActions = [act('abstain', 'abstain', 'text')];
|
|
363
|
-
cv.
|
|
364
|
-
cv.result = '';
|
|
375
|
+
cv.result = kind === 'idea' ? 'Read its sections under more, then vote.' : '';
|
|
365
376
|
return cv;
|
|
366
377
|
}
|
|
367
378
|
cv.readOnly = true;
|
|
@@ -370,13 +381,16 @@
|
|
|
370
381
|
}
|
|
371
382
|
|
|
372
383
|
// ---------- the card for every other source ----------
|
|
373
|
-
|
|
384
|
+
// The one fact a source's card says (task 1004566). Its acts are the ones the
|
|
385
|
+
// server handed the matter (`acts`), drawn from the desk row for the same item
|
|
386
|
+
// (GovDesk.cardFor), so the card never sends anyone to another page to decide.
|
|
387
|
+
var SOURCE_WORDS = {
|
|
374
388
|
criteria_uat: 'Someone who did not build it tries it and signs it off.',
|
|
375
|
-
blockers: 'It holds work until someone decides.
|
|
376
|
-
removal_proposals: 'A proposal to take work out of a goal.
|
|
377
|
-
redteam_reports: 'A report on the project\'s own walls.
|
|
378
|
-
autobongos_fence: 'Whether work is built while no one is watching.
|
|
379
|
-
module_rechecks: 'A module waits for its
|
|
389
|
+
blockers: 'It holds work until someone decides.',
|
|
390
|
+
removal_proposals: 'A proposal to take work out of a goal.',
|
|
391
|
+
redteam_reports: 'A report on the project\'s own walls.',
|
|
392
|
+
autobongos_fence: 'Whether work is built while no one is watching.',
|
|
393
|
+
module_rechecks: 'A module version waits for its check.',
|
|
380
394
|
};
|
|
381
395
|
function joinGoalId(m) { var hit = /goal\/(\d+)/.exec(str(m.href)); return hit ? hit[1] : null; }
|
|
382
396
|
|
|
@@ -386,21 +400,20 @@
|
|
|
386
400
|
cv.subject = { kind: m.source, id: id, who: who, goalId: m.source === 'goal_join_requests' ? joinGoalId(m) : null };
|
|
387
401
|
var mine = m.state === 'needs';
|
|
388
402
|
if (m.source === 'access_requests') {
|
|
389
|
-
if (who) cv.
|
|
403
|
+
if (who) cv.asked = who + ' asked to join as a builder' + (/vouched/.test(str(m.where)) ? ', and someone vouched for them.' : '.');
|
|
390
404
|
if (!mine) { cv.readOnly = true; cv.result = 'Not yours to decide.'; return cv; }
|
|
391
405
|
cv.result = 'The call is yours. Fine by you lets them in; an objection keeps them out.';
|
|
392
406
|
cv.actions = [act('fine', 'fine by me', 'primary'), act('refuse', 'object', 'quiet')];
|
|
393
407
|
return cv;
|
|
394
408
|
}
|
|
395
409
|
if (m.source === 'goal_join_requests') {
|
|
396
|
-
if (who) cv.
|
|
397
|
-
if (!mine || !cv.subject.goalId) { cv.readOnly = true; cv.result = mine ? '
|
|
410
|
+
if (who) cv.asked = who + ' asked to join this goal\'s team.';
|
|
411
|
+
if (!mine || !cv.subject.goalId) { cv.readOnly = true; cv.result = mine ? 'The goal could not be named, so it cannot be decided here.' : 'You asked. The goal\'s owner decides.'; return cv; }
|
|
398
412
|
cv.result = 'The goal\'s team is yours to keep. Fine by you lets them in.';
|
|
399
413
|
cv.actions = [act('fine', 'fine by me', 'primary'), act('refuse', 'object', 'quiet')];
|
|
400
414
|
return cv;
|
|
401
415
|
}
|
|
402
416
|
if (m.source === 'override_requests' || m.source === 'gate_approvals') {
|
|
403
|
-
if (who) cv.quote = { by: '', words: 'Raised by ' + who + '.' };
|
|
404
417
|
if (!mine) { cv.readOnly = true; cv.result = 'Someone who may decide it will.'; return cv; }
|
|
405
418
|
cv.result = m.source === 'gate_approvals'
|
|
406
419
|
? 'It is held at the gate. Releasing it lets it merge and go live. The call is yours.'
|
|
@@ -410,15 +423,68 @@
|
|
|
410
423
|
}
|
|
411
424
|
if (m.source === 'core_update') {
|
|
412
425
|
if (!mine) { cv.readOnly = true; cv.result = 'Someone who may move the core will.'; return cv; }
|
|
413
|
-
|
|
426
|
+
// Its check-then-take is the Release desk's core row (over: core_update),
|
|
427
|
+
// which GovDesk.cardFor draws here. Only where no such row came (the hall
|
|
428
|
+
// runs without the provisioning module) is the pin moved on its own page.
|
|
429
|
+
cv.result = 'A newer core is out. Check it first; taking it moves the live pin.';
|
|
414
430
|
cv.actions = [act('release', 'release, with its check', 'primary', { go: safeHref(m.href) || '/settings#software-update' }), act('hold', 'hold', 'quiet')];
|
|
415
431
|
return cv;
|
|
416
432
|
}
|
|
417
|
-
|
|
418
|
-
cv.
|
|
433
|
+
// the server handed this viewer acts on it: the card draws the desk row's
|
|
434
|
+
cv.readOnly = !(Array.isArray(m.acts) && m.acts.length);
|
|
435
|
+
cv.result = SOURCE_WORDS[m.source] || (mine ? 'This waits on you.' : 'This is moving without you.');
|
|
419
436
|
return cv;
|
|
420
437
|
}
|
|
421
438
|
|
|
439
|
+
// ---------- more: the detail, in place (task 1004566) ----------
|
|
440
|
+
// What the old page showed, read from what the docket and the Board Room
|
|
441
|
+
// already hand this viewer: the full text, who and when, and the record. A
|
|
442
|
+
// Full Idea's sections are all here, so it is voted on with them in front of
|
|
443
|
+
// the voter (ADR 0175 §8) without leaving the street.
|
|
444
|
+
var IDEA_WORDS = {
|
|
445
|
+
purpose: 'what it is for', big_picture: 'the big picture', why_had: 'why we had it', why_matters: 'why it matters',
|
|
446
|
+
fit: 'how it fits the rest', scope: 'what it changes', build_context: 'what building it takes', success: 'how we will know it worked',
|
|
447
|
+
};
|
|
448
|
+
var MORE_TEXT = 4000;
|
|
449
|
+
var MORE_HISTORY = 8;
|
|
450
|
+
function dayOf(v) { var t = Date.parse(v); return isNaN(t) ? '' : new Date(t).toISOString().slice(0, 10); }
|
|
451
|
+
// the full text keeps its paragraphs and lines (clip() folds them); bounded
|
|
452
|
+
function bound(s) { return s.length > MORE_TEXT ? s.slice(0, MORE_TEXT - 1).trim() + '…' : s; }
|
|
453
|
+
function moreOf(cv, m, item) {
|
|
454
|
+
var text = [], when = [], history = [], who = '';
|
|
455
|
+
var name = m && m.proposer ? personName(m.proposer) : '';
|
|
456
|
+
if (item) {
|
|
457
|
+
var s = item.subject || {};
|
|
458
|
+
var k = subjectKind(item);
|
|
459
|
+
if (k === 'idea') {
|
|
460
|
+
var f = s.field_values || {};
|
|
461
|
+
Object.keys(f).forEach(function (key) { var w = str(f[key]).trim(); if (w) text.push({ label: IDEA_WORDS[key] || key.replace(/_/g, ' '), words: bound(w) }); });
|
|
462
|
+
} else {
|
|
463
|
+
var full = str(k === 'matter' ? s.detail_md : s.rationale_md).trim();
|
|
464
|
+
if (full) text.push({ label: '', words: bound(full) });
|
|
465
|
+
}
|
|
466
|
+
if (name) who = 'Put up by ' + name + '.';
|
|
467
|
+
if (dayOf(item.opened_at)) when.push('opened ' + dayOf(item.opened_at));
|
|
468
|
+
if (item.closed_at) when.push('closed ' + dayOf(item.closed_at));
|
|
469
|
+
else if (dayOf(item.closes_at)) when.push('closes ' + dayOf(item.closes_at));
|
|
470
|
+
if (cv.revision) history.push('an earlier sitting sent it back to be revised');
|
|
471
|
+
(item.votes || []).slice().sort(function (a, b) { return (Date.parse(a.cast_at) || 0) - (Date.parse(b.cast_at) || 0); }).forEach(function (v) {
|
|
472
|
+
var by = personName(v) || 'someone';
|
|
473
|
+
var said = v.direction === 'yes' ? by + ' voted for' : by + ' objected to ' + sectionWords(v.section_key) + (v.reason_md ? ': ' + clip(str(v.reason_md).replace(/\s+/g, ' ').trim(), 160) : '');
|
|
474
|
+
history.push([dayOf(v.cast_at), said].filter(Boolean).join(' · '));
|
|
475
|
+
});
|
|
476
|
+
} else if (m) {
|
|
477
|
+
if (cv.asked) text.push({ label: '', words: cv.asked });
|
|
478
|
+
if (m.detail) text.push({ label: '', words: bound(str(m.detail).trim()) });
|
|
479
|
+
if (name) who = 'Raised by ' + name + '.';
|
|
480
|
+
if (dayOf(m.opened_at)) when.push('opened ' + dayOf(m.opened_at));
|
|
481
|
+
if (dayOf(m.deadline)) when.push('closes ' + dayOf(m.deadline));
|
|
482
|
+
if (m.rule && RULE_WORDS[m.rule]) history.push('how it is decided: ' + RULE_WORDS[m.rule]);
|
|
483
|
+
}
|
|
484
|
+
if (!text.length && !who && !when.length && !history.length) return null;
|
|
485
|
+
return { text: text, who: who, when: when.join(' · '), history: history.slice(0, MORE_HISTORY) };
|
|
486
|
+
}
|
|
487
|
+
|
|
422
488
|
// The whole card for one matter. ctx: { docket, key, room, constitution, viewerId, now }.
|
|
423
489
|
// null for an unknown key. A board sitting the room has not been read for yet
|
|
424
490
|
// is the short card with `loading: true`.
|
|
@@ -430,7 +496,8 @@
|
|
|
430
496
|
var cv = {
|
|
431
497
|
key: short.key, building: short.building, where: short.where, when: short.when, title: short.title, href: short.href,
|
|
432
498
|
source: null, quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
|
|
433
|
-
result: short.result, actions: [], textActions: [],
|
|
499
|
+
result: short.result, actions: [], textActions: [], reviseForm: null, readOnly: false, loading: false, lines: short.lines || [], subject: null,
|
|
500
|
+
more: null, row: null,
|
|
434
501
|
};
|
|
435
502
|
if (short.key === 'queue') { cv.source = 'queue'; cv.readOnly = true; return cv; }
|
|
436
503
|
var m = findMatter(c.docket, c.key);
|
|
@@ -446,9 +513,13 @@
|
|
|
446
513
|
cv.result = c.room ? 'This sitting could not be read just now.' : 'Reading the sitting…';
|
|
447
514
|
return cv;
|
|
448
515
|
}
|
|
449
|
-
|
|
516
|
+
boardCard(cv, m, item, { docket: c.docket, room: c.room, constitution: c.constitution, viewerId: viewerId });
|
|
517
|
+
cv.more = moreOf(cv, m, item);
|
|
518
|
+
return cv;
|
|
450
519
|
}
|
|
451
|
-
|
|
520
|
+
sourceCard(cv, m);
|
|
521
|
+
cv.more = moreOf(cv, m, null);
|
|
522
|
+
return cv;
|
|
452
523
|
}
|
|
453
524
|
|
|
454
525
|
// ---------- the acts the server hands a row (task 1004559) ----------
|
|
@@ -461,7 +532,7 @@
|
|
|
461
532
|
return {
|
|
462
533
|
key: str(x.key), building: x.building || null, where: '', when: '', title: str(x.title), href: safeHref(x.href),
|
|
463
534
|
source: str(x.kind), quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
|
|
464
|
-
result: '', actions: [], textActions: [],
|
|
535
|
+
result: '', actions: [], textActions: [], reviseForm: null, readOnly: false, loading: false, lines: [], more: null, row: null,
|
|
465
536
|
subject: { kind: str(x.kind), id: str(x.id), who: str(x.who) },
|
|
466
537
|
};
|
|
467
538
|
}
|
|
@@ -521,6 +592,15 @@
|
|
|
521
592
|
module_off: function (id) { return post('/modules/' + id + '/enable', { enabled: false }); },
|
|
522
593
|
};
|
|
523
594
|
ACT_ROUTES.backup = { backup: function () { return post('/backup/trigger', {}); } };
|
|
595
|
+
// A module version flagged by its security check is checked again (task
|
|
596
|
+
// 1004566): its ref id is "<module key>@<version>", the reassess route's two
|
|
597
|
+
// path parts; a reason is one tap.
|
|
598
|
+
ACT_ROUTES.module_rechecks = {
|
|
599
|
+
recheck: function (id, inp) {
|
|
600
|
+
var hit = /^([a-z0-9-]+)@(\d+\.\d+\.\d+)$/.exec(decoded(id));
|
|
601
|
+
return hit ? post('/store/modules/' + hit[1] + '/versions/' + hit[2] + '/reassess', { reason: 'Asked on the street: ' + (str(inp.label) || 'look again') + '.' }) : null;
|
|
602
|
+
},
|
|
603
|
+
};
|
|
524
604
|
ACT_ROUTES.invite = { invite: function (id, inp) { return str(inp.option).trim() ? post('/access-requests/invite', { github_login: str(inp.option).trim() }) : null; } };
|
|
525
605
|
ACT_ROUTES.autonomy_fence = ACT_ROUTES.autobongos_fence;
|
|
526
606
|
// The Planning desks (task 1004560). A blocker is decided by one of its chips,
|
|
@@ -667,8 +747,9 @@
|
|
|
667
747
|
rot: 'Rot timer set.', module_on: 'Turned on.', module_off: 'Turned off.', backup: 'Backing up now.', invite: '{who} is invited.',
|
|
668
748
|
decide: 'Decided. The work it held moves again.', ratify: 'Ratified. The promise is on its version.', ratify_goal: 'Ratified. It is a goal now.',
|
|
669
749
|
budget: '{who} has a new monthly limit.', unsign: 'Undone. The promise is open again.',
|
|
750
|
+
recheck: 'Checking it again. The answer lands at Release when it is done.',
|
|
670
751
|
};
|
|
671
|
-
var ACT_KINDS = { budget: 'light', unsign: 'settle', decide: 'rise', ratify: 'rise', ratify_goal: 'rise', check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
|
|
752
|
+
var ACT_KINDS = { recheck: 'light', budget: 'light', unsign: 'settle', decide: 'rise', ratify: 'rise', ratify_goal: 'rise', check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
|
|
672
753
|
|
|
673
754
|
// A refused act, in words. The server's own message wins when it has one.
|
|
674
755
|
var REFUSALS = {
|
|
@@ -720,8 +801,9 @@
|
|
|
720
801
|
return b;
|
|
721
802
|
}
|
|
722
803
|
|
|
723
|
-
// Build the card's body into `host` (emptied first). on: { act(key, input),
|
|
724
|
-
// ui: { form: '
|
|
804
|
+
// Build the card's body into `host` (emptied first). on: { act(key, input),
|
|
805
|
+
// openForm(name), ask(key), more() }. ui: { form: 'revise'|null, ask: actKey|null,
|
|
806
|
+
// more: false, error: '', busy: false, fresh: seatKey|null }.
|
|
725
807
|
function render(doc, host, cv, on, ui) {
|
|
726
808
|
var u = ui || {};
|
|
727
809
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
@@ -785,20 +867,10 @@
|
|
|
785
867
|
host.appendChild(co);
|
|
786
868
|
}
|
|
787
869
|
if (cv.result) host.appendChild(el(doc, 'p', 'street-card__result', cv.result));
|
|
788
|
-
if (u.form === '
|
|
789
|
-
else if (u.form === 'revise' && cv.reviseForm) host.appendChild(reviseForm(doc, cv, on, u));
|
|
870
|
+
if (u.form === 'revise' && cv.reviseForm) host.appendChild(reviseForm(doc, cv, on, u));
|
|
790
871
|
else {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
cv.actions.forEach(function (a) {
|
|
794
|
-
var btn;
|
|
795
|
-
if (a.go) { btn = el(doc, 'a', 'mcard-btn mcard-btn--' + a.tone, a.label); btn.setAttribute('href', a.go); btn.setAttribute('data-act', a.key); }
|
|
796
|
-
else btn = button(doc, 'mcard-btn mcard-btn--' + a.tone, a.label, a.key, function () { if (a.key === 'object') on.openForm('object'); else on.act(a.key, {}); }, u.busy);
|
|
797
|
-
row.appendChild(btn);
|
|
798
|
-
});
|
|
799
|
-
host.appendChild(row);
|
|
800
|
-
}
|
|
801
|
-
if (cv.textActions.length) {
|
|
872
|
+
actionsInto(doc, host, cv, on, u);
|
|
873
|
+
if (cv.textActions.length && !cv.row) {
|
|
802
874
|
var tr = el(doc, 'div', 'mcard-actions mcard-actions--text');
|
|
803
875
|
cv.textActions.forEach(function (a) { tr.appendChild(button(doc, 'mcard-textbtn', a.label, a.key, function () { on.act(a.key, {}); }, u.busy)); });
|
|
804
876
|
host.appendChild(tr);
|
|
@@ -809,9 +881,90 @@
|
|
|
809
881
|
er.setAttribute('role', 'alert');
|
|
810
882
|
host.appendChild(er);
|
|
811
883
|
}
|
|
884
|
+
if (cv.more || cv.queue) moreInto(doc, host, cv, on, u);
|
|
812
885
|
return host;
|
|
813
886
|
}
|
|
814
887
|
|
|
888
|
+
// The acts (task 1004566): the desk row's buttons for this item, with its
|
|
889
|
+
// one-tap chips under the act that asks (a reason, an option, a GitHub name),
|
|
890
|
+
// or, for a card no row stands behind, the model's own. An act that asks
|
|
891
|
+
// opens its chips first; a chip sends the act with that chip's input.
|
|
892
|
+
function actionsInto(doc, host, cv, on, u) {
|
|
893
|
+
var r = cv.row;
|
|
894
|
+
var list = r ? r.actions : cv.actions;
|
|
895
|
+
if (!list.length) return;
|
|
896
|
+
var open = r ? (u.ask && r.choices[u.ask] ? u.ask : r.show || null) : null;
|
|
897
|
+
var row = el(doc, 'div', 'mcard-actions');
|
|
898
|
+
list.forEach(function (a) {
|
|
899
|
+
var btn;
|
|
900
|
+
if (a.go) { btn = el(doc, 'a', 'mcard-btn mcard-btn--' + a.tone, a.label); btn.setAttribute('href', a.go); btn.setAttribute('data-act', a.key); }
|
|
901
|
+
else {
|
|
902
|
+
btn = button(doc, 'mcard-btn mcard-btn--' + a.tone, a.label, a.key, function () { if (a.ask) on.ask(a.key); else on.act(a.key, {}); }, u.busy);
|
|
903
|
+
if (a.ask) btn.setAttribute('aria-expanded', open === a.key ? 'true' : 'false');
|
|
904
|
+
}
|
|
905
|
+
row.appendChild(btn);
|
|
906
|
+
});
|
|
907
|
+
host.appendChild(row);
|
|
908
|
+
if (cv.row && cv.textActions.length) {
|
|
909
|
+
var tr = el(doc, 'div', 'mcard-actions mcard-actions--text');
|
|
910
|
+
cv.textActions.forEach(function (a) { tr.appendChild(button(doc, 'mcard-textbtn', a.label, a.key, function () { on.act(a.key, {}); }, u.busy)); });
|
|
911
|
+
host.appendChild(tr);
|
|
912
|
+
}
|
|
913
|
+
var ch = open ? r.choices[open] : null;
|
|
914
|
+
if (!ch) return;
|
|
915
|
+
var box = el(doc, 'div', 'mcard-chips');
|
|
916
|
+
box.setAttribute('data-chips', open);
|
|
917
|
+
box.appendChild(el(doc, 'span', 'mcard-chips__ask', ch.ask));
|
|
918
|
+
if (ch.field) {
|
|
919
|
+
var input = el(doc, 'input', 'mcard-chips__field');
|
|
920
|
+
input.setAttribute('id', 'mcardField');
|
|
921
|
+
input.setAttribute('type', 'text');
|
|
922
|
+
input.setAttribute('aria-label', ch.ask);
|
|
923
|
+
input.setAttribute('maxlength', '39');
|
|
924
|
+
var send = function () { var v = input.value.trim(); if (v) on.act(open, { option: v, label: v }); };
|
|
925
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') send(); });
|
|
926
|
+
box.appendChild(input);
|
|
927
|
+
box.appendChild(button(doc, 'mcard-chip', 'send', 'field-send', send, u.busy));
|
|
928
|
+
} else {
|
|
929
|
+
ch.options.forEach(function (c) { box.appendChild(button(doc, 'mcard-chip', c.label, 'chip-' + c.key, function () { on.act(open, c.input); }, u.busy)); });
|
|
930
|
+
}
|
|
931
|
+
host.appendChild(box);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// "more": the card grows in place with the detail (task 1004566). The toggle
|
|
935
|
+
// says whether it is open (aria-expanded); the queue's other pieces of work
|
|
936
|
+
// are listed there, each opening in this same card.
|
|
937
|
+
function moreInto(doc, host, cv, on, u) {
|
|
938
|
+
var open = !!u.more;
|
|
939
|
+
var t = button(doc, 'mcard-textbtn mcard-more__toggle', open ? 'less' : 'more', 'more', function () { on.more(); }, false);
|
|
940
|
+
t.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
941
|
+
t.setAttribute('aria-controls', 'mcardMore');
|
|
942
|
+
host.appendChild(t);
|
|
943
|
+
if (!open) return;
|
|
944
|
+
var box = el(doc, 'div', 'mcard-more');
|
|
945
|
+
box.setAttribute('id', 'mcardMore');
|
|
946
|
+
var mo = cv.more || { text: [], who: '', when: '', history: [] };
|
|
947
|
+
mo.text.forEach(function (x) {
|
|
948
|
+
if (x.label) box.appendChild(label(doc, x.label));
|
|
949
|
+
box.appendChild(el(doc, 'p', 'mcard-more__text', x.words));
|
|
950
|
+
});
|
|
951
|
+
var meta = [mo.who, mo.when].filter(Boolean).join(' ');
|
|
952
|
+
if (meta) box.appendChild(el(doc, 'p', 'mcard-more__meta', meta));
|
|
953
|
+
if (mo.history.length) {
|
|
954
|
+
box.appendChild(label(doc, 'the record'));
|
|
955
|
+
var ul = el(doc, 'ul', 'mcard-more__history');
|
|
956
|
+
mo.history.forEach(function (h) { ul.appendChild(el(doc, 'li', '', h)); });
|
|
957
|
+
box.appendChild(ul);
|
|
958
|
+
}
|
|
959
|
+
if (cv.queue && cv.queue.others.length) {
|
|
960
|
+
box.appendChild(label(doc, 'also waiting'));
|
|
961
|
+
var ql = el(doc, 'div', 'mcard-more__queue');
|
|
962
|
+
cv.queue.others.forEach(function (o) { ql.appendChild(button(doc, 'mcard-textbtn', o.title, 'pick', function () { on.pick(o.key); }, false)); });
|
|
963
|
+
box.appendChild(ql);
|
|
964
|
+
}
|
|
965
|
+
host.appendChild(box);
|
|
966
|
+
}
|
|
967
|
+
|
|
815
968
|
function formShell(doc, name, labelText, field, fieldId) {
|
|
816
969
|
var f = el(doc, 'div', 'mcard-form');
|
|
817
970
|
f.setAttribute('data-form', name);
|
|
@@ -830,25 +983,6 @@
|
|
|
830
983
|
return ta;
|
|
831
984
|
}
|
|
832
985
|
|
|
833
|
-
function objectForm(doc, cv, on, u) {
|
|
834
|
-
var ta = textArea(doc, 'mcardReason', 3, '');
|
|
835
|
-
var f = formShell(doc, 'object', 'why you object (it goes on the record)', ta, 'mcardReason');
|
|
836
|
-
var sel = el(doc, 'select', 'mcard-form__select');
|
|
837
|
-
sel.setAttribute('id', 'mcardSection');
|
|
838
|
-
var none = el(doc, 'option', '', 'pick one');
|
|
839
|
-
none.setAttribute('value', '');
|
|
840
|
-
sel.appendChild(none);
|
|
841
|
-
cv.objectForm.sections.forEach(function (s) { var o = el(doc, 'option', '', s.label); o.setAttribute('value', s.key); sel.appendChild(o); });
|
|
842
|
-
var sl = el(doc, 'label', 'mcard-form__label', 'the part you object to');
|
|
843
|
-
sl.setAttribute('for', 'mcardSection');
|
|
844
|
-
f.appendChild(sl);
|
|
845
|
-
f.appendChild(sel);
|
|
846
|
-
var row = el(doc, 'div', 'mcard-actions');
|
|
847
|
-
row.appendChild(button(doc, 'mcard-btn mcard-btn--danger', 'put it on the record', 'object-send', function () { on.act('object', { reason: ta.value, section: sel.value }); }, u.busy));
|
|
848
|
-
row.appendChild(button(doc, 'mcard-btn mcard-btn--quiet', 'not now', 'form-close', function () { on.openForm(null); }, false));
|
|
849
|
-
f.appendChild(row);
|
|
850
|
-
return f;
|
|
851
|
-
}
|
|
852
986
|
|
|
853
987
|
function reviseForm(doc, cv, on, u) {
|
|
854
988
|
var ta = textArea(doc, 'mcardRevise', 4, cv.reviseForm.text || '');
|
|
@@ -862,7 +996,7 @@
|
|
|
862
996
|
|
|
863
997
|
var api = {
|
|
864
998
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
865
|
-
model: model, actsCard: actsCard, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
|
|
999
|
+
model: model, actsCard: actsCard, moreOf: moreOf, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
|
|
866
1000
|
memberWords: memberWords, windowWords: windowWords,
|
|
867
1001
|
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
868
1002
|
change: changeOf,
|