@yuneta/gobj-ui 7.25.16 → 7.25.17

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 CHANGED
@@ -1824,6 +1824,20 @@ gobj_send_event(engine, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, host); //
1824
1824
  gobj_send_event(engine, "EV_SAVE_GRAPH", {}, engine); // writes users; Save dark
1825
1825
  ```
1826
1826
 
1827
+ **The refusal reaches the engine through the real transports** (since
1828
+ `7.25.17`). The answer of a command carries back only the request's
1829
+ `__md_command__`, as its `command_stack` frame -- `C_IEVENT_CLI` and gui_agent's
1830
+ `C_AGENT_TREEDB_LINK` both do, and neither echoes the request's `record`. The
1831
+ host read the topic from that `record`, found none, logged *"a refused
1832
+ __graphs__ write names no topic"* and never told the engine: the 7.25.15 fix
1833
+ worked only in a test whose fake transport echoed the whole request. The host
1834
+ now puts the topic in the echo it asks for:
1835
+
1836
+ ```js
1837
+ kw.__md_command__ = {topic_name: "__graphs__", graph_topic: "users"};
1838
+ // the answer: __md_iev__.command_stack[0].kw === that echo, and nothing else
1839
+ ```
1840
+
1827
1841
  **Every saved look has a way back, in the context menu** (since `7.23.134`,
1828
1842
  replacing `reset sizes` / `reset topic sizes`). In edition, the node, the port
1829
1843
  and the edge menus each offer three resets — this one, its kind (`reset topic
@@ -2259,6 +2273,39 @@ else.
2259
2273
  gobj_send_event(editor, "EV_REFRESH", {}, host); // logged, waits
2260
2274
  // ... both deletes answered -> ST_LOADING -> the model is read again
2261
2275
  ```
2276
+ - **A write in flight takes the body out of the keyboard's reach too**
2277
+ (7.25.17). `ST_SAVING` keeps the screen under it and marks the body busy.
2278
+ The busy class only stopped the pointer (`pointer-events: none`); the
2279
+ shell's focus trap put the focus back on the row control that had it, and
2280
+ Enter or Delete there sent `EV_DELETE_COLUMN` or `EV_SELECT_TOPIC`, which
2281
+ `ST_SAVING` does not declare (*"Event NOT DEFINED"*). The busy body is now
2282
+ also `inert`. `ST_LOADING` needs nothing: its body is emptied.
2283
+
2284
+ ```js
2285
+ // in ST_SAVING
2286
+ $container.querySelector(".SCHEMA_BODY").hasAttribute("inert"); // true
2287
+ ```
2288
+ - **A write marks the topic of its RECORD, not the topic on screen**
2289
+ (7.25.17). What "its version still has to move" is asked about is the set of
2290
+ topics this session wrote (the draft chip, the export warning,
2291
+ `validate_schema()`'s `written_topics`). A column write marked the topic on
2292
+ screen, so an import from the topics screen (no topic on it) marked nothing,
2293
+ and an import that added `roles` from the screen of `users` marked `users`.
2294
+ The mark is read from the record the store answered (a new topic has its id
2295
+ only there), then from the record queued (a column names its topic in its
2296
+ fkey), and only then from the screen.
2297
+
2298
+ ```js
2299
+ // import adding topic `roles`, run from db/users, every write answered
2300
+ editor.priv.written; // {"db.roles": true} (was {"db.users": true})
2301
+ ```
2302
+ - **The reason a load failed is a key** (7.25.17). The notice's detail was
2303
+ the text of `t()` taken when the load failed (`cannot reach the treedb`,
2304
+ `cannot load the schemas`) or the backend's words, shown as plain text: it
2305
+ did not change language, and with no comment it repeated the notice's own
2306
+ title. It is now kept as a key, written with `data-i18n` (the backend's
2307
+ words translate to themselves when no locale has them), and left out when
2308
+ it is the title.
2262
2309
  - **The loading screen is drawn** (7.25.8): the body is cleared, the drawing
2263
2310
  destroyed and every toolbar button disabled, as in `ST_SAVING`, because
2264
2311
  `ST_LOADING` declares none of a screen's actions. A **dialog** left open is
@@ -68378,6 +68378,7 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options) {
68378
68378
  options: options || {}
68379
68379
  };
68380
68380
  kw.__md_command__ = { topic_name };
68381
+ if (topic_name === "__graphs__") kw.__md_command__.graph_topic = graph_topic_of(record);
68381
68382
  let ret = (0, _yuneta_gobj_js.gobj_command)(priv.gobj_remote_yuno, command, kw, gobj);
68382
68383
  if (ret) {
68383
68384
  (0, _yuneta_gobj_js.log_error)(ret);
@@ -68656,7 +68657,7 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
68656
68657
  case "delete-node":
68657
68658
  case "link-nodes":
68658
68659
  case "unlink-nodes":
68659
- if (result < 0 && command === "update-node") graphs_write_refused(gobj, (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0), (0, _yuneta_gobj_js.kw_get_dict)(gobj, kw_command, "record", {}, 0));
68660
+ if (result < 0 && command === "update-node") graphs_write_refused(gobj, (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0), (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "graph_topic", "", 0));
68660
68661
  if (result >= 0 && (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_RECORD_WRITTEN", {
68661
68662
  treedb_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "treedb_name", "", 0),
68662
68663
  topic_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0),
@@ -68905,29 +68906,35 @@ function ac_update_node(gobj, event, kw, src) {
68905
68906
  let record = kw.record;
68906
68907
  let options = kw.options || {};
68907
68908
  if (refuse_if_readonly$1(gobj, event)) {
68908
- graphs_write_refused(gobj, topic_name, record);
68909
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
68909
68910
  return -1;
68910
68911
  }
68911
68912
  if (topic_name === "__graphs__" && !is_connected(gobj)) {
68912
68913
  (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no session, the arrangement was not sent`);
68913
- graphs_write_refused(gobj, topic_name, record);
68914
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
68914
68915
  return -1;
68915
68916
  }
68916
68917
  let ret = treedb_update_node(gobj, treedb_name, topic_name, record, options);
68917
- if (ret < 0) graphs_write_refused(gobj, topic_name, record);
68918
+ if (ret < 0) graphs_write_refused(gobj, topic_name, graph_topic_of(record));
68918
68919
  return ret;
68919
68920
  }
68920
68921
  /************************************************************
68922
+ * The topic a `__graphs__` record arranges.
68923
+ ************************************************************/
68924
+ function graph_topic_of(record) {
68925
+ if (!record || typeof record !== "object") return "";
68926
+ return record.topic || record.id || "";
68927
+ }
68928
+ /************************************************************
68921
68929
  * A write of `__graphs__` did not land. The engine
68922
68930
  * (C_G6_NODES_TREE) took it for granted when it left -- nothing
68923
68931
  * answers it when it lands -- so it is told, and its next Save
68924
68932
  * writes the topic again. Other topics are the operator's data:
68925
68933
  * the answer's error says what happened to them.
68926
68934
  ************************************************************/
68927
- function graphs_write_refused(gobj, topic_name, record) {
68935
+ function graphs_write_refused(gobj, topic_name, topic) {
68928
68936
  let priv = gobj.priv;
68929
68937
  if (topic_name !== "__graphs__") return;
68930
- let topic = record && typeof record === "object" ? record.topic || record.id : "";
68931
68938
  if (!topic) {
68932
68939
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a refused __graphs__ write names no topic`);
68933
68940
  return;
@@ -72486,11 +72493,18 @@ function el_attrs(attrs) {
72486
72493
  return attrs;
72487
72494
  }
72488
72495
  /***************************************************************
72489
- * The body stops answering while a write is in flight.
72496
+ * The body stops answering while a write is in flight. For the
72497
+ * keyboard too: `pointer-events` stops the mouse alone, and the
72498
+ * shell's focus trap puts the focus back on the row control that
72499
+ * had it, where Enter or Delete sent an action ST_SAVING does
72500
+ * not declare. `inert` takes the whole body out of reach.
72490
72501
  ***************************************************************/
72491
72502
  function set_busy(gobj, busy) {
72492
72503
  let $body = $of(gobj, ".SCHEMA_BODY");
72493
- if ($body) $body.classList.toggle("SCHEMA_BUSY", !!busy);
72504
+ if ($body) {
72505
+ $body.classList.toggle("SCHEMA_BUSY", !!busy);
72506
+ $body.toggleAttribute("inert", !!busy);
72507
+ }
72494
72508
  }
72495
72509
  /***************************************************************
72496
72510
  * Shorthands for the three panes of the frame.
@@ -72706,7 +72720,7 @@ function request_model(gobj, keep_written) {
72706
72720
  priv.records_before = null;
72707
72721
  priv.load_round++;
72708
72722
  priv.reload_on_open = true;
72709
- priv.load_error = (0, i18next.t)("cannot reach the treedb");
72723
+ priv.load_error = "cannot reach the treedb";
72710
72724
  if (priv.model) yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", { t: i18next.t });
72711
72725
  end_load(gobj);
72712
72726
  return -1;
@@ -72917,10 +72931,13 @@ function show_notice(gobj, key, detail) {
72917
72931
  },
72918
72932
  (0, i18next.t)(key)
72919
72933
  ]));
72920
- if (detail) $notice.appendChild((0, _yuneta_gobj_js.createElement2)([
72934
+ if (detail && detail !== key) $notice.appendChild((0, _yuneta_gobj_js.createElement2)([
72921
72935
  "span",
72922
- { class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2" },
72923
- `${detail}`
72936
+ {
72937
+ class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2",
72938
+ i18n: `${detail}`
72939
+ },
72940
+ (0, i18next.t)(`${detail}`)
72924
72941
  ]));
72925
72942
  }
72926
72943
  /***************************************************************
@@ -75475,7 +75492,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75475
75492
  return 0;
75476
75493
  }
75477
75494
  priv.pending--;
75478
- if (result < 0) priv.load_error = comment || (0, i18next.t)("cannot load the schemas");
75495
+ if (result < 0) priv.load_error = comment || "cannot load the schemas";
75479
75496
  else if (priv.records && Array.isArray(priv.records[topic_name])) priv.records[topic_name] = Array.isArray(data) ? data : [];
75480
75497
  if (priv.pending > 0) return 0;
75481
75498
  if (priv.load_error) {
@@ -75511,9 +75528,10 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75511
75528
  }
75512
75529
  if (result < 0) return end_writes(gobj, comment || (0, i18next.t)("the treedb refused the write"));
75513
75530
  priv.save_wrote = true;
75531
+ let record = null;
75514
75532
  if (command === "delete-node") forget_record(gobj, topic_name, (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "record_id", "", 0));
75515
75533
  else {
75516
- let record = (0, _yuneta_gobj_js.is_object)(data) ? data : Array.isArray(data) && (0, _yuneta_gobj_js.is_object)(data[0]) ? data[0] : null;
75534
+ record = (0, _yuneta_gobj_js.is_object)(data) ? data : Array.isArray(data) && (0, _yuneta_gobj_js.is_object)(data[0]) ? data[0] : null;
75517
75535
  if (record) patch_record(gobj, topic_name, record);
75518
75536
  else {
75519
75537
  let not_sent = priv.save_queue.length - 1;
@@ -75521,7 +75539,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75521
75539
  if (not_sent > 0) yui_shell_show_error(yui_shell_of(gobj), "the treedb did not describe a write back: the writes after it were not sent", { t: i18next.t });
75522
75540
  if (topic_name === T_TOPICS || topic_name === T_COLS) {
75523
75541
  let write = priv.save_queue[0];
75524
- mark_written(gobj, topic_name, write ? write.record : null);
75542
+ mark_written(gobj, topic_name, null, write ? write.record : null);
75525
75543
  }
75526
75544
  priv.reload_after_write = false;
75527
75545
  priv.save_queue = [];
@@ -75536,7 +75554,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75536
75554
  }
75537
75555
  if (topic_name === T_TOPICS || topic_name === T_COLS) {
75538
75556
  let write = priv.save_queue[0];
75539
- mark_written(gobj, topic_name, write ? write.record : null);
75557
+ mark_written(gobj, topic_name, record, write ? write.record : null);
75540
75558
  }
75541
75559
  priv.save_queue.shift();
75542
75560
  return run_next_write(gobj);
@@ -75593,25 +75611,34 @@ function late_write_done(gobj, command, topic_name, data) {
75593
75611
  ***************************************************************/
75594
75612
  function mark_written_record(gobj, topic_name, record) {
75595
75613
  let priv = gobj.priv;
75596
- if (!record) return;
75614
+ let marked = 0;
75615
+ if (!record) return 0;
75597
75616
  if (topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
75598
75617
  priv.written[record.id] = true;
75599
- return;
75618
+ return 1;
75619
+ }
75620
+ if (topic_name !== T_COLS) return 0;
75621
+ for (let ref of parse_fkey_ref(record.topics)) {
75622
+ priv.written[ref.id] = true;
75623
+ marked++;
75600
75624
  }
75601
- if (topic_name !== T_COLS) return;
75602
- for (let ref of parse_fkey_ref(record.topics)) priv.written[ref.id] = true;
75625
+ return marked;
75603
75626
  }
75604
75627
  /***************************************************************
75605
75628
  * Remember which topic this write belongs to, which is what
75606
75629
  * "its version still has to move" is asked about.
75630
+ *
75631
+ * Asked of the RECORD first: the one the store answered (a new
75632
+ * topic has its id only there), then the one queued (a column
75633
+ * names its topic in its fkey). The screen is the last resort,
75634
+ * and a poor one: an import writes topics the operator is not
75635
+ * on, and from the topics screen there is no topic on it at all.
75607
75636
  ***************************************************************/
75608
- function mark_written(gobj, topic_name, record) {
75637
+ function mark_written(gobj, topic_name, answered, queued) {
75609
75638
  let priv = gobj.priv;
75639
+ if (mark_written_record(gobj, topic_name, answered) > 0) return;
75640
+ if (mark_written_record(gobj, topic_name, queued) > 0) return;
75610
75641
  let topic = current_topic(gobj);
75611
- if (topic_name === T_TOPICS && record && record.id) {
75612
- priv.written[record.id] = true;
75613
- return;
75614
- }
75615
75642
  if (topic) priv.written[topic.id] = true;
75616
75643
  }
75617
75644
  /***************************************************************
@@ -68373,6 +68373,7 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options) {
68373
68373
  options: options || {}
68374
68374
  };
68375
68375
  kw.__md_command__ = { topic_name };
68376
+ if (topic_name === "__graphs__") kw.__md_command__.graph_topic = graph_topic_of(record);
68376
68377
  let ret = gobj_command(priv.gobj_remote_yuno, command, kw, gobj);
68377
68378
  if (ret) {
68378
68379
  log_error(ret);
@@ -68651,7 +68652,7 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
68651
68652
  case "delete-node":
68652
68653
  case "link-nodes":
68653
68654
  case "unlink-nodes":
68654
- if (result < 0 && command === "update-node") graphs_write_refused(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0), kw_get_dict(gobj, kw_command, "record", {}, 0));
68655
+ if (result < 0 && command === "update-node") graphs_write_refused(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0), kw_get_str(gobj, kw_command, "graph_topic", "", 0));
68655
68656
  if (result >= 0 && kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
68656
68657
  treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
68657
68658
  topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
@@ -68900,29 +68901,35 @@ function ac_update_node(gobj, event, kw, src) {
68900
68901
  let record = kw.record;
68901
68902
  let options = kw.options || {};
68902
68903
  if (refuse_if_readonly$1(gobj, event)) {
68903
- graphs_write_refused(gobj, topic_name, record);
68904
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
68904
68905
  return -1;
68905
68906
  }
68906
68907
  if (topic_name === "__graphs__" && !is_connected(gobj)) {
68907
68908
  log_warning(`${gobj_short_name(gobj)}: no session, the arrangement was not sent`);
68908
- graphs_write_refused(gobj, topic_name, record);
68909
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
68909
68910
  return -1;
68910
68911
  }
68911
68912
  let ret = treedb_update_node(gobj, treedb_name, topic_name, record, options);
68912
- if (ret < 0) graphs_write_refused(gobj, topic_name, record);
68913
+ if (ret < 0) graphs_write_refused(gobj, topic_name, graph_topic_of(record));
68913
68914
  return ret;
68914
68915
  }
68915
68916
  /************************************************************
68917
+ * The topic a `__graphs__` record arranges.
68918
+ ************************************************************/
68919
+ function graph_topic_of(record) {
68920
+ if (!record || typeof record !== "object") return "";
68921
+ return record.topic || record.id || "";
68922
+ }
68923
+ /************************************************************
68916
68924
  * A write of `__graphs__` did not land. The engine
68917
68925
  * (C_G6_NODES_TREE) took it for granted when it left -- nothing
68918
68926
  * answers it when it lands -- so it is told, and its next Save
68919
68927
  * writes the topic again. Other topics are the operator's data:
68920
68928
  * the answer's error says what happened to them.
68921
68929
  ************************************************************/
68922
- function graphs_write_refused(gobj, topic_name, record) {
68930
+ function graphs_write_refused(gobj, topic_name, topic) {
68923
68931
  let priv = gobj.priv;
68924
68932
  if (topic_name !== "__graphs__") return;
68925
- let topic = record && typeof record === "object" ? record.topic || record.id : "";
68926
68933
  if (!topic) {
68927
68934
  log_error(`${gobj_short_name(gobj)}: a refused __graphs__ write names no topic`);
68928
68935
  return;
@@ -72481,11 +72488,18 @@ function el_attrs(attrs) {
72481
72488
  return attrs;
72482
72489
  }
72483
72490
  /***************************************************************
72484
- * The body stops answering while a write is in flight.
72491
+ * The body stops answering while a write is in flight. For the
72492
+ * keyboard too: `pointer-events` stops the mouse alone, and the
72493
+ * shell's focus trap puts the focus back on the row control that
72494
+ * had it, where Enter or Delete sent an action ST_SAVING does
72495
+ * not declare. `inert` takes the whole body out of reach.
72485
72496
  ***************************************************************/
72486
72497
  function set_busy(gobj, busy) {
72487
72498
  let $body = $of(gobj, ".SCHEMA_BODY");
72488
- if ($body) $body.classList.toggle("SCHEMA_BUSY", !!busy);
72499
+ if ($body) {
72500
+ $body.classList.toggle("SCHEMA_BUSY", !!busy);
72501
+ $body.toggleAttribute("inert", !!busy);
72502
+ }
72489
72503
  }
72490
72504
  /***************************************************************
72491
72505
  * Shorthands for the three panes of the frame.
@@ -72701,7 +72715,7 @@ function request_model(gobj, keep_written) {
72701
72715
  priv.records_before = null;
72702
72716
  priv.load_round++;
72703
72717
  priv.reload_on_open = true;
72704
- priv.load_error = t("cannot reach the treedb");
72718
+ priv.load_error = "cannot reach the treedb";
72705
72719
  if (priv.model) yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", { t });
72706
72720
  end_load(gobj);
72707
72721
  return -1;
@@ -72912,10 +72926,13 @@ function show_notice(gobj, key, detail) {
72912
72926
  },
72913
72927
  t(key)
72914
72928
  ]));
72915
- if (detail) $notice.appendChild(createElement2([
72929
+ if (detail && detail !== key) $notice.appendChild(createElement2([
72916
72930
  "span",
72917
- { class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2" },
72918
- `${detail}`
72931
+ {
72932
+ class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2",
72933
+ i18n: `${detail}`
72934
+ },
72935
+ t(`${detail}`)
72919
72936
  ]));
72920
72937
  }
72921
72938
  /***************************************************************
@@ -75470,7 +75487,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75470
75487
  return 0;
75471
75488
  }
75472
75489
  priv.pending--;
75473
- if (result < 0) priv.load_error = comment || t("cannot load the schemas");
75490
+ if (result < 0) priv.load_error = comment || "cannot load the schemas";
75474
75491
  else if (priv.records && Array.isArray(priv.records[topic_name])) priv.records[topic_name] = Array.isArray(data) ? data : [];
75475
75492
  if (priv.pending > 0) return 0;
75476
75493
  if (priv.load_error) {
@@ -75506,9 +75523,10 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75506
75523
  }
75507
75524
  if (result < 0) return end_writes(gobj, comment || t("the treedb refused the write"));
75508
75525
  priv.save_wrote = true;
75526
+ let record = null;
75509
75527
  if (command === "delete-node") forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
75510
75528
  else {
75511
- let record = is_object(data) ? data : Array.isArray(data) && is_object(data[0]) ? data[0] : null;
75529
+ record = is_object(data) ? data : Array.isArray(data) && is_object(data[0]) ? data[0] : null;
75512
75530
  if (record) patch_record(gobj, topic_name, record);
75513
75531
  else {
75514
75532
  let not_sent = priv.save_queue.length - 1;
@@ -75516,7 +75534,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75516
75534
  if (not_sent > 0) yui_shell_show_error(yui_shell_of(gobj), "the treedb did not describe a write back: the writes after it were not sent", { t });
75517
75535
  if (topic_name === T_TOPICS || topic_name === T_COLS) {
75518
75536
  let write = priv.save_queue[0];
75519
- mark_written(gobj, topic_name, write ? write.record : null);
75537
+ mark_written(gobj, topic_name, null, write ? write.record : null);
75520
75538
  }
75521
75539
  priv.reload_after_write = false;
75522
75540
  priv.save_queue = [];
@@ -75531,7 +75549,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75531
75549
  }
75532
75550
  if (topic_name === T_TOPICS || topic_name === T_COLS) {
75533
75551
  let write = priv.save_queue[0];
75534
- mark_written(gobj, topic_name, write ? write.record : null);
75552
+ mark_written(gobj, topic_name, record, write ? write.record : null);
75535
75553
  }
75536
75554
  priv.save_queue.shift();
75537
75555
  return run_next_write(gobj);
@@ -75588,25 +75606,34 @@ function late_write_done(gobj, command, topic_name, data) {
75588
75606
  ***************************************************************/
75589
75607
  function mark_written_record(gobj, topic_name, record) {
75590
75608
  let priv = gobj.priv;
75591
- if (!record) return;
75609
+ let marked = 0;
75610
+ if (!record) return 0;
75592
75611
  if (topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
75593
75612
  priv.written[record.id] = true;
75594
- return;
75613
+ return 1;
75614
+ }
75615
+ if (topic_name !== T_COLS) return 0;
75616
+ for (let ref of parse_fkey_ref(record.topics)) {
75617
+ priv.written[ref.id] = true;
75618
+ marked++;
75595
75619
  }
75596
- if (topic_name !== T_COLS) return;
75597
- for (let ref of parse_fkey_ref(record.topics)) priv.written[ref.id] = true;
75620
+ return marked;
75598
75621
  }
75599
75622
  /***************************************************************
75600
75623
  * Remember which topic this write belongs to, which is what
75601
75624
  * "its version still has to move" is asked about.
75625
+ *
75626
+ * Asked of the RECORD first: the one the store answered (a new
75627
+ * topic has its id only there), then the one queued (a column
75628
+ * names its topic in its fkey). The screen is the last resort,
75629
+ * and a poor one: an import writes topics the operator is not
75630
+ * on, and from the topics screen there is no topic on it at all.
75602
75631
  ***************************************************************/
75603
- function mark_written(gobj, topic_name, record) {
75632
+ function mark_written(gobj, topic_name, answered, queued) {
75604
75633
  let priv = gobj.priv;
75634
+ if (mark_written_record(gobj, topic_name, answered) > 0) return;
75635
+ if (mark_written_record(gobj, topic_name, queued) > 0) return;
75605
75636
  let topic = current_topic(gobj);
75606
- if (topic_name === T_TOPICS && record && record.id) {
75607
- priv.written[record.id] = true;
75608
- return;
75609
- }
75610
75637
  if (topic) priv.written[topic.id] = true;
75611
75638
  }
75612
75639
  /***************************************************************
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.25.16",
3
+ "version": "7.25.17",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@rollup/plugin-terser": "^1.0.0",
32
32
  "@vitest/coverage-v8": "^4.1.10",
33
- "@yuneta/gobj-js": "^7.25.2",
33
+ "@yuneta/gobj-js": "^7.25.3",
34
34
  "vite": "^8.2.1",
35
35
  "vitest": "^4.1.10"
36
36
  },
@@ -403,7 +403,11 @@ function el_attrs(attrs)
403
403
  }
404
404
 
405
405
  /***************************************************************
406
- * The body stops answering while a write is in flight.
406
+ * The body stops answering while a write is in flight. For the
407
+ * keyboard too: `pointer-events` stops the mouse alone, and the
408
+ * shell's focus trap puts the focus back on the row control that
409
+ * had it, where Enter or Delete sent an action ST_SAVING does
410
+ * not declare. `inert` takes the whole body out of reach.
407
411
  ***************************************************************/
408
412
  function set_busy(gobj, busy)
409
413
  {
@@ -411,6 +415,7 @@ function set_busy(gobj, busy)
411
415
 
412
416
  if($body) {
413
417
  $body.classList.toggle("SCHEMA_BUSY", !!busy);
418
+ $body.toggleAttribute("inert", !!busy);
414
419
  }
415
420
  }
416
421
 
@@ -707,7 +712,7 @@ function request_model(gobj, keep_written)
707
712
  priv.records_before = null;
708
713
  priv.load_round++;
709
714
  priv.reload_on_open = true;
710
- priv.load_error = t("cannot reach the treedb");
715
+ priv.load_error = "cannot reach the treedb"; /* a key: the notice translates it */
711
716
  if(priv.model) {
712
717
  yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", {t: t});
713
718
  }
@@ -993,9 +998,14 @@ function show_notice(gobj, key, detail)
993
998
  $notice.appendChild(createElement2(
994
999
  ["span", {class: "SCHEMA_NOTICE_TEXT", i18n: key}, t(key)]
995
1000
  ));
996
- if(detail) {
1001
+ /* The detail is a KEY too (ours, or the backend's words, which
1002
+ * translate to themselves when no locale has them), so a change
1003
+ * of language changes it with the rest. One that repeats the
1004
+ * title says nothing. */
1005
+ if(detail && detail !== key) {
997
1006
  $notice.appendChild(createElement2(
998
- ["span", {class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2"}, `${detail}`]
1007
+ ["span", {class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2", i18n: `${detail}`},
1008
+ t(`${detail}`)]
999
1009
  ));
1000
1010
  }
1001
1011
  }
@@ -3350,7 +3360,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3350
3360
  }
3351
3361
  priv.pending--;
3352
3362
  if(result < 0) {
3353
- priv.load_error = comment || t("cannot load the schemas");
3363
+ priv.load_error = comment || "cannot load the schemas"; /* a key */
3354
3364
  } else if(priv.records && Array.isArray(priv.records[topic_name])) {
3355
3365
  priv.records[topic_name] = Array.isArray(data) ? data : [];
3356
3366
  }
@@ -3427,10 +3437,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
3427
3437
  return end_writes(gobj, comment || t("the treedb refused the write"));
3428
3438
  }
3429
3439
  priv.save_wrote = true;
3440
+ let record = null;
3430
3441
  if(command === "delete-node") {
3431
3442
  forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
3432
3443
  } else {
3433
- let record = is_object(data) ? data
3444
+ record = is_object(data) ? data
3434
3445
  : (Array.isArray(data) && is_object(data[0]) ? data[0] : null);
3435
3446
  if(record) {
3436
3447
  patch_record(gobj, topic_name, record);
@@ -3453,7 +3464,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3453
3464
  }
3454
3465
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
3455
3466
  let write = priv.save_queue[0];
3456
- mark_written(gobj, topic_name, write ? write.record : null);
3467
+ mark_written(gobj, topic_name, null, write ? write.record : null);
3457
3468
  }
3458
3469
  priv.reload_after_write = false;
3459
3470
  priv.save_queue = [];
@@ -3471,11 +3482,12 @@ function ac_mt_command_answer(gobj, event, kw, src)
3471
3482
  }
3472
3483
  }
3473
3484
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
3474
- /* Which record it was is in the QUEUE, not in the answer: an
3475
- * adapter echoes only what it was asked to echo, and the
3476
- * record is not small enough to be worth echoing. */
3485
+ /* Which record it was: the one the store answered, else the
3486
+ * one in the QUEUE -- an adapter echoes only what it was
3487
+ * asked to echo, and the record is not small enough to be
3488
+ * worth echoing. */
3477
3489
  let write = priv.save_queue[0];
3478
- mark_written(gobj, topic_name, write ? write.record : null);
3490
+ mark_written(gobj, topic_name, record, write ? write.record : null);
3479
3491
  }
3480
3492
  priv.save_queue.shift();
3481
3493
  return run_next_write(gobj);
@@ -3544,35 +3556,46 @@ function late_write_done(gobj, command, topic_name, data)
3544
3556
  function mark_written_record(gobj, topic_name, record)
3545
3557
  {
3546
3558
  let priv = gobj.priv;
3559
+ let marked = 0;
3547
3560
 
3548
3561
  if(!record) {
3549
- return;
3562
+ return 0;
3550
3563
  }
3551
3564
  if(topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
3552
3565
  priv.written[record.id] = true;
3553
- return;
3566
+ return 1;
3554
3567
  }
3555
3568
  if(topic_name !== T_COLS) {
3556
- return;
3569
+ return 0;
3557
3570
  }
3558
3571
  for(let ref of parse_fkey_ref(record.topics)) {
3559
3572
  priv.written[ref.id] = true;
3573
+ marked++;
3560
3574
  }
3575
+ return marked;
3561
3576
  }
3562
3577
 
3563
3578
  /***************************************************************
3564
3579
  * Remember which topic this write belongs to, which is what
3565
3580
  * "its version still has to move" is asked about.
3581
+ *
3582
+ * Asked of the RECORD first: the one the store answered (a new
3583
+ * topic has its id only there), then the one queued (a column
3584
+ * names its topic in its fkey). The screen is the last resort,
3585
+ * and a poor one: an import writes topics the operator is not
3586
+ * on, and from the topics screen there is no topic on it at all.
3566
3587
  ***************************************************************/
3567
- function mark_written(gobj, topic_name, record)
3588
+ function mark_written(gobj, topic_name, answered, queued)
3568
3589
  {
3569
3590
  let priv = gobj.priv;
3570
- let topic = current_topic(gobj);
3571
3591
 
3572
- if(topic_name === T_TOPICS && record && record.id) {
3573
- priv.written[record.id] = true;
3592
+ if(mark_written_record(gobj, topic_name, answered) > 0) {
3574
3593
  return;
3575
3594
  }
3595
+ if(mark_written_record(gobj, topic_name, queued) > 0) {
3596
+ return;
3597
+ }
3598
+ let topic = current_topic(gobj);
3576
3599
  if(topic) {
3577
3600
  priv.written[topic.id] = true;
3578
3601
  }
@@ -1520,6 +1520,12 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1520
1520
  kw.__md_command__ = { // Data to be returned
1521
1521
  topic_name: topic_name,
1522
1522
  };
1523
+ /* The answer carries this echo and nothing else of the request: a
1524
+ * refused `__graphs__` write is told to the engine by the topic it
1525
+ * arranges, so that topic has to travel here. */
1526
+ if(topic_name === "__graphs__") {
1527
+ kw.__md_command__.graph_topic = graph_topic_of(record);
1528
+ }
1523
1529
 
1524
1530
  let ret = gobj_command(priv.gobj_remote_yuno,
1525
1531
  command,
@@ -2118,7 +2124,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2118
2124
  if(result < 0 && command === "update-node") {
2119
2125
  graphs_write_refused(gobj,
2120
2126
  kw_get_str(gobj, kw_command, "topic_name", "", 0),
2121
- kw_get_dict(gobj, kw_command, "record", {}, 0));
2127
+ kw_get_str(gobj, kw_command, "graph_topic", "", 0));
2122
2128
  }
2123
2129
  if(result >= 0 &&
2124
2130
  kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
@@ -2486,7 +2492,7 @@ function ac_update_node(gobj, event, kw, src)
2486
2492
  let options = kw.options || {};
2487
2493
 
2488
2494
  if(refuse_if_readonly(gobj, event)) {
2489
- graphs_write_refused(gobj, topic_name, record);
2495
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2490
2496
  return -1;
2491
2497
  }
2492
2498
 
@@ -2496,7 +2502,7 @@ function ac_update_node(gobj, event, kw, src)
2496
2502
  if(topic_name === "__graphs__" && !is_connected(gobj)) {
2497
2503
  log_warning(`${gobj_short_name(gobj)}: no session, the arrangement ` +
2498
2504
  `was not sent`);
2499
- graphs_write_refused(gobj, topic_name, record);
2505
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2500
2506
  return -1;
2501
2507
  }
2502
2508
 
@@ -2508,11 +2514,22 @@ function ac_update_node(gobj, event, kw, src)
2508
2514
  options
2509
2515
  );
2510
2516
  if(ret < 0) {
2511
- graphs_write_refused(gobj, topic_name, record);
2517
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2512
2518
  }
2513
2519
  return ret;
2514
2520
  }
2515
2521
 
2522
+ /************************************************************
2523
+ * The topic a `__graphs__` record arranges.
2524
+ ************************************************************/
2525
+ function graph_topic_of(record)
2526
+ {
2527
+ if(!record || typeof record !== "object") {
2528
+ return "";
2529
+ }
2530
+ return record.topic || record.id || "";
2531
+ }
2532
+
2516
2533
  /************************************************************
2517
2534
  * A write of `__graphs__` did not land. The engine
2518
2535
  * (C_G6_NODES_TREE) took it for granted when it left -- nothing
@@ -2520,14 +2537,13 @@ function ac_update_node(gobj, event, kw, src)
2520
2537
  * writes the topic again. Other topics are the operator's data:
2521
2538
  * the answer's error says what happened to them.
2522
2539
  ************************************************************/
2523
- function graphs_write_refused(gobj, topic_name, record)
2540
+ function graphs_write_refused(gobj, topic_name, topic)
2524
2541
  {
2525
2542
  let priv = gobj.priv;
2526
2543
 
2527
2544
  if(topic_name !== "__graphs__") {
2528
2545
  return;
2529
2546
  }
2530
- let topic = (record && typeof record === "object") ? (record.topic || record.id) : "";
2531
2547
  if(!topic) {
2532
2548
  log_error(`${gobj_short_name(gobj)}: a refused __graphs__ write names no topic`);
2533
2549
  return;
@@ -1501,3 +1501,164 @@ describe("EV_REFRESH while a write is in flight", () => {
1501
1501
  expect(not_defined()).toEqual([]);
1502
1502
  });
1503
1503
  });
1504
+
1505
+ describe("the body while a write is in flight", () => {
1506
+
1507
+ /* pointer-events stops the mouse and nothing else: the shell's
1508
+ * focus trap puts the focus back on the row control that had it,
1509
+ * and Delete or Enter there sent an action ST_SAVING does not
1510
+ * declare. `inert` takes the body out of the keyboard's reach too. */
1511
+ test("is inert, and answers again when the write ends", () => {
1512
+ const {editor, remote, host} = build("r16k1", "db/users");
1513
+ const $body = $in(editor, ".SCHEMA_BODY");
1514
+ expect($body.hasAttribute("inert")).toBe(false);
1515
+
1516
+ const write = start_a_write(editor, host, "name");
1517
+ expect($body.hasAttribute("inert")).toBe(true);
1518
+
1519
+ answer(editor, remote, write, 0, write.kw.record);
1520
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1521
+ expect($in(editor, ".SCHEMA_BODY").hasAttribute("inert")).toBe(false);
1522
+ expect(errors()).toEqual([]);
1523
+ });
1524
+
1525
+ test("a drop that ends the write gives the body back too", () => {
1526
+ const {editor, remote, host} = build("r16k2", "db/users");
1527
+ start_a_write(editor, host, "name");
1528
+ drop(editor, remote, host);
1529
+ expect($in(editor, ".SCHEMA_BODY").hasAttribute("inert")).toBe(false);
1530
+ });
1531
+ });
1532
+
1533
+ describe("the writes of an import mark the topic they write", () => {
1534
+
1535
+ /* An import that adds a topic, `roles`. */
1536
+ const IMPORT = JSON.stringify({id: "db", topics: [
1537
+ {id: "users", pkey: "id", cols: [
1538
+ {id: "id", type: "string", flag: ["persistent", "required"]}
1539
+ ]},
1540
+ {id: "roles", pkey: "id", cols: [
1541
+ {id: "id", type: "string", flag: ["persistent", "required"]}
1542
+ ]}
1543
+ ]});
1544
+
1545
+ /* The record as the store answers it: with the id it gave a new
1546
+ * one, and its fkeys in list_dict. */
1547
+ function as_the_store_answers(write)
1548
+ {
1549
+ const record = JSON.parse(JSON.stringify(write.kw.record));
1550
+ const to_dict = (refs) => refs.map((ref) => {
1551
+ const [topic_name, id, hook_name] = ref.split("^");
1552
+ return {topic_name, id, hook_name};
1553
+ });
1554
+ if(write.kw.topic_name === "topics") {
1555
+ record.id = record.id || `db.${record.value}`;
1556
+ record.treedbs = to_dict(record.treedbs || []);
1557
+ }
1558
+ if(write.kw.topic_name === "cols") {
1559
+ record.id = record.id || `${record.topics[0].split("^")[1]}.${record.value}`;
1560
+ record.topics = to_dict(record.topics || []);
1561
+ }
1562
+ return record;
1563
+ }
1564
+
1565
+ function run_the_import(editor, host)
1566
+ {
1567
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
1568
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
1569
+ model_gen: editor.priv.model_gen}, editor);
1570
+ expect(editor.priv.import_plan.writes.length).toBeGreaterThan(0);
1571
+ gobj_send_event(editor, "EV_APPLY_IMPORT", {model_gen: editor.priv.model_gen}, editor);
1572
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
1573
+ }
1574
+
1575
+ function answer_every_write(editor, remote)
1576
+ {
1577
+ for(let i = 0; i < 10; i++) {
1578
+ const [write] = take("update-node");
1579
+ if(!write) {
1580
+ break;
1581
+ }
1582
+ answer(editor, remote, write, 0, as_the_store_answers(write));
1583
+ }
1584
+ }
1585
+
1586
+ test("from another topic's screen: the topic it wrote, not the one on screen", () => {
1587
+ const {editor, remote, host} = build("r16m1", "db/users");
1588
+ run_the_import(editor, host);
1589
+ answer_every_write(editor, remote);
1590
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1591
+ expect(editor.priv.written).toEqual({"db.roles": true});
1592
+ expect(errors()).toEqual([]);
1593
+ });
1594
+
1595
+ test("from the topics screen, where no topic is on screen", () => {
1596
+ const {editor, remote, host} = build("r16m2", "db");
1597
+ run_the_import(editor, host);
1598
+ answer_every_write(editor, remote);
1599
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1600
+ expect(editor.priv.written).toEqual({"db.roles": true});
1601
+ expect(errors()).toEqual([]);
1602
+ });
1603
+
1604
+ test("a column answered with no record: the record it queued says the topic", () => {
1605
+ const {editor, remote, host} = build("r16m3", "db/users");
1606
+ run_the_import(editor, host);
1607
+ const [topic] = take("update-node");
1608
+ expect(topic.kw.topic_name).toBe("topics");
1609
+ answer(editor, remote, topic, 0, as_the_store_answers(topic));
1610
+ const [col] = take("update-node");
1611
+ expect(col.kw.topic_name).toBe("cols");
1612
+ answer(editor, remote, col, 0, null);
1613
+ expect(editor.priv.written).toEqual({"db.roles": true});
1614
+ });
1615
+ });
1616
+
1617
+ describe("the reason a load failed", () => {
1618
+
1619
+ test("is a key the notice translates, not a string frozen in one language", () => {
1620
+ const remote = gobj_create_service("r16e1_remote", "C_TEST_REMOTE", {}, yuno);
1621
+ gobj_change_state(remote, "ST_SESSION");
1622
+ const host = gobj_create("r16e1_host", "C_TEST_EDITOR_HOST", {}, yuno);
1623
+ const editor = gobj_create("r16e1_editor", "C_YUI_SCHEMA_EDITOR", {
1624
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1625
+ }, host);
1626
+ gobj_start(editor);
1627
+ for(const c of take("nodes")) {
1628
+ answer(editor, remote, c, -1, null, "");
1629
+ }
1630
+ expect(gobj_current_state(editor)).toBe("ST_IDLE");
1631
+ expect(editor.priv.load_error).toBe("cannot load the schemas");
1632
+ /* The notice already says it: no detail repeating the title. */
1633
+ expect($in(editor, ".SCHEMA_NOTICE_DETAIL")).toBe(null);
1634
+ });
1635
+
1636
+ test("the backend's own words travel as its key", () => {
1637
+ const remote = gobj_create_service("r16e2_remote", "C_TEST_REMOTE", {}, yuno);
1638
+ gobj_change_state(remote, "ST_SESSION");
1639
+ const host = gobj_create("r16e2_host", "C_TEST_EDITOR_HOST", {}, yuno);
1640
+ const editor = gobj_create("r16e2_editor", "C_YUI_SCHEMA_EDITOR", {
1641
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1642
+ }, host);
1643
+ gobj_start(editor);
1644
+ for(const c of take("nodes")) {
1645
+ answer(editor, remote, c, -1, null, "treedb not found");
1646
+ }
1647
+ const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
1648
+ expect($detail.getAttribute("data-i18n")).toBe("treedb not found");
1649
+ });
1650
+
1651
+ test("a load that cannot leave keeps its key", () => {
1652
+ const remote = gobj_create_service("r16e3_remote", "C_TEST_REMOTE", {}, yuno);
1653
+ const host = gobj_create("r16e3_host", "C_TEST_EDITOR_HOST", {}, yuno);
1654
+ const editor = gobj_create("r16e3_editor", "C_YUI_SCHEMA_EDITOR", {
1655
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1656
+ }, host);
1657
+ gobj_start(editor);
1658
+ gobj_change_state(remote, "ST_DISCONNECTED");
1659
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1660
+ expect(editor.priv.load_error).toBe("cannot reach the treedb");
1661
+ const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
1662
+ expect($detail.getAttribute("data-i18n")).toBe("cannot reach the treedb");
1663
+ });
1664
+ });
@@ -149,13 +149,19 @@ function save_arrangement(engine, topic)
149
149
  });
150
150
  }
151
151
 
152
+ /* The answer as the real transports build it: the command frame
153
+ * carries ONLY the request's `__md_command__` (C_IEVENT_CLI's
154
+ * mt_command, gui_agent's C_AGENT_TREEDB_LINK), never the request's
155
+ * own kw. Echoing the whole kw here once hid that the refusal read
156
+ * its topic from a `record` no real answer carries. */
152
157
  function answer(host, remote, request, result, comment)
153
158
  {
159
+ const md_command = request.kw.__md_command__ || {};
154
160
  gobj_send_event(host, "EV_MT_COMMAND_ANSWER", {
155
161
  result: result,
156
162
  comment: comment || "",
157
163
  data: null,
158
- __md_iev__: {command_stack: [{command: request.command, kw: request.kw}]}
164
+ __md_iev__: {command_stack: [{command: request.command, kw: md_command}]}
159
165
  }, remote);
160
166
  }
161
167
 
@@ -172,6 +178,7 @@ describe("a __graphs__ write that does not land is said to the engine", () => {
172
178
  expect(commands.length).toBe(1);
173
179
  answer(host, remote, commands[0], -1, "not authorized");
174
180
  expect(refused()).toEqual(["yunos"]);
181
+ expect(logged.filter((l) => /names no topic/.test(l.msg))).toEqual([]);
175
182
  });
176
183
 
177
184
  test("landed: nothing is said", () => {