@yuneta/gobj-ui 7.23.140 → 7.23.142
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/README.md +123 -37
- package/dist/gobj-ui.cjs.js +412 -140
- package/dist/gobj-ui.es.js +412 -140
- package/package.json +2 -2
- package/src/c_g6_nodes_tree.js +98 -59
- package/src/c_yui_schema_editor.js +16 -0
- package/src/c_yui_treedb_graph.js +153 -23
- package/src/schema_import.js +2 -1
- package/src/schema_model.js +2 -1
- package/src/schema_to_c.js +1 -1
- package/src/schema_validate.js +20 -0
- package/src/schema_validate.test.js +44 -0
- package/src/treedb_fold_model.js +185 -21
- package/src/treedb_fold_model.test.js +120 -5
package/src/schema_model.js
CHANGED
|
@@ -183,7 +183,7 @@ function sort_by_order(list)
|
|
|
183
183
|
* schema_version, c_schema_version, system_schema_version,
|
|
184
184
|
* topics: [{
|
|
185
185
|
* id, name, order, record,
|
|
186
|
-
* pkey, topic_version, system_topic,
|
|
186
|
+
* pkey, topic_version, system_topic, main_topic,
|
|
187
187
|
* cols: [{id, name, order, record}]
|
|
188
188
|
* }]
|
|
189
189
|
* }],
|
|
@@ -239,6 +239,7 @@ function build_schema_model(records)
|
|
|
239
239
|
tkey: record.tkey || "",
|
|
240
240
|
topic_version: record.topic_version,
|
|
241
241
|
system_topic: !!record.system_topic,
|
|
242
|
+
main_topic: !!record.main_topic,
|
|
242
243
|
cols: []
|
|
243
244
|
};
|
|
244
245
|
by_topic_id[entry.id] = entry;
|
package/src/schema_to_c.js
CHANGED
|
@@ -47,7 +47,7 @@ const DEFAULT_PAD = 68;
|
|
|
47
47
|
* and every export diffs against the last one. */
|
|
48
48
|
const TOPIC_KEY_ORDER = [
|
|
49
49
|
"id", "pkey", "pkey2s", "system_flag", "tkey",
|
|
50
|
-
"topic_version", "system_topic"
|
|
50
|
+
"topic_version", "system_topic", "main_topic"
|
|
51
51
|
];
|
|
52
52
|
|
|
53
53
|
/* Same for a column, in the order the .c literals write it. */
|
package/src/schema_validate.js
CHANGED
|
@@ -189,6 +189,26 @@ function validate_schema(treedb, options)
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/* The topic the tree hangs from (`main_topic`): ONE per treedb, and
|
|
193
|
+
* only a topic hooked to itself. treedb_open_db logs either mistake
|
|
194
|
+
* and ignores the mark, so the restart succeeds and the graph goes
|
|
195
|
+
* on deducing -- better said here, before it. */
|
|
196
|
+
let marked = treedb.topics.filter((topic) => topic.main_topic);
|
|
197
|
+
if(marked.length > 1) {
|
|
198
|
+
add("error", "more than one main topic", marked[1].name, "",
|
|
199
|
+
marked.map((topic) => topic.name).join(", "));
|
|
200
|
+
}
|
|
201
|
+
for(let topic of marked) {
|
|
202
|
+
let hooks_itself = topic.cols.some((col) => {
|
|
203
|
+
let hook = col_hook(col.record);
|
|
204
|
+
return col_flags(col.record).indexOf("hook") >= 0 &&
|
|
205
|
+
!!hook && Object.prototype.hasOwnProperty.call(hook, topic.name);
|
|
206
|
+
});
|
|
207
|
+
if(!hooks_itself) {
|
|
208
|
+
add("error", "main topic not hooked to itself", topic.name, "", "");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
192
212
|
/* How many hooks name each fkey column. Both answers are a
|
|
193
213
|
* finding: NONE and the parent side of the link was renamed or
|
|
194
214
|
* deleted, so the references are written by nobody; TWO and the
|
|
@@ -94,6 +94,50 @@ describe("the topic will not open", () => {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
describe("the main topic mark", () => {
|
|
98
|
+
/* departments hooked to itself: the one topic the mark can go on */
|
|
99
|
+
const self_hook = {
|
|
100
|
+
id: "db.departments.departments", value: "departments", order: 3,
|
|
101
|
+
type: "dict", flag: ["hook"], hook: {departments: "departments"},
|
|
102
|
+
topics: ["topics^db.departments^cols"]
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
test("on a topic hooked to itself, says nothing", () => {
|
|
106
|
+
const t = schema({extra_col: self_hook});
|
|
107
|
+
t.topics[0].main_topic = true;
|
|
108
|
+
const c = codes(validate_schema(t));
|
|
109
|
+
expect(c).not.toContain("main topic not hooked to itself");
|
|
110
|
+
expect(c).not.toContain("more than one main topic");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("on a topic that is not hooked to itself", () => {
|
|
114
|
+
const t = schema();
|
|
115
|
+
t.topics[1].main_topic = true;
|
|
116
|
+
const f = validate_schema(t);
|
|
117
|
+
expect(codes(f)).toContain("main topic not hooked to itself");
|
|
118
|
+
expect(has_errors(f)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("on two topics", () => {
|
|
122
|
+
const t = schema({extra_col: self_hook});
|
|
123
|
+
t.topics[0].main_topic = true;
|
|
124
|
+
t.topics[1].main_topic = true;
|
|
125
|
+
const f = validate_schema(t);
|
|
126
|
+
expect(codes(f)).toContain("more than one main topic");
|
|
127
|
+
expect(f.find((x) => x.code === "more than one main topic").detail)
|
|
128
|
+
.toBe("departments, users");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("the model reads the mark from the record", () => {
|
|
132
|
+
const model = build_schema_model({
|
|
133
|
+
treedbs: [{id: "db"}],
|
|
134
|
+
topics: [{id: "db.a", value: "a", main_topic: true, treedbs: ["treedbs^db^topics"]}],
|
|
135
|
+
cols: [],
|
|
136
|
+
});
|
|
137
|
+
expect(model.treedbs[0].topics[0].main_topic).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
97
141
|
describe("the link does nothing, and the write succeeded", () => {
|
|
98
142
|
test("a hook with no mapping", () => {
|
|
99
143
|
expect(codes(validate_schema(schema({hook: null})))).toContain("hook has no mapping");
|
package/src/treedb_fold_model.js
CHANGED
|
@@ -152,7 +152,13 @@ export function fold_build_model(descs, records, opts)
|
|
|
152
152
|
|
|
153
153
|
let hidden = new Set(is_array(opts.hidden_topics)? opts.hidden_topics : []);
|
|
154
154
|
let loose_shown = new Set(is_array(opts.loose_topics)? opts.loose_topics : []);
|
|
155
|
-
let main_topic = opts.main_topic ||
|
|
155
|
+
let main_topic = opts.main_topic || "";
|
|
156
|
+
if(main_topic && !fold_is_hierarchical(descs, main_topic)) {
|
|
157
|
+
main_topic = ""; /* a pick the rule does not allow (see fold_main_topic): deduce */
|
|
158
|
+
}
|
|
159
|
+
if(!main_topic) {
|
|
160
|
+
main_topic = fold_main_topic(descs);
|
|
161
|
+
}
|
|
156
162
|
if(main_topic && (!descs[main_topic] || hidden.has(main_topic))) {
|
|
157
163
|
main_topic = ""; /* a main topic that is not drawn governs nothing */
|
|
158
164
|
}
|
|
@@ -281,7 +287,7 @@ export function fold_build_model(descs, records, opts)
|
|
|
281
287
|
}
|
|
282
288
|
}
|
|
283
289
|
|
|
284
|
-
|
|
290
|
+
let model = {
|
|
285
291
|
nodes: nodes,
|
|
286
292
|
groups: groups,
|
|
287
293
|
topics: topics,
|
|
@@ -291,38 +297,124 @@ export function fold_build_model(descs, records, opts)
|
|
|
291
297
|
linked: linked, /* Set of topics the schema hangs from main */
|
|
292
298
|
loose: loose, /* {topic: [key, ...]} records that should hang and do not */
|
|
293
299
|
loose_shown: loose_shown,
|
|
300
|
+
level_of: null, /* Map<node_key, level> of the MAIN tree, see below */
|
|
301
|
+
levels: 0, /* its deepest level (0 = no tree to step through) */
|
|
294
302
|
};
|
|
303
|
+
count_levels(model);
|
|
304
|
+
return model;
|
|
295
305
|
}
|
|
296
306
|
|
|
297
307
|
/************************************************************
|
|
298
|
-
* The
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
308
|
+
* The LEVELS of the main tree: the roots of the main topic --
|
|
309
|
+
* and of the topics hung from it, whose roots are the loose
|
|
310
|
+
* records the reader asked for -- are level 1, their children
|
|
311
|
+
* level 2, and so on down every hook of every topic that is not
|
|
312
|
+
* hidden. A node reached by two paths takes the shallowest.
|
|
313
|
+
*
|
|
314
|
+
* With no main topic every topic is a tree of its own and all
|
|
315
|
+
* of them count. A topic the schema does not tie to the main
|
|
316
|
+
* one is a separate tree, and has no level: the stepper of the
|
|
317
|
+
* toolbar speaks for the main tree and nothing else, those
|
|
318
|
+
* trees are opened by their own pills.
|
|
319
|
+
************************************************************/
|
|
320
|
+
function count_levels(model)
|
|
321
|
+
{
|
|
322
|
+
let level_of = new Map();
|
|
323
|
+
let queue = [];
|
|
324
|
+
let deepest = 0;
|
|
325
|
+
|
|
326
|
+
for(let topic_name of model.topics) {
|
|
327
|
+
if(model.main_topic && !model.linked.has(topic_name)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
let roots = model.groups.get(fold_root_group_key(topic_name)) || [];
|
|
331
|
+
for(let key of roots) {
|
|
332
|
+
if(!level_of.has(key)) {
|
|
333
|
+
level_of.set(key, 1);
|
|
334
|
+
queue.push(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
let head = 0;
|
|
339
|
+
while(head < queue.length) {
|
|
340
|
+
let key = queue[head++];
|
|
341
|
+
let lv = level_of.get(key);
|
|
342
|
+
if(lv > deepest) {
|
|
343
|
+
deepest = lv;
|
|
344
|
+
}
|
|
345
|
+
let node = model.nodes.get(key);
|
|
346
|
+
if(!node) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
350
|
+
for(let child of node.hooks[hook]) {
|
|
351
|
+
if(!level_of.has(child)) {
|
|
352
|
+
level_of.set(child, lv + 1);
|
|
353
|
+
queue.push(child);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
model.level_of = level_of;
|
|
360
|
+
model.levels = deepest;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/************************************************************
|
|
364
|
+
* A HIERARCHICAL topic: one hooked to ITSELF -- places inside
|
|
365
|
+
* places, groups inside groups, roles inside roles. It is the
|
|
366
|
+
* only kind of topic a tree can hang from: a topic that only
|
|
367
|
+
* reaches OTHER topics is a list with children, and making it
|
|
368
|
+
* the trunk drew the agent's treedb from `yunos` instead of
|
|
369
|
+
* `realms`.
|
|
370
|
+
*
|
|
371
|
+
* (Not the engine's `entity` tier, which is any topic with
|
|
372
|
+
* hooks AND fkeys: that one sizes a card and says nothing about
|
|
373
|
+
* where a tree starts.)
|
|
374
|
+
************************************************************/
|
|
375
|
+
export function fold_is_hierarchical(descs, topic_name)
|
|
376
|
+
{
|
|
377
|
+
if(!is_object(descs) || !descs[topic_name]) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return hook_targets(descs, topic_name).has(topic_name);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/************************************************************
|
|
384
|
+
* The topic the tree hangs from. Only a HIERARCHICAL topic can
|
|
385
|
+
* be it, and in this order:
|
|
386
|
+
*
|
|
387
|
+
* the one the schema MARKS (`main_topic: true` on the topic,
|
|
388
|
+
* sent by the backend in its desc since SDK 7.19; the first
|
|
389
|
+
* one in schema order if a schema marks two);
|
|
390
|
+
* else the one whose hooks reach the most OTHER topics,
|
|
391
|
+
* schema order breaking a tie.
|
|
392
|
+
*
|
|
393
|
+
* "" when no topic is hooked to itself: there is no trunk, and
|
|
394
|
+
* every topic is a tree of its own. (A pick by the reader comes
|
|
395
|
+
* first of all, and is fold_build_model's business.)
|
|
303
396
|
************************************************************/
|
|
304
397
|
export function fold_main_topic(descs)
|
|
305
398
|
{
|
|
306
399
|
descs = is_object(descs)? descs : {};
|
|
307
|
-
let
|
|
308
|
-
|
|
309
|
-
|
|
400
|
+
let candidates = Object.keys(descs).filter((topic_name) => {
|
|
401
|
+
return topic_name.substring(0, 2) !== "__" && fold_is_hierarchical(descs, topic_name);
|
|
402
|
+
});
|
|
310
403
|
|
|
311
|
-
for(let topic_name of
|
|
312
|
-
if(topic_name.
|
|
313
|
-
|
|
404
|
+
for(let topic_name of candidates) {
|
|
405
|
+
if(descs[topic_name].main_topic === true) {
|
|
406
|
+
return topic_name;
|
|
314
407
|
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let best = "";
|
|
411
|
+
let best_score = -1;
|
|
412
|
+
for(let topic_name of candidates) {
|
|
315
413
|
let reach = hook_targets(descs, topic_name);
|
|
316
|
-
let self = reach.has(topic_name);
|
|
317
414
|
reach.delete(topic_name);
|
|
318
|
-
|
|
319
|
-
if(score === 0) {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if(score > best_score || (score === best_score && self && !best_self)) {
|
|
415
|
+
if(reach.size > best_score) {
|
|
323
416
|
best = topic_name;
|
|
324
|
-
best_score =
|
|
325
|
-
best_self = self;
|
|
417
|
+
best_score = reach.size;
|
|
326
418
|
}
|
|
327
419
|
}
|
|
328
420
|
return best;
|
|
@@ -565,6 +657,78 @@ export function fold_expand_to_depth(model, state, depth)
|
|
|
565
657
|
}
|
|
566
658
|
}
|
|
567
659
|
|
|
660
|
+
/************************************************************
|
|
661
|
+
* The toolbar's STEPPER over the levels of the main tree (see
|
|
662
|
+
* count_levels): level N shows the roots and N-1 levels under
|
|
663
|
+
* them. A level is a floor of what is open, not a picture that
|
|
664
|
+
* replaces it:
|
|
665
|
+
*
|
|
666
|
+
* opening to N opens, on its first page, every hook of
|
|
667
|
+
* every node above level N that is folded;
|
|
668
|
+
* what the reader opened, and every page
|
|
669
|
+
* already shown, stays
|
|
670
|
+
* closing to N folds every hook of every node at level N
|
|
671
|
+
* or below, and touches nothing above it
|
|
672
|
+
*
|
|
673
|
+
* A separate tree (a topic not tied to the main one) has no
|
|
674
|
+
* level, so neither ever touches it.
|
|
675
|
+
************************************************************/
|
|
676
|
+
export function fold_level_clamp(model, level)
|
|
677
|
+
{
|
|
678
|
+
let top = (model.levels > 0)? model.levels : 1;
|
|
679
|
+
level = Math.floor(Number(level)) || 1;
|
|
680
|
+
if(level < 1) {
|
|
681
|
+
return 1;
|
|
682
|
+
}
|
|
683
|
+
if(level > top) {
|
|
684
|
+
return top;
|
|
685
|
+
}
|
|
686
|
+
return level;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function fold_open_levels(model, state, level)
|
|
690
|
+
{
|
|
691
|
+
for(let [key, lv] of model.level_of) {
|
|
692
|
+
if(lv >= level) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
let node = model.nodes.get(key);
|
|
696
|
+
if(!node) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
700
|
+
let group_key = fold_group_key(key, hook);
|
|
701
|
+
if(node.hooks[hook].length > 0 && !fold_is_open(state, group_key)) {
|
|
702
|
+
state.shown.set(group_key, state.page_size);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function fold_close_levels(model, state, level)
|
|
709
|
+
{
|
|
710
|
+
for(let [key, lv] of model.level_of) {
|
|
711
|
+
if(lv < level) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
let node = model.nodes.get(key);
|
|
715
|
+
if(!node) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
for(let hook of Object.keys(node.hooks)) {
|
|
719
|
+
state.shown.delete(fold_group_key(key, hook));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/* From scratch: the roots of every tree on their first page, and
|
|
725
|
+
* the main tree open down to `level`. What a load opens. */
|
|
726
|
+
export function fold_expand_to_level(model, state, level)
|
|
727
|
+
{
|
|
728
|
+
fold_collapse_all(model, state);
|
|
729
|
+
fold_open_levels(model, state, fold_level_clamp(model, level));
|
|
730
|
+
}
|
|
731
|
+
|
|
568
732
|
/************************************************************
|
|
569
733
|
* The set of node keys on screen, from the roots down through
|
|
570
734
|
* every open group -- and only through open groups whose parent
|
|
@@ -26,11 +26,17 @@ import {
|
|
|
26
26
|
fold_split_group_key,
|
|
27
27
|
fold_main_topic,
|
|
28
28
|
fold_group_shown,
|
|
29
|
+
fold_is_open,
|
|
30
|
+
fold_level_clamp,
|
|
31
|
+
fold_open_levels,
|
|
32
|
+
fold_close_levels,
|
|
33
|
+
fold_expand_to_level,
|
|
34
|
+
fold_is_hierarchical,
|
|
29
35
|
} from "./treedb_fold_model.js";
|
|
30
36
|
|
|
31
37
|
const places_desc = {
|
|
32
38
|
topic_name: "places",
|
|
33
|
-
node_treedb_type: "
|
|
39
|
+
node_treedb_type: "entity",
|
|
34
40
|
cols: [
|
|
35
41
|
{id: "id", flag: []},
|
|
36
42
|
{id: "place", flag: ["fkey"], fkey: {places: "places"}},
|
|
@@ -51,13 +57,20 @@ const devices_desc = {
|
|
|
51
57
|
};
|
|
52
58
|
const controllers_desc = {
|
|
53
59
|
topic_name: "controllers",
|
|
54
|
-
node_treedb_type: "
|
|
60
|
+
node_treedb_type: "entity",
|
|
55
61
|
cols: [
|
|
56
62
|
{id: "id", flag: []},
|
|
57
63
|
{id: "place", flag: ["fkey"], fkey: {places: "controllers"}},
|
|
58
64
|
{id: "devices", flag: ["hook"], hook: {devices: "controller"}},
|
|
59
65
|
],
|
|
60
66
|
};
|
|
67
|
+
/* controllers inside controllers: a second HIERARCHICAL topic */
|
|
68
|
+
const controllers_self = Object.assign({}, controllers_desc, {
|
|
69
|
+
cols: controllers_desc.cols.concat([
|
|
70
|
+
{id: "controllers", flag: ["hook"], hook: {controllers: "parent"}},
|
|
71
|
+
{id: "parent", flag: ["fkey"], fkey: {controllers: "controllers"}},
|
|
72
|
+
]),
|
|
73
|
+
});
|
|
61
74
|
const groups_desc = {
|
|
62
75
|
topic_name: "device_groups",
|
|
63
76
|
node_treedb_type: "extended",
|
|
@@ -184,10 +197,33 @@ describe("main topic, hidden topics, loose records", () => {
|
|
|
184
197
|
expect(m.linked.has("device_groups")).toBe(false);
|
|
185
198
|
});
|
|
186
199
|
|
|
200
|
+
test("only a topic hooked to ITSELF can be the main one", () => {
|
|
201
|
+
expect(fold_is_hierarchical(descs, "places")).toBe(true);
|
|
202
|
+
expect(fold_is_hierarchical(descs, "controllers")).toBe(false);
|
|
203
|
+
expect(fold_is_hierarchical(descs, "device_groups")).toBe(false);
|
|
204
|
+
/* c reaches x and nothing reaches itself: no trunk at all */
|
|
205
|
+
let c = {topic_name: "c", cols: [{id: "id", flag: []},
|
|
206
|
+
{id: "xs", flag: ["hook"], hook: {x: "c"}}]};
|
|
207
|
+
let x = {topic_name: "x", cols: [{id: "id", flag: []}, {id: "c", flag: ["fkey"]}]};
|
|
208
|
+
expect(fold_main_topic({c: c, x: x})).toBe("");
|
|
209
|
+
/* a tree of places alone, reaching no other topic, is still a trunk */
|
|
210
|
+
expect(fold_main_topic({places: places_desc})).toBe("places");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("the schema's mark wins over the deduction, among hierarchical topics", () => {
|
|
214
|
+
let d = Object.assign({}, descs, {controllers: controllers_self});
|
|
215
|
+
expect(fold_main_topic(d)).toBe("places"); /* places reaches more */
|
|
216
|
+
let marked = Object.assign({}, d, {controllers: Object.assign({}, controllers_self, {main_topic: true})});
|
|
217
|
+
expect(fold_main_topic(marked)).toBe("controllers");
|
|
218
|
+
/* a mark on a topic that is not hierarchical is not a trunk */
|
|
219
|
+
let wrong = Object.assign({}, descs, {devices: Object.assign({}, devices_desc, {main_topic: true})});
|
|
220
|
+
expect(fold_main_topic(wrong)).toBe("places");
|
|
221
|
+
});
|
|
222
|
+
|
|
187
223
|
test("a self-referent hook breaks a tie", () => {
|
|
188
224
|
let a = {topic_name: "a", node_treedb_type: "extended",
|
|
189
225
|
cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "a"}}]};
|
|
190
|
-
let b = {topic_name: "b", node_treedb_type: "
|
|
226
|
+
let b = {topic_name: "b", node_treedb_type: "entity",
|
|
191
227
|
cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "b"}},
|
|
192
228
|
{id: "bs", flag: ["hook"], hook: {b: "parent"}},
|
|
193
229
|
{id: "parent", flag: ["fkey"], fkey: {b: "bs"}}]};
|
|
@@ -196,12 +232,16 @@ describe("main topic, hidden topics, loose records", () => {
|
|
|
196
232
|
expect(fold_main_topic({x: x})).toBe("");
|
|
197
233
|
});
|
|
198
234
|
|
|
199
|
-
test("
|
|
200
|
-
let
|
|
235
|
+
test("the reader's pick wins -- if it is a hierarchical topic", () => {
|
|
236
|
+
let d = Object.assign({}, descs, {controllers: controllers_self});
|
|
237
|
+
let m = fold_build_model(d, make_records(3), {main_topic: "controllers"});
|
|
201
238
|
expect(m.main_topic).toBe("controllers");
|
|
202
239
|
expect([...m.linked].sort()).toEqual(["controllers", "devices"]);
|
|
203
240
|
/* places is not linked to controllers, so its roots are roots */
|
|
204
241
|
expect(m.groups.get(fold_root_group_key("places"))).toEqual([K("places", "es")]);
|
|
242
|
+
/* a pick the rule refuses is ignored, and the trunk is deduced */
|
|
243
|
+
let m2 = fold_build_model(descs, make_records(3), {main_topic: "controllers"});
|
|
244
|
+
expect(m2.main_topic).toBe("places");
|
|
205
245
|
});
|
|
206
246
|
|
|
207
247
|
test("a device with no place is LOOSE: counted, not a root, unless asked for", () => {
|
|
@@ -323,6 +363,81 @@ describe("depth", () => {
|
|
|
323
363
|
});
|
|
324
364
|
});
|
|
325
365
|
|
|
366
|
+
describe("levels of the main tree (the toolbar's stepper)", () => {
|
|
367
|
+
test("levels count from the main topic's roots, down every hook", () => {
|
|
368
|
+
let m = fold_build_model(descs, make_records(30));
|
|
369
|
+
/* es 1, norte/sur 2, nave1 3, its devices and c1 4 */
|
|
370
|
+
expect(m.levels).toBe(4);
|
|
371
|
+
expect(m.level_of.get(K("places", "es"))).toBe(1);
|
|
372
|
+
expect(m.level_of.get(K("places", "nave1"))).toBe(3);
|
|
373
|
+
/* d0 hangs from nave1 (3) and c1 (4): the shallowest wins */
|
|
374
|
+
expect(m.level_of.get(K("devices", "d0"))).toBe(4);
|
|
375
|
+
/* device_groups is not tied to places: a tree of its own, no level */
|
|
376
|
+
expect(m.level_of.has(K("device_groups", "g1"))).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("a hidden topic takes its levels with it", () => {
|
|
380
|
+
let m = fold_build_model(descs, make_records(30), {hidden_topics: ["devices", "controllers"]});
|
|
381
|
+
expect(m.levels).toBe(3);
|
|
382
|
+
let m0 = fold_build_model({}, {});
|
|
383
|
+
expect(m0.levels).toBe(0);
|
|
384
|
+
expect(fold_level_clamp(m0, 5)).toBe(1);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("clamp keeps a level between 1 and the deepest one", () => {
|
|
388
|
+
let m = fold_build_model(descs, make_records(3));
|
|
389
|
+
expect(fold_level_clamp(m, 0)).toBe(1);
|
|
390
|
+
expect(fold_level_clamp(m, 3)).toBe(3);
|
|
391
|
+
expect(fold_level_clamp(m, 9)).toBe(4);
|
|
392
|
+
expect(fold_level_clamp(m, "2")).toBe(2);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("level 1 is the roots of every tree, and each step opens one more", () => {
|
|
396
|
+
let m = fold_build_model(descs, make_records(30));
|
|
397
|
+
let st = fold_new_state(24);
|
|
398
|
+
fold_expand_to_level(m, st, 1);
|
|
399
|
+
expect([...fold_visible_set(m, st)].sort())
|
|
400
|
+
.toEqual([K("device_groups", "g1"), K("places", "es")]);
|
|
401
|
+
fold_open_levels(m, st, 2);
|
|
402
|
+
let v = fold_visible_set(m, st);
|
|
403
|
+
expect(v.has(K("places", "norte"))).toBe(true);
|
|
404
|
+
expect(v.has(K("places", "nave1"))).toBe(false);
|
|
405
|
+
fold_open_levels(m, st, 3);
|
|
406
|
+
expect(fold_visible_set(m, st).has(K("places", "nave1"))).toBe(true);
|
|
407
|
+
expect(fold_visible_set(m, st).has(K("devices", "d0"))).toBe(false);
|
|
408
|
+
fold_open_levels(m, st, 4);
|
|
409
|
+
v = fold_visible_set(m, st);
|
|
410
|
+
expect(v.has(K("devices", "d23"))).toBe(true);
|
|
411
|
+
expect(v.has(K("devices", "d24"))).toBe(false); /* one page */
|
|
412
|
+
expect(v.has(K("controllers", "c1"))).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("opening keeps the pages already shown; closing folds only from its level down", () => {
|
|
416
|
+
let m = fold_build_model(descs, make_records(30));
|
|
417
|
+
let st = fold_new_state(24);
|
|
418
|
+
fold_expand_to_level(m, st, 4);
|
|
419
|
+
let g = fold_group_key(K("places", "nave1"), "devices");
|
|
420
|
+
fold_show_more(m, st, g); /* 30 */
|
|
421
|
+
fold_open_levels(m, st, 4);
|
|
422
|
+
expect(fold_group_shown(m, st, g)).toBe(30);
|
|
423
|
+
fold_close_levels(m, st, 3);
|
|
424
|
+
let v = fold_visible_set(m, st);
|
|
425
|
+
expect(v.has(K("places", "nave1"))).toBe(true); /* level 3 stays */
|
|
426
|
+
expect(v.has(K("devices", "d0"))).toBe(false); /* its hooks folded */
|
|
427
|
+
expect(v.has(K("controllers", "c1"))).toBe(false);
|
|
428
|
+
expect(fold_is_open(st, fold_group_key(K("places", "norte"), "places"))).toBe(true);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("closing does not reopen what the reader folded above the level", () => {
|
|
432
|
+
let m = fold_build_model(descs, make_records(3));
|
|
433
|
+
let st = fold_new_state(24);
|
|
434
|
+
fold_expand_to_level(m, st, 4);
|
|
435
|
+
fold_toggle(m, st, fold_group_key(K("places", "es"), "places")); /* fold es */
|
|
436
|
+
fold_close_levels(m, st, 3);
|
|
437
|
+
expect(fold_is_open(st, fold_group_key(K("places", "es"), "places"))).toBe(false);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
326
441
|
describe("toggle and pages", () => {
|
|
327
442
|
test("a folded group opens on its first page and closes again", () => {
|
|
328
443
|
let m = fold_build_model(descs, make_records(30));
|