@bongos/core 1.19.281 → 1.19.282
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 +20 -15
- package/docs/copy-inventory.md +90 -91
- package/docs/copy-registry.json +101 -110
- package/modules/hall-ui/public/goals-page.js +70 -69
- package/modules/hall-ui/public/goals.css +12 -15
- package/modules/hall-ui/public/goals.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/goals_card_view.mjs +61 -0
|
@@ -168,6 +168,8 @@
|
|
|
168
168
|
active: { label: 'recently active', cmp: (a, b) => goalActivityMs(b) - goalActivityMs(a), stamp: (g) => ({ label: 'last work', ms: goalActivityMs(g) }) },
|
|
169
169
|
};
|
|
170
170
|
const GOAL_SORT_PREF = 'otb.goals.sort';
|
|
171
|
+
// task 1003136 — the list|cards view choice, persisted like the sort above.
|
|
172
|
+
const GOAL_VIEW_PREF = 'otb.goals.view';
|
|
171
173
|
const currentGoalSort = () => GOAL_SORTS[$('#f-goal-order')?.value] || GOAL_SORTS.priority;
|
|
172
174
|
|
|
173
175
|
// Stable: filteredGoals already returns the server's priority order, and
|
|
@@ -262,11 +264,10 @@
|
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
// the server-side rank gate on POST /goals/reorder is the real enforcement.
|
|
267
|
+
// Archon check for the detail view's manage/visibility affordances (the
|
|
268
|
+
// server-side rank gates are the real enforcement). The task-1760 drag-reorder
|
|
269
|
+
// affordance this once also served is retired — see the note where
|
|
270
|
+
// wireDragReorder used to live (task 1003136).
|
|
270
271
|
const isArchon = () => (state.me?.builder?.rank || '').toLowerCase() === 'archon';
|
|
271
272
|
|
|
272
273
|
// task 1002164 — confirming a criterion is Metic+ (POST /done-when/:id/satisfy,
|
|
@@ -321,13 +322,16 @@
|
|
|
321
322
|
return ` · ${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}`;
|
|
322
323
|
}
|
|
323
324
|
|
|
324
|
-
|
|
325
|
+
// The facts a goal row/card renders, computed ONCE so the two views (task
|
|
326
|
+
// 1003136) can't drift: ring, meta line, roll line, and the single
|
|
327
|
+
// next-action ladder (achieved → criteria-met → claim-next → drill → quiet).
|
|
328
|
+
function goalRenderFacts(g, ringSize) {
|
|
325
329
|
const d = state.details.get(String(g.id));
|
|
326
330
|
const crit = d ? (d.criteria || []) : [];
|
|
327
331
|
const sat = crit.filter((c) => c.satisfied).length;
|
|
328
332
|
const r = rollupOf(g.id);
|
|
329
333
|
const achieved = g.status === 'achieved';
|
|
330
|
-
const ringHtml = crit.length ? ring(sat, crit.length, achieved,
|
|
334
|
+
const ringHtml = crit.length ? ring(sat, crit.length, achieved, ringSize)
|
|
331
335
|
: `<span class="gr-crit-none" aria-hidden="true">—</span>`;
|
|
332
336
|
const goalHref = `#/goal/${escapeHtml(String(g.id))}`;
|
|
333
337
|
let right;
|
|
@@ -343,6 +347,12 @@
|
|
|
343
347
|
} else {
|
|
344
348
|
right = `<span class="gr-quiet">No tasks yet</span>`;
|
|
345
349
|
}
|
|
350
|
+
const metaHtml = `#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}${goalStampHtml(g)}`;
|
|
351
|
+
return { r, achieved, ringHtml, goalHref, right, metaHtml };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function goalRowHtml(g) {
|
|
355
|
+
const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 44);
|
|
346
356
|
// NOTE: a rollup row nests real links (title → goal, Claim → task overlay,
|
|
347
357
|
// drill → goal), so the row itself is a <div>, not an <a> — nested anchors
|
|
348
358
|
// are invalid. Only the explicit links navigate.
|
|
@@ -350,13 +360,31 @@
|
|
|
350
360
|
<div class="gr-ringcol">${ringHtml}</div>
|
|
351
361
|
<div class="gr-main">
|
|
352
362
|
<a class="gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
353
|
-
<div class="gr-meta"
|
|
363
|
+
<div class="gr-meta">${metaHtml}</div>
|
|
354
364
|
${achieved ? '' : goalRollLine(r)}
|
|
355
365
|
</div>
|
|
356
366
|
<div class="gr-right">${right}</div>
|
|
357
367
|
</div>`;
|
|
358
368
|
}
|
|
359
369
|
|
|
370
|
+
// task 1003136 — the same goal as a CARD: identical facts as the row via
|
|
371
|
+
// goalRenderFacts, so nothing is lost switching views. Reuses the surviving
|
|
372
|
+
// .goal-card* CSS (goals.css — task 1002439 neutralised it, never deleted it).
|
|
373
|
+
// Deliberately emits NO draggable attribute: drag-to-reorder is retired, see
|
|
374
|
+
// the note where wireDragReorder used to live.
|
|
375
|
+
function goalCardHtml(g) {
|
|
376
|
+
const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 36);
|
|
377
|
+
return `<div class="goal-card" data-goal-id="${escapeHtml(String(g.id))}">
|
|
378
|
+
<div class="goal-card__head">
|
|
379
|
+
<div class="gr-ringcol">${ringHtml}</div>
|
|
380
|
+
<a class="goal-card__title gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="goal-card__meta">${metaHtml}</div>
|
|
383
|
+
${achieved ? '' : goalRollLine(r)}
|
|
384
|
+
<div class="goal-card__foot">${right}</div>
|
|
385
|
+
</div>`;
|
|
386
|
+
}
|
|
387
|
+
|
|
360
388
|
// task 1002439 — the Goals page, reorganised around momentum: a task-readiness
|
|
361
389
|
// summary, then goals grouped Active (has ready/in-progress work) · Blocked
|
|
362
390
|
// (only waiting-on-upstream tasks) · Achieved. "Blocked" here is a DERIVED
|
|
@@ -399,7 +427,15 @@
|
|
|
399
427
|
</div>`;
|
|
400
428
|
|
|
401
429
|
const sec = (label, n) => `<div class="gr-sec"><b>${label}</b><span class="gr-sec__n">${n}</span></div>`;
|
|
402
|
-
|
|
430
|
+
// task 1003136 — the list|cards toggle swaps how the open groups render;
|
|
431
|
+
// grouping, filters, and sort run above the fork so both views share them.
|
|
432
|
+
// Achieved/archived pills stay pills (already the compact form). The inner
|
|
433
|
+
// div is class-only .goal-grid on purpose: task 1002439's block override
|
|
434
|
+
// targets #goal-grid.goal-grid, so the original card-grid CSS applies here.
|
|
435
|
+
const cardsView = ($('#f-goal-view')?.value || 'list') === 'cards';
|
|
436
|
+
const listOf = cardsView
|
|
437
|
+
? (arr) => `<div class="goal-grid">${arr.map(goalCardHtml).join('')}</div>`
|
|
438
|
+
: (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
|
|
403
439
|
// task 1002965 — an achieved/archived goal is a compact pill, so its sort
|
|
404
440
|
// date rides as its own muted span (the row's `gr-meta` equivalent). The
|
|
405
441
|
// pills row already wraps, so the extra text can't widen the page.
|
|
@@ -418,63 +454,14 @@
|
|
|
418
454
|
+ (archived.length ? sec('Archived', archived.length) + `<div class="gr-pills gr-pills--muted">${archived.map(pill).join('')}</div>` : '');
|
|
419
455
|
}
|
|
420
456
|
|
|
421
|
-
// ---- drag-to-reorder (task
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (!el || !el.hasAttribute('draggable')) return;
|
|
430
|
-
dragEl = el;
|
|
431
|
-
el.classList.add('goal-card--dragging');
|
|
432
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
433
|
-
});
|
|
434
|
-
grid.addEventListener('dragover', (e) => {
|
|
435
|
-
if (!dragEl) return;
|
|
436
|
-
const over = e.target.closest('.goal-card');
|
|
437
|
-
if (!over || over === dragEl || !grid.contains(over)) return;
|
|
438
|
-
e.preventDefault();
|
|
439
|
-
const rect = over.getBoundingClientRect();
|
|
440
|
-
const after = (e.clientX - rect.left) > rect.width / 2;
|
|
441
|
-
grid.insertBefore(dragEl, after ? over.nextSibling : over);
|
|
442
|
-
});
|
|
443
|
-
grid.addEventListener('drop', (e) => { if (dragEl) e.preventDefault(); });
|
|
444
|
-
grid.addEventListener('dragend', async () => {
|
|
445
|
-
if (!dragEl) return;
|
|
446
|
-
const el = dragEl;
|
|
447
|
-
el.classList.remove('goal-card--dragging');
|
|
448
|
-
dragEl = null;
|
|
449
|
-
const ver = $('#f-goal-version')?.value || '';
|
|
450
|
-
if (!ver) return;
|
|
451
|
-
// task 1932 — with pagination the grid shows only the current PAGE, so the
|
|
452
|
-
// reorder must submit the FULL version order: the goals before this page +
|
|
453
|
-
// the page's cards in their new DOM order + the goals after it. Without
|
|
454
|
-
// this, POSTing only the visible page's ids would corrupt the priorities
|
|
455
|
-
// of the goals on other pages. Priority numbers reflect the global
|
|
456
|
-
// position (start + i + 1).
|
|
457
|
-
const filtered = filteredGoals();
|
|
458
|
-
const start = (pagers.list.page - 1) * pagers.list.perPage;
|
|
459
|
-
const cards = [...grid.querySelectorAll('.goal-card')];
|
|
460
|
-
cards.forEach((card, i) => {
|
|
461
|
-
const num = card.querySelector('.goal-card__priority');
|
|
462
|
-
if (num) num.textContent = String(start + i + 1);
|
|
463
|
-
});
|
|
464
|
-
const pageIds = cards.map((card) => Number(card.dataset.goalId));
|
|
465
|
-
const beforeIds = filtered.slice(0, start).map((g) => Number(g.id));
|
|
466
|
-
const afterIds = filtered.slice(start + pageIds.length).map((g) => Number(g.id));
|
|
467
|
-
const orderedIds = [...beforeIds, ...pageIds, ...afterIds];
|
|
468
|
-
try {
|
|
469
|
-
await postJSON(`${API}/goals/reorder`, { version_id: ver, ordered_ids: orderedIds });
|
|
470
|
-
toast('Priority order saved.');
|
|
471
|
-
} catch (err) {
|
|
472
|
-
toast((err && err.data && err.data.message) || 'Could not save the new order — reloading.');
|
|
473
|
-
state.goals = (await getJSON(`${API}/goals`).catch(() => ({ goals: state.goals }))).goals || state.goals;
|
|
474
|
-
renderList();
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
}
|
|
457
|
+
// ---- drag-to-reorder: RETIRED (task 1003136; was task 1760) ---------------
|
|
458
|
+
// wireDragReorder() lived here, dead since task 1002439's grouped rollup (its
|
|
459
|
+
// .goal-card selector matched nothing). The card view revives .goal-card
|
|
460
|
+
// markup, so the decision is now DELIBERATE: the listener is DELETED — both
|
|
461
|
+
// views render grouped, sorted, filtered sets, so DOM order no longer
|
|
462
|
+
// expresses the version priority the old dragend POSTed to /goals/reorder;
|
|
463
|
+
// re-enabling it would submit a corrupted order. Cards emit no draggable
|
|
464
|
+
// attribute. POST /goals/reorder itself is untouched (Archon, pinned).
|
|
478
465
|
|
|
479
466
|
/* ---------------- detail view ---------------- */
|
|
480
467
|
|
|
@@ -1822,7 +1809,12 @@
|
|
|
1822
1809
|
// reads it, so keeping the pref off the URL leaves deep links shareable and
|
|
1823
1810
|
// unchanged. Private-mode writes throw — swallowed, the pref is a nicety.
|
|
1824
1811
|
const onListFilterChange = () => {
|
|
1825
|
-
try {
|
|
1812
|
+
try {
|
|
1813
|
+
window.localStorage.setItem(GOAL_SORT_PREF, $('#f-goal-order')?.value || 'priority');
|
|
1814
|
+
// task 1003136 — the list|cards view choice survives reload, same
|
|
1815
|
+
// precedent (localStorage, never the URL).
|
|
1816
|
+
window.localStorage.setItem(GOAL_VIEW_PREF, $('#f-goal-view')?.value || 'list');
|
|
1817
|
+
} catch (_) { /* private mode */ }
|
|
1826
1818
|
pagers.list.page = 1;
|
|
1827
1819
|
renderList();
|
|
1828
1820
|
};
|
|
@@ -1833,7 +1825,11 @@
|
|
|
1833
1825
|
// task 1002965 adds Sort (f-goal-order — NOT f-goal-sort, which is already
|
|
1834
1826
|
// taken by the goal-detail To-do bar further down this file).
|
|
1835
1827
|
let savedGoalSort = null;
|
|
1836
|
-
|
|
1828
|
+
let savedGoalView = null;
|
|
1829
|
+
try {
|
|
1830
|
+
savedGoalSort = window.localStorage.getItem(GOAL_SORT_PREF);
|
|
1831
|
+
savedGoalView = window.localStorage.getItem(GOAL_VIEW_PREF);
|
|
1832
|
+
} catch (_) { /* private mode */ }
|
|
1837
1833
|
// task 1003135 — Search declared on the kit spec (live 'input' listener comes
|
|
1838
1834
|
// free, hall-kit.js:273; no debounce needed, the match is pure client-side over
|
|
1839
1835
|
// the already-loaded state.goals). Id is f-goals-search — f-goal-search is
|
|
@@ -1853,6 +1849,12 @@
|
|
|
1853
1849
|
label: 'Sort',
|
|
1854
1850
|
value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
|
|
1855
1851
|
options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
|
|
1852
|
+
// task 1003136 — list|cards toggle; renderList forks its group renderer
|
|
1853
|
+
// on this. Persisted via GOAL_VIEW_PREF, never the URL.
|
|
1854
|
+
{ id: 'f-goal-view',
|
|
1855
|
+
label: 'View',
|
|
1856
|
+
value: savedGoalView === 'cards' ? 'cards' : undefined,
|
|
1857
|
+
options: [{ value: 'list', label: 'list' }, { value: 'cards', label: 'cards' }] },
|
|
1856
1858
|
],
|
|
1857
1859
|
onChange: onListFilterChange,
|
|
1858
1860
|
});
|
|
@@ -1981,7 +1983,6 @@
|
|
|
1981
1983
|
if (denyBtn) { respondToJoinRequest(state.current, denyBtn.dataset.denyReq, 'deny', denyBtn); return; }
|
|
1982
1984
|
});
|
|
1983
1985
|
window.addEventListener('hashchange', route);
|
|
1984
|
-
wireDragReorder();
|
|
1985
1986
|
|
|
1986
1987
|
boot();
|
|
1987
1988
|
})();
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
are in style.css's hall-kit section. Everything derives from the shared
|
|
7
7
|
design tokens so instance packs reskin this page along with the rest. */
|
|
8
8
|
|
|
9
|
-
/* ---- goal-card grid (
|
|
9
|
+
/* ---- goal-card grid (the CARDS view) ----
|
|
10
10
|
task 2171 density pass: minmax 300→230 (four across at desktop width),
|
|
11
|
-
tighter gaps/padding/type
|
|
12
|
-
|
|
11
|
+
tighter gaps/padding/type. Retired by task 1002439's grouped rollup, REVIVED
|
|
12
|
+
by task 1003136 as the list|cards toggle's card view: renderList nests
|
|
13
|
+
id-less .goal-grid wrappers per group, which this rule styles — the
|
|
14
|
+
#goal-grid override further down only flattens the OUTER container. */
|
|
13
15
|
.goal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
|
|
14
16
|
.goal-card {
|
|
15
17
|
display: flex; flex-direction: column; gap: 7px;
|
|
@@ -19,16 +21,9 @@
|
|
|
19
21
|
}
|
|
20
22
|
a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); background: var(--surface-2); }
|
|
21
23
|
.goal-card__head { display: flex; align-items: flex-start; gap: 9px; }
|
|
22
|
-
/* task 1760
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.goal-card__handle:active { cursor: grabbing; }
|
|
26
|
-
.goal-card__priority {
|
|
27
|
-
flex: none; min-width: 1.4em; text-align: center;
|
|
28
|
-
font-size: 11.5px; font-weight: 600; color: var(--ink-faint);
|
|
29
|
-
font-variant-numeric: tabular-nums;
|
|
30
|
-
}
|
|
31
|
-
.goal-card--dragging { opacity: 0.45; }
|
|
24
|
+
/* task 1003136: the task-1760 drag handle/priority/dragging rules that lived
|
|
25
|
+
here are deleted with wireDragReorder (see goals-page.js) — the revived
|
|
26
|
+
cards deliberately carry no drag affordance. */
|
|
32
27
|
/* task 1761: a goal blocked by an unachieved prerequisite is dimmed + hue-shifted. */
|
|
33
28
|
.goal-card--blocked { opacity: 0.62; border-style: dashed; }
|
|
34
29
|
.goal-card--blocked:hover { opacity: 0.85; }
|
|
@@ -209,8 +204,10 @@ a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); b
|
|
|
209
204
|
tasks up into ready/in-progress/blocked; the task→task dependency
|
|
210
205
|
graph itself lives on the Tasks page. Uses the shared style.css tokens.
|
|
211
206
|
============================================================ */
|
|
212
|
-
/* The
|
|
213
|
-
|
|
207
|
+
/* The OUTER container renders grouped full-width blocks (summary + section
|
|
208
|
+
heads), so flip it to normal block flow. Id-selected on purpose: the cards
|
|
209
|
+
view (task 1003136) nests id-less .goal-grid wrappers per group, which keep
|
|
210
|
+
the real card grid above. */
|
|
214
211
|
#goal-grid.goal-grid { display: block; gap: 0; }
|
|
215
212
|
.gr-summary { display: flex; flex-wrap: wrap; gap: 22px; margin: 6px 0 20px; }
|
|
216
213
|
.gr-sm { display: flex; align-items: baseline; gap: 8px; font-size: 13px; color: var(--ink-soft); }
|
|
@@ -199,6 +199,6 @@
|
|
|
199
199
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
|
|
200
200
|
<script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
|
|
201
201
|
<script src="/builders/goals-lib.js?v=2026-08-21-goalslib1"></script>
|
|
202
|
-
<script src="/builders/goals-page.js?v=2026-08-21-
|
|
202
|
+
<script src="/builders/goals-page.js?v=2026-08-21-goals7"></script>
|
|
203
203
|
</body>
|
|
204
204
|
</html>
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.282",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.282",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.282",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -989,8 +989,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
989
989
|
// landed since 1.19.279 with no explicit bump. run 32523185119. (task 1002620)
|
|
990
990
|
// 1.19.281 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
991
991
|
// landed since 1.19.280 with no explicit bump. run 32525071763. (task 1002620)
|
|
992
|
+
// 1.19.282 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
993
|
+
// landed since 1.19.281 with no explicit bump. run 32526392955. (task 1002620)
|
|
992
994
|
// ---------------------------------------------------------------------------
|
|
993
|
-
const CORE_VERSION = '1.19.
|
|
995
|
+
const CORE_VERSION = '1.19.282'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
994
996
|
|
|
995
997
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
996
998
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// tests/goals_card_view.mjs — the Goals page list|cards toggle (task 1003136).
|
|
2
|
+
//
|
|
3
|
+
// Source-contract pins over goals-page.js: the DONE-WHEN properties that a DOM
|
|
4
|
+
// harness can't cheaply hold are exactly the ones that rot silently — the two
|
|
5
|
+
// views sharing one fact source, the drag landmine staying DELIBERATELY dead,
|
|
6
|
+
// and the view choice persisting off-URL. The interactive render itself is
|
|
7
|
+
// exercised by tests/goal_criterion_confirm_ui.mjs, which runs the real page.
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/goals_card_view.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { makeRunner } from './helpers.mjs';
|
|
16
|
+
|
|
17
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.js'), 'utf8');
|
|
19
|
+
const CSS = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals.css'), 'utf8');
|
|
20
|
+
const { test, summary } = makeRunner();
|
|
21
|
+
|
|
22
|
+
await test('both views render from ONE fact source (goalRenderFacts) so they cannot drift', () => {
|
|
23
|
+
assert.match(SRC, /function goalRenderFacts\(/, 'the shared fact fn must exist');
|
|
24
|
+
const rowBody = SRC.slice(SRC.indexOf('function goalRowHtml'), SRC.indexOf('function goalCardHtml'));
|
|
25
|
+
assert.match(rowBody, /goalRenderFacts\(g/, 'the row must consume the shared facts');
|
|
26
|
+
const cardBody = SRC.slice(SRC.indexOf('function goalCardHtml'));
|
|
27
|
+
assert.match(cardBody.slice(0, 1200), /goalRenderFacts\(g/, 'the card must consume the shared facts');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await test('renderList forks on the f-goal-view toggle AFTER grouping/filter/sort (both views share them)', () => {
|
|
31
|
+
const body = SRC.slice(SRC.indexOf('function renderList'), SRC.indexOf('drag-to-reorder: RETIRED'));
|
|
32
|
+
assert.match(body, /#f-goal-view/, 'the fork reads the toggle');
|
|
33
|
+
const sortIdx = body.indexOf('sortGoals(');
|
|
34
|
+
const forkIdx = body.indexOf('#f-goal-view');
|
|
35
|
+
assert.ok(sortIdx >= 0 && forkIdx > sortIdx, 'grouping/sort must run before the view fork');
|
|
36
|
+
assert.match(body, /goalCardHtml/, 'cards view renders goalCardHtml');
|
|
37
|
+
assert.match(body, /goalRowHtml/, 'list view still renders goalRowHtml');
|
|
38
|
+
assert.match(body, /<div class="goal-grid">/, 'cards nest an id-less .goal-grid so the block override does not flatten them');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await test('the view choice persists via localStorage (otb.goals.view), never the URL', () => {
|
|
42
|
+
assert.match(SRC, /GOAL_VIEW_PREF = 'otb\.goals\.view'/);
|
|
43
|
+
assert.match(SRC, /setItem\(GOAL_VIEW_PREF/, 'onChange writes the pref');
|
|
44
|
+
assert.match(SRC, /getItem\(GOAL_VIEW_PREF\)/, 'boot reads the pref back');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await test('drag-to-reorder is deliberately retired: no listener, no draggable cards, decision stated', () => {
|
|
48
|
+
assert.ok(!/function wireDragReorder/.test(SRC), 'wireDragReorder must stay deleted');
|
|
49
|
+
assert.ok(!/addEventListener\('drag/.test(SRC), 'no drag listeners anywhere on the page');
|
|
50
|
+
const cardBody = SRC.slice(SRC.indexOf('function goalCardHtml'), SRC.indexOf('function renderList'));
|
|
51
|
+
assert.ok(!/draggable/.test(cardBody), 'goalCardHtml must not emit a draggable attribute');
|
|
52
|
+
assert.match(SRC, /RETIRED \(task 1003136/, 'the retirement decision is stated where the code lived');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await test('goals.css: the block override stays id-scoped so inner .goal-grid card grids survive', () => {
|
|
56
|
+
assert.match(CSS, /#goal-grid\.goal-grid \{ display: block/, 'the outer flatten must stay id-selected');
|
|
57
|
+
assert.match(CSS, /\.goal-grid \{ display: grid; grid-template-columns: repeat\(auto-fill/, 'the card grid rule must survive');
|
|
58
|
+
assert.ok(!/goal-card__handle|goal-card--dragging/.test(CSS), 'the drag affordance CSS is gone with the listener');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
summary();
|