@yuneta/gobj-ui 7.25.7 → 7.25.9

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.
@@ -135,6 +135,7 @@ import {
135
135
  col_enum,
136
136
  is_empty_value,
137
137
  fkey_ref,
138
+ parse_fkey_ref,
138
139
  next_order,
139
140
  moved_orders,
140
141
  } from "./schema_model.js";
@@ -202,10 +203,13 @@ SDATA_END()
202
203
  let PRIVATE_DATA = {
203
204
  /* What the store answered, and what it means */
204
205
  records: null, /* {treedbs, topics, cols} as they arrived */
206
+ records_before: null, /* the records of the model shown, while a load replaces them */
205
207
  model: null, /* build_schema_model() of the above */
208
+ model_gen: 0, /* which LOAD the model is: a dialog is stamped with it */
206
209
  pending: 0, /* `nodes` requests still in flight */
207
210
  load_error: "",
208
211
  load_round: 0, /* which load an answer belongs to: echoed as `round` */
212
+ load_keeps_written: false, /* the load in flight is not the host's: `written` survives it */
209
213
 
210
214
  /* Where the operator is. The url carries it; these mirror it. */
211
215
  treedb_id: "",
@@ -223,6 +227,7 @@ let PRIVATE_DATA = {
223
227
  /* Children and overlays */
224
228
  diagram_gobj: null,
225
229
  dialog: null,
230
+ dialog_gen: 0, /* the model_gen the open dialog was built on */
226
231
 
227
232
  /* A write, or a plan of them, in flight */
228
233
  save_queue: null,
@@ -443,6 +448,36 @@ function refuse_if_readonly(gobj, event)
443
448
  return true;
444
449
  }
445
450
 
451
+ /***************************************************************
452
+ * A decision taken on a model a load has since REPLACED: the Save
453
+ * of a form, a confirmation, an import. Each carries the
454
+ * `model_gen` it was built on (a number, not the model: a kw is
455
+ * dumped by the machine trace).
456
+ *
457
+ * Refused, and said. A column form sends every field it shows, so
458
+ * its Save after a reload wrote the OLD record over the newer one
459
+ * (fourth independent review, probe A3); a form whose column the
460
+ * reload removed answered "names no column" for ever. end_load()
461
+ * closes such a dialog when the load lands; this is the same rule
462
+ * for whatever arrives anyway -- a stale screen, a confirmation
463
+ * that was up, a keyboard.
464
+ *
465
+ * A kw with no stamp is not a dialog's and is not asked.
466
+ ***************************************************************/
467
+ function built_on_a_replaced_model(gobj, event, kw)
468
+ {
469
+ let priv = gobj.priv;
470
+
471
+ if(!kw || kw.model_gen === undefined || kw.model_gen === priv.model_gen) {
472
+ return false;
473
+ }
474
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, it was decided on ` +
475
+ `the schemas a reload replaced`);
476
+ yui_shell_show_error(yui_shell_of(gobj),
477
+ "the schemas were read again: open the dialog again", {t: t});
478
+ return true;
479
+ }
480
+
446
481
 
447
482
 
448
483
 
@@ -458,8 +493,31 @@ function refuse_if_readonly(gobj, event)
458
493
  * schema set of the yuno is here: everything below — the
459
494
  * drawing, the validation, the export — is computed from these
460
495
  * and asks the backend nothing more.
496
+ *
497
+ * THE LOADING SCREEN IS DRAWN, not assumed. ST_LOADING declares
498
+ * none of the actions of a screen, so the screen it replaces goes
499
+ * and the toolbar stops answering: left up, a click on a card, on
500
+ * Back or on the drawing answered "Event NOT DEFINED in state
501
+ * ST_LOADING" and was lost (third independent review). A dialog
502
+ * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
503
+ * and a load that LANDS closes it (end_load()): it was built on the
504
+ * model the load replaced, and its Save sends every field it shows.
505
+ * An import plan is forgotten here for the same reason: it is a list
506
+ * of writes computed against the model going away.
507
+ *
508
+ * `keep_written`: a load the host did not ask for -- the reconnect,
509
+ * a write that turned out done -- keeps what this session wrote.
510
+ * Only the host's EV_REFRESH (a Save sends it) forgets it.
511
+ *
512
+ * A load that cannot leave WHOLE did not happen (out of session it
513
+ * is not even asked): the records the model was built on are put
514
+ * back (emptied, the next write patched them into a model with no
515
+ * treedb), the round is spent so a request that did leave is
516
+ * stale, and the session back asks again. A load that left and
517
+ * FAILED keeps them the same way (`records_before`,
518
+ * ac_mt_command_answer()).
461
519
  ***************************************************************/
462
- function request_model(gobj)
520
+ function request_model(gobj, keep_written)
463
521
  {
464
522
  let priv = gobj.priv;
465
523
  let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
@@ -468,27 +526,45 @@ function request_model(gobj)
468
526
  return -1; /* not mounted on a transport yet: mt_start retries via EV_TRANSPORT_STATE */
469
527
  }
470
528
 
529
+ forget_import_plan(gobj);
530
+
531
+ let previous = priv.records;
532
+ priv.records_before = previous;
471
533
  priv.records = {treedbs: [], topics: [], cols: []};
472
534
  priv.load_error = "";
473
535
  priv.pending = 0;
474
536
  priv.load_round++;
537
+ priv.load_keeps_written = !!keep_written;
475
538
  gobj_change_state(gobj, "ST_LOADING");
476
- show_notice(gobj, "loading the schemas");
477
-
478
- for(let topic_name of [T_TREEDBS, T_TOPICS, T_COLS]) {
479
- if(remote_command(gobj, "nodes", {
480
- topic_name: topic_name,
481
- options: {list_dict: true}
482
- }, {round: priv.load_round}) === 0) {
483
- priv.pending++;
539
+ set_busy(gobj, false);
540
+ render(gobj);
541
+
542
+ /* Out of session nothing is asked: each request would come back
543
+ * refused and logged as an ERROR, for a drop that is not one. */
544
+ let left = 0;
545
+ if(transport_in_session(gobj)) {
546
+ for(let topic_name of [T_TREEDBS, T_TOPICS, T_COLS]) {
547
+ if(remote_command(gobj, "nodes", {
548
+ topic_name: topic_name,
549
+ options: {list_dict: true}
550
+ }, {round: priv.load_round}) === 0) {
551
+ left++;
552
+ }
484
553
  }
485
554
  }
486
- if(priv.pending === 0) {
555
+ if(left < 3) {
556
+ priv.records = previous;
557
+ priv.records_before = null;
558
+ priv.load_round++;
559
+ priv.reload_on_open = true;
487
560
  priv.load_error = t("cannot reach the treedb");
488
- gobj_change_state(gobj, "ST_IDLE");
489
- show_notice(gobj, "cannot reach the treedb");
561
+ if(priv.model) {
562
+ yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", {t: t});
563
+ }
564
+ end_load(gobj);
490
565
  return -1;
491
566
  }
567
+ priv.pending = left;
492
568
  return 0;
493
569
  }
494
570
 
@@ -571,17 +647,23 @@ function build_model(gobj)
571
647
  * From here on, "did this session change it?" has an answer.
572
648
  *
573
649
  * Called ONLY when records arrive from the store: that is what a
574
- * measurement of "changed since" can be measured against. Both
575
- * halves reset together -- a baseline without its `written` is
576
- * the same false positive from the other side, since every
577
- * version equals a baseline just taken.
650
+ * measurement of "changed since" can be measured against.
651
+ *
652
+ * `written` is forgotten only by a load the HOST asked for (its
653
+ * EV_REFRESH, which a Save sends). Since M36 a write moves no
654
+ * version, so `written` is the draft chip of what this session
655
+ * wrote and no longer half of a version measurement: a reload of
656
+ * the reconnect, or of a write that turned out done, forgot the
657
+ * chips of writes the host had not been told about.
578
658
  ***************************************************************/
579
659
  function start_measuring(gobj)
580
660
  {
581
661
  let priv = gobj.priv;
582
662
 
583
663
  priv.baseline = {};
584
- priv.written = {};
664
+ if(!priv.load_keeps_written) {
665
+ priv.written = {};
666
+ }
585
667
  priv.host_ids = {};
586
668
 
587
669
  if(!priv.model) {
@@ -605,9 +687,10 @@ function start_measuring(gobj)
605
687
  * until the host saves it (C_TREEDB's save-schema, which raises the
606
688
  * versions of what changed). Two sources say a topic is one, and
607
689
  * they are kept APART because they are forgotten differently:
608
- * - `written`: what THIS session wrote since the model was loaded
609
- * (a reload, the host's EV_REFRESH, forgets it -- the store says
610
- * what is left);
690
+ * - `written`: what THIS session wrote. Only a load the HOST asked
691
+ * for forgets it (its EV_REFRESH, which a Save sends); the
692
+ * reconnect's reload, the one owed after a write and a late
693
+ * write's keep it (start_measuring(), request_model());
611
694
  * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
612
695
  * by the next EV_DRAFTS. Folded into `written`, a topic the host
613
696
  * named once stayed a draft after the Save that published it.
@@ -805,12 +888,27 @@ function render_toolbar(gobj)
805
888
  let state = gobj_current_state(gobj);
806
889
  let readonly = is_readonly(gobj);
807
890
  let has_model = !!priv.model;
891
+ /* The position can outlive its screen (a load that failed keeps the
892
+ * url's; ST_EMPTY has a treedb in the url and none in the model),
893
+ * and a button there sends what only a treedb screen declares. */
894
+ let on_a_treedb = state !== "ST_IDLE" && state !== "ST_EMPTY";
895
+ /* ...and the screen can outlive the treedb or the topic it is ON: a
896
+ * reload that no longer has them keeps the position (it says so,
897
+ * and Back is the way out). Asked of the MODEL, not of the state:
898
+ * gated on the state, Diagram, Check, Export, Import and New topic
899
+ * stayed up there and each logged "with no treedb open" (fourth
900
+ * independent review, probe D2). */
901
+ let treedb_here = on_a_treedb && !!current_treedb(gobj);
902
+ let topic_here = treedb_here && !!current_topic(gobj);
903
+ /* Nothing is legal while the model is in the air: every action is
904
+ * computed against it. */
905
+ let in_flight = state === "ST_SAVING" || state === "ST_LOADING";
808
906
 
809
907
  /*----------------------------------------------*
810
908
  * Left: back, then the position as a trail.
811
909
  *----------------------------------------------*/
812
910
  let $left = [];
813
- if(priv.treedb_id) {
911
+ if(priv.treedb_id && on_a_treedb) {
814
912
  $left.push(["button", {class: "SCHEMA_BACK button",
815
913
  type: "button",
816
914
  title: t("back"), "aria-label": t("back"),
@@ -847,7 +945,7 @@ function render_toolbar(gobj)
847
945
  * refused says why when it is pressed.
848
946
  *----------------------------------------------*/
849
947
  let $right = [];
850
- if(priv.treedb_id && has_model) {
948
+ if(priv.treedb_id && has_model && treedb_here) {
851
949
  /* The compass, like every other door to a SCHEMA: the
852
950
  * `hexagon-nodes` it wore is the graph of the DATA. */
853
951
  $right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-compass-drafting",
@@ -861,7 +959,7 @@ function render_toolbar(gobj)
861
959
  "import", "EV_IMPORT", false));
862
960
  }
863
961
  }
864
- if(priv.topic_name && !readonly && !priv.diagram) {
962
+ if(priv.topic_name && !readonly && !priv.diagram && topic_here) {
865
963
  /* Only while there IS somewhere to go back to: a drag is a write,
866
964
  * and this is the way back from one. */
867
965
  if(undo_order_writes(gobj, current_topic(gobj)).length > 0) {
@@ -871,7 +969,7 @@ function render_toolbar(gobj)
871
969
  $right.push(toolbar_button("SCHEMA_ADD_COL_BTN", "yi-plus",
872
970
  "new column", "EV_ADD_COLUMN", false));
873
971
  }
874
- if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram) {
972
+ if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram && treedb_here) {
875
973
  $right.push(toolbar_button("SCHEMA_ADD_TOPIC_BTN", "yi-plus",
876
974
  "new topic", "EV_ADD_TOPIC", false));
877
975
  }
@@ -894,7 +992,7 @@ function render_toolbar(gobj)
894
992
  * destroyed. */
895
993
  for(let $button of $bar.querySelectorAll("button")) {
896
994
  $button.__gobj__ = gobj;
897
- if(state === "ST_SAVING") {
995
+ if(in_flight) {
898
996
  $button.disabled = true;
899
997
  }
900
998
  }
@@ -918,19 +1016,25 @@ function render(gobj)
918
1016
  }
919
1017
  render_toolbar(gobj);
920
1018
 
1019
+ /* A screen with nothing to draw takes the drawing down too: left
1020
+ * alive, it was only re-shown on the way back -- into a body that
1021
+ * no longer held it. */
921
1022
  if(state === "ST_LOADING") {
922
1023
  show_notice(gobj, "loading the schemas");
1024
+ destroy_diagram(gobj);
923
1025
  clear($body);
924
1026
  return;
925
1027
  }
926
1028
  if(state === "ST_IDLE") {
927
1029
  show_notice(gobj, priv.load_error ? "cannot load the schemas" : "not connected",
928
1030
  priv.load_error);
1031
+ destroy_diagram(gobj);
929
1032
  clear($body);
930
1033
  return;
931
1034
  }
932
1035
  if(state === "ST_EMPTY") {
933
1036
  show_notice(gobj, "this yuno stores no schema");
1037
+ destroy_diagram(gobj);
934
1038
  clear($body);
935
1039
  return;
936
1040
  }
@@ -1471,12 +1575,21 @@ function render_diagram(gobj)
1471
1575
  let $body = $of(gobj, ".SCHEMA_BODY");
1472
1576
  let treedb = current_treedb(gobj);
1473
1577
 
1474
- if(!$body || !treedb) {
1578
+ if(!$body) {
1579
+ return;
1580
+ }
1581
+ /* Said on an EMPTY body, as the topics and the columns say it:
1582
+ * render() clears the body for every screen but this one, and the
1583
+ * screen before stayed up under the notice, clickable. */
1584
+ if(!treedb) {
1585
+ destroy_diagram(gobj);
1586
+ clear($body);
1475
1587
  show_notice(gobj, "that treedb is not here any more", priv.treedb_id);
1476
1588
  return;
1477
1589
  }
1478
1590
  if(!gclass_find_by_name("C_YUI_TREEDB_SCHEMA")) {
1479
1591
  log_error(`${gobj_short_name(gobj)}: C_YUI_TREEDB_SCHEMA not registered by the app`);
1592
+ clear($body);
1480
1593
  show_notice(gobj, "the schema drawing is not available");
1481
1594
  return;
1482
1595
  }
@@ -1571,6 +1684,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1571
1684
  let priv = gobj.priv;
1572
1685
 
1573
1686
  close_dialog(gobj);
1687
+ priv.dialog_gen = priv.model_gen;
1574
1688
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
1575
1689
  dialog: true,
1576
1690
  title: title,
@@ -1619,6 +1733,10 @@ function name_control($spec, label)
1619
1733
  $spec[1]["aria-label"] = t(label);
1620
1734
  $spec[1]["data-i18n-aria-label"] = label;
1621
1735
  }
1736
+ if(!$spec[1].title) {
1737
+ $spec[1].title = t(label);
1738
+ $spec[1]["data-i18n-title"] = label;
1739
+ }
1622
1740
  return true;
1623
1741
  }
1624
1742
  for(let child of ($spec[2] || [])) {
@@ -1629,6 +1747,19 @@ function name_control($spec, label)
1629
1747
  return false;
1630
1748
  }
1631
1749
 
1750
+ /***************************************************************
1751
+ * The four attributes that NAME a control -- a title and an
1752
+ * aria-label, each with the key a language change re-translates
1753
+ * it by. A visible label is not a name (CLAUDE.md).
1754
+ ***************************************************************/
1755
+ function name_attrs(key, attrs)
1756
+ {
1757
+ return Object.assign({
1758
+ title: t(key), "data-i18n-title": key,
1759
+ "aria-label": t(key), "data-i18n-aria-label": key
1760
+ }, attrs);
1761
+ }
1762
+
1632
1763
  function field(logical, name, label, $control, help)
1633
1764
  {
1634
1765
  /* The label is a SIBLING of the control -- Bulma's `field` shape --
@@ -1782,6 +1913,7 @@ function open_column_form(gobj, topic, col, prefill)
1782
1913
  let hook_col = hook_topic ? hook[hook_topic] : "";
1783
1914
  let treedb = current_treedb(gobj);
1784
1915
  let sibling_topics = treedb ? treedb.topics.map(x => x.name) : [];
1916
+ let model_gen = priv.model_gen;
1785
1917
 
1786
1918
  let $form = createElement2(
1787
1919
  ["div", {class: "SCHEMA_COL_FORM box"}, [
@@ -1823,9 +1955,10 @@ function open_column_form(gobj, topic, col, prefill)
1823
1955
  text_input("description", record.description, "")),
1824
1956
  ["div", {class: "SCHEMA_COL_FORM_ACTIONS is-flex mt-4",
1825
1957
  style: "gap:.5rem; justify-content:flex-end;"}, [
1826
- ["button", {class: "SCHEMA_COL_FORM_CANCEL button", type: "button"},
1958
+ ["button", name_attrs("cancel", {class: "SCHEMA_COL_FORM_CANCEL button", type: "button"}),
1827
1959
  [["span", {i18n: "cancel"}, t("cancel")]]],
1828
- ["button", {class: "SCHEMA_COL_FORM_SAVE button is-primary", type: "button"},
1960
+ ["button", name_attrs("save", {class: "SCHEMA_COL_FORM_SAVE button is-primary",
1961
+ type: "button"}),
1829
1962
  [["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
1830
1963
  ["span", {i18n: "save"}, t("save")]]]
1831
1964
  ]]
@@ -1866,10 +1999,11 @@ function open_column_form(gobj, topic, col, prefill)
1866
1999
  let values = read_form($form);
1867
2000
  values.flag = read_flags($form);
1868
2001
  gobj_send_event(gobj, "EV_SAVE_COLUMN", {
1869
- topic: topic.name,
1870
- col: creating ? "" : col.name,
1871
- creating: creating,
1872
- values: values
2002
+ topic: topic.name,
2003
+ col: creating ? "" : col.name,
2004
+ creating: creating,
2005
+ values: values,
2006
+ model_gen: model_gen
1873
2007
  }, gobj);
1874
2008
  });
1875
2009
 
@@ -1902,6 +2036,7 @@ function open_topic_form(gobj, treedb, topic)
1902
2036
  let record = (topic && topic.record) || {};
1903
2037
  let creating = !topic;
1904
2038
  let col_names = topic ? topic.cols.map(c => c.name) : [];
2039
+ let model_gen = gobj.priv.model_gen;
1905
2040
 
1906
2041
  let $form = createElement2(
1907
2042
  ["div", {class: "SCHEMA_TOPIC_FORM box"}, [
@@ -1941,10 +2076,10 @@ function open_topic_form(gobj, treedb, topic)
1941
2076
  ]],
1942
2077
  ["div", {class: "SCHEMA_TOPIC_FORM_ACTIONS is-flex mt-4",
1943
2078
  style: "gap:.5rem; justify-content:flex-end;"}, [
1944
- ["button", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"},
2079
+ ["button", name_attrs("cancel", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"}),
1945
2080
  [["span", {i18n: "cancel"}, t("cancel")]]],
1946
- ["button", {class: "SCHEMA_TOPIC_FORM_SAVE button is-primary",
1947
- type: "button"},
2081
+ ["button", name_attrs("save", {class: "SCHEMA_TOPIC_FORM_SAVE button is-primary",
2082
+ type: "button"}),
1948
2083
  [["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
1949
2084
  ["span", {i18n: "save"}, t("save")]]]
1950
2085
  ]]
@@ -1958,9 +2093,10 @@ function open_topic_form(gobj, treedb, topic)
1958
2093
  $form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
1959
2094
  evt.stopPropagation();
1960
2095
  gobj_send_event(gobj, "EV_SAVE_TOPIC", {
1961
- topic: creating ? "" : topic.name,
1962
- creating: creating,
1963
- values: read_form($form)
2096
+ topic: creating ? "" : topic.name,
2097
+ creating: creating,
2098
+ values: read_form($form),
2099
+ model_gen: model_gen
1964
2100
  }, gobj);
1965
2101
  });
1966
2102
 
@@ -2113,14 +2249,15 @@ function copy_text(gobj, $text)
2113
2249
  ***************************************************************/
2114
2250
  function open_import(gobj, treedb)
2115
2251
  {
2252
+ let model_gen = gobj.priv.model_gen;
2116
2253
  let $content = createElement2(
2117
2254
  ["div", {class: "SCHEMA_IMPORT box"}, [
2118
2255
  ["p", {class: "SCHEMA_IMPORT_HELP help mb-2",
2119
2256
  i18n: "paste a schema as json: the plan is shown before anything is written"},
2120
2257
  t("paste a schema as json: the plan is shown before anything is written")],
2121
- ["textarea", {class: "SCHEMA_IMPORT_TEXT textarea is-small is-family-monospace",
2258
+ ["textarea", name_attrs("schema", {class: "SCHEMA_IMPORT_TEXT textarea is-small is-family-monospace",
2122
2259
  rows: "12", spellcheck: "false",
2123
- placeholder: '{"id": "...", "schema_version": "1", "topics": []}'}, ""],
2260
+ placeholder: '{"id": "...", "schema_version": "1", "topics": []}'}), ""],
2124
2261
  ["label", {class: "SCHEMA_IMPORT_PRUNE checkbox mt-2"}, [
2125
2262
  ["input", {type: "checkbox", class: "SCHEMA_IMPORT_PRUNE_BOX", checked: "checked"}],
2126
2263
  ["span", {class: "ml-1", i18n: "delete what the pasted schema does not declare"},
@@ -2129,13 +2266,13 @@ function open_import(gobj, treedb)
2129
2266
  ["div", {class: "SCHEMA_IMPORT_PLAN mt-3"}, []],
2130
2267
  ["div", {class: "SCHEMA_IMPORT_ACTIONS is-flex mt-3",
2131
2268
  style: "gap:.5rem; justify-content:flex-end;"}, [
2132
- ["button", {class: "SCHEMA_IMPORT_CANCEL button", type: "button"},
2269
+ ["button", name_attrs("cancel", {class: "SCHEMA_IMPORT_CANCEL button", type: "button"}),
2133
2270
  [["span", {i18n: "cancel"}, t("cancel")]]],
2134
- ["button", {class: "SCHEMA_IMPORT_PREVIEW button", type: "button"},
2271
+ ["button", name_attrs("preview", {class: "SCHEMA_IMPORT_PREVIEW button", type: "button"}),
2135
2272
  [["span", {class: "icon"}, [["i", {class: "yi-eye"}]]],
2136
2273
  ["span", {i18n: "preview"}, t("preview")]]],
2137
- ["button", {class: "SCHEMA_IMPORT_RUN button is-warning",
2138
- type: "button", disabled: "disabled"},
2274
+ ["button", name_attrs("import", {class: "SCHEMA_IMPORT_RUN button is-warning",
2275
+ type: "button", disabled: "disabled"}),
2139
2276
  [["span", {class: "icon"}, [["i", {class: "yi-upload"}]]],
2140
2277
  ["span", {i18n: "import"}, t("import")]]]
2141
2278
  ]]
@@ -2154,13 +2291,14 @@ function open_import(gobj, treedb)
2154
2291
  $content.querySelector(".SCHEMA_IMPORT_PREVIEW").addEventListener("click", (evt) => {
2155
2292
  evt.stopPropagation();
2156
2293
  gobj_send_event(gobj, "EV_PREVIEW_IMPORT", {
2157
- text: $text.value,
2158
- prune: $prune.checked
2294
+ text: $text.value,
2295
+ prune: $prune.checked,
2296
+ model_gen: model_gen
2159
2297
  }, gobj);
2160
2298
  });
2161
2299
  $run.addEventListener("click", (evt) => {
2162
2300
  evt.stopPropagation();
2163
- gobj_send_event(gobj, "EV_APPLY_IMPORT", {}, gobj);
2301
+ gobj_send_event(gobj, "EV_APPLY_IMPORT", {model_gen: model_gen}, gobj);
2164
2302
  });
2165
2303
 
2166
2304
  gobj.priv.import_pane = $plan_pane;
@@ -2168,6 +2306,23 @@ function open_import(gobj, treedb)
2168
2306
  open_dialog(gobj, $content, "import", "SCHEMA_IMPORT_DIALOG", treedb.id);
2169
2307
  }
2170
2308
 
2309
+ /***************************************************************
2310
+ * The plan is a list of writes computed against the model shown:
2311
+ * a load replaces the model, so the plan goes with it and the
2312
+ * dialog, if it stays up (a load that failed keeps the model),
2313
+ * asks for a Preview again.
2314
+ ***************************************************************/
2315
+ function forget_import_plan(gobj)
2316
+ {
2317
+ let priv = gobj.priv;
2318
+
2319
+ priv.import_plan = null;
2320
+ clear(priv.import_pane);
2321
+ if(priv.import_run) {
2322
+ priv.import_run.disabled = true;
2323
+ }
2324
+ }
2325
+
2171
2326
  /***************************************************************
2172
2327
  * The plan, drawn where the paste is: one line per write, the
2173
2328
  * deletions apart and counted, because they are what the
@@ -2244,7 +2399,7 @@ function open_orphans(gobj)
2244
2399
  ["td", {class: "SCHEMA_ORPHAN_ID"}, [["code", {}, `${entry.id}`]]],
2245
2400
  ["td", {class: "SCHEMA_ORPHAN_ACTIONS has-text-right"},
2246
2401
  readonly ? [] : [row_icon(gobj, "SCHEMA_ORPHAN_DELETE", "yi-trash", "delete",
2247
- "EV_DELETE_ORPHAN", {kind: kind, id: entry.id})]]
2402
+ "EV_DELETE_ORPHAN", {kind: kind, id: entry.id, model_gen: priv.model_gen})]]
2248
2403
  ]];
2249
2404
  };
2250
2405
 
@@ -2459,6 +2614,11 @@ function transport_dropped(gobj)
2459
2614
  priv.load_round++;
2460
2615
  priv.pending = 0;
2461
2616
  priv.load_error = "";
2617
+ if(priv.records_before) {
2618
+ /* The records of the model kept, not the half the load got. */
2619
+ priv.records = priv.records_before;
2620
+ priv.records_before = null;
2621
+ }
2462
2622
  return end_load(gobj);
2463
2623
  }
2464
2624
  return 0;
@@ -2470,6 +2630,12 @@ function transport_dropped(gobj)
2470
2630
  * in flight (ac_show() kept it), or the one it had. Without a
2471
2631
  * model there is no screen to go to: the position is kept for the
2472
2632
  * next load, and the editor says why it is empty.
2633
+ *
2634
+ * A dialog built on the model this load REPLACED is closed, and the
2635
+ * operator told to open it again: it shows the old record, and its
2636
+ * Save would write every field of it over the newer one. A load
2637
+ * that failed or never left replaced nothing (`model_gen` did not
2638
+ * move), and the dialog stays with what was typed.
2473
2639
  ***************************************************************/
2474
2640
  function end_load(gobj)
2475
2641
  {
@@ -2477,6 +2643,13 @@ function end_load(gobj)
2477
2643
  let seg = priv.pending_seg;
2478
2644
 
2479
2645
  priv.pending_seg = null;
2646
+ if(priv.dialog && priv.dialog_gen !== priv.model_gen) {
2647
+ log_warning(`${gobj_short_name(gobj)}: a dialog was open on the schemas ` +
2648
+ `the load replaced: closed`);
2649
+ close_dialog(gobj);
2650
+ yui_shell_show_error(yui_shell_of(gobj),
2651
+ "the schemas were read again: open the dialog again", {t: t});
2652
+ }
2480
2653
  if(priv.model) {
2481
2654
  if(seg !== null) {
2482
2655
  return apply_seg(gobj, seg);
@@ -2501,29 +2674,15 @@ function end_writes(gobj, error)
2501
2674
  {
2502
2675
  let priv = gobj.priv;
2503
2676
 
2677
+ let seg = priv.pending_seg;
2678
+
2504
2679
  priv.save_queue = [];
2680
+ priv.pending_seg = null;
2505
2681
  gobj_change_state(gobj, priv.save_return || "ST_TREEDBS");
2506
2682
  priv.save_return = "";
2507
2683
  show_notice(gobj, "");
2508
2684
  set_busy(gobj, false);
2509
2685
 
2510
- /* A position that arrived while the write was in flight: applied
2511
- * now, so a Back pressed mid-save is not simply lost. */
2512
- if(priv.pending_seg !== null) {
2513
- let seg = priv.pending_seg;
2514
- priv.pending_seg = null;
2515
- if(!error) {
2516
- apply_seg(gobj, seg);
2517
- if(priv.save_wrote) {
2518
- gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2519
- treedb_name: gobj_read_str_attr(gobj, "treedb_name")
2520
- });
2521
- }
2522
- priv.save_wrote = false;
2523
- return reload_if_owed(gobj);
2524
- }
2525
- }
2526
-
2527
2686
  if(error) {
2528
2687
  yui_shell_show_error(yui_shell_of(gobj), error, {t: t});
2529
2688
  }
@@ -2533,8 +2692,18 @@ function end_writes(gobj, error)
2533
2692
  });
2534
2693
  }
2535
2694
  priv.save_wrote = false;
2536
- render(gobj);
2537
- publish_check(gobj);
2695
+
2696
+ /* A position that arrived while the write was in flight: applied
2697
+ * now, so a Back pressed mid-save is not simply lost -- with an
2698
+ * error too. The host's url already says it, and a load cut by a
2699
+ * drop applies it (end_load()): the write kept the old screen, and
2700
+ * the url and the view disagreed. */
2701
+ if(seg !== null) {
2702
+ apply_seg(gobj, seg);
2703
+ } else {
2704
+ render(gobj);
2705
+ publish_check(gobj);
2706
+ }
2538
2707
  reload_if_owed(gobj);
2539
2708
  return error ? -1 : 0;
2540
2709
  }
@@ -2556,7 +2725,7 @@ function reload_if_owed(gobj)
2556
2725
  priv.reload_on_open = true;
2557
2726
  return 0;
2558
2727
  }
2559
- return request_model(gobj);
2728
+ return request_model(gobj, true);
2560
2729
  }
2561
2730
 
2562
2731
  /***************************************************************
@@ -2660,6 +2829,9 @@ function go(gobj, treedb_id, topic_name, diagram, mirror)
2660
2829
  {
2661
2830
  let priv = gobj.priv;
2662
2831
 
2832
+ /* Moving IS the answer to a position that waited: one left behind
2833
+ * sent a later load back to it. */
2834
+ priv.pending_seg = null;
2663
2835
  priv.treedb_id = treedb_id || "";
2664
2836
  priv.topic_name = topic_name || "";
2665
2837
  priv.diagram = !!diagram;
@@ -2826,6 +2998,23 @@ function ac_mt_command_answer(gobj, event, kw, src)
2826
2998
  return 0;
2827
2999
  }
2828
3000
  if(priv.load_error) {
3001
+ /* Refused IN session (a deadline of a routing adapter on a
3002
+ * slow `nodes`): the model shown is what the store held a
3003
+ * moment ago, and blanking it took the screen and every open
3004
+ * form with it -- a form's Save then answered "Event NOT
3005
+ * DEFINED in state ST_IDLE" (fourth independent review). It
3006
+ * stays, as it does when the load cannot leave, and the
3007
+ * reload is owed. */
3008
+ if(priv.model) {
3009
+ log_warning(`${gobj_short_name(gobj)}: the schemas could not be read ` +
3010
+ `again (${priv.load_error}): the previous ones stay`);
3011
+ priv.records = priv.records_before;
3012
+ priv.records_before = null;
3013
+ priv.reload_on_open = true;
3014
+ yui_shell_show_error(yui_shell_of(gobj),
3015
+ "cannot read the schemas again: the previous ones stay", {t: t});
3016
+ return end_load(gobj);
3017
+ }
2829
3018
  priv.model = null;
2830
3019
  return end_load(gobj);
2831
3020
  }
@@ -2834,6 +3023,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
2834
3023
  * (NOT in build_model(): that also runs on the patch after every
2835
3024
  * write, which is exactly when the undo has to survive.) */
2836
3025
  priv.order_undo = {};
3026
+ priv.records_before = null;
3027
+ priv.model_gen++;
2837
3028
  build_model(gobj);
2838
3029
  /* Same reason as order_undo above: this is the LOAD, and the
2839
3030
  * baseline is what a later "the version did not move" is measured
@@ -2857,7 +3048,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2857
3048
  `for a write that is over (result ${result}): ignored`);
2858
3049
  return 0;
2859
3050
  }
2860
- return late_write_done(gobj, command, topic_name);
3051
+ return late_write_done(gobj, command, topic_name, data);
2861
3052
  }
2862
3053
  if(result < 0) {
2863
3054
  return end_writes(gobj, comment || t("the treedb refused the write"));
@@ -2872,10 +3063,33 @@ function ac_mt_command_answer(gobj, event, kw, src)
2872
3063
  patch_record(gobj, topic_name, record);
2873
3064
  } else {
2874
3065
  /* The treedb wrote it and did not describe it back: ask
2875
- * again rather than draw a model that has drifted. */
2876
- log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record`);
3066
+ * again rather than draw a model that has drifted. What
3067
+ * was queued after it is not sent -- it was computed on
3068
+ * the model that drifted -- and that is SAID: dropped in
3069
+ * silence, a drag of five columns stopped at the first.
3070
+ * The reload is also the one a late write may have owed
3071
+ * (reload_after_write): left set, the next write loaded
3072
+ * the whole model once more (fourth independent review). */
3073
+ let not_sent = priv.save_queue.length - 1;
3074
+ log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record` +
3075
+ (not_sent > 0 ? `: ${not_sent} write(s) after it were not sent` : ""));
3076
+ if(not_sent > 0) {
3077
+ yui_shell_show_error(yui_shell_of(gobj),
3078
+ "the treedb did not describe a write back: the writes after it were not sent",
3079
+ {t: t});
3080
+ }
3081
+ if(topic_name === T_TOPICS || topic_name === T_COLS) {
3082
+ let write = priv.save_queue[0];
3083
+ mark_written(gobj, topic_name, write ? write.record : null);
3084
+ }
3085
+ priv.reload_after_write = false;
2877
3086
  priv.save_queue = [];
2878
- request_model(gobj);
3087
+ priv.save_return = "";
3088
+ priv.save_wrote = false;
3089
+ gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
3090
+ treedb_name: gobj_read_str_attr(gobj, "treedb_name")
3091
+ });
3092
+ request_model(gobj, true);
2879
3093
  return 0;
2880
3094
  }
2881
3095
  }
@@ -2907,14 +3121,24 @@ function ac_mt_command_answer(gobj, event, kw, src)
2907
3121
  * the view was answered once, as failed -- and echoes the node
2908
3122
  * event instead, which this view does not hear: there, Refresh is
2909
3123
  * what reads the store.
3124
+ *
3125
+ * It is a WRITE like any other for the host (EV_RECORD_WRITTEN):
3126
+ * the host was told nothing when it was given up, and it owns the
3127
+ * Save that publishes it. The reload keeps what this session wrote
3128
+ * (request_model()), and the record it answered marks its topic.
2910
3129
  ***************************************************************/
2911
- function late_write_done(gobj, command, topic_name)
3130
+ function late_write_done(gobj, command, topic_name, data)
2912
3131
  {
2913
3132
  let priv = gobj.priv;
2914
3133
  let state = gobj_current_state(gobj);
2915
3134
 
2916
3135
  log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' was given up, ` +
2917
3136
  `and it was DONE: the model is read again`);
3137
+ mark_written_record(gobj, topic_name, is_object(data) ? data
3138
+ : (Array.isArray(data) && is_object(data[0]) ? data[0] : null));
3139
+ gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
3140
+ treedb_name: gobj_read_str_attr(gobj, "treedb_name")
3141
+ });
2918
3142
  if(state === "ST_LOADING") {
2919
3143
  return 0;
2920
3144
  }
@@ -2926,7 +3150,38 @@ function late_write_done(gobj, command, topic_name)
2926
3150
  priv.reload_on_open = true;
2927
3151
  return 0;
2928
3152
  }
2929
- return request_model(gobj);
3153
+ return request_model(gobj, true);
3154
+ }
3155
+
3156
+ /***************************************************************
3157
+ * The topic a record written belongs to, read off the RECORD and
3158
+ * not off the screen: a write answered late may be about a topic
3159
+ * the operator has left. A column names its topic in its fkey; a
3160
+ * topic is its own id.
3161
+ *
3162
+ * The fkey is read with parse_fkey_ref(), because the store answers
3163
+ * in list_dict -- `[{topic_name, id, hook_name}]` -- and not in the
3164
+ * `topics^<topic id>^cols` strings this view writes. Split as a
3165
+ * string, every production answer marked nothing (fourth independent
3166
+ * review, probe C).
3167
+ ***************************************************************/
3168
+ function mark_written_record(gobj, topic_name, record)
3169
+ {
3170
+ let priv = gobj.priv;
3171
+
3172
+ if(!record) {
3173
+ return;
3174
+ }
3175
+ if(topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
3176
+ priv.written[record.id] = true;
3177
+ return;
3178
+ }
3179
+ if(topic_name !== T_COLS) {
3180
+ return;
3181
+ }
3182
+ for(let ref of parse_fkey_ref(record.topics)) {
3183
+ priv.written[ref.id] = true;
3184
+ }
2930
3185
  }
2931
3186
 
2932
3187
  /***************************************************************
@@ -3004,7 +3259,7 @@ function ac_transport_state(gobj, event, kw, src)
3004
3259
  if((!priv.model || priv.reload_on_open) &&
3005
3260
  state !== "ST_LOADING" && state !== "ST_SAVING") {
3006
3261
  priv.reload_on_open = false;
3007
- return request_model(gobj);
3262
+ return request_model(gobj, true);
3008
3263
  }
3009
3264
  render_toolbar(gobj);
3010
3265
  return 0;
@@ -3023,7 +3278,33 @@ function ac_language_changed(gobj, event, kw, src)
3023
3278
 
3024
3279
  function ac_refresh(gobj, event, kw, src)
3025
3280
  {
3026
- return request_model(gobj);
3281
+ return request_model(gobj, false);
3282
+ }
3283
+
3284
+ /***************************************************************
3285
+ * What a DIALOG sends while the model is being read again: the
3286
+ * Save of a form left open when the reload started, a
3287
+ * confirmation, an import. The loading screen took the rest of
3288
+ * the view away (request_model()); a dialog is the one thing the
3289
+ * operator can still reach, and what it asks is computed against
3290
+ * a model the answers in the air are about to replace.
3291
+ *
3292
+ * Refused, and SAID. Undeclared, it answered "Event NOT DEFINED
3293
+ * in state ST_LOADING" and the edit was gone with no word (third
3294
+ * independent review).
3295
+ *
3296
+ * And NOT "try again": the same Save, once the load was in, sent
3297
+ * the form built on the old model -- every field of the old record
3298
+ * over the newer one (fourth independent review). The load that
3299
+ * lands closes such a dialog (end_load()); one that fails leaves it
3300
+ * up, on the model it was built on.
3301
+ ***************************************************************/
3302
+ function ac_wait_for_the_load(gobj, event, kw, src)
3303
+ {
3304
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas are loading`);
3305
+ yui_shell_show_error(yui_shell_of(gobj),
3306
+ "the schemas are loading: wait for them", {t: t});
3307
+ return -1;
3027
3308
  }
3028
3309
 
3029
3310
  /***************************************************************
@@ -3116,6 +3397,7 @@ function ac_show_orphans(gobj, event, kw, src)
3116
3397
  ***************************************************************/
3117
3398
  function confirm_then(gobj, message, detail, kw)
3118
3399
  {
3400
+ kw = Object.assign({model_gen: gobj.priv.model_gen}, kw);
3119
3401
  yui_shell_confirm_danger(yui_shell_of(gobj), message, {
3120
3402
  t: t,
3121
3403
  detail: detail
@@ -3201,6 +3483,9 @@ function ac_save_column(gobj, event, kw, src)
3201
3483
  if(refuse_if_readonly(gobj, event)) {
3202
3484
  return -1;
3203
3485
  }
3486
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3487
+ return -1;
3488
+ }
3204
3489
  if(!topic || (!kw.creating && !col)) {
3205
3490
  log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.topic}.${kw.col}`);
3206
3491
  return -1;
@@ -3355,6 +3640,9 @@ function ac_save_topic(gobj, event, kw, src)
3355
3640
  if(refuse_if_readonly(gobj, event)) {
3356
3641
  return -1;
3357
3642
  }
3643
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3644
+ return -1;
3645
+ }
3358
3646
  if(!treedb || (!kw.creating && !topic)) {
3359
3647
  log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
3360
3648
  return -1;
@@ -3409,6 +3697,9 @@ function ac_delete_orphan(gobj, event, kw, src)
3409
3697
  if(refuse_if_readonly(gobj, event)) {
3410
3698
  return -1;
3411
3699
  }
3700
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3701
+ return -1;
3702
+ }
3412
3703
  if(empty_string(kw.id)) {
3413
3704
  log_error(`${gobj_short_name(gobj)}: ${event} names nothing`);
3414
3705
  return -1;
@@ -3481,6 +3772,9 @@ function ac_preview_import(gobj, event, kw, src)
3481
3772
  let treedb = current_treedb(gobj);
3482
3773
  let incoming = null;
3483
3774
 
3775
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3776
+ return -1;
3777
+ }
3484
3778
  if(!treedb) {
3485
3779
  log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
3486
3780
  return -1;
@@ -3508,6 +3802,9 @@ function ac_apply_import(gobj, event, kw, src)
3508
3802
  if(refuse_if_readonly(gobj, event)) {
3509
3803
  return -1;
3510
3804
  }
3805
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3806
+ return -1;
3807
+ }
3511
3808
  if(!plan || plan.writes.length === 0) {
3512
3809
  log_error(`${gobj_short_name(gobj)}: ${event} with no plan to run`);
3513
3810
  return -1;
@@ -3558,6 +3855,9 @@ function ac_confirmed(gobj, event, kw, src)
3558
3855
  if(refuse_if_readonly(gobj, event)) {
3559
3856
  return -1;
3560
3857
  }
3858
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3859
+ return -1;
3860
+ }
3561
3861
  if(kw.what === "import") {
3562
3862
  return run_import(gobj);
3563
3863
  }
@@ -3655,7 +3955,14 @@ function create_gclass(gclass_name)
3655
3955
  const states = [
3656
3956
  ["ST_IDLE", COMMON.slice()],
3657
3957
 
3658
- ["ST_LOADING", COMMON.slice()],
3958
+ ["ST_LOADING", COMMON.concat([
3959
+ ["EV_SAVE_COLUMN", ac_wait_for_the_load, null],
3960
+ ["EV_SAVE_TOPIC", ac_wait_for_the_load, null],
3961
+ ["EV_DELETE_ORPHAN", ac_wait_for_the_load, null],
3962
+ ["EV_PREVIEW_IMPORT", ac_wait_for_the_load, null],
3963
+ ["EV_APPLY_IMPORT", ac_wait_for_the_load, null],
3964
+ ["EV_CONFIRMED", ac_wait_for_the_load, null]
3965
+ ])],
3659
3966
 
3660
3967
  ["ST_EMPTY", COMMON.slice()],
3661
3968