@bongos/core 1.19.781 → 1.19.782
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +33 -33
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +50 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +29 -27
- package/docs/copy-registry.json +86 -68
- package/docs/module-api-changelog.md +2 -0
- package/modules/builder-settings/builder-needs.js +9 -3
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/collab-lib.js +94 -2
- package/modules/hall-ui/public/collab.css +110 -5
- package/modules/hall-ui/public/collab.html +51 -37
- package/modules/hall-ui/public/collab.js +153 -60
- package/modules/hall-ui/public/shell.js +3 -1
- package/modules/lifecycle/help-requests.js +24 -2
- package/modules/lifecycle/routes/help-requests.js +11 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/builder_needs.mjs +24 -0
- package/tests/collab_page.mjs +119 -0
- package/tests/hall_collab_world.mjs +7 -3
- package/tests/hall_nav.mjs +47 -0
- package/tests/help_requests.mjs +31 -0
- package/tests/social_mode.mjs +3 -1
package/tests/collab_page.mjs
CHANGED
|
@@ -167,6 +167,125 @@ test('context links to the task or goal, and to neither when absent', () => {
|
|
|
167
167
|
assert.equal(L.contextOf({}), null, 'no context means no element, not an empty one');
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
// ── A ROOM OF PEOPLE (task 1003942, owner report) ───────────────────────────
|
|
171
|
+
//
|
|
172
|
+
// "Way too text heavy, and doesn't feel collaborative at all." The page now
|
|
173
|
+
// leads with faces and figures; the rules that decide WHOSE face and WHAT number
|
|
174
|
+
// are pure in collab-lib.js, so they run here rather than being grepped.
|
|
175
|
+
|
|
176
|
+
test('an incoming row leads with the ASKER, an outgoing one with who you asked', () => {
|
|
177
|
+
const L = lib();
|
|
178
|
+
const inc = L.faceOf({ requested_by: 92, requested_by_name: 'Preview Gamma', requested_by_avatar_url: 'https://x/a.png' }, 'in');
|
|
179
|
+
assert.equal(inc.kind, 'person');
|
|
180
|
+
assert.equal(inc.id, 92);
|
|
181
|
+
assert.equal(inc.name, 'Preview Gamma');
|
|
182
|
+
assert.equal(inc.avatar, 'https://x/a.png');
|
|
183
|
+
const out = L.faceOf({ needs_builder_id: 90, needs_builder_name: 'Preview Alpha', needs_builder_avatar_url: 'https://x/b.png' }, 'out');
|
|
184
|
+
assert.equal(out.id, 90);
|
|
185
|
+
assert.equal(out.name, 'Preview Alpha');
|
|
186
|
+
assert.equal(out.avatar, 'https://x/b.png');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('a craft ask wears the craft\'s letter, never a person\'s face', () => {
|
|
190
|
+
const L = lib();
|
|
191
|
+
const f = L.faceOf({ needs_craft: 'artist' }, 'out');
|
|
192
|
+
assert.equal(f.kind, 'craft');
|
|
193
|
+
assert.equal(f.initial, 'A');
|
|
194
|
+
assert.equal(f.avatar, null);
|
|
195
|
+
assert.equal(f.name, 'artists');
|
|
196
|
+
const css = read('modules', 'hall-ui', 'public', 'collab.css');
|
|
197
|
+
assert.match(css, /\.cb-face--craft\s*\{[^}]*font-family: var\(--label-face\)/, 'drawn in the label face, so it cannot pass for a person');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('no picture means the INITIAL — and a src the page did not vouch for is dropped', () => {
|
|
201
|
+
const L = lib();
|
|
202
|
+
assert.equal(L.initialOf('preview-gamma'), 'P');
|
|
203
|
+
assert.equal(L.initialOf('@lyra'), 'L', 'a leading @ is not a letter');
|
|
204
|
+
assert.equal(L.initialOf(''), '?', 'never empty');
|
|
205
|
+
assert.equal(L.httpUrl('javascript:alert(1)'), null, 'roster.js\'s allowlist rule, kept');
|
|
206
|
+
assert.equal(L.httpUrl('https://avatars.githubusercontent.com/u/1?v=4'), 'https://avatars.githubusercontent.com/u/1?v=4');
|
|
207
|
+
const f = L.faceOf({ requested_by: 5, requested_by_login: 'gamma', requested_by_avatar_url: 'data:image/png;base64,AAAA' }, 'in');
|
|
208
|
+
assert.equal(f.avatar, null);
|
|
209
|
+
assert.equal(f.initial, 'G');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('the face stack shows each PERSON once, however many times they asked', () => {
|
|
213
|
+
const L = lib();
|
|
214
|
+
const rows = [
|
|
215
|
+
{ requested_by: 92, requested_by_name: 'Gamma' },
|
|
216
|
+
{ requested_by: 90, requested_by_name: 'Alpha' },
|
|
217
|
+
{ requested_by: 92, requested_by_name: 'Gamma' },
|
|
218
|
+
];
|
|
219
|
+
assert.deepEqual(L.askers(rows).map((f) => f.name), ['Gamma', 'Alpha']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('THREE BLOCKS: the newest three show, the rest wait behind one door', () => {
|
|
223
|
+
// The owner's direction: asked of you left, your asks right, every open ask
|
|
224
|
+
// below, each condensed to the three most recent and expandable. The split
|
|
225
|
+
// and the order are pure, so the button's number and the rows it reveals
|
|
226
|
+
// come from one place.
|
|
227
|
+
const L = lib();
|
|
228
|
+
const rows = [{ id: 1, created_at: daysAgo(9) }, { id: 2, created_at: daysAgo(0) }, { id: 3, created_at: daysAgo(4) }, { id: 4, created_at: daysAgo(1) }];
|
|
229
|
+
const newest = rows.slice().sort(L.byNewestFirst).map((r) => r.id);
|
|
230
|
+
assert.deepEqual(newest, [2, 4, 3, 1], 'newest first is the display order');
|
|
231
|
+
const { shown, hidden } = L.splitShown(rows.slice().sort(L.byNewestFirst), 3);
|
|
232
|
+
assert.deepEqual(shown.map((r) => r.id), [2, 4, 3]);
|
|
233
|
+
assert.deepEqual(hidden.map((r) => r.id), [1], 'the fourth waits behind "show all"');
|
|
234
|
+
assert.equal(L.splitShown(rows.slice(0, 2), 3).hidden.length, 0, 'two rows need no door');
|
|
235
|
+
// The ageing band still marks every row whatever the order — the criterion
|
|
236
|
+
// is met by the pill, not by the sort.
|
|
237
|
+
assert.equal(L.ageBand(rows[0].created_at, NOW).id, 'stale');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('the board of everyone\'s asks speaks in the third person', () => {
|
|
241
|
+
const L = lib();
|
|
242
|
+
assert.equal(L.boardLine({ requested_by_name: 'Alpha', needs_builder_id: 9, needs_builder_name: 'Beta' }), 'Alpha asked Beta by name');
|
|
243
|
+
assert.equal(L.boardLine({ requested_by_name: 'Gamma', needs_craft: 'builder' }), 'Gamma asked builders');
|
|
244
|
+
assert.equal(L.boardLine({ requested_by: 5, needs_builder_id: 9 }), 'builder 5 asked one person', 'an older server still names both ends');
|
|
245
|
+
assert.ok(!/\byou\b/i.test(L.boardLine({ requested_by_name: 'Alpha', needs_builder_id: 9, needs_builder_name: 'Beta' })), 'never "you" on the board');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('the page is the three blocks, led by faces, with the ledes gone', () => {
|
|
249
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
250
|
+
const html = read('modules', 'hall-ui', 'public', 'collab.html');
|
|
251
|
+
assert.match(js, /L\.faceOf\(req, 'in'\)/, 'the incoming row asks the lib whose face');
|
|
252
|
+
assert.match(js, /L\.faceOf\(req, 'out'\)/, 'and so does the outgoing one');
|
|
253
|
+
assert.match(js, /paintBlock\('for-me'/); assert.match(js, /paintBlock\('mine'/); assert.match(js, /paintBlock\('all'/);
|
|
254
|
+
assert.match(js, /L\.splitShown\(list, SHOWN\)/, 'the condensing is the lib\'s split');
|
|
255
|
+
assert.match(js, /const SHOWN = 3;/, 'three, as asked');
|
|
256
|
+
assert.match(js, /api\.request\('GET', '\/help-requests'\)/, 'the third block reads the whole board');
|
|
257
|
+
assert.match(html, /class="view-zone view-zone--band cb-band"/, 'the top two ride the shell\'s two-up');
|
|
258
|
+
assert.match(html, /id="cb-all"/, 'and the board is the wide block under them');
|
|
259
|
+
for (const k of ['for-me', 'mine', 'all']) assert.match(html, new RegExp(`id="cb-${k}-more"[^>]*aria-expanded="false"`), `${k} has its door`);
|
|
260
|
+
// Prose budget: no block opens with a lede except the social one, which
|
|
261
|
+
// keeps ONE line; the rules sentence is said once, at the foot.
|
|
262
|
+
assert.equal((html.match(/class="scroll__lede"/g) || []).length, 1, 'one lede on the whole page');
|
|
263
|
+
assert.equal((html.match(/declining costs nothing/g) || []).length, 1, 'the rule is said once');
|
|
264
|
+
assert.match(html, /class="cb-foot"/, 'and it is the foot, not the opener');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('the by-name ask is the one LIT thing — the accent goes on the personal call', () => {
|
|
268
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
269
|
+
const css = read('modules', 'hall-ui', 'public', 'collab.css');
|
|
270
|
+
assert.match(js, /req\.needs_builder_id != null \? ' cb-row--live' : ''/, 'only a by-name ask takes the class');
|
|
271
|
+
assert.match(css, /\.cb-row--live \.ov-row__facts > \.ov-fact \{ color: var\(--accent-ink\); \}/);
|
|
272
|
+
assert.ok(!/var\(--accent\)/.test(css), 'nothing in the sheet spends the accent FILL');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('every colour in the sheet is a token — no literal, both modes for free', () => {
|
|
276
|
+
const css = read('modules', 'hall-ui', 'public', 'collab.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
277
|
+
assert.deepEqual(css.match(/#[0-9a-f]{3,8}\b|rgba?\(/gi) || [], [], 'the Fifteen Rule');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('the SIDEBAR badge: the Collab item declares the help_requests count', () => {
|
|
281
|
+
const shell = read('modules', 'hall-ui', 'public', 'shell.js');
|
|
282
|
+
assert.match(shell, /id: 'collab',[^}]*count: 'help_requests'/, 'the same field the Board Room item carries');
|
|
283
|
+
const needsSrc = read('modules', 'builder-settings', 'builder-needs.js');
|
|
284
|
+
const fn = needsSrc.slice(needsSrc.indexOf('function helpRequestsNeed'), needsSrc.indexOf('function taskRecommendationsNeed'));
|
|
285
|
+
assert.match(fn, /count: n,/, 'and the need states its number for the badge to read');
|
|
286
|
+
assert.match(fn, /href: '\/collab'/, 'pointing at the page, not the retired hash view');
|
|
287
|
+
});
|
|
288
|
+
|
|
170
289
|
// ---- THE ANSWERABLE CLAUSE ------------------------------------------------
|
|
171
290
|
test('the asker can settle in place, with both outcomes', () => {
|
|
172
291
|
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
@@ -66,7 +66,8 @@ test('every list is the family\'s row, and the card-in-a-card is gone', () => {
|
|
|
66
66
|
for (const old of ['cd-card', 'cd-stat', 'cd-tag', 'cd-filters__select', 'cd-browse', 'cd-empty']) {
|
|
67
67
|
assert.ok(gone(read('copy-desk.js'), old) && gone(read('copy-desk.css'), `\\.${old}`), `copy desk no longer emits ${old}`);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// The kit's class, whole or with a page modifier beside it (collab's .cb-empty).
|
|
70
|
+
for (const p of PAGES) assert.match(read(`${p}.html`), /class="empty-state( [a-z-]+)*"/, `${p}.html empties with the kit's own state`);
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
// ---- collab: the facts the audit found thrown away ---------------------------
|
|
@@ -135,9 +136,12 @@ test('"asked of you" reads in runs — by name first, then each craft', () => {
|
|
|
135
136
|
// task 1003274: the naming moved to directionLine, which also says WHICH WAY
|
|
136
137
|
// the ask points — the half the owner's report was actually about.
|
|
137
138
|
assert.match(L.directionLine({ needs_builder_id: 7, needs_builder_name: 'Preview Alpha' }, 'out'), /Preview Alpha by name/, 'a named addressee is named');
|
|
139
|
+
// task 1003942 (owner direction): the block is CONDENSED to the newest three,
|
|
140
|
+
// so the runs and their group heads are gone from the page; the personal ask
|
|
141
|
+
// is still told apart, as the lit row rather than a run of its own.
|
|
138
142
|
const js = read('collab.js');
|
|
139
|
-
assert.match(js, /
|
|
140
|
-
assert.match(js, /
|
|
143
|
+
assert.match(js, /req\.needs_builder_id != null \? ' cb-row--live' : ''/, 'a by-name ask is the lit row');
|
|
144
|
+
assert.match(js, /sort\(L\.byNewestFirst\)/, 'the block reads newest first');
|
|
141
145
|
});
|
|
142
146
|
|
|
143
147
|
test('the age is a state pill in the row\'s right column, and every band has its own colour', () => {
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -562,6 +562,53 @@ test('a big count is capped rather than allowed to stretch the rail', () => {
|
|
|
562
562
|
assert.equal(b.navBadge('board-room').text, '99+');
|
|
563
563
|
});
|
|
564
564
|
|
|
565
|
+
// ── the SECOND consumer: open help requests on the Collab item (task 1003942) ──
|
|
566
|
+
//
|
|
567
|
+
// The owner's ask: a notification badge on the Collab link like the Board Room's.
|
|
568
|
+
// Exactly the adoption the mechanism above promised — one field on the nav item,
|
|
569
|
+
// one field on the need — so these pin that it landed and inherits every rule.
|
|
570
|
+
|
|
571
|
+
const helpNeeds = (count, extra = {}) => ({
|
|
572
|
+
items: [{
|
|
573
|
+
id: 'help_requests', state: 'action_needed', count,
|
|
574
|
+
title: 'People asked for your help', ...extra,
|
|
575
|
+
}],
|
|
576
|
+
});
|
|
577
|
+
const signedIn = { authed: true, rank: 'xenos', permissions: {} };
|
|
578
|
+
|
|
579
|
+
test('COLLAB: open help requests put a count on the Collab item', () => {
|
|
580
|
+
const b = boot();
|
|
581
|
+
b.shell.applyAccess({ ...signedIn, needs: helpNeeds(4) });
|
|
582
|
+
assert.deepEqual(b.navBadge('collab'), { text: '4', label: 'People asked for your help' });
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('COLLAB: zero, no count, or a covered need is silent', () => {
|
|
586
|
+
for (const needs of [helpNeeds(0), { items: [{ id: 'help_requests', state: 'action_needed' }] }, helpNeeds(3, { state: 'covered' })]) {
|
|
587
|
+
const b = boot();
|
|
588
|
+
b.shell.applyAccess({ ...signedIn, needs });
|
|
589
|
+
assert.equal(b.navBadge('collab'), null);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test('COLLAB: the badge never out-runs the gate — signed out, no count', () => {
|
|
594
|
+
const b = boot();
|
|
595
|
+
b.shell.applyAccess({ ...signedIn, needs: helpNeeds(2) });
|
|
596
|
+
assert.ok(b.navBadge('collab'), 'precondition: it showed');
|
|
597
|
+
b.shell.applyAccess({ authed: false, needs: helpNeeds(2) });
|
|
598
|
+
assert.equal(b.navHidden('collab'), true, 'precondition: Collab needs a session');
|
|
599
|
+
assert.equal(b.navBadge('collab'), null, 'a hidden item carries no count');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('COLLAB and the Board Room each read their OWN need — the counts do not bleed', () => {
|
|
603
|
+
const b = boot();
|
|
604
|
+
b.shell.applyAccess({
|
|
605
|
+
authed: true, rank: 'xenos', permissions: { 'board.vote.cast': true },
|
|
606
|
+
needs: { items: [...helpNeeds(4).items, ...boardNeeds(2).items] },
|
|
607
|
+
});
|
|
608
|
+
assert.equal(b.navBadge('collab').text, '4');
|
|
609
|
+
assert.equal(b.navBadge('board-room').text, '2');
|
|
610
|
+
});
|
|
611
|
+
|
|
565
612
|
test('the count reaches a screen reader as WORDS, not a floating numeral', () => {
|
|
566
613
|
// The numeral is aria-hidden decoration; the need's own sentence rides beside
|
|
567
614
|
// it in an sr-only span. "3" alone inside a link named Board Room says nothing.
|
package/tests/help_requests.mjs
CHANGED
|
@@ -231,6 +231,37 @@ test('both reads are capped', () => {
|
|
|
231
231
|
}
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
// ── the BOARD read (task 1003942, owner direction) ─────────────────────────
|
|
235
|
+
//
|
|
236
|
+
// The Collab page's third block is every open ask in the project. A fourth
|
|
237
|
+
// read beside the addressed one and the asker's own, and the one that could
|
|
238
|
+
// reopen C01's privacy hole if it took a craft or a builder from the caller —
|
|
239
|
+
// so these pin that it takes nothing, stays open-only, and is capped like the
|
|
240
|
+
// other two.
|
|
241
|
+
test('the board read lists every OPEN ask, newest first, and takes nothing from the caller', async () => {
|
|
242
|
+
const calls = [];
|
|
243
|
+
const pool = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 1 }] }; } };
|
|
244
|
+
const rows = await help.listOpenAll({}, { pool });
|
|
245
|
+
assert.deepEqual(rows, [{ id: 1 }]);
|
|
246
|
+
assert.equal(calls.length, 1);
|
|
247
|
+
assert.match(calls[0].sql, /WHERE h\.status = 'open'/, 'open only — a settled ask leaves the board');
|
|
248
|
+
assert.match(calls[0].sql, /ORDER BY h\.created_at DESC/, 'newest first is the board\'s order');
|
|
249
|
+
assert.ok(!/needs_craft\s*=|requested_by\s*=|needs_builder_id\s*=/.test(calls[0].sql), 'no craft or builder narrows it — nothing for a ?craft= hole to widen');
|
|
250
|
+
assert.deepEqual(calls[0].params, [help.MAX_ROWS], 'capped, and the cap is the only parameter');
|
|
251
|
+
assert.match(calls[0].sql, /requested_by_avatar_url/, 'it carries the faces the page leads with');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('the board route is signed-in only and reads no query', () => {
|
|
255
|
+
const routes = read('modules', 'lifecycle', 'routes', 'help-requests.js');
|
|
256
|
+
const idx = routes.indexOf("'GET /help-requests'");
|
|
257
|
+
assert.ok(idx > 0, 'the route exists');
|
|
258
|
+
const line = routes.slice(routes.lastIndexOf('router.get', idx), idx);
|
|
259
|
+
assert.match(line, /api\.requireBuilder/, 'any builder, no stranger');
|
|
260
|
+
const body = routes.slice(idx, routes.indexOf('errorCode', idx));
|
|
261
|
+
assert.ok(!/req\.query|req\.body/.test(body), 'nothing from the caller reaches the read');
|
|
262
|
+
assert.match(body, /help\.listOpenAll\(\)/);
|
|
263
|
+
});
|
|
264
|
+
|
|
234
265
|
test('core reaches the count through the PORT, never by importing lifecycle', () => {
|
|
235
266
|
const me = read('src', 'bongos', 'routes', 'me.js');
|
|
236
267
|
assert.match(me, /openHelpRequestsFor/);
|
package/tests/social_mode.mjs
CHANGED
|
@@ -120,7 +120,9 @@ test('HOME changes: an introduction becomes a need in the roster', () => {
|
|
|
120
120
|
const row = roster.find((n) => n.id === 'mingle_introductions');
|
|
121
121
|
assert.ok(row, 'the need is produced');
|
|
122
122
|
assert.equal(row.state, 'action_needed');
|
|
123
|
-
|
|
123
|
+
// Collab has been its own page since task 1003172; the '/#/collab' hash form
|
|
124
|
+
// this used to pin pointed at a view the index no longer routes (task 1003942).
|
|
125
|
+
assert.match(row.action.href, /^\/collab$/, 'it points at an EXISTING surface, not a new one');
|
|
124
126
|
});
|
|
125
127
|
|
|
126
128
|
test('no introductions means no row — the roster is not padded', () => {
|