@yuneta/gobj-ui 7.25.8 → 7.25.10

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.
@@ -26,9 +26,10 @@
26
26
  * the owner's design of M36 (2026-09-21 review) every write here
27
27
  * raised both versions, so an edit half made was already the
28
28
  * schema of the next start. The topic list marks what this session
29
- * wrote and has not saved (the host's EV_REFRESH after a save is
30
- * what forgets it) and what the host says is not saved (EV_DRAFTS,
31
- * replaced whole each time it is sent).
29
+ * wrote and has not saved (EV_REFRESH forgets it: the view's own
30
+ * Refresh button sends it, and a host may after a save -- the
31
+ * agent console's does not) and what the host says is not saved
32
+ * (EV_DRAFTS, replaced whole each time it is sent).
32
33
  *
33
34
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
34
35
  *
@@ -124,6 +125,7 @@ import {yui_tint} from "./bulma_tint.js";
124
125
  import {host_draft_ids} from "./host_drafts.js";
125
126
 
126
127
  import "./c_yui_schema_editor.css";
128
+ import "./yui_toolbar.css"; /* `selected_state`, the export's chosen view */
127
129
 
128
130
  import {
129
131
  build_schema_model,
@@ -135,6 +137,7 @@ import {
135
137
  col_enum,
136
138
  is_empty_value,
137
139
  fkey_ref,
140
+ parse_fkey_ref,
138
141
  next_order,
139
142
  moved_orders,
140
143
  } from "./schema_model.js";
@@ -202,7 +205,9 @@ SDATA_END()
202
205
  let PRIVATE_DATA = {
203
206
  /* What the store answered, and what it means */
204
207
  records: null, /* {treedbs, topics, cols} as they arrived */
208
+ records_before: null, /* the records of the model shown, while a load replaces them */
205
209
  model: null, /* build_schema_model() of the above */
210
+ model_gen: 0, /* which LOAD the model is: a dialog is stamped with it */
206
211
  pending: 0, /* `nodes` requests still in flight */
207
212
  load_error: "",
208
213
  load_round: 0, /* which load an answer belongs to: echoed as `round` */
@@ -224,6 +229,7 @@ let PRIVATE_DATA = {
224
229
  /* Children and overlays */
225
230
  diagram_gobj: null,
226
231
  dialog: null,
232
+ dialog_gen: 0, /* the model_gen the open dialog was built on */
227
233
 
228
234
  /* A write, or a plan of them, in flight */
229
235
  save_queue: null,
@@ -444,6 +450,90 @@ function refuse_if_readonly(gobj, event)
444
450
  return true;
445
451
  }
446
452
 
453
+ /***************************************************************
454
+ * A decision taken on a model a load has since REPLACED: the Save
455
+ * of a form, a confirmation, an import. Each carries the
456
+ * `model_gen` it was built on (a number, not the model: a kw is
457
+ * dumped by the machine trace).
458
+ *
459
+ * Refused, and said. A column form sends every field it shows, so
460
+ * its Save after a reload wrote the OLD record over the newer one
461
+ * (fourth independent review, probe A3); a form whose column the
462
+ * reload removed answered "names no column" for ever. end_load()
463
+ * closes such a dialog when the load lands; this is the same rule
464
+ * for whatever arrives anyway -- a stale screen, a confirmation
465
+ * that was up, a keyboard.
466
+ *
467
+ * A kw with no stamp is not a dialog's and is not asked.
468
+ ***************************************************************/
469
+ function built_on_a_replaced_model(gobj, event, kw)
470
+ {
471
+ let priv = gobj.priv;
472
+
473
+ if(!kw || kw.model_gen === undefined || kw.model_gen === priv.model_gen) {
474
+ return false;
475
+ }
476
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, it was decided on ` +
477
+ `the schemas a reload replaced`);
478
+ yui_shell_show_error(yui_shell_of(gobj),
479
+ "the schemas were read again: open the dialog again", {t: t});
480
+ return true;
481
+ }
482
+
483
+ /***************************************************************
484
+ * The reload a load refused IN session left owed (a routing
485
+ * adapter's deadline on a slow `nodes`, ac_mt_command_answer()):
486
+ * the model shown is kept, and it may be older than the store.
487
+ * The session is up, so no reconnect will ask for it again -- it
488
+ * was owed for as long as the session lasted, and a form opened
489
+ * on that model wrote every field of it over the newer record
490
+ * (fifth independent review; A3 of the fourth, by another path).
491
+ *
492
+ * So the operator's NEXT action asks it. An edit -- a form, a
493
+ * write, a confirmation, what reads the model whole (check,
494
+ * export) -- is refused and said: it would be computed on what
495
+ * the store may no longer hold, and the load that lands closes a
496
+ * dialog built on the old model anyway (end_load()). A move is
497
+ * not refused (reload_after_move()): it goes, and reads there.
498
+ *
499
+ * Not on a timer, and not at once: a load refused on a deadline
500
+ * asked again at once is refused the same way, and one asked on a
501
+ * clock is polling. Out of session nothing is asked; the reconnect
502
+ * does it (ac_transport_state()). The reload keeps this session's
503
+ * draft chips: the host did not ask for it.
504
+ ***************************************************************/
505
+ function owes_reload(gobj)
506
+ {
507
+ let state = gobj_current_state(gobj);
508
+
509
+ return gobj.priv.reload_on_open && transport_in_session(gobj) &&
510
+ state !== "ST_LOADING" && state !== "ST_SAVING";
511
+ }
512
+
513
+ function reload_first(gobj, event)
514
+ {
515
+ if(!owes_reload(gobj)) {
516
+ return false;
517
+ }
518
+ gobj.priv.reload_on_open = false;
519
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas shown ` +
520
+ `may be out of date: they are read again first`);
521
+ yui_shell_show_error(yui_shell_of(gobj),
522
+ "the schemas shown may be out of date: they are read again, try again when they are in",
523
+ {t: t});
524
+ request_model(gobj, true);
525
+ return true;
526
+ }
527
+
528
+ function reload_after_move(gobj)
529
+ {
530
+ if(!owes_reload(gobj)) {
531
+ return 0;
532
+ }
533
+ gobj.priv.reload_on_open = false;
534
+ return request_model(gobj, true);
535
+ }
536
+
447
537
 
448
538
 
449
539
 
@@ -465,17 +555,26 @@ function refuse_if_readonly(gobj, event)
465
555
  * and the toolbar stops answering: left up, a click on a card, on
466
556
  * Back or on the drawing answered "Event NOT DEFINED in state
467
557
  * ST_LOADING" and was lost (third independent review). A dialog
468
- * left open is answered in ST_LOADING instead (ac_wait_for_the_load).
558
+ * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
559
+ * and a load that LANDS closes it (end_load()): it was built on the
560
+ * model the load replaced, and its Save sends every field it shows.
561
+ * An import plan is forgotten here for the same reason: it is a list
562
+ * of writes computed against the model going away -- once the load
563
+ * has LEFT (one that could not be sent replaces nothing).
469
564
  *
470
- * `keep_written`: a load the host did not ask for -- the reconnect,
471
- * a write that turned out done -- keeps what this session wrote.
472
- * Only the host's EV_REFRESH (a Save sends it) forgets it.
565
+ * `keep_written`: a load nobody ASKED for -- the reconnect, a write
566
+ * that turned out done, the reload a refused load owed -- keeps what
567
+ * this session wrote. Only EV_REFRESH forgets it: the view's own
568
+ * Refresh button, or a host that sends it (after a Save, say; the
569
+ * agent console does not).
473
570
  *
474
571
  * A load that cannot leave WHOLE did not happen (out of session it
475
572
  * is not even asked): the records the model was built on are put
476
573
  * back (emptied, the next write patched them into a model with no
477
574
  * treedb), the round is spent so a request that did leave is
478
- * stale, and the session back asks again.
575
+ * stale, and the session back asks again. A load that left and
576
+ * FAILED keeps them the same way (`records_before`,
577
+ * ac_mt_command_answer()).
479
578
  ***************************************************************/
480
579
  function request_model(gobj, keep_written)
481
580
  {
@@ -486,7 +585,13 @@ function request_model(gobj, keep_written)
486
585
  return -1; /* not mounted on a transport yet: mt_start retries via EV_TRANSPORT_STATE */
487
586
  }
488
587
 
489
- let previous = priv.records;
588
+ /* A load asked while one is in flight (the host's EV_REFRESH is
589
+ * heard in ST_LOADING) keeps the records the model SHOWN was built
590
+ * on: `priv.records` is the half the first load got by then, and
591
+ * put back after a failure it was a model with no treedb. */
592
+ let previous = (gobj_current_state(gobj) === "ST_LOADING")?
593
+ priv.records_before : priv.records;
594
+ priv.records_before = previous;
490
595
  priv.records = {treedbs: [], topics: [], cols: []};
491
596
  priv.load_error = "";
492
597
  priv.pending = 0;
@@ -511,6 +616,7 @@ function request_model(gobj, keep_written)
511
616
  }
512
617
  if(left < 3) {
513
618
  priv.records = previous;
619
+ priv.records_before = null;
514
620
  priv.load_round++;
515
621
  priv.reload_on_open = true;
516
622
  priv.load_error = t("cannot reach the treedb");
@@ -520,6 +626,9 @@ function request_model(gobj, keep_written)
520
626
  end_load(gobj);
521
627
  return -1;
522
628
  }
629
+ /* Only a load that LEFT replaces the model the plan was computed
630
+ * on; one that could not be sent keeps it, and the plan with it. */
631
+ forget_import_plan(gobj);
523
632
  priv.pending = left;
524
633
  return 0;
525
634
  }
@@ -605,8 +714,8 @@ function build_model(gobj)
605
714
  * Called ONLY when records arrive from the store: that is what a
606
715
  * measurement of "changed since" can be measured against.
607
716
  *
608
- * `written` is forgotten only by a load the HOST asked for (its
609
- * EV_REFRESH, which a Save sends). Since M36 a write moves no
717
+ * `written` is forgotten only by an EV_REFRESH (the view's own
718
+ * Refresh button, or a host that sends one). Since M36 a write moves no
610
719
  * version, so `written` is the draft chip of what this session
611
720
  * wrote and no longer half of a version measurement: a reload of
612
721
  * the reconnect, or of a write that turned out done, forgot the
@@ -643,9 +752,12 @@ function start_measuring(gobj)
643
752
  * until the host saves it (C_TREEDB's save-schema, which raises the
644
753
  * versions of what changed). Two sources say a topic is one, and
645
754
  * they are kept APART because they are forgotten differently:
646
- * - `written`: what THIS session wrote since the model was loaded
647
- * (a reload, the host's EV_REFRESH, forgets it -- the store says
648
- * what is left);
755
+ * - `written`: what THIS session wrote. Only EV_REFRESH forgets
756
+ * it -- the view's own Refresh button sends it (in the agent
757
+ * console that is the only sender), and a host may after a
758
+ * Save; the reconnect's reload, the one owed after a write, a
759
+ * refused load's and a late write's keep it (start_measuring(),
760
+ * request_model());
649
761
  * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
650
762
  * by the next EV_DRAFTS. Folded into `written`, a topic the host
651
763
  * named once stayed a draft after the Save that published it.
@@ -847,6 +959,14 @@ function render_toolbar(gobj)
847
959
  * url's; ST_EMPTY has a treedb in the url and none in the model),
848
960
  * and a button there sends what only a treedb screen declares. */
849
961
  let on_a_treedb = state !== "ST_IDLE" && state !== "ST_EMPTY";
962
+ /* ...and the screen can outlive the treedb or the topic it is ON: a
963
+ * reload that no longer has them keeps the position (it says so,
964
+ * and Back is the way out). Asked of the MODEL, not of the state:
965
+ * gated on the state, Diagram, Check, Export, Import and New topic
966
+ * stayed up there and each logged "with no treedb open" (fourth
967
+ * independent review, probe D2). */
968
+ let treedb_here = on_a_treedb && !!current_treedb(gobj);
969
+ let topic_here = treedb_here && !!current_topic(gobj);
850
970
  /* Nothing is legal while the model is in the air: every action is
851
971
  * computed against it. */
852
972
  let in_flight = state === "ST_SAVING" || state === "ST_LOADING";
@@ -892,7 +1012,7 @@ function render_toolbar(gobj)
892
1012
  * refused says why when it is pressed.
893
1013
  *----------------------------------------------*/
894
1014
  let $right = [];
895
- if(priv.treedb_id && has_model && on_a_treedb) {
1015
+ if(priv.treedb_id && has_model && treedb_here) {
896
1016
  /* The compass, like every other door to a SCHEMA: the
897
1017
  * `hexagon-nodes` it wore is the graph of the DATA. */
898
1018
  $right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-compass-drafting",
@@ -906,7 +1026,7 @@ function render_toolbar(gobj)
906
1026
  "import", "EV_IMPORT", false));
907
1027
  }
908
1028
  }
909
- if(priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
1029
+ if(priv.topic_name && !readonly && !priv.diagram && topic_here) {
910
1030
  /* Only while there IS somewhere to go back to: a drag is a write,
911
1031
  * and this is the way back from one. */
912
1032
  if(undo_order_writes(gobj, current_topic(gobj)).length > 0) {
@@ -916,7 +1036,7 @@ function render_toolbar(gobj)
916
1036
  $right.push(toolbar_button("SCHEMA_ADD_COL_BTN", "yi-plus",
917
1037
  "new column", "EV_ADD_COLUMN", false));
918
1038
  }
919
- if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
1039
+ if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram && treedb_here) {
920
1040
  $right.push(toolbar_button("SCHEMA_ADD_TOPIC_BTN", "yi-plus",
921
1041
  "new topic", "EV_ADD_TOPIC", false));
922
1042
  }
@@ -1631,6 +1751,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1631
1751
  let priv = gobj.priv;
1632
1752
 
1633
1753
  close_dialog(gobj);
1754
+ priv.dialog_gen = priv.model_gen;
1634
1755
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
1635
1756
  dialog: true,
1636
1757
  title: title,
@@ -1679,6 +1800,10 @@ function name_control($spec, label)
1679
1800
  $spec[1]["aria-label"] = t(label);
1680
1801
  $spec[1]["data-i18n-aria-label"] = label;
1681
1802
  }
1803
+ if(!$spec[1].title) {
1804
+ $spec[1].title = t(label);
1805
+ $spec[1]["data-i18n-title"] = label;
1806
+ }
1682
1807
  return true;
1683
1808
  }
1684
1809
  for(let child of ($spec[2] || [])) {
@@ -1689,6 +1814,19 @@ function name_control($spec, label)
1689
1814
  return false;
1690
1815
  }
1691
1816
 
1817
+ /***************************************************************
1818
+ * The four attributes that NAME a control -- a title and an
1819
+ * aria-label, each with the key a language change re-translates
1820
+ * it by. A visible label is not a name (CLAUDE.md).
1821
+ ***************************************************************/
1822
+ function name_attrs(key, attrs)
1823
+ {
1824
+ return Object.assign({
1825
+ title: t(key), "data-i18n-title": key,
1826
+ "aria-label": t(key), "data-i18n-aria-label": key
1827
+ }, attrs);
1828
+ }
1829
+
1692
1830
  function field(logical, name, label, $control, help)
1693
1831
  {
1694
1832
  /* The label is a SIBLING of the control -- Bulma's `field` shape --
@@ -1842,6 +1980,7 @@ function open_column_form(gobj, topic, col, prefill)
1842
1980
  let hook_col = hook_topic ? hook[hook_topic] : "";
1843
1981
  let treedb = current_treedb(gobj);
1844
1982
  let sibling_topics = treedb ? treedb.topics.map(x => x.name) : [];
1983
+ let model_gen = priv.model_gen;
1845
1984
 
1846
1985
  let $form = createElement2(
1847
1986
  ["div", {class: "SCHEMA_COL_FORM box"}, [
@@ -1883,9 +2022,10 @@ function open_column_form(gobj, topic, col, prefill)
1883
2022
  text_input("description", record.description, "")),
1884
2023
  ["div", {class: "SCHEMA_COL_FORM_ACTIONS is-flex mt-4",
1885
2024
  style: "gap:.5rem; justify-content:flex-end;"}, [
1886
- ["button", {class: "SCHEMA_COL_FORM_CANCEL button", type: "button"},
2025
+ ["button", name_attrs("cancel", {class: "SCHEMA_COL_FORM_CANCEL button", type: "button"}),
1887
2026
  [["span", {i18n: "cancel"}, t("cancel")]]],
1888
- ["button", {class: "SCHEMA_COL_FORM_SAVE button is-primary", type: "button"},
2027
+ ["button", name_attrs("save", {class: "SCHEMA_COL_FORM_SAVE button is-primary",
2028
+ type: "button"}),
1889
2029
  [["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
1890
2030
  ["span", {i18n: "save"}, t("save")]]]
1891
2031
  ]]
@@ -1926,10 +2066,11 @@ function open_column_form(gobj, topic, col, prefill)
1926
2066
  let values = read_form($form);
1927
2067
  values.flag = read_flags($form);
1928
2068
  gobj_send_event(gobj, "EV_SAVE_COLUMN", {
1929
- topic: topic.name,
1930
- col: creating ? "" : col.name,
1931
- creating: creating,
1932
- values: values
2069
+ topic: topic.name,
2070
+ col: creating ? "" : col.name,
2071
+ creating: creating,
2072
+ values: values,
2073
+ model_gen: model_gen
1933
2074
  }, gobj);
1934
2075
  });
1935
2076
 
@@ -1962,6 +2103,7 @@ function open_topic_form(gobj, treedb, topic)
1962
2103
  let record = (topic && topic.record) || {};
1963
2104
  let creating = !topic;
1964
2105
  let col_names = topic ? topic.cols.map(c => c.name) : [];
2106
+ let model_gen = gobj.priv.model_gen;
1965
2107
 
1966
2108
  let $form = createElement2(
1967
2109
  ["div", {class: "SCHEMA_TOPIC_FORM box"}, [
@@ -2001,10 +2143,10 @@ function open_topic_form(gobj, treedb, topic)
2001
2143
  ]],
2002
2144
  ["div", {class: "SCHEMA_TOPIC_FORM_ACTIONS is-flex mt-4",
2003
2145
  style: "gap:.5rem; justify-content:flex-end;"}, [
2004
- ["button", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"},
2146
+ ["button", name_attrs("cancel", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"}),
2005
2147
  [["span", {i18n: "cancel"}, t("cancel")]]],
2006
- ["button", {class: "SCHEMA_TOPIC_FORM_SAVE button is-primary",
2007
- type: "button"},
2148
+ ["button", name_attrs("save", {class: "SCHEMA_TOPIC_FORM_SAVE button is-primary",
2149
+ type: "button"}),
2008
2150
  [["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
2009
2151
  ["span", {i18n: "save"}, t("save")]]]
2010
2152
  ]]
@@ -2018,9 +2160,10 @@ function open_topic_form(gobj, treedb, topic)
2018
2160
  $form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
2019
2161
  evt.stopPropagation();
2020
2162
  gobj_send_event(gobj, "EV_SAVE_TOPIC", {
2021
- topic: creating ? "" : topic.name,
2022
- creating: creating,
2023
- values: read_form($form)
2163
+ topic: creating ? "" : topic.name,
2164
+ creating: creating,
2165
+ values: read_form($form),
2166
+ model_gen: model_gen
2024
2167
  }, gobj);
2025
2168
  });
2026
2169
 
@@ -2092,13 +2235,21 @@ function open_export(gobj, treedb)
2092
2235
 
2093
2236
  let $content = createElement2(
2094
2237
  ["div", {class: "SCHEMA_EXPORT box"}, [
2095
- ["div", {class: "SCHEMA_EXPORT_TABS tabs is-toggle mb-2"}, [
2096
- ["ul", {}, [
2097
- ["li", {class: "SCHEMA_EXPORT_TAB is-active", "data-pane": "c"},
2098
- [["a", {}, [["span", {}, "C"]]]]],
2099
- ["li", {class: "SCHEMA_EXPORT_TAB", "data-pane": "json"},
2100
- [["a", {}, [["span", {}, "JSON"]]]]]
2101
- ]]
2238
+ /* Two BUTTONS, not Bulma tabs: a tab's <a> with no href is
2239
+ * reached by no keyboard and named by nothing but its "C".
2240
+ * The one shown is the selected segment (`selected_state`,
2241
+ * yui_toolbar.css) and says so with aria-pressed. */
2242
+ ["div", {class: "SCHEMA_EXPORT_TABS buttons has-addons mb-2"}, [
2243
+ ["button", {class: "SCHEMA_EXPORT_TAB button selected_state", type: "button",
2244
+ "data-pane": "c", "aria-pressed": "true",
2245
+ title: t("schema as c source"), "data-i18n-title": "schema as c source",
2246
+ "aria-label": t("schema as c source"), "data-i18n-aria-label": "schema as c source"},
2247
+ [["span", {}, "C"]]],
2248
+ ["button", {class: "SCHEMA_EXPORT_TAB button", type: "button",
2249
+ "data-pane": "json", "aria-pressed": "false",
2250
+ title: t("schema as json"), "data-i18n-title": "schema as json",
2251
+ "aria-label": t("schema as json"), "data-i18n-aria-label": "schema as json"},
2252
+ [["span", {}, "JSON"]]]
2102
2253
  ]],
2103
2254
  ["p", {class: "SCHEMA_EXPORT_HELP help mb-2",
2104
2255
  i18n: "an edit made here works and lives in no source: this is what to paste back"},
@@ -2129,7 +2280,8 @@ function open_export(gobj, treedb)
2129
2280
  $tab.addEventListener("click", (evt) => {
2130
2281
  evt.stopPropagation();
2131
2282
  for(let $other of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
2132
- $other.classList.toggle("is-active", $other === $tab);
2283
+ $other.classList.toggle("selected_state", $other === $tab);
2284
+ $other.setAttribute("aria-pressed", ($other === $tab)? "true": "false");
2133
2285
  }
2134
2286
  $text.value = ($tab.dataset.pane === "c") ? c_text : json_text;
2135
2287
  });
@@ -2173,14 +2325,15 @@ function copy_text(gobj, $text)
2173
2325
  ***************************************************************/
2174
2326
  function open_import(gobj, treedb)
2175
2327
  {
2328
+ let model_gen = gobj.priv.model_gen;
2176
2329
  let $content = createElement2(
2177
2330
  ["div", {class: "SCHEMA_IMPORT box"}, [
2178
2331
  ["p", {class: "SCHEMA_IMPORT_HELP help mb-2",
2179
2332
  i18n: "paste a schema as json: the plan is shown before anything is written"},
2180
2333
  t("paste a schema as json: the plan is shown before anything is written")],
2181
- ["textarea", {class: "SCHEMA_IMPORT_TEXT textarea is-small is-family-monospace",
2334
+ ["textarea", name_attrs("schema", {class: "SCHEMA_IMPORT_TEXT textarea is-small is-family-monospace",
2182
2335
  rows: "12", spellcheck: "false",
2183
- placeholder: '{"id": "...", "schema_version": "1", "topics": []}'}, ""],
2336
+ placeholder: '{"id": "...", "schema_version": "1", "topics": []}'}), ""],
2184
2337
  ["label", {class: "SCHEMA_IMPORT_PRUNE checkbox mt-2"}, [
2185
2338
  ["input", {type: "checkbox", class: "SCHEMA_IMPORT_PRUNE_BOX", checked: "checked"}],
2186
2339
  ["span", {class: "ml-1", i18n: "delete what the pasted schema does not declare"},
@@ -2189,13 +2342,13 @@ function open_import(gobj, treedb)
2189
2342
  ["div", {class: "SCHEMA_IMPORT_PLAN mt-3"}, []],
2190
2343
  ["div", {class: "SCHEMA_IMPORT_ACTIONS is-flex mt-3",
2191
2344
  style: "gap:.5rem; justify-content:flex-end;"}, [
2192
- ["button", {class: "SCHEMA_IMPORT_CANCEL button", type: "button"},
2345
+ ["button", name_attrs("cancel", {class: "SCHEMA_IMPORT_CANCEL button", type: "button"}),
2193
2346
  [["span", {i18n: "cancel"}, t("cancel")]]],
2194
- ["button", {class: "SCHEMA_IMPORT_PREVIEW button", type: "button"},
2347
+ ["button", name_attrs("preview", {class: "SCHEMA_IMPORT_PREVIEW button", type: "button"}),
2195
2348
  [["span", {class: "icon"}, [["i", {class: "yi-eye"}]]],
2196
2349
  ["span", {i18n: "preview"}, t("preview")]]],
2197
- ["button", {class: "SCHEMA_IMPORT_RUN button is-warning",
2198
- type: "button", disabled: "disabled"},
2350
+ ["button", name_attrs("import", {class: "SCHEMA_IMPORT_RUN button is-warning",
2351
+ type: "button", disabled: "disabled"}),
2199
2352
  [["span", {class: "icon"}, [["i", {class: "yi-upload"}]]],
2200
2353
  ["span", {i18n: "import"}, t("import")]]]
2201
2354
  ]]
@@ -2214,13 +2367,14 @@ function open_import(gobj, treedb)
2214
2367
  $content.querySelector(".SCHEMA_IMPORT_PREVIEW").addEventListener("click", (evt) => {
2215
2368
  evt.stopPropagation();
2216
2369
  gobj_send_event(gobj, "EV_PREVIEW_IMPORT", {
2217
- text: $text.value,
2218
- prune: $prune.checked
2370
+ text: $text.value,
2371
+ prune: $prune.checked,
2372
+ model_gen: model_gen
2219
2373
  }, gobj);
2220
2374
  });
2221
2375
  $run.addEventListener("click", (evt) => {
2222
2376
  evt.stopPropagation();
2223
- gobj_send_event(gobj, "EV_APPLY_IMPORT", {}, gobj);
2377
+ gobj_send_event(gobj, "EV_APPLY_IMPORT", {model_gen: model_gen}, gobj);
2224
2378
  });
2225
2379
 
2226
2380
  gobj.priv.import_pane = $plan_pane;
@@ -2228,6 +2382,23 @@ function open_import(gobj, treedb)
2228
2382
  open_dialog(gobj, $content, "import", "SCHEMA_IMPORT_DIALOG", treedb.id);
2229
2383
  }
2230
2384
 
2385
+ /***************************************************************
2386
+ * The plan is a list of writes computed against the model shown:
2387
+ * a load replaces the model, so the plan goes with it and the
2388
+ * dialog, if it stays up (a load that failed keeps the model),
2389
+ * asks for a Preview again.
2390
+ ***************************************************************/
2391
+ function forget_import_plan(gobj)
2392
+ {
2393
+ let priv = gobj.priv;
2394
+
2395
+ priv.import_plan = null;
2396
+ clear(priv.import_pane);
2397
+ if(priv.import_run) {
2398
+ priv.import_run.disabled = true;
2399
+ }
2400
+ }
2401
+
2231
2402
  /***************************************************************
2232
2403
  * The plan, drawn where the paste is: one line per write, the
2233
2404
  * deletions apart and counted, because they are what the
@@ -2304,7 +2475,7 @@ function open_orphans(gobj)
2304
2475
  ["td", {class: "SCHEMA_ORPHAN_ID"}, [["code", {}, `${entry.id}`]]],
2305
2476
  ["td", {class: "SCHEMA_ORPHAN_ACTIONS has-text-right"},
2306
2477
  readonly ? [] : [row_icon(gobj, "SCHEMA_ORPHAN_DELETE", "yi-trash", "delete",
2307
- "EV_DELETE_ORPHAN", {kind: kind, id: entry.id})]]
2478
+ "EV_DELETE_ORPHAN", {kind: kind, id: entry.id, model_gen: priv.model_gen})]]
2308
2479
  ]];
2309
2480
  };
2310
2481
 
@@ -2519,6 +2690,11 @@ function transport_dropped(gobj)
2519
2690
  priv.load_round++;
2520
2691
  priv.pending = 0;
2521
2692
  priv.load_error = "";
2693
+ if(priv.records_before) {
2694
+ /* The records of the model kept, not the half the load got. */
2695
+ priv.records = priv.records_before;
2696
+ priv.records_before = null;
2697
+ }
2522
2698
  return end_load(gobj);
2523
2699
  }
2524
2700
  return 0;
@@ -2530,6 +2706,12 @@ function transport_dropped(gobj)
2530
2706
  * in flight (ac_show() kept it), or the one it had. Without a
2531
2707
  * model there is no screen to go to: the position is kept for the
2532
2708
  * next load, and the editor says why it is empty.
2709
+ *
2710
+ * A dialog built on the model this load REPLACED is closed, and the
2711
+ * operator told to open it again: it shows the old record, and its
2712
+ * Save would write every field of it over the newer one. A load
2713
+ * that failed or never left replaced nothing (`model_gen` did not
2714
+ * move), and the dialog stays with what was typed.
2533
2715
  ***************************************************************/
2534
2716
  function end_load(gobj)
2535
2717
  {
@@ -2537,6 +2719,13 @@ function end_load(gobj)
2537
2719
  let seg = priv.pending_seg;
2538
2720
 
2539
2721
  priv.pending_seg = null;
2722
+ if(priv.dialog && priv.dialog_gen !== priv.model_gen) {
2723
+ log_warning(`${gobj_short_name(gobj)}: a dialog was open on the schemas ` +
2724
+ `the load replaced: closed`);
2725
+ close_dialog(gobj);
2726
+ yui_shell_show_error(yui_shell_of(gobj),
2727
+ "the schemas were read again: open the dialog again", {t: t});
2728
+ }
2540
2729
  if(priv.model) {
2541
2730
  if(seg !== null) {
2542
2731
  return apply_seg(gobj, seg);
@@ -2885,6 +3074,25 @@ function ac_mt_command_answer(gobj, event, kw, src)
2885
3074
  return 0;
2886
3075
  }
2887
3076
  if(priv.load_error) {
3077
+ /* Refused IN session (a deadline of a routing adapter on a
3078
+ * slow `nodes`): the model shown is what the store held a
3079
+ * moment ago, and blanking it took the screen and every open
3080
+ * form with it -- a form's Save then answered "Event NOT
3081
+ * DEFINED in state ST_IDLE" (fourth independent review). It
3082
+ * stays, as it does when the load cannot leave, and the
3083
+ * reload is owed: the operator's next action asks it
3084
+ * (reload_first(), fifth independent review). */
3085
+ if(priv.model) {
3086
+ log_warning(`${gobj_short_name(gobj)}: the schemas could not be read ` +
3087
+ `again (${priv.load_error}): the previous ones stay`);
3088
+ priv.records = priv.records_before;
3089
+ priv.records_before = null;
3090
+ priv.reload_on_open = true;
3091
+ yui_shell_show_error(yui_shell_of(gobj),
3092
+ "cannot read the schemas again: the ones shown may be out of date, your next change reads them first",
3093
+ {t: t});
3094
+ return end_load(gobj);
3095
+ }
2888
3096
  priv.model = null;
2889
3097
  return end_load(gobj);
2890
3098
  }
@@ -2893,6 +3101,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
2893
3101
  * (NOT in build_model(): that also runs on the patch after every
2894
3102
  * write, which is exactly when the undo has to survive.) */
2895
3103
  priv.order_undo = {};
3104
+ priv.records_before = null;
3105
+ priv.model_gen++;
2896
3106
  build_model(gobj);
2897
3107
  /* Same reason as order_undo above: this is the LOAD, and the
2898
3108
  * baseline is what a later "the version did not move" is measured
@@ -2931,12 +3141,26 @@ function ac_mt_command_answer(gobj, event, kw, src)
2931
3141
  patch_record(gobj, topic_name, record);
2932
3142
  } else {
2933
3143
  /* The treedb wrote it and did not describe it back: ask
2934
- * again rather than draw a model that has drifted. */
2935
- log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record`);
3144
+ * again rather than draw a model that has drifted. What
3145
+ * was queued after it is not sent -- it was computed on
3146
+ * the model that drifted -- and that is SAID: dropped in
3147
+ * silence, a drag of five columns stopped at the first.
3148
+ * The reload is also the one a late write may have owed
3149
+ * (reload_after_write): left set, the next write loaded
3150
+ * the whole model once more (fourth independent review). */
3151
+ let not_sent = priv.save_queue.length - 1;
3152
+ log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record` +
3153
+ (not_sent > 0 ? `: ${not_sent} write(s) after it were not sent` : ""));
3154
+ if(not_sent > 0) {
3155
+ yui_shell_show_error(yui_shell_of(gobj),
3156
+ "the treedb did not describe a write back: the writes after it were not sent",
3157
+ {t: t});
3158
+ }
2936
3159
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
2937
3160
  let write = priv.save_queue[0];
2938
3161
  mark_written(gobj, topic_name, write ? write.record : null);
2939
3162
  }
3163
+ priv.reload_after_write = false;
2940
3164
  priv.save_queue = [];
2941
3165
  priv.save_return = "";
2942
3166
  priv.save_wrote = false;
@@ -3010,8 +3234,14 @@ function late_write_done(gobj, command, topic_name, data)
3010
3234
  /***************************************************************
3011
3235
  * The topic a record written belongs to, read off the RECORD and
3012
3236
  * not off the screen: a write answered late may be about a topic
3013
- * the operator has left. A column names its topic in its fkey
3014
- * (`topics^<topic id>^cols`); a topic is its own id.
3237
+ * the operator has left. A column names its topic in its fkey; a
3238
+ * topic is its own id.
3239
+ *
3240
+ * The fkey is read with parse_fkey_ref(), because the store answers
3241
+ * in list_dict -- `[{topic_name, id, hook_name}]` -- and not in the
3242
+ * `topics^<topic id>^cols` strings this view writes. Split as a
3243
+ * string, every production answer marked nothing (fourth independent
3244
+ * review, probe C).
3015
3245
  ***************************************************************/
3016
3246
  function mark_written_record(gobj, topic_name, record)
3017
3247
  {
@@ -3027,13 +3257,8 @@ function mark_written_record(gobj, topic_name, record)
3027
3257
  if(topic_name !== T_COLS) {
3028
3258
  return;
3029
3259
  }
3030
- let refs = Array.isArray(record.topics) ? record.topics
3031
- : (typeof record.topics === "string" ? [record.topics] : []);
3032
- for(let ref of refs) {
3033
- let parts = String(ref).split("^");
3034
- if(parts.length >= 2 && parts[1]) {
3035
- priv.written[parts[1]] = true;
3036
- }
3260
+ for(let ref of parse_fkey_ref(record.topics)) {
3261
+ priv.written[ref.id] = true;
3037
3262
  }
3038
3263
  }
3039
3264
 
@@ -3142,16 +3367,43 @@ function ac_refresh(gobj, event, kw, src)
3142
3367
  * operator can still reach, and what it asks is computed against
3143
3368
  * a model the answers in the air are about to replace.
3144
3369
  *
3145
- * Refused, and SAID -- and the dialog stays open with what was
3146
- * typed, so the same Save works once the load is in. Undeclared,
3147
- * it answered "Event NOT DEFINED in state ST_LOADING" and the
3148
- * edit was gone with no word (third independent review).
3370
+ * Refused, and SAID. Undeclared, it answered "Event NOT DEFINED
3371
+ * in state ST_LOADING" and the edit was gone with no word (third
3372
+ * independent review).
3373
+ *
3374
+ * And NOT "try again": the same Save, once the load was in, sent
3375
+ * the form built on the old model -- every field of the old record
3376
+ * over the newer one (fourth independent review). The load that
3377
+ * lands closes such a dialog (end_load()); one that fails leaves it
3378
+ * up, on the model it was built on.
3149
3379
  ***************************************************************/
3150
3380
  function ac_wait_for_the_load(gobj, event, kw, src)
3151
3381
  {
3152
3382
  log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas are loading`);
3153
3383
  yui_shell_show_error(yui_shell_of(gobj),
3154
- "the schemas are loading: try again when they are in", {t: t});
3384
+ "the schemas are loading: wait for them", {t: t});
3385
+ return -1;
3386
+ }
3387
+
3388
+ /***************************************************************
3389
+ * A confirmation answered where no treedb is open: ST_EMPTY (a
3390
+ * reload landed on zero treedbs while it was up) or ST_IDLE (no
3391
+ * model). A confirmation is a shell modal, not this view's dialog,
3392
+ * so the load that lands does not close it, and its Yes arrives
3393
+ * anyway. Undeclared, it answered "Event NOT DEFINED" (fifth
3394
+ * independent review).
3395
+ *
3396
+ * Refused with the stamp's own words: whatever it was decided on
3397
+ * is not on screen any more.
3398
+ ***************************************************************/
3399
+ function ac_confirmed_on_nothing(gobj, event, kw, src)
3400
+ {
3401
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3402
+ return -1;
3403
+ }
3404
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, no treedb is open`);
3405
+ yui_shell_show_error(yui_shell_of(gobj),
3406
+ "the schemas were read again: open the dialog again", {t: t});
3155
3407
  return -1;
3156
3408
  }
3157
3409
 
@@ -3186,32 +3438,34 @@ function ac_drafts(gobj, event, kw, src)
3186
3438
  ***************************************************************/
3187
3439
  function ac_select_treedb(gobj, event, kw, src)
3188
3440
  {
3189
- return go(gobj, kw.treedb, "", false, true);
3441
+ go(gobj, kw.treedb, "", false, true);
3442
+ return reload_after_move(gobj);
3190
3443
  }
3191
3444
 
3192
3445
  function ac_select_topic(gobj, event, kw, src)
3193
3446
  {
3194
- return go(gobj, gobj.priv.treedb_id, kw.topic, false, true);
3447
+ go(gobj, gobj.priv.treedb_id, kw.topic, false, true);
3448
+ return reload_after_move(gobj);
3195
3449
  }
3196
3450
 
3197
3451
  function ac_back(gobj, event, kw, src)
3198
3452
  {
3199
3453
  let priv = gobj.priv;
3200
3454
 
3201
- if(priv.diagram) {
3202
- return go(gobj, priv.treedb_id, "", false, true);
3203
- }
3204
- if(priv.topic_name) {
3205
- return go(gobj, priv.treedb_id, "", false, true);
3455
+ if(priv.diagram || priv.topic_name) {
3456
+ go(gobj, priv.treedb_id, "", false, true);
3457
+ } else {
3458
+ go(gobj, "", "", false, true);
3206
3459
  }
3207
- return go(gobj, "", "", false, true);
3460
+ return reload_after_move(gobj);
3208
3461
  }
3209
3462
 
3210
3463
  function ac_toggle_diagram(gobj, event, kw, src)
3211
3464
  {
3212
3465
  let priv = gobj.priv;
3213
3466
 
3214
- return go(gobj, priv.treedb_id, priv.topic_name, !priv.diagram, true);
3467
+ go(gobj, priv.treedb_id, priv.topic_name, !priv.diagram, true);
3468
+ return reload_after_move(gobj);
3215
3469
  }
3216
3470
 
3217
3471
  /***************************************************************
@@ -3226,11 +3480,15 @@ function ac_node_click(gobj, event, kw, src)
3226
3480
  if(!topic) {
3227
3481
  return 0;
3228
3482
  }
3229
- return go(gobj, gobj.priv.treedb_id, topic, false, true);
3483
+ go(gobj, gobj.priv.treedb_id, topic, false, true);
3484
+ return reload_after_move(gobj);
3230
3485
  }
3231
3486
 
3232
3487
  function ac_show_orphans(gobj, event, kw, src)
3233
3488
  {
3489
+ if(reload_first(gobj, event)) {
3490
+ return -1;
3491
+ }
3234
3492
  open_orphans(gobj);
3235
3493
  return 0;
3236
3494
  }
@@ -3245,6 +3503,7 @@ function ac_show_orphans(gobj, event, kw, src)
3245
3503
  ***************************************************************/
3246
3504
  function confirm_then(gobj, message, detail, kw)
3247
3505
  {
3506
+ kw = Object.assign({model_gen: gobj.priv.model_gen}, kw);
3248
3507
  yui_shell_confirm_danger(yui_shell_of(gobj), message, {
3249
3508
  t: t,
3250
3509
  detail: detail
@@ -3266,6 +3525,9 @@ function ac_add_column(gobj, event, kw, src)
3266
3525
  if(refuse_if_readonly(gobj, event)) {
3267
3526
  return -1;
3268
3527
  }
3528
+ if(reload_first(gobj, event)) {
3529
+ return -1;
3530
+ }
3269
3531
  if(!topic) {
3270
3532
  log_error(`${gobj_short_name(gobj)}: ${event} with no topic open`);
3271
3533
  return -1;
@@ -3283,6 +3545,9 @@ function ac_edit_column(gobj, event, kw, src)
3283
3545
  if(refuse_if_readonly(gobj, event)) {
3284
3546
  return -1;
3285
3547
  }
3548
+ if(reload_first(gobj, event)) {
3549
+ return -1;
3550
+ }
3286
3551
  if(!col) {
3287
3552
  log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.col}`);
3288
3553
  return -1;
@@ -3305,6 +3570,9 @@ function ac_duplicate_column(gobj, event, kw, src)
3305
3570
  if(refuse_if_readonly(gobj, event)) {
3306
3571
  return -1;
3307
3572
  }
3573
+ if(reload_first(gobj, event)) {
3574
+ return -1;
3575
+ }
3308
3576
  if(!col) {
3309
3577
  log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.col}`);
3310
3578
  return -1;
@@ -3330,6 +3598,12 @@ function ac_save_column(gobj, event, kw, src)
3330
3598
  if(refuse_if_readonly(gobj, event)) {
3331
3599
  return -1;
3332
3600
  }
3601
+ if(reload_first(gobj, event)) {
3602
+ return -1;
3603
+ }
3604
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3605
+ return -1;
3606
+ }
3333
3607
  if(!topic || (!kw.creating && !col)) {
3334
3608
  log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.topic}.${kw.col}`);
3335
3609
  return -1;
@@ -3362,6 +3636,9 @@ function ac_delete_column(gobj, event, kw, src)
3362
3636
  if(refuse_if_readonly(gobj, event)) {
3363
3637
  return -1;
3364
3638
  }
3639
+ if(reload_first(gobj, event)) {
3640
+ return -1;
3641
+ }
3365
3642
  if(!col) {
3366
3643
  log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.col}`);
3367
3644
  return -1;
@@ -3384,6 +3661,9 @@ function ac_move_column(gobj, event, kw, src)
3384
3661
  if(refuse_if_readonly(gobj, event)) {
3385
3662
  return -1;
3386
3663
  }
3664
+ if(reload_first(gobj, event)) {
3665
+ return -1;
3666
+ }
3387
3667
  if(!topic) {
3388
3668
  log_error(`${gobj_short_name(gobj)}: ${event} with no topic open`);
3389
3669
  return -1;
@@ -3423,6 +3703,9 @@ function ac_undo_order(gobj, event, kw, src)
3423
3703
  if(refuse_if_readonly(gobj, event)) {
3424
3704
  return -1;
3425
3705
  }
3706
+ if(reload_first(gobj, event)) {
3707
+ return -1;
3708
+ }
3426
3709
  if(!topic) {
3427
3710
  log_error(`${gobj_short_name(gobj)}: ${event} with no topic open`);
3428
3711
  return -1;
@@ -3450,6 +3733,9 @@ function ac_add_topic(gobj, event, kw, src)
3450
3733
  if(refuse_if_readonly(gobj, event)) {
3451
3734
  return -1;
3452
3735
  }
3736
+ if(reload_first(gobj, event)) {
3737
+ return -1;
3738
+ }
3453
3739
  if(!treedb) {
3454
3740
  log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
3455
3741
  return -1;
@@ -3467,6 +3753,9 @@ function ac_edit_topic(gobj, event, kw, src)
3467
3753
  if(refuse_if_readonly(gobj, event)) {
3468
3754
  return -1;
3469
3755
  }
3756
+ if(reload_first(gobj, event)) {
3757
+ return -1;
3758
+ }
3470
3759
  if(!topic) {
3471
3760
  log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
3472
3761
  return -1;
@@ -3484,6 +3773,12 @@ function ac_save_topic(gobj, event, kw, src)
3484
3773
  if(refuse_if_readonly(gobj, event)) {
3485
3774
  return -1;
3486
3775
  }
3776
+ if(reload_first(gobj, event)) {
3777
+ return -1;
3778
+ }
3779
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3780
+ return -1;
3781
+ }
3487
3782
  if(!treedb || (!kw.creating && !topic)) {
3488
3783
  log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
3489
3784
  return -1;
@@ -3520,6 +3815,9 @@ function ac_delete_topic(gobj, event, kw, src)
3520
3815
  if(refuse_if_readonly(gobj, event)) {
3521
3816
  return -1;
3522
3817
  }
3818
+ if(reload_first(gobj, event)) {
3819
+ return -1;
3820
+ }
3523
3821
  if(!topic) {
3524
3822
  log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
3525
3823
  return -1;
@@ -3538,6 +3836,12 @@ function ac_delete_orphan(gobj, event, kw, src)
3538
3836
  if(refuse_if_readonly(gobj, event)) {
3539
3837
  return -1;
3540
3838
  }
3839
+ if(reload_first(gobj, event)) {
3840
+ return -1;
3841
+ }
3842
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3843
+ return -1;
3844
+ }
3541
3845
  if(empty_string(kw.id)) {
3542
3846
  log_error(`${gobj_short_name(gobj)}: ${event} names nothing`);
3543
3847
  return -1;
@@ -3558,6 +3862,10 @@ function ac_validate(gobj, event, kw, src)
3558
3862
  let priv = gobj.priv;
3559
3863
  let treedb = current_treedb(gobj);
3560
3864
 
3865
+ if(reload_first(gobj, event)) {
3866
+ return -1;
3867
+ }
3868
+
3561
3869
  if(!treedb) {
3562
3870
  log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
3563
3871
  return -1;
@@ -3577,6 +3885,10 @@ function ac_export(gobj, event, kw, src)
3577
3885
  {
3578
3886
  let treedb = current_treedb(gobj);
3579
3887
 
3888
+ if(reload_first(gobj, event)) {
3889
+ return -1;
3890
+ }
3891
+
3580
3892
  if(!treedb) {
3581
3893
  log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
3582
3894
  return -1;
@@ -3592,6 +3904,9 @@ function ac_import(gobj, event, kw, src)
3592
3904
  if(refuse_if_readonly(gobj, event)) {
3593
3905
  return -1;
3594
3906
  }
3907
+ if(reload_first(gobj, event)) {
3908
+ return -1;
3909
+ }
3595
3910
  if(!treedb) {
3596
3911
  log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
3597
3912
  return -1;
@@ -3610,6 +3925,13 @@ function ac_preview_import(gobj, event, kw, src)
3610
3925
  let treedb = current_treedb(gobj);
3611
3926
  let incoming = null;
3612
3927
 
3928
+ if(reload_first(gobj, event)) {
3929
+ return -1;
3930
+ }
3931
+
3932
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3933
+ return -1;
3934
+ }
3613
3935
  if(!treedb) {
3614
3936
  log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
3615
3937
  return -1;
@@ -3637,6 +3959,12 @@ function ac_apply_import(gobj, event, kw, src)
3637
3959
  if(refuse_if_readonly(gobj, event)) {
3638
3960
  return -1;
3639
3961
  }
3962
+ if(reload_first(gobj, event)) {
3963
+ return -1;
3964
+ }
3965
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3966
+ return -1;
3967
+ }
3640
3968
  if(!plan || plan.writes.length === 0) {
3641
3969
  log_error(`${gobj_short_name(gobj)}: ${event} with no plan to run`);
3642
3970
  return -1;
@@ -3660,8 +3988,12 @@ function run_import(gobj)
3660
3988
  let priv = gobj.priv;
3661
3989
  let plan = priv.import_plan;
3662
3990
 
3991
+ /* Not an ERROR: a load that left forgets the plan (request_model()),
3992
+ * and the confirmation of it may still be up. The operator's Yes
3993
+ * is answered, not lost in the log. */
3663
3994
  if(!plan) {
3664
- log_error(`${gobj_short_name(gobj)}: the import plan is gone`);
3995
+ log_warning(`${gobj_short_name(gobj)}: the import plan is gone`);
3996
+ yui_shell_show_error(yui_shell_of(gobj), "the import plan is gone: preview it again", {t: t});
3665
3997
  return -1;
3666
3998
  }
3667
3999
  close_dialog(gobj);
@@ -3687,6 +4019,12 @@ function ac_confirmed(gobj, event, kw, src)
3687
4019
  if(refuse_if_readonly(gobj, event)) {
3688
4020
  return -1;
3689
4021
  }
4022
+ if(reload_first(gobj, event)) {
4023
+ return -1;
4024
+ }
4025
+ if(built_on_a_replaced_model(gobj, event, kw)) {
4026
+ return -1;
4027
+ }
3690
4028
  if(kw.what === "import") {
3691
4029
  return run_import(gobj);
3692
4030
  }
@@ -3782,7 +4120,9 @@ function create_gclass(gclass_name)
3782
4120
  * a click is worth routing through a machine.
3783
4121
  *---------------------------------------------*/
3784
4122
  const states = [
3785
- ["ST_IDLE", COMMON.slice()],
4123
+ ["ST_IDLE", COMMON.concat([
4124
+ ["EV_CONFIRMED", ac_confirmed_on_nothing, null]
4125
+ ])],
3786
4126
 
3787
4127
  ["ST_LOADING", COMMON.concat([
3788
4128
  ["EV_SAVE_COLUMN", ac_wait_for_the_load, null],
@@ -3793,7 +4133,9 @@ function create_gclass(gclass_name)
3793
4133
  ["EV_CONFIRMED", ac_wait_for_the_load, null]
3794
4134
  ])],
3795
4135
 
3796
- ["ST_EMPTY", COMMON.slice()],
4136
+ ["ST_EMPTY", COMMON.concat([
4137
+ ["EV_CONFIRMED", ac_confirmed_on_nothing, null]
4138
+ ])],
3797
4139
 
3798
4140
  ["ST_TREEDBS", COMMON.concat([
3799
4141
  ["EV_SELECT_TREEDB", ac_select_treedb, null],