@yuneta/gobj-ui 6.1.0 → 6.1.2
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 +20 -7
- package/dist/gobj-ui.cjs.js +248 -84
- package/dist/gobj-ui.es.js +248 -84
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +124 -36
- package/src/c_yui_treedb_schema.js +17 -7
- package/src/schema_descs.js +14 -6
- package/src/schema_descs.test.js +25 -0
- package/src/schema_import.js +17 -6
- package/src/schema_import.test.js +21 -0
- package/src/schema_model.js +26 -0
- package/src/schema_model.test.js +25 -0
- package/src/schema_to_c.js +22 -8
- package/src/schema_to_c.test.js +47 -0
- package/src/schema_write_options.js +60 -0
- package/src/schema_write_options.test.js +65 -0
- package/src/yui_icons.css +14 -0
|
@@ -120,6 +120,7 @@ import {
|
|
|
120
120
|
col_flags,
|
|
121
121
|
col_hook,
|
|
122
122
|
col_enum,
|
|
123
|
+
is_empty_value,
|
|
123
124
|
fkey_ref,
|
|
124
125
|
next_order,
|
|
125
126
|
moved_orders,
|
|
@@ -129,6 +130,7 @@ import {topic_descs} from "./schema_descs.js";
|
|
|
129
130
|
import {schema_to_json, schema_to_c} from "./schema_to_c.js";
|
|
130
131
|
import {plan_import, plan_deletes} from "./schema_import.js";
|
|
131
132
|
import {grouped_flags, toggle_flag} from "./schema_flags.js";
|
|
133
|
+
import {write_command, write_options} from "./schema_write_options.js";
|
|
132
134
|
import {COL_TYPES} from "./schema_validate.js";
|
|
133
135
|
|
|
134
136
|
import {yui_shell_of} from "./c_yui_shell.js";
|
|
@@ -488,10 +490,23 @@ function remote_command(gobj, command, kw)
|
|
|
488
490
|
service: treedb_name,
|
|
489
491
|
treedb_name: treedb_name
|
|
490
492
|
}, kw || {});
|
|
491
|
-
|
|
493
|
+
|
|
494
|
+
/* What has to come BACK with the answer.
|
|
495
|
+
*
|
|
496
|
+
* Its keys repeat the ones the command already carries, on purpose:
|
|
497
|
+
* the transport may be a direct C_IEVENT_CLI, which echoes the whole
|
|
498
|
+
* request as the command_stack frame, or a routing adapter, which
|
|
499
|
+
* echoes `__md_command__` AS that frame (c_agent_treedb_link.js).
|
|
500
|
+
* Same names in both places is what makes the answer readable
|
|
501
|
+
* whichever one is under this view — and `record_id` is here and
|
|
502
|
+
* NOT at the top level because a delete answers with nothing, and a
|
|
503
|
+
* parameter the treedb does not know has no business travelling. */
|
|
504
|
+
full_kw.__md_command__ = {
|
|
492
505
|
purpose: PURPOSE,
|
|
493
|
-
|
|
494
|
-
|
|
506
|
+
topic_name: (kw && kw.topic_name) || "",
|
|
507
|
+
record_id: (kw && kw.record_id) || ""
|
|
508
|
+
};
|
|
509
|
+
delete full_kw.record_id;
|
|
495
510
|
|
|
496
511
|
let ret = gobj_command(remote, command, full_kw, gobj);
|
|
497
512
|
if(ret) {
|
|
@@ -743,12 +758,12 @@ function render_toolbar(gobj)
|
|
|
743
758
|
if(priv.treedb_id && has_model) {
|
|
744
759
|
$right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-hexagon-nodes",
|
|
745
760
|
priv.diagram ? "topics" : "diagram", "EV_TOGGLE_DIAGRAM", false));
|
|
746
|
-
$right.push(toolbar_button("SCHEMA_VALIDATE_BTN", "yi-
|
|
761
|
+
$right.push(toolbar_button("SCHEMA_VALIDATE_BTN", "yi-square-check",
|
|
747
762
|
"check", "EV_VALIDATE", false));
|
|
748
|
-
$right.push(toolbar_button("SCHEMA_EXPORT_BTN", "yi-
|
|
763
|
+
$right.push(toolbar_button("SCHEMA_EXPORT_BTN", "yi-download",
|
|
749
764
|
"export", "EV_EXPORT", false));
|
|
750
765
|
if(!readonly) {
|
|
751
|
-
$right.push(toolbar_button("SCHEMA_IMPORT_BTN", "yi-
|
|
766
|
+
$right.push(toolbar_button("SCHEMA_IMPORT_BTN", "yi-upload",
|
|
752
767
|
"import", "EV_IMPORT", false));
|
|
753
768
|
}
|
|
754
769
|
}
|
|
@@ -944,17 +959,23 @@ function render_topics(gobj, $body)
|
|
|
944
959
|
}
|
|
945
960
|
|
|
946
961
|
let readonly = is_readonly(gobj);
|
|
962
|
+
let twice = repeated_names(treedb.topics);
|
|
947
963
|
let $rows = [];
|
|
948
964
|
for(let topic of treedb.topics) {
|
|
949
965
|
let pending = priv.written[topic.id] &&
|
|
950
966
|
String(priv.baseline[topic.id]) === String(topic.topic_version);
|
|
967
|
+
/* Two topics of one name: the id is what tells them apart, and
|
|
968
|
+
* it is what the url has to carry — addressing by name would
|
|
969
|
+
* open the first one whichever row was clicked. */
|
|
970
|
+
let ambiguous = !!twice[topic.name];
|
|
971
|
+
let address = ambiguous ? topic.id : topic.name;
|
|
951
972
|
|
|
952
973
|
let $actions = [];
|
|
953
974
|
if(!readonly) {
|
|
954
975
|
$actions.push(row_icon(gobj, "SCHEMA_TOPIC_EDIT", "yi-pen", "edit topic",
|
|
955
|
-
"EV_EDIT_TOPIC", {topic:
|
|
976
|
+
"EV_EDIT_TOPIC", {topic: address}));
|
|
956
977
|
$actions.push(row_icon(gobj, "SCHEMA_TOPIC_DELETE", "yi-trash", "delete topic",
|
|
957
|
-
"EV_DELETE_TOPIC", {topic:
|
|
978
|
+
"EV_DELETE_TOPIC", {topic: address}));
|
|
958
979
|
}
|
|
959
980
|
|
|
960
981
|
$rows.push(["tr", {class: "SCHEMA_TOPIC_ROW", role: "button", tabindex: "0"}, [
|
|
@@ -963,6 +984,10 @@ function render_topics(gobj, $body)
|
|
|
963
984
|
topic.system_topic
|
|
964
985
|
? ["span", {class: "SCHEMA_TOPIC_SYSTEM tag is-light ml-2",
|
|
965
986
|
i18n: "system"}, t("system")]
|
|
987
|
+
: ["span", {}, ""],
|
|
988
|
+
ambiguous
|
|
989
|
+
? ["div", {class: "SCHEMA_TOPIC_ID is-size-7 has-text-grey"},
|
|
990
|
+
[["code", {}, `${topic.id}`]]]
|
|
966
991
|
: ["span", {}, ""]
|
|
967
992
|
]],
|
|
968
993
|
["td", {class: "SCHEMA_TOPIC_PKEY"}, [["code", {}, `${topic.pkey || "id"}`]]],
|
|
@@ -977,14 +1002,14 @@ function render_topics(gobj, $body)
|
|
|
977
1002
|
if(evt.target.closest(".SCHEMA_TOPIC_ACTIONS")) {
|
|
978
1003
|
return; /* a row action, not the row */
|
|
979
1004
|
}
|
|
980
|
-
gobj_send_event(gobj, "EV_SELECT_TOPIC", {topic:
|
|
1005
|
+
gobj_send_event(gobj, "EV_SELECT_TOPIC", {topic: address}, gobj);
|
|
981
1006
|
},
|
|
982
1007
|
keydown: (evt) => {
|
|
983
1008
|
if(evt.key !== "Enter" && evt.key !== " ") {
|
|
984
1009
|
return;
|
|
985
1010
|
}
|
|
986
1011
|
evt.preventDefault();
|
|
987
|
-
gobj_send_event(gobj, "EV_SELECT_TOPIC", {topic:
|
|
1012
|
+
gobj_send_event(gobj, "EV_SELECT_TOPIC", {topic: address}, gobj);
|
|
988
1013
|
}
|
|
989
1014
|
}]);
|
|
990
1015
|
}
|
|
@@ -1029,6 +1054,31 @@ function row_icon(gobj, logical, icon, key, event, kw)
|
|
|
1029
1054
|
}];
|
|
1030
1055
|
}
|
|
1031
1056
|
|
|
1057
|
+
/***************************************************************
|
|
1058
|
+
* Which names appear more than once.
|
|
1059
|
+
*
|
|
1060
|
+
* A store can hold TWO GENERATIONS of the same schema: the
|
|
1061
|
+
* projector never deletes, so a treedb re-keyed by a newer SDK
|
|
1062
|
+
* keeps its old rowid-keyed rows next to the qualified ones, and
|
|
1063
|
+
* both are children of the same treedb with the same name. Drawn
|
|
1064
|
+
* as two identical rows that behave differently, that reads as a
|
|
1065
|
+
* bug in the editor; drawn with what tells them apart, it reads
|
|
1066
|
+
* as what it is.
|
|
1067
|
+
***************************************************************/
|
|
1068
|
+
function repeated_names(list)
|
|
1069
|
+
{
|
|
1070
|
+
let seen = {};
|
|
1071
|
+
let twice = {};
|
|
1072
|
+
|
|
1073
|
+
for(let entry of list) {
|
|
1074
|
+
if(seen[entry.name]) {
|
|
1075
|
+
twice[entry.name] = true;
|
|
1076
|
+
}
|
|
1077
|
+
seen[entry.name] = true;
|
|
1078
|
+
}
|
|
1079
|
+
return twice;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1032
1082
|
/***************************************************************
|
|
1033
1083
|
* ST_COLUMNS — the columns of one topic, in schema order.
|
|
1034
1084
|
*
|
|
@@ -1062,9 +1112,10 @@ function render_columns(gobj, $body)
|
|
|
1062
1112
|
$wrap.appendChild($banner);
|
|
1063
1113
|
}
|
|
1064
1114
|
|
|
1115
|
+
let twice = repeated_names(topic.cols);
|
|
1065
1116
|
let $rows = [];
|
|
1066
1117
|
for(let i = 0; i < topic.cols.length; i++) {
|
|
1067
|
-
$rows.push(column_row(gobj, topic, topic.cols[i], i, readonly));
|
|
1118
|
+
$rows.push(column_row(gobj, topic, topic.cols[i], i, readonly, twice));
|
|
1068
1119
|
}
|
|
1069
1120
|
|
|
1070
1121
|
let $table = createElement2(
|
|
@@ -1133,7 +1184,7 @@ function version_banner(gobj, topic)
|
|
|
1133
1184
|
title: t("raise the version"), "aria-label": t("raise the version"),
|
|
1134
1185
|
"data-i18n-title": "raise the version",
|
|
1135
1186
|
"data-i18n-aria-label": "raise the version"}, [
|
|
1136
|
-
["span", {class: "icon"}, [["i", {class: "yi-
|
|
1187
|
+
["span", {class: "icon"}, [["i", {class: "yi-chevron-up"}]]],
|
|
1137
1188
|
["span", {i18n: "raise"}, t("raise")]
|
|
1138
1189
|
]]
|
|
1139
1190
|
]]
|
|
@@ -1152,9 +1203,13 @@ function version_banner(gobj, topic)
|
|
|
1152
1203
|
* Yuneta event is plain JSON, so a DOM node never travels in
|
|
1153
1204
|
* one.
|
|
1154
1205
|
***************************************************************/
|
|
1155
|
-
function column_row(gobj, topic, col, index, readonly)
|
|
1206
|
+
function column_row(gobj, topic, col, index, readonly, twice)
|
|
1156
1207
|
{
|
|
1157
1208
|
let record = col.record || {};
|
|
1209
|
+
/* Same two generations as the topics above: when a name is not
|
|
1210
|
+
* unique the id is what addresses the row. */
|
|
1211
|
+
let ambiguous = !!(twice && twice[col.name]);
|
|
1212
|
+
let address = ambiguous ? col.id : col.name;
|
|
1158
1213
|
let flags = col_flags(record);
|
|
1159
1214
|
let hook = col_hook(record);
|
|
1160
1215
|
let is_pkey = (col.name === (topic.pkey || "id"));
|
|
@@ -1163,8 +1218,11 @@ function column_row(gobj, topic, col, index, readonly)
|
|
|
1163
1218
|
return ["span", {class: `SCHEMA_COL_FLAG tag is-light ${flag_style(flag)}`}, `${flag}`];
|
|
1164
1219
|
});
|
|
1165
1220
|
|
|
1221
|
+
/* An unset `blob` column comes back as `{}`, which is an object and
|
|
1222
|
+
* is not a hook: read as one, every column in the topic draws an
|
|
1223
|
+
* arrow pointing at nothing. */
|
|
1166
1224
|
let link = "";
|
|
1167
|
-
if(hook) {
|
|
1225
|
+
if(hook && !is_empty_value(hook)) {
|
|
1168
1226
|
link = "→ " + Object.keys(hook).join(", ");
|
|
1169
1227
|
} else if(flags.indexOf("fkey") >= 0) {
|
|
1170
1228
|
link = "↖";
|
|
@@ -1173,11 +1231,11 @@ function column_row(gobj, topic, col, index, readonly)
|
|
|
1173
1231
|
let $actions = [];
|
|
1174
1232
|
if(!readonly) {
|
|
1175
1233
|
$actions.push(row_icon(gobj, "SCHEMA_COL_EDIT", "yi-pen", "edit column",
|
|
1176
|
-
"EV_EDIT_COLUMN", {col:
|
|
1234
|
+
"EV_EDIT_COLUMN", {col: address}));
|
|
1177
1235
|
$actions.push(row_icon(gobj, "SCHEMA_COL_DUPLICATE", "yi-copy", "duplicate column",
|
|
1178
|
-
"EV_DUPLICATE_COLUMN", {col:
|
|
1236
|
+
"EV_DUPLICATE_COLUMN", {col: address}));
|
|
1179
1237
|
$actions.push(row_icon(gobj, "SCHEMA_COL_DELETE", "yi-trash", "delete column",
|
|
1180
|
-
"EV_DELETE_COLUMN", {col:
|
|
1238
|
+
"EV_DELETE_COLUMN", {col: address}));
|
|
1181
1239
|
}
|
|
1182
1240
|
|
|
1183
1241
|
let $row = createElement2(
|
|
@@ -1188,6 +1246,10 @@ function column_row(gobj, topic, col, index, readonly)
|
|
|
1188
1246
|
[["i", {class: "yi-grip-vertical"}]]]]],
|
|
1189
1247
|
["td", {class: "SCHEMA_COL_NAME"}, [
|
|
1190
1248
|
["code", {}, `${col.name}`],
|
|
1249
|
+
ambiguous
|
|
1250
|
+
? ["span", {class: "SCHEMA_COL_ID is-size-7 has-text-grey ml-2"},
|
|
1251
|
+
[["code", {}, `${col.id}`]]]
|
|
1252
|
+
: ["span", {}, ""],
|
|
1191
1253
|
is_pkey
|
|
1192
1254
|
? ["span", {class: "SCHEMA_COL_PKEY tag is-info is-light ml-2",
|
|
1193
1255
|
i18n: "pkey"}, t("pkey")]
|
|
@@ -1210,7 +1272,7 @@ function column_row(gobj, topic, col, index, readonly)
|
|
|
1210
1272
|
if(readonly || evt.target.closest(".SCHEMA_COL_ACTIONS")) {
|
|
1211
1273
|
return;
|
|
1212
1274
|
}
|
|
1213
|
-
gobj_send_event(gobj, "EV_EDIT_COLUMN", {col:
|
|
1275
|
+
gobj_send_event(gobj, "EV_EDIT_COLUMN", {col: address}, gobj);
|
|
1214
1276
|
});
|
|
1215
1277
|
return $row;
|
|
1216
1278
|
}
|
|
@@ -1327,9 +1389,11 @@ function render_diagram(gobj)
|
|
|
1327
1389
|
subscriber: gobj,
|
|
1328
1390
|
descs: descs,
|
|
1329
1391
|
/* No `node_route`: this diagram lives inside the editor and
|
|
1330
|
-
* a node click is answered
|
|
1331
|
-
* would have to own
|
|
1332
|
-
|
|
1392
|
+
* a node click is answered HERE, not by a hash the shell
|
|
1393
|
+
* would have to own — which is what `with_node_click` asks
|
|
1394
|
+
* for, and why EV_NODE_CLICK is declared in this FSM. */
|
|
1395
|
+
node_route: "",
|
|
1396
|
+
with_node_click: true,
|
|
1333
1397
|
system: true
|
|
1334
1398
|
},
|
|
1335
1399
|
gobj
|
|
@@ -1552,7 +1616,10 @@ function open_column_form(gobj, topic, col, prefill)
|
|
|
1552
1616
|
let record = prefill || (col && col.record) || {};
|
|
1553
1617
|
let creating = !col;
|
|
1554
1618
|
let type = record.type || "string";
|
|
1555
|
-
let hook = col_hook(record)
|
|
1619
|
+
let hook = col_hook(record);
|
|
1620
|
+
if(is_empty_value(hook)) {
|
|
1621
|
+
hook = {};
|
|
1622
|
+
}
|
|
1556
1623
|
let hook_topic = Object.keys(hook)[0] || "";
|
|
1557
1624
|
let hook_col = hook_topic ? hook[hook_topic] : "";
|
|
1558
1625
|
let treedb = current_treedb(gobj);
|
|
@@ -1686,7 +1753,7 @@ function open_topic_form(gobj, treedb, topic)
|
|
|
1686
1753
|
col_names.length > 0 ? col_names : [record.pkey || "id"])),
|
|
1687
1754
|
field("SCHEMA_TOPIC_FORM_PKEY2S", "pkey2s", "secondary keys",
|
|
1688
1755
|
text_input("pkey2s", stringify_field(record.pkey2s), ""),
|
|
1689
|
-
"the column a record is
|
|
1756
|
+
"the column a record is known by when its key is composed"),
|
|
1690
1757
|
field("SCHEMA_TOPIC_FORM_SYSTEM_FLAG", "system_flag", "system flag",
|
|
1691
1758
|
text_input("system_flag", record.system_flag || "sf_string_key", "")),
|
|
1692
1759
|
field("SCHEMA_TOPIC_FORM_TKEY", "tkey", "time key",
|
|
@@ -1888,7 +1955,7 @@ function open_import(gobj, treedb)
|
|
|
1888
1955
|
["span", {i18n: "preview"}, t("preview")]]],
|
|
1889
1956
|
["button", {class: "SCHEMA_IMPORT_RUN button is-small is-warning",
|
|
1890
1957
|
type: "button", disabled: "disabled"},
|
|
1891
|
-
[["span", {class: "icon"}, [["i", {class: "yi-
|
|
1958
|
+
[["span", {class: "icon"}, [["i", {class: "yi-upload"}]]],
|
|
1892
1959
|
["span", {i18n: "import"}, t("import")]]]
|
|
1893
1960
|
]]
|
|
1894
1961
|
]]
|
|
@@ -2112,13 +2179,18 @@ function run_next_write(gobj)
|
|
|
2112
2179
|
return end_writes(gobj, "");
|
|
2113
2180
|
}
|
|
2114
2181
|
let write = priv.save_queue[0];
|
|
2115
|
-
|
|
2116
|
-
|
|
2182
|
+
|
|
2183
|
+
/* Which command and which options: three words that decide whether
|
|
2184
|
+
* a write does what it says, and each one wrong in a way that
|
|
2185
|
+
* succeeds. They live in schema_write_options.js, with the reasons
|
|
2186
|
+
* and the tests. */
|
|
2187
|
+
let command = write_command(write.op);
|
|
2188
|
+
let options = write_options(write.op);
|
|
2117
2189
|
|
|
2118
2190
|
if(remote_command(gobj, command, {
|
|
2119
2191
|
topic_name: write.topic_name,
|
|
2120
2192
|
record: write.record,
|
|
2121
|
-
options:
|
|
2193
|
+
options: options,
|
|
2122
2194
|
record_id: write.record.id || ""
|
|
2123
2195
|
}) < 0) {
|
|
2124
2196
|
return end_writes(gobj, t("cannot reach the treedb"));
|
|
@@ -2198,7 +2270,14 @@ function column_record(gobj, topic, col, values)
|
|
|
2198
2270
|
record.header = values.header || "";
|
|
2199
2271
|
record.type = values.type || "string";
|
|
2200
2272
|
record.flag = flags;
|
|
2201
|
-
|
|
2273
|
+
/* An empty number field is ABSENT, not zero: sending "" stored a
|
|
2274
|
+
* fillspace of 0 and the column's schema default (10) never
|
|
2275
|
+
* applied. */
|
|
2276
|
+
if(String(values.fillspace).trim() !== "") {
|
|
2277
|
+
record.fillspace = Number(values.fillspace);
|
|
2278
|
+
} else if(!creating) {
|
|
2279
|
+
record.fillspace = ""; /* cleared on purpose */
|
|
2280
|
+
}
|
|
2202
2281
|
record.placeholder = values.placeholder || "";
|
|
2203
2282
|
record.description = values.description || "";
|
|
2204
2283
|
record.default = values.default || "";
|
|
@@ -2359,7 +2438,14 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2359
2438
|
let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
|
|
2360
2439
|
let command = kw_get_str(gobj, __command__, "command", "", 0);
|
|
2361
2440
|
let kw_command = kw_get_dict(gobj, __command__, "kw", {}, 0);
|
|
2362
|
-
|
|
2441
|
+
|
|
2442
|
+
/* The frame carries either the whole request (a direct transport)
|
|
2443
|
+
* or the `__md_command__` this view sent (a routing adapter). Both
|
|
2444
|
+
* answer the same questions under the same names, so unwrap one
|
|
2445
|
+
* level when it is there and read from whichever it is. */
|
|
2446
|
+
/* Not kw_get_dict with a null default: it answers Object(null), which
|
|
2447
|
+
* is `{}` and truthy, so the fallback would never run. */
|
|
2448
|
+
let md = is_object(kw_command.__md_command__) ? kw_command.__md_command__ : kw_command;
|
|
2363
2449
|
|
|
2364
2450
|
if(kw_get_str(gobj, md, "purpose", "", 0) !== PURPOSE) {
|
|
2365
2451
|
return 0; /* another panel's answer on a shared transport */
|
|
@@ -2388,15 +2474,14 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2388
2474
|
return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
2389
2475
|
}
|
|
2390
2476
|
|
|
2391
|
-
if(command === "
|
|
2392
|
-
let topic_name = kw_get_str(gobj,
|
|
2477
|
+
if(command === "update-node" || command === "delete-node") {
|
|
2478
|
+
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
2393
2479
|
if(result < 0) {
|
|
2394
2480
|
return end_writes(gobj, comment || t("the treedb refused the write"));
|
|
2395
2481
|
}
|
|
2396
2482
|
priv.save_wrote = true;
|
|
2397
2483
|
if(command === "delete-node") {
|
|
2398
|
-
forget_record(gobj, topic_name,
|
|
2399
|
-
kw_get_str(gobj, kw_command, "record_id", "", 0));
|
|
2484
|
+
forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
|
|
2400
2485
|
} else {
|
|
2401
2486
|
let record = is_object(data) ? data
|
|
2402
2487
|
: (Array.isArray(data) && is_object(data[0]) ? data[0] : null);
|
|
@@ -2412,8 +2497,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2412
2497
|
}
|
|
2413
2498
|
}
|
|
2414
2499
|
if(topic_name === T_TOPICS || topic_name === T_COLS) {
|
|
2415
|
-
|
|
2416
|
-
|
|
2500
|
+
/* Which record it was is in the QUEUE, not in the answer: an
|
|
2501
|
+
* adapter echoes only what it was asked to echo, and the
|
|
2502
|
+
* record is not small enough to be worth echoing. */
|
|
2503
|
+
let write = priv.save_queue[0];
|
|
2504
|
+
mark_written(gobj, topic_name, write ? write.record : null);
|
|
2417
2505
|
}
|
|
2418
2506
|
priv.save_queue.shift();
|
|
2419
2507
|
return run_next_write(gobj);
|
|
@@ -2927,7 +3015,7 @@ function ac_preview_import(gobj, event, kw, src)
|
|
|
2927
3015
|
} catch(e) {
|
|
2928
3016
|
priv.import_plan = null;
|
|
2929
3017
|
render_import_plan(gobj, {writes: [], summary: {},
|
|
2930
|
-
conflicts: [{code: "that is not
|
|
3018
|
+
conflicts: [{code: "that is not json", topic: "", col: ""}]});
|
|
2931
3019
|
return -1;
|
|
2932
3020
|
}
|
|
2933
3021
|
|
|
@@ -94,6 +94,7 @@ const attrs_table = [
|
|
|
94
94
|
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
95
95
|
SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Treedb schema: {topic_name: desc}"),
|
|
96
96
|
SDATA(data_type_t.DTP_STRING, "node_route", 0, "", "Hash-route template with a {topic} placeholder: a node click opens that topic (e.g. '#/topics/db/<sel>/{topic}')"),
|
|
97
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_node_click", 0, false, "With no `node_route`, PUBLISH a node click as EV_NODE_CLICK instead of dropping it. Opt-in, because a subscriber has to DECLARE the event: the hosts that route by hash, and the ones that want nothing, must not have it appear underneath them"),
|
|
97
98
|
SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Include system topics (__*__) too"),
|
|
98
99
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element"),
|
|
99
100
|
SDATA_END()
|
|
@@ -622,12 +623,19 @@ function destroy_graph(gobj)
|
|
|
622
623
|
* With a `node_route` the click IS a navigation and this view
|
|
623
624
|
* makes it: that is what the route was handed down for.
|
|
624
625
|
*
|
|
625
|
-
* Without one it is still an action, and it
|
|
626
|
-
* mounted this view — the schema editor draws the same
|
|
627
|
-
* inside its own screens, where a topic opens in place
|
|
628
|
-
* hash is involved.
|
|
629
|
-
*
|
|
630
|
-
*
|
|
626
|
+
* Without one it is still an action, and it can belong to
|
|
627
|
+
* whoever mounted this view — the schema editor draws the same
|
|
628
|
+
* picture inside its own screens, where a topic opens in place
|
|
629
|
+
* and no hash is involved. That host asks for it with
|
|
630
|
+
* `with_node_click` and DECLARES EV_NODE_CLICK in its own FSM,
|
|
631
|
+
* as with every event a child publishes.
|
|
632
|
+
*
|
|
633
|
+
* It is opt-in and not the default for exactly that reason: the
|
|
634
|
+
* CHILD subscription model subscribes the host to ALL of this
|
|
635
|
+
* view's events, so publishing one unasked turns a click into
|
|
636
|
+
* "Event NOT DEFINED in state" in every host that never wanted
|
|
637
|
+
* it — the topics view that mounts this as its schema landing,
|
|
638
|
+
* and the offline demo that mounts it against a json file.
|
|
631
639
|
************************************************************/
|
|
632
640
|
function ac_node_click(gobj, event, kw, src)
|
|
633
641
|
{
|
|
@@ -638,7 +646,9 @@ function ac_node_click(gobj, event, kw, src)
|
|
|
638
646
|
return 0;
|
|
639
647
|
}
|
|
640
648
|
if(!route) {
|
|
641
|
-
|
|
649
|
+
if(gobj_read_bool_attr(gobj, "with_node_click")) {
|
|
650
|
+
gobj_publish_event(gobj, "EV_NODE_CLICK", {topic: topic});
|
|
651
|
+
}
|
|
642
652
|
return 0;
|
|
643
653
|
}
|
|
644
654
|
let href = route.replace("{topic}", topic);
|
package/src/schema_descs.js
CHANGED
|
@@ -34,16 +34,18 @@ import {
|
|
|
34
34
|
col_enum,
|
|
35
35
|
topic_pkey2s,
|
|
36
36
|
as_json,
|
|
37
|
+
is_empty_value,
|
|
37
38
|
} from "./schema_model.js";
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
/* Storage-only fields of a `cols` record: they describe where the
|
|
41
42
|
* record LIVES in treedb_system_schema, not the column it declares.
|
|
42
43
|
* The C validator drops the same ones (_treedb_create_topic_cols_desc). */
|
|
43
|
-
const COL_STORAGE_FIELDS = ["id", "value", "topics", "order", "__md_treedb__"];
|
|
44
|
+
const COL_STORAGE_FIELDS = ["id", "value", "topics", "order", "_geometry", "__md_treedb__"];
|
|
44
45
|
|
|
45
46
|
/* Same, one level up, for a `topics` record. */
|
|
46
|
-
const TOPIC_STORAGE_FIELDS = ["id", "value", "treedbs", "cols", "order", "
|
|
47
|
+
const TOPIC_STORAGE_FIELDS = ["id", "value", "treedbs", "cols", "order", "_geometry",
|
|
48
|
+
"__md_treedb__"];
|
|
47
49
|
|
|
48
50
|
/* Fields of a column whose stored form is TEXT and whose desc form is
|
|
49
51
|
* the value: they are `blob` columns of the `cols` topic. */
|
|
@@ -67,16 +69,22 @@ function col_desc(col)
|
|
|
67
69
|
if(COL_STORAGE_FIELDS.indexOf(key) >= 0) {
|
|
68
70
|
continue;
|
|
69
71
|
}
|
|
70
|
-
if(value
|
|
72
|
+
if(is_empty_value(value)) {
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
73
|
-
|
|
75
|
+
let read = COL_JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
|
|
76
|
+
if(is_empty_value(read)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
desc[key] = read;
|
|
74
80
|
}
|
|
75
81
|
desc.id = col.name;
|
|
76
82
|
desc.flag = col_flags(record);
|
|
77
83
|
let hook = col_hook(record);
|
|
78
|
-
if(hook) {
|
|
84
|
+
if(hook && !is_empty_value(hook)) {
|
|
79
85
|
desc.hook = hook;
|
|
86
|
+
} else {
|
|
87
|
+
delete desc.hook;
|
|
80
88
|
}
|
|
81
89
|
let e = col_enum(record);
|
|
82
90
|
if(e.length > 0) {
|
|
@@ -105,7 +113,7 @@ function topic_descs(treedb)
|
|
|
105
113
|
if(TOPIC_STORAGE_FIELDS.indexOf(key) >= 0) {
|
|
106
114
|
continue;
|
|
107
115
|
}
|
|
108
|
-
if(value
|
|
116
|
+
if(is_empty_value(value)) {
|
|
109
117
|
continue;
|
|
110
118
|
}
|
|
111
119
|
desc[key] = value;
|
package/src/schema_descs.test.js
CHANGED
|
@@ -143,3 +143,28 @@ describe("edges", () => {
|
|
|
143
143
|
expect(topic_descs({})).toEqual({});
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
describe("the empty collections the store answers with", () => {
|
|
148
|
+
const model = build_schema_model({
|
|
149
|
+
treedbs: [{id: "db"}],
|
|
150
|
+
topics: [{id: "db.t", value: "t", _geometry: {}, treedbs: ["treedbs^db^topics"]}],
|
|
151
|
+
cols: [{id: "db.t.id", value: "id", type: "string",
|
|
152
|
+
enum: {}, hook: {}, default: {}, _geometry: {},
|
|
153
|
+
topics: ["topics^db.t^cols"]}],
|
|
154
|
+
});
|
|
155
|
+
const descs = topic_descs(model.treedbs[0]);
|
|
156
|
+
|
|
157
|
+
test("an unset blob does not become a value in the desc", () => {
|
|
158
|
+
for(const key of ["enum", "hook", "default", "_geometry"]) {
|
|
159
|
+
expect(key in descs.t.cols[0]).toBe(false);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("an empty hook is NOT a hook — the info panel would draw an arrow to nothing", () => {
|
|
164
|
+
expect(descs.t.cols[0].hook).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("the topic record's own _geometry does not travel", () => {
|
|
168
|
+
expect("_geometry" in descs.t).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
package/src/schema_import.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
col_hook,
|
|
30
30
|
col_enum,
|
|
31
31
|
as_json,
|
|
32
|
+
is_empty_value,
|
|
32
33
|
fkey_ref,
|
|
33
34
|
} from "./schema_model.js";
|
|
34
35
|
|
|
@@ -66,7 +67,7 @@ function same_value(field, a, b)
|
|
|
66
67
|
list = list.filter(f => typeof f === "string" && f.length > 0).sort();
|
|
67
68
|
return JSON.stringify(list);
|
|
68
69
|
}
|
|
69
|
-
if(v
|
|
70
|
+
if(is_empty_value(v)) {
|
|
70
71
|
return "";
|
|
71
72
|
}
|
|
72
73
|
if(typeof v === "object") {
|
|
@@ -86,10 +87,14 @@ function incoming_col_fields(col)
|
|
|
86
87
|
|
|
87
88
|
for(let field of COL_FIELDS) {
|
|
88
89
|
let value = col ? col[field] : undefined;
|
|
89
|
-
if(value
|
|
90
|
+
if(is_empty_value(value)) {
|
|
90
91
|
continue;
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
let read = JSON_FIELDS.indexOf(field) >= 0 ? as_json(value) : value;
|
|
94
|
+
if(is_empty_value(read)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
out[field] = read;
|
|
93
98
|
}
|
|
94
99
|
if(Array.isArray(out.flag)) {
|
|
95
100
|
out.flag = out.flag.filter(f => typeof f === "string" && f.length > 0);
|
|
@@ -108,10 +113,14 @@ function stored_col_fields(record)
|
|
|
108
113
|
|
|
109
114
|
for(let field of COL_FIELDS) {
|
|
110
115
|
let value = record ? record[field] : undefined;
|
|
111
|
-
if(value
|
|
116
|
+
if(is_empty_value(value)) {
|
|
112
117
|
continue;
|
|
113
118
|
}
|
|
114
|
-
|
|
119
|
+
let read = JSON_FIELDS.indexOf(field) >= 0 ? as_json(value) : value;
|
|
120
|
+
if(is_empty_value(read)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
out[field] = read;
|
|
115
124
|
}
|
|
116
125
|
let flags = col_flags(record);
|
|
117
126
|
if(flags.length > 0) {
|
|
@@ -120,8 +129,10 @@ function stored_col_fields(record)
|
|
|
120
129
|
delete out.flag;
|
|
121
130
|
}
|
|
122
131
|
let hook = col_hook(record);
|
|
123
|
-
if(hook) {
|
|
132
|
+
if(hook && !is_empty_value(hook)) {
|
|
124
133
|
out.hook = hook;
|
|
134
|
+
} else {
|
|
135
|
+
delete out.hook;
|
|
125
136
|
}
|
|
126
137
|
let e = col_enum(record);
|
|
127
138
|
if(e.length > 0) {
|
|
@@ -232,3 +232,24 @@ describe("what is not a schema", () => {
|
|
|
232
232
|
expect(plan_import(null, {topics: []}).conflicts[0].code).toBe("schema has no id");
|
|
233
233
|
});
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
describe("the empty collections the store answers with", () => {
|
|
237
|
+
test("a stored `{}` and an absent field are the same, so nothing is planned", () => {
|
|
238
|
+
/* Otherwise every import of an unchanged schema rewrites every
|
|
239
|
+
column to clear fields that were never set. */
|
|
240
|
+
const stored = build_schema_model({
|
|
241
|
+
treedbs: [{id: "db", schema_version: 1}],
|
|
242
|
+
topics: [{id: "db.t", value: "t", order: 1, pkey: "id", topic_version: 1,
|
|
243
|
+
treedbs: ["treedbs^db^topics"]}],
|
|
244
|
+
cols: [{id: "db.t.id", value: "id", order: 1, header: "Id", type: "string",
|
|
245
|
+
enum: {}, hook: {}, default: {}, template: {}, properties: {},
|
|
246
|
+
topics: ["topics^db.t^cols"]}],
|
|
247
|
+
}).treedbs[0];
|
|
248
|
+
const incoming = {
|
|
249
|
+
id: "db", schema_version: "1",
|
|
250
|
+
topics: [{id: "t", pkey: "id", topic_version: "1",
|
|
251
|
+
cols: {id: {header: "Id", type: "string"}}}]
|
|
252
|
+
};
|
|
253
|
+
expect(plan_import(stored, incoming).writes).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
});
|
package/src/schema_model.js
CHANGED
|
@@ -294,6 +294,31 @@ function build_schema_model(records)
|
|
|
294
294
|
};
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/***************************************************************
|
|
298
|
+
* Is there a value here at all?
|
|
299
|
+
*
|
|
300
|
+
* The store answers a `blob` column that was never set with an
|
|
301
|
+
* EMPTY COLLECTION, not with nothing: `enum: {}`, `hook: {}`,
|
|
302
|
+
* `default: {}`. Read as "a value", those turn every export into
|
|
303
|
+
* a schema full of empty objects — and `'hook': {}` in a literal
|
|
304
|
+
* is a hook with no mapping, which is a link the treedb builds
|
|
305
|
+
* and nothing writes.
|
|
306
|
+
***************************************************************/
|
|
307
|
+
function is_empty_value(value)
|
|
308
|
+
{
|
|
309
|
+
if(value === null || value === undefined || value === "") {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if(Array.isArray(value)) {
|
|
313
|
+
return value.length === 0;
|
|
314
|
+
}
|
|
315
|
+
if(typeof value === "object") {
|
|
316
|
+
return Object.keys(value).length === 0;
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
297
322
|
/***************************************************************
|
|
298
323
|
* The fields of a column that are DECLARED as one thing and
|
|
299
324
|
* STORED as another.
|
|
@@ -495,6 +520,7 @@ export {
|
|
|
495
520
|
REF_SEP,
|
|
496
521
|
parse_fkey_ref,
|
|
497
522
|
as_json,
|
|
523
|
+
is_empty_value,
|
|
498
524
|
col_flags,
|
|
499
525
|
col_has_flag,
|
|
500
526
|
col_hook,
|
package/src/schema_model.test.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
fkey_ref,
|
|
23
23
|
next_order,
|
|
24
24
|
moved_orders,
|
|
25
|
+
is_empty_value,
|
|
25
26
|
} from "./schema_model.js";
|
|
26
27
|
|
|
27
28
|
|
|
@@ -231,3 +232,27 @@ describe("writing helpers", () => {
|
|
|
231
232
|
expect(moved_orders(list)).toEqual([{id: "a", order: 1}, {id: "b", order: 2}]);
|
|
232
233
|
});
|
|
233
234
|
});
|
|
235
|
+
|
|
236
|
+
describe("is_empty_value — what the store answers when a blob was never set", () => {
|
|
237
|
+
test("nothing is nothing", () => {
|
|
238
|
+
expect(is_empty_value(null)).toBe(true);
|
|
239
|
+
expect(is_empty_value(undefined)).toBe(true);
|
|
240
|
+
expect(is_empty_value("")).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("an EMPTY COLLECTION is nothing too — this is the whole point", () => {
|
|
244
|
+
/* A `blob` column that was never set comes back as `{}`, not as
|
|
245
|
+
nothing. Read as a value it puts `'hook': {}` into an exported
|
|
246
|
+
literal, which is a hook with no mapping. */
|
|
247
|
+
expect(is_empty_value({})).toBe(true);
|
|
248
|
+
expect(is_empty_value([])).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("anything with something in it is something", () => {
|
|
252
|
+
expect(is_empty_value({a: 1})).toBe(false);
|
|
253
|
+
expect(is_empty_value(["a"])).toBe(false);
|
|
254
|
+
expect(is_empty_value("x")).toBe(false);
|
|
255
|
+
expect(is_empty_value(0)).toBe(false);
|
|
256
|
+
expect(is_empty_value(false)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
});
|