@bongos/core 1.21.35 → 1.21.36
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 +54 -39
- package/clients/bongos-client/index.d.ts +3 -3
- package/docs/api/openapi.json +25 -35
- package/docs/api-reference.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +350 -304
- package/modules/autonomy/matter-acts.js +41 -0
- package/modules/autonomy/routes/autonomy.js +2 -0
- package/modules/government/board.js +4 -0
- package/modules/government/db.js +14 -0
- package/modules/government/decision-board.js +90 -11
- package/modules/government/docket-vocab.js +5 -0
- package/modules/government/docket.js +11 -0
- package/modules/government/routes/government.js +2 -1
- package/modules/hall-ui/public/street-card.js +62 -2
- package/modules/hall-ui/public/street-desk.js +101 -8
- package/modules/hall-ui/public/street-inside.js +6 -3
- package/modules/hall-ui/public/street.css +15 -0
- package/modules/hall-ui/public/street.js +73 -4
- package/modules/hall-ui/public/street.states.json +124 -0
- package/modules/ideas/governor-docket.js +91 -0
- package/modules/lifecycle/governor-docket.js +69 -0
- package/modules/lifecycle/matter-acts.js +63 -0
- package/modules/lifecycle/routes/done-when.js +26 -12
- package/modules/lifecycle/routes/gate-approvals.js +74 -59
- package/modules/lifecycle/routes/lifecycle.js +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/src/bongos/route-rank-check.js +2 -0
- package/src/bongos/routes/builders.js +56 -1
- package/src/module-api.js +1 -1
- package/tests/governor_matter_acts.mjs +241 -0
- package/tests/hall_street_desk.mjs +244 -4
- package/tests/hall_street_inside.mjs +3 -3
|
@@ -163,7 +163,7 @@ test('a floor holds its own source; the ideas floor holds the open ideas; the re
|
|
|
163
163
|
const at = (k, f) => D.floorMatters(k, f, dk).map((m) => m.id);
|
|
164
164
|
assert.deepEqual(at('people', 4), ['access_requests:1'], 'Waiting to join: the open request only');
|
|
165
165
|
assert.deepEqual(at('people', 0), ['board:5'], 'a sitting sits at the front desk');
|
|
166
|
-
assert.deepEqual(at('planning',
|
|
166
|
+
assert.deepEqual(at('planning', 1), ['board:6'], 'Ideas to ratify');
|
|
167
167
|
assert.deepEqual(at('planning', 0), [], 'an idea is not also at the front desk');
|
|
168
168
|
assert.equal(D.firstNeeds('people', dk), 0);
|
|
169
169
|
assert.equal(D.firstNeeds('rules', dk), null);
|
|
@@ -234,12 +234,12 @@ function fakeDom() {
|
|
|
234
234
|
}
|
|
235
235
|
return { document: { documentElement: mk('html'), getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_n, t) => mk(t) }, byId, focused };
|
|
236
236
|
}
|
|
237
|
-
async function bootPage(hash, ms, { refuse = false } = {}) {
|
|
237
|
+
async function bootPage(hash, ms, { refuse = false, over = {} } = {}) {
|
|
238
238
|
const { document, byId, focused } = fakeDom();
|
|
239
239
|
const sent = [];
|
|
240
240
|
const store = new Map();
|
|
241
241
|
let matters = ms.slice();
|
|
242
|
-
const feeds = () => ({ '/api/bongos/government/docket': docket(matters), '/api/bongos/goals': { goals: [] }, '/api/bongos/goals/rollups': { rollups: [] } });
|
|
242
|
+
const feeds = () => ({ '/api/bongos/government/docket': docket(matters, over), '/api/bongos/goals': { goals: [] }, '/api/bongos/goals/rollups': { rollups: [] } });
|
|
243
243
|
const location = { hash, href: '' };
|
|
244
244
|
const window = {
|
|
245
245
|
innerWidth: 1440, innerHeight: 900, location,
|
|
@@ -268,7 +268,7 @@ async function bootPage(hash, ms, { refuse = false } = {}) {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
test('the page: the desk acts in place — the row clears, the floor lights with a tick, the meter takes a pip, the toast speaks', async () => {
|
|
271
|
-
const ms = [matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 }, opened_at:
|
|
271
|
+
const ms = [matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 }, opened_at: new Date(Date.now() - 2 * 86400000 - 3600000).toISOString() })]; // two days before the real clock: the page reads real time
|
|
272
272
|
const p = await bootPage('#building=people', ms);
|
|
273
273
|
const html = read('street.html');
|
|
274
274
|
assert.ok(html.indexOf('/builders/street-desk.js?') > html.indexOf('/builders/street-inside.js?') && html.indexOf('/builders/street-desk.js?') < html.indexOf('/builders/street.js?'), 'the desk loads after the interior and before the page');
|
|
@@ -490,10 +490,78 @@ function moreDesks({ preflight = null, longWords = false } = {}) {
|
|
|
490
490
|
desk('rules.modules', 'rules', modulesRoute.moduleDeskRows([{ key: 'discord', title: t('Discord'), enabled: true }, { key: 'economy', title: 'Economy', enabled: false }])),
|
|
491
491
|
desk('treasury.backup', 'treasury', backupRoute.backupDeskRows({ age_hours: 30, stale: false }, false)),
|
|
492
492
|
desk('people.invite', 'people', [accessRoute.INVITE_DESK_ROW]),
|
|
493
|
+
// task 1004561: a builder's monthly limit, and a sign-off undone
|
|
494
|
+
desk('treasury.budgets', 'treasury', buildersRoute.budgetDeskRows(
|
|
495
|
+
[{ id: 7, display_name: t('Ana Ruiz'), github_login: 'ana', monthly_budget_usd: '50' }, { id: 8, github_login: 'kai', monthly_budget_usd: null }],
|
|
496
|
+
)),
|
|
497
|
+
desk('review.signed', 'review', lifeDocket.signedDeskRows([
|
|
498
|
+
{ id: 11, criterion_md: t('Refunds land'), satisfied: true, checked_at: '2026-10-02T10:00:00.000Z' },
|
|
499
|
+
{ id: 12, criterion_md: 'Not yet', satisfied: false, checked_at: null },
|
|
500
|
+
])),
|
|
493
501
|
);
|
|
494
502
|
return dk;
|
|
495
503
|
}
|
|
496
504
|
|
|
505
|
+
// A docket whose decision table puts a building to a sitting (task 1004561).
|
|
506
|
+
function heldBy(dk, key, rule = 'majority') {
|
|
507
|
+
dk.buildings = dk.buildings.map((b) => (b.key === key ? { ...b, decision: { rule, sitting: true, who: 'the council' }, holder_count: 5 } : b));
|
|
508
|
+
return dk;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
test('Treasury: each builder\'s monthly limit in one tap, the limit they have left off; no limit clears it', () => {
|
|
512
|
+
const rows = actedRows(moreDesks(), 'treasury', 3);
|
|
513
|
+
const ana = rows.find((r) => r.name === 'Ana Ruiz');
|
|
514
|
+
assert.equal(ana.fact, '$50 a month');
|
|
515
|
+
assert.deepEqual(ana.actions.map((a) => a.label), ['set limit']);
|
|
516
|
+
assert.deepEqual(ana.choices.budget.options.map((o) => o.key), ['25', '100', '250', 'none'], 'the current limit is not offered');
|
|
517
|
+
assert.deepEqual(j(C.request(ana.card, 'budget', ana.choices.budget.options[1].input)), { method: 'PATCH', url: '/api/bongos/builders/7/budget', body: { monthly_budget_usd: 100 } });
|
|
518
|
+
assert.deepEqual(j(C.request(ana.card, 'budget', ana.choices.budget.options[3].input)).body, { monthly_budget_usd: null });
|
|
519
|
+
const kai = rows.find((r) => r.name === 'kai');
|
|
520
|
+
assert.equal(kai.fact, 'no limit');
|
|
521
|
+
assert.ok(!kai.choices.budget.options.some((o) => o.key === 'none'), 'no limit is not offered to someone without one');
|
|
522
|
+
assert.equal(C.answer(ana.card, 'budget', {}).toast, 'Ana Ruiz has a new monthly limit.');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test('Review: a signed-off promise is undone in place, the reason one tap', () => {
|
|
526
|
+
const rows = actedRows(moreDesks(), 'review', 2);
|
|
527
|
+
assert.deepEqual(rows.map((r) => r.name), ['Refunds land'], 'only what is signed off');
|
|
528
|
+
const r = rows[0];
|
|
529
|
+
assert.equal(r.fact, 'signed off oct 2');
|
|
530
|
+
assert.deepEqual(r.actions.map((a) => a.label), ['undo']);
|
|
531
|
+
assert.ok(r.choices.unsign.options.every((o) => o.label.length <= D.LIMITS.chip));
|
|
532
|
+
assert.deepEqual(j(C.request(r.card, 'unsign', r.choices.unsign.options[0].input)), { method: 'POST', url: '/api/bongos/done-when/11/unsatisfy', body: { reason: 'it broke again' } });
|
|
533
|
+
assert.equal(C.answer(r.card, 'unsign', {}).kind, 'settle');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('a building held by a sitting: its desk acts are filed as that building\'s matter, which acts once passed', () => {
|
|
537
|
+
const filed = (dk, key, floor, name, act, pick) => {
|
|
538
|
+
const r = actedRows(dk, key, floor).find((x) => x.name === name);
|
|
539
|
+
assert.ok(r, `${name} on ${key}`);
|
|
540
|
+
const input = pick == null ? {} : r.choices[act].options[pick].input;
|
|
541
|
+
return { req: j(C.request(r.card, act, input)), card: r.card };
|
|
542
|
+
};
|
|
543
|
+
const t = filed(heldBy(moreDesks(), 'treasury'), 'treasury', 3, 'Ana Ruiz', 'budget', 1);
|
|
544
|
+
assert.equal(t.req.url, '/api/bongos/government/board/matters');
|
|
545
|
+
assert.deepEqual(t.req.body.action, { type: 'set_budget', builder_id: 7, monthly_usd: 100 });
|
|
546
|
+
assert.equal(t.req.body.area, 'treasury');
|
|
547
|
+
assert.equal(C.answer(t.card, 'budget', {}).toast, 'Sent to the council. It is carried out when they pass it.');
|
|
548
|
+
const rv = filed(heldBy(moreDesks(), 'review'), 'review', 1, 'Refunds work', 'force', 0);
|
|
549
|
+
assert.deepEqual(rv.req.body, { area: 'review', title: 'Sign off: Refunds work', action: { type: 'satisfy_criterion', criterion_id: 8 } });
|
|
550
|
+
const au = heldBy(moreDesks(), 'automation');
|
|
551
|
+
const sw = actedRows(au, 'automation', 0).find((x) => x.actions.some((a) => a.key === 'switch_on'));
|
|
552
|
+
assert.deepEqual(j(C.request(sw.card, 'switch_on', {})).body.action, { type: 'switch_on' });
|
|
553
|
+
// where the building decides alone, the same tap is the direct route
|
|
554
|
+
const alone = filed(heldBy(moreDesks(), 'treasury', 'decide'), 'treasury', 3, 'Ana Ruiz', 'budget', 1);
|
|
555
|
+
assert.equal(alone.req.url, '/api/bongos/builders/7/budget');
|
|
556
|
+
const plain = filed(moreDesks(), 'treasury', 3, 'Ana Ruiz', 'budget', 1);
|
|
557
|
+
assert.equal(plain.req.url, '/api/bongos/builders/7/budget');
|
|
558
|
+
// the new desks keep the line rule while a sitting holds them
|
|
559
|
+
for (const key of ['treasury', 'review', 'automation']) {
|
|
560
|
+
const dk = heldBy(moreDesks({ longWords: true }), key);
|
|
561
|
+
I.PLAN[key].floors.forEach((_f, floor) => assert.deepEqual(D.breaks(D.deskModel({ key, floor, docket: dk, goals: GOALS, now: NOW, viewerId: '95' })), []));
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
497
565
|
test('the second-pass desks keep the line rule on every floor', () => {
|
|
498
566
|
for (const longWords of [false, true]) {
|
|
499
567
|
for (const preflight of [null, { target: '1.21.20', ok: true }, { target: '1.21.20', ok: false, error: LONG }]) {
|
|
@@ -555,3 +623,175 @@ test('the Treasury desk reads the newest backup without blocking (no sync disk r
|
|
|
555
623
|
const body = src.split('async function newestDumpAge')[1].split('\nfunction latestDump')[0];
|
|
556
624
|
assert.doesNotMatch(body, /Sync\(/);
|
|
557
625
|
});
|
|
626
|
+
|
|
627
|
+
// ── the Planning desks (task 1004560) ─────────────────────────────────────────
|
|
628
|
+
// Priorities, Ideas to ratify and Waiting on you, as on the Desks board, built
|
|
629
|
+
// from the REAL server mappers through the docket's own normalisers.
|
|
630
|
+
const ideasDocket = SRV('modules/ideas/governor-docket.js');
|
|
631
|
+
const ORDER_GOALS = [
|
|
632
|
+
{ id: 31, title: 'Governor city', status: 'open', version_id: 'V2' }, { id: 32, title: 'Payments', status: 'open', version_id: 'V2' },
|
|
633
|
+
{ id: 33, title: 'Onboarding', status: 'open', version_id: 'V2' }, { id: 34, title: 'Elsewhere', status: 'open', version_id: 'V9' },
|
|
634
|
+
];
|
|
635
|
+
const ORDER_CRITERIA = [{ goal_id: 31, satisfied: true }, { goal_id: 31, satisfied: true }, { goal_id: 32, satisfied: true }, { goal_id: 32, satisfied: false }, { goal_id: 32, satisfied: false }];
|
|
636
|
+
function planningDocket({ longWords = false, sitting = null } = {}) {
|
|
637
|
+
const t = (s) => (longWords ? LONG + ' ' + s : s);
|
|
638
|
+
const goals = ORDER_GOALS.map((g) => ({ ...g, title: t(g.title) }));
|
|
639
|
+
const desk = (key, rows) => ({ key, building: 'planning', status: 'live', rows: rows.map((r) => DK.normalizeDeskRow(r, { desk: key, building: 'planning' })).filter(Boolean) });
|
|
640
|
+
const ideas = [
|
|
641
|
+
{ id: 5, status: 'open', kind: 'criterion-proposal', title: t('Calm mode promise'), body_md: JSON.stringify({ target_version: 'V2', criterion_md: t('A quiet floor shows one line') }), created_at: '2026-09-28T09:00:00.000Z' },
|
|
642
|
+
{ id: 6, status: 'open', kind: 'idea', grade: 'full', title: t('Objection goes back'), template_key: 'base-v2', field_values: { success: 'It comes back with its reason', scope: 'the return path' }, created_at: '2026-09-25T09:00:00.000Z' },
|
|
643
|
+
{ id: 7, status: 'open', kind: 'idea', grade: 'full', title: 'Still sitting', template_key: 'base-v2', field_values: { success: 'x' } },
|
|
644
|
+
{ id: 8, status: 'open', kind: 'idea', grade: 'full', title: t('No shape'), template_key: 'base-v0', field_values: {} },
|
|
645
|
+
];
|
|
646
|
+
const blockers = [
|
|
647
|
+
{ id: 12, title: t('Payment provider'), body_md: 'Which one?\n- Option A: Stripe — cheaper\n- **Option B:** Paddle (merchant of record)\n- Option C: Wait a month', created_at: '2026-09-30T10:00:00.000Z', blocking_task_ids: [1, 2, 3, 4] },
|
|
648
|
+
{ id: 13, title: t('Hosting'), body_md: 'No options listed.', created_at: '2026-10-01T10:00:00.000Z', blocking_task_ids: [] },
|
|
649
|
+
];
|
|
650
|
+
const matters = blockers.map((b) => DK.normalizeMatter(ideasDocket.blockerDocketMatter(b, true), { source: 'blockers', building: 'planning' })).filter(Boolean);
|
|
651
|
+
const over = {
|
|
652
|
+
desks: [
|
|
653
|
+
desk('planning.order', lifeDocket.orderDeskRows(goals, ORDER_CRITERIA, 'V2', true)),
|
|
654
|
+
desk('planning.ratify', ideasDocket.ratifyDeskRows(ideas, [{ id: 'V2', name: 'Version two' }, { id: 'V3' }], new Set(['6', '8']))),
|
|
655
|
+
],
|
|
656
|
+
};
|
|
657
|
+
const dk = docket(matters, over);
|
|
658
|
+
if (sitting) dk.buildings.find((b) => b.key === 'planning').decision = sitting.decision, dk.buildings.find((b) => b.key === 'planning').holder_count = sitting.holders;
|
|
659
|
+
return dk;
|
|
660
|
+
}
|
|
661
|
+
const planDesk = (dk, floor, extra = {}) => D.deskModel({ key: 'planning', floor, docket: dk, goals: GOALS, now: NOW, viewerId: '95', ...extra });
|
|
662
|
+
|
|
663
|
+
test('Planning: the three desks are the first three floors, as on the Desks board', () => {
|
|
664
|
+
assert.deepEqual(I.PLAN.planning.floors.slice(0, 3).map((f) => f[0]), ['Priorities', 'Ideas to ratify', 'Waiting on you']);
|
|
665
|
+
assert.deepEqual(I.PLAN.planning.floors.slice(0, 3).map((f) => f[2].desk || f[2].source), ['planning.order', 'planning.ratify', 'blockers']);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test('Priorities: the version\'s open goals in order with promise pips; arrows move them; a save bar with undo sets the order', () => {
|
|
669
|
+
assert.deepEqual(lifeDocket.orderDeskRows(ORDER_GOALS, ORDER_CRITERIA, 'V2', false), [], 'no rows for a viewer who may not set the order');
|
|
670
|
+
assert.deepEqual(lifeDocket.orderDeskRows(ORDER_GOALS.slice(0, 1), [], 'V2', true), [], 'one goal has no order to set');
|
|
671
|
+
const dk = planningDocket();
|
|
672
|
+
const desk = planDesk(dk, 0);
|
|
673
|
+
assert.deepEqual(desk.rows.map((r) => [r.pos, r.name, r.fact]), [[1, 'Governor city', '2 of 2 promises kept'], [2, 'Payments', '1 of 3 promises kept'], [3, 'Onboarding', 'no promises yet']], 'only the building version\'s open goals, in their order');
|
|
674
|
+
assert.deepEqual(desk.rows[1].pips.map((p) => p.on), [true, false, false], 'one pip per promise, filled when kept');
|
|
675
|
+
assert.deepEqual(desk.rows[0].actions.map((a) => a.label), ['move up', 'move down']);
|
|
676
|
+
assert.equal(desk.status, '3 goals in order');
|
|
677
|
+
assert.equal(desk.hot, false);
|
|
678
|
+
assert.deepEqual(desk.tiles.map((t) => [t.v, t.l]), [['60%', 'promises kept'], ['5', 'on record']]);
|
|
679
|
+
assert.equal(desk.save, null, 'nothing to save until the order changes');
|
|
680
|
+
const keys = desk.rows.map((r) => r.key);
|
|
681
|
+
assert.deepEqual(D.moved(keys, keys[0], -1), keys, 'the top cannot move up');
|
|
682
|
+
const order = D.moved(keys, keys[2], -1);
|
|
683
|
+
const moved = planDesk(dk, 0, { order });
|
|
684
|
+
assert.deepEqual(moved.rows.map((r) => r.name), ['Governor city', 'Onboarding', 'Payments']);
|
|
685
|
+
assert.deepEqual(j(moved.save), { line: 'new order · saved for everyone', label: 'save order', council: false, said: 'Saved. Goals are worked in this order now.', set: 'V2', ids: ['31', '33', '32'], names: ['Governor city', 'Onboarding', 'Payments'] });
|
|
686
|
+
assert.deepEqual(j(C.orderRequest(moved.save)), { method: 'POST', url: '/api/bongos/goals/reorder', body: { version_id: 'V2', ordered_ids: [31, 33, 32] } });
|
|
687
|
+
assert.deepEqual(D.breaks(moved), []);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('Priorities: where the decision table puts Planning to a sitting, the save files a Planning matter and says so', () => {
|
|
691
|
+
const majority = planningDocket({ sitting: { decision: { rule: 'majority', sitting: true, who: 'the council' }, holders: 5 } });
|
|
692
|
+
const keys = planDesk(majority, 0).rows.map((r) => r.key);
|
|
693
|
+
const save = planDesk(majority, 0, { order: D.moved(keys, keys[1], -1) }).save;
|
|
694
|
+
assert.equal(save.line, 'new order · goes to the council · 3 yes needed');
|
|
695
|
+
assert.equal(save.label, 'send to council');
|
|
696
|
+
const req = j(C.orderRequest(save));
|
|
697
|
+
assert.equal(req.url, '/api/bongos/government/board/matters');
|
|
698
|
+
assert.equal(req.body.area, 'planning');
|
|
699
|
+
assert.match(req.body.detail_md, /1\. Payments\n2\. Governor city\n3\. Onboarding$/);
|
|
700
|
+
const alone = planningDocket({ sitting: { decision: { rule: 'decide', sitting: true }, holders: 1 } });
|
|
701
|
+
assert.equal(planDesk(alone, 0, { order: D.moved(keys, keys[1], -1) }).save.council, false, 'a sitting that decides alone sets the order itself');
|
|
702
|
+
const consent = planningDocket({ sitting: { decision: { rule: 'consent', sitting: true }, holders: 4 } });
|
|
703
|
+
const cs = planDesk(consent, 0, { order: D.moved(keys, keys[1], -1) }).save;
|
|
704
|
+
assert.match(cs.line, /unless one objects/);
|
|
705
|
+
assert.equal(cs.said, 'The new order is with the council. It passes unless someone objects.', 'the toast is whole sentences');
|
|
706
|
+
assert.equal(save.said, 'The new order is with the council. It needs 3 yes.');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test('Ideas to ratify: a promise ratifies in one tap; a carried Full Idea becomes a goal by a version chip, or opens its page; a sitting idea stays the board\'s vote', () => {
|
|
710
|
+
const dk = planningDocket();
|
|
711
|
+
const rows = D.rowsFor('planning', 1, dk, { now: NOW, goals: GOALS });
|
|
712
|
+
const promise = rows.find((r) => r.name === 'Calm mode promise');
|
|
713
|
+
assert.deepEqual(promise.actions.map((a) => a.label), ['ratify', 'open']);
|
|
714
|
+
assert.deepEqual(j(C.request(promise.card, 'ratify')), { method: 'POST', url: '/api/bongos/inbox/5/ratify', body: { notes_md: 'Ratified on the floor desk.' } });
|
|
715
|
+
const full = rows.find((r) => r.name === 'Objection goes back');
|
|
716
|
+
assert.deepEqual(full.actions.map((a) => a.label), ['make goal', 'open']);
|
|
717
|
+
assert.deepEqual(full.choices.ratify_goal.options.map((o) => o.label), ['V2', 'V3'], 'the target version is a chip');
|
|
718
|
+
assert.deepEqual(j(C.request(full.card, 'ratify_goal', full.choices.ratify_goal.options[1].input)), { method: 'POST', url: '/api/bongos/inbox/6/ratify-goal', body: { target_version: 'V3', notes_md: 'Ratified on the floor desk.' } });
|
|
719
|
+
assert.ok(!rows.some((r) => r.name === 'Still sitting'), 'a Full Idea the board has not carried is not ratified from here');
|
|
720
|
+
const shapeless = rows.find((r) => r.name === 'No shape');
|
|
721
|
+
assert.deepEqual(shapeless.actions.map((a) => [a.key, a.go]), [['open', '/idea/8']], 'inputs that do not fit chips: a link');
|
|
722
|
+
assert.match(planDesk(dk, 1).status, /^3 to ratify · oldest/, 'counted in the noun of its desk');
|
|
723
|
+
// the board's vote row keeps its place and shows its seats as pips
|
|
724
|
+
const room = { open: [{ id: '900', subject_type: 'full_idea', subject: { title: 'Watch a goal' }, constitution: { pass_rule: 'majority' }, votes: [{ voter_id: '3', direction: 'yes' }, { voter_id: '4', direction: 'yes' }], tally: { open: 3 } }], closed: [] };
|
|
725
|
+
const withVote = docket(dk.matters.concat([matter({ id: 'board:900', source: 'board', short: 'idea to ratify', title: 'Watch a goal', state: 'needs' })]), { desks: dk.desks });
|
|
726
|
+
const vote = D.rowsFor('planning', 1, withVote, { now: NOW, room, viewerId: '95' }).find((r) => r.source === 'board');
|
|
727
|
+
assert.ok(vote.pips && vote.pips.filter((p) => p.on).length === 2, 'a filled pip per yes');
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test('Waiting on you: a blocker is decided by one of its own options, or go ahead / not now / need more, written as the resolution note', () => {
|
|
731
|
+
assert.equal(ideasDocket.blockerDocketMatter({ id: 1, title: 'x', created_at: null }, false).acts, undefined, 'no act for a viewer who may not resolve');
|
|
732
|
+
const dk = planningDocket();
|
|
733
|
+
const rows = D.rowsFor('planning', 2, dk, { now: NOW });
|
|
734
|
+
const pay = rows.find((r) => r.name === 'Payment provider');
|
|
735
|
+
assert.deepEqual(pay.actions.map((a) => a.label), ['decide']);
|
|
736
|
+
assert.equal(pay.show, 'decide', 'its chips stand open: deciding is picking one');
|
|
737
|
+
assert.deepEqual(pay.choices.decide.options.map((o) => o.label), ['Stripe', 'Paddle', 'Wait a month']);
|
|
738
|
+
assert.deepEqual(j(C.request(pay.card, 'decide', pay.choices.decide.options[1].input)), { method: 'POST', url: '/api/bongos/blockers/12/resolve', body: { resolution_note: 'Decided on the floor desk: Paddle.' } });
|
|
739
|
+
const host = rows.find((r) => r.name === 'Hosting');
|
|
740
|
+
assert.deepEqual(host.choices.decide.options.map((o) => o.label), ['go ahead', 'not now', 'need more']);
|
|
741
|
+
const desk = planDesk(dk, 2);
|
|
742
|
+
assert.equal(desk.hot, true);
|
|
743
|
+
assert.match(desk.status, /^2 waiting · oldest/);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test('the Planning desks keep the line rule, long words or short, with a moved order and a sitting', () => {
|
|
747
|
+
for (const longWords of [false, true]) {
|
|
748
|
+
const dk = planningDocket({ longWords, sitting: { decision: { rule: 'unanimous', sitting: true }, holders: 12 } });
|
|
749
|
+
const keys = planDesk(dk, 0).rows.map((r) => r.key);
|
|
750
|
+
const desks = [0, 1, 2, 3, 4, 5].map((f) => planDesk(dk, f, f === 0 ? { order: keys.slice().reverse() } : {}));
|
|
751
|
+
assert.ok(desks[0].save, 'the moved order has its save bar');
|
|
752
|
+
assert.deepEqual(desks.flatMap((d) => D.breaks(d)), []);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
test('the page: the arrows move a goal in place, undo puts it back, save sends the order', async () => {
|
|
757
|
+
const dk = planningDocket();
|
|
758
|
+
const p = await bootPage('#building=planning', dk.matters, { over: { desks: dk.desks } });
|
|
759
|
+
p.byId.get('insideFloors').children[0].fire('click');
|
|
760
|
+
const rowsEl = () => p.part('desk-rows').children;
|
|
761
|
+
const arrows = (i) => rowsEl()[i].children[0].children.find((c) => c.className === 'desk-acts').children;
|
|
762
|
+
assert.deepEqual(rowsEl().map((r) => r.children[0].children[0].textContent), ['1', '2', '3'], 'each goal shows its place');
|
|
763
|
+
assert.equal(p.part('desk-save'), undefined);
|
|
764
|
+
assert.equal(arrows(0)[0].attrs.disabled, '', 'the top goal cannot go up');
|
|
765
|
+
assert.equal(arrows(2)[1].attrs.disabled, '', 'the last goal cannot go down');
|
|
766
|
+
assert.equal(arrows(1)[0].attrs.disabled, undefined);
|
|
767
|
+
rowsEl()[0].children[0].children[2].fire('click');
|
|
768
|
+
assert.deepEqual(rowsEl()[0].children[1].children.map((f) => f.textContent), ['version V2'], 'a goal\'s facts open in place like any row');
|
|
769
|
+
rowsEl()[0].children[0].children[2].fire('click');
|
|
770
|
+
arrows(0)[1].fire('click');
|
|
771
|
+
assert.deepEqual(rowsEl().map((r) => r.children[0].children[2].children[0].textContent), ['Payments', 'Governor city', 'Onboarding']);
|
|
772
|
+
const bar = p.part('desk-save');
|
|
773
|
+
assert.deepEqual(bar.children.map((c) => c.textContent), ['new order · saved for everyone', 'undo', 'save order']);
|
|
774
|
+
bar.children[1].fire('click');
|
|
775
|
+
assert.equal(p.part('desk-save'), undefined, 'undo puts the order back');
|
|
776
|
+
arrows(2)[0].fire('click');
|
|
777
|
+
p.part('desk-save').children[2].fire('click');
|
|
778
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 0));
|
|
779
|
+
assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/goals/reorder', body: { version_id: 'V2', ordered_ids: [31, 33, 32] } }]);
|
|
780
|
+
assert.match(p.byId.get('streetToast').textContent, /worked in this order/);
|
|
781
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('the page: one tap on its option decides a blocker, its options standing open with no extra button', async () => {
|
|
785
|
+
const dk = planningDocket();
|
|
786
|
+
const p = await bootPage('#building=planning', dk.matters, { over: { desks: dk.desks } });
|
|
787
|
+
p.byId.get('insideFloors').children[2].fire('click');
|
|
788
|
+
const pay = p.part('desk-rows').children.find((r) => r.children[0].children[1].children[0].textContent === 'Payment provider');
|
|
789
|
+
assert.equal(pay.children[0].children.find((c) => c.className === 'desk-acts').children.length, 0, 'no decide button: the chip is the act');
|
|
790
|
+
const chips = pay.children[1];
|
|
791
|
+
assert.equal(chips.className, 'desk-choices');
|
|
792
|
+
assert.deepEqual(chips.children.map((c) => c.textContent), ['pick one', 'Stripe', 'Paddle', 'Wait a month']);
|
|
793
|
+
chips.children[2].fire('click');
|
|
794
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 0));
|
|
795
|
+
assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/blockers/12/resolve', body: { resolution_note: 'Decided on the floor desk: Paddle.' } }]);
|
|
796
|
+
assert.match(p.byId.get('streetToast').textContent, /Decided/);
|
|
797
|
+
});
|
|
@@ -231,7 +231,7 @@ test('a floor bound to a reading shows its figure and line when live; Planning c
|
|
|
231
231
|
assert.equal(I.civicInfo('treasury', { docket: d, now: NOW }).floors.find((f) => f.name === 'Reserve').count, '$1,100');
|
|
232
232
|
const goals = [goal(1), goal(2), goal(3), goal(4), goal(5), goal(6, 'achieved')];
|
|
233
233
|
const m = I.buildingModel({ key: 'planning', docket: d, goals, mode: 'dark', now: NOW });
|
|
234
|
-
const g = I.civicInfo('planning', { docket: d, goals: goals.filter((x) => x.status === 'open'), now: NOW }).floors
|
|
234
|
+
const g = I.civicInfo('planning', { docket: d, goals: goals.filter((x) => x.status === 'open'), now: NOW }).floors.find((f) => f.name === 'Goals');
|
|
235
235
|
assert.deepEqual([g.name, g.count, g.line], ['Goals', '5', '5 goals open']);
|
|
236
236
|
assert.equal(g.lines.length, 5, 'four goals and a line for the rest');
|
|
237
237
|
assert.ok(m.texts.some((t) => t.v === '5' && t.s === 20), 'only open goals are counted on the wall');
|
|
@@ -311,9 +311,9 @@ test('a floor the docket cannot answer never shows a number; Goals falls back to
|
|
|
311
311
|
for (const [n, st] of [['Ranks', 'not_yours'], ['The council', 'off'], ['Newcomers', 'unavailable'], ['Testers', 'unavailable']]) {
|
|
312
312
|
assert.deepEqual([by[n].status, by[n].count], [st, ''], `${n} says ${st} and no number`);
|
|
313
313
|
}
|
|
314
|
-
const g = I.civicInfo('planning', { docket: docket(), goals: [goal(1), goal(2)], now: NOW }).floors
|
|
314
|
+
const g = I.civicInfo('planning', { docket: docket(), goals: [goal(1), goal(2)], now: NOW }).floors.find((f) => f.name === 'Goals');
|
|
315
315
|
assert.deepEqual([g.count, g.line], ['2', '2 goals open'], 'an older docket without floors still counts the open goals');
|
|
316
|
-
const live = I.civicInfo('planning', { docket: docket([], { floors: [floorRow('planning.goals', { count: '1', line: '1 goal open', lines: [['Hall', '5 of 5 kept']] })] }), goals: [goal(1), goal(2)], now: NOW }).floors
|
|
316
|
+
const live = I.civicInfo('planning', { docket: docket([], { floors: [floorRow('planning.goals', { count: '1', line: '1 goal open', lines: [['Hall', '5 of 5 kept']] })] }), goals: [goal(1), goal(2)], now: NOW }).floors.find((f) => f.name === 'Goals');
|
|
317
317
|
assert.deepEqual([live.count, live.lines], ['1', [['Hall', '5 of 5 kept']]], 'with the floor, its promises kept of total');
|
|
318
318
|
});
|
|
319
319
|
|