@yuneta/gobj-ui 7.23.66 → 7.23.68

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.
@@ -140,7 +140,7 @@ function parents_of(descs, desc, record)
140
140
  * topics: [topic_name, ...] topics with records, in desc order
141
141
  * }
142
142
  ************************************************************/
143
- export function fold_build_model(descs, records)
143
+ export function fold_build_model(descs, records, opts)
144
144
  {
145
145
  let nodes = new Map();
146
146
  let groups = new Map();
@@ -148,8 +148,21 @@ export function fold_build_model(descs, records)
148
148
 
149
149
  descs = is_object(descs)? descs : {};
150
150
  records = is_object(records)? records : {};
151
+ opts = is_object(opts)? opts : {};
151
152
 
152
- /* Pass 1: every record is a node. */
153
+ let hidden = new Set(is_array(opts.hidden_topics)? opts.hidden_topics : []);
154
+ let loose_shown = new Set(is_array(opts.loose_topics)? opts.loose_topics : []);
155
+ let main_topic = opts.main_topic || fold_main_topic(descs);
156
+ if(main_topic && (!descs[main_topic] || hidden.has(main_topic))) {
157
+ main_topic = ""; /* a main topic that is not drawn governs nothing */
158
+ }
159
+ let linked = fold_linked_topics(descs, main_topic);
160
+ let totals = {};
161
+
162
+ /* Pass 1: every record is a node -- of the topics that are SHOWN.
163
+ * A hidden topic leaves the model whole: no pills count it, no
164
+ * edge reaches it, and the records that hung from it alone become
165
+ * roots, the way any record with no drawable parent does. */
153
166
  for(let topic_name of Object.keys(descs)) {
154
167
  if(topic_name.substring(0, 2) === "__") {
155
168
  continue;
@@ -159,6 +172,10 @@ export function fold_build_model(descs, records)
159
172
  if(!is_array(list)) {
160
173
  continue;
161
174
  }
175
+ totals[topic_name] = list.length;
176
+ if(hidden.has(topic_name)) {
177
+ continue;
178
+ }
162
179
  topics.push(topic_name);
163
180
  for(let i = 0; i < list.length; i++) {
164
181
  let record = list[i];
@@ -203,28 +220,60 @@ export function fold_build_model(descs, records)
203
220
  }
204
221
  }
205
222
 
206
- /* Roots: no drawable parent. Then whatever the roots cannot reach
207
- * -- a cycle with no way in -- becomes a root too, so that every
208
- * record has a place on screen to be opened from. */
223
+ /* Roots: no drawable parent -- with one distinction when there is
224
+ * a MAIN topic. A record of a topic the schema hangs from the main
225
+ * one (`linked`) that has no parent is LOOSE: a device with no
226
+ * place. It should hang and does not, so it is not a root of the
227
+ * tree; it is counted per topic and shown only on request
228
+ * (`loose_topics`). A topic the schema does not tie to the main
229
+ * one at all is a tree of its own, and its parentless records are
230
+ * its roots, as before.
231
+ *
232
+ * Then whatever the roots cannot reach -- a cycle with no way in
233
+ * -- becomes a root too, so that every record has a place on
234
+ * screen to be opened from. */
209
235
  let roots = {};
236
+ let loose = {};
210
237
  for(let topic_name of topics) {
211
238
  roots[topic_name] = [];
239
+ loose[topic_name] = [];
212
240
  }
213
241
  for(let node of nodes.values()) {
214
- if(node.parents.length === 0) {
215
- roots[node.topic_name].push(node.key);
242
+ if(node.parents.length > 0) {
243
+ continue;
244
+ }
245
+ let t = node.topic_name;
246
+ if(main_topic && t !== main_topic && linked.has(t)) {
247
+ loose[t].push(node.key);
248
+ if(loose_shown.has(t)) {
249
+ roots[t].push(node.key);
250
+ }
251
+ } else {
252
+ roots[t].push(node.key);
216
253
  }
217
254
  }
218
255
  let reached = reach_from(nodes, [].concat(...Object.values(roots)));
219
256
  for(let node of nodes.values()) {
220
- if(!reached.has(node.key)) {
221
- roots[node.topic_name].push(node.key);
222
- /* Its own subtree is reachable now; mark it so a whole
223
- * cycle does not become a row of roots. */
224
- for(let k of reach_from(nodes, [node.key])) {
225
- reached.add(k);
257
+ if(reached.has(node.key)) {
258
+ continue;
259
+ }
260
+ let t = node.topic_name;
261
+ if(main_topic && t !== main_topic && linked.has(t)) {
262
+ /* Reachable through a loose record that is not shown, or
263
+ * through a hidden topic: loose as well, not a root. */
264
+ if(node.parents.length > 0) {
265
+ loose[t].push(node.key);
266
+ }
267
+ if(!loose_shown.has(t)) {
268
+ continue;
226
269
  }
227
270
  }
271
+ roots[t].push(node.key);
272
+ /* Its own subtree is reachable now; mark it so a whole
273
+ * cycle does not become a row of roots. */
274
+ for(let k of reach_from(nodes, [node.key])) {
275
+ reached.add(k);
276
+ }
228
277
  }
229
278
  for(let topic_name of topics) {
230
279
  if(roots[topic_name].length > 0) {
@@ -232,7 +281,103 @@ export function fold_build_model(descs, records)
232
281
  }
233
282
  }
234
283
 
235
- return {nodes: nodes, groups: groups, topics: topics};
284
+ return {
285
+ nodes: nodes,
286
+ groups: groups,
287
+ topics: topics,
288
+ totals: totals, /* {topic: records}, hidden topics included */
289
+ hidden: hidden, /* Set of hidden topics */
290
+ main_topic: main_topic, /* "" when nothing governs the tree */
291
+ linked: linked, /* Set of topics the schema hangs from main */
292
+ loose: loose, /* {topic: [key, ...]} records that should hang and do not */
293
+ loose_shown: loose_shown,
294
+ };
295
+ }
296
+
297
+ /************************************************************
298
+ * The topic the schema hangs the others from: the one whose hooks
299
+ * reach the most OTHER topics, a self-referent hook breaking a tie
300
+ * (a tree of places over a flat list of groups), schema order
301
+ * breaking the rest. "" when no topic reaches another -- a treedb of
302
+ * unrelated lists has no trunk, and every topic is a tree of its own.
303
+ ************************************************************/
304
+ export function fold_main_topic(descs)
305
+ {
306
+ descs = is_object(descs)? descs : {};
307
+ let best = "";
308
+ let best_score = 0;
309
+ let best_self = false;
310
+
311
+ for(let topic_name of Object.keys(descs)) {
312
+ if(topic_name.substring(0, 2) === "__") {
313
+ continue;
314
+ }
315
+ let reach = hook_targets(descs, topic_name);
316
+ let self = reach.has(topic_name);
317
+ reach.delete(topic_name);
318
+ let score = reach.size;
319
+ if(score === 0) {
320
+ continue;
321
+ }
322
+ if(score > best_score || (score === best_score && self && !best_self)) {
323
+ best = topic_name;
324
+ best_score = score;
325
+ best_self = self;
326
+ }
327
+ }
328
+ return best;
329
+ }
330
+
331
+ /* The topics a topic's hooks name (drawn topics only). An `extended`
332
+ * topic names nothing: the engine draws no edge from one, so its
333
+ * records hang nothing and it can govern no tree. */
334
+ function hook_targets(descs, topic_name)
335
+ {
336
+ let out = new Set();
337
+ let desc = descs[topic_name];
338
+ if(!desc || desc.node_treedb_type === "extended") {
339
+ return out;
340
+ }
341
+ let cols = is_array(desc.cols)? desc.cols : [];
342
+ for(let col of cols) {
343
+ if(!is_object(col.hook) || !is_array(col.flag) || col.flag.indexOf("hook") < 0) {
344
+ continue;
345
+ }
346
+ for(let child of Object.keys(col.hook)) {
347
+ if(child.substring(0, 2) !== "__" && descs[child]) {
348
+ out.add(child);
349
+ }
350
+ }
351
+ }
352
+ return out;
353
+ }
354
+
355
+ /************************************************************
356
+ * The topics the schema hangs from `main_topic`, main included:
357
+ * reached by following hooks from it. A record of one of these
358
+ * with no parent is LOOSE; a topic outside the set is its own tree.
359
+ * Empty when there is no main topic.
360
+ ************************************************************/
361
+ export function fold_linked_topics(descs, main_topic)
362
+ {
363
+ let linked = new Set();
364
+ if(!main_topic || !is_object(descs) || !descs[main_topic]) {
365
+ return linked;
366
+ }
367
+ let queue = [main_topic];
368
+ while(queue.length) {
369
+ let t = queue.shift();
370
+ if(linked.has(t)) {
371
+ continue;
372
+ }
373
+ linked.add(t);
374
+ for(let child of hook_targets(descs, t)) {
375
+ if(!linked.has(child)) {
376
+ queue.push(child);
377
+ }
378
+ }
379
+ }
380
+ return linked;
236
381
  }
237
382
 
238
383
  /************************************************************
@@ -283,7 +428,18 @@ export function fold_group_total(model, group_key)
283
428
  export function fold_group_shown(model, state, group_key)
284
429
  {
285
430
  let total = fold_group_total(model, group_key);
286
- let n = state.shown.get(group_key) || 0;
431
+ let n;
432
+ if(state.shown.has(group_key)) {
433
+ n = state.shown.get(group_key);
434
+ } else if(group_key.charAt(0) === "|") {
435
+ /* A ROOT group nobody has set is open on its first page: no
436
+ * pill folds the roots, so "unset" cannot mean "folded" -- a
437
+ * topic shown again after being hidden, or one that appears
438
+ * when the main topic moves, would otherwise draw nothing. */
439
+ n = state.page_size;
440
+ } else {
441
+ n = 0;
442
+ }
287
443
  return Math.min(n, total);
288
444
  }
289
445
 
@@ -24,6 +24,8 @@ import {
24
24
  fold_group_key,
25
25
  fold_root_group_key,
26
26
  fold_split_group_key,
27
+ fold_main_topic,
28
+ fold_group_shown,
27
29
  } from "./treedb_fold_model.js";
28
30
 
29
31
  const places_desc = {
@@ -133,12 +135,21 @@ describe("the model", () => {
133
135
  expect(m.groups.get(fold_root_group_key("device_groups"))).toEqual([K("device_groups", "g1")]);
134
136
  });
135
137
 
136
- test("a child of an `extended` parent only is a root: the engine draws no edge to one", () => {
138
+ test("a child of an `extended` parent only has no parent: the engine draws no edge to one", () => {
137
139
  let recs = make_records(1);
138
140
  recs.devices[0] = {id: "d0", group: "device_groups^g1^members"};
139
141
  let m = fold_build_model(descs, recs);
140
- expect(m.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d0")]);
141
142
  expect(m.nodes.get(K("device_groups", "g1")).hooks).toEqual({});
143
+ /* devices hang from places (the main topic), so a device with
144
+ * no parent is LOOSE, not a root -- see the "main topic" block */
145
+ expect(m.loose.devices).toEqual([K("devices", "d0")]);
146
+ expect(m.groups.get(fold_root_group_key("devices"))).toBeUndefined();
147
+ /* and with no main topic it is a plain root */
148
+ let m2 = fold_build_model({places: places_desc, devices: devices_desc, device_groups: groups_desc}, recs, {main_topic: ""});
149
+ expect(m2.main_topic).toBe("places"); /* "" means "deduce", and places reaches devices */
150
+ let m3 = fold_build_model({devices: devices_desc, device_groups: groups_desc}, recs);
151
+ expect(m3.main_topic).toBe("");
152
+ expect(m3.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d0")]);
142
153
  });
143
154
 
144
155
  test("a cycle with no way in gets one root, not a row of them", () => {
@@ -164,6 +175,113 @@ describe("the model", () => {
164
175
  });
165
176
  });
166
177
 
178
+ describe("main topic, hidden topics, loose records", () => {
179
+ test("the main topic is the one whose hooks reach the most others; places, not device_groups", () => {
180
+ expect(fold_main_topic(descs)).toBe("places");
181
+ let m = fold_build_model(descs, make_records(3));
182
+ expect(m.main_topic).toBe("places");
183
+ expect([...m.linked].sort()).toEqual(["controllers", "devices", "places"]);
184
+ expect(m.linked.has("device_groups")).toBe(false);
185
+ });
186
+
187
+ test("a self-referent hook breaks a tie", () => {
188
+ let a = {topic_name: "a", node_treedb_type: "extended",
189
+ cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "a"}}]};
190
+ let b = {topic_name: "b", node_treedb_type: "hierarchical",
191
+ cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "b"}},
192
+ {id: "bs", flag: ["hook"], hook: {b: "parent"}},
193
+ {id: "parent", flag: ["fkey"], fkey: {b: "bs"}}]};
194
+ let x = {topic_name: "x", node_treedb_type: "child", cols: [{id: "id", flag: []}]};
195
+ expect(fold_main_topic({a: a, b: b, x: x})).toBe("b");
196
+ expect(fold_main_topic({x: x})).toBe("");
197
+ });
198
+
199
+ test("an explicit main topic wins over the deduced one", () => {
200
+ let m = fold_build_model(descs, make_records(3), {main_topic: "controllers"});
201
+ expect(m.main_topic).toBe("controllers");
202
+ expect([...m.linked].sort()).toEqual(["controllers", "devices"]);
203
+ /* places is not linked to controllers, so its roots are roots */
204
+ expect(m.groups.get(fold_root_group_key("places"))).toEqual([K("places", "es")]);
205
+ });
206
+
207
+ test("a device with no place is LOOSE: counted, not a root, unless asked for", () => {
208
+ let recs = make_records(3);
209
+ recs.devices.push({id: "d_loose"});
210
+ let m = fold_build_model(descs, recs);
211
+ expect(m.loose.devices).toEqual([K("devices", "d_loose")]);
212
+ expect(m.groups.get(fold_root_group_key("devices"))).toBeUndefined();
213
+ expect(m.totals.devices).toBe(4);
214
+ let m2 = fold_build_model(descs, recs, {loose_topics: ["devices"]});
215
+ expect(m2.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d_loose")]);
216
+ expect(m2.loose.devices).toEqual([K("devices", "d_loose")]);
217
+ });
218
+
219
+ test("a device hanging from a loose controller only is loose too, and follows it", () => {
220
+ let recs = make_records(0);
221
+ recs.controllers.push({id: "c_loose"});
222
+ recs.devices.push({id: "d_under", controller: ["controllers^c_loose^devices"]});
223
+ let m = fold_build_model(descs, recs);
224
+ expect(m.loose.controllers).toEqual([K("controllers", "c_loose")]);
225
+ expect(m.loose.devices).toEqual([K("devices", "d_under")]);
226
+ let st = fold_new_state(24);
227
+ fold_expand_all(m, st);
228
+ expect(fold_visible_set(m, st).has(K("devices", "d_under"))).toBe(false);
229
+ let m2 = fold_build_model(descs, recs, {loose_topics: ["controllers"]});
230
+ fold_expand_all(m2, st);
231
+ expect(fold_visible_set(m2, st).has(K("devices", "d_under"))).toBe(true);
232
+ });
233
+
234
+ test("a hidden topic leaves the model whole: no node, no pill, no edge", () => {
235
+ let m = fold_build_model(descs, make_records(30), {hidden_topics: ["devices"]});
236
+ expect(m.topics).toEqual(["places", "controllers", "device_groups"]);
237
+ expect(m.totals.devices).toBe(30);
238
+ expect(m.nodes.get(K("places", "nave1")).hooks.devices).toBeUndefined();
239
+ expect(fold_pills_of(m, fold_new_state(24), K("places", "nave1")).map((p) => p.hook))
240
+ .toEqual(["controllers"]);
241
+ expect(m.hidden.has("devices")).toBe(true);
242
+ });
243
+
244
+ test("hiding a topic in the middle: what hung from it alone becomes loose", () => {
245
+ let m = fold_build_model(descs, make_records(12), {hidden_topics: ["controllers"]});
246
+ /* d0..d9 hang from nave1 AND c1: still under nave1 */
247
+ expect(m.nodes.get(K("devices", "d0")).parents.length).toBe(1);
248
+ expect(m.loose.devices).toEqual([]);
249
+ /* places hidden = the main topic gone: nothing governs, and
250
+ * whatever has no parent left is a root -- c1, with d0 and d1
251
+ * still hanging from it */
252
+ let m2 = fold_build_model(descs, make_records(2), {hidden_topics: ["places"]});
253
+ expect(m2.main_topic).toBe("");
254
+ expect(m2.groups.get(fold_root_group_key("controllers"))).toEqual([K("controllers", "c1")]);
255
+ expect(m2.groups.get(fold_root_group_key("devices"))).toBeUndefined();
256
+ let m3 = fold_build_model(descs, {devices: [{id: "d9"}]}, {hidden_topics: ["places"]});
257
+ expect(m3.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d9")]);
258
+ });
259
+
260
+ test("a topic shown again opens on its roots' first page: nobody folds the roots", () => {
261
+ let recs = make_records(3);
262
+ let st = fold_new_state(24);
263
+ let m = fold_build_model(descs, recs, {hidden_topics: ["device_groups"]});
264
+ fold_expand_to_depth(m, st, 2);
265
+ expect(fold_visible_set(m, st).has(K("device_groups", "g1"))).toBe(false);
266
+ let m2 = fold_build_model(descs, recs);
267
+ expect(fold_visible_set(m2, st).has(K("device_groups", "g1"))).toBe(true);
268
+ expect(fold_pending_groups(m2, st, fold_visible_set(m2, st))).toEqual([]);
269
+ });
270
+
271
+ test("the fold state survives a hide and a show", () => {
272
+ let recs = make_records(30);
273
+ let st = fold_new_state(24);
274
+ let m = fold_build_model(descs, recs);
275
+ fold_expand_to_depth(m, st, 4);
276
+ let g = fold_group_key(K("places", "nave1"), "devices");
277
+ expect(fold_group_shown(m, st, g)).toBe(24);
278
+ let m_hidden = fold_build_model(descs, recs, {hidden_topics: ["devices"]});
279
+ expect(fold_group_shown(m_hidden, st, g)).toBe(0); /* no group: nothing shown */
280
+ let m_back = fold_build_model(descs, recs);
281
+ expect(fold_group_shown(m_back, st, g)).toBe(24); /* remembered */
282
+ });
283
+ });
284
+
167
285
  describe("depth", () => {
168
286
  test("depth 1 is the roots alone", () => {
169
287
  let m = fold_build_model(descs, make_records(30));