@bongos/core 1.21.59 → 1.21.60
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 +44 -29
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +14 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +294 -3
- package/docs/api-reference.md +6 -2
- package/docs/copy-inventory.md +13 -8
- package/docs/copy-registry.json +54 -9
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +174 -169
- package/modules/builder-settings/filter-views.js +136 -0
- package/modules/builder-settings/routes/settings.js +90 -0
- package/modules/hall-ui/public/goals-page.js +8 -2
- package/modules/hall-ui/public/hall-kit.js +128 -2
- package/modules/hall-ui/public/style.css +32 -0
- package/modules/hall-ui/public/work.js +11 -3
- package/modules/hall-ui/records/filter-views.md +11 -0
- 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/filter_views.mjs +144 -0
- package/tests/focus_version_filter_default.mjs +3 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// tests/filter_views.mjs — saved filter views (task 1004553): the pure storage rules
|
|
2
|
+
// in modules/builder-settings/filter-views.js and the own-scoped /me/filter-views
|
|
3
|
+
// routes, run against a fake prefs store keyed by builder. No DB needed.
|
|
4
|
+
//
|
|
5
|
+
// Run: node --test tests/filter_views.mjs
|
|
6
|
+
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
process.env.NODE_ENV = 'test';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const fv = require('../modules/builder-settings/filter-views.js');
|
|
15
|
+
|
|
16
|
+
// One behavior_prefs blob per builder — the column the routes really write.
|
|
17
|
+
const rows = new Map();
|
|
18
|
+
const db = require('../modules/builder-settings/db.js');
|
|
19
|
+
db.getBuilderBehaviorPrefs = async (id) => rows.get(id) || {};
|
|
20
|
+
db.setBuilderBehaviorPrefs = async (id, prefs) => { rows.set(id, prefs); return prefs; };
|
|
21
|
+
const router = require('../modules/builder-settings/routes/settings.js')();
|
|
22
|
+
|
|
23
|
+
const src = (rel) => readFileSync(new URL('../' + rel, import.meta.url), 'utf8');
|
|
24
|
+
|
|
25
|
+
function handler(method, path) {
|
|
26
|
+
const layer = router.stack.find((l) => l.route && l.route.path === path && l.route.methods[method]);
|
|
27
|
+
assert.ok(layer, `${method} ${path} is registered`);
|
|
28
|
+
return layer.route.stack[layer.route.stack.length - 1].handle;
|
|
29
|
+
}
|
|
30
|
+
async function call(method, path, { builder, body, query = {}, params = {} }) {
|
|
31
|
+
const out = { status: 200, body: null };
|
|
32
|
+
const res = {
|
|
33
|
+
status(s) { out.status = s; return this; },
|
|
34
|
+
json(b) { out.body = b; return this; },
|
|
35
|
+
fail(code, opts) { out.status = (opts && opts.status) || 400; out.body = { error: { code, message: opts && opts.message } }; return this; },
|
|
36
|
+
};
|
|
37
|
+
await handler(method, path)({ builder, body, query, params }, res);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
const ALICE = { id: 1, rank: 'thetes' };
|
|
41
|
+
const BOB = { id: 2, rank: 'metic' };
|
|
42
|
+
|
|
43
|
+
test('saving a view keeps sibling prefs and only what was sent', () => {
|
|
44
|
+
const r = fv.saveView({ wandering: 'low' }, 'goals', { name: ' My open goals ', values: { 'f-goal-status': 'open' } });
|
|
45
|
+
assert.equal(r.ok, true);
|
|
46
|
+
assert.equal(r.prefs.wandering, 'low', 'sibling knobs survive');
|
|
47
|
+
assert.deepEqual(r.view, { id: 1, name: 'My open goals', values: { 'f-goal-status': 'open' }, is_default: false });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('saving under an existing name updates that view instead of adding a second', () => {
|
|
51
|
+
let p = fv.saveView({}, 'goals', { name: 'Mine', values: { a: '1' } }).prefs;
|
|
52
|
+
p = fv.saveView(p, 'goals', { name: 'mine', values: { a: '2' } }).prefs;
|
|
53
|
+
const list = fv.listViews(p, 'goals');
|
|
54
|
+
assert.equal(list.length, 1);
|
|
55
|
+
assert.deepEqual(list[0].values, { a: '2' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('only one view per page is the default', () => {
|
|
59
|
+
let p = fv.saveView({}, 'goals', { name: 'A', values: {}, is_default: true }).prefs;
|
|
60
|
+
p = fv.saveView(p, 'goals', { name: 'B', values: {}, is_default: true }).prefs;
|
|
61
|
+
assert.deepEqual(fv.listViews(p, 'goals').map((v) => [v.name, v.is_default]), [['A', false], ['B', true]]);
|
|
62
|
+
p = fv.updateView(p, 'goals', 1, { is_default: true }).prefs;
|
|
63
|
+
assert.deepEqual(fv.listViews(p, 'goals').map((v) => v.is_default), [true, false]);
|
|
64
|
+
p = fv.updateView(p, 'goals', 1, { is_default: false }).prefs;
|
|
65
|
+
assert.deepEqual(fv.listViews(p, 'goals').map((v) => v.is_default), [false, false]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('pages keep separate lists, and deleting the last view leaves no empty key behind', () => {
|
|
69
|
+
let p = fv.saveView({}, 'goals', { name: 'G', values: {} }).prefs;
|
|
70
|
+
p = fv.saveView(p, 'work', { name: 'W', values: {} }).prefs;
|
|
71
|
+
assert.deepEqual(fv.listViews(p, 'work').map((v) => v.name), ['W']);
|
|
72
|
+
p = fv.deleteView(p, 'goals', 1).prefs;
|
|
73
|
+
p = fv.deleteView(p, 'work', 1).prefs;
|
|
74
|
+
assert.equal(fv.RESERVED_KEY in p, false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('bad input is refused with a readable reason', () => {
|
|
78
|
+
assert.equal(fv.saveView({}, 'Goals!', { name: 'x', values: {} }).ok, false);
|
|
79
|
+
assert.equal(fv.saveView({}, 'goals', { name: ' ', values: {} }).ok, false);
|
|
80
|
+
assert.equal(fv.saveView({}, 'goals', { name: 'x'.repeat(61), values: {} }).ok, false);
|
|
81
|
+
assert.equal(fv.saveView({}, 'goals', { name: 'x', values: [] }).ok, false);
|
|
82
|
+
assert.equal(fv.saveView({}, 'goals', { name: 'x', values: { 'f-a': 3 } }).ok, false);
|
|
83
|
+
let p = {};
|
|
84
|
+
for (let i = 0; i < fv.MAX_VIEWS_PER_PAGE; i++) p = fv.saveView(p, 'goals', { name: `v${i}`, values: {} }).prefs;
|
|
85
|
+
assert.match(fv.saveView(p, 'goals', { name: 'one more', values: {} }).errors[0], /at most/);
|
|
86
|
+
let q = fv.saveView({}, 'goals', { name: 'A', values: {} }).prefs;
|
|
87
|
+
q = fv.saveView(q, 'goals', { name: 'B', values: {} }).prefs;
|
|
88
|
+
assert.match(fv.updateView(q, 'goals', 2, { name: 'a' }).errors[0], /already have/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('a builder saves a view and reads it back from a fresh request (another device)', async () => {
|
|
92
|
+
rows.clear();
|
|
93
|
+
const saved = await call('post', '/me/filter-views', { builder: ALICE, body: { page: 'goals', name: 'Open', values: { 'f-goal-status': 'open' }, is_default: true } });
|
|
94
|
+
assert.equal(saved.status, 201);
|
|
95
|
+
const got = await call('get', '/me/filter-views', { builder: ALICE, query: { page: 'goals' } });
|
|
96
|
+
assert.deepEqual(got.body.views, [{ id: 1, name: 'Open', values: { 'f-goal-status': 'open' }, is_default: true }]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('another builder cannot see, rename, default or delete it', async () => {
|
|
100
|
+
const seen = await call('get', '/me/filter-views', { builder: BOB, query: { page: 'goals' } });
|
|
101
|
+
assert.deepEqual(seen.body.views, []);
|
|
102
|
+
const patch = await call('patch', '/me/filter-views/:id', { builder: BOB, params: { id: '1' }, body: { page: 'goals', name: 'Hijacked' } });
|
|
103
|
+
assert.equal(patch.status, 404);
|
|
104
|
+
const del = await call('delete', '/me/filter-views/:id', { builder: BOB, params: { id: '1' }, query: { page: 'goals' } });
|
|
105
|
+
assert.equal(del.status, 404);
|
|
106
|
+
const still = await call('get', '/me/filter-views', { builder: ALICE, query: { page: 'goals' } });
|
|
107
|
+
assert.equal(still.body.views[0].name, 'Open', 'Alice\'s view is untouched');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('the owner renames and deletes their own view', async () => {
|
|
111
|
+
const ren = await call('patch', '/me/filter-views/:id', { builder: ALICE, params: { id: '1' }, body: { page: 'goals', name: 'Open goals' } });
|
|
112
|
+
assert.equal(ren.body.view.name, 'Open goals');
|
|
113
|
+
const del = await call('delete', '/me/filter-views/:id', { builder: ALICE, params: { id: '1' }, query: { page: 'goals' } });
|
|
114
|
+
assert.deepEqual(del.body.views, []);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('a missing page is a 400; a wrong-shaped body is refused before it is stored; a bad value is a 422', async () => {
|
|
118
|
+
rows.clear();
|
|
119
|
+
assert.equal((await call('get', '/me/filter-views', { builder: ALICE, query: {} })).status, 400);
|
|
120
|
+
assert.equal((await call('post', '/me/filter-views', { builder: ALICE, body: { page: 'goals', name: 'x' } })).status, 400, 'values missing');
|
|
121
|
+
assert.equal((await call('post', '/me/filter-views', { builder: ALICE, body: { page: 'goals', name: 'x', values: {}, owner: 2 } })).status, 400, 'unknown field');
|
|
122
|
+
assert.equal((await call('post', '/me/filter-views', { builder: ALICE, body: { page: 'goals', name: ' ', values: {} } })).status, 422);
|
|
123
|
+
assert.equal(rows.size, 0, 'nothing was written');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('every route is own-scoped: requireBuilder, and no id from the request picks the builder', () => {
|
|
127
|
+
const routes = src('modules/builder-settings/routes/settings.js');
|
|
128
|
+
for (const m of ['get', 'post', 'patch', 'delete']) {
|
|
129
|
+
assert.match(routes, new RegExp(`router\\.${m}\\('/me/filter-views[^']*', api\\.requireBuilder,`));
|
|
130
|
+
}
|
|
131
|
+
const block = routes.slice(routes.indexOf("'/me/filter-views'"), routes.indexOf('Per-builder DISPLAY NAME'));
|
|
132
|
+
assert.doesNotMatch(block, /BehaviorPrefs\((?!req\.builder\.id)/, 'prefs are only ever read/written for req.builder.id');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('both pages opt in, and apply my default view before the focus version', () => {
|
|
136
|
+
const work = src('modules/hall-ui/public/work.js');
|
|
137
|
+
assert.match(work, /views: \{ page: 'work', client: api, base: `\$\{API\}\/me\/filter-views` \}/);
|
|
138
|
+
assert.match(work, /applyDefault\(\{ fire: true \}\)[\s\S]{0,160}!applied && applyFocusVersion\(focusResp\)/);
|
|
139
|
+
const goals = src('modules/hall-ui/public/goals-page.js');
|
|
140
|
+
assert.match(goals, /views: \{ page: 'goals', client: api, base: `\$\{API\}\/me\/filter-views` \}/);
|
|
141
|
+
assert.match(goals, /if \(!\(views && await views\.applyDefault\(\{ fire: false \}\)\)\) applyFocusVersion\(focusResp\);/);
|
|
142
|
+
const kit = src('modules/hall-ui/public/hall-kit.js');
|
|
143
|
+
assert.match(kit, /if \(!\(await ready\) \|\| urlHasFilter\) return false;/, 'a URL filter beats my default view');
|
|
144
|
+
});
|
|
@@ -39,9 +39,10 @@ test('a focus the select has no option for is left alone, not selected as a blan
|
|
|
39
39
|
test('both pages read the focus and apply it after their version options fill', () => {
|
|
40
40
|
const work = src('modules/hall-ui/public/work.js');
|
|
41
41
|
assert.match(work, /\/versions\/focus/);
|
|
42
|
-
|
|
42
|
+
// task 1004553 moved the call after the Goal options fill, behind the builder's default view.
|
|
43
|
+
assert.match(work, /restoreUrlFilter\('f-goal'\)[\s\S]{0,600}!applied && applyFocusVersion\(focusResp\)/);
|
|
43
44
|
const goals = src('modules/hall-ui/public/goals-page.js');
|
|
44
45
|
assert.match(goals, /\/versions\/focus/);
|
|
45
|
-
assert.match(goals, /populateVersionFilter\(\)
|
|
46
|
+
assert.match(goals, /populateVersionFilter\(\);[\s\S]{0,400}applyFocusVersion\(focusResp\);/);
|
|
46
47
|
for (const page of [work, goals]) assert.match(page, /OTBTaskLib\.focusVersionDefault\(/);
|
|
47
48
|
});
|