@yuneta/gobj-ui 7.26.21 → 7.26.23

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.
@@ -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,52 @@ function plan_import(treedb, incoming, options) {
73594
73618
  conflicts
73595
73619
  };
73596
73620
  }
73621
+ if (topic_name_breaks_the_rule(treedb_id)) conflicts.push({
73622
+ code: "treedb name breaks the name rule",
73623
+ topic: "",
73624
+ col: ""
73625
+ });
73626
+ for (let in_topic of incoming.topics) {
73627
+ let name = (in_topic || {}).id;
73628
+ if (name && topic_name_breaks_the_rule(name)) conflicts.push({
73629
+ code: "topic name breaks the name rule",
73630
+ topic: name,
73631
+ col: ""
73632
+ });
73633
+ let cols = (in_topic || {}).cols;
73634
+ if (Array.isArray(cols)) {
73635
+ for (let c of cols) if (!c || !c.id) conflicts.push({
73636
+ code: "column has no name",
73637
+ topic: name || "",
73638
+ col: ""
73639
+ });
73640
+ }
73641
+ for (let in_col of incoming_cols(in_topic || {})) {
73642
+ if (col_name_breaks_the_rule(in_col.name)) conflicts.push({
73643
+ code: "column name breaks the name rule",
73644
+ topic: name || "",
73645
+ col: in_col.name
73646
+ });
73647
+ let hook = (in_col.col || {}).hook;
73648
+ if (hook && typeof hook === "object" && !Array.isArray(hook)) for (let [child_topic, fkey_col] of Object.entries(hook)) {
73649
+ if (topic_name_breaks_the_rule(child_topic)) conflicts.push({
73650
+ code: "topic name breaks the name rule",
73651
+ topic: name || "",
73652
+ col: in_col.name
73653
+ });
73654
+ if (col_name_breaks_the_rule(fkey_col)) conflicts.push({
73655
+ code: "column name breaks the name rule",
73656
+ topic: name || "",
73657
+ col: in_col.name
73658
+ });
73659
+ }
73660
+ }
73661
+ }
73662
+ if (conflicts.length > 0) return {
73663
+ writes,
73664
+ summary,
73665
+ conflicts
73666
+ };
73597
73667
  let stored_topics = {};
73598
73668
  for (let topic of treedb && treedb.topics || []) stored_topics[topic.name] = topic;
73599
73669
  let seen_topics = {};
@@ -74324,6 +74394,7 @@ var PRIVATE_DATA$1 = {
74324
74394
  dialog: null,
74325
74395
  dialog_gen: 0,
74326
74396
  dialog_kind: "",
74397
+ dialog_content: null,
74327
74398
  save_queue: null,
74328
74399
  save_return: "",
74329
74400
  save_wrote: false,
@@ -74387,14 +74458,27 @@ function mt_destroy$1(gobj) {
74387
74458
  * Local Methods
74388
74459
  ***************************/
74389
74460
  /************************************************************
74390
- * THE name rule of timeranger2 and treedb
74391
- * (tranger2_name_is_identifier(), SDK 7.26.9): a topic or a column
74392
- * name is a directory and a key, so it cannot be "." or "..", nor hold
74393
- * '/' or the path delimiter '`'. Said here, before the backend answers
74394
- * a generic "cannot create topic".
74461
+ * A name the treedb refuses (the two name rules of
74462
+ * schema_model.js), said at once: the error carries its key as
74463
+ * a LITERAL (so a consumer's validate-locales sees it), and the
74464
+ * name field is marked aria-invalid for a reader.
74395
74465
  ************************************************************/
74396
- function name_breaks_the_rule(name) {
74397
- return name === "." || name === ".." || name.includes("/") || name.includes("`");
74466
+ function refuse_bad_name(gobj, $message) {
74467
+ mark_name_field(gobj, true);
74468
+ yui_shell_show_error(yui_shell_of(gobj), $message, { t: i18next.t });
74469
+ return -1;
74470
+ }
74471
+ /************************************************************
74472
+ * The name field says whether the LAST save refused its name:
74473
+ * set by every refusal of the name (empty, a rule, a duplicate),
74474
+ * cleared by a save that gets past them.
74475
+ ************************************************************/
74476
+ function mark_name_field(gobj, invalid) {
74477
+ let priv = gobj.priv;
74478
+ let $value = priv.dialog_content ? priv.dialog_content.querySelector("[data-name=\"value\"]") : null;
74479
+ if (!$value) return;
74480
+ if (invalid) $value.setAttribute("aria-invalid", "true");
74481
+ else $value.removeAttribute("aria-invalid");
74398
74482
  }
74399
74483
  /***************************************************************
74400
74484
  * The frame: a toolbar that never changes place, a notice line
@@ -75859,6 +75943,7 @@ function close_dialog(gobj) {
75859
75943
  let dialog = priv.dialog;
75860
75944
  priv.dialog = null;
75861
75945
  priv.dialog_kind = "";
75946
+ priv.dialog_content = null;
75862
75947
  priv.export_view = null;
75863
75948
  try {
75864
75949
  dialog.close();
@@ -75871,6 +75956,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
75871
75956
  close_dialog(gobj);
75872
75957
  priv.dialog_gen = priv.model_gen;
75873
75958
  priv.dialog_kind = logical;
75959
+ priv.dialog_content = $content;
75874
75960
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
75875
75961
  dialog: true,
75876
75962
  title,
@@ -75880,6 +75966,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
75880
75966
  on_close: () => {
75881
75967
  priv.dialog = null;
75882
75968
  priv.dialog_kind = "";
75969
+ priv.dialog_content = null;
75883
75970
  priv.export_view = null;
75884
75971
  }
75885
75972
  });
@@ -77905,17 +77992,21 @@ function ac_save_column(gobj, event, kw, src) {
77905
77992
  }
77906
77993
  let values = kw.values || {};
77907
77994
  if (kw.creating && (0, _yuneta_gobj_js.empty_string)(String(values.value || "").trim())) {
77995
+ mark_name_field(gobj, true);
77908
77996
  yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", { t: i18next.t });
77909
77997
  return -1;
77910
77998
  }
77911
- if (kw.creating && name_breaks_the_rule(String(values.value).trim())) {
77912
- yui_shell_show_error(yui_shell_of(gobj), "a name cannot be . or .., nor hold / or `", { t: i18next.t });
77913
- return -1;
77914
- }
77999
+ if (kw.creating && col_name_breaks_the_rule(String(values.value).trim())) return refuse_bad_name(gobj, (0, _yuneta_gobj_js.createElement2)([
78000
+ "span",
78001
+ { i18n: "invalid column name" },
78002
+ (0, i18next.t)("invalid column name")
78003
+ ]));
77915
78004
  if (kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
78005
+ mark_name_field(gobj, true);
77916
78006
  yui_shell_show_error(yui_shell_of(gobj), "that column is already there", { t: i18next.t });
77917
78007
  return -1;
77918
78008
  }
78009
+ mark_name_field(gobj, false);
77919
78010
  close_dialog(gobj);
77920
78011
  return queue_writes(gobj, [{
77921
78012
  op: kw.creating ? "create" : "update",
@@ -78163,17 +78254,21 @@ function ac_save_topic(gobj, event, kw, src) {
78163
78254
  if (kw.creating) {
78164
78255
  let name = String(values.value || "").trim();
78165
78256
  if ((0, _yuneta_gobj_js.empty_string)(name)) {
78257
+ mark_name_field(gobj, true);
78166
78258
  yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", { t: i18next.t });
78167
78259
  return -1;
78168
78260
  }
78169
- if (name_breaks_the_rule(name)) {
78170
- yui_shell_show_error(yui_shell_of(gobj), "a name cannot be . or .., nor hold / or `", { t: i18next.t });
78171
- return -1;
78172
- }
78261
+ if (topic_name_breaks_the_rule(name)) return refuse_bad_name(gobj, (0, _yuneta_gobj_js.createElement2)([
78262
+ "span",
78263
+ { i18n: "invalid topic name" },
78264
+ (0, i18next.t)("invalid topic name")
78265
+ ]));
78173
78266
  if (find_topic(priv.model, priv.treedb_id, name)) {
78267
+ mark_name_field(gobj, true);
78174
78268
  yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", { t: i18next.t });
78175
78269
  return -1;
78176
78270
  }
78271
+ mark_name_field(gobj, false);
78177
78272
  }
78178
78273
  close_dialog(gobj);
78179
78274
  return queue_writes(gobj, [{
@@ -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,52 @@ function plan_import(treedb, incoming, options) {
73589
73613
  conflicts
73590
73614
  };
73591
73615
  }
73616
+ if (topic_name_breaks_the_rule(treedb_id)) conflicts.push({
73617
+ code: "treedb name breaks the name rule",
73618
+ topic: "",
73619
+ col: ""
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
+ let cols = (in_topic || {}).cols;
73629
+ if (Array.isArray(cols)) {
73630
+ for (let c of cols) if (!c || !c.id) conflicts.push({
73631
+ code: "column has no name",
73632
+ topic: name || "",
73633
+ col: ""
73634
+ });
73635
+ }
73636
+ for (let in_col of incoming_cols(in_topic || {})) {
73637
+ if (col_name_breaks_the_rule(in_col.name)) conflicts.push({
73638
+ code: "column name breaks the name rule",
73639
+ topic: name || "",
73640
+ col: in_col.name
73641
+ });
73642
+ let hook = (in_col.col || {}).hook;
73643
+ if (hook && typeof hook === "object" && !Array.isArray(hook)) for (let [child_topic, fkey_col] of Object.entries(hook)) {
73644
+ if (topic_name_breaks_the_rule(child_topic)) conflicts.push({
73645
+ code: "topic name breaks the name rule",
73646
+ topic: name || "",
73647
+ col: in_col.name
73648
+ });
73649
+ if (col_name_breaks_the_rule(fkey_col)) conflicts.push({
73650
+ code: "column name breaks the name rule",
73651
+ topic: name || "",
73652
+ col: in_col.name
73653
+ });
73654
+ }
73655
+ }
73656
+ }
73657
+ if (conflicts.length > 0) return {
73658
+ writes,
73659
+ summary,
73660
+ conflicts
73661
+ };
73592
73662
  let stored_topics = {};
73593
73663
  for (let topic of treedb && treedb.topics || []) stored_topics[topic.name] = topic;
73594
73664
  let seen_topics = {};
@@ -74319,6 +74389,7 @@ var PRIVATE_DATA$1 = {
74319
74389
  dialog: null,
74320
74390
  dialog_gen: 0,
74321
74391
  dialog_kind: "",
74392
+ dialog_content: null,
74322
74393
  save_queue: null,
74323
74394
  save_return: "",
74324
74395
  save_wrote: false,
@@ -74382,14 +74453,27 @@ function mt_destroy$1(gobj) {
74382
74453
  * Local Methods
74383
74454
  ***************************/
74384
74455
  /************************************************************
74385
- * THE name rule of timeranger2 and treedb
74386
- * (tranger2_name_is_identifier(), SDK 7.26.9): a topic or a column
74387
- * name is a directory and a key, so it cannot be "." or "..", nor hold
74388
- * '/' or the path delimiter '`'. Said here, before the backend answers
74389
- * a generic "cannot create topic".
74456
+ * A name the treedb refuses (the two name rules of
74457
+ * schema_model.js), said at once: the error carries its key as
74458
+ * a LITERAL (so a consumer's validate-locales sees it), and the
74459
+ * name field is marked aria-invalid for a reader.
74390
74460
  ************************************************************/
74391
- function name_breaks_the_rule(name) {
74392
- return name === "." || name === ".." || name.includes("/") || name.includes("`");
74461
+ function refuse_bad_name(gobj, $message) {
74462
+ mark_name_field(gobj, true);
74463
+ yui_shell_show_error(yui_shell_of(gobj), $message, { t });
74464
+ return -1;
74465
+ }
74466
+ /************************************************************
74467
+ * The name field says whether the LAST save refused its name:
74468
+ * set by every refusal of the name (empty, a rule, a duplicate),
74469
+ * cleared by a save that gets past them.
74470
+ ************************************************************/
74471
+ function mark_name_field(gobj, invalid) {
74472
+ let priv = gobj.priv;
74473
+ let $value = priv.dialog_content ? priv.dialog_content.querySelector("[data-name=\"value\"]") : null;
74474
+ if (!$value) return;
74475
+ if (invalid) $value.setAttribute("aria-invalid", "true");
74476
+ else $value.removeAttribute("aria-invalid");
74393
74477
  }
74394
74478
  /***************************************************************
74395
74479
  * The frame: a toolbar that never changes place, a notice line
@@ -75854,6 +75938,7 @@ function close_dialog(gobj) {
75854
75938
  let dialog = priv.dialog;
75855
75939
  priv.dialog = null;
75856
75940
  priv.dialog_kind = "";
75941
+ priv.dialog_content = null;
75857
75942
  priv.export_view = null;
75858
75943
  try {
75859
75944
  dialog.close();
@@ -75866,6 +75951,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
75866
75951
  close_dialog(gobj);
75867
75952
  priv.dialog_gen = priv.model_gen;
75868
75953
  priv.dialog_kind = logical;
75954
+ priv.dialog_content = $content;
75869
75955
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
75870
75956
  dialog: true,
75871
75957
  title,
@@ -75875,6 +75961,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
75875
75961
  on_close: () => {
75876
75962
  priv.dialog = null;
75877
75963
  priv.dialog_kind = "";
75964
+ priv.dialog_content = null;
75878
75965
  priv.export_view = null;
75879
75966
  }
75880
75967
  });
@@ -77900,17 +77987,21 @@ function ac_save_column(gobj, event, kw, src) {
77900
77987
  }
77901
77988
  let values = kw.values || {};
77902
77989
  if (kw.creating && empty_string(String(values.value || "").trim())) {
77990
+ mark_name_field(gobj, true);
77903
77991
  yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", { t });
77904
77992
  return -1;
77905
77993
  }
77906
- if (kw.creating && name_breaks_the_rule(String(values.value).trim())) {
77907
- yui_shell_show_error(yui_shell_of(gobj), "a name cannot be . or .., nor hold / or `", { t });
77908
- return -1;
77909
- }
77994
+ if (kw.creating && col_name_breaks_the_rule(String(values.value).trim())) return refuse_bad_name(gobj, createElement2([
77995
+ "span",
77996
+ { i18n: "invalid column name" },
77997
+ t("invalid column name")
77998
+ ]));
77910
77999
  if (kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
78000
+ mark_name_field(gobj, true);
77911
78001
  yui_shell_show_error(yui_shell_of(gobj), "that column is already there", { t });
77912
78002
  return -1;
77913
78003
  }
78004
+ mark_name_field(gobj, false);
77914
78005
  close_dialog(gobj);
77915
78006
  return queue_writes(gobj, [{
77916
78007
  op: kw.creating ? "create" : "update",
@@ -78158,17 +78249,21 @@ function ac_save_topic(gobj, event, kw, src) {
78158
78249
  if (kw.creating) {
78159
78250
  let name = String(values.value || "").trim();
78160
78251
  if (empty_string(name)) {
78252
+ mark_name_field(gobj, true);
78161
78253
  yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", { t });
78162
78254
  return -1;
78163
78255
  }
78164
- if (name_breaks_the_rule(name)) {
78165
- yui_shell_show_error(yui_shell_of(gobj), "a name cannot be . or .., nor hold / or `", { t });
78166
- return -1;
78167
- }
78256
+ if (topic_name_breaks_the_rule(name)) return refuse_bad_name(gobj, createElement2([
78257
+ "span",
78258
+ { i18n: "invalid topic name" },
78259
+ t("invalid topic name")
78260
+ ]));
78168
78261
  if (find_topic(priv.model, priv.treedb_id, name)) {
78262
+ mark_name_field(gobj, true);
78169
78263
  yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", { t });
78170
78264
  return -1;
78171
78265
  }
78266
+ mark_name_field(gobj, false);
78172
78267
  }
78173
78268
  close_dialog(gobj);
78174
78269
  return queue_writes(gobj, [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.26.21",
3
+ "version": "7.26.23",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -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,
@@ -362,15 +365,36 @@ function mt_destroy(gobj)
362
365
 
363
366
 
364
367
  /************************************************************
365
- * THE name rule of timeranger2 and treedb
366
- * (tranger2_name_is_identifier(), SDK 7.26.9): a topic or a column
367
- * name is a directory and a key, so it cannot be "." or "..", nor hold
368
- * '/' or the path delimiter '`'. Said here, before the backend answers
369
- * a generic "cannot create topic".
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.
370
372
  ************************************************************/
371
- function name_breaks_the_rule(name)
373
+ function refuse_bad_name(gobj, $message)
372
374
  {
373
- return name === "." || name === ".." || name.includes("/") || name.includes("`");
375
+ mark_name_field(gobj, true);
376
+ yui_shell_show_error(yui_shell_of(gobj), $message, {t: t});
377
+ return -1;
378
+ }
379
+
380
+ /************************************************************
381
+ * The name field says whether the LAST save refused its name:
382
+ * set by every refusal of the name (empty, a rule, a duplicate),
383
+ * cleared by a save that gets past them.
384
+ ************************************************************/
385
+ function mark_name_field(gobj, invalid)
386
+ {
387
+ let priv = gobj.priv;
388
+ let $value = priv.dialog_content?
389
+ priv.dialog_content.querySelector('[data-name="value"]') : null;
390
+ if(!$value) {
391
+ return;
392
+ }
393
+ if(invalid) {
394
+ $value.setAttribute("aria-invalid", "true");
395
+ } else {
396
+ $value.removeAttribute("aria-invalid");
397
+ }
374
398
  }
375
399
 
376
400
  /***************************************************************
@@ -1870,6 +1894,7 @@ function close_dialog(gobj)
1870
1894
  let dialog = priv.dialog;
1871
1895
  priv.dialog = null;
1872
1896
  priv.dialog_kind = "";
1897
+ priv.dialog_content = null;
1873
1898
  priv.export_view = null;
1874
1899
  try {
1875
1900
  dialog.close();
@@ -1885,6 +1910,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1885
1910
  close_dialog(gobj);
1886
1911
  priv.dialog_gen = priv.model_gen;
1887
1912
  priv.dialog_kind = logical;
1913
+ priv.dialog_content = $content;
1888
1914
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
1889
1915
  dialog: true,
1890
1916
  title: title,
@@ -1894,6 +1920,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1894
1920
  on_close: () => {
1895
1921
  priv.dialog = null;
1896
1922
  priv.dialog_kind = "";
1923
+ priv.dialog_content = null;
1897
1924
  priv.export_view = null;
1898
1925
  }
1899
1926
  });
@@ -4035,17 +4062,20 @@ function ac_save_column(gobj, event, kw, src)
4035
4062
 
4036
4063
  let values = kw.values || {};
4037
4064
  if(kw.creating && empty_string(String(values.value || "").trim())) {
4065
+ mark_name_field(gobj, true);
4038
4066
  yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", {t: t});
4039
4067
  return -1;
4040
4068
  }
4041
- if(kw.creating && name_breaks_the_rule(String(values.value).trim())) {
4042
- yui_shell_show_error(yui_shell_of(gobj), "a name cannot be . or .., nor hold / or `", {t: t});
4043
- return -1;
4069
+ if(kw.creating && col_name_breaks_the_rule(String(values.value).trim())) {
4070
+ return refuse_bad_name(gobj, createElement2(
4071
+ ["span", {i18n: "invalid column name"}, t("invalid column name")]));
4044
4072
  }
4045
4073
  if(kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
4074
+ mark_name_field(gobj, true);
4046
4075
  yui_shell_show_error(yui_shell_of(gobj), "that column is already there", {t: t});
4047
4076
  return -1;
4048
4077
  }
4078
+ mark_name_field(gobj, false);
4049
4079
 
4050
4080
  close_dialog(gobj);
4051
4081
  return queue_writes(gobj, [{
@@ -4346,17 +4376,20 @@ function ac_save_topic(gobj, event, kw, src)
4346
4376
  if(kw.creating) {
4347
4377
  let name = String(values.value || "").trim();
4348
4378
  if(empty_string(name)) {
4379
+ mark_name_field(gobj, true);
4349
4380
  yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", {t: t});
4350
4381
  return -1;
4351
4382
  }
4352
- if(name_breaks_the_rule(name)) {
4353
- yui_shell_show_error(yui_shell_of(gobj), "a name cannot be . or .., nor hold / or `", {t: t});
4354
- return -1;
4383
+ if(topic_name_breaks_the_rule(name)) {
4384
+ return refuse_bad_name(gobj, createElement2(
4385
+ ["span", {i18n: "invalid topic name"}, t("invalid topic name")]));
4355
4386
  }
4356
4387
  if(find_topic(priv.model, priv.treedb_id, name)) {
4388
+ mark_name_field(gobj, true);
4357
4389
  yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", {t: t});
4358
4390
  return -1;
4359
4391
  }
4392
+ mark_name_field(gobj, false);
4360
4393
  }
4361
4394
 
4362
4395
  close_dialog(gobj);
@@ -1716,3 +1716,60 @@ 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
+ });
1751
+
1752
+
1753
+ /*
1754
+ * The name field says whether the LAST save refused its name (REV-CLOUD
1755
+ * V15.4): set by every refusal of the name, cleared by a save that gets
1756
+ * past them. It was set by the rule only, and never cleared.
1757
+ */
1758
+ describe("aria-invalid of the name field", () => {
1759
+ test("set by a bad name and by a duplicate, cleared by a good one", () => {
1760
+ const {editor, host} = build("aria_topic", "db");
1761
+ gobj_send_event(editor, "EV_ADD_TOPIC", {}, host);
1762
+ const form = modals[modals.length - 1];
1763
+ const $value = form.$content.querySelector('[data-name="value"]');
1764
+ const save = (name) => {
1765
+ gobj_send_event(editor, "EV_SAVE_TOPIC", {
1766
+ creating: true, topic: "", values: {value: name}
1767
+ }, host);
1768
+ };
1769
+ save("a/b");
1770
+ expect($value.getAttribute("aria-invalid")).toBe("true");
1771
+ save("ok_name");
1772
+ expect($value.getAttribute("aria-invalid")).toBe(null);
1773
+ take("update-node");
1774
+ });
1775
+ });
@@ -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,54 @@ 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
+ if(topic_name_breaks_the_rule(treedb_id)) {
215
+ /* The treedb is a directory too (treedb_open_db() refuses it) */
216
+ conflicts.push({code: "treedb name breaks the name rule", topic: "", col: ""});
217
+ }
218
+ for(let in_topic of incoming.topics) {
219
+ let name = (in_topic || {}).id;
220
+ if(name && topic_name_breaks_the_rule(name)) {
221
+ conflicts.push({code: "topic name breaks the name rule", topic: name, col: ""});
222
+ }
223
+ let cols = (in_topic || {}).cols;
224
+ if(Array.isArray(cols)) {
225
+ /* incoming_cols() drops a column with no id: said, not dropped */
226
+ for(let c of cols) {
227
+ if(!c || !c.id) {
228
+ conflicts.push({code: "column has no name", topic: name || "", col: ""});
229
+ }
230
+ }
231
+ }
232
+ for(let in_col of incoming_cols(in_topic || {})) {
233
+ if(col_name_breaks_the_rule(in_col.name)) {
234
+ conflicts.push({code: "column name breaks the name rule", topic: name || "", col: in_col.name});
235
+ }
236
+ /* The hook map names a child topic and its fkey column: names
237
+ * the treedb refuses too, even when no column declares them. */
238
+ let hook = (in_col.col || {}).hook;
239
+ if(hook && typeof hook === "object" && !Array.isArray(hook)) {
240
+ for(let [child_topic, fkey_col] of Object.entries(hook)) {
241
+ if(topic_name_breaks_the_rule(child_topic)) {
242
+ conflicts.push({code: "topic name breaks the name rule",
243
+ topic: name || "", col: in_col.name});
244
+ }
245
+ if(col_name_breaks_the_rule(fkey_col)) {
246
+ conflicts.push({code: "column name breaks the name rule",
247
+ topic: name || "", col: in_col.name});
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ if(conflicts.length > 0) {
254
+ return {writes: writes, summary: summary, conflicts: conflicts};
255
+ }
256
+
207
257
  let stored_topics = {};
208
258
  for(let topic of ((treedb && treedb.topics) || [])) {
209
259
  stored_topics[topic.name] = topic;
@@ -253,3 +253,60 @@ 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
+ });
285
+
286
+
287
+ describe("the import reads the hook map and the treedb id (REV-CLOUD V15.4)", () => {
288
+ test("a hook to an fkey x`y or to a topic a/b that no column declares: conflicts", () => {
289
+ let plan = plan_import(null, {
290
+ id: "db", schema_version: "1",
291
+ topics: [{id: "p", pkey: "id", cols: {
292
+ "id": {id: "id", type: "string"},
293
+ "kids": {id: "kids", type: "dict", flag: ["hook"], hook: {"k": "x`y", "a/b": "pid"}},
294
+ }}]
295
+ });
296
+ expect(plan.writes).toEqual([]);
297
+ let codes = plan.conflicts.map((c) => c.code);
298
+ expect(codes).toContain("column name breaks the name rule");
299
+ expect(codes).toContain("topic name breaks the name rule");
300
+ });
301
+
302
+ test("a treedb a/b, and a column with no id in an array: conflicts, not dropped", () => {
303
+ let plan = plan_import(null, {
304
+ id: "a/b", schema_version: "1",
305
+ topics: [{id: "t", pkey: "id", cols: [{id: "id", type: "string"}, {id: "", type: "string"}]}]
306
+ });
307
+ expect(plan.writes).toEqual([]);
308
+ let codes = plan.conflicts.map((c) => c.code);
309
+ expect(codes).toContain("treedb name breaks the name rule");
310
+ expect(codes).toContain("column has no name");
311
+ });
312
+ });
@@ -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,
@@ -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
+ });
@@ -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
+ });