@yuneta/gobj-ui 7.25.8 → 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,7 +203,9 @@ 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` */
@@ -224,6 +227,7 @@ let PRIVATE_DATA = {
224
227
  /* Children and overlays */
225
228
  diagram_gobj: null,
226
229
  dialog: null,
230
+ dialog_gen: 0, /* the model_gen the open dialog was built on */
227
231
 
228
232
  /* A write, or a plan of them, in flight */
229
233
  save_queue: null,
@@ -444,6 +448,36 @@ function refuse_if_readonly(gobj, event)
444
448
  return true;
445
449
  }
446
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
+
447
481
 
448
482
 
449
483
 
@@ -465,7 +499,11 @@ function refuse_if_readonly(gobj, event)
465
499
  * and the toolbar stops answering: left up, a click on a card, on
466
500
  * Back or on the drawing answered "Event NOT DEFINED in state
467
501
  * ST_LOADING" and was lost (third independent review). A dialog
468
- * left open is answered in ST_LOADING instead (ac_wait_for_the_load).
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.
469
507
  *
470
508
  * `keep_written`: a load the host did not ask for -- the reconnect,
471
509
  * a write that turned out done -- keeps what this session wrote.
@@ -475,7 +513,9 @@ function refuse_if_readonly(gobj, event)
475
513
  * is not even asked): the records the model was built on are put
476
514
  * back (emptied, the next write patched them into a model with no
477
515
  * treedb), the round is spent so a request that did leave is
478
- * stale, and the session back asks again.
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()).
479
519
  ***************************************************************/
480
520
  function request_model(gobj, keep_written)
481
521
  {
@@ -486,7 +526,10 @@ function request_model(gobj, keep_written)
486
526
  return -1; /* not mounted on a transport yet: mt_start retries via EV_TRANSPORT_STATE */
487
527
  }
488
528
 
529
+ forget_import_plan(gobj);
530
+
489
531
  let previous = priv.records;
532
+ priv.records_before = previous;
490
533
  priv.records = {treedbs: [], topics: [], cols: []};
491
534
  priv.load_error = "";
492
535
  priv.pending = 0;
@@ -511,6 +554,7 @@ function request_model(gobj, keep_written)
511
554
  }
512
555
  if(left < 3) {
513
556
  priv.records = previous;
557
+ priv.records_before = null;
514
558
  priv.load_round++;
515
559
  priv.reload_on_open = true;
516
560
  priv.load_error = t("cannot reach the treedb");
@@ -643,9 +687,10 @@ function start_measuring(gobj)
643
687
  * until the host saves it (C_TREEDB's save-schema, which raises the
644
688
  * versions of what changed). Two sources say a topic is one, and
645
689
  * 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);
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());
649
694
  * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
650
695
  * by the next EV_DRAFTS. Folded into `written`, a topic the host
651
696
  * named once stayed a draft after the Save that published it.
@@ -847,6 +892,14 @@ function render_toolbar(gobj)
847
892
  * url's; ST_EMPTY has a treedb in the url and none in the model),
848
893
  * and a button there sends what only a treedb screen declares. */
849
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);
850
903
  /* Nothing is legal while the model is in the air: every action is
851
904
  * computed against it. */
852
905
  let in_flight = state === "ST_SAVING" || state === "ST_LOADING";
@@ -892,7 +945,7 @@ function render_toolbar(gobj)
892
945
  * refused says why when it is pressed.
893
946
  *----------------------------------------------*/
894
947
  let $right = [];
895
- if(priv.treedb_id && has_model && on_a_treedb) {
948
+ if(priv.treedb_id && has_model && treedb_here) {
896
949
  /* The compass, like every other door to a SCHEMA: the
897
950
  * `hexagon-nodes` it wore is the graph of the DATA. */
898
951
  $right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-compass-drafting",
@@ -906,7 +959,7 @@ function render_toolbar(gobj)
906
959
  "import", "EV_IMPORT", false));
907
960
  }
908
961
  }
909
- if(priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
962
+ if(priv.topic_name && !readonly && !priv.diagram && topic_here) {
910
963
  /* Only while there IS somewhere to go back to: a drag is a write,
911
964
  * and this is the way back from one. */
912
965
  if(undo_order_writes(gobj, current_topic(gobj)).length > 0) {
@@ -916,7 +969,7 @@ function render_toolbar(gobj)
916
969
  $right.push(toolbar_button("SCHEMA_ADD_COL_BTN", "yi-plus",
917
970
  "new column", "EV_ADD_COLUMN", false));
918
971
  }
919
- if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
972
+ if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram && treedb_here) {
920
973
  $right.push(toolbar_button("SCHEMA_ADD_TOPIC_BTN", "yi-plus",
921
974
  "new topic", "EV_ADD_TOPIC", false));
922
975
  }
@@ -1631,6 +1684,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1631
1684
  let priv = gobj.priv;
1632
1685
 
1633
1686
  close_dialog(gobj);
1687
+ priv.dialog_gen = priv.model_gen;
1634
1688
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
1635
1689
  dialog: true,
1636
1690
  title: title,
@@ -1679,6 +1733,10 @@ function name_control($spec, label)
1679
1733
  $spec[1]["aria-label"] = t(label);
1680
1734
  $spec[1]["data-i18n-aria-label"] = label;
1681
1735
  }
1736
+ if(!$spec[1].title) {
1737
+ $spec[1].title = t(label);
1738
+ $spec[1]["data-i18n-title"] = label;
1739
+ }
1682
1740
  return true;
1683
1741
  }
1684
1742
  for(let child of ($spec[2] || [])) {
@@ -1689,6 +1747,19 @@ function name_control($spec, label)
1689
1747
  return false;
1690
1748
  }
1691
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
+
1692
1763
  function field(logical, name, label, $control, help)
1693
1764
  {
1694
1765
  /* The label is a SIBLING of the control -- Bulma's `field` shape --
@@ -1842,6 +1913,7 @@ function open_column_form(gobj, topic, col, prefill)
1842
1913
  let hook_col = hook_topic ? hook[hook_topic] : "";
1843
1914
  let treedb = current_treedb(gobj);
1844
1915
  let sibling_topics = treedb ? treedb.topics.map(x => x.name) : [];
1916
+ let model_gen = priv.model_gen;
1845
1917
 
1846
1918
  let $form = createElement2(
1847
1919
  ["div", {class: "SCHEMA_COL_FORM box"}, [
@@ -1883,9 +1955,10 @@ function open_column_form(gobj, topic, col, prefill)
1883
1955
  text_input("description", record.description, "")),
1884
1956
  ["div", {class: "SCHEMA_COL_FORM_ACTIONS is-flex mt-4",
1885
1957
  style: "gap:.5rem; justify-content:flex-end;"}, [
1886
- ["button", {class: "SCHEMA_COL_FORM_CANCEL button", type: "button"},
1958
+ ["button", name_attrs("cancel", {class: "SCHEMA_COL_FORM_CANCEL button", type: "button"}),
1887
1959
  [["span", {i18n: "cancel"}, t("cancel")]]],
1888
- ["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"}),
1889
1962
  [["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
1890
1963
  ["span", {i18n: "save"}, t("save")]]]
1891
1964
  ]]
@@ -1926,10 +1999,11 @@ function open_column_form(gobj, topic, col, prefill)
1926
1999
  let values = read_form($form);
1927
2000
  values.flag = read_flags($form);
1928
2001
  gobj_send_event(gobj, "EV_SAVE_COLUMN", {
1929
- topic: topic.name,
1930
- col: creating ? "" : col.name,
1931
- creating: creating,
1932
- values: values
2002
+ topic: topic.name,
2003
+ col: creating ? "" : col.name,
2004
+ creating: creating,
2005
+ values: values,
2006
+ model_gen: model_gen
1933
2007
  }, gobj);
1934
2008
  });
1935
2009
 
@@ -1962,6 +2036,7 @@ function open_topic_form(gobj, treedb, topic)
1962
2036
  let record = (topic && topic.record) || {};
1963
2037
  let creating = !topic;
1964
2038
  let col_names = topic ? topic.cols.map(c => c.name) : [];
2039
+ let model_gen = gobj.priv.model_gen;
1965
2040
 
1966
2041
  let $form = createElement2(
1967
2042
  ["div", {class: "SCHEMA_TOPIC_FORM box"}, [
@@ -2001,10 +2076,10 @@ function open_topic_form(gobj, treedb, topic)
2001
2076
  ]],
2002
2077
  ["div", {class: "SCHEMA_TOPIC_FORM_ACTIONS is-flex mt-4",
2003
2078
  style: "gap:.5rem; justify-content:flex-end;"}, [
2004
- ["button", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"},
2079
+ ["button", name_attrs("cancel", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"}),
2005
2080
  [["span", {i18n: "cancel"}, t("cancel")]]],
2006
- ["button", {class: "SCHEMA_TOPIC_FORM_SAVE button is-primary",
2007
- type: "button"},
2081
+ ["button", name_attrs("save", {class: "SCHEMA_TOPIC_FORM_SAVE button is-primary",
2082
+ type: "button"}),
2008
2083
  [["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
2009
2084
  ["span", {i18n: "save"}, t("save")]]]
2010
2085
  ]]
@@ -2018,9 +2093,10 @@ function open_topic_form(gobj, treedb, topic)
2018
2093
  $form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
2019
2094
  evt.stopPropagation();
2020
2095
  gobj_send_event(gobj, "EV_SAVE_TOPIC", {
2021
- topic: creating ? "" : topic.name,
2022
- creating: creating,
2023
- values: read_form($form)
2096
+ topic: creating ? "" : topic.name,
2097
+ creating: creating,
2098
+ values: read_form($form),
2099
+ model_gen: model_gen
2024
2100
  }, gobj);
2025
2101
  });
2026
2102
 
@@ -2173,14 +2249,15 @@ function copy_text(gobj, $text)
2173
2249
  ***************************************************************/
2174
2250
  function open_import(gobj, treedb)
2175
2251
  {
2252
+ let model_gen = gobj.priv.model_gen;
2176
2253
  let $content = createElement2(
2177
2254
  ["div", {class: "SCHEMA_IMPORT box"}, [
2178
2255
  ["p", {class: "SCHEMA_IMPORT_HELP help mb-2",
2179
2256
  i18n: "paste a schema as json: the plan is shown before anything is written"},
2180
2257
  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",
2258
+ ["textarea", name_attrs("schema", {class: "SCHEMA_IMPORT_TEXT textarea is-small is-family-monospace",
2182
2259
  rows: "12", spellcheck: "false",
2183
- placeholder: '{"id": "...", "schema_version": "1", "topics": []}'}, ""],
2260
+ placeholder: '{"id": "...", "schema_version": "1", "topics": []}'}), ""],
2184
2261
  ["label", {class: "SCHEMA_IMPORT_PRUNE checkbox mt-2"}, [
2185
2262
  ["input", {type: "checkbox", class: "SCHEMA_IMPORT_PRUNE_BOX", checked: "checked"}],
2186
2263
  ["span", {class: "ml-1", i18n: "delete what the pasted schema does not declare"},
@@ -2189,13 +2266,13 @@ function open_import(gobj, treedb)
2189
2266
  ["div", {class: "SCHEMA_IMPORT_PLAN mt-3"}, []],
2190
2267
  ["div", {class: "SCHEMA_IMPORT_ACTIONS is-flex mt-3",
2191
2268
  style: "gap:.5rem; justify-content:flex-end;"}, [
2192
- ["button", {class: "SCHEMA_IMPORT_CANCEL button", type: "button"},
2269
+ ["button", name_attrs("cancel", {class: "SCHEMA_IMPORT_CANCEL button", type: "button"}),
2193
2270
  [["span", {i18n: "cancel"}, t("cancel")]]],
2194
- ["button", {class: "SCHEMA_IMPORT_PREVIEW button", type: "button"},
2271
+ ["button", name_attrs("preview", {class: "SCHEMA_IMPORT_PREVIEW button", type: "button"}),
2195
2272
  [["span", {class: "icon"}, [["i", {class: "yi-eye"}]]],
2196
2273
  ["span", {i18n: "preview"}, t("preview")]]],
2197
- ["button", {class: "SCHEMA_IMPORT_RUN button is-warning",
2198
- type: "button", disabled: "disabled"},
2274
+ ["button", name_attrs("import", {class: "SCHEMA_IMPORT_RUN button is-warning",
2275
+ type: "button", disabled: "disabled"}),
2199
2276
  [["span", {class: "icon"}, [["i", {class: "yi-upload"}]]],
2200
2277
  ["span", {i18n: "import"}, t("import")]]]
2201
2278
  ]]
@@ -2214,13 +2291,14 @@ function open_import(gobj, treedb)
2214
2291
  $content.querySelector(".SCHEMA_IMPORT_PREVIEW").addEventListener("click", (evt) => {
2215
2292
  evt.stopPropagation();
2216
2293
  gobj_send_event(gobj, "EV_PREVIEW_IMPORT", {
2217
- text: $text.value,
2218
- prune: $prune.checked
2294
+ text: $text.value,
2295
+ prune: $prune.checked,
2296
+ model_gen: model_gen
2219
2297
  }, gobj);
2220
2298
  });
2221
2299
  $run.addEventListener("click", (evt) => {
2222
2300
  evt.stopPropagation();
2223
- gobj_send_event(gobj, "EV_APPLY_IMPORT", {}, gobj);
2301
+ gobj_send_event(gobj, "EV_APPLY_IMPORT", {model_gen: model_gen}, gobj);
2224
2302
  });
2225
2303
 
2226
2304
  gobj.priv.import_pane = $plan_pane;
@@ -2228,6 +2306,23 @@ function open_import(gobj, treedb)
2228
2306
  open_dialog(gobj, $content, "import", "SCHEMA_IMPORT_DIALOG", treedb.id);
2229
2307
  }
2230
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
+
2231
2326
  /***************************************************************
2232
2327
  * The plan, drawn where the paste is: one line per write, the
2233
2328
  * deletions apart and counted, because they are what the
@@ -2304,7 +2399,7 @@ function open_orphans(gobj)
2304
2399
  ["td", {class: "SCHEMA_ORPHAN_ID"}, [["code", {}, `${entry.id}`]]],
2305
2400
  ["td", {class: "SCHEMA_ORPHAN_ACTIONS has-text-right"},
2306
2401
  readonly ? [] : [row_icon(gobj, "SCHEMA_ORPHAN_DELETE", "yi-trash", "delete",
2307
- "EV_DELETE_ORPHAN", {kind: kind, id: entry.id})]]
2402
+ "EV_DELETE_ORPHAN", {kind: kind, id: entry.id, model_gen: priv.model_gen})]]
2308
2403
  ]];
2309
2404
  };
2310
2405
 
@@ -2519,6 +2614,11 @@ function transport_dropped(gobj)
2519
2614
  priv.load_round++;
2520
2615
  priv.pending = 0;
2521
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
+ }
2522
2622
  return end_load(gobj);
2523
2623
  }
2524
2624
  return 0;
@@ -2530,6 +2630,12 @@ function transport_dropped(gobj)
2530
2630
  * in flight (ac_show() kept it), or the one it had. Without a
2531
2631
  * model there is no screen to go to: the position is kept for the
2532
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.
2533
2639
  ***************************************************************/
2534
2640
  function end_load(gobj)
2535
2641
  {
@@ -2537,6 +2643,13 @@ function end_load(gobj)
2537
2643
  let seg = priv.pending_seg;
2538
2644
 
2539
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
+ }
2540
2653
  if(priv.model) {
2541
2654
  if(seg !== null) {
2542
2655
  return apply_seg(gobj, seg);
@@ -2885,6 +2998,23 @@ function ac_mt_command_answer(gobj, event, kw, src)
2885
2998
  return 0;
2886
2999
  }
2887
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
+ }
2888
3018
  priv.model = null;
2889
3019
  return end_load(gobj);
2890
3020
  }
@@ -2893,6 +3023,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
2893
3023
  * (NOT in build_model(): that also runs on the patch after every
2894
3024
  * write, which is exactly when the undo has to survive.) */
2895
3025
  priv.order_undo = {};
3026
+ priv.records_before = null;
3027
+ priv.model_gen++;
2896
3028
  build_model(gobj);
2897
3029
  /* Same reason as order_undo above: this is the LOAD, and the
2898
3030
  * baseline is what a later "the version did not move" is measured
@@ -2931,12 +3063,26 @@ function ac_mt_command_answer(gobj, event, kw, src)
2931
3063
  patch_record(gobj, topic_name, record);
2932
3064
  } else {
2933
3065
  /* 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`);
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
+ }
2936
3081
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
2937
3082
  let write = priv.save_queue[0];
2938
3083
  mark_written(gobj, topic_name, write ? write.record : null);
2939
3084
  }
3085
+ priv.reload_after_write = false;
2940
3086
  priv.save_queue = [];
2941
3087
  priv.save_return = "";
2942
3088
  priv.save_wrote = false;
@@ -3010,8 +3156,14 @@ function late_write_done(gobj, command, topic_name, data)
3010
3156
  /***************************************************************
3011
3157
  * The topic a record written belongs to, read off the RECORD and
3012
3158
  * 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.
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).
3015
3167
  ***************************************************************/
3016
3168
  function mark_written_record(gobj, topic_name, record)
3017
3169
  {
@@ -3027,13 +3179,8 @@ function mark_written_record(gobj, topic_name, record)
3027
3179
  if(topic_name !== T_COLS) {
3028
3180
  return;
3029
3181
  }
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
- }
3182
+ for(let ref of parse_fkey_ref(record.topics)) {
3183
+ priv.written[ref.id] = true;
3037
3184
  }
3038
3185
  }
3039
3186
 
@@ -3142,16 +3289,21 @@ function ac_refresh(gobj, event, kw, src)
3142
3289
  * operator can still reach, and what it asks is computed against
3143
3290
  * a model the answers in the air are about to replace.
3144
3291
  *
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).
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.
3149
3301
  ***************************************************************/
3150
3302
  function ac_wait_for_the_load(gobj, event, kw, src)
3151
3303
  {
3152
3304
  log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas are loading`);
3153
3305
  yui_shell_show_error(yui_shell_of(gobj),
3154
- "the schemas are loading: try again when they are in", {t: t});
3306
+ "the schemas are loading: wait for them", {t: t});
3155
3307
  return -1;
3156
3308
  }
3157
3309
 
@@ -3245,6 +3397,7 @@ function ac_show_orphans(gobj, event, kw, src)
3245
3397
  ***************************************************************/
3246
3398
  function confirm_then(gobj, message, detail, kw)
3247
3399
  {
3400
+ kw = Object.assign({model_gen: gobj.priv.model_gen}, kw);
3248
3401
  yui_shell_confirm_danger(yui_shell_of(gobj), message, {
3249
3402
  t: t,
3250
3403
  detail: detail
@@ -3330,6 +3483,9 @@ function ac_save_column(gobj, event, kw, src)
3330
3483
  if(refuse_if_readonly(gobj, event)) {
3331
3484
  return -1;
3332
3485
  }
3486
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3487
+ return -1;
3488
+ }
3333
3489
  if(!topic || (!kw.creating && !col)) {
3334
3490
  log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.topic}.${kw.col}`);
3335
3491
  return -1;
@@ -3484,6 +3640,9 @@ function ac_save_topic(gobj, event, kw, src)
3484
3640
  if(refuse_if_readonly(gobj, event)) {
3485
3641
  return -1;
3486
3642
  }
3643
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3644
+ return -1;
3645
+ }
3487
3646
  if(!treedb || (!kw.creating && !topic)) {
3488
3647
  log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
3489
3648
  return -1;
@@ -3538,6 +3697,9 @@ function ac_delete_orphan(gobj, event, kw, src)
3538
3697
  if(refuse_if_readonly(gobj, event)) {
3539
3698
  return -1;
3540
3699
  }
3700
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3701
+ return -1;
3702
+ }
3541
3703
  if(empty_string(kw.id)) {
3542
3704
  log_error(`${gobj_short_name(gobj)}: ${event} names nothing`);
3543
3705
  return -1;
@@ -3610,6 +3772,9 @@ function ac_preview_import(gobj, event, kw, src)
3610
3772
  let treedb = current_treedb(gobj);
3611
3773
  let incoming = null;
3612
3774
 
3775
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3776
+ return -1;
3777
+ }
3613
3778
  if(!treedb) {
3614
3779
  log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
3615
3780
  return -1;
@@ -3637,6 +3802,9 @@ function ac_apply_import(gobj, event, kw, src)
3637
3802
  if(refuse_if_readonly(gobj, event)) {
3638
3803
  return -1;
3639
3804
  }
3805
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3806
+ return -1;
3807
+ }
3640
3808
  if(!plan || plan.writes.length === 0) {
3641
3809
  log_error(`${gobj_short_name(gobj)}: ${event} with no plan to run`);
3642
3810
  return -1;
@@ -3687,6 +3855,9 @@ function ac_confirmed(gobj, event, kw, src)
3687
3855
  if(refuse_if_readonly(gobj, event)) {
3688
3856
  return -1;
3689
3857
  }
3858
+ if(built_on_a_replaced_model(gobj, event, kw)) {
3859
+ return -1;
3860
+ }
3690
3861
  if(kw.what === "import") {
3691
3862
  return run_import(gobj);
3692
3863
  }