@yuneta/gobj-ui 7.26.20 → 7.26.22
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/dist/gobj-ui.cjs.js +69 -0
- package/dist/gobj-ui.es.js +69 -0
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +32 -0
- package/src/g6_long_press_clock.test.js +51 -0
- package/src/schema_editor_reload.wiring.test.js +32 -0
- package/src/schema_import.js +22 -0
- package/src/schema_import.test.js +29 -0
- package/src/schema_model.js +24 -0
- package/src/schema_model.test.js +19 -0
- package/src/schema_validate.js +17 -0
- package/src/schema_validate.test.js +33 -0
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -25091,6 +25091,24 @@ function moved_orders(list) {
|
|
|
25091
25091
|
}
|
|
25092
25092
|
return writes;
|
|
25093
25093
|
}
|
|
25094
|
+
/***************************************************************
|
|
25095
|
+
* The TWO name rules of the treedb (SDK 7.26.9):
|
|
25096
|
+
*
|
|
25097
|
+
* a TOPIC name is a directory and a key: not empty, not "."
|
|
25098
|
+
* or "..", no '/' and no '`' (tranger2_name_is_identifier());
|
|
25099
|
+
* a COLUMN name (a column id, a hook, an fkey) is only a key:
|
|
25100
|
+
* not empty and no '`' (treedb_col_name_is_identifier()).
|
|
25101
|
+
*
|
|
25102
|
+
* Said here as in C, so the editor refuses what the treedb will.
|
|
25103
|
+
***************************************************************/
|
|
25104
|
+
function topic_name_breaks_the_rule(name) {
|
|
25105
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25106
|
+
return s === "" || s === "." || s === ".." || s.includes("/") || s.includes("`");
|
|
25107
|
+
}
|
|
25108
|
+
function col_name_breaks_the_rule(name) {
|
|
25109
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25110
|
+
return s === "" || s.includes("`");
|
|
25111
|
+
}
|
|
25094
25112
|
//#endregion
|
|
25095
25113
|
//#region src/treedb_topic_keys.js
|
|
25096
25114
|
/***********************************************************************
|
|
@@ -72670,6 +72688,8 @@ function validate_schema(treedb, options) {
|
|
|
72670
72688
|
if (treedb.topics.length === 0) add("warning", "schema has no topic", "", "", "");
|
|
72671
72689
|
for (let topic of treedb.topics) {
|
|
72672
72690
|
let col_names = topic.cols.map((c) => c.name);
|
|
72691
|
+
if (topic_name_breaks_the_rule(topic.name)) add("error", "topic name breaks the name rule", topic.name, "", topic.name);
|
|
72692
|
+
for (let col of topic.cols) if (col_name_breaks_the_rule(col.name)) add("error", "column name breaks the name rule", topic.name, col.name, col.name);
|
|
72673
72693
|
if (topic.cols.length === 0) add("error", "topic has no column", topic.name, "", "");
|
|
72674
72694
|
let pkey = topic.pkey || "id";
|
|
72675
72695
|
if (col_names.indexOf(pkey) < 0) add("error", "pkey names no column", topic.name, "", pkey);
|
|
@@ -72687,6 +72707,10 @@ function validate_schema(treedb, options) {
|
|
|
72687
72707
|
let hook = col_hook(col.record);
|
|
72688
72708
|
if (!hook || Object.keys(hook).length === 0) add("error", "hook has no mapping", topic.name, col.name, "");
|
|
72689
72709
|
else for (let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
72710
|
+
if (col_name_breaks_the_rule(fkey_col)) {
|
|
72711
|
+
add("error", "column name breaks the name rule", topic.name, col.name, `${child_topic}.${fkey_col}`);
|
|
72712
|
+
continue;
|
|
72713
|
+
}
|
|
72690
72714
|
let child = topic_by_name[child_topic];
|
|
72691
72715
|
if (!child) {
|
|
72692
72716
|
add("error", "hook names no topic", topic.name, col.name, child_topic);
|
|
@@ -73594,6 +73618,24 @@ function plan_import(treedb, incoming, options) {
|
|
|
73594
73618
|
conflicts
|
|
73595
73619
|
};
|
|
73596
73620
|
}
|
|
73621
|
+
for (let in_topic of incoming.topics) {
|
|
73622
|
+
let name = (in_topic || {}).id;
|
|
73623
|
+
if (name && topic_name_breaks_the_rule(name)) conflicts.push({
|
|
73624
|
+
code: "topic name breaks the name rule",
|
|
73625
|
+
topic: name,
|
|
73626
|
+
col: ""
|
|
73627
|
+
});
|
|
73628
|
+
for (let in_col of incoming_cols(in_topic || {})) if (col_name_breaks_the_rule(in_col.name)) conflicts.push({
|
|
73629
|
+
code: "column name breaks the name rule",
|
|
73630
|
+
topic: name || "",
|
|
73631
|
+
col: in_col.name
|
|
73632
|
+
});
|
|
73633
|
+
}
|
|
73634
|
+
if (conflicts.length > 0) return {
|
|
73635
|
+
writes,
|
|
73636
|
+
summary,
|
|
73637
|
+
conflicts
|
|
73638
|
+
};
|
|
73597
73639
|
let stored_topics = {};
|
|
73598
73640
|
for (let topic of treedb && treedb.topics || []) stored_topics[topic.name] = topic;
|
|
73599
73641
|
let seen_topics = {};
|
|
@@ -74324,6 +74366,7 @@ var PRIVATE_DATA$1 = {
|
|
|
74324
74366
|
dialog: null,
|
|
74325
74367
|
dialog_gen: 0,
|
|
74326
74368
|
dialog_kind: "",
|
|
74369
|
+
dialog_content: null,
|
|
74327
74370
|
save_queue: null,
|
|
74328
74371
|
save_return: "",
|
|
74329
74372
|
save_wrote: false,
|
|
@@ -74386,6 +74429,19 @@ function mt_destroy$1(gobj) {
|
|
|
74386
74429
|
/***************************
|
|
74387
74430
|
* Local Methods
|
|
74388
74431
|
***************************/
|
|
74432
|
+
/************************************************************
|
|
74433
|
+
* A name the treedb refuses (the two name rules of
|
|
74434
|
+
* schema_model.js), said at once: the error carries its key as
|
|
74435
|
+
* a LITERAL (so a consumer's validate-locales sees it), and the
|
|
74436
|
+
* name field is marked aria-invalid for a reader.
|
|
74437
|
+
************************************************************/
|
|
74438
|
+
function refuse_bad_name(gobj, $message) {
|
|
74439
|
+
let priv = gobj.priv;
|
|
74440
|
+
let $value = priv.dialog_content ? priv.dialog_content.querySelector("[data-name=\"value\"]") : null;
|
|
74441
|
+
if ($value) $value.setAttribute("aria-invalid", "true");
|
|
74442
|
+
yui_shell_show_error(yui_shell_of(gobj), $message, { t: i18next.t });
|
|
74443
|
+
return -1;
|
|
74444
|
+
}
|
|
74389
74445
|
/***************************************************************
|
|
74390
74446
|
* The frame: a toolbar that never changes place, a notice line
|
|
74391
74447
|
* for what the whole view has to say, and one body the screens
|
|
@@ -75849,6 +75905,7 @@ function close_dialog(gobj) {
|
|
|
75849
75905
|
let dialog = priv.dialog;
|
|
75850
75906
|
priv.dialog = null;
|
|
75851
75907
|
priv.dialog_kind = "";
|
|
75908
|
+
priv.dialog_content = null;
|
|
75852
75909
|
priv.export_view = null;
|
|
75853
75910
|
try {
|
|
75854
75911
|
dialog.close();
|
|
@@ -75861,6 +75918,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75861
75918
|
close_dialog(gobj);
|
|
75862
75919
|
priv.dialog_gen = priv.model_gen;
|
|
75863
75920
|
priv.dialog_kind = logical;
|
|
75921
|
+
priv.dialog_content = $content;
|
|
75864
75922
|
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
75865
75923
|
dialog: true,
|
|
75866
75924
|
title,
|
|
@@ -75870,6 +75928,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75870
75928
|
on_close: () => {
|
|
75871
75929
|
priv.dialog = null;
|
|
75872
75930
|
priv.dialog_kind = "";
|
|
75931
|
+
priv.dialog_content = null;
|
|
75873
75932
|
priv.export_view = null;
|
|
75874
75933
|
}
|
|
75875
75934
|
});
|
|
@@ -77898,6 +77957,11 @@ function ac_save_column(gobj, event, kw, src) {
|
|
|
77898
77957
|
yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", { t: i18next.t });
|
|
77899
77958
|
return -1;
|
|
77900
77959
|
}
|
|
77960
|
+
if (kw.creating && col_name_breaks_the_rule(String(values.value).trim())) return refuse_bad_name(gobj, (0, _yuneta_gobj_js.createElement2)([
|
|
77961
|
+
"span",
|
|
77962
|
+
{ i18n: "invalid column name" },
|
|
77963
|
+
(0, i18next.t)("invalid column name")
|
|
77964
|
+
]));
|
|
77901
77965
|
if (kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
|
|
77902
77966
|
yui_shell_show_error(yui_shell_of(gobj), "that column is already there", { t: i18next.t });
|
|
77903
77967
|
return -1;
|
|
@@ -78152,6 +78216,11 @@ function ac_save_topic(gobj, event, kw, src) {
|
|
|
78152
78216
|
yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", { t: i18next.t });
|
|
78153
78217
|
return -1;
|
|
78154
78218
|
}
|
|
78219
|
+
if (topic_name_breaks_the_rule(name)) return refuse_bad_name(gobj, (0, _yuneta_gobj_js.createElement2)([
|
|
78220
|
+
"span",
|
|
78221
|
+
{ i18n: "invalid topic name" },
|
|
78222
|
+
(0, i18next.t)("invalid topic name")
|
|
78223
|
+
]));
|
|
78155
78224
|
if (find_topic(priv.model, priv.treedb_id, name)) {
|
|
78156
78225
|
yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", { t: i18next.t });
|
|
78157
78226
|
return -1;
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -25086,6 +25086,24 @@ function moved_orders(list) {
|
|
|
25086
25086
|
}
|
|
25087
25087
|
return writes;
|
|
25088
25088
|
}
|
|
25089
|
+
/***************************************************************
|
|
25090
|
+
* The TWO name rules of the treedb (SDK 7.26.9):
|
|
25091
|
+
*
|
|
25092
|
+
* a TOPIC name is a directory and a key: not empty, not "."
|
|
25093
|
+
* or "..", no '/' and no '`' (tranger2_name_is_identifier());
|
|
25094
|
+
* a COLUMN name (a column id, a hook, an fkey) is only a key:
|
|
25095
|
+
* not empty and no '`' (treedb_col_name_is_identifier()).
|
|
25096
|
+
*
|
|
25097
|
+
* Said here as in C, so the editor refuses what the treedb will.
|
|
25098
|
+
***************************************************************/
|
|
25099
|
+
function topic_name_breaks_the_rule(name) {
|
|
25100
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25101
|
+
return s === "" || s === "." || s === ".." || s.includes("/") || s.includes("`");
|
|
25102
|
+
}
|
|
25103
|
+
function col_name_breaks_the_rule(name) {
|
|
25104
|
+
let s = String(name === void 0 || name === null ? "" : name);
|
|
25105
|
+
return s === "" || s.includes("`");
|
|
25106
|
+
}
|
|
25089
25107
|
//#endregion
|
|
25090
25108
|
//#region src/treedb_topic_keys.js
|
|
25091
25109
|
/***********************************************************************
|
|
@@ -72665,6 +72683,8 @@ function validate_schema(treedb, options) {
|
|
|
72665
72683
|
if (treedb.topics.length === 0) add("warning", "schema has no topic", "", "", "");
|
|
72666
72684
|
for (let topic of treedb.topics) {
|
|
72667
72685
|
let col_names = topic.cols.map((c) => c.name);
|
|
72686
|
+
if (topic_name_breaks_the_rule(topic.name)) add("error", "topic name breaks the name rule", topic.name, "", topic.name);
|
|
72687
|
+
for (let col of topic.cols) if (col_name_breaks_the_rule(col.name)) add("error", "column name breaks the name rule", topic.name, col.name, col.name);
|
|
72668
72688
|
if (topic.cols.length === 0) add("error", "topic has no column", topic.name, "", "");
|
|
72669
72689
|
let pkey = topic.pkey || "id";
|
|
72670
72690
|
if (col_names.indexOf(pkey) < 0) add("error", "pkey names no column", topic.name, "", pkey);
|
|
@@ -72682,6 +72702,10 @@ function validate_schema(treedb, options) {
|
|
|
72682
72702
|
let hook = col_hook(col.record);
|
|
72683
72703
|
if (!hook || Object.keys(hook).length === 0) add("error", "hook has no mapping", topic.name, col.name, "");
|
|
72684
72704
|
else for (let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
72705
|
+
if (col_name_breaks_the_rule(fkey_col)) {
|
|
72706
|
+
add("error", "column name breaks the name rule", topic.name, col.name, `${child_topic}.${fkey_col}`);
|
|
72707
|
+
continue;
|
|
72708
|
+
}
|
|
72685
72709
|
let child = topic_by_name[child_topic];
|
|
72686
72710
|
if (!child) {
|
|
72687
72711
|
add("error", "hook names no topic", topic.name, col.name, child_topic);
|
|
@@ -73589,6 +73613,24 @@ function plan_import(treedb, incoming, options) {
|
|
|
73589
73613
|
conflicts
|
|
73590
73614
|
};
|
|
73591
73615
|
}
|
|
73616
|
+
for (let in_topic of incoming.topics) {
|
|
73617
|
+
let name = (in_topic || {}).id;
|
|
73618
|
+
if (name && topic_name_breaks_the_rule(name)) conflicts.push({
|
|
73619
|
+
code: "topic name breaks the name rule",
|
|
73620
|
+
topic: name,
|
|
73621
|
+
col: ""
|
|
73622
|
+
});
|
|
73623
|
+
for (let in_col of incoming_cols(in_topic || {})) if (col_name_breaks_the_rule(in_col.name)) conflicts.push({
|
|
73624
|
+
code: "column name breaks the name rule",
|
|
73625
|
+
topic: name || "",
|
|
73626
|
+
col: in_col.name
|
|
73627
|
+
});
|
|
73628
|
+
}
|
|
73629
|
+
if (conflicts.length > 0) return {
|
|
73630
|
+
writes,
|
|
73631
|
+
summary,
|
|
73632
|
+
conflicts
|
|
73633
|
+
};
|
|
73592
73634
|
let stored_topics = {};
|
|
73593
73635
|
for (let topic of treedb && treedb.topics || []) stored_topics[topic.name] = topic;
|
|
73594
73636
|
let seen_topics = {};
|
|
@@ -74319,6 +74361,7 @@ var PRIVATE_DATA$1 = {
|
|
|
74319
74361
|
dialog: null,
|
|
74320
74362
|
dialog_gen: 0,
|
|
74321
74363
|
dialog_kind: "",
|
|
74364
|
+
dialog_content: null,
|
|
74322
74365
|
save_queue: null,
|
|
74323
74366
|
save_return: "",
|
|
74324
74367
|
save_wrote: false,
|
|
@@ -74381,6 +74424,19 @@ function mt_destroy$1(gobj) {
|
|
|
74381
74424
|
/***************************
|
|
74382
74425
|
* Local Methods
|
|
74383
74426
|
***************************/
|
|
74427
|
+
/************************************************************
|
|
74428
|
+
* A name the treedb refuses (the two name rules of
|
|
74429
|
+
* schema_model.js), said at once: the error carries its key as
|
|
74430
|
+
* a LITERAL (so a consumer's validate-locales sees it), and the
|
|
74431
|
+
* name field is marked aria-invalid for a reader.
|
|
74432
|
+
************************************************************/
|
|
74433
|
+
function refuse_bad_name(gobj, $message) {
|
|
74434
|
+
let priv = gobj.priv;
|
|
74435
|
+
let $value = priv.dialog_content ? priv.dialog_content.querySelector("[data-name=\"value\"]") : null;
|
|
74436
|
+
if ($value) $value.setAttribute("aria-invalid", "true");
|
|
74437
|
+
yui_shell_show_error(yui_shell_of(gobj), $message, { t });
|
|
74438
|
+
return -1;
|
|
74439
|
+
}
|
|
74384
74440
|
/***************************************************************
|
|
74385
74441
|
* The frame: a toolbar that never changes place, a notice line
|
|
74386
74442
|
* for what the whole view has to say, and one body the screens
|
|
@@ -75844,6 +75900,7 @@ function close_dialog(gobj) {
|
|
|
75844
75900
|
let dialog = priv.dialog;
|
|
75845
75901
|
priv.dialog = null;
|
|
75846
75902
|
priv.dialog_kind = "";
|
|
75903
|
+
priv.dialog_content = null;
|
|
75847
75904
|
priv.export_view = null;
|
|
75848
75905
|
try {
|
|
75849
75906
|
dialog.close();
|
|
@@ -75856,6 +75913,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75856
75913
|
close_dialog(gobj);
|
|
75857
75914
|
priv.dialog_gen = priv.model_gen;
|
|
75858
75915
|
priv.dialog_kind = logical;
|
|
75916
|
+
priv.dialog_content = $content;
|
|
75859
75917
|
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
75860
75918
|
dialog: true,
|
|
75861
75919
|
title,
|
|
@@ -75865,6 +75923,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
|
|
|
75865
75923
|
on_close: () => {
|
|
75866
75924
|
priv.dialog = null;
|
|
75867
75925
|
priv.dialog_kind = "";
|
|
75926
|
+
priv.dialog_content = null;
|
|
75868
75927
|
priv.export_view = null;
|
|
75869
75928
|
}
|
|
75870
75929
|
});
|
|
@@ -77893,6 +77952,11 @@ function ac_save_column(gobj, event, kw, src) {
|
|
|
77893
77952
|
yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", { t });
|
|
77894
77953
|
return -1;
|
|
77895
77954
|
}
|
|
77955
|
+
if (kw.creating && col_name_breaks_the_rule(String(values.value).trim())) return refuse_bad_name(gobj, createElement2([
|
|
77956
|
+
"span",
|
|
77957
|
+
{ i18n: "invalid column name" },
|
|
77958
|
+
t("invalid column name")
|
|
77959
|
+
]));
|
|
77896
77960
|
if (kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
|
|
77897
77961
|
yui_shell_show_error(yui_shell_of(gobj), "that column is already there", { t });
|
|
77898
77962
|
return -1;
|
|
@@ -78147,6 +78211,11 @@ function ac_save_topic(gobj, event, kw, src) {
|
|
|
78147
78211
|
yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", { t });
|
|
78148
78212
|
return -1;
|
|
78149
78213
|
}
|
|
78214
|
+
if (topic_name_breaks_the_rule(name)) return refuse_bad_name(gobj, createElement2([
|
|
78215
|
+
"span",
|
|
78216
|
+
{ i18n: "invalid topic name" },
|
|
78217
|
+
t("invalid topic name")
|
|
78218
|
+
]));
|
|
78150
78219
|
if (find_topic(priv.model, priv.treedb_id, name)) {
|
|
78151
78220
|
yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", { t });
|
|
78152
78221
|
return -1;
|
package/package.json
CHANGED
|
@@ -140,6 +140,8 @@ import {
|
|
|
140
140
|
parse_fkey_ref,
|
|
141
141
|
next_order,
|
|
142
142
|
moved_orders,
|
|
143
|
+
topic_name_breaks_the_rule,
|
|
144
|
+
col_name_breaks_the_rule,
|
|
143
145
|
} from "./schema_model.js";
|
|
144
146
|
import {validate_schema, validate_model, has_errors} from "./schema_validate.js";
|
|
145
147
|
import {topic_descs} from "./schema_descs.js";
|
|
@@ -250,6 +252,7 @@ let PRIVATE_DATA = {
|
|
|
250
252
|
dialog: null,
|
|
251
253
|
dialog_gen: 0, /* the model_gen the open dialog was built on */
|
|
252
254
|
dialog_kind: "", /* the logical class of the open dialog */
|
|
255
|
+
dialog_content: null, /* the content of the open dialog (its name field is marked) */
|
|
253
256
|
|
|
254
257
|
/* A write, or a plan of them, in flight */
|
|
255
258
|
save_queue: null,
|
|
@@ -361,6 +364,24 @@ function mt_destroy(gobj)
|
|
|
361
364
|
|
|
362
365
|
|
|
363
366
|
|
|
367
|
+
/************************************************************
|
|
368
|
+
* A name the treedb refuses (the two name rules of
|
|
369
|
+
* schema_model.js), said at once: the error carries its key as
|
|
370
|
+
* a LITERAL (so a consumer's validate-locales sees it), and the
|
|
371
|
+
* name field is marked aria-invalid for a reader.
|
|
372
|
+
************************************************************/
|
|
373
|
+
function refuse_bad_name(gobj, $message)
|
|
374
|
+
{
|
|
375
|
+
let priv = gobj.priv;
|
|
376
|
+
let $value = priv.dialog_content?
|
|
377
|
+
priv.dialog_content.querySelector('[data-name="value"]') : null;
|
|
378
|
+
if($value) {
|
|
379
|
+
$value.setAttribute("aria-invalid", "true");
|
|
380
|
+
}
|
|
381
|
+
yui_shell_show_error(yui_shell_of(gobj), $message, {t: t});
|
|
382
|
+
return -1;
|
|
383
|
+
}
|
|
384
|
+
|
|
364
385
|
/***************************************************************
|
|
365
386
|
* The frame: a toolbar that never changes place, a notice line
|
|
366
387
|
* for what the whole view has to say, and one body the screens
|
|
@@ -1858,6 +1879,7 @@ function close_dialog(gobj)
|
|
|
1858
1879
|
let dialog = priv.dialog;
|
|
1859
1880
|
priv.dialog = null;
|
|
1860
1881
|
priv.dialog_kind = "";
|
|
1882
|
+
priv.dialog_content = null;
|
|
1861
1883
|
priv.export_view = null;
|
|
1862
1884
|
try {
|
|
1863
1885
|
dialog.close();
|
|
@@ -1873,6 +1895,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
|
|
|
1873
1895
|
close_dialog(gobj);
|
|
1874
1896
|
priv.dialog_gen = priv.model_gen;
|
|
1875
1897
|
priv.dialog_kind = logical;
|
|
1898
|
+
priv.dialog_content = $content;
|
|
1876
1899
|
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
1877
1900
|
dialog: true,
|
|
1878
1901
|
title: title,
|
|
@@ -1882,6 +1905,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
|
|
|
1882
1905
|
on_close: () => {
|
|
1883
1906
|
priv.dialog = null;
|
|
1884
1907
|
priv.dialog_kind = "";
|
|
1908
|
+
priv.dialog_content = null;
|
|
1885
1909
|
priv.export_view = null;
|
|
1886
1910
|
}
|
|
1887
1911
|
});
|
|
@@ -4026,6 +4050,10 @@ function ac_save_column(gobj, event, kw, src)
|
|
|
4026
4050
|
yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", {t: t});
|
|
4027
4051
|
return -1;
|
|
4028
4052
|
}
|
|
4053
|
+
if(kw.creating && col_name_breaks_the_rule(String(values.value).trim())) {
|
|
4054
|
+
return refuse_bad_name(gobj, createElement2(
|
|
4055
|
+
["span", {i18n: "invalid column name"}, t("invalid column name")]));
|
|
4056
|
+
}
|
|
4029
4057
|
if(kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
|
|
4030
4058
|
yui_shell_show_error(yui_shell_of(gobj), "that column is already there", {t: t});
|
|
4031
4059
|
return -1;
|
|
@@ -4333,6 +4361,10 @@ function ac_save_topic(gobj, event, kw, src)
|
|
|
4333
4361
|
yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", {t: t});
|
|
4334
4362
|
return -1;
|
|
4335
4363
|
}
|
|
4364
|
+
if(topic_name_breaks_the_rule(name)) {
|
|
4365
|
+
return refuse_bad_name(gobj, createElement2(
|
|
4366
|
+
["span", {i18n: "invalid topic name"}, t("invalid topic name")]));
|
|
4367
|
+
}
|
|
4336
4368
|
if(find_topic(priv.model, priv.treedb_id, name)) {
|
|
4337
4369
|
yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", {t: t});
|
|
4338
4370
|
return -1;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* g6_long_press_clock.test.js
|
|
3
|
+
*
|
|
4
|
+
* The long press of the G6 graphs is measured on a MONOTONIC clock
|
|
5
|
+
* (yunovatios REV-CLOUD.md V11.4 / V12.4). press_arbiter.test.js
|
|
6
|
+
* injects the time into classify_press(), so it passes whatever
|
|
7
|
+
* clock the handler reads; this drives the handler itself, with the
|
|
8
|
+
* wall clock set back an hour in the middle of the press -- an NTP
|
|
9
|
+
* step -- while the monotonic one moves on as a finger does.
|
|
10
|
+
*
|
|
11
|
+
* Copyright (c) 2026, ArtGins.
|
|
12
|
+
* All Rights Reserved.
|
|
13
|
+
***********************************************************************/
|
|
14
|
+
import {describe, test, expect, vi, afterEach} from "vitest";
|
|
15
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
16
|
+
|
|
17
|
+
install_dom_double();
|
|
18
|
+
|
|
19
|
+
const {CommonEvent} = await import("@antv/g6");
|
|
20
|
+
const {install_long_press_contextmenu} = await import("./g6_touch_gestures.js");
|
|
21
|
+
const {LONG_PRESS_MS} = await import("./press_arbiter.js");
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("the long press of a G6 graph and the clocks", () => {
|
|
28
|
+
test("a wall clock set back mid-press does not turn a long press into a tap", () => {
|
|
29
|
+
const handlers = {};
|
|
30
|
+
const emitted = [];
|
|
31
|
+
const graph = {
|
|
32
|
+
on(name, fn) { handlers[name] = fn; },
|
|
33
|
+
off() {},
|
|
34
|
+
emit(name) { emitted.push(name); },
|
|
35
|
+
};
|
|
36
|
+
let mono = 1000;
|
|
37
|
+
let wall = 1_800_000_000_000;
|
|
38
|
+
vi.spyOn(performance, "now").mockImplementation(() => mono);
|
|
39
|
+
vi.spyOn(Date, "now").mockImplementation(() => wall);
|
|
40
|
+
|
|
41
|
+
install_long_press_contextmenu(graph, {});
|
|
42
|
+
handlers[CommonEvent.POINTER_DOWN]({
|
|
43
|
+
pointerType: "touch", client: {x: 10, y: 10}, targetType: "node"
|
|
44
|
+
});
|
|
45
|
+
mono += LONG_PRESS_MS + 100; /* the finger stays long enough */
|
|
46
|
+
wall -= 3600 * 1000; /* and the wall clock steps back */
|
|
47
|
+
handlers[CommonEvent.POINTER_UP]({client: {x: 10, y: 10}});
|
|
48
|
+
|
|
49
|
+
expect(emitted).toContain(CommonEvent.CONTEXT_MENU);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -1716,3 +1716,35 @@ describe("the reason a load failed", () => {
|
|
|
1716
1716
|
expect($detail.getAttribute("data-i18n")).toBe("cannot reach the treedb");
|
|
1717
1717
|
});
|
|
1718
1718
|
});
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
/*
|
|
1722
|
+
* The two name rules (REV-CLOUD V14.2): a name the treedb refuses is
|
|
1723
|
+
* said at once, with its key, and nothing is written; a '/' in a column
|
|
1724
|
+
* is not refused (a column is a key, not a directory).
|
|
1725
|
+
*/
|
|
1726
|
+
describe("names the treedb refuses", () => {
|
|
1727
|
+
test("a column x`y is refused with its key; kids/all is written", () => {
|
|
1728
|
+
const {editor, host} = build("names_col", "db/users");
|
|
1729
|
+
gobj_send_event(editor, "EV_SAVE_COLUMN", {
|
|
1730
|
+
creating: true, topic: "users", values: {value: "x`y", type: "string"}
|
|
1731
|
+
}, host);
|
|
1732
|
+
expect(take("update-node").length).toBe(0);
|
|
1733
|
+
expect(shown.length).toBe(1);
|
|
1734
|
+
expect(shown[0].getAttribute("data-i18n")).toBe("invalid column name");
|
|
1735
|
+
start_a_write(editor, host, "kids/all");
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
test("a topic a/b or .. is refused with its key", () => {
|
|
1739
|
+
const {editor, host} = build("names_topic", "db");
|
|
1740
|
+
for(const name of ["a/b", ".."]) {
|
|
1741
|
+
shown.length = 0;
|
|
1742
|
+
gobj_send_event(editor, "EV_SAVE_TOPIC", {
|
|
1743
|
+
creating: true, topic: "", values: {value: name}
|
|
1744
|
+
}, host);
|
|
1745
|
+
expect(take("update-node").length).toBe(0);
|
|
1746
|
+
expect(shown.length).toBe(1);
|
|
1747
|
+
expect(shown[0].getAttribute("data-i18n")).toBe("invalid topic name");
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
});
|
package/src/schema_import.js
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
as_json,
|
|
32
32
|
is_empty_value,
|
|
33
33
|
fkey_ref,
|
|
34
|
+
topic_name_breaks_the_rule,
|
|
35
|
+
col_name_breaks_the_rule,
|
|
34
36
|
} from "./schema_model.js";
|
|
35
37
|
|
|
36
38
|
|
|
@@ -204,6 +206,26 @@ function plan_import(treedb, incoming, options)
|
|
|
204
206
|
return {writes: writes, summary: summary, conflicts: conflicts};
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
/*
|
|
210
|
+
* Names the treedb refuses (the two name rules of schema_model.js)
|
|
211
|
+
* stop the plan BEFORE any write: run through, the backend refused
|
|
212
|
+
* them half way, with the writes before them already done.
|
|
213
|
+
*/
|
|
214
|
+
for(let in_topic of incoming.topics) {
|
|
215
|
+
let name = (in_topic || {}).id;
|
|
216
|
+
if(name && topic_name_breaks_the_rule(name)) {
|
|
217
|
+
conflicts.push({code: "topic name breaks the name rule", topic: name, col: ""});
|
|
218
|
+
}
|
|
219
|
+
for(let in_col of incoming_cols(in_topic || {})) {
|
|
220
|
+
if(col_name_breaks_the_rule(in_col.name)) {
|
|
221
|
+
conflicts.push({code: "column name breaks the name rule", topic: name || "", col: in_col.name});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if(conflicts.length > 0) {
|
|
226
|
+
return {writes: writes, summary: summary, conflicts: conflicts};
|
|
227
|
+
}
|
|
228
|
+
|
|
207
229
|
let stored_topics = {};
|
|
208
230
|
for(let topic of ((treedb && treedb.topics) || [])) {
|
|
209
231
|
stored_topics[topic.name] = topic;
|
|
@@ -253,3 +253,32 @@ describe("the empty collections the store answers with", () => {
|
|
|
253
253
|
expect(plan_import(stored, incoming).writes).toEqual([]);
|
|
254
254
|
});
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
describe("names the treedb refuses stop the plan before any write (REV-CLOUD V14.2)", () => {
|
|
259
|
+
test("a topic a/b, a topic .., a column x`y: conflicts, no writes", () => {
|
|
260
|
+
let plan = plan_import(null, {
|
|
261
|
+
id: "db", schema_version: "1",
|
|
262
|
+
topics: [
|
|
263
|
+
{id: "a/b", pkey: "id", cols: {id: {id: "id", type: "string"}}},
|
|
264
|
+
{id: "..", pkey: "id", cols: {id: {id: "id", type: "string"}}},
|
|
265
|
+
{id: "ok", pkey: "id", cols: {"id": {id: "id", type: "string"},
|
|
266
|
+
"x`y": {id: "x`y", type: "string"}}},
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
expect(plan.writes).toEqual([]);
|
|
270
|
+
let codes = plan.conflicts.map((c) => c.code);
|
|
271
|
+
expect(codes.filter((c) => c === "topic name breaks the name rule").length).toBe(2);
|
|
272
|
+
expect(codes).toContain("column name breaks the name rule");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("a '/' in a column and a leading '.' in a topic are imported", () => {
|
|
276
|
+
let plan = plan_import(null, {
|
|
277
|
+
id: "db", schema_version: "1",
|
|
278
|
+
topics: [{id: ".hidden", pkey: "id",
|
|
279
|
+
cols: {"id": {id: "id", type: "string"}, "kids/all": {id: "kids/all", type: "dict"}}}]
|
|
280
|
+
});
|
|
281
|
+
expect(plan.conflicts).toEqual([]);
|
|
282
|
+
expect(plan.writes.length).toBeGreaterThan(0);
|
|
283
|
+
});
|
|
284
|
+
});
|
package/src/schema_model.js
CHANGED
|
@@ -515,8 +515,32 @@ function moved_orders(list)
|
|
|
515
515
|
return writes;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
/***************************************************************
|
|
519
|
+
* The TWO name rules of the treedb (SDK 7.26.9):
|
|
520
|
+
*
|
|
521
|
+
* a TOPIC name is a directory and a key: not empty, not "."
|
|
522
|
+
* or "..", no '/' and no '`' (tranger2_name_is_identifier());
|
|
523
|
+
* a COLUMN name (a column id, a hook, an fkey) is only a key:
|
|
524
|
+
* not empty and no '`' (treedb_col_name_is_identifier()).
|
|
525
|
+
*
|
|
526
|
+
* Said here as in C, so the editor refuses what the treedb will.
|
|
527
|
+
***************************************************************/
|
|
528
|
+
function topic_name_breaks_the_rule(name)
|
|
529
|
+
{
|
|
530
|
+
let s = String(name === undefined || name === null ? "" : name);
|
|
531
|
+
return s === "" || s === "." || s === ".." || s.includes("/") || s.includes("`");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function col_name_breaks_the_rule(name)
|
|
535
|
+
{
|
|
536
|
+
let s = String(name === undefined || name === null ? "" : name);
|
|
537
|
+
return s === "" || s.includes("`");
|
|
538
|
+
}
|
|
539
|
+
|
|
518
540
|
|
|
519
541
|
export {
|
|
542
|
+
topic_name_breaks_the_rule,
|
|
543
|
+
col_name_breaks_the_rule,
|
|
520
544
|
DEFAULT_ORDER,
|
|
521
545
|
REF_SEP,
|
|
522
546
|
parse_fkey_ref,
|
package/src/schema_model.test.js
CHANGED
|
@@ -256,3 +256,22 @@ describe("is_empty_value — what the store answers when a blob was never set",
|
|
|
256
256
|
expect(is_empty_value(false)).toBe(false);
|
|
257
257
|
});
|
|
258
258
|
});
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
describe("the two name rules", async () => {
|
|
262
|
+
const { topic_name_breaks_the_rule, col_name_breaks_the_rule } = await import("./schema_model.js");
|
|
263
|
+
test("as C: topic = tranger2_name_is_identifier(), column = treedb_col_name_is_identifier()", () => {
|
|
264
|
+
for(let n of ["", ".", "..", "a/b", "a`b"]) {
|
|
265
|
+
expect(topic_name_breaks_the_rule(n)).toBe(true);
|
|
266
|
+
}
|
|
267
|
+
for(let n of ["users", ".hidden", "a.b"]) {
|
|
268
|
+
expect(topic_name_breaks_the_rule(n)).toBe(false);
|
|
269
|
+
}
|
|
270
|
+
for(let n of ["", "a`b"]) {
|
|
271
|
+
expect(col_name_breaks_the_rule(n)).toBe(true);
|
|
272
|
+
}
|
|
273
|
+
for(let n of ["id", "kids/all", "..", ".x"]) {
|
|
274
|
+
expect(col_name_breaks_the_rule(n)).toBe(false);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
package/src/schema_validate.js
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
col_hook,
|
|
33
33
|
col_enum,
|
|
34
34
|
topic_pkey2s,
|
|
35
|
+
topic_name_breaks_the_rule,
|
|
36
|
+
col_name_breaks_the_rule,
|
|
35
37
|
} from "./schema_model.js";
|
|
36
38
|
|
|
37
39
|
|
|
@@ -100,6 +102,16 @@ function validate_schema(treedb, options)
|
|
|
100
102
|
for(let topic of treedb.topics) {
|
|
101
103
|
let col_names = topic.cols.map(c => c.name);
|
|
102
104
|
|
|
105
|
+
/* A name the treedb refuses: the topic is left out when it opens. */
|
|
106
|
+
if(topic_name_breaks_the_rule(topic.name)) {
|
|
107
|
+
add("error", "topic name breaks the name rule", topic.name, "", topic.name);
|
|
108
|
+
}
|
|
109
|
+
for(let col of topic.cols) {
|
|
110
|
+
if(col_name_breaks_the_rule(col.name)) {
|
|
111
|
+
add("error", "column name breaks the name rule", topic.name, col.name, col.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
if(topic.cols.length === 0) {
|
|
104
116
|
add("error", "topic has no column", topic.name, "", "");
|
|
105
117
|
}
|
|
@@ -143,6 +155,11 @@ function validate_schema(treedb, options)
|
|
|
143
155
|
add("error", "hook has no mapping", topic.name, col.name, "");
|
|
144
156
|
} else {
|
|
145
157
|
for(let [child_topic, fkey_col] of Object.entries(hook)) {
|
|
158
|
+
if(col_name_breaks_the_rule(fkey_col)) {
|
|
159
|
+
add("error", "column name breaks the name rule", topic.name, col.name,
|
|
160
|
+
`${child_topic}.${fkey_col}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
146
163
|
let child = topic_by_name[child_topic];
|
|
147
164
|
if(!child) {
|
|
148
165
|
add("error", "hook names no topic", topic.name, col.name,
|
|
@@ -287,3 +287,36 @@ describe("only one hook may name a fkey column", () => {
|
|
|
287
287
|
expect(dup[0].severity).toBe("error");
|
|
288
288
|
});
|
|
289
289
|
});
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
describe("the two name rules (REV-CLOUD V14.2)", () => {
|
|
293
|
+
test("a column with the delimiter is an error; a '/' in a column is fine", () => {
|
|
294
|
+
let bad = validate_schema(schema({extra_col:
|
|
295
|
+
{id: "db.users.x`y", value: "x`y", order: 3, type: "string",
|
|
296
|
+
flag: ["persistent"], topics: ["topics^db.users^cols"]}}));
|
|
297
|
+
expect(codes(bad)).toContain("column name breaks the name rule");
|
|
298
|
+
let fine = validate_schema(schema({extra_col:
|
|
299
|
+
{id: "db.users.kids/all", value: "kids/all", order: 3, type: "string",
|
|
300
|
+
flag: ["persistent"], topics: ["topics^db.users^cols"]}}));
|
|
301
|
+
expect(codes(fine)).not.toContain("column name breaks the name rule");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("a topic named with '/' or '..' is an error; a leading '.' is not", () => {
|
|
305
|
+
for(let [name, wanted] of [["a/b", true], ["..", true], [".hidden", false]]) {
|
|
306
|
+
let treedb = build_schema_model({
|
|
307
|
+
treedbs: [{id: "db", schema_version: 1}],
|
|
308
|
+
topics: [{id: `db.${name}`, value: name, order: 1, pkey: "id",
|
|
309
|
+
topic_version: 1, treedbs: ["treedbs^db^topics"]}],
|
|
310
|
+
cols: [{id: `db.${name}.id`, value: "id", order: 1, type: "string",
|
|
311
|
+
flag: ["persistent"], topics: [`topics^db.${name}^cols`]}],
|
|
312
|
+
}).treedbs[0];
|
|
313
|
+
expect(codes(validate_schema(treedb)).includes("topic name breaks the name rule"))
|
|
314
|
+
.toBe(wanted);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("a hook mapping to an fkey column with the delimiter is an error", () => {
|
|
319
|
+
let found = validate_schema(schema({hook: {users: "dep`artments"}}));
|
|
320
|
+
expect(codes(found)).toContain("column name breaks the name rule");
|
|
321
|
+
});
|
|
322
|
+
});
|