@bongos/core 1.21.12 → 1.21.14
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 +49 -24
- package/docs/copy-inventory.md +34 -34
- package/docs/copy-registry.json +38 -38
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -3
- package/docs/page-readings.json +88 -55
- package/modules/autonomy/fence.js +86 -1
- package/modules/hall-ui/public/board-room.js +9 -0
- package/modules/hall-ui/public/street-card.css +98 -0
- package/modules/hall-ui/public/street-card.js +697 -0
- package/modules/hall-ui/public/street-card.states.json +191 -0
- package/modules/hall-ui/public/street.html +5 -3
- package/modules/hall-ui/public/street.js +107 -17
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/autobongos-run.js +15 -3
- package/scripts/hall-preview/server.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/autobongos_host_access.mjs +176 -0
- package/tests/hall_matter_card.mjs +387 -0
- package/tests/hall_street.mjs +3 -2
- package/tests/hall_street_inside.mjs +1 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
// modules/hall-ui/public/street-card.js — THE MATTER CARD THAT ACTS (task
|
|
2
|
+
// 1004528, goal 1000125; R06 of the Governor City).
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS. The prototype's card() (docs/design/mocks/governor-city/
|
|
5
|
+
// Street.dc.html) over the real docket: where · when, the title, the proposer in
|
|
6
|
+
// their own words, the exact wording change (struck and inserted, plus the
|
|
7
|
+
// revision when a returned sitting came back), the seats filling with the tally
|
|
8
|
+
// and what it needs, the voices against (and who is for), a counsel note, a
|
|
9
|
+
// result line and up to two actions, with the quiet text actions under them
|
|
10
|
+
// (abstain, revise it this way and sit again). The street's pin opens it
|
|
11
|
+
// (street.js); GovStreet.cardFor() (street-draw.js) still gives the short card's
|
|
12
|
+
// where/when/title/href, and this file builds the rest.
|
|
13
|
+
//
|
|
14
|
+
// FOUR PARTS, all here so the street page loads one file for the card:
|
|
15
|
+
// model() pure: the card for one matter, from the docket matter and, for a
|
|
16
|
+
// board sitting, the Board Room's own read (GET /government/board/
|
|
17
|
+
// items) and the constitution in force (GET /government/constitution).
|
|
18
|
+
// request() pure: the act → the EXISTING route it calls, or null when the act
|
|
19
|
+
// records nothing (abstain, hold). Every request goes through that
|
|
20
|
+
// route's own permission checks; there is no other path.
|
|
21
|
+
// answer() pure: what the street says and does after an act succeeds — the
|
|
22
|
+
// toast, which building rises or lights, whether the card closes.
|
|
23
|
+
// render() DOM: the card's body as created nodes. No innerHTML: every docket
|
|
24
|
+
// and ballot string lands as text.
|
|
25
|
+
//
|
|
26
|
+
// WHERE THE REAL DATA BENDS THE DESIGN (said once here and in the ship notes):
|
|
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). A sitting
|
|
29
|
+
// that is not a Full Idea offers five of those keys in plain words ("what it
|
|
30
|
+
// changes", ...). A Full Idea is voted on in the Board Room, where its
|
|
31
|
+
// sections are in front of the voter (ADR 0175 §8) — the card links there.
|
|
32
|
+
// * Abstain. The ballot has yes and no and nothing else, so abstaining records
|
|
33
|
+
// nothing: the card says so and the matter still waits on you.
|
|
34
|
+
// * Counsel. There is no model behind it: the note is written from the sitting's
|
|
35
|
+
// own rule, tally and objections (what it needs, what an objection does under
|
|
36
|
+
// this rule, what would answer it), so it can never say something the ballot
|
|
37
|
+
// does not.
|
|
38
|
+
// * Revise and sit again. A returned sitting is re-filed through the route that
|
|
39
|
+
// filed it — an amendment with its revised words, a who-decides change with
|
|
40
|
+
// its rows, a building's matter with its revised detail. The constitution's own
|
|
41
|
+
// fields are changed in the Board Room's amendment form, not here.
|
|
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 is not: moving the live pin
|
|
44
|
+
// runs its dry-run check first, on its own page, so the card's release goes
|
|
45
|
+
// there.
|
|
46
|
+
|
|
47
|
+
(function (root) {
|
|
48
|
+
'use strict';
|
|
49
|
+
|
|
50
|
+
var API = '/api/bongos';
|
|
51
|
+
|
|
52
|
+
// The keys the server accepts for an objection, in plain words, for a sitting
|
|
53
|
+
// that has no sections of its own. Every key is in board.js
|
|
54
|
+
// FULL_IDEA_SECTION_KEYS (tests/hall_matter_card.mjs holds them inside it).
|
|
55
|
+
var SECTIONS = [
|
|
56
|
+
{ key: 'scope', label: 'what it changes' },
|
|
57
|
+
{ key: 'purpose', label: 'what it is for' },
|
|
58
|
+
{ key: 'why_matters', label: 'why it matters' },
|
|
59
|
+
{ key: 'fit', label: 'how it fits the rest' },
|
|
60
|
+
{ key: 'success', label: 'how we will know it worked' },
|
|
61
|
+
];
|
|
62
|
+
var SECTION_WORDS = {};
|
|
63
|
+
SECTIONS.forEach(function (s) { SECTION_WORDS[s.key] = s.label; });
|
|
64
|
+
|
|
65
|
+
var PASS_WORDS = {
|
|
66
|
+
first_ratifier: 'the first yes decides',
|
|
67
|
+
consent: 'passes unless someone objects',
|
|
68
|
+
majority: 'more than half must say yes',
|
|
69
|
+
unanimous: 'everyone has to agree',
|
|
70
|
+
};
|
|
71
|
+
var FIELD_WORDS = {
|
|
72
|
+
membership: 'who sits',
|
|
73
|
+
pass_rule: 'how it passes',
|
|
74
|
+
window_minutes: 'how long it sits',
|
|
75
|
+
close_early_on_full_turnout: 'closes once everyone has voted',
|
|
76
|
+
};
|
|
77
|
+
var RULE_WORDS = {
|
|
78
|
+
decide: 'decides alone', first: 'first yes wins', consent: 'passes unless someone objects',
|
|
79
|
+
majority: 'majority vote', unanimous: 'everyone agrees', signoff: 'signed off by someone who did not build it',
|
|
80
|
+
vacant: 'no one holds this yet', rule: 'settled by a rule',
|
|
81
|
+
};
|
|
82
|
+
var BUILDING_NAMES = {
|
|
83
|
+
charter: 'Charter', rules: 'Rules', disputes: 'Disputes', people: 'People', planning: 'Planning',
|
|
84
|
+
automation: 'Automation', review: 'Review', release: 'Release', treasury: 'Treasury',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ---------- small words ----------
|
|
88
|
+
function str(v) { return v == null ? '' : String(v); }
|
|
89
|
+
function personName(p) { return p ? str(p.display_name || p.name || p.github_login || p.login) : ''; }
|
|
90
|
+
function firstName(n) { return str(n).split(/\s+/)[0] || ''; }
|
|
91
|
+
function initial(n) { var c = str(n).replace(/^[^A-Za-z0-9]+/, '').charAt(0); return c ? c.toUpperCase() : '?'; }
|
|
92
|
+
function list(names) {
|
|
93
|
+
if (names.length <= 1) return names.join('');
|
|
94
|
+
return names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
|
|
95
|
+
}
|
|
96
|
+
function clip(s, n) {
|
|
97
|
+
var t = str(s).replace(/\s+/g, ' ').trim();
|
|
98
|
+
return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : t;
|
|
99
|
+
}
|
|
100
|
+
// the first paragraph of a markdown text, as words
|
|
101
|
+
function firstPara(md) {
|
|
102
|
+
var paras = str(md).split(/\r?\n\s*\r?\n/).map(function (p) { return p.replace(/^[#>*\-\s]+/gm, '').replace(/\s+/g, ' ').trim(); }).filter(Boolean);
|
|
103
|
+
return paras[0] || '';
|
|
104
|
+
}
|
|
105
|
+
function memberWords(m) {
|
|
106
|
+
var s = str(m);
|
|
107
|
+
var one = /^rank:([a-z][a-z0-9-]*)$/.exec(s);
|
|
108
|
+
if (one) return 'the ' + one[1] + ' rank';
|
|
109
|
+
var up = /^rank:([a-z][a-z0-9-]*)\+$/.exec(s);
|
|
110
|
+
if (up) return up[1] + ' and above';
|
|
111
|
+
var many = /^rank:([a-z0-9,-]+)$/.exec(s);
|
|
112
|
+
if (many) return list(many[1].split(',').filter(Boolean));
|
|
113
|
+
if (s === 'gate') return 'as it is decided today';
|
|
114
|
+
if (s === 'nobody') return 'no one yet';
|
|
115
|
+
return s;
|
|
116
|
+
}
|
|
117
|
+
function windowWords(w) {
|
|
118
|
+
if (w == null) return 'no clock';
|
|
119
|
+
var n = Number(w);
|
|
120
|
+
if (!isFinite(n)) return str(w);
|
|
121
|
+
if (n % 1440 === 0) return n / 1440 + (n === 1440 ? ' day' : ' days');
|
|
122
|
+
if (n % 60 === 0) return n / 60 + (n === 60 ? ' hour' : ' hours');
|
|
123
|
+
return n + ' minutes';
|
|
124
|
+
}
|
|
125
|
+
function fieldValue(k, v) {
|
|
126
|
+
if (k === 'membership') return memberWords(v);
|
|
127
|
+
if (k === 'pass_rule') return PASS_WORDS[v] || str(v);
|
|
128
|
+
if (k === 'window_minutes') return windowWords(v);
|
|
129
|
+
if (typeof v === 'boolean') return v ? 'yes' : 'no';
|
|
130
|
+
if (v == null) return 'none';
|
|
131
|
+
return typeof v === 'object' ? JSON.stringify(v) : str(v);
|
|
132
|
+
}
|
|
133
|
+
function safeHref(h) { return typeof h === 'string' && h.charAt(0) === '/' && h.charAt(1) !== '/' ? h : null; }
|
|
134
|
+
|
|
135
|
+
function findMatter(docket, key) {
|
|
136
|
+
var ms = docket && Array.isArray(docket.matters) ? docket.matters : [];
|
|
137
|
+
for (var i = 0; i < ms.length; i++) if (ms[i] && String(ms[i].id) === String(key)) return ms[i];
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function findItem(room, id) {
|
|
141
|
+
if (!room) return null;
|
|
142
|
+
var all = [].concat(room.open || [], room.closed || []);
|
|
143
|
+
for (var i = 0; i < all.length; i++) if (all[i] && String(all[i].id) === String(id)) return all[i];
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
// Which board item a docket matter is, or null when it is not a sitting.
|
|
147
|
+
function itemIdOf(m) {
|
|
148
|
+
if (!m || m.source !== 'board') return null;
|
|
149
|
+
if (m.ref && m.ref.type === 'board_item' && m.ref.id != null) return String(m.ref.id);
|
|
150
|
+
var hit = /^board:(.+)$/.exec(str(m.id));
|
|
151
|
+
return hit ? hit[1] : null;
|
|
152
|
+
}
|
|
153
|
+
function needsRoom(m) { return itemIdOf(m) != null; }
|
|
154
|
+
|
|
155
|
+
function areaOf(item) {
|
|
156
|
+
var t = str(item && item.subject_type);
|
|
157
|
+
var hit = /^([a-z]+)_matter$/.exec(t);
|
|
158
|
+
return hit ? hit[1] : null;
|
|
159
|
+
}
|
|
160
|
+
function subjectKind(item) {
|
|
161
|
+
if (!item) return null;
|
|
162
|
+
if (item.subject_type === 'full_idea') return 'idea';
|
|
163
|
+
if (item.subject_type === 'genesis_stage') return 'genesis';
|
|
164
|
+
if (areaOf(item)) return 'matter';
|
|
165
|
+
if (item.subject && item.subject.kind === 'decision_rules') return 'rules';
|
|
166
|
+
return 'amendment';
|
|
167
|
+
}
|
|
168
|
+
// The proposer's own words for a sitting.
|
|
169
|
+
function proposerWords(item) {
|
|
170
|
+
var s = item && item.subject;
|
|
171
|
+
if (!s) return '';
|
|
172
|
+
var k = subjectKind(item);
|
|
173
|
+
if (k === 'amendment' || k === 'rules') return firstPara(s.rationale_md);
|
|
174
|
+
if (k === 'matter') return firstPara(s.detail_md);
|
|
175
|
+
if (k === 'idea') {
|
|
176
|
+
var f = s.field_values || {};
|
|
177
|
+
return firstPara(f.purpose || f.why_had || f.scope || '');
|
|
178
|
+
}
|
|
179
|
+
return '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------- the wording change ----------
|
|
183
|
+
function decisionRow(docket, area) {
|
|
184
|
+
var bs = docket && Array.isArray(docket.buildings) ? docket.buildings : [];
|
|
185
|
+
for (var i = 0; i < bs.length; i++) if (bs[i] && bs[i].key === area) return bs[i].decision || null;
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function changeOf(item, docket, constitution) {
|
|
189
|
+
var k = subjectKind(item);
|
|
190
|
+
var s = item && item.subject;
|
|
191
|
+
if (!s) return null;
|
|
192
|
+
if (k === 'rules') {
|
|
193
|
+
var rows = (s.proposed && Array.isArray(s.proposed.decision_rules) ? s.proposed.decision_rules : []).map(function (r) {
|
|
194
|
+
var today = decisionRow(docket, r.area);
|
|
195
|
+
return {
|
|
196
|
+
label: BUILDING_NAMES[r.area] || str(r.area),
|
|
197
|
+
struck: today ? [today.who, today.rule_words].filter(Boolean).join(', ') : '',
|
|
198
|
+
inserted: [r.who || memberWords(r.decided_by), r.rule ? RULE_WORDS[r.rule] || r.rule : (r.decided_by === 'gate' ? 'as today' : '')].filter(Boolean).join(', '),
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
return rows.length ? { rows: rows } : null;
|
|
202
|
+
}
|
|
203
|
+
if (k === 'amendment') {
|
|
204
|
+
var live = constitution && constitution.board ? constitution.board : null;
|
|
205
|
+
var p = s.proposed || {};
|
|
206
|
+
var out = [];
|
|
207
|
+
Object.keys(p).forEach(function (f) {
|
|
208
|
+
var was = live ? live[f] : undefined;
|
|
209
|
+
if (live && JSON.stringify(was) === JSON.stringify(p[f])) return;
|
|
210
|
+
out.push({ label: FIELD_WORDS[f] || f.replace(/_/g, ' '), struck: live ? fieldValue(f, was) : '', inserted: fieldValue(f, p[f]) });
|
|
211
|
+
});
|
|
212
|
+
return out.length ? { rows: out } : null;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The sitting this one revises: the newest RETURNED sitting by the same author
|
|
218
|
+
// on the same subject, closed before this one opened. Its words are struck and
|
|
219
|
+
// this sitting's inserted, when they differ.
|
|
220
|
+
function revisionOf(item, room) {
|
|
221
|
+
if (!item || !room || !item.subject) return null;
|
|
222
|
+
var author = str(item.subject.author_id);
|
|
223
|
+
if (!author) return null;
|
|
224
|
+
var opened = Date.parse(item.opened_at) || 0;
|
|
225
|
+
var k = subjectKind(item), area = areaOf(item);
|
|
226
|
+
var prev = (room.closed || []).filter(function (c) {
|
|
227
|
+
return c && c.id !== item.id && c.outcome === 'returned' && c.subject && str(c.subject.author_id) === author
|
|
228
|
+
&& subjectKind(c) === k && areaOf(c) === area && c.subject_type === item.subject_type
|
|
229
|
+
&& (Date.parse(c.closed_at) || 0) <= (opened || Infinity);
|
|
230
|
+
}).sort(function (a, b) { return (Date.parse(b.closed_at) || 0) - (Date.parse(a.closed_at) || 0); })[0];
|
|
231
|
+
if (!prev) return null;
|
|
232
|
+
var was = proposerWords(prev), now = proposerWords(item);
|
|
233
|
+
if (!was || !now || was === now) return null;
|
|
234
|
+
return { struck: clip(was, 240), inserted: clip(now, 240) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------- the ballot ----------
|
|
238
|
+
function ballotOf(item, m, viewerId) {
|
|
239
|
+
var viewer = str(viewerId);
|
|
240
|
+
var votes = (item && item.votes) || [];
|
|
241
|
+
var t = (m && m.tally) || {};
|
|
242
|
+
var mine = null;
|
|
243
|
+
var seats = [];
|
|
244
|
+
votes.forEach(function (v) {
|
|
245
|
+
var name = personName(v);
|
|
246
|
+
var isYou = str(v.voter_id) === viewer;
|
|
247
|
+
if (isYou) mine = v;
|
|
248
|
+
seats.push({ key: isYou ? 'you' : 'v' + str(v.voter_id), init: isYou ? 'you' : initial(name), name: isYou ? 'you' : name, pos: v.direction === 'yes' ? 'for' : 'against' });
|
|
249
|
+
});
|
|
250
|
+
var open = Math.max(0, Number(t.open) || 0);
|
|
251
|
+
var isAuthor = !!(item && item.subject && str(item.subject.author_id) === viewer);
|
|
252
|
+
var youOpen = !mine && !item.closed_at && !item.expired && (m.state === 'needs' || (m.state === 'moving' && !isAuthor));
|
|
253
|
+
if (youOpen) { seats.push({ key: 'you', init: 'you', name: 'you', pos: 'you' }); open -= 1; }
|
|
254
|
+
for (var i = 0; i < Math.max(0, open) && seats.length < 9; i++) seats.push({ key: 'o' + i, init: '', name: 'an open seat', pos: 'open' });
|
|
255
|
+
var forN = votes.filter(function (v) { return v.direction === 'yes'; }).length;
|
|
256
|
+
var againstN = votes.length - forN;
|
|
257
|
+
var tally = forN + ' for · ' + againstN + ' against' + (t.needs ? ' · ' + t.needs : '');
|
|
258
|
+
return { seats: seats, tally: tally, mine: mine, isAuthor: isAuthor, youOpen: youOpen, forN: forN, againstN: againstN, members: votes.length + Math.max(0, Number(t.open) || 0) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function sectionWords(k) { return SECTION_WORDS[k] || str(k).replace(/_/g, ' ') || 'it'; }
|
|
262
|
+
|
|
263
|
+
function voicesOf(item) {
|
|
264
|
+
var votes = (item && item.votes) || [];
|
|
265
|
+
var out = [];
|
|
266
|
+
votes.filter(function (v) { return v.direction === 'no'; }).forEach(function (v) {
|
|
267
|
+
out.push({ side: 'against', by: firstName(personName(v)) + ' · objects to ' + sectionWords(v.section_key), words: str(v.reason_md) });
|
|
268
|
+
});
|
|
269
|
+
var yes = votes.filter(function (v) { return v.direction === 'yes'; }).map(function (v) { return firstName(personName(v)); });
|
|
270
|
+
if (yes.length && out.length) out.unshift({ side: 'for', by: 'for', words: list(yes) });
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Counsel, written from the rule, the tally and the objections.
|
|
275
|
+
function counselOf(item, b, rule, proposer) {
|
|
276
|
+
var votes = (item && item.votes) || [];
|
|
277
|
+
var no = votes.filter(function (v) { return v.direction === 'no'; })[0];
|
|
278
|
+
var noBy = no ? firstName(personName(no)) : '';
|
|
279
|
+
var noAt = no ? sectionWords(no.section_key) : '';
|
|
280
|
+
var kind = subjectKind(item);
|
|
281
|
+
if (item.closed_at) {
|
|
282
|
+
if (item.outcome === 'returned' && b.isAuthor && kind !== 'idea' && kind !== 'genesis') {
|
|
283
|
+
return { words: no
|
|
284
|
+
? noBy + ' objected to ' + noAt + ': “' + clip(no.reason_md, 160) + '” Answer that in the wording, and the sitting can carry.'
|
|
285
|
+
: 'It did not carry. Change the wording so it can, and put it to the sitting again.', revise: true };
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if (no && rule === 'majority') return { words: noBy + ' objects to ' + noAt + '. Under a majority an objection is on the record but does not stop it: more than half of the seats must still say yes.', revise: false };
|
|
290
|
+
if (!b.youOpen || kind === 'idea') return null;
|
|
291
|
+
if (rule === 'unanimous') return { words: 'Everyone who votes has to agree, so one objection sends it back to ' + (proposer || 'its author') + ' to revise. If you object, say what would answer it.', revise: false };
|
|
292
|
+
if (rule === 'majority') {
|
|
293
|
+
var need = Math.floor(b.members / 2) + 1;
|
|
294
|
+
var left = Math.max(0, need - b.forN);
|
|
295
|
+
return { words: 'More than half of the ' + b.members + ' seats must say yes. It has ' + b.forN + (left ? '; ' + left + ' more ' + (left === 1 ? 'yes carries' : 'carry') + ' it.' : '.'), revise: false };
|
|
296
|
+
}
|
|
297
|
+
if (rule === 'consent') return { words: 'It passes unless someone objects. Saying nothing is saying yes; an objection sends it back to ' + (proposer || 'its author') + '.', revise: false };
|
|
298
|
+
if (rule === 'first') return { words: 'The first yes decides it.', revise: false };
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function act(key, label, tone, extra) {
|
|
303
|
+
var a = { key: key, label: label, tone: tone };
|
|
304
|
+
if (extra) Object.keys(extra).forEach(function (k) { a[k] = extra[k]; });
|
|
305
|
+
return a;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------- the card for a board sitting ----------
|
|
309
|
+
function boardCard(cv, m, item, ctx) {
|
|
310
|
+
var kind = subjectKind(item);
|
|
311
|
+
var proposer = m.proposer ? firstName(m.proposer.name || m.proposer.login) : '';
|
|
312
|
+
var words = proposerWords(item);
|
|
313
|
+
if (words) cv.quote = { by: proposer, words: clip(words, 200) };
|
|
314
|
+
cv.change = changeOf(item, ctx.docket, ctx.constitution);
|
|
315
|
+
cv.revision = revisionOf(item, ctx.room);
|
|
316
|
+
var b = ballotOf(item, m, ctx.viewerId);
|
|
317
|
+
cv.seats = b.seats;
|
|
318
|
+
cv.tally = b.tally;
|
|
319
|
+
cv.voices = voicesOf(item);
|
|
320
|
+
var rule = m.rule || null;
|
|
321
|
+
cv.counsel = counselOf(item, b, rule, proposer);
|
|
322
|
+
cv.subject = { kind: kind, itemId: String(item.id), area: areaOf(item), title: item.subject ? str(item.subject.title) : '', proposed: item.subject ? item.subject.proposed || null : null, action: item.subject ? item.subject.action || null : null, words: words };
|
|
323
|
+
|
|
324
|
+
if (item.closed_at) {
|
|
325
|
+
cv.readOnly = true;
|
|
326
|
+
if (item.outcome === 'passed') cv.result = 'Passed ' + b.forN + '–' + b.againstN + '. In force from ' + str(item.closed_at).slice(0, 10) + '.';
|
|
327
|
+
else if (b.isAuthor && cv.counsel && cv.counsel.revise) { cv.result = 'It came back to you.'; cv.readOnly = false; cv.reviseForm = { text: words, label: kind === 'matter' ? 'the matter, in your words' : 'why, in your words' }; }
|
|
328
|
+
else cv.result = 'It went back to ' + (proposer || 'its author') + ' to revise.';
|
|
329
|
+
return cv;
|
|
330
|
+
}
|
|
331
|
+
if (item.expired) { cv.readOnly = true; cv.result = 'Its time is up. It closes on the next sweep.'; return cv; }
|
|
332
|
+
if (b.mine) {
|
|
333
|
+
cv.readOnly = true;
|
|
334
|
+
if (b.mine.direction === 'yes') {
|
|
335
|
+
var tail = '';
|
|
336
|
+
if (rule === 'majority') { var left = Math.max(0, Math.floor(b.members / 2) + 1 - b.forN); tail = left ? ' It needs ' + left + ' more yes.' : ' It has its majority.'; }
|
|
337
|
+
else if (rule === 'unanimous') tail = ' It passes unless someone objects before it closes.';
|
|
338
|
+
else if (rule === 'consent') tail = ' It passes unless someone objects.';
|
|
339
|
+
cv.result = 'You voted for.' + tail;
|
|
340
|
+
} else cv.result = 'You objected to ' + sectionWords(b.mine.section_key) + '. It is on the record.';
|
|
341
|
+
return cv;
|
|
342
|
+
}
|
|
343
|
+
if (kind === 'idea') {
|
|
344
|
+
cv.readOnly = true;
|
|
345
|
+
cv.result = m.state === 'needs' ? 'Read the idea in the Board Room and vote there, with its sections in front of you.' : 'Not yours to vote on.';
|
|
346
|
+
return cv;
|
|
347
|
+
}
|
|
348
|
+
if (b.youOpen) {
|
|
349
|
+
cv.actions = [act('for', 'vote for', 'primary'), act('object', 'object', 'quiet')];
|
|
350
|
+
cv.textActions = [act('abstain', 'abstain', 'text')];
|
|
351
|
+
cv.objectForm = { sections: SECTIONS.slice() };
|
|
352
|
+
cv.result = '';
|
|
353
|
+
return cv;
|
|
354
|
+
}
|
|
355
|
+
cv.readOnly = true;
|
|
356
|
+
cv.result = b.isAuthor ? 'Yours. It is with the sitting now.' : 'Not yours to vote on. You will see how it ends.';
|
|
357
|
+
return cv;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------- the card for every other source ----------
|
|
361
|
+
var READ_ONLY_WORDS = {
|
|
362
|
+
criteria_uat: 'Someone who did not build it tries it and signs it off.',
|
|
363
|
+
blockers: 'It holds work until someone decides. It is decided where it lives.',
|
|
364
|
+
removal_proposals: 'A proposal to take work out of a goal. It is decided where it lives.',
|
|
365
|
+
redteam_reports: 'A report on the project\'s own walls. It is judged where it lives.',
|
|
366
|
+
autobongos_fence: 'Whether work is built while no one is watching. It is switched where it lives.',
|
|
367
|
+
module_rechecks: 'A module waits for its re-check. It is decided where it lives.',
|
|
368
|
+
};
|
|
369
|
+
function joinGoalId(m) { var hit = /goal\/(\d+)/.exec(str(m.href)); return hit ? hit[1] : null; }
|
|
370
|
+
|
|
371
|
+
function sourceCard(cv, m) {
|
|
372
|
+
var id = m.ref && m.ref.id != null ? String(m.ref.id) : str(m.id).replace(/^[^:]*:/, '');
|
|
373
|
+
var who = m.proposer ? str(m.proposer.name || m.proposer.login) : '';
|
|
374
|
+
cv.subject = { kind: m.source, id: id, who: who, goalId: m.source === 'goal_join_requests' ? joinGoalId(m) : null };
|
|
375
|
+
var mine = m.state === 'needs';
|
|
376
|
+
if (m.source === 'access_requests') {
|
|
377
|
+
if (who) cv.quote = { by: '', words: who + ' asked to join as a builder' + (/vouched/.test(str(m.where)) ? ', and someone vouched for them.' : '.') };
|
|
378
|
+
if (!mine) { cv.readOnly = true; cv.result = 'Not yours to decide.'; return cv; }
|
|
379
|
+
cv.result = 'The call is yours. Fine by you lets them in; an objection keeps them out.';
|
|
380
|
+
cv.actions = [act('fine', 'fine by me', 'primary'), act('refuse', 'object', 'quiet')];
|
|
381
|
+
return cv;
|
|
382
|
+
}
|
|
383
|
+
if (m.source === 'goal_join_requests') {
|
|
384
|
+
if (who) cv.quote = { by: '', words: who + ' asked to join this goal\'s team.' };
|
|
385
|
+
if (!mine || !cv.subject.goalId) { cv.readOnly = true; cv.result = mine ? 'Decide it on the goal\'s page.' : 'You asked. The goal\'s owner decides.'; return cv; }
|
|
386
|
+
cv.result = 'The goal\'s team is yours to keep. Fine by you lets them in.';
|
|
387
|
+
cv.actions = [act('fine', 'fine by me', 'primary'), act('refuse', 'object', 'quiet')];
|
|
388
|
+
return cv;
|
|
389
|
+
}
|
|
390
|
+
if (m.source === 'override_requests' || m.source === 'gate_approvals') {
|
|
391
|
+
if (who) cv.quote = { by: '', words: 'Raised by ' + who + '.' };
|
|
392
|
+
if (!mine) { cv.readOnly = true; cv.result = 'Someone who may decide it will.'; return cv; }
|
|
393
|
+
cv.result = m.source === 'gate_approvals'
|
|
394
|
+
? 'It is held at the gate. Releasing it lets it merge and go live. The call is yours.'
|
|
395
|
+
: 'It failed a check or a grade. Releasing it lets it go live anyway. The call is yours.';
|
|
396
|
+
cv.actions = [act('release', 'release', 'primary'), act('hold', 'hold', 'quiet')];
|
|
397
|
+
return cv;
|
|
398
|
+
}
|
|
399
|
+
if (m.source === 'core_update') {
|
|
400
|
+
if (!mine) { cv.readOnly = true; cv.result = 'Someone who may move the core will.'; return cv; }
|
|
401
|
+
cv.result = 'A newer core is out. Taking it moves the live pin, so it runs its check first, where the pin is moved.';
|
|
402
|
+
cv.actions = [act('release', 'release, with its check', 'primary', { go: safeHref(m.href) || '/settings#software-update' }), act('hold', 'hold', 'quiet')];
|
|
403
|
+
return cv;
|
|
404
|
+
}
|
|
405
|
+
cv.readOnly = true;
|
|
406
|
+
cv.result = READ_ONLY_WORDS[m.source] || (mine ? 'This waits on you, where it lives.' : 'This is moving without you.');
|
|
407
|
+
return cv;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// The whole card for one matter. ctx: { docket, key, room, constitution, viewerId, now }.
|
|
411
|
+
// null for an unknown key. A board sitting the room has not been read for yet
|
|
412
|
+
// is the short card with `loading: true`.
|
|
413
|
+
function model(ctx) {
|
|
414
|
+
var c = ctx || {};
|
|
415
|
+
var Street = root.GovStreet;
|
|
416
|
+
var short = Street && Street.cardFor ? Street.cardFor(c.docket, c.key, c.now || 0) : null;
|
|
417
|
+
if (!short) return null;
|
|
418
|
+
var cv = {
|
|
419
|
+
key: short.key, building: short.building, where: short.where, when: short.when, title: short.title, href: short.href,
|
|
420
|
+
source: null, quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
|
|
421
|
+
result: short.result, actions: [], textActions: [], objectForm: null, reviseForm: null, readOnly: false, loading: false, lines: short.lines || [], subject: null,
|
|
422
|
+
};
|
|
423
|
+
if (short.key === 'queue') { cv.source = 'queue'; cv.readOnly = true; return cv; }
|
|
424
|
+
var m = findMatter(c.docket, c.key);
|
|
425
|
+
cv.source = m.source;
|
|
426
|
+
var viewerId = c.viewerId != null ? c.viewerId : (c.docket && c.docket.viewer ? c.docket.viewer.id : null);
|
|
427
|
+
var itemId = itemIdOf(m);
|
|
428
|
+
if (itemId != null) {
|
|
429
|
+
var item = findItem(c.room, itemId);
|
|
430
|
+
if (!item) {
|
|
431
|
+
cv.loading = !c.room;
|
|
432
|
+
cv.readOnly = true;
|
|
433
|
+
cv.tally = short.tally;
|
|
434
|
+
cv.result = c.room ? 'This sitting could not be read just now.' : 'Reading the sitting…';
|
|
435
|
+
return cv;
|
|
436
|
+
}
|
|
437
|
+
return boardCard(cv, m, item, { docket: c.docket, room: c.room, constitution: c.constitution, viewerId: viewerId });
|
|
438
|
+
}
|
|
439
|
+
return sourceCard(cv, m);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------- act → route ----------
|
|
443
|
+
function post(url, body) { return { method: 'POST', url: API + url, body: body || {} }; }
|
|
444
|
+
// input: { reason, section } for object; { text } for revise.
|
|
445
|
+
function request(cv, key, input) {
|
|
446
|
+
if (!cv || !cv.subject) return null;
|
|
447
|
+
var s = cv.subject, inp = input || {};
|
|
448
|
+
var enc = encodeURIComponent;
|
|
449
|
+
if (s.itemId != null) {
|
|
450
|
+
if (key === 'for') return post('/government/board/items/' + enc(s.itemId) + '/votes', { direction: 'yes' });
|
|
451
|
+
if (key === 'object') return post('/government/board/items/' + enc(s.itemId) + '/votes', { direction: 'no', reason_md: str(inp.reason).trim(), section_key: str(inp.section) });
|
|
452
|
+
if (key === 'revise') {
|
|
453
|
+
var text = str(inp.text).trim();
|
|
454
|
+
if (s.kind === 'amendment') return post('/government/board/items', { subject_type: 'constitutional_amendment', proposal: s.proposed, rationale_md: text });
|
|
455
|
+
if (s.kind === 'rules') return post('/government/board/decision-rules', { rules: (s.proposed && s.proposed.decision_rules) || [], rationale_md: text });
|
|
456
|
+
if (s.kind === 'matter') {
|
|
457
|
+
var body = { area: s.area, title: s.title || cv.title, detail_md: text };
|
|
458
|
+
if (s.action) body.action = s.action;
|
|
459
|
+
return post('/government/board/matters', body);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return null; // abstain records nothing
|
|
463
|
+
}
|
|
464
|
+
if (s.kind === 'access_requests') {
|
|
465
|
+
if (key === 'fine') return { method: 'PATCH', url: API + '/access-requests/' + enc(s.id), body: { status: 'invited' } };
|
|
466
|
+
if (key === 'refuse') return { method: 'PATCH', url: API + '/access-requests/' + enc(s.id), body: { status: 'dismissed' } };
|
|
467
|
+
}
|
|
468
|
+
if (s.kind === 'goal_join_requests' && s.goalId) {
|
|
469
|
+
if (key === 'fine') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'approve' });
|
|
470
|
+
if (key === 'refuse') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'deny' });
|
|
471
|
+
}
|
|
472
|
+
if (s.kind === 'override_requests' && key === 'release') return post('/override-requests/' + enc(s.id) + '/decide', { decision: 'approve' });
|
|
473
|
+
if (s.kind === 'gate_approvals' && key === 'release') return post('/gate-approvals/' + enc(s.id) + '/approve', {});
|
|
474
|
+
return null; // hold, abstain and the core's release (a link) record nothing here
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Before a request: the refusal the server would give, said first.
|
|
478
|
+
function check(cv, key, input) {
|
|
479
|
+
var inp = input || {};
|
|
480
|
+
if (key === 'object' && cv && cv.subject && cv.subject.itemId != null) {
|
|
481
|
+
if (!str(inp.reason).trim()) return 'Say why. The reason goes on the record for the author to answer.';
|
|
482
|
+
if (!SECTION_WORDS[inp.section]) return 'Pick the part you object to.';
|
|
483
|
+
}
|
|
484
|
+
if (key === 'revise' && !str(inp.text).trim()) return 'Write the revised words first.';
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ---------- what the street does after an act ----------
|
|
489
|
+
// kind: 'rise' (something passed or went live), 'light' (a vote or a revision
|
|
490
|
+
// is in), 'settle' (it went back, or was turned away). close: the card closes.
|
|
491
|
+
function answer(cv, key, res) {
|
|
492
|
+
var r = res || {};
|
|
493
|
+
var who = cv && cv.subject && cv.subject.who ? cv.subject.who : 'They';
|
|
494
|
+
var b = cv ? cv.building : null;
|
|
495
|
+
if (key === 'for') {
|
|
496
|
+
if (r.item_closed && r.outcome === 'passed') return { toast: 'Passed. It is in force from today.', building: b, kind: 'rise', close: false };
|
|
497
|
+
if (r.item_closed) return { toast: 'The sitting closed: it went back to its author.', building: b, kind: 'settle', close: false };
|
|
498
|
+
return { toast: r.already_voted ? 'Your vote was already on the record.' : 'Your yes is on the record.', building: b, kind: 'light', close: false };
|
|
499
|
+
}
|
|
500
|
+
if (key === 'object') return { toast: r.item_closed ? 'Your objection is on the record. It goes back to its author to revise.' : 'Your objection is on the record.', building: b, kind: 'settle', close: false };
|
|
501
|
+
if (key === 'abstain') return { toast: 'Noted. Nothing goes on the record, and it still waits for your vote until it closes.', building: null, kind: null, close: true };
|
|
502
|
+
if (key === 'revise') return { toast: 'Revised. It sits again on the new wording.', building: b, kind: 'light', close: true };
|
|
503
|
+
if (key === 'hold') return { toast: 'Held. It waits for you.', building: null, kind: null, close: true };
|
|
504
|
+
if (key === 'release') return { toast: cv && cv.source === 'gate_approvals' ? 'Released from the gate. It merges and goes live.' : 'Released. It goes live.', building: b, kind: 'rise', close: true };
|
|
505
|
+
if (key === 'fine') return { toast: cv && cv.source === 'goal_join_requests' ? 'Noted. ' + who + ' joins the team.' : 'Noted. ' + who + ' is invited in.', building: b, kind: 'rise', close: false };
|
|
506
|
+
if (key === 'refuse') return { toast: 'Noted. ' + who + ' is not let in.', building: b, kind: 'settle', close: false };
|
|
507
|
+
return { toast: '', building: null, kind: null, close: false };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// A refused act, in words. The server's own message wins when it has one.
|
|
511
|
+
var REFUSALS = {
|
|
512
|
+
not_board_member: 'Only the members of this sitting may vote on it.',
|
|
513
|
+
item_closed: 'This sitting has closed.',
|
|
514
|
+
vote_window_expired: 'Its time ran out before your vote.',
|
|
515
|
+
objection_needs_reason: 'An objection must say why.',
|
|
516
|
+
objection_needs_section: 'An objection must name the part it objects to.',
|
|
517
|
+
request_not_open: 'Someone already decided this.',
|
|
518
|
+
not_pending: 'Someone already decided this.',
|
|
519
|
+
override_owner_only: 'Releasing this is the project owner\'s call alone.',
|
|
520
|
+
publish_unconfigured: 'The server cannot reach GitHub to release it.',
|
|
521
|
+
proposer_would_lose_their_seat: 'Re-filing this would take your own seat. Change it in the Board Room.',
|
|
522
|
+
no_change: 'That already is how it works.',
|
|
523
|
+
preview_readonly: 'This preview records nothing.',
|
|
524
|
+
};
|
|
525
|
+
function refusal(status, body) {
|
|
526
|
+
var e = body && body.error ? body.error : {};
|
|
527
|
+
var code = e.code || (body && body.code) || '';
|
|
528
|
+
if (REFUSALS[code]) return REFUSALS[code];
|
|
529
|
+
if (status === 403) return 'That is not yours to do.';
|
|
530
|
+
return e.message && e.message !== code ? String(e.message) : 'It could not be done just now.';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ---------- the DOM ----------
|
|
534
|
+
function el(doc, tag, cls, text) {
|
|
535
|
+
var n = doc.createElement(tag);
|
|
536
|
+
if (cls) n.className = cls;
|
|
537
|
+
if (text != null) n.textContent = String(text);
|
|
538
|
+
return n;
|
|
539
|
+
}
|
|
540
|
+
function label(doc, text) { return el(doc, 'span', 'street-label', text); }
|
|
541
|
+
function button(doc, cls, text, key, onClick, busy) {
|
|
542
|
+
var b = el(doc, 'button', cls, text);
|
|
543
|
+
b.setAttribute('type', 'button');
|
|
544
|
+
b.setAttribute('data-act', key);
|
|
545
|
+
if (busy) b.setAttribute('disabled', '');
|
|
546
|
+
b.addEventListener('click', onClick);
|
|
547
|
+
return b;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Build the card's body into `host` (emptied first). on: { act(key, input), openForm(name) }.
|
|
551
|
+
// ui: { form: 'object'|'revise'|null, error: '', busy: false, fresh: seatKey|null }.
|
|
552
|
+
function render(doc, host, cv, on, ui) {
|
|
553
|
+
var u = ui || {};
|
|
554
|
+
while (host.firstChild) host.removeChild(host.firstChild);
|
|
555
|
+
if (cv.quote && cv.quote.words) host.appendChild(el(doc, 'p', 'mcard-quote', (cv.quote.by ? cv.quote.by + ': ' : '') + cv.quote.words));
|
|
556
|
+
if (cv.lines && cv.lines.length) {
|
|
557
|
+
var ul = el(doc, 'ul', 'street-card__lines');
|
|
558
|
+
cv.lines.forEach(function (t) { ul.appendChild(el(doc, 'li', '', t)); });
|
|
559
|
+
host.appendChild(ul);
|
|
560
|
+
}
|
|
561
|
+
if (cv.change && cv.change.rows.length) {
|
|
562
|
+
var ch = el(doc, 'div', 'mcard-change');
|
|
563
|
+
ch.appendChild(label(doc, 'the change'));
|
|
564
|
+
cv.change.rows.forEach(function (r) {
|
|
565
|
+
var row = el(doc, 'p', 'mcard-change__row');
|
|
566
|
+
row.appendChild(el(doc, 'span', 'mcard-change__label', r.label));
|
|
567
|
+
if (r.struck) row.appendChild(el(doc, 'del', 'mcard-del', r.struck));
|
|
568
|
+
row.appendChild(el(doc, 'ins', 'mcard-ins', r.inserted));
|
|
569
|
+
ch.appendChild(row);
|
|
570
|
+
});
|
|
571
|
+
host.appendChild(ch);
|
|
572
|
+
}
|
|
573
|
+
if (cv.revision) {
|
|
574
|
+
var rv = el(doc, 'div', 'mcard-change');
|
|
575
|
+
rv.appendChild(label(doc, 'revised'));
|
|
576
|
+
var rr = el(doc, 'p', 'mcard-change__row');
|
|
577
|
+
rr.appendChild(el(doc, 'del', 'mcard-del', cv.revision.struck));
|
|
578
|
+
rr.appendChild(el(doc, 'ins', 'mcard-ins', cv.revision.inserted));
|
|
579
|
+
rv.appendChild(rr);
|
|
580
|
+
host.appendChild(rv);
|
|
581
|
+
}
|
|
582
|
+
if (cv.seats.length || cv.tally) {
|
|
583
|
+
var sw = el(doc, 'div', 'mcard-seats');
|
|
584
|
+
var seats = el(doc, 'div', 'mcard-seats__row');
|
|
585
|
+
seats.setAttribute('role', 'list');
|
|
586
|
+
seats.setAttribute('aria-label', 'The seats');
|
|
587
|
+
cv.seats.forEach(function (s) {
|
|
588
|
+
var chip = el(doc, 'span', 'mcard-seat mcard-seat--' + s.pos + (u.fresh && u.fresh === s.key ? ' mcard-seat--fresh' : ''), s.init);
|
|
589
|
+
chip.setAttribute('role', 'listitem');
|
|
590
|
+
var said = s.pos === 'for' ? ', for' : s.pos === 'against' ? ', against' : s.pos === 'you' ? ', waiting for your vote' : '';
|
|
591
|
+
chip.setAttribute('title', s.name + said);
|
|
592
|
+
chip.setAttribute('aria-label', s.name + said);
|
|
593
|
+
seats.appendChild(chip);
|
|
594
|
+
});
|
|
595
|
+
sw.appendChild(seats);
|
|
596
|
+
if (cv.tally) sw.appendChild(el(doc, 'span', 'mcard-tally', cv.tally));
|
|
597
|
+
host.appendChild(sw);
|
|
598
|
+
}
|
|
599
|
+
cv.voices.forEach(function (v) {
|
|
600
|
+
var vo = el(doc, 'div', 'mcard-voice mcard-voice--' + v.side);
|
|
601
|
+
vo.appendChild(el(doc, 'span', 'mcard-voice__by', v.by));
|
|
602
|
+
vo.appendChild(el(doc, 'span', 'mcard-voice__words', v.words));
|
|
603
|
+
host.appendChild(vo);
|
|
604
|
+
});
|
|
605
|
+
if (cv.counsel) {
|
|
606
|
+
var co = el(doc, 'div', 'mcard-counsel');
|
|
607
|
+
co.appendChild(label(doc, 'counsel'));
|
|
608
|
+
co.appendChild(el(doc, 'span', 'mcard-counsel__words', cv.counsel.words));
|
|
609
|
+
if (cv.counsel.revise && cv.reviseForm && u.form !== 'revise') {
|
|
610
|
+
co.appendChild(button(doc, 'mcard-textbtn mcard-textbtn--accent', 'revise it this way and sit again', 'revise-open', function () { on.openForm('revise'); }, false));
|
|
611
|
+
}
|
|
612
|
+
host.appendChild(co);
|
|
613
|
+
}
|
|
614
|
+
if (cv.result) host.appendChild(el(doc, 'p', 'street-card__result', cv.result));
|
|
615
|
+
if (u.form === 'object' && cv.objectForm) host.appendChild(objectForm(doc, cv, on, u));
|
|
616
|
+
else if (u.form === 'revise' && cv.reviseForm) host.appendChild(reviseForm(doc, cv, on, u));
|
|
617
|
+
else {
|
|
618
|
+
if (cv.actions.length) {
|
|
619
|
+
var row = el(doc, 'div', 'mcard-actions');
|
|
620
|
+
cv.actions.forEach(function (a) {
|
|
621
|
+
var btn;
|
|
622
|
+
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); }
|
|
623
|
+
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);
|
|
624
|
+
row.appendChild(btn);
|
|
625
|
+
});
|
|
626
|
+
host.appendChild(row);
|
|
627
|
+
}
|
|
628
|
+
if (cv.textActions.length) {
|
|
629
|
+
var tr = el(doc, 'div', 'mcard-actions mcard-actions--text');
|
|
630
|
+
cv.textActions.forEach(function (a) { tr.appendChild(button(doc, 'mcard-textbtn', a.label, a.key, function () { on.act(a.key, {}); }, u.busy)); });
|
|
631
|
+
host.appendChild(tr);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (u.error) {
|
|
635
|
+
var er = el(doc, 'p', 'mcard-error', u.error);
|
|
636
|
+
er.setAttribute('role', 'alert');
|
|
637
|
+
host.appendChild(er);
|
|
638
|
+
}
|
|
639
|
+
return host;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function formShell(doc, name, labelText, field, fieldId) {
|
|
643
|
+
var f = el(doc, 'div', 'mcard-form');
|
|
644
|
+
f.setAttribute('data-form', name);
|
|
645
|
+
var lab = el(doc, 'label', 'mcard-form__label', labelText);
|
|
646
|
+
lab.setAttribute('for', fieldId);
|
|
647
|
+
f.appendChild(lab);
|
|
648
|
+
f.appendChild(field);
|
|
649
|
+
return f;
|
|
650
|
+
}
|
|
651
|
+
function textArea(doc, id, rows, value) {
|
|
652
|
+
var ta = el(doc, 'textarea', 'mcard-form__text');
|
|
653
|
+
ta.setAttribute('id', id);
|
|
654
|
+
ta.setAttribute('rows', String(rows));
|
|
655
|
+
ta.setAttribute('maxlength', '5000');
|
|
656
|
+
if (value) ta.value = value;
|
|
657
|
+
return ta;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function objectForm(doc, cv, on, u) {
|
|
661
|
+
var ta = textArea(doc, 'mcardReason', 3, '');
|
|
662
|
+
var f = formShell(doc, 'object', 'why you object (it goes on the record)', ta, 'mcardReason');
|
|
663
|
+
var sel = el(doc, 'select', 'mcard-form__select');
|
|
664
|
+
sel.setAttribute('id', 'mcardSection');
|
|
665
|
+
var none = el(doc, 'option', '', 'pick one');
|
|
666
|
+
none.setAttribute('value', '');
|
|
667
|
+
sel.appendChild(none);
|
|
668
|
+
cv.objectForm.sections.forEach(function (s) { var o = el(doc, 'option', '', s.label); o.setAttribute('value', s.key); sel.appendChild(o); });
|
|
669
|
+
var sl = el(doc, 'label', 'mcard-form__label', 'the part you object to');
|
|
670
|
+
sl.setAttribute('for', 'mcardSection');
|
|
671
|
+
f.appendChild(sl);
|
|
672
|
+
f.appendChild(sel);
|
|
673
|
+
var row = el(doc, 'div', 'mcard-actions');
|
|
674
|
+
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));
|
|
675
|
+
row.appendChild(button(doc, 'mcard-btn mcard-btn--quiet', 'not now', 'form-close', function () { on.openForm(null); }, false));
|
|
676
|
+
f.appendChild(row);
|
|
677
|
+
return f;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function reviseForm(doc, cv, on, u) {
|
|
681
|
+
var ta = textArea(doc, 'mcardRevise', 4, cv.reviseForm.text || '');
|
|
682
|
+
var f = formShell(doc, 'revise', cv.reviseForm.label, ta, 'mcardRevise');
|
|
683
|
+
var row = el(doc, 'div', 'mcard-actions');
|
|
684
|
+
row.appendChild(button(doc, 'mcard-btn mcard-btn--primary', 'sit again', 'revise', function () { on.act('revise', { text: ta.value }); }, u.busy));
|
|
685
|
+
row.appendChild(button(doc, 'mcard-btn mcard-btn--quiet', 'not now', 'form-close', function () { on.openForm(null); }, false));
|
|
686
|
+
f.appendChild(row);
|
|
687
|
+
return f;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
var api = {
|
|
691
|
+
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
692
|
+
model: model, request: request, check: check, answer: answer, refusal: refusal, render: render,
|
|
693
|
+
memberWords: memberWords, windowWords: windowWords,
|
|
694
|
+
};
|
|
695
|
+
root.GovMatterCard = api;
|
|
696
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
697
|
+
}(typeof window !== 'undefined' ? window : globalThis));
|