@yuneta/gobj-ui 7.25.14 → 7.25.16

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.
Files changed (35) hide show
  1. package/README.md +100 -4
  2. package/dist/gobj-ui.cjs.js +406 -94
  3. package/dist/gobj-ui.es.js +406 -94
  4. package/package.json +2 -2
  5. package/src/c_g6_nodes_tree.js +79 -15
  6. package/src/c_yui_schema_editor.js +343 -66
  7. package/src/c_yui_treedb_graph.js +53 -6
  8. package/src/c_yui_treedb_topic_with_form.js +10 -10
  9. package/src/c_yui_treedb_topics.js +7 -7
  10. package/src/form_busy.js +2 -3
  11. package/src/form_busy.test.js +1 -1
  12. package/src/form_time_value.js +1 -1
  13. package/src/form_time_value.test.js +1 -1
  14. package/src/form_writes_in_flight.js +1 -2
  15. package/src/form_writes_in_flight.test.js +1 -1
  16. package/src/g6_graphs_write_owed.wiring.test.js +200 -0
  17. package/src/g6_nodes_tree_fsm.test.js +33 -0
  18. package/src/graph_save_plan.js +28 -0
  19. package/src/graph_save_plan.test.js +36 -2
  20. package/src/host_drafts.js +2 -3
  21. package/src/host_drafts.test.js +2 -2
  22. package/src/schema_editor_drafts.wiring.test.js +2 -2
  23. package/src/schema_editor_reload.wiring.test.js +329 -49
  24. package/src/schema_editor_transport.wiring.test.js +2 -2
  25. package/src/schema_version_bump.test.js +1 -1
  26. package/src/shell_modals.js +31 -11
  27. package/src/shell_toast_once.test.js +73 -10
  28. package/src/table_cell_text.js +1 -1
  29. package/src/table_cell_text.test.js +1 -1
  30. package/src/treedb_graph_writes.wiring.test.js +208 -0
  31. package/src/treedb_topics_transport.wiring.test.js +2 -2
  32. package/src/yui_install.js +14 -3
  33. package/src/yui_install.test.js +23 -0
  34. package/src/yui_row_search.test.js +1 -1
  35. package/src/yui_table_select.test.js +1 -1
@@ -22,11 +22,10 @@
22
22
  * AN EDIT IS A DRAFT. A write here moves no version and reaches no
23
23
  * treedb: the host SAVES it (C_TREEDB's save-schema raises the
24
24
  * topic_version of what changed and the schema_version, once) and
25
- * APPLIES it (apply-schema + a restart of the owning yuno). Until
26
- * the owner's design of M36 (2026-09-21 review) every write here
27
- * raised both versions, so an edit half made was already the
28
- * schema of the next start. The topic list marks what this session
29
- * wrote and has not saved (EV_REFRESH forgets it: the view's own
25
+ * APPLIES it (apply-schema + a restart of the owning yuno). If a
26
+ * write here raised both versions, an edit half made would already
27
+ * be the schema of the next start. The topic list marks what this
28
+ * session wrote and has not saved (EV_REFRESH forgets it: the view's own
30
29
  * Refresh button sends it, and a host may after a save -- the
31
30
  * agent console's does not) and what the host says is not saved
32
31
  * (EV_DRAFTS, replaced whole each time it is sent).
@@ -118,6 +117,7 @@ import {
118
117
  kw_get_int,
119
118
  kw_get_dict,
120
119
  is_object,
120
+ json_deep_copy,
121
121
  is_array,
122
122
  empty_string,
123
123
  } from "@yuneta/gobj-js";
@@ -188,6 +188,11 @@ const DIAGRAM_SEG = "diagram";
188
188
  * them), and each filters on its own purpose. */
189
189
  const PURPOSE = "schema_editor";
190
190
 
191
+ /* The dialogs that show what they were opened with and send nothing a
192
+ * screen declares: a move sent by the host leaves them up (see
193
+ * close_dialog_left_behind()). */
194
+ const DIALOGS_THAT_READ_NOTHING = ["SCHEMA_EXPORT_DIALOG", "SCHEMA_REPORT_DIALOG"];
195
+
191
196
 
192
197
  /***************************************************************
193
198
  * Attrs
@@ -230,6 +235,7 @@ let PRIVATE_DATA = {
230
235
  diagram_gobj: null,
231
236
  dialog: null,
232
237
  dialog_gen: 0, /* the model_gen the open dialog was built on */
238
+ dialog_kind: "", /* the logical class of the open dialog */
233
239
 
234
240
  /* A write, or a plan of them, in flight */
235
241
  save_queue: null,
@@ -241,6 +247,8 @@ let PRIVATE_DATA = {
241
247
  dropped_round: 0, /* the load a drop ended: its failures are the drop, said once */
242
248
  dropped_write: 0, /* the same for the write a drop ended */
243
249
  reload_after_write: false, /* a write given up turned out DONE while another was in flight */
250
+ refresh_after_write: false, /* EV_REFRESH heard while writes were in flight: run at their end */
251
+ reopen_form: null, /* the form whose Save ran the owed reload: opened again once it lands */
244
252
 
245
253
  /* A position that arrived while a load or a write was in flight */
246
254
  pending_seg: null,
@@ -460,9 +468,9 @@ function refuse_if_readonly(gobj, event)
460
468
  * dumped by the machine trace).
461
469
  *
462
470
  * Refused, and said. A column form sends every field it shows, so
463
- * its Save after a reload wrote the OLD record over the newer one
464
- * (fourth independent review, probe A3); a form whose column the
465
- * reload removed answered "names no column" for ever. end_load()
471
+ * its Save after a reload wrote the OLD record over the newer one;
472
+ * a form whose column the reload removed answered "names no column"
473
+ * for ever. end_load()
466
474
  * closes such a dialog when the load lands; this is the same rule
467
475
  * for whatever arrives anyway -- a stale screen, a confirmation
468
476
  * that was up, a keyboard.
@@ -489,8 +497,7 @@ function built_on_a_replaced_model(gobj, event, kw)
489
497
  * the model shown is kept, and it may be older than the store.
490
498
  * The session is up, so no reconnect will ask for it again -- it
491
499
  * was owed for as long as the session lasted, and a form opened
492
- * on that model wrote every field of it over the newer record
493
- * (fifth independent review; A3 of the fourth, by another path).
500
+ * on that model wrote every field of it over the newer record.
494
501
  *
495
502
  * So the operator's NEXT action asks it. An edit -- a form, a
496
503
  * write, a confirmation, what reads the model whole (check,
@@ -518,12 +525,37 @@ function owes_reload(gobj)
518
525
  state !== "ST_LOADING" && state !== "ST_SAVING";
519
526
  }
520
527
 
521
- function reload_first(gobj, event)
528
+ function reload_first(gobj, event, reopen)
522
529
  {
530
+ let priv = gobj.priv;
531
+
523
532
  if(!owes_reload(gobj)) {
524
533
  return false;
525
534
  }
526
- gobj.priv.reload_on_open = false;
535
+ priv.reload_on_open = false;
536
+
537
+ /* The Save of a FORM: what the operator typed is not thrown away
538
+ * with the model it was typed on. The load that lands closes the
539
+ * form (end_load()) and opens it again on the schemas it read,
540
+ * with the fields the operator CHANGED put back on top -- only
541
+ * those: a field left as it was shows what the store holds now,
542
+ * so the next Save does not write the old value over a newer
543
+ * one. Closed alone, the form took what was typed with it. */
544
+ if(reopen && priv.dialog) {
545
+ priv.reopen_form = Object.assign(json_deep_copy(reopen), {
546
+ dialog: priv.dialog,
547
+ treedb_id: priv.treedb_id
548
+ });
549
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas shown ` +
550
+ `may be out of date: they are read again first, and the form opens again ` +
551
+ `on them with its changes`);
552
+ yui_shell_show_error(yui_shell_of(gobj),
553
+ "the schemas shown may be out of date: they are read again, and the form opens again on them with your changes",
554
+ {t: t});
555
+ request_model(gobj, true);
556
+ return true;
557
+ }
558
+
527
559
  log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas shown ` +
528
560
  `may be out of date: they are read again first`);
529
561
  yui_shell_show_error(yui_shell_of(gobj),
@@ -533,6 +565,53 @@ function reload_first(gobj, event)
533
565
  return true;
534
566
  }
535
567
 
568
+ /***************************************************************
569
+ * The form reload_first() took down, opened again on the model
570
+ * the load just made, with the operator's changes on top.
571
+ *
572
+ * Only where it can mean the same thing: on the treedb it was
573
+ * opened on, on the screen whose Save it has, and on a record
574
+ * that is still there. Otherwise it is not opened, and that is
575
+ * said -- a form opened on another treedb, or on nothing, would
576
+ * save somewhere the operator never meant.
577
+ ***************************************************************/
578
+ function reopen_form(gobj, r)
579
+ {
580
+ let priv = gobj.priv;
581
+ let state = gobj_current_state(gobj);
582
+ let treedb = current_treedb(gobj);
583
+ let topic = null;
584
+ let col = null;
585
+ let fits = false;
586
+
587
+ if(treedb && r.treedb_id === priv.treedb_id) {
588
+ if(r.form === "column" && state === "ST_COLUMNS") {
589
+ topic = find_topic(priv.model, priv.treedb_id, r.topic);
590
+ col = (topic && !r.creating) ?
591
+ find_col(priv.model, priv.treedb_id, r.topic, r.col) : null;
592
+ fits = !!topic && (r.creating || !!col);
593
+ } else if(r.form === "topic" && state === "ST_TOPICS") {
594
+ topic = r.creating ? null : find_topic(priv.model, priv.treedb_id, r.topic);
595
+ fits = r.creating || !!topic;
596
+ }
597
+ }
598
+ if(!fits) {
599
+ log_warning(`${gobj_short_name(gobj)}: the ${r.form} form was not opened again: ` +
600
+ `what it edited is not on the schemas the reload read ` +
601
+ `(${r.treedb_id} ${r.topic}${r.col ? "." + r.col : ""}, now ${state})`);
602
+ yui_shell_show_error(yui_shell_of(gobj),
603
+ "the schemas were read again and what the form was editing is not there any more",
604
+ {t: t});
605
+ return -1;
606
+ }
607
+ if(r.form === "column") {
608
+ open_column_form(gobj, topic, col, null, r.changed);
609
+ } else {
610
+ open_topic_form(gobj, treedb, topic, r.changed);
611
+ }
612
+ return 0;
613
+ }
614
+
536
615
  function reload_after_move(gobj)
537
616
  {
538
617
  if(!owes_reload(gobj)) {
@@ -562,7 +641,7 @@ function reload_after_move(gobj)
562
641
  * none of the actions of a screen, so the screen it replaces goes
563
642
  * and the toolbar stops answering: left up, a click on a card, on
564
643
  * Back or on the drawing answered "Event NOT DEFINED in state
565
- * ST_LOADING" and was lost (third independent review). A dialog
644
+ * ST_LOADING" and was lost. A dialog
566
645
  * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
567
646
  * and a load that LANDS closes it (end_load()): it was built on the
568
647
  * model the load replaced, and its Save sends every field it shows.
@@ -721,7 +800,7 @@ function build_model(gobj)
721
800
  * measurement of "changed since" can be measured against.
722
801
  *
723
802
  * `written` is forgotten only by an EV_REFRESH (the view's own
724
- * Refresh button, or a host that sends one). Since M36 a write moves no
803
+ * Refresh button, or a host that sends one). Since 7.23.196 a write moves no
725
804
  * version, so `written` is the draft chip of what this session
726
805
  * wrote and no longer half of a version measurement: a reload of
727
806
  * the reconnect, or of a write that turned out done, forgot the
@@ -969,8 +1048,7 @@ function render_toolbar(gobj)
969
1048
  * reload that no longer has them keeps the position (it says so,
970
1049
  * and Back is the way out). Asked of the MODEL, not of the state:
971
1050
  * gated on the state, Diagram, Check, Export, Import and New topic
972
- * stayed up there and each logged "with no treedb open" (fourth
973
- * independent review, probe D2). */
1051
+ * stayed up there and each logged "with no treedb open". */
974
1052
  let treedb_here = on_a_treedb && !!current_treedb(gobj);
975
1053
  let topic_here = treedb_here && !!current_topic(gobj);
976
1054
  /* Nothing is legal while the model is in the air: every action is
@@ -1745,6 +1823,7 @@ function close_dialog(gobj)
1745
1823
  }
1746
1824
  let dialog = priv.dialog;
1747
1825
  priv.dialog = null;
1826
+ priv.dialog_kind = "";
1748
1827
  priv.export_view = null;
1749
1828
  try {
1750
1829
  dialog.close();
@@ -1759,6 +1838,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1759
1838
 
1760
1839
  close_dialog(gobj);
1761
1840
  priv.dialog_gen = priv.model_gen;
1841
+ priv.dialog_kind = logical;
1762
1842
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
1763
1843
  dialog: true,
1764
1844
  title: title,
@@ -1767,6 +1847,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1767
1847
  t: t,
1768
1848
  on_close: () => {
1769
1849
  priv.dialog = null;
1850
+ priv.dialog_kind = "";
1770
1851
  priv.export_view = null;
1771
1852
  }
1772
1853
  });
@@ -1909,6 +1990,75 @@ function read_form($root)
1909
1990
  return out;
1910
1991
  }
1911
1992
 
1993
+ /***************************************************************
1994
+ * A column form as its Save sends it: the fields and the flags.
1995
+ ***************************************************************/
1996
+ function column_form_state($form)
1997
+ {
1998
+ let values = read_form($form);
1999
+ values.flag = read_flags($form);
2000
+ return values;
2001
+ }
2002
+
2003
+ /***************************************************************
2004
+ * The fields the operator CHANGED: those whose value is not the
2005
+ * one the form was opened with.
2006
+ ***************************************************************/
2007
+ function form_changes(initial, values)
2008
+ {
2009
+ let changed = {};
2010
+
2011
+ for(let name of Object.keys(values)) {
2012
+ if(JSON.stringify(values[name]) !== JSON.stringify(initial[name])) {
2013
+ changed[name] = values[name];
2014
+ }
2015
+ }
2016
+ return changed;
2017
+ }
2018
+
2019
+ /***************************************************************
2020
+ * Put changes back into a form just built (reopen_form()). The
2021
+ * flags are drawn again for the type the changes leave, holding
2022
+ * the flags they name, so none of them is hidden by the type.
2023
+ ***************************************************************/
2024
+ function apply_form_changes(gobj, $form, changes)
2025
+ {
2026
+ for(let name of Object.keys(changes)) {
2027
+ if(name === "flag") {
2028
+ continue;
2029
+ }
2030
+ let $control = $form.querySelector(`[data-name="${name}"]`);
2031
+ if(!$control) {
2032
+ log_warning(`${gobj_short_name(gobj)}: a change of '${name}' has no field ` +
2033
+ `in the form opened again: not put back`);
2034
+ continue;
2035
+ }
2036
+ if($control.type === "checkbox") {
2037
+ $control.checked = !!changes[name];
2038
+ } else {
2039
+ $control.value = changes[name];
2040
+ }
2041
+ }
2042
+
2043
+ let $type = $form.querySelector('[data-name="type"]');
2044
+ if($type && $form.querySelector(".SCHEMA_FLAGS")) {
2045
+ let flags = Array.isArray(changes.flag) ? changes.flag : read_flags($form);
2046
+ redraw_flags(gobj, $form, $type.value, flags);
2047
+ }
2048
+ }
2049
+
2050
+ /***************************************************************
2051
+ * Draw the flags of a column form again, for `type`, with
2052
+ * `current` checked.
2053
+ ***************************************************************/
2054
+ function redraw_flags(gobj, $form, type, current)
2055
+ {
2056
+ let $holder = $form.querySelector(".SCHEMA_FLAGS");
2057
+ let $new = createElement2(flags_field(gobj, type, current));
2058
+ $holder.parentNode.replaceChild($new, $holder);
2059
+ refresh_language($new, t);
2060
+ }
2061
+
1912
2062
  /***************************************************************
1913
2063
  * The flags of a column, as checkboxes that say what they do.
1914
2064
  *
@@ -1971,7 +2121,7 @@ function read_flags($root)
1971
2121
  * Offering a field that silently does something else is worse
1972
2122
  * than not offering it.
1973
2123
  ***************************************************************/
1974
- function open_column_form(gobj, topic, col, prefill)
2124
+ function open_column_form(gobj, topic, col, prefill, changes)
1975
2125
  {
1976
2126
  let priv = gobj.priv;
1977
2127
  /* A DUPLICATE is a creation whose fields start filled: the name must
@@ -2045,11 +2195,7 @@ function open_column_form(gobj, topic, col, prefill)
2045
2195
  * new type does not use. */
2046
2196
  let $type = $form.querySelector('[data-name="type"]');
2047
2197
  $type.addEventListener("change", () => {
2048
- let $holder = $form.querySelector(".SCHEMA_FLAGS");
2049
- let current = read_flags($form);
2050
- let $new = createElement2(flags_field(gobj, $type.value, current));
2051
- $holder.parentNode.replaceChild($new, $holder);
2052
- refresh_language($new, t);
2198
+ redraw_flags(gobj, $form, $type.value, read_flags($form));
2053
2199
  });
2054
2200
 
2055
2201
  /* A flag that excludes another is answered here and not on save,
@@ -2069,15 +2215,22 @@ function open_column_form(gobj, topic, col, prefill)
2069
2215
  evt.stopPropagation();
2070
2216
  close_dialog(gobj);
2071
2217
  });
2218
+ /* What the form showed before the operator touched it: what
2219
+ * was CHANGED is measured against it (form_changes()). */
2220
+ let initial = column_form_state($form);
2221
+ if(changes) {
2222
+ apply_form_changes(gobj, $form, changes);
2223
+ }
2224
+
2072
2225
  $form.querySelector(".SCHEMA_COL_FORM_SAVE").addEventListener("click", (evt) => {
2073
2226
  evt.stopPropagation();
2074
- let values = read_form($form);
2075
- values.flag = read_flags($form);
2227
+ let values = column_form_state($form);
2076
2228
  gobj_send_event(gobj, "EV_SAVE_COLUMN", {
2077
2229
  topic: topic.name,
2078
2230
  col: creating ? "" : col.name,
2079
2231
  creating: creating,
2080
2232
  values: values,
2233
+ changed: creating ? values : form_changes(initial, values),
2081
2234
  model_gen: model_gen
2082
2235
  }, gobj);
2083
2236
  });
@@ -2106,7 +2259,7 @@ function stringify_field(value)
2106
2259
  * actually has: a pkey naming no column is a topic that does
2107
2260
  * not open, and it is the first thing the validator looks for.
2108
2261
  ***************************************************************/
2109
- function open_topic_form(gobj, treedb, topic)
2262
+ function open_topic_form(gobj, treedb, topic, changes)
2110
2263
  {
2111
2264
  let record = (topic && topic.record) || {};
2112
2265
  let creating = !topic;
@@ -2165,12 +2318,19 @@ function open_topic_form(gobj, treedb, topic)
2165
2318
  evt.stopPropagation();
2166
2319
  close_dialog(gobj);
2167
2320
  });
2321
+ let initial = read_form($form);
2322
+ if(changes) {
2323
+ apply_form_changes(gobj, $form, changes);
2324
+ }
2325
+
2168
2326
  $form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
2169
2327
  evt.stopPropagation();
2328
+ let values = read_form($form);
2170
2329
  gobj_send_event(gobj, "EV_SAVE_TOPIC", {
2171
2330
  topic: creating ? "" : topic.name,
2172
2331
  creating: creating,
2173
- values: read_form($form),
2332
+ values: values,
2333
+ changed: creating ? values : form_changes(initial, values),
2174
2334
  model_gen: model_gen
2175
2335
  }, gobj);
2176
2336
  });
@@ -2674,11 +2834,11 @@ function transport_in_session(gobj)
2674
2834
  * The state is part of the question, and that is only true
2675
2835
  * because NOTHING moves the editor out of ST_LOADING or ST_SAVING
2676
2836
  * but the end of what is in flight: a position the host sends
2677
- * meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
2678
- * move a RELOAD out of ST_LOADING, and every answer of it was then
2679
- * "for a load that is over": the records it had emptied stayed
2680
- * empty, and the next write patched them into a model with no
2681
- * treedb at all.
2837
+ * meanwhile waits in `pending_seg` (ac_show()). If EV_SHOW moved a
2838
+ * RELOAD out of ST_LOADING, every answer of it would be "for a load
2839
+ * that is over": the records it had emptied would stay empty, and
2840
+ * the next write would patch them into a model with no treedb at
2841
+ * all.
2682
2842
  ***************************************************************/
2683
2843
  function is_current_load(gobj, md)
2684
2844
  {
@@ -2750,27 +2910,46 @@ function end_load(gobj)
2750
2910
  {
2751
2911
  let priv = gobj.priv;
2752
2912
  let seg = priv.pending_seg;
2913
+ let reopen = priv.reopen_form;
2753
2914
 
2754
2915
  priv.pending_seg = null;
2916
+ priv.reopen_form = null;
2755
2917
  if(priv.dialog && priv.dialog_gen !== priv.model_gen) {
2756
- log_warning(`${gobj_short_name(gobj)}: a dialog was open on the schemas ` +
2757
- `the load replaced: closed`);
2918
+ /* The form whose Save asked for this load is opened again,
2919
+ * below, once the screen is there (reload_first()). Any other
2920
+ * dialog is closed, and that is said. */
2921
+ if(!reopen || reopen.dialog !== priv.dialog) {
2922
+ reopen = null;
2923
+ log_warning(`${gobj_short_name(gobj)}: a dialog was open on the schemas ` +
2924
+ `the load replaced: closed`);
2925
+ yui_shell_show_error(yui_shell_of(gobj),
2926
+ "the schemas were read again: open the dialog again", {t: t});
2927
+ }
2758
2928
  close_dialog(gobj);
2759
- yui_shell_show_error(yui_shell_of(gobj),
2760
- "the schemas were read again: open the dialog again", {t: t});
2929
+ } else {
2930
+ /* Closed meanwhile by the operator, or a load that replaced
2931
+ * nothing: the form is where it was, or where it was left. */
2932
+ reopen = null;
2761
2933
  }
2934
+
2935
+ let ret = 0;
2762
2936
  if(priv.model) {
2763
2937
  if(seg !== null) {
2764
- return apply_seg(gobj, seg);
2938
+ ret = apply_seg(gobj, seg);
2939
+ } else {
2940
+ ret = go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2765
2941
  }
2766
- return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2942
+ } else {
2943
+ if(seg !== null) {
2944
+ adopt_seg(gobj, seg);
2945
+ }
2946
+ gobj_change_state(gobj, "ST_IDLE");
2947
+ render(gobj);
2767
2948
  }
2768
- if(seg !== null) {
2769
- adopt_seg(gobj, seg);
2949
+ if(reopen) {
2950
+ reopen_form(gobj, reopen);
2770
2951
  }
2771
- gobj_change_state(gobj, "ST_IDLE");
2772
- render(gobj);
2773
- return 0;
2952
+ return ret;
2774
2953
  }
2775
2954
 
2776
2955
  /***************************************************************
@@ -2813,7 +2992,17 @@ function end_writes(gobj, error)
2813
2992
  render(gobj);
2814
2993
  publish_check(gobj);
2815
2994
  }
2816
- reload_if_owed(gobj);
2995
+ if(priv.refresh_after_write) {
2996
+ /* The EV_REFRESH heard while the writes were in flight
2997
+ * (ac_refresh_after_the_writes()), run now that none is. It
2998
+ * reads the store whole, so it also pays a reload a late
2999
+ * write owed. */
3000
+ priv.refresh_after_write = false;
3001
+ priv.reload_after_write = false;
3002
+ request_model(gobj, false);
3003
+ } else {
3004
+ reload_if_owed(gobj);
3005
+ }
2817
3006
  return error ? -1 : 0;
2818
3007
  }
2819
3008
 
@@ -2995,12 +3184,74 @@ function position_seg(gobj)
2995
3184
  * ...and the same in reverse. A tail naming a treedb or a topic
2996
3185
  * the store does not have lands one level up rather than on an
2997
3186
  * empty screen: a shared link outlives the schema it points at.
3187
+ *
3188
+ * Every position the HOST sends comes through here, at once
3189
+ * (ac_show()) or when what was in flight ends (end_load(),
3190
+ * end_writes()). A move to another position closes the dialog of
3191
+ * the screen the editor left (close_dialog_left_behind()).
2998
3192
  ***************************************************************/
2999
3193
  function apply_seg(gobj, seg)
3000
3194
  {
3195
+ let before = position_seg(gobj);
3001
3196
  let pos = seg_position(seg);
3002
3197
 
3003
- return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
3198
+ let ret = go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
3199
+ if(position_seg(gobj) !== before) {
3200
+ close_dialog_left_behind(gobj);
3201
+ }
3202
+ return ret;
3203
+ }
3204
+
3205
+ /***************************************************************
3206
+ * The host moved the editor while a dialog was up. C_YUI_SHELL
3207
+ * keeps the overlays open on a move that changes only the subpath
3208
+ * (a url typed in, a link), so the dialog outlives its screen.
3209
+ *
3210
+ * A form, the import and the orphans send events that only the
3211
+ * screen they were opened from declares, and they name their
3212
+ * topic under the treedb open at the time. Left up on another
3213
+ * screen, their Save answered "Event NOT DEFINED" and the edit was
3214
+ * lost with no word. So the dialog is closed and the operator told.
3215
+ * The import plan goes with its dialog: it was computed for the
3216
+ * treedb the editor left.
3217
+ *
3218
+ * The export and the check stay: they show the texts they were
3219
+ * opened with, and what they send is legal in every state.
3220
+ ***************************************************************/
3221
+ function close_dialog_left_behind(gobj)
3222
+ {
3223
+ let priv = gobj.priv;
3224
+
3225
+ if(!priv.dialog || DIALOGS_THAT_READ_NOTHING.includes(priv.dialog_kind)) {
3226
+ return;
3227
+ }
3228
+ log_warning(`${gobj_short_name(gobj)}: the host moved the view while the dialog ` +
3229
+ `${priv.dialog_kind} was open: closed`);
3230
+ close_dialog(gobj);
3231
+ forget_import_plan(gobj);
3232
+ yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", {t: t});
3233
+ }
3234
+
3235
+ /***************************************************************
3236
+ * A confirmation answered on another screen than the one it was
3237
+ * asked from. A confirmation is a shell modal, so a move sent by
3238
+ * the host does not close it. It names its topic and column, and
3239
+ * the actions look them up under the treedb open NOW: a Yes after
3240
+ * a move to another treedb deleted the topic or column of the same
3241
+ * name THERE.
3242
+ *
3243
+ * Refused, and said. A kw with no `seg` stamp is not a
3244
+ * confirmation's and is not asked.
3245
+ ***************************************************************/
3246
+ function asked_on_another_screen(gobj, event, kw)
3247
+ {
3248
+ if(!kw || kw.seg === undefined || kw.seg === position_seg(gobj)) {
3249
+ return false;
3250
+ }
3251
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, it was asked on ` +
3252
+ `'${kw.seg}' and the view is on '${position_seg(gobj)}'`);
3253
+ yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", {t: t});
3254
+ return true;
3004
3255
  }
3005
3256
 
3006
3257
  function seg_position(seg)
@@ -3111,10 +3362,10 @@ function ac_mt_command_answer(gobj, event, kw, src)
3111
3362
  * slow `nodes`): the model shown is what the store held a
3112
3363
  * moment ago, and blanking it took the screen and every open
3113
3364
  * form with it -- a form's Save then answered "Event NOT
3114
- * DEFINED in state ST_IDLE" (fourth independent review). It
3365
+ * DEFINED in state ST_IDLE". It
3115
3366
  * stays, as it does when the load cannot leave, and the
3116
3367
  * reload is owed: the operator's next action asks it
3117
- * (reload_first(), fifth independent review). */
3368
+ * (reload_first()). */
3118
3369
  if(priv.model) {
3119
3370
  log_warning(`${gobj_short_name(gobj)}: the schemas could not be read ` +
3120
3371
  `again (${priv.load_error}): the previous ones stay`);
@@ -3139,13 +3390,13 @@ function ac_mt_command_answer(gobj, event, kw, src)
3139
3390
  /* The store was read whole: whatever reload was owed is paid,
3140
3391
  * whoever asked for this one -- the operator's Refresh, a late
3141
3392
  * write, the reconnect. Left owed, the next edit was refused
3142
- * and read the store AGAIN (sixth independent review). */
3393
+ * and read the store AGAIN. */
3143
3394
  priv.reload_on_open = false;
3144
3395
  /* The plan was a list of writes computed on the model this load
3145
3396
  * replaces. Forgotten HERE, where the model goes, and not when
3146
3397
  * the requests leave: a load that fails replaces nothing, and
3147
3398
  * forgetting early left its dialog up with no plan and Import
3148
- * disabled (sixth independent review). */
3399
+ * disabled. */
3149
3400
  forget_import_plan(gobj);
3150
3401
  build_model(gobj);
3151
3402
  /* Same reason as order_undo above: this is the LOAD, and the
@@ -3191,7 +3442,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3191
3442
  * silence, a drag of five columns stopped at the first.
3192
3443
  * The reload is also the one a late write may have owed
3193
3444
  * (reload_after_write): left set, the next write loaded
3194
- * the whole model once more (fourth independent review). */
3445
+ * the whole model once more. */
3195
3446
  let not_sent = priv.save_queue.length - 1;
3196
3447
  log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record` +
3197
3448
  (not_sent > 0 ? `: ${not_sent} write(s) after it were not sent` : ""));
@@ -3211,7 +3462,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
3211
3462
  gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
3212
3463
  treedb_name: gobj_read_str_attr(gobj, "treedb_name")
3213
3464
  });
3214
- request_model(gobj, true);
3465
+ /* An EV_REFRESH heard during the writes is paid by this
3466
+ * load: it forgets the drafts, as the refresh would. */
3467
+ let refresh = priv.refresh_after_write;
3468
+ priv.refresh_after_write = false;
3469
+ request_model(gobj, !refresh);
3215
3470
  return 0;
3216
3471
  }
3217
3472
  }
@@ -3284,8 +3539,7 @@ function late_write_done(gobj, command, topic_name, data)
3284
3539
  * The fkey is read with parse_fkey_ref(), because the store answers
3285
3540
  * in list_dict -- `[{topic_name, id, hook_name}]` -- and not in the
3286
3541
  * `topics^<topic id>^cols` strings this view writes. Split as a
3287
- * string, every production answer marked nothing (fourth independent
3288
- * review, probe C).
3542
+ * string, every production answer marked nothing.
3289
3543
  ***************************************************************/
3290
3544
  function mark_written_record(gobj, topic_name, record)
3291
3545
  {
@@ -3344,7 +3598,7 @@ function ac_show(gobj, event, kw, src)
3344
3598
  /* A move like the editor's own (ac_back() and the rest): it goes,
3345
3599
  * and pays the reload a refused load owes there. Without it a url
3346
3600
  * or the browser's Back left it owed on a screen that asked for
3347
- * nothing (sixth independent review). */
3601
+ * nothing. */
3348
3602
  apply_seg(gobj, seg);
3349
3603
  return reload_after_move(gobj);
3350
3604
  }
@@ -3368,7 +3622,7 @@ function ac_hide(gobj, event, kw, src)
3368
3622
  *
3369
3623
  * Nothing else ends a load or a write the drop cut: their answers
3370
3624
  * will never come, and waiting in ST_LOADING skipped the reload
3371
- * itself (M-1 of the independent review of 7.25.4).
3625
+ * itself.
3372
3626
  ***************************************************************/
3373
3627
  function ac_transport_state(gobj, event, kw, src)
3374
3628
  {
@@ -3408,6 +3662,25 @@ function ac_refresh(gobj, event, kw, src)
3408
3662
  return request_model(gobj, false);
3409
3663
  }
3410
3664
 
3665
+ /***************************************************************
3666
+ * EV_REFRESH while writes are in flight: the view's own Refresh
3667
+ * is busy then, so it is a host's (after a Save, say).
3668
+ *
3669
+ * Run when the writes END (end_writes()), not now. Run now, the
3670
+ * load replaced ST_SAVING and what was still queued was never
3671
+ * sent, with no word: nothing but the end of what is in flight
3672
+ * moves this view out of ST_LOADING or ST_SAVING.
3673
+ ***************************************************************/
3674
+ function ac_refresh_after_the_writes(gobj, event, kw, src)
3675
+ {
3676
+ let priv = gobj.priv;
3677
+
3678
+ log_warning(`${gobj_short_name(gobj)}: ${event} while writes are in flight: ` +
3679
+ `the schemas are read again when the writes end`);
3680
+ priv.refresh_after_write = true;
3681
+ return 0;
3682
+ }
3683
+
3411
3684
  /***************************************************************
3412
3685
  * What a DIALOG sends while the model is being read again: the
3413
3686
  * Save of a form left open when the reload started, a
@@ -3417,12 +3690,11 @@ function ac_refresh(gobj, event, kw, src)
3417
3690
  * a model the answers in the air are about to replace.
3418
3691
  *
3419
3692
  * Refused, and SAID. Undeclared, it answered "Event NOT DEFINED
3420
- * in state ST_LOADING" and the edit was gone with no word (third
3421
- * independent review).
3693
+ * in state ST_LOADING" and the edit was gone with no word.
3422
3694
  *
3423
3695
  * And NOT "try again": the same Save, once the load was in, sent
3424
3696
  * the form built on the old model -- every field of the old record
3425
- * over the newer one (fourth independent review). The load that
3697
+ * over the newer one. The load that
3426
3698
  * lands closes such a dialog (end_load()); one that fails leaves it
3427
3699
  * up, on the model it was built on.
3428
3700
  ***************************************************************/
@@ -3439,8 +3711,7 @@ function ac_wait_for_the_load(gobj, event, kw, src)
3439
3711
  * reload landed on zero treedbs while it was up) or ST_IDLE (no
3440
3712
  * model). A confirmation is a shell modal, not this view's dialog,
3441
3713
  * so the load that lands does not close it, and its Yes arrives
3442
- * anyway. Undeclared, it answered "Event NOT DEFINED" (fifth
3443
- * independent review).
3714
+ * anyway. Undeclared, it answered "Event NOT DEFINED".
3444
3715
  *
3445
3716
  * Refused with the stamp's own words: whatever it was decided on
3446
3717
  * is not on screen any more.
@@ -3461,13 +3732,12 @@ function ac_confirmed_on_nothing(gobj, event, kw, src)
3461
3732
  * topics hold a draft in __system__ that is not saved (C_TREEDB's
3462
3733
  * saved-schema, `draft_changed`). The mark of a write lived in this
3463
3734
  * session's memory only, so a reload of the page, a reconnect or a
3464
- * refresh showed no draft while __system__ still differed (N13 of
3465
- * the 2026-09-22 review).
3735
+ * refresh showed no draft while __system__ still differed.
3466
3736
  *
3467
3737
  * REPLACES what the host said before -- the host is the truth after
3468
3738
  * a reload, and a Save is exactly the host saying "none any more".
3469
3739
  * It only ever added, and a topic stayed a draft after the Save that
3470
- * published it (M1 of the 2026-09-23 review). What this session wrote
3740
+ * published it. What this session wrote
3471
3741
  * stays marked: it is kept apart (see topic_is_draft()).
3472
3742
  ***************************************************************/
3473
3743
  function ac_drafts(gobj, event, kw, src)
@@ -3552,7 +3822,7 @@ function ac_show_orphans(gobj, event, kw, src)
3552
3822
  ***************************************************************/
3553
3823
  function confirm_then(gobj, message, detail, kw)
3554
3824
  {
3555
- kw = Object.assign({model_gen: gobj.priv.model_gen}, kw);
3825
+ kw = Object.assign({model_gen: gobj.priv.model_gen, seg: position_seg(gobj)}, kw);
3556
3826
  /* The answers are i18n KEYS: shell_modals names each button by its
3557
3827
  * label, and its defaults ("Delete", "Cancel") are not lower-case, so
3558
3828
  * no validated locale holds them -- they read English in every
@@ -3653,7 +3923,8 @@ function ac_save_column(gobj, event, kw, src)
3653
3923
  if(refuse_if_readonly(gobj, event)) {
3654
3924
  return -1;
3655
3925
  }
3656
- if(reload_first(gobj, event)) {
3926
+ if(reload_first(gobj, event, {form: "column", topic: kw.topic, col: kw.col,
3927
+ creating: !!kw.creating, changed: kw.changed || {}})) {
3657
3928
  return -1;
3658
3929
  }
3659
3930
  if(built_on_a_replaced_model(gobj, event, kw)) {
@@ -3828,7 +4099,8 @@ function ac_save_topic(gobj, event, kw, src)
3828
4099
  if(refuse_if_readonly(gobj, event)) {
3829
4100
  return -1;
3830
4101
  }
3831
- if(reload_first(gobj, event)) {
4102
+ if(reload_first(gobj, event, {form: "topic", topic: kw.topic, col: "",
4103
+ creating: !!kw.creating, changed: kw.changed || {}})) {
3832
4104
  return -1;
3833
4105
  }
3834
4106
  if(built_on_a_replaced_model(gobj, event, kw)) {
@@ -4092,6 +4364,9 @@ function ac_confirmed(gobj, event, kw, src)
4092
4364
  if(built_on_a_replaced_model(gobj, event, kw)) {
4093
4365
  return -1;
4094
4366
  }
4367
+ if(asked_on_another_screen(gobj, event, kw)) {
4368
+ return -1;
4369
+ }
4095
4370
  if(kw.what === "import") {
4096
4371
  return run_import(gobj);
4097
4372
  }
@@ -4239,7 +4514,9 @@ function create_gclass(gclass_name)
4239
4514
  * the host are heard: every other action would be computed
4240
4515
  * against a model the answer still in the air is about to
4241
4516
  * change. */
4242
- ["ST_SAVING", COMMON.slice()]
4517
+ ["ST_SAVING", COMMON.filter((a) => a[0] !== "EV_REFRESH").concat([
4518
+ ["EV_REFRESH", ac_refresh_after_the_writes, null]
4519
+ ])]
4243
4520
  ];
4244
4521
 
4245
4522
  /*---------------------------------------------*