@bongos/core 1.21.37 → 1.21.39

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.
Files changed (33) hide show
  1. package/.bongos-core.json +51 -36
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/api/openapi.json +77 -3
  8. package/docs/api-reference.md +3 -2
  9. package/docs/copy-inventory.md +6 -7
  10. package/docs/copy-registry.json +8 -17
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-readings.json +310 -318
  13. package/modules/government/board-withdraw.js +38 -0
  14. package/modules/government/board.js +1 -0
  15. package/modules/government/db.js +20 -0
  16. package/modules/government/migrations/government_026_board_outcome_withdrawn.sql +29 -0
  17. package/modules/government/routes/government.js +26 -0
  18. package/modules/hall-ui/public/street-card.js +16 -0
  19. package/modules/hall-ui/public/street-desk.js +2 -0
  20. package/modules/hall-ui/public/street-inside.js +3 -13
  21. package/modules/hall-ui/public/street.css +2 -6
  22. package/modules/hall-ui/public/street.html +0 -4
  23. package/modules/hall-ui/public/street.js +3 -18
  24. package/modules/hall-ui/records/governor-street.md +2 -2
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/release-notes.json +12 -0
  28. package/src/bongos/route-rank-check.js +4 -0
  29. package/src/module-api.js +1 -1
  30. package/tests/government_board_withdraw.mjs +144 -0
  31. package/tests/government_seed.mjs +1 -0
  32. package/tests/hall_street_desk.mjs +37 -0
  33. package/tests/hall_street_inside.mjs +18 -16
@@ -202,6 +202,43 @@ test('a row\'s buttons are the card\'s acts under the board\'s words, and send t
202
202
  assert.equal(D.rowsFor('charter', 0, dk, { now: NOW })[0].wait, 'reading…');
203
203
  });
204
204
 
205
+ // task 1004564: the proposer's own sitting, before anyone has voted, is one row
206
+ // with one button — withdraw — sent to the withdraw route. A vote takes it away.
207
+ test('your own sitting with no vote on it: one row, withdraw, sent to its route; a vote or another viewer takes it away', () => {
208
+ const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', ref: { type: 'board_item', id: '1' } })];
209
+ const dk = docket(ms);
210
+ const room = (votes) => ({ open: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: LONG, author_id: '95' }, votes }], closed: [] });
211
+ const mine = D.rowsFor('charter', 0, dk, { room: room([]), now: NOW, viewerId: '95' })[0];
212
+ assert.deepEqual(mine.actions.map((a) => [a.key, a.label]), [['withdraw', 'withdraw']]);
213
+ assert.equal(mine.chips, null, 'withdraw asks nothing: no chips');
214
+ assert.deepEqual(j(C.request(mine.card, 'withdraw')), { method: 'POST', url: '/api/bongos/government/board/items/1/withdraw', body: {} });
215
+ assert.match(C.answer(mine.card, 'withdraw', { item_closed: true, outcome: 'withdrawn' }).toast, /^Withdrawn/);
216
+ assert.equal(C.refusal(409, { error: { code: 'votes_cast' } }), 'Someone has voted on it, so it is the sitting\'s to decide now.');
217
+ // one vote — anyone's — and the row is the sitting's again
218
+ const voted = D.rowsFor('charter', 0, dk, { room: room([{ voter_id: '7', direction: 'yes', name: 'Ana' }]), now: NOW, viewerId: '95' })[0];
219
+ assert.ok(!voted.actions.some((a) => a.key === 'withdraw'), 'no withdraw once a vote is cast');
220
+ // someone else's sitting never offers it
221
+ const theirs = D.rowsFor('charter', 0, dk, { room: room([]), now: NOW, viewerId: '7' })[0];
222
+ assert.ok(!theirs.actions.some((a) => a.key === 'withdraw'), 'only the proposer sees withdraw');
223
+ // a proposer who may also vote keeps yes first, withdraw second — still two buttons
224
+ const needs = D.rowsFor('charter', 0, docket([{ ...ms[0], state: 'needs' }]), { room: room([]), now: NOW, viewerId: '95' })[0];
225
+ assert.deepEqual(needs.actions.map((a) => a.key), ['for', 'withdraw']);
226
+ // and the line rule holds on every floor with the proposer looking
227
+ const own = { ...ROOM, open: ROOM.open.map((it) => it.subject_type === 'full_idea' ? it : { ...it, subject: { ...it.subject, author_id: '95' } }) };
228
+ const desks = everyDesk(docket(longMatters()), { room: own });
229
+ assert.ok(desks.flatMap((d) => d.rows).some((r) => r.actions.some((a) => a.key === 'withdraw')), 'the stress docket reached a withdraw row');
230
+ assert.deepEqual(desks.flatMap((d) => D.breaks(d)), []);
231
+ });
232
+
233
+ test('a withdrawn sitting reads as withdrawn on the card, never as sent back to revise', () => {
234
+ const dk = docket([matter({ id: 'board:1', source: 'board', building: 'charter', state: 'settled', ref: { type: 'board_item', id: '1' } })]);
235
+ const room = { open: [], closed: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: 'Shorter windows', author_id: '95' }, votes: [], closed_at: '2026-10-03T10:00:00.000Z', outcome: 'withdrawn' }] };
236
+ const cv = (viewerId) => C.model({ docket: dk, key: 'board:1', room, now: NOW, viewerId });
237
+ assert.equal(cv('95').result, 'You withdrew it before anyone voted.');
238
+ assert.match(cv('7').result, /^Withdrawn by .* before anyone voted\.$/);
239
+ assert.equal(cv('95').readOnly, true);
240
+ });
241
+
205
242
  // ── the page: the desk acts in place, and the game layer answers ──────────────
206
243
 
207
244
  function fakeDom() {
@@ -218,7 +218,7 @@ test('a source that is off, coming, not yours or unreadable never shows a number
218
218
  const none = I.civicInfo('release', { docket: null, now: NOW });
219
219
  assert.ok(none.floors.every((f) => f.count === '' && f.status !== 'ok' && f.status !== 'pending'), 'no docket: no numbers anywhere');
220
220
  assert.equal(none.onFile, 'the docket could not be read just now');
221
- assert.deepEqual(none.table, []);
221
+ assert.deepEqual(none.record, [], 'no docket: nothing filed');
222
222
  });
223
223
 
224
224
  test('a floor bound to a reading shows its figure and line when live; Planning counts the open goals', () => {
@@ -237,12 +237,12 @@ test('a floor bound to a reading shows its figure and line when live; Planning c
237
237
  assert.ok(m.texts.some((t) => t.v === '5' && t.s === 20), 'only open goals are counted on the wall');
238
238
  });
239
239
 
240
- test('on the table: the building\'s matters, needs first; each row names the matter it opens', () => {
241
- const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', title: 'B', deadline: '2026-10-04T09:00:00.000Z' }), matter({ id: 'board:2', source: 'board', building: 'charter', state: 'needs', title: 'A', deadline: '2026-10-02T20:00:00.000Z' }), matter({ id: 'blockers:3' })];
240
+ test('no separate list of matters: the floor desks carry every matter, so the model keeps no table (task 1004565)', () => {
241
+ const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', title: 'B' }), matter({ id: 'board:2', source: 'board', building: 'charter', state: 'needs', title: 'A' })];
242
242
  const m = I.buildingModel({ key: 'charter', docket: docket(ms), mode: 'light', now: NOW });
243
- assert.deepEqual(m.table.map((r) => [r.matter, r.label, r.status, r.state]), [['board:2', 'A', 'needs you · today', 'needs'], ['board:1', 'B', 'moving · sun', 'moving']]);
244
- assert.equal(m.table[0].color, S.theme('light').accentText);
245
- assert.ok(m.hasTable);
243
+ assert.ok(!('table' in m) && !('hasTable' in m), 'no table fields on the model');
244
+ assert.doesNotMatch(m.label, /on the table/, 'the building\'s label no longer counts a table');
245
+ assert.match(m.label, /floors in use\.$/);
246
246
  });
247
247
 
248
248
  test('the record files the building\'s matters by the month each was opened, newest drawer first', () => {
@@ -524,7 +524,8 @@ const FEEDS = (ms, extra = {}) => ({
524
524
 
525
525
  test('the markup carries the interior: one persistent lift, two flip blocks, the panel and the back link', () => {
526
526
  const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
527
- for (const id of ['streetInside', 'insideBack', 'insideSvg', 'insideFloors', 'insideLift', 'insideAnnex', 'insidePanel', 'insideName', 'insideAmend', 'insideAmendNote', 'insideFill', 'insideTable', 'insideOpenA', 'insideOpenB', 'insideDrawers', 'insideDrawerItems', 'insideDName', 'insideLegend', 'insideItems']) assert.match(html, new RegExp(`id="${id}"`), id);
527
+ for (const id of ['streetInside', 'insideBack', 'insideSvg', 'insideFloors', 'insideLift', 'insideAnnex', 'insidePanel', 'insideName', 'insideAmend', 'insideAmendNote', 'insideFill', 'insideOpenA', 'insideOpenB', 'insideDrawers', 'insideDrawerItems', 'insideDName', 'insideLegend', 'insideItems']) assert.match(html, new RegExp(`id="${id}"`), id);
528
+ assert.doesNotMatch(html, /insideTable|on the table here/i, 'the "on the table here" list is gone (task 1004565)');
528
529
  assert.equal((html.match(/<h1\b/g) || []).length, 1, 'the street keeps its one h1; the interior names are h2');
529
530
  assert.match(html, /<section class="inside" id="streetInside"[^>]*hidden>/, 'hidden until an address names a building or a district');
530
531
  assert.match(html, /<button type="button" class="inside-textbtn" id="insideAmend"/);
@@ -539,8 +540,7 @@ test('a building address draws the interior in place of the street, from the doc
539
540
  assert.equal(p.byId.get('insideFloors').children.length, 7, 'a button per floor in use');
540
541
  assert.equal(p.byId.get('insideFloors').children[4].attrs['aria-pressed'], 'true', 'the floor being decided is chosen');
541
542
  assert.equal(p.byId.get('insideLift').style.transform, 'translateY(-208px)');
542
- assert.equal(p.byId.get('insideTable').children.length, 1);
543
- assert.equal(p.byId.get('insideTable').children[0].className, 'inside-row inside-row--needs');
543
+ assert.equal(p.byId.get('insideTable'), undefined, 'no separate list of matters');
544
544
  assert.equal(p.byId.get('insideSeats').children[0].className, 'inside-seat inside-seat--open');
545
545
  assert.equal(p.focused.el, null, 'a page opened at the address keeps the browser\'s own start');
546
546
  p.location.hash = '#building=rules';
@@ -575,15 +575,17 @@ test('picking a floor moves the SAME lift node and re-enters the chosen block; d
575
575
  assert.match(p.byId.get('insideAmendNote').textContent, /could not be read/);
576
576
  });
577
577
 
578
- test('a table row opens the short card; Escape gives focus back to the row; back to the street returns to the door', async () => {
578
+ test('a matter is a desk row: its name opens it in place; back to the street returns to the door', async () => {
579
579
  const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'needs', title: '<b>x</b>' })];
580
580
  const p = await bootPage({ hash: '#building=charter', feeds: FEEDS(ms) });
581
- p.byId.get('insideTable').children[0].fire('click');
582
- assert.equal(p.byId.get('streetCard').hidden, false);
583
- assert.equal(p.byId.get('streetCardTitle').textContent, '<b>x</b>', 'docket text lands as text');
584
- p.fireWin('keydown', { key: 'Escape', preventDefault() {} });
585
- assert.equal(p.byId.get('streetCard').hidden, true);
586
- assert.equal(p.focused.el.attrs['data-matter'], 'board:1', 'focus goes back to the row');
581
+ const desk = () => (p.byId.get('insideOpenA').hidden ? p.byId.get('insideOpenB') : p.byId.get('insideOpenA'));
582
+ const find = (el, pred) => { if (pred(el)) return el; for (const c of el.children) { const hit = find(c, pred); if (hit) return hit; } return null; };
583
+ const nameOf = () => find(desk(), (el) => el.attrs && el.attrs['data-matter'] === 'board:1');
584
+ assert.ok(nameOf(), 'the desk carries the matter as a row');
585
+ assert.equal(nameOf().attrs['aria-expanded'], 'false');
586
+ nameOf().fire('click');
587
+ assert.equal(nameOf().attrs['aria-expanded'], 'true', 'the row opens in place');
588
+ assert.equal(p.byId.get('streetCard').hidden, true, 'no card is needed inside: the desk acts');
587
589
  p.byId.get('insideBack').fire('click');
588
590
  assert.deepEqual(p.history, ['/builders/street']);
589
591
  assert.equal(p.byId.get('streetInside').hidden, true);