@yuneta/gobj-ui 7.23.141 → 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 +24 -11
- package/dist/gobj-ui.cjs.js +109 -35
- package/dist/gobj-ui.es.js +109 -35
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +24 -14
- package/src/c_yui_schema_editor.js +16 -0
- package/src/c_yui_treedb_graph.js +7 -2
- 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 +52 -20
- package/src/treedb_fold_model.test.js +40 -5
package/README.md
CHANGED
|
@@ -1294,8 +1294,8 @@ a level or two, with a count on every cut.
|
|
|
1294
1294
|
parent shows it or it is a shown root — the **union**, so a device that hangs
|
|
1295
1295
|
from its place AND its controller is drawn once, under whichever opened
|
|
1296
1296
|
first, and folding one of the two leaves it where the other shows it.
|
|
1297
|
-
- **`expand_depth`** (default `2`) is how much
|
|
1298
|
-
|
|
1297
|
+
- **`expand_depth`** (default `1` since `7.23.142`, `2` before) is how much
|
|
1298
|
+
opens on load until the reader steps: the roots alone. **`fold_page_size`** (default `24`) is how many children of
|
|
1299
1299
|
one hook are shown at a time; the rest is a **`+N` chip** at the end of the
|
|
1300
1300
|
page that opens the next one. Both are attrs of `C_YUI_TREEDB_GRAPH`,
|
|
1301
1301
|
forwarded to the engine — and since `7.23.93` so are the seven the engine
|
|
@@ -1596,18 +1596,31 @@ them:
|
|
|
1596
1596
|
| control | what it does |
|
|
1597
1597
|
|---|---|
|
|
1598
1598
|
| the chip body | **show / hide** the topic. A hidden topic leaves the MODEL, not just the drawing: no card, no pill counts it, no edge reaches it, and whatever hung from it alone has no parent any more. The name is struck through and the count is the total. |
|
|
1599
|
-
| `★` / `☆` | the **main topic**: the trunk the tree hangs from. Filled on the main one (a mark, not a button — the main topic cannot be hidden, because hiding the trunk turns everything into roots, which is the pile); hollow on the others, where it moves the star. |
|
|
1599
|
+
| `★` / `☆` | the **main topic**: the trunk the tree hangs from, always a **hierarchical** topic (hooked to itself); the hollow star appears only on the other hierarchical topics, and only when there are two or more (`7.23.142`). Filled on the main one (a mark, not a button — the main topic cannot be hidden, because hiding the trunk turns everything into roots, which is the pile); hollow on the others, where it moves the star. |
|
|
1600
1600
|
| `+N` | the topic's **loose records**: the ones that should hang from the main tree and do not (a device with no place). Counted here, shown only on request. |
|
|
1601
1601
|
| `⌖` | **highlight** the topic — the focus, with `reveal: "all"`. |
|
|
1602
1602
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1603
|
+
**Only a HIERARCHICAL topic can be the main one** (since `7.23.142`): a topic
|
|
1604
|
+
hooked to ITSELF — places inside places, groups inside groups, roles inside
|
|
1605
|
+
roles (`fold_is_hierarchical()`). A topic that only reaches OTHER topics is a
|
|
1606
|
+
list with children, and the old rule (*the topic whose hooks reach the most
|
|
1607
|
+
others*) drew the agent's treedb from `yunos` instead of `realms`. In order:
|
|
1608
|
+
|
|
1609
|
+
1. the reader's pick (the star), if it is hierarchical — a saved pick the rule
|
|
1610
|
+
refuses is ignored;
|
|
1611
|
+
2. the topic the schema **marks**, `main_topic: true` on the topic, sent in the
|
|
1612
|
+
desc by SDK 7.19+ (`tranger2_topic_desc()`); the schema allows one, and only
|
|
1613
|
+
on a topic hooked to itself;
|
|
1614
|
+
3. the hierarchical topic that reaches the most other topics, schema order
|
|
1615
|
+
breaking a tie.
|
|
1616
|
+
|
|
1617
|
+
In every treedb of the SDK and the apps there is exactly one hierarchical
|
|
1618
|
+
topic (`places`, `device_groups`, `systems`, `roles`, `realms`), so the mark
|
|
1619
|
+
only has something to decide once a schema grows a second one. A treedb with
|
|
1620
|
+
no topic hooked to itself has no trunk, and every topic is a tree of its own.
|
|
1621
|
+
|
|
1622
|
+
(Not the engine's card tier: a topic with hooks AND fkeys is an `entity` card
|
|
1623
|
+
— it was called `hierarchical` until `7.23.142` — and that only sizes a card.)
|
|
1611
1624
|
|
|
1612
1625
|
What the main topic governs, in `treedb_fold_model.js`:
|
|
1613
1626
|
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -27675,7 +27675,9 @@ function fold_build_model(descs, records, opts) {
|
|
|
27675
27675
|
opts = (0, _yuneta_gobj_js.is_object)(opts) ? opts : {};
|
|
27676
27676
|
let hidden = new Set((0, _yuneta_gobj_js.is_array)(opts.hidden_topics) ? opts.hidden_topics : []);
|
|
27677
27677
|
let loose_shown = new Set((0, _yuneta_gobj_js.is_array)(opts.loose_topics) ? opts.loose_topics : []);
|
|
27678
|
-
let main_topic = opts.main_topic ||
|
|
27678
|
+
let main_topic = opts.main_topic || "";
|
|
27679
|
+
if (main_topic && !fold_is_hierarchical(descs, main_topic)) main_topic = "";
|
|
27680
|
+
if (!main_topic) main_topic = fold_main_topic(descs);
|
|
27679
27681
|
if (main_topic && (!descs[main_topic] || hidden.has(main_topic))) main_topic = "";
|
|
27680
27682
|
let linked = fold_linked_topics(descs, main_topic);
|
|
27681
27683
|
let totals = {};
|
|
@@ -27796,28 +27798,49 @@ function count_levels(model) {
|
|
|
27796
27798
|
model.levels = deepest;
|
|
27797
27799
|
}
|
|
27798
27800
|
/************************************************************
|
|
27799
|
-
*
|
|
27800
|
-
*
|
|
27801
|
-
*
|
|
27802
|
-
*
|
|
27803
|
-
*
|
|
27801
|
+
* A HIERARCHICAL topic: one hooked to ITSELF -- places inside
|
|
27802
|
+
* places, groups inside groups, roles inside roles. It is the
|
|
27803
|
+
* only kind of topic a tree can hang from: a topic that only
|
|
27804
|
+
* reaches OTHER topics is a list with children, and making it
|
|
27805
|
+
* the trunk drew the agent's treedb from `yunos` instead of
|
|
27806
|
+
* `realms`.
|
|
27807
|
+
*
|
|
27808
|
+
* (Not the engine's `entity` tier, which is any topic with
|
|
27809
|
+
* hooks AND fkeys: that one sizes a card and says nothing about
|
|
27810
|
+
* where a tree starts.)
|
|
27811
|
+
************************************************************/
|
|
27812
|
+
function fold_is_hierarchical(descs, topic_name) {
|
|
27813
|
+
if (!(0, _yuneta_gobj_js.is_object)(descs) || !descs[topic_name]) return false;
|
|
27814
|
+
return hook_targets(descs, topic_name).has(topic_name);
|
|
27815
|
+
}
|
|
27816
|
+
/************************************************************
|
|
27817
|
+
* The topic the tree hangs from. Only a HIERARCHICAL topic can
|
|
27818
|
+
* be it, and in this order:
|
|
27819
|
+
*
|
|
27820
|
+
* the one the schema MARKS (`main_topic: true` on the topic,
|
|
27821
|
+
* sent by the backend in its desc since SDK 7.19; the first
|
|
27822
|
+
* one in schema order if a schema marks two);
|
|
27823
|
+
* else the one whose hooks reach the most OTHER topics,
|
|
27824
|
+
* schema order breaking a tie.
|
|
27825
|
+
*
|
|
27826
|
+
* "" when no topic is hooked to itself: there is no trunk, and
|
|
27827
|
+
* every topic is a tree of its own. (A pick by the reader comes
|
|
27828
|
+
* first of all, and is fold_build_model's business.)
|
|
27804
27829
|
************************************************************/
|
|
27805
27830
|
function fold_main_topic(descs) {
|
|
27806
27831
|
descs = (0, _yuneta_gobj_js.is_object)(descs) ? descs : {};
|
|
27832
|
+
let candidates = Object.keys(descs).filter((topic_name) => {
|
|
27833
|
+
return topic_name.substring(0, 2) !== "__" && fold_is_hierarchical(descs, topic_name);
|
|
27834
|
+
});
|
|
27835
|
+
for (let topic_name of candidates) if (descs[topic_name].main_topic === true) return topic_name;
|
|
27807
27836
|
let best = "";
|
|
27808
|
-
let best_score =
|
|
27809
|
-
let
|
|
27810
|
-
for (let topic_name of Object.keys(descs)) {
|
|
27811
|
-
if (topic_name.substring(0, 2) === "__") continue;
|
|
27837
|
+
let best_score = -1;
|
|
27838
|
+
for (let topic_name of candidates) {
|
|
27812
27839
|
let reach = hook_targets(descs, topic_name);
|
|
27813
|
-
let self = reach.has(topic_name);
|
|
27814
27840
|
reach.delete(topic_name);
|
|
27815
|
-
|
|
27816
|
-
if (score === 0) continue;
|
|
27817
|
-
if (score > best_score || score === best_score && self && !best_self) {
|
|
27841
|
+
if (reach.size > best_score) {
|
|
27818
27842
|
best = topic_name;
|
|
27819
|
-
best_score =
|
|
27820
|
-
best_self = self;
|
|
27843
|
+
best_score = reach.size;
|
|
27821
27844
|
}
|
|
27822
27845
|
}
|
|
27823
27846
|
return best;
|
|
@@ -55303,13 +55326,13 @@ var NODE_MODES$1 = [
|
|
|
55303
55326
|
"shape"
|
|
55304
55327
|
];
|
|
55305
55328
|
var PILL_SIZE = {
|
|
55306
|
-
|
|
55329
|
+
entity: [200, 30],
|
|
55307
55330
|
extended: [180, 28],
|
|
55308
55331
|
child: [160, 26]
|
|
55309
55332
|
};
|
|
55310
55333
|
var PORT_R_PILL = 6;
|
|
55311
55334
|
var SHAPE_SIZE = {
|
|
55312
|
-
|
|
55335
|
+
entity: 32,
|
|
55313
55336
|
extended: 28,
|
|
55314
55337
|
child: 22
|
|
55315
55338
|
};
|
|
@@ -55397,7 +55420,7 @@ var attrs_table$4 = [
|
|
|
55397
55420
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_LIST, "layout_names", _yuneta_gobj_js.sdata_flag_t.SDF_RD, JSON.stringify(Object.keys(_layouts)), "Available layout names (read-only, for parent to query)"),
|
|
55398
55421
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook port position"),
|
|
55399
55422
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
|
|
55400
|
-
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "expand_depth", 0,
|
|
55423
|
+
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "expand_depth", 0, 1, "Levels open when a treedb loads, until the reader steps (`fold_level`): 1 = the roots alone (the default), 2 = the roots and their children. Every open hook shows its first page"),
|
|
55401
55424
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "fold_level", 0, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = its roots, N = N-1 levels under them; 0 = `expand_depth`. The host persists it and sends EV_SET_FOLD_LEVEL; it is published back in EV_LEGEND_STATE (`fold`) with the levels the tree has"),
|
|
55402
55425
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page; the `+N` chip after the page opens the next one"),
|
|
55403
55426
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_LIST, "hidden_topics", 0, "[]", "Topics left out of the tree: no card, no pill counts them, no edge reaches them"),
|
|
@@ -55489,7 +55512,7 @@ var PRIVATE_DATA$4 = {
|
|
|
55489
55512
|
_layout_asked: "",
|
|
55490
55513
|
_nodes_total: 0,
|
|
55491
55514
|
_nodes_placed: 0,
|
|
55492
|
-
expand_depth:
|
|
55515
|
+
expand_depth: 1,
|
|
55493
55516
|
fold_level: 0,
|
|
55494
55517
|
fold_page_size: 24,
|
|
55495
55518
|
hidden_topics: null,
|
|
@@ -56213,7 +56236,12 @@ function configure_behaviour(gobj) {
|
|
|
56213
56236
|
graph_write_behaviors(gobj, behaviors);
|
|
56214
56237
|
}
|
|
56215
56238
|
/************************************************************
|
|
56216
|
-
* Count hooks and fkeys in topic desc, classify node type
|
|
56239
|
+
* Count hooks and fkeys in topic desc, classify node type:
|
|
56240
|
+
* `child` (no hooks), `extended` (hooks, no fkeys), `entity`
|
|
56241
|
+
* (both). It sizes the card and nothing else. It used to be
|
|
56242
|
+
* called `hierarchical`, and that word means something else now:
|
|
56243
|
+
* a topic hooked to ITSELF, the only kind a tree can hang from
|
|
56244
|
+
* (treedb_fold_model).
|
|
56217
56245
|
************************************************************/
|
|
56218
56246
|
function calculate_hooks_fkeys_counter(desc) {
|
|
56219
56247
|
let cols = desc.cols;
|
|
@@ -56230,7 +56258,7 @@ function calculate_hooks_fkeys_counter(desc) {
|
|
|
56230
56258
|
}
|
|
56231
56259
|
if (desc.hooks_counter === 0) desc.node_treedb_type = "child";
|
|
56232
56260
|
else if (desc.fkeys_counter === 0) desc.node_treedb_type = "extended";
|
|
56233
|
-
else desc.node_treedb_type = "
|
|
56261
|
+
else desc.node_treedb_type = "entity";
|
|
56234
56262
|
}
|
|
56235
56263
|
/************************************************************
|
|
56236
56264
|
* Build ports for a topic desc
|
|
@@ -56603,7 +56631,7 @@ function dress_ports(gobj, style, ports, geometry, topic_defaults, r_default, fi
|
|
|
56603
56631
|
************************************************************/
|
|
56604
56632
|
function pill_shape_of(gobj, desc, record, geometry) {
|
|
56605
56633
|
let priv = gobj.priv;
|
|
56606
|
-
let size = PILL_SIZE[desc.node_treedb_type] || PILL_SIZE.
|
|
56634
|
+
let size = PILL_SIZE[desc.node_treedb_type] || PILL_SIZE.entity;
|
|
56607
56635
|
let ports = build_ports(gobj, desc);
|
|
56608
56636
|
let paint = node_paint_of(gobj, desc, geometry);
|
|
56609
56637
|
let style = {
|
|
@@ -56652,7 +56680,7 @@ function figure_shape_of(gobj, desc, record, geometry, flags) {
|
|
|
56652
56680
|
let dark = priv.theme === "dark";
|
|
56653
56681
|
let f = flags || {};
|
|
56654
56682
|
let tier = desc.node_treedb_type;
|
|
56655
|
-
let side = SHAPE_SIZE[tier] || SHAPE_SIZE.
|
|
56683
|
+
let side = SHAPE_SIZE[tier] || SHAPE_SIZE.entity;
|
|
56656
56684
|
let labels = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "node_labels");
|
|
56657
56685
|
let figure = node_figure_of(gobj, desc, geometry || {});
|
|
56658
56686
|
let paint = node_paint_of(gobj, desc, geometry);
|
|
@@ -59718,7 +59746,7 @@ function show_node_detail_popover(gobj, node_id) {
|
|
|
59718
59746
|
clamp_popover_position(gobj, popover);
|
|
59719
59747
|
}
|
|
59720
59748
|
/************************************************************
|
|
59721
|
-
* Re-render every HTML (
|
|
59749
|
+
* Re-render every HTML (entity) node so its card adopts
|
|
59722
59750
|
* the given theme: entity cards and leaf chips (baked innerHTML,
|
|
59723
59751
|
* not re-themed by setTheme()) are regenerated; structural
|
|
59724
59752
|
* junction diamonds get their neutral fill/stroke re-themed.
|
|
@@ -59837,7 +59865,7 @@ function refresh_minimap(gobj) {
|
|
|
59837
59865
|
}
|
|
59838
59866
|
/************************************************************
|
|
59839
59867
|
* The innerHTML of ONE node, in the given theme and highlight state.
|
|
59840
|
-
* The three treedb tiers (
|
|
59868
|
+
* The three treedb tiers (entity / extended / child) each have
|
|
59841
59869
|
* their own card, and both the theme refresh and the highlight need the
|
|
59842
59870
|
* same three-way choice — it lived inline in the theme refresh and is
|
|
59843
59871
|
* shared now. Returns null for a node that carries no desc.
|
|
@@ -59854,7 +59882,7 @@ function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html)
|
|
|
59854
59882
|
switch (desc.node_treedb_type) {
|
|
59855
59883
|
case "child": return build_chip_innerHTML(fill, theme, record.icon, label, record.id, highlight, selected, anchored);
|
|
59856
59884
|
case "extended": return build_node_innerHTML(fill, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored, pills_html);
|
|
59857
|
-
case "
|
|
59885
|
+
case "entity": return build_node_innerHTML(fill, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored, pills_html);
|
|
59858
59886
|
}
|
|
59859
59887
|
return null;
|
|
59860
59888
|
}
|
|
@@ -60051,7 +60079,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
|
|
|
60051
60079
|
`;
|
|
60052
60080
|
}
|
|
60053
60081
|
/************************************************************
|
|
60054
|
-
* Build innerHTML for
|
|
60082
|
+
* Build innerHTML for entity (HTML) nodes.
|
|
60055
60083
|
*
|
|
60056
60084
|
* Card style modelled on the documentation schema graphs
|
|
60057
60085
|
* (docs gen_treedb_graphs.py): rounded card, soft tint of the
|
|
@@ -61369,12 +61397,13 @@ function publish_legend_state(gobj, visible) {
|
|
|
61369
61397
|
loose: loose.length,
|
|
61370
61398
|
loose_shown: model.loose_shown.has(topic_name),
|
|
61371
61399
|
hidden: model.hidden.has(topic_name),
|
|
61372
|
-
linked: model.linked.has(topic_name)
|
|
61400
|
+
linked: model.linked.has(topic_name),
|
|
61401
|
+
hierarchical: fold_is_hierarchical(priv.descs, topic_name)
|
|
61373
61402
|
});
|
|
61374
61403
|
}
|
|
61375
61404
|
(0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_LEGEND_STATE", {
|
|
61376
61405
|
main_topic: model.main_topic,
|
|
61377
|
-
main_chosen: !!(0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "main_topic"),
|
|
61406
|
+
main_chosen: !!model.main_topic && (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "main_topic") === model.main_topic,
|
|
61378
61407
|
topics,
|
|
61379
61408
|
fold: {
|
|
61380
61409
|
topic: model.main_topic,
|
|
@@ -62950,7 +62979,7 @@ var attrs_table$3 = [
|
|
|
62950
62979
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
62951
62980
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_LIST, "operation_modes", 0, "[\"reading\", \"operation\", \"writing\", \"edition\"]", "Available **permission** or behaviour modes. These operation modes are required to be accomplish by the graph handler (G6 child). TODO permissions must match treedb permissions."),
|
|
62952
62981
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_treedb_tables", 0, false, "Include treedb tables"),
|
|
62953
|
-
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "expand_depth", 0,
|
|
62982
|
+
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "expand_depth", 0, 1, "Levels of the treedb open when it loads, until the reader steps (`fold_level`): 1 = the roots alone (the default), 2 = the roots and their children. Forwarded to the G6 child"),
|
|
62954
62983
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page in the graph; a `+N` chip opens the next page. Forwarded to the G6 child"),
|
|
62955
62984
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "minimap_min_nodes", 0, 30, "Show the minimap from this many nodes on (0 = never). Forwarded to the G6 child"),
|
|
62956
62985
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_gridline", 0, true, "Draw the grid under the graph. Forwarded to the G6 child"),
|
|
@@ -64601,6 +64630,7 @@ function refresh_legend(gobj) {
|
|
|
64601
64630
|
let topics = state.topics.slice().sort((a, b) => {
|
|
64602
64631
|
return String(a.topic).localeCompare(String(b.topic));
|
|
64603
64632
|
});
|
|
64633
|
+
let hierarchical_count = topics.filter((e) => e.hierarchical && !e.hidden).length;
|
|
64604
64634
|
for (let entry of topics) {
|
|
64605
64635
|
let topic_name = entry.topic;
|
|
64606
64636
|
let is_main = topic_name === state.main_topic;
|
|
@@ -64695,7 +64725,7 @@ function refresh_legend(gobj) {
|
|
|
64695
64725
|
"★"
|
|
64696
64726
|
]]
|
|
64697
64727
|
]);
|
|
64698
|
-
else if (!hidden) extras.push([
|
|
64728
|
+
else if (!hidden && entry.hierarchical && hierarchical_count > 1) extras.push([
|
|
64699
64729
|
"button",
|
|
64700
64730
|
{
|
|
64701
64731
|
class: "GRAPH_LEGEND_STAR button",
|
|
@@ -66044,7 +66074,7 @@ function sort_by_order(list) {
|
|
|
66044
66074
|
* schema_version, c_schema_version, system_schema_version,
|
|
66045
66075
|
* topics: [{
|
|
66046
66076
|
* id, name, order, record,
|
|
66047
|
-
* pkey, topic_version, system_topic,
|
|
66077
|
+
* pkey, topic_version, system_topic, main_topic,
|
|
66048
66078
|
* cols: [{id, name, order, record}]
|
|
66049
66079
|
* }]
|
|
66050
66080
|
* }],
|
|
@@ -66093,6 +66123,7 @@ function build_schema_model(records) {
|
|
|
66093
66123
|
tkey: record.tkey || "",
|
|
66094
66124
|
topic_version: record.topic_version,
|
|
66095
66125
|
system_topic: !!record.system_topic,
|
|
66126
|
+
main_topic: !!record.main_topic,
|
|
66096
66127
|
cols: []
|
|
66097
66128
|
};
|
|
66098
66129
|
by_topic_id[entry.id] = entry;
|
|
@@ -66389,6 +66420,12 @@ function validate_schema(treedb, options) {
|
|
|
66389
66420
|
if (is_fkey && FKEY_TYPES.indexOf(type) < 0) add("error", "fkey has a bad type", topic.name, col.name, type);
|
|
66390
66421
|
}
|
|
66391
66422
|
}
|
|
66423
|
+
let marked = treedb.topics.filter((topic) => topic.main_topic);
|
|
66424
|
+
if (marked.length > 1) add("error", "more than one main topic", marked[1].name, "", marked.map((topic) => topic.name).join(", "));
|
|
66425
|
+
for (let topic of marked) if (!topic.cols.some((col) => {
|
|
66426
|
+
let hook = col_hook(col.record);
|
|
66427
|
+
return col_flags(col.record).indexOf("hook") >= 0 && !!hook && Object.prototype.hasOwnProperty.call(hook, topic.name);
|
|
66428
|
+
})) add("error", "main topic not hooked to itself", topic.name, "", "");
|
|
66392
66429
|
let hooked = {};
|
|
66393
66430
|
for (let topic of treedb.topics) for (let col of topic.cols) {
|
|
66394
66431
|
let hook = col_hook(col.record);
|
|
@@ -66580,7 +66617,8 @@ var TOPIC_KEY_ORDER = [
|
|
|
66580
66617
|
"system_flag",
|
|
66581
66618
|
"tkey",
|
|
66582
66619
|
"topic_version",
|
|
66583
|
-
"system_topic"
|
|
66620
|
+
"system_topic",
|
|
66621
|
+
"main_topic"
|
|
66584
66622
|
];
|
|
66585
66623
|
var COL_KEY_ORDER = [
|
|
66586
66624
|
"header",
|
|
@@ -66838,7 +66876,8 @@ var TOPIC_FIELDS = [
|
|
|
66838
66876
|
"system_flag",
|
|
66839
66877
|
"tkey",
|
|
66840
66878
|
"topic_version",
|
|
66841
|
-
"system_topic"
|
|
66879
|
+
"system_topic",
|
|
66880
|
+
"main_topic"
|
|
66842
66881
|
];
|
|
66843
66882
|
var COL_FIELDS = [
|
|
66844
66883
|
"header",
|
|
@@ -68455,6 +68494,20 @@ function render_topics(gobj, $body) {
|
|
|
68455
68494
|
{},
|
|
68456
68495
|
""
|
|
68457
68496
|
],
|
|
68497
|
+
topic.main_topic ? [
|
|
68498
|
+
"span",
|
|
68499
|
+
{
|
|
68500
|
+
class: "SCHEMA_TOPIC_MAIN ml-2",
|
|
68501
|
+
style: "color:#d4a017;",
|
|
68502
|
+
title: (0, i18next.t)("main topic"),
|
|
68503
|
+
"data-i18n-title": "main topic"
|
|
68504
|
+
},
|
|
68505
|
+
"★"
|
|
68506
|
+
] : [
|
|
68507
|
+
"span",
|
|
68508
|
+
{},
|
|
68509
|
+
""
|
|
68510
|
+
],
|
|
68458
68511
|
ambiguous ? [
|
|
68459
68512
|
"div",
|
|
68460
68513
|
{ class: "SCHEMA_TOPIC_ID is-size-7 yui-text-quiet" },
|
|
@@ -69449,6 +69502,26 @@ function open_topic_form(gobj, treedb, topic) {
|
|
|
69449
69502
|
]]
|
|
69450
69503
|
]]
|
|
69451
69504
|
],
|
|
69505
|
+
[
|
|
69506
|
+
"div",
|
|
69507
|
+
{ class: "SCHEMA_TOPIC_FORM_MAIN field mb-3" },
|
|
69508
|
+
[[
|
|
69509
|
+
"label",
|
|
69510
|
+
{ class: "checkbox" },
|
|
69511
|
+
[["input", el_attrs({
|
|
69512
|
+
type: "checkbox",
|
|
69513
|
+
"data-name": "main_topic",
|
|
69514
|
+
checked: record.main_topic ? "checked" : void 0
|
|
69515
|
+
})], [
|
|
69516
|
+
"span",
|
|
69517
|
+
{
|
|
69518
|
+
class: "ml-1",
|
|
69519
|
+
i18n: "main topic"
|
|
69520
|
+
},
|
|
69521
|
+
(0, i18next.t)("main topic")
|
|
69522
|
+
]]
|
|
69523
|
+
]]
|
|
69524
|
+
],
|
|
69452
69525
|
[
|
|
69453
69526
|
"div",
|
|
69454
69527
|
{
|
|
@@ -70241,6 +70314,7 @@ function topic_record(gobj, treedb, topic, values) {
|
|
|
70241
70314
|
record.tkey = values.tkey || "";
|
|
70242
70315
|
record.topic_version = values.topic_version === "" ? 1 : Number(values.topic_version);
|
|
70243
70316
|
record.system_topic = !!values.system_topic;
|
|
70317
|
+
record.main_topic = !!values.main_topic;
|
|
70244
70318
|
return record;
|
|
70245
70319
|
}
|
|
70246
70320
|
/***************************
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -27670,7 +27670,9 @@ function fold_build_model(descs, records, opts) {
|
|
|
27670
27670
|
opts = is_object(opts) ? opts : {};
|
|
27671
27671
|
let hidden = new Set(is_array(opts.hidden_topics) ? opts.hidden_topics : []);
|
|
27672
27672
|
let loose_shown = new Set(is_array(opts.loose_topics) ? opts.loose_topics : []);
|
|
27673
|
-
let main_topic = opts.main_topic ||
|
|
27673
|
+
let main_topic = opts.main_topic || "";
|
|
27674
|
+
if (main_topic && !fold_is_hierarchical(descs, main_topic)) main_topic = "";
|
|
27675
|
+
if (!main_topic) main_topic = fold_main_topic(descs);
|
|
27674
27676
|
if (main_topic && (!descs[main_topic] || hidden.has(main_topic))) main_topic = "";
|
|
27675
27677
|
let linked = fold_linked_topics(descs, main_topic);
|
|
27676
27678
|
let totals = {};
|
|
@@ -27791,28 +27793,49 @@ function count_levels(model) {
|
|
|
27791
27793
|
model.levels = deepest;
|
|
27792
27794
|
}
|
|
27793
27795
|
/************************************************************
|
|
27794
|
-
*
|
|
27795
|
-
*
|
|
27796
|
-
*
|
|
27797
|
-
*
|
|
27798
|
-
*
|
|
27796
|
+
* A HIERARCHICAL topic: one hooked to ITSELF -- places inside
|
|
27797
|
+
* places, groups inside groups, roles inside roles. It is the
|
|
27798
|
+
* only kind of topic a tree can hang from: a topic that only
|
|
27799
|
+
* reaches OTHER topics is a list with children, and making it
|
|
27800
|
+
* the trunk drew the agent's treedb from `yunos` instead of
|
|
27801
|
+
* `realms`.
|
|
27802
|
+
*
|
|
27803
|
+
* (Not the engine's `entity` tier, which is any topic with
|
|
27804
|
+
* hooks AND fkeys: that one sizes a card and says nothing about
|
|
27805
|
+
* where a tree starts.)
|
|
27806
|
+
************************************************************/
|
|
27807
|
+
function fold_is_hierarchical(descs, topic_name) {
|
|
27808
|
+
if (!is_object(descs) || !descs[topic_name]) return false;
|
|
27809
|
+
return hook_targets(descs, topic_name).has(topic_name);
|
|
27810
|
+
}
|
|
27811
|
+
/************************************************************
|
|
27812
|
+
* The topic the tree hangs from. Only a HIERARCHICAL topic can
|
|
27813
|
+
* be it, and in this order:
|
|
27814
|
+
*
|
|
27815
|
+
* the one the schema MARKS (`main_topic: true` on the topic,
|
|
27816
|
+
* sent by the backend in its desc since SDK 7.19; the first
|
|
27817
|
+
* one in schema order if a schema marks two);
|
|
27818
|
+
* else the one whose hooks reach the most OTHER topics,
|
|
27819
|
+
* schema order breaking a tie.
|
|
27820
|
+
*
|
|
27821
|
+
* "" when no topic is hooked to itself: there is no trunk, and
|
|
27822
|
+
* every topic is a tree of its own. (A pick by the reader comes
|
|
27823
|
+
* first of all, and is fold_build_model's business.)
|
|
27799
27824
|
************************************************************/
|
|
27800
27825
|
function fold_main_topic(descs) {
|
|
27801
27826
|
descs = is_object(descs) ? descs : {};
|
|
27827
|
+
let candidates = Object.keys(descs).filter((topic_name) => {
|
|
27828
|
+
return topic_name.substring(0, 2) !== "__" && fold_is_hierarchical(descs, topic_name);
|
|
27829
|
+
});
|
|
27830
|
+
for (let topic_name of candidates) if (descs[topic_name].main_topic === true) return topic_name;
|
|
27802
27831
|
let best = "";
|
|
27803
|
-
let best_score =
|
|
27804
|
-
let
|
|
27805
|
-
for (let topic_name of Object.keys(descs)) {
|
|
27806
|
-
if (topic_name.substring(0, 2) === "__") continue;
|
|
27832
|
+
let best_score = -1;
|
|
27833
|
+
for (let topic_name of candidates) {
|
|
27807
27834
|
let reach = hook_targets(descs, topic_name);
|
|
27808
|
-
let self = reach.has(topic_name);
|
|
27809
27835
|
reach.delete(topic_name);
|
|
27810
|
-
|
|
27811
|
-
if (score === 0) continue;
|
|
27812
|
-
if (score > best_score || score === best_score && self && !best_self) {
|
|
27836
|
+
if (reach.size > best_score) {
|
|
27813
27837
|
best = topic_name;
|
|
27814
|
-
best_score =
|
|
27815
|
-
best_self = self;
|
|
27838
|
+
best_score = reach.size;
|
|
27816
27839
|
}
|
|
27817
27840
|
}
|
|
27818
27841
|
return best;
|
|
@@ -55298,13 +55321,13 @@ var NODE_MODES$1 = [
|
|
|
55298
55321
|
"shape"
|
|
55299
55322
|
];
|
|
55300
55323
|
var PILL_SIZE = {
|
|
55301
|
-
|
|
55324
|
+
entity: [200, 30],
|
|
55302
55325
|
extended: [180, 28],
|
|
55303
55326
|
child: [160, 26]
|
|
55304
55327
|
};
|
|
55305
55328
|
var PORT_R_PILL = 6;
|
|
55306
55329
|
var SHAPE_SIZE = {
|
|
55307
|
-
|
|
55330
|
+
entity: 32,
|
|
55308
55331
|
extended: 28,
|
|
55309
55332
|
child: 22
|
|
55310
55333
|
};
|
|
@@ -55392,7 +55415,7 @@ var attrs_table$4 = [
|
|
|
55392
55415
|
SDATA(data_type_t.DTP_LIST, "layout_names", sdata_flag_t.SDF_RD, JSON.stringify(Object.keys(_layouts)), "Available layout names (read-only, for parent to query)"),
|
|
55393
55416
|
SDATA(data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook port position"),
|
|
55394
55417
|
SDATA(data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
|
|
55395
|
-
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0,
|
|
55418
|
+
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 1, "Levels open when a treedb loads, until the reader steps (`fold_level`): 1 = the roots alone (the default), 2 = the roots and their children. Every open hook shows its first page"),
|
|
55396
55419
|
SDATA(data_type_t.DTP_INTEGER, "fold_level", 0, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = its roots, N = N-1 levels under them; 0 = `expand_depth`. The host persists it and sends EV_SET_FOLD_LEVEL; it is published back in EV_LEGEND_STATE (`fold`) with the levels the tree has"),
|
|
55397
55420
|
SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page; the `+N` chip after the page opens the next one"),
|
|
55398
55421
|
SDATA(data_type_t.DTP_LIST, "hidden_topics", 0, "[]", "Topics left out of the tree: no card, no pill counts them, no edge reaches them"),
|
|
@@ -55484,7 +55507,7 @@ var PRIVATE_DATA$4 = {
|
|
|
55484
55507
|
_layout_asked: "",
|
|
55485
55508
|
_nodes_total: 0,
|
|
55486
55509
|
_nodes_placed: 0,
|
|
55487
|
-
expand_depth:
|
|
55510
|
+
expand_depth: 1,
|
|
55488
55511
|
fold_level: 0,
|
|
55489
55512
|
fold_page_size: 24,
|
|
55490
55513
|
hidden_topics: null,
|
|
@@ -56208,7 +56231,12 @@ function configure_behaviour(gobj) {
|
|
|
56208
56231
|
graph_write_behaviors(gobj, behaviors);
|
|
56209
56232
|
}
|
|
56210
56233
|
/************************************************************
|
|
56211
|
-
* Count hooks and fkeys in topic desc, classify node type
|
|
56234
|
+
* Count hooks and fkeys in topic desc, classify node type:
|
|
56235
|
+
* `child` (no hooks), `extended` (hooks, no fkeys), `entity`
|
|
56236
|
+
* (both). It sizes the card and nothing else. It used to be
|
|
56237
|
+
* called `hierarchical`, and that word means something else now:
|
|
56238
|
+
* a topic hooked to ITSELF, the only kind a tree can hang from
|
|
56239
|
+
* (treedb_fold_model).
|
|
56212
56240
|
************************************************************/
|
|
56213
56241
|
function calculate_hooks_fkeys_counter(desc) {
|
|
56214
56242
|
let cols = desc.cols;
|
|
@@ -56225,7 +56253,7 @@ function calculate_hooks_fkeys_counter(desc) {
|
|
|
56225
56253
|
}
|
|
56226
56254
|
if (desc.hooks_counter === 0) desc.node_treedb_type = "child";
|
|
56227
56255
|
else if (desc.fkeys_counter === 0) desc.node_treedb_type = "extended";
|
|
56228
|
-
else desc.node_treedb_type = "
|
|
56256
|
+
else desc.node_treedb_type = "entity";
|
|
56229
56257
|
}
|
|
56230
56258
|
/************************************************************
|
|
56231
56259
|
* Build ports for a topic desc
|
|
@@ -56598,7 +56626,7 @@ function dress_ports(gobj, style, ports, geometry, topic_defaults, r_default, fi
|
|
|
56598
56626
|
************************************************************/
|
|
56599
56627
|
function pill_shape_of(gobj, desc, record, geometry) {
|
|
56600
56628
|
let priv = gobj.priv;
|
|
56601
|
-
let size = PILL_SIZE[desc.node_treedb_type] || PILL_SIZE.
|
|
56629
|
+
let size = PILL_SIZE[desc.node_treedb_type] || PILL_SIZE.entity;
|
|
56602
56630
|
let ports = build_ports(gobj, desc);
|
|
56603
56631
|
let paint = node_paint_of(gobj, desc, geometry);
|
|
56604
56632
|
let style = {
|
|
@@ -56647,7 +56675,7 @@ function figure_shape_of(gobj, desc, record, geometry, flags) {
|
|
|
56647
56675
|
let dark = priv.theme === "dark";
|
|
56648
56676
|
let f = flags || {};
|
|
56649
56677
|
let tier = desc.node_treedb_type;
|
|
56650
|
-
let side = SHAPE_SIZE[tier] || SHAPE_SIZE.
|
|
56678
|
+
let side = SHAPE_SIZE[tier] || SHAPE_SIZE.entity;
|
|
56651
56679
|
let labels = gobj_read_bool_attr(gobj, "node_labels");
|
|
56652
56680
|
let figure = node_figure_of(gobj, desc, geometry || {});
|
|
56653
56681
|
let paint = node_paint_of(gobj, desc, geometry);
|
|
@@ -59713,7 +59741,7 @@ function show_node_detail_popover(gobj, node_id) {
|
|
|
59713
59741
|
clamp_popover_position(gobj, popover);
|
|
59714
59742
|
}
|
|
59715
59743
|
/************************************************************
|
|
59716
|
-
* Re-render every HTML (
|
|
59744
|
+
* Re-render every HTML (entity) node so its card adopts
|
|
59717
59745
|
* the given theme: entity cards and leaf chips (baked innerHTML,
|
|
59718
59746
|
* not re-themed by setTheme()) are regenerated; structural
|
|
59719
59747
|
* junction diamonds get their neutral fill/stroke re-themed.
|
|
@@ -59832,7 +59860,7 @@ function refresh_minimap(gobj) {
|
|
|
59832
59860
|
}
|
|
59833
59861
|
/************************************************************
|
|
59834
59862
|
* The innerHTML of ONE node, in the given theme and highlight state.
|
|
59835
|
-
* The three treedb tiers (
|
|
59863
|
+
* The three treedb tiers (entity / extended / child) each have
|
|
59836
59864
|
* their own card, and both the theme refresh and the highlight need the
|
|
59837
59865
|
* same three-way choice — it lived inline in the theme refresh and is
|
|
59838
59866
|
* shared now. Returns null for a node that carries no desc.
|
|
@@ -59849,7 +59877,7 @@ function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html)
|
|
|
59849
59877
|
switch (desc.node_treedb_type) {
|
|
59850
59878
|
case "child": return build_chip_innerHTML(fill, theme, record.icon, label, record.id, highlight, selected, anchored);
|
|
59851
59879
|
case "extended": return build_node_innerHTML(fill, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored, pills_html);
|
|
59852
|
-
case "
|
|
59880
|
+
case "entity": return build_node_innerHTML(fill, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored, pills_html);
|
|
59853
59881
|
}
|
|
59854
59882
|
return null;
|
|
59855
59883
|
}
|
|
@@ -60046,7 +60074,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
|
|
|
60046
60074
|
`;
|
|
60047
60075
|
}
|
|
60048
60076
|
/************************************************************
|
|
60049
|
-
* Build innerHTML for
|
|
60077
|
+
* Build innerHTML for entity (HTML) nodes.
|
|
60050
60078
|
*
|
|
60051
60079
|
* Card style modelled on the documentation schema graphs
|
|
60052
60080
|
* (docs gen_treedb_graphs.py): rounded card, soft tint of the
|
|
@@ -61364,12 +61392,13 @@ function publish_legend_state(gobj, visible) {
|
|
|
61364
61392
|
loose: loose.length,
|
|
61365
61393
|
loose_shown: model.loose_shown.has(topic_name),
|
|
61366
61394
|
hidden: model.hidden.has(topic_name),
|
|
61367
|
-
linked: model.linked.has(topic_name)
|
|
61395
|
+
linked: model.linked.has(topic_name),
|
|
61396
|
+
hierarchical: fold_is_hierarchical(priv.descs, topic_name)
|
|
61368
61397
|
});
|
|
61369
61398
|
}
|
|
61370
61399
|
gobj_publish_event(gobj, "EV_LEGEND_STATE", {
|
|
61371
61400
|
main_topic: model.main_topic,
|
|
61372
|
-
main_chosen: !!gobj_read_str_attr(gobj, "main_topic"),
|
|
61401
|
+
main_chosen: !!model.main_topic && gobj_read_str_attr(gobj, "main_topic") === model.main_topic,
|
|
61373
61402
|
topics,
|
|
61374
61403
|
fold: {
|
|
61375
61404
|
topic: model.main_topic,
|
|
@@ -62945,7 +62974,7 @@ var attrs_table$3 = [
|
|
|
62945
62974
|
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
62946
62975
|
SDATA(data_type_t.DTP_LIST, "operation_modes", 0, "[\"reading\", \"operation\", \"writing\", \"edition\"]", "Available **permission** or behaviour modes. These operation modes are required to be accomplish by the graph handler (G6 child). TODO permissions must match treedb permissions."),
|
|
62947
62976
|
SDATA(data_type_t.DTP_BOOLEAN, "with_treedb_tables", 0, false, "Include treedb tables"),
|
|
62948
|
-
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0,
|
|
62977
|
+
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 1, "Levels of the treedb open when it loads, until the reader steps (`fold_level`): 1 = the roots alone (the default), 2 = the roots and their children. Forwarded to the G6 child"),
|
|
62949
62978
|
SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page in the graph; a `+N` chip opens the next page. Forwarded to the G6 child"),
|
|
62950
62979
|
SDATA(data_type_t.DTP_INTEGER, "minimap_min_nodes", 0, 30, "Show the minimap from this many nodes on (0 = never). Forwarded to the G6 child"),
|
|
62951
62980
|
SDATA(data_type_t.DTP_BOOLEAN, "with_gridline", 0, true, "Draw the grid under the graph. Forwarded to the G6 child"),
|
|
@@ -64596,6 +64625,7 @@ function refresh_legend(gobj) {
|
|
|
64596
64625
|
let topics = state.topics.slice().sort((a, b) => {
|
|
64597
64626
|
return String(a.topic).localeCompare(String(b.topic));
|
|
64598
64627
|
});
|
|
64628
|
+
let hierarchical_count = topics.filter((e) => e.hierarchical && !e.hidden).length;
|
|
64599
64629
|
for (let entry of topics) {
|
|
64600
64630
|
let topic_name = entry.topic;
|
|
64601
64631
|
let is_main = topic_name === state.main_topic;
|
|
@@ -64690,7 +64720,7 @@ function refresh_legend(gobj) {
|
|
|
64690
64720
|
"★"
|
|
64691
64721
|
]]
|
|
64692
64722
|
]);
|
|
64693
|
-
else if (!hidden) extras.push([
|
|
64723
|
+
else if (!hidden && entry.hierarchical && hierarchical_count > 1) extras.push([
|
|
64694
64724
|
"button",
|
|
64695
64725
|
{
|
|
64696
64726
|
class: "GRAPH_LEGEND_STAR button",
|
|
@@ -66039,7 +66069,7 @@ function sort_by_order(list) {
|
|
|
66039
66069
|
* schema_version, c_schema_version, system_schema_version,
|
|
66040
66070
|
* topics: [{
|
|
66041
66071
|
* id, name, order, record,
|
|
66042
|
-
* pkey, topic_version, system_topic,
|
|
66072
|
+
* pkey, topic_version, system_topic, main_topic,
|
|
66043
66073
|
* cols: [{id, name, order, record}]
|
|
66044
66074
|
* }]
|
|
66045
66075
|
* }],
|
|
@@ -66088,6 +66118,7 @@ function build_schema_model(records) {
|
|
|
66088
66118
|
tkey: record.tkey || "",
|
|
66089
66119
|
topic_version: record.topic_version,
|
|
66090
66120
|
system_topic: !!record.system_topic,
|
|
66121
|
+
main_topic: !!record.main_topic,
|
|
66091
66122
|
cols: []
|
|
66092
66123
|
};
|
|
66093
66124
|
by_topic_id[entry.id] = entry;
|
|
@@ -66384,6 +66415,12 @@ function validate_schema(treedb, options) {
|
|
|
66384
66415
|
if (is_fkey && FKEY_TYPES.indexOf(type) < 0) add("error", "fkey has a bad type", topic.name, col.name, type);
|
|
66385
66416
|
}
|
|
66386
66417
|
}
|
|
66418
|
+
let marked = treedb.topics.filter((topic) => topic.main_topic);
|
|
66419
|
+
if (marked.length > 1) add("error", "more than one main topic", marked[1].name, "", marked.map((topic) => topic.name).join(", "));
|
|
66420
|
+
for (let topic of marked) if (!topic.cols.some((col) => {
|
|
66421
|
+
let hook = col_hook(col.record);
|
|
66422
|
+
return col_flags(col.record).indexOf("hook") >= 0 && !!hook && Object.prototype.hasOwnProperty.call(hook, topic.name);
|
|
66423
|
+
})) add("error", "main topic not hooked to itself", topic.name, "", "");
|
|
66387
66424
|
let hooked = {};
|
|
66388
66425
|
for (let topic of treedb.topics) for (let col of topic.cols) {
|
|
66389
66426
|
let hook = col_hook(col.record);
|
|
@@ -66575,7 +66612,8 @@ var TOPIC_KEY_ORDER = [
|
|
|
66575
66612
|
"system_flag",
|
|
66576
66613
|
"tkey",
|
|
66577
66614
|
"topic_version",
|
|
66578
|
-
"system_topic"
|
|
66615
|
+
"system_topic",
|
|
66616
|
+
"main_topic"
|
|
66579
66617
|
];
|
|
66580
66618
|
var COL_KEY_ORDER = [
|
|
66581
66619
|
"header",
|
|
@@ -66833,7 +66871,8 @@ var TOPIC_FIELDS = [
|
|
|
66833
66871
|
"system_flag",
|
|
66834
66872
|
"tkey",
|
|
66835
66873
|
"topic_version",
|
|
66836
|
-
"system_topic"
|
|
66874
|
+
"system_topic",
|
|
66875
|
+
"main_topic"
|
|
66837
66876
|
];
|
|
66838
66877
|
var COL_FIELDS = [
|
|
66839
66878
|
"header",
|
|
@@ -68450,6 +68489,20 @@ function render_topics(gobj, $body) {
|
|
|
68450
68489
|
{},
|
|
68451
68490
|
""
|
|
68452
68491
|
],
|
|
68492
|
+
topic.main_topic ? [
|
|
68493
|
+
"span",
|
|
68494
|
+
{
|
|
68495
|
+
class: "SCHEMA_TOPIC_MAIN ml-2",
|
|
68496
|
+
style: "color:#d4a017;",
|
|
68497
|
+
title: t("main topic"),
|
|
68498
|
+
"data-i18n-title": "main topic"
|
|
68499
|
+
},
|
|
68500
|
+
"★"
|
|
68501
|
+
] : [
|
|
68502
|
+
"span",
|
|
68503
|
+
{},
|
|
68504
|
+
""
|
|
68505
|
+
],
|
|
68453
68506
|
ambiguous ? [
|
|
68454
68507
|
"div",
|
|
68455
68508
|
{ class: "SCHEMA_TOPIC_ID is-size-7 yui-text-quiet" },
|
|
@@ -69444,6 +69497,26 @@ function open_topic_form(gobj, treedb, topic) {
|
|
|
69444
69497
|
]]
|
|
69445
69498
|
]]
|
|
69446
69499
|
],
|
|
69500
|
+
[
|
|
69501
|
+
"div",
|
|
69502
|
+
{ class: "SCHEMA_TOPIC_FORM_MAIN field mb-3" },
|
|
69503
|
+
[[
|
|
69504
|
+
"label",
|
|
69505
|
+
{ class: "checkbox" },
|
|
69506
|
+
[["input", el_attrs({
|
|
69507
|
+
type: "checkbox",
|
|
69508
|
+
"data-name": "main_topic",
|
|
69509
|
+
checked: record.main_topic ? "checked" : void 0
|
|
69510
|
+
})], [
|
|
69511
|
+
"span",
|
|
69512
|
+
{
|
|
69513
|
+
class: "ml-1",
|
|
69514
|
+
i18n: "main topic"
|
|
69515
|
+
},
|
|
69516
|
+
t("main topic")
|
|
69517
|
+
]]
|
|
69518
|
+
]]
|
|
69519
|
+
],
|
|
69447
69520
|
[
|
|
69448
69521
|
"div",
|
|
69449
69522
|
{
|
|
@@ -70236,6 +70309,7 @@ function topic_record(gobj, treedb, topic, values) {
|
|
|
70236
70309
|
record.tkey = values.tkey || "";
|
|
70237
70310
|
record.topic_version = values.topic_version === "" ? 1 : Number(values.topic_version);
|
|
70238
70311
|
record.system_topic = !!values.system_topic;
|
|
70312
|
+
record.main_topic = !!values.main_topic;
|
|
70239
70313
|
return record;
|
|
70240
70314
|
}
|
|
70241
70315
|
/***************************
|
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -122,6 +122,7 @@ import {
|
|
|
122
122
|
fold_open_levels,
|
|
123
123
|
fold_close_levels,
|
|
124
124
|
fold_level_clamp,
|
|
125
|
+
fold_is_hierarchical,
|
|
125
126
|
fold_toggle,
|
|
126
127
|
fold_show_more,
|
|
127
128
|
fold_reveal,
|
|
@@ -301,14 +302,14 @@ const PORT_LINE_WIDTH = 2;
|
|
|
301
302
|
const NODE_MODES = ['expanded', 'compact', 'shape'];
|
|
302
303
|
|
|
303
304
|
const PILL_SIZE = {
|
|
304
|
-
|
|
305
|
+
entity: [200, 30],
|
|
305
306
|
extended: [180, 28],
|
|
306
307
|
child: [160, 26],
|
|
307
308
|
};
|
|
308
309
|
const PORT_R_PILL = 6;
|
|
309
310
|
|
|
310
311
|
const SHAPE_SIZE = {
|
|
311
|
-
|
|
312
|
+
entity: 32,
|
|
312
313
|
extended: 28,
|
|
313
314
|
child: 22,
|
|
314
315
|
};
|
|
@@ -436,7 +437,7 @@ SDATA(data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook po
|
|
|
436
437
|
SDATA(data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
|
|
437
438
|
|
|
438
439
|
/*---------------- Folding ----------------*/
|
|
439
|
-
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0,
|
|
440
|
+
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 1, "Levels open when a treedb loads, until the reader steps (`fold_level`): 1 = the roots alone (the default), 2 = the roots and their children. Every open hook shows its first page"),
|
|
440
441
|
SDATA(data_type_t.DTP_INTEGER, "fold_level", 0, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = its roots, N = N-1 levels under them; 0 = `expand_depth`. The host persists it and sends EV_SET_FOLD_LEVEL; it is published back in EV_LEGEND_STATE (`fold`) with the levels the tree has"),
|
|
441
442
|
SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page; the `+N` chip after the page opens the next one"),
|
|
442
443
|
SDATA(data_type_t.DTP_LIST, "hidden_topics", 0, "[]", "Topics left out of the tree: no card, no pill counts them, no edge reaches them"),
|
|
@@ -545,7 +546,7 @@ let PRIVATE_DATA = {
|
|
|
545
546
|
_nodes_placed: 0, // ...of which carry a saved position
|
|
546
547
|
|
|
547
548
|
/*---------------- folding ----------------*/
|
|
548
|
-
expand_depth:
|
|
549
|
+
expand_depth: 1,
|
|
549
550
|
fold_level: 0,
|
|
550
551
|
fold_page_size: 24,
|
|
551
552
|
hidden_topics: null,
|
|
@@ -1859,7 +1860,12 @@ function configure_behaviour(gobj)
|
|
|
1859
1860
|
}
|
|
1860
1861
|
|
|
1861
1862
|
/************************************************************
|
|
1862
|
-
* Count hooks and fkeys in topic desc, classify node type
|
|
1863
|
+
* Count hooks and fkeys in topic desc, classify node type:
|
|
1864
|
+
* `child` (no hooks), `extended` (hooks, no fkeys), `entity`
|
|
1865
|
+
* (both). It sizes the card and nothing else. It used to be
|
|
1866
|
+
* called `hierarchical`, and that word means something else now:
|
|
1867
|
+
* a topic hooked to ITSELF, the only kind a tree can hang from
|
|
1868
|
+
* (treedb_fold_model).
|
|
1863
1869
|
************************************************************/
|
|
1864
1870
|
function calculate_hooks_fkeys_counter(desc)
|
|
1865
1871
|
{
|
|
@@ -1885,7 +1891,7 @@ function calculate_hooks_fkeys_counter(desc)
|
|
|
1885
1891
|
} else if(desc.fkeys_counter === 0) {
|
|
1886
1892
|
desc.node_treedb_type = 'extended';
|
|
1887
1893
|
} else {
|
|
1888
|
-
desc.node_treedb_type = '
|
|
1894
|
+
desc.node_treedb_type = 'entity';
|
|
1889
1895
|
}
|
|
1890
1896
|
}
|
|
1891
1897
|
|
|
@@ -2306,7 +2312,7 @@ function card_shape_of(gobj, desc, record, geometry, pills_html)
|
|
|
2306
2312
|
desc.topic_name, true, record.id, false, false, false, pills_html
|
|
2307
2313
|
);
|
|
2308
2314
|
} else {
|
|
2309
|
-
//
|
|
2315
|
+
// Entity, hooks and fkeys (ROOT / container): largest tier.
|
|
2310
2316
|
style.innerHTML = build_node_innerHTML(
|
|
2311
2317
|
paint.fill, priv.theme, record.icon, label,
|
|
2312
2318
|
desc.topic_name, false, record.id, false, false, false, pills_html
|
|
@@ -2409,7 +2415,7 @@ function pill_shape_of(gobj, desc, record, geometry)
|
|
|
2409
2415
|
{
|
|
2410
2416
|
let priv = gobj.priv;
|
|
2411
2417
|
let tier = desc.node_treedb_type;
|
|
2412
|
-
let size = PILL_SIZE[tier] || PILL_SIZE.
|
|
2418
|
+
let size = PILL_SIZE[tier] || PILL_SIZE.entity;
|
|
2413
2419
|
let ports = build_ports(gobj, desc);
|
|
2414
2420
|
let paint = node_paint_of(gobj, desc, geometry);
|
|
2415
2421
|
|
|
@@ -2472,7 +2478,7 @@ function figure_shape_of(gobj, desc, record, geometry, flags)
|
|
|
2472
2478
|
let dark = (priv.theme === "dark");
|
|
2473
2479
|
let f = flags || {};
|
|
2474
2480
|
let tier = desc.node_treedb_type;
|
|
2475
|
-
let side = SHAPE_SIZE[tier] || SHAPE_SIZE.
|
|
2481
|
+
let side = SHAPE_SIZE[tier] || SHAPE_SIZE.entity;
|
|
2476
2482
|
let labels = gobj_read_bool_attr(gobj, "node_labels");
|
|
2477
2483
|
let figure = node_figure_of(gobj, desc, geometry || {});
|
|
2478
2484
|
let paint = node_paint_of(gobj, desc, geometry);
|
|
@@ -7042,7 +7048,7 @@ function show_node_detail_popover(gobj, node_id)
|
|
|
7042
7048
|
}
|
|
7043
7049
|
|
|
7044
7050
|
/************************************************************
|
|
7045
|
-
* Re-render every HTML (
|
|
7051
|
+
* Re-render every HTML (entity) node so its card adopts
|
|
7046
7052
|
* the given theme: entity cards and leaf chips (baked innerHTML,
|
|
7047
7053
|
* not re-themed by setTheme()) are regenerated; structural
|
|
7048
7054
|
* junction diamonds get their neutral fill/stroke re-themed.
|
|
@@ -7258,7 +7264,7 @@ function refresh_minimap(gobj)
|
|
|
7258
7264
|
|
|
7259
7265
|
/************************************************************
|
|
7260
7266
|
* The innerHTML of ONE node, in the given theme and highlight state.
|
|
7261
|
-
* The three treedb tiers (
|
|
7267
|
+
* The three treedb tiers (entity / extended / child) each have
|
|
7262
7268
|
* their own card, and both the theme refresh and the highlight need the
|
|
7263
7269
|
* same three-way choice — it lived inline in the theme refresh and is
|
|
7264
7270
|
* shared now. Returns null for a node that carries no desc.
|
|
@@ -7295,7 +7301,7 @@ function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html)
|
|
|
7295
7301
|
desc.topic_name, true, record.id, highlight, selected, anchored,
|
|
7296
7302
|
pills_html
|
|
7297
7303
|
);
|
|
7298
|
-
case '
|
|
7304
|
+
case 'entity':
|
|
7299
7305
|
return build_node_innerHTML(
|
|
7300
7306
|
fill, theme, record.icon, label,
|
|
7301
7307
|
desc.topic_name, false, record.id, highlight, selected, anchored,
|
|
@@ -7569,7 +7575,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
|
|
|
7569
7575
|
}
|
|
7570
7576
|
|
|
7571
7577
|
/************************************************************
|
|
7572
|
-
* Build innerHTML for
|
|
7578
|
+
* Build innerHTML for entity (HTML) nodes.
|
|
7573
7579
|
*
|
|
7574
7580
|
* Card style modelled on the documentation schema graphs
|
|
7575
7581
|
* (docs gen_treedb_graphs.py): rounded card, soft tint of the
|
|
@@ -9538,12 +9544,16 @@ function publish_legend_state(gobj, visible)
|
|
|
9538
9544
|
loose_shown: model.loose_shown.has(topic_name),
|
|
9539
9545
|
hidden: model.hidden.has(topic_name),
|
|
9540
9546
|
linked: model.linked.has(topic_name),
|
|
9547
|
+
hierarchical: fold_is_hierarchical(priv.descs, topic_name),
|
|
9541
9548
|
});
|
|
9542
9549
|
}
|
|
9543
9550
|
|
|
9544
9551
|
gobj_publish_event(gobj, "EV_LEGEND_STATE", {
|
|
9545
9552
|
main_topic: model.main_topic,
|
|
9546
|
-
|
|
9553
|
+
/* CHOSEN only when the reader's pick is the one in force: a
|
|
9554
|
+
* pick the rule refuses (not hierarchical) is ignored. */
|
|
9555
|
+
main_chosen: !!model.main_topic &&
|
|
9556
|
+
gobj_read_str_attr(gobj, "main_topic") === model.main_topic,
|
|
9547
9557
|
topics: topics,
|
|
9548
9558
|
/* The toolbar's stepper: the tree it walks, where it stands,
|
|
9549
9559
|
* and how deep it goes. `levels` 0 = nothing to step through. */
|
|
@@ -1061,6 +1061,13 @@ function render_topics(gobj, $body)
|
|
|
1061
1061
|
? ["span", {class: "SCHEMA_TOPIC_SYSTEM tag ml-2",
|
|
1062
1062
|
i18n: "system"}, t("system")]
|
|
1063
1063
|
: ["span", {}, ""],
|
|
1064
|
+
/* The schema's mark of the topic the tree hangs from: the
|
|
1065
|
+
* same gold star the treedb graph's legend wears. */
|
|
1066
|
+
topic.main_topic
|
|
1067
|
+
? ["span", {class: "SCHEMA_TOPIC_MAIN ml-2",
|
|
1068
|
+
style: "color:#d4a017;",
|
|
1069
|
+
title: t("main topic"), "data-i18n-title": "main topic"}, "★"]
|
|
1070
|
+
: ["span", {}, ""],
|
|
1064
1071
|
ambiguous
|
|
1065
1072
|
? ["div", {class: "SCHEMA_TOPIC_ID is-size-7 yui-text-quiet"},
|
|
1066
1073
|
[["code", {}, `${topic.id}`]]]
|
|
@@ -1898,6 +1905,14 @@ function open_topic_form(gobj, treedb, topic)
|
|
|
1898
1905
|
["span", {class: "ml-1", i18n: "system topic"}, t("system topic")]
|
|
1899
1906
|
]]
|
|
1900
1907
|
]],
|
|
1908
|
+
/* A LABEL that wraps its control names it: no aria-label. */
|
|
1909
|
+
["div", {class: "SCHEMA_TOPIC_FORM_MAIN field mb-3"}, [
|
|
1910
|
+
["label", {class: "checkbox"}, [
|
|
1911
|
+
["input", el_attrs({type: "checkbox", "data-name": "main_topic",
|
|
1912
|
+
checked: record.main_topic ? "checked" : undefined})],
|
|
1913
|
+
["span", {class: "ml-1", i18n: "main topic"}, t("main topic")]
|
|
1914
|
+
]]
|
|
1915
|
+
]],
|
|
1901
1916
|
["div", {class: "SCHEMA_TOPIC_FORM_ACTIONS is-flex mt-4",
|
|
1902
1917
|
style: "gap:.5rem; justify-content:flex-end;"}, [
|
|
1903
1918
|
["button", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"},
|
|
@@ -2501,6 +2516,7 @@ function topic_record(gobj, treedb, topic, values)
|
|
|
2501
2516
|
record.tkey = values.tkey || "";
|
|
2502
2517
|
record.topic_version = values.topic_version === "" ? 1 : Number(values.topic_version);
|
|
2503
2518
|
record.system_topic = !!values.system_topic;
|
|
2519
|
+
record.main_topic = !!values.main_topic;
|
|
2504
2520
|
|
|
2505
2521
|
return record;
|
|
2506
2522
|
}
|
|
@@ -111,7 +111,7 @@ SDATA(data_type_t.DTP_LIST, "operation_modes", 0,
|
|
|
111
111
|
'["reading", "operation", "writing", "edition"]',
|
|
112
112
|
"Available **permission** or behaviour modes. These operation modes are required to be accomplish by the graph handler (G6 child). TODO permissions must match treedb permissions."),
|
|
113
113
|
SDATA(data_type_t.DTP_BOOLEAN, "with_treedb_tables",0, false, "Include treedb tables"),
|
|
114
|
-
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0,
|
|
114
|
+
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 1, "Levels of the treedb open when it loads, until the reader steps (`fold_level`): 1 = the roots alone (the default), 2 = the roots and their children. Forwarded to the G6 child"),
|
|
115
115
|
SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page in the graph; a `+N` chip opens the next page. Forwarded to the G6 child"),
|
|
116
116
|
|
|
117
117
|
/*---------------- Forwarded to the G6 child (its own defaults) ----------------*/
|
|
@@ -2699,6 +2699,11 @@ function refresh_legend(gobj)
|
|
|
2699
2699
|
return String(a.topic).localeCompare(String(b.topic));
|
|
2700
2700
|
});
|
|
2701
2701
|
|
|
2702
|
+
/* Only a topic hooked to ITSELF can be the main one, so only those
|
|
2703
|
+
* offer the star -- and only when there is more than one to pick
|
|
2704
|
+
* from: with a single hierarchical topic the choice is made. */
|
|
2705
|
+
let hierarchical_count = topics.filter((e) => e.hierarchical && !e.hidden).length;
|
|
2706
|
+
|
|
2702
2707
|
for(let entry of topics) {
|
|
2703
2708
|
let topic_name = entry.topic;
|
|
2704
2709
|
let is_main = (topic_name === state.main_topic);
|
|
@@ -2813,7 +2818,7 @@ function refresh_legend(gobj)
|
|
|
2813
2818
|
style: 'padding:0 .5rem; cursor:default;',
|
|
2814
2819
|
title: t('main topic'), 'data-i18n-title': 'main topic'},
|
|
2815
2820
|
[['span', {style: LEGEND_STAR_MAIN_STYLE}, '★']]]);
|
|
2816
|
-
} else if(!hidden) {
|
|
2821
|
+
} else if(!hidden && entry.hierarchical && hierarchical_count > 1) {
|
|
2817
2822
|
extras.push(['button', {class: 'GRAPH_LEGEND_STAR button',
|
|
2818
2823
|
type: 'button', style: 'padding:0 .5rem;',
|
|
2819
2824
|
title: t('main topic'), 'data-i18n-title': 'main topic',
|
package/src/schema_import.js
CHANGED
|
@@ -38,7 +38,8 @@ import {
|
|
|
38
38
|
* the fkey, `order`) is composed, and `_geometry` belongs to whoever
|
|
39
39
|
* laid the graph out — an import must not move their boxes. */
|
|
40
40
|
const TOPIC_FIELDS = [
|
|
41
|
-
"pkey", "pkey2s", "system_flag", "tkey", "topic_version", "system_topic"
|
|
41
|
+
"pkey", "pkey2s", "system_flag", "tkey", "topic_version", "system_topic",
|
|
42
|
+
"main_topic"
|
|
42
43
|
];
|
|
43
44
|
|
|
44
45
|
/* Same for a `cols` record. */
|
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
|
}
|
|
@@ -355,34 +361,60 @@ function count_levels(model)
|
|
|
355
361
|
}
|
|
356
362
|
|
|
357
363
|
/************************************************************
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
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.)
|
|
363
396
|
************************************************************/
|
|
364
397
|
export function fold_main_topic(descs)
|
|
365
398
|
{
|
|
366
399
|
descs = is_object(descs)? descs : {};
|
|
367
|
-
let
|
|
368
|
-
|
|
369
|
-
|
|
400
|
+
let candidates = Object.keys(descs).filter((topic_name) => {
|
|
401
|
+
return topic_name.substring(0, 2) !== "__" && fold_is_hierarchical(descs, topic_name);
|
|
402
|
+
});
|
|
370
403
|
|
|
371
|
-
for(let topic_name of
|
|
372
|
-
if(topic_name.
|
|
373
|
-
|
|
404
|
+
for(let topic_name of candidates) {
|
|
405
|
+
if(descs[topic_name].main_topic === true) {
|
|
406
|
+
return topic_name;
|
|
374
407
|
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let best = "";
|
|
411
|
+
let best_score = -1;
|
|
412
|
+
for(let topic_name of candidates) {
|
|
375
413
|
let reach = hook_targets(descs, topic_name);
|
|
376
|
-
let self = reach.has(topic_name);
|
|
377
414
|
reach.delete(topic_name);
|
|
378
|
-
|
|
379
|
-
if(score === 0) {
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
if(score > best_score || (score === best_score && self && !best_self)) {
|
|
415
|
+
if(reach.size > best_score) {
|
|
383
416
|
best = topic_name;
|
|
384
|
-
best_score =
|
|
385
|
-
best_self = self;
|
|
417
|
+
best_score = reach.size;
|
|
386
418
|
}
|
|
387
419
|
}
|
|
388
420
|
return best;
|
|
@@ -31,11 +31,12 @@ import {
|
|
|
31
31
|
fold_open_levels,
|
|
32
32
|
fold_close_levels,
|
|
33
33
|
fold_expand_to_level,
|
|
34
|
+
fold_is_hierarchical,
|
|
34
35
|
} from "./treedb_fold_model.js";
|
|
35
36
|
|
|
36
37
|
const places_desc = {
|
|
37
38
|
topic_name: "places",
|
|
38
|
-
node_treedb_type: "
|
|
39
|
+
node_treedb_type: "entity",
|
|
39
40
|
cols: [
|
|
40
41
|
{id: "id", flag: []},
|
|
41
42
|
{id: "place", flag: ["fkey"], fkey: {places: "places"}},
|
|
@@ -56,13 +57,20 @@ const devices_desc = {
|
|
|
56
57
|
};
|
|
57
58
|
const controllers_desc = {
|
|
58
59
|
topic_name: "controllers",
|
|
59
|
-
node_treedb_type: "
|
|
60
|
+
node_treedb_type: "entity",
|
|
60
61
|
cols: [
|
|
61
62
|
{id: "id", flag: []},
|
|
62
63
|
{id: "place", flag: ["fkey"], fkey: {places: "controllers"}},
|
|
63
64
|
{id: "devices", flag: ["hook"], hook: {devices: "controller"}},
|
|
64
65
|
],
|
|
65
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
|
+
});
|
|
66
74
|
const groups_desc = {
|
|
67
75
|
topic_name: "device_groups",
|
|
68
76
|
node_treedb_type: "extended",
|
|
@@ -189,10 +197,33 @@ describe("main topic, hidden topics, loose records", () => {
|
|
|
189
197
|
expect(m.linked.has("device_groups")).toBe(false);
|
|
190
198
|
});
|
|
191
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
|
+
|
|
192
223
|
test("a self-referent hook breaks a tie", () => {
|
|
193
224
|
let a = {topic_name: "a", node_treedb_type: "extended",
|
|
194
225
|
cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "a"}}]};
|
|
195
|
-
let b = {topic_name: "b", node_treedb_type: "
|
|
226
|
+
let b = {topic_name: "b", node_treedb_type: "entity",
|
|
196
227
|
cols: [{id: "id", flag: []}, {id: "xs", flag: ["hook"], hook: {x: "b"}},
|
|
197
228
|
{id: "bs", flag: ["hook"], hook: {b: "parent"}},
|
|
198
229
|
{id: "parent", flag: ["fkey"], fkey: {b: "bs"}}]};
|
|
@@ -201,12 +232,16 @@ describe("main topic, hidden topics, loose records", () => {
|
|
|
201
232
|
expect(fold_main_topic({x: x})).toBe("");
|
|
202
233
|
});
|
|
203
234
|
|
|
204
|
-
test("
|
|
205
|
-
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"});
|
|
206
238
|
expect(m.main_topic).toBe("controllers");
|
|
207
239
|
expect([...m.linked].sort()).toEqual(["controllers", "devices"]);
|
|
208
240
|
/* places is not linked to controllers, so its roots are roots */
|
|
209
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");
|
|
210
245
|
});
|
|
211
246
|
|
|
212
247
|
test("a device with no place is LOOSE: counted, not a root, unless asked for", () => {
|