@bongos/core 1.21.54 → 1.21.55
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 +23 -23
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +88 -79
- package/modules/government/docket.js +9 -4
- package/modules/hall-ui/public/street-card.js +26 -1
- package/modules/hall-ui/public/street-desk.js +41 -4
- package/modules/hall-ui/public/street.css +2 -0
- package/modules/hall-ui/public/street.js +59 -4
- package/modules/hall-ui/public/street.states.json +41 -1
- package/modules/hall-ui/records/governor-street.md +13 -0
- package/modules/lifecycle/goal-category.js +15 -0
- package/modules/lifecycle/routes/project-settings.js +42 -7
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/goal_category.mjs +16 -0
- package/tests/hall_street_desk.mjs +171 -4
|
@@ -525,6 +525,14 @@ const settingsRoute = SRV('modules/lifecycle/routes/project-settings.js');
|
|
|
525
525
|
const modulesRoute = SRV('src/bongos/routes/modules.js');
|
|
526
526
|
const backupRoute = SRV('src/bongos/routes/backup.js');
|
|
527
527
|
const accessRoute = SRV('modules/onboarding/routes/access-requests.js');
|
|
528
|
+
// the work categories (task 1004580): three offered, in their order, and one retired
|
|
529
|
+
const CATS = [
|
|
530
|
+
{ id: 1, slug: 'money', label: 'Money', sort_order: 10, active: true },
|
|
531
|
+
{ id: 2, slug: 'walls', label: 'The walls', sort_order: 20, active: true },
|
|
532
|
+
{ id: 3, slug: 'hall', label: 'The hall', sort_order: 21, active: true },
|
|
533
|
+
{ id: 4, slug: 'legal', label: 'Legal', sort_order: 15, active: false },
|
|
534
|
+
];
|
|
535
|
+
const CAT_GOALS = new Map([['1', 4], ['2', 1]]);
|
|
528
536
|
const PREVIEW = { instance_id: 41, served_version: '1.21.19', recommended_version: '1.21.20', behind: true, observed_at: '2026-10-03T10:00:00.000Z' };
|
|
529
537
|
|
|
530
538
|
function moreDesks({ preflight = null, longWords = false } = {}) {
|
|
@@ -534,7 +542,7 @@ function moreDesks({ preflight = null, longWords = false } = {}) {
|
|
|
534
542
|
dk.matters.push(DK.normalizeMatter({ id: '1.21.20', rule: 'decide', state: 'needs', title: 'Take core 1.21.20 (running 1.21.19)', short: 'core 1.21.20', href: '/settings#software-update', ref: { type: 'core_version', id: '1.21.20' } }, { source: 'core_update', building: 'release' }));
|
|
535
543
|
dk.desks.push(
|
|
536
544
|
desk('release.core', 'release', coreRoute.coreDeskRows({ ...PREVIEW, preflight })),
|
|
537
|
-
desk('rules.settings', 'rules', settingsRoute.settingsDeskRows({ days: 14, categories:
|
|
545
|
+
desk('rules.settings', 'rules', settingsRoute.settingsDeskRows({ days: 14, categories: CATS.map((c) => ({ ...c, label: t(c.label) })), goals: CAT_GOALS, mayAdd: true })),
|
|
538
546
|
desk('rules.modules', 'rules', modulesRoute.moduleDeskRows([{ key: 'discord', title: t('Discord'), enabled: true }, { key: 'economy', title: 'Economy', enabled: false }])),
|
|
539
547
|
desk('treasury.backup', 'treasury', backupRoute.backupDeskRows({ age_hours: 30, stale: false }, false)),
|
|
540
548
|
desk('people.invite', 'people', [accessRoute.INVITE_DESK_ROW]),
|
|
@@ -643,13 +651,13 @@ test('Release: the core update is checked first; take is offered only after a pa
|
|
|
643
651
|
assert.deepEqual(D.breaks(desk), []);
|
|
644
652
|
});
|
|
645
653
|
|
|
646
|
-
test('Rules: the rot timer as day chips, work categories
|
|
654
|
+
test('Rules: the rot timer as day chips, work categories in place (never a link), modules on in one tap and off with a yes', () => {
|
|
647
655
|
const rows = actedRows(moreDesks(), 'rules', 0);
|
|
648
656
|
const rot = rows.find((r) => r.name === 'Rot timer');
|
|
649
657
|
assert.deepEqual(rot.choices.rot.options.map((o) => o.key), ['7', '30', '60', '90'], 'the current value is not offered');
|
|
650
658
|
assert.deepEqual(j(C.request(rot.card, 'rot', rot.choices.rot.options[1].input)), { method: 'PATCH', url: '/api/bongos/project-settings/rot-days', body: { days: 30 } });
|
|
651
|
-
|
|
652
|
-
assert.
|
|
659
|
+
assert.equal(rows.find((r) => r.name === 'Work categories'), undefined, 'no link-out row (task 1004580)');
|
|
660
|
+
assert.ok(rows.every((r) => r.actions.every((a) => !a.go)), 'nothing on the Rules floor leaves the building');
|
|
653
661
|
const on = rows.find((r) => r.name === 'Economy');
|
|
654
662
|
assert.deepEqual(j(C.request(on.card, 'module_on')), { method: 'POST', url: '/api/bongos/modules/economy/enable', body: { enabled: true } });
|
|
655
663
|
const off = rows.find((r) => r.name === 'Discord');
|
|
@@ -1300,3 +1308,162 @@ test('1004579 the card: a failed upload says why on the card\'s line and keeps t
|
|
|
1300
1308
|
assert.equal(p.byId.get('streetTodayN').textContent, '0 decided');
|
|
1301
1309
|
assert.equal(walk(body, (n) => n.className === 'mcard-error').length, 0, 'said once, on its own line');
|
|
1302
1310
|
});
|
|
1311
|
+
|
|
1312
|
+
// ── task 1004580: the work categories, edited in place on the Rules desk ─────
|
|
1313
|
+
// The Project settings page's category list (project-settings.js catsBodyHtml)
|
|
1314
|
+
// renames, retires and brings back, reorders and adds. Each of those is now an
|
|
1315
|
+
// act on the floor desk, sent to the SAME two routes the page uses, under the
|
|
1316
|
+
// same atoms; the desk's old link-out row is gone.
|
|
1317
|
+
const catDesk = (rows) => ({ key: 'rules.settings', building: 'rules', status: 'live', rows: rows.map((r) => DK.normalizeDeskRow(r, { desk: 'rules.settings', building: 'rules' })).filter(Boolean) });
|
|
1318
|
+
const catRows = (opts = {}) => settingsRoute.settingsDeskRows({ days: 14, categories: CATS, goals: CAT_GOALS, mayAdd: true, ...opts });
|
|
1319
|
+
const catDocket = (opts) => docket([], { desks: [catDesk(catRows(opts))] });
|
|
1320
|
+
const rulesRows = (opts) => actedRows(catDocket(opts), 'rules', 0);
|
|
1321
|
+
const byName = (rows, n) => rows.find((r) => r.name === n);
|
|
1322
|
+
|
|
1323
|
+
test('1004580 the server: one row per category, offered ones in order then retired, its goals as the fact; add only with goal.create', () => {
|
|
1324
|
+
const rows = catRows();
|
|
1325
|
+
assert.deepEqual(rows.map((r) => r.id), ['rot', 'cat-1', 'cat-2', 'cat-3', 'cat-4', 'cat-new']);
|
|
1326
|
+
assert.deepEqual(rows.slice(1, 5).map((r) => r.fact), ['4 goals', '1 goal', '0 goals', 'retired · 0 goals']);
|
|
1327
|
+
assert.deepEqual(rows[1].acts, [{ key: 'rename', field: 'name' }, { key: 'retire' }]);
|
|
1328
|
+
assert.deepEqual(rows[4].acts, [{ key: 'restore' }, { key: 'rename', field: 'name' }], 'a retired one is brought back, never retired twice');
|
|
1329
|
+
assert.deepEqual(rows.slice(1, 5).map((r) => r.seq), [10, 20, 21, undefined], 'only an offered one has a place to move');
|
|
1330
|
+
assert.ok(rows.slice(1).every((r) => r.stay && r.tool && !r.href), 'every category row stays, counts as a tool and links nowhere');
|
|
1331
|
+
assert.deepEqual(rows[5].acts, [{ key: 'add', field: 'name' }]);
|
|
1332
|
+
assert.equal(rows[5].fact, '3 in use');
|
|
1333
|
+
assert.equal(catRows({ mayAdd: false }).find((r) => r.id === 'cat-new'), undefined, 'no add row for a viewer POST /work-categories refuses');
|
|
1334
|
+
// the docket keeps the typed name, the place and stay; a place that is no whole number is dropped
|
|
1335
|
+
const n = DK.normalizeDeskRow(rows[1], { desk: 'rules.settings', building: 'rules' });
|
|
1336
|
+
assert.deepEqual([n.acts[0].field, n.seq, n.stay], ['name', 10, true]);
|
|
1337
|
+
assert.equal(DK.normalizeDeskRow({ ...rows[1], seq: 1.5 }, { desk: 'rules.settings', building: 'rules' }).seq, undefined);
|
|
1338
|
+
assert.equal(DK.normalizeDeskRow({ ...rows[1], acts: [{ key: 'rename', field: 'password' }] }, { desk: 'rules.settings', building: 'rules' }).acts[0].field, undefined, 'only a name may be typed');
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
test('1004580 the server: the desk is read under the settings page’s own atom, and the add row under POST’s', () => {
|
|
1342
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/project-settings.js'), 'utf8');
|
|
1343
|
+
const decl = src.split('function contributeSettingsDesk')[1].split('\n}')[0];
|
|
1344
|
+
assert.match(decl, /desk: 'rules\.settings',\s*gate: \{ permission: 'project\.settings\.manage' \}/, 'the same atom as GET /project-settings and PATCH /work-categories/:id');
|
|
1345
|
+
assert.match(decl, /mayAdd: [^\n]*ctx\.has\('goal\.create'\)/, 'the same atom as POST /work-categories');
|
|
1346
|
+
const goals = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/goals.js'), 'utf8');
|
|
1347
|
+
assert.match(goals, /router\.patch\('\/work-categories\/:id', auth\.requireBuilder,\s*auth\.requirePermission\('project\.settings\.manage'\)/);
|
|
1348
|
+
assert.match(goals, /router\.post\('\/work-categories', auth\.requireBuilder, auth\.requirePermission\('goal\.create'\)/);
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
test('1004580 the desk: each act is sent to the settings page’s own route, and nothing typed sends nothing', () => {
|
|
1352
|
+
const rows = rulesRows();
|
|
1353
|
+
const money = byName(rows, 'Money');
|
|
1354
|
+
assert.deepEqual(money.actions.map((a) => [a.key, a.label, a.tone]), [['rename', 'rename', 'primary'], ['retire', 'retire', 'quiet']]);
|
|
1355
|
+
assert.deepEqual([money.choices.rename.ask, money.choices.rename.field, money.choices.rename.value], ['new name', 'name', 'Money'], 'a rename starts from the name it has');
|
|
1356
|
+
assert.deepEqual(j(C.request(money.card, 'rename', { option: ' Revenue ' })), { method: 'PATCH', url: '/api/bongos/work-categories/1', body: { label: 'Revenue' } });
|
|
1357
|
+
assert.equal(C.request(money.card, 'rename', { option: ' ' }), null);
|
|
1358
|
+
assert.deepEqual(j(C.request(money.card, 'retire')), { method: 'PATCH', url: '/api/bongos/work-categories/1', body: { active: false } });
|
|
1359
|
+
const legal = byName(rows, 'Legal');
|
|
1360
|
+
assert.deepEqual(legal.actions.map((a) => a.label), ['bring back', 'rename']);
|
|
1361
|
+
assert.deepEqual(j(C.request(legal.card, 'restore')), { method: 'PATCH', url: '/api/bongos/work-categories/4', body: { active: true } });
|
|
1362
|
+
assert.deepEqual(j(C.request(money.card, 'move', { sort_order: 25 })), { method: 'PATCH', url: '/api/bongos/work-categories/1', body: { sort_order: 25 } });
|
|
1363
|
+
assert.equal(C.request(money.card, 'move', { sort_order: 2.5 }), null);
|
|
1364
|
+
const add = byName(rows, 'New category');
|
|
1365
|
+
assert.deepEqual([add.actions[0].label, add.choices.add.ask, add.choices.add.field], ['add', 'its name', 'name']);
|
|
1366
|
+
assert.deepEqual(j(C.request(add.card, 'add', { option: 'Developer Relations' })), { method: 'POST', url: '/api/bongos/work-categories', body: { label: 'Developer Relations' } });
|
|
1367
|
+
assert.equal(C.request(add.card, 'add', { option: '' }), null);
|
|
1368
|
+
assert.deepEqual(j([byName(rows, 'Money').moves, byName(rows, 'The hall').moves, byName(rows, 'Legal').moves || null]), [{ up: false, down: true }, { up: true, down: false }, null]);
|
|
1369
|
+
assert.deepEqual(D.breaks(D.deskModel({ key: 'rules', floor: 0, docket: catDocket(), goals: GOALS, now: NOW })), [], 'the line rule holds');
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
test('1004580 a move: one write when a whole number sits between its new neighbours, else the order numbered again', () => {
|
|
1373
|
+
const rows = rulesRows();
|
|
1374
|
+
// Money (10) down, between The walls (20) and The hall (21): no whole number fits, so all three again in tens
|
|
1375
|
+
assert.deepEqual(D.placeMoves(rows, byName(rows, 'Money').key, 1), [{ id: '2', sort_order: 10 }, { id: '1', sort_order: 20 }, { id: '3', sort_order: 30 }]);
|
|
1376
|
+
// The hall (21) up, to after Money (10) and before The walls (20): one write, 15
|
|
1377
|
+
assert.deepEqual(D.placeMoves(rows, byName(rows, 'The hall').key, -1), [{ id: '3', sort_order: 15 }]);
|
|
1378
|
+
// The walls (20) up to the top: ten before Money
|
|
1379
|
+
assert.deepEqual(D.placeMoves(rows, byName(rows, 'The walls').key, -1), [{ id: '2', sort_order: 0 }]);
|
|
1380
|
+
assert.deepEqual(D.placeMoves(rows, byName(rows, 'Money').key, -1), [], 'the top does not go up');
|
|
1381
|
+
assert.deepEqual(D.placeMoves(rows, byName(rows, 'Legal').key, -1), [], 'a retired one has no place');
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// the page, on the Rules desk
|
|
1385
|
+
async function rulesPage(opts = {}) {
|
|
1386
|
+
const p = await bootPage('#building=rules', [], { over: { desks: [catDesk(catRows())] }, ...opts });
|
|
1387
|
+
const rowOf = (n) => p.part('desk-rows').children.find((r) => r.children[0].children[1].children[0].textContent === n);
|
|
1388
|
+
const button = (row, label) => row.children[0].children.find((c) => /desk-acts/.test(c.className)).children.find((b) => b.textContent === label);
|
|
1389
|
+
return { ...p, rowOf, button };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
test('1004580 the page: rename in place; the field starts from the name, the write is the page’s PATCH, the row stays, pip, toast, record', async () => {
|
|
1393
|
+
const p = await rulesPage({ answer: (url, body) => (body.label ? { status: 200, body: { ok: true, category: { id: 1, label: body.label } } } : null) });
|
|
1394
|
+
assert.ok(walk(p.deskEl(), (n) => n.tagName === 'a').length === 0, 'no link on the desk');
|
|
1395
|
+
assert.equal(walk(p.deskEl(), (n) => n.textContent === 'Work categories').length, 0, 'the link-out row is gone');
|
|
1396
|
+
p.button(p.rowOf('Money'), 'rename').fire('click');
|
|
1397
|
+
const ask = p.rowOf('Money').children[1];
|
|
1398
|
+
assert.equal(ask.className, 'desk-choices');
|
|
1399
|
+
assert.equal(ask.children[0].textContent, 'new name');
|
|
1400
|
+
const input = ask.children[1];
|
|
1401
|
+
assert.equal(input.value, 'Money');
|
|
1402
|
+
input.value = 'Revenue';
|
|
1403
|
+
ask.children[2].fire('click');
|
|
1404
|
+
await settle();
|
|
1405
|
+
assert.deepEqual(p.sent, [{ method: 'PATCH', url: '/api/bongos/work-categories/1', body: { label: 'Revenue' } }]);
|
|
1406
|
+
assert.ok(p.rowOf('Money'), 'the row stays on the desk');
|
|
1407
|
+
assert.equal(p.byId.get('streetToast').textContent, 'Renamed. Goals read Revenue now.');
|
|
1408
|
+
assert.match(p.byId.get('streetToast').className, /street-toast--desk/);
|
|
1409
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
1410
|
+
assert.equal(p.part('desk-record').children[1].textContent, 'renamed Money · now');
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
test('1004580 the page: retire and bring back in one tap each', async () => {
|
|
1414
|
+
const p = await rulesPage();
|
|
1415
|
+
p.button(p.rowOf('The walls'), 'retire').fire('click');
|
|
1416
|
+
await settle();
|
|
1417
|
+
p.button(p.rowOf('Legal'), 'bring back').fire('click');
|
|
1418
|
+
await settle();
|
|
1419
|
+
assert.deepEqual(p.sent, [{ method: 'PATCH', url: '/api/bongos/work-categories/2', body: { active: false } }, { method: 'PATCH', url: '/api/bongos/work-categories/4', body: { active: true } }]);
|
|
1420
|
+
assert.equal(p.byId.get('streetToast').textContent, 'Brought back. New goals can pick it again.');
|
|
1421
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '2 decided');
|
|
1422
|
+
assert.deepEqual(p.part('desk-record').children.slice(1, 3).map((x) => x.textContent), ['brought back Legal · now', 'retired The walls · now']);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
test('1004580 the page: tapping a name opens its detail in place, and it moves from there', async () => {
|
|
1426
|
+
const p = await rulesPage();
|
|
1427
|
+
p.rowOf('The hall').children[0].children[1].fire('click');
|
|
1428
|
+
const detail = p.rowOf('The hall').children[1];
|
|
1429
|
+
assert.equal(detail.className, 'desk-facts');
|
|
1430
|
+
assert.deepEqual(detail.children.slice(0, 3).map((c) => c.textContent).concat(detail.children[3].children.map((c) => c.textContent)), ['key hall', '3 of 3 in order', 'offered to new goals', 'move up'], 'the page’s detail: its key, its place, whether it is offered; the bottom one only moves up');
|
|
1431
|
+
byAct(detail, 'move-up').fire('click');
|
|
1432
|
+
await settle();
|
|
1433
|
+
assert.deepEqual(p.sent, [{ method: 'PATCH', url: '/api/bongos/work-categories/3', body: { sort_order: 15 } }]);
|
|
1434
|
+
assert.equal(p.byId.get('streetToast').textContent, 'Moved. New goals are offered the categories in this order.');
|
|
1435
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
1436
|
+
assert.equal(p.part('desk-record').children[1].textContent, 'moved The hall · now');
|
|
1437
|
+
assert.equal(p.rowOf('The hall').children[1].className, 'desk-facts', 'the detail stays open to move it again');
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
test('1004580 the page: a new category is added by its typed name, and the record names it', async () => {
|
|
1441
|
+
const p = await rulesPage({ answer: (url, body) => (url.endsWith('/work-categories') ? { status: 201, body: { ok: true, created: true, category: { id: 9, label: body.label } } } : null) });
|
|
1442
|
+
p.button(p.rowOf('New category'), 'add').fire('click');
|
|
1443
|
+
const ask = p.rowOf('New category').children[1];
|
|
1444
|
+
assert.equal(ask.children[0].textContent, 'its name');
|
|
1445
|
+
assert.equal(ask.children[1].value || '', '', 'an add starts empty');
|
|
1446
|
+
ask.children[1].value = 'Outreach';
|
|
1447
|
+
ask.children[2].fire('click');
|
|
1448
|
+
await settle();
|
|
1449
|
+
assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/work-categories', body: { label: 'Outreach' } }]);
|
|
1450
|
+
assert.equal(p.byId.get('streetToast').textContent, 'Outreach is a category now. New goals can pick it.');
|
|
1451
|
+
assert.equal(p.part('desk-record').children[1].textContent, 'added Outreach · now');
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
test('1004580 the page: a refused act says why, keeps the row, and counts nothing', async () => {
|
|
1455
|
+
const p = await rulesPage({ answer: () => ({ status: 403, body: { error: { code: 'forbidden' } } }) });
|
|
1456
|
+
p.button(p.rowOf('Money'), 'retire').fire('click');
|
|
1457
|
+
await settle();
|
|
1458
|
+
assert.equal(p.sent.length, 1);
|
|
1459
|
+
assert.equal(p.byId.get('streetToast').textContent, 'That is not yours to do.');
|
|
1460
|
+
assert.ok(p.rowOf('Money'));
|
|
1461
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '0 decided');
|
|
1462
|
+
assert.ok(!walk(p.part('desk-record'), (n) => /retired Money/.test(n.textContent)).length, 'nothing on the record');
|
|
1463
|
+
// a refused move stops at the first write and counts nothing
|
|
1464
|
+
p.rowOf('Money').children[0].children[1].fire('click');
|
|
1465
|
+
byAct(p.rowOf('Money').children[1], 'move-down').fire('click');
|
|
1466
|
+
await settle();
|
|
1467
|
+
assert.equal(p.sent.length, 2, 'the renumbering stopped at its first refusal');
|
|
1468
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '0 decided');
|
|
1469
|
+
});
|