@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
@@ -3038,9 +3038,8 @@ function maybe_apply_translator($node, opts) {
3038
3038
  * it -- until every handle is closed or has timed out. Handed the
3039
3039
  * first caller's handle and timer, a repeat asking `timeout: 0`
3040
3040
  * was dismissed on the first one's timer, and closing one
3041
- * caller's handle closed a toast another was still showing
3042
- * (third independent review). The operator's ✕ closes it for
3043
- * everybody.
3041
+ * caller's handle closed a toast another was still showing.
3042
+ * The operator's ✕ closes it for everybody.
3044
3043
  ***************************************************************/
3045
3044
  function show_notification(shell, kind, message, opts) {
3046
3045
  let $layer = notification_layer(shell);
@@ -3438,8 +3437,28 @@ function build_dialog(shell, message, buttons, opts) {
3438
3437
  if ($primary) $primary.focus();
3439
3438
  });
3440
3439
  }
3440
+ /***************************************************************
3441
+ * The labels a confirmation shows when the caller names none.
3442
+ *
3443
+ * They are i18n KEYS, lower-case like every key: a button
3444
+ * translates its label with the app's t() -- its text, its title
3445
+ * and its aria-label. Until 7.25.16 they were "OK", "Yes", "No",
3446
+ * "Delete" and "Cancel", keys that no locale carries, so a dialog
3447
+ * that took the defaults read in English in every language.
3448
+ *
3449
+ * Written as `i18n:` entries so the validate-locales script of a
3450
+ * consumer finds them when it scans this module: an app that
3451
+ * mounts these dialogs must define the five keys.
3452
+ ***************************************************************/
3453
+ var DEFAULT_LABEL = Object.freeze({
3454
+ ok: Object.freeze({ i18n: "ok" }),
3455
+ yes: Object.freeze({ i18n: "yes" }),
3456
+ no: Object.freeze({ i18n: "no" }),
3457
+ delete: Object.freeze({ i18n: "delete" }),
3458
+ cancel: Object.freeze({ i18n: "cancel" })
3459
+ });
3441
3460
  function yui_shell_confirm_ok(shell, message, opts) {
3442
- let label = opts && opts.ok_label || "OK";
3461
+ let label = opts && opts.ok_label || DEFAULT_LABEL.ok.i18n;
3443
3462
  if (!opts || !opts.type) opts = Object.assign({}, opts, { type: "success" });
3444
3463
  return build_dialog(shell, message, [{
3445
3464
  label,
@@ -3448,8 +3467,8 @@ function yui_shell_confirm_ok(shell, message, opts) {
3448
3467
  }], opts).then(() => void 0);
3449
3468
  }
3450
3469
  function yui_shell_confirm_yesno(shell, message, opts) {
3451
- let yes_label = opts && opts.yes_label || "Yes";
3452
- let no_label = opts && opts.no_label || "No";
3470
+ let yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes.i18n;
3471
+ let no_label = opts && opts.no_label || DEFAULT_LABEL.no.i18n;
3453
3472
  return build_dialog(shell, message, [{
3454
3473
  label: yes_label,
3455
3474
  value: "yes",
@@ -3474,8 +3493,8 @@ function yui_shell_confirm_yesno(shell, message, opts) {
3474
3493
  * pressed.
3475
3494
  ***************************************************************/
3476
3495
  function yui_shell_confirm_danger(shell, message, opts) {
3477
- let confirm_label = opts && opts.confirm_label || "Delete";
3478
- let cancel_label = opts && opts.cancel_label || "Cancel";
3496
+ let confirm_label = opts && opts.confirm_label || DEFAULT_LABEL.delete.i18n;
3497
+ let cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel.i18n;
3479
3498
  if (!opts || !opts.type) opts = Object.assign({}, opts, { type: "danger" });
3480
3499
  return build_dialog(shell, message, [{
3481
3500
  label: confirm_label,
@@ -3487,9 +3506,9 @@ function yui_shell_confirm_danger(shell, message, opts) {
3487
3506
  }], opts).then((v) => v === "confirm");
3488
3507
  }
3489
3508
  function yui_shell_confirm_yesnocancel(shell, message, opts) {
3490
- let yes_label = opts && opts.yes_label || "Yes";
3491
- let no_label = opts && opts.no_label || "No";
3492
- let cancel_label = opts && opts.cancel_label || "Cancel";
3509
+ let yes_label = opts && opts.yes_label || DEFAULT_LABEL.yes.i18n;
3510
+ let no_label = opts && opts.no_label || DEFAULT_LABEL.no.i18n;
3511
+ let cancel_label = opts && opts.cancel_label || DEFAULT_LABEL.cancel.i18n;
3493
3512
  return build_dialog(shell, message, [
3494
3513
  {
3495
3514
  label: yes_label,
@@ -7425,7 +7444,7 @@ function json_diff_rows(doc_a, doc_b) {
7425
7444
  * A Date -> the value of a `datetime-local` input, in local time,
7426
7445
  * WITH seconds: the input is built with `step: 1`, which is what
7427
7446
  * lets it hold them. Written as "YYYY-MM-DDTHH:mm" it lost them on
7428
- * every save of any field (M26 of the 2026-09-21 review).
7447
+ * every save of any field.
7429
7448
  ************************************************************/
7430
7449
  function date_to_datetime_local(value) {
7431
7450
  if (!(value instanceof Date) || isNaN(value.getTime())) return "";
@@ -23130,8 +23149,7 @@ function yui_shell_show_route_map(shell, opts) {
23130
23149
  * answers, so the host has to answer EVERY way a write ends --
23131
23150
  * and the transport closing under a write in flight, or a
23132
23151
  * command that never left because there was no session, are two
23133
- * of them that nobody answered: the form stayed busy for ever
23134
- * (N8 of the 2026-09-22 review).
23152
+ * of them that nobody answered: the form stayed busy for ever.
23135
23153
  *
23136
23154
  * A write is known by its TOPIC and its `form_write` serial,
23137
23155
  * never by the serial alone: each topic has its own form, and
@@ -25682,8 +25700,8 @@ function ac_update_record(gobj, event, kw, src) {
25682
25700
  /************************************************************
25683
25701
  * A write sent with no session never leaves: C_IEVENT_CLI logs
25684
25702
  * "Not in session" and answers null, as it does when the command
25685
- * went -- so a form waiting for the answer waited for ever (N8 of
25686
- * the 2026-09-22 review). Refused before it is sent, aloud.
25703
+ * went -- so a form waiting for the answer waited for ever. Refused
25704
+ * before it is sent, aloud.
25687
25705
  ************************************************************/
25688
25706
  function refuse_without_session(gobj, topic_name) {
25689
25707
  if (is_connected$1(gobj)) return false;
@@ -26795,9 +26813,8 @@ function delete_impact(desc, records) {
26795
26813
  * that back. So a second busy(true) while already busy must not
26796
26814
  * run at all -- it read the buttons it had just disabled as
26797
26815
  * "disabled on their own", and the busy(false) of a refused write
26798
- * left Save and Cancel dead (N9 of the 2026-09-22 review: a record
26799
- * with a picked file went busy for the read, then busy again for
26800
- * the write). The caller owns `state` ({busy}) for the form's life.
26816
+ * left Save and Cancel dead (a record with a picked file went
26817
+ * busy for the read, then busy again for the write). The caller owns `state` ({busy}) for the form's life.
26801
26818
  *
26802
26819
  * Copyright (c) 2026, ArtGins.
26803
26820
  * All Rights Reserved.
@@ -26835,7 +26852,7 @@ function set_toolbar_busy($container, state, busy) {
26835
26852
  * (only its own `plaintext` formatter escapes), so a record's text
26836
26853
  * returned as a string is parsed as markup: `a<b and c>d` showed
26837
26854
  * as `ad`, and a field holding `<img src=x onerror=...>` ran it in
26838
- * the operator's browser (M31 of the 2026-09-21 review). Record
26855
+ * the operator's browser. Record
26839
26856
  * data goes in as a TEXT NODE, and a cell built around it as DOM,
26840
26857
  * never as an HTML string with the data interpolated.
26841
26858
  *
@@ -29597,7 +29614,7 @@ function ac_new_row(gobj, event, kw, src) {
29597
29614
  * What crosses the confirmation is the rows' IDENTITY, never their
29598
29615
  * position: the view applies EV_TREEDB_NODE_* of every writer while the
29599
29616
  * dialog is open, and a position names whatever row sits there when the
29600
- * person answers -- with force:true (A6 of the 2026-09-21 review).
29617
+ * person answers -- with force:true.
29601
29618
  ************************************************************/
29602
29619
  function ac_delete_rows(gobj, event, kw, src) {
29603
29620
  if (refuse_if_readonly$2(gobj, event)) return -1;
@@ -29808,8 +29825,8 @@ function ac_files_read(gobj, event, kw, src) {
29808
29825
  * open and BUSY, and the serial it waits for is taken here.
29809
29826
  *
29810
29827
  * A refused write used to close the form all the same and throw
29811
- * away what was typed, although the README said it stayed open
29812
- * (M25 of the 2026-09-21 review): the close was posted with the
29828
+ * away what was typed, although the README said it stayed open:
29829
+ * the close was posted with the
29813
29830
  * publish, before the backend had answered anything.
29814
29831
  ************************************************************/
29815
29832
  function wait_for_write_answer(gobj) {
@@ -30981,6 +30998,28 @@ function apply_graphs_echo(live, saved, records, rec) {
30981
30998
  delete saved[rec.topic];
30982
30999
  }
30983
31000
  }
31001
+ /***************************************************************
31002
+ * A write of one topic's arrangement did NOT land: the backend
31003
+ * refused it, the transport refused it on the way out, or there
31004
+ * was no session to send it on.
31005
+ *
31006
+ * saved {topic_name: properties} what the backend has
31007
+ * topic_name the topic of the refused record
31008
+ *
31009
+ * The view records a write as saved BEFORE it leaves, because
31010
+ * nothing answers it when it lands. So a refused one left the view
31011
+ * believing the backend held an arrangement it does not, and the
31012
+ * next Save found nothing to write. What the backend holds for the
31013
+ * topic is not known any more, so it is forgotten: the next Save
31014
+ * writes the topic again (plan_graph_saves()).
31015
+ *
31016
+ * Returns TRUE if there was something to forget.
31017
+ ***************************************************************/
31018
+ function forget_refused_graphs_write(saved, topic_name) {
31019
+ if (!(0, _yuneta_gobj_js.is_object)(saved) || !topic_name || !Object.prototype.hasOwnProperty.call(saved, topic_name)) return false;
31020
+ delete saved[topic_name];
31021
+ return true;
31022
+ }
30984
31023
  //#endregion
30985
31024
  //#region src/treedb_node_label.js
30986
31025
  /***********************************************************************
@@ -59213,6 +59252,7 @@ var PRIVATE_DATA$4 = {
59213
59252
  __graphs__: [],
59214
59253
  _graph_properties: {},
59215
59254
  _saved_graph_properties: {},
59255
+ _graphs_writes_owed: {},
59216
59256
  yet_showed: false,
59217
59257
  edit_mode: false,
59218
59258
  operation_mode: null,
@@ -60908,6 +60948,7 @@ function save_topic_graph_properties(gobj, topic_name) {
60908
60948
  if (!topic_arrangement_changed(properties, priv._saved_graph_properties[topic_name])) return;
60909
60949
  properties.__origin__ = (0, _yuneta_gobj_js.gobj_read_str_attr)((0, _yuneta_gobj_js.gobj_yuno)(), "node_uuid");
60910
60950
  priv._saved_graph_properties[topic_name] = (0, _yuneta_gobj_js.json_deep_copy)(properties);
60951
+ delete priv._graphs_writes_owed[topic_name];
60911
60952
  let kw_update = {
60912
60953
  treedb_name: priv.treedb_name,
60913
60954
  topic_name: "__graphs__",
@@ -60989,8 +61030,13 @@ function update_history_buttons(gobj) {
60989
61030
  } else {
60990
61031
  disableElements($container, ".EV_HISTORY_UNDO");
60991
61032
  set_active_state($container, ".EV_HISTORY_UNDO", false);
60992
- disableElements($container, ".EV_SAVE_GRAPH");
60993
- set_submit_state($container, ".EV_SAVE_GRAPH", false);
61033
+ if (graphs_write_is_owed(gobj)) {
61034
+ enableElements($container, ".EV_SAVE_GRAPH");
61035
+ set_submit_state($container, ".EV_SAVE_GRAPH", true);
61036
+ } else {
61037
+ disableElements($container, ".EV_SAVE_GRAPH");
61038
+ set_submit_state($container, ".EV_SAVE_GRAPH", false);
61039
+ }
60994
61040
  }
60995
61041
  } else {
60996
61042
  disableElements($container, ".EV_HISTORY_REDO");
@@ -61947,6 +61993,15 @@ function run_preview_undo(gobj, key) {
61947
61993
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot undo the preview: ${e}`);
61948
61994
  }
61949
61995
  }
61996
+ /************************************************************
61997
+ * Is a `__graphs__` write owed? The host said one did not land
61998
+ * (EV_GRAPHS_WRITE_REFUSED), and no Save has written that topic
61999
+ * since. The Save button stays lit while it is: the history
62000
+ * knows nothing of it, so `canUndo()` alone would put it out.
62001
+ ************************************************************/
62002
+ function graphs_write_is_owed(gobj) {
62003
+ return Object.keys(gobj.priv._graphs_writes_owed).length > 0;
62004
+ }
61950
62005
  function mark_graph_dirty(gobj) {
61951
62006
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
61952
62007
  enableElements($container, ".EV_SAVE_GRAPH");
@@ -65687,6 +65742,7 @@ function ac_clear_data(gobj, event, kw, src) {
65687
65742
  deselect_node(gobj);
65688
65743
  hide_create_popover(gobj);
65689
65744
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "records", {});
65745
+ priv._graphs_writes_owed = {};
65690
65746
  priv._nodes_total = 0;
65691
65747
  priv._nodes_placed = 0;
65692
65748
  priv._fold_model = null;
@@ -65779,15 +65835,36 @@ function ac_save_graph(gobj, event, kw, src) {
65779
65835
  disableElements($container, ".EV_SAVE_GRAPH");
65780
65836
  set_submit_state($container, ".EV_SAVE_GRAPH", false);
65781
65837
  let history = graph_get_plugin(gobj, "history");
65782
- if (history) {
65783
- history.clear();
65784
- update_history_buttons(gobj);
65785
- }
65838
+ if (history) history.clear();
65786
65839
  save_geometry(gobj);
65840
+ update_history_buttons(gobj);
65787
65841
  }
65788
65842
  return 0;
65789
65843
  }
65790
65844
  /************************************************************
65845
+ * The host says a write of `__graphs__` did not land: the
65846
+ * backend refused it, the transport refused it on the way out,
65847
+ * or there was no session to send it on. kw: {topic}
65848
+ *
65849
+ * save_topic_graph_properties() recorded it as saved before it
65850
+ * left. Forgotten now, and the Save is lit again: the next Save
65851
+ * writes the topic again. Before, the view believed the backend
65852
+ * held the arrangement, and the next Save had nothing to write.
65853
+ ************************************************************/
65854
+ function ac_graphs_write_refused(gobj, event, kw, src) {
65855
+ let priv = gobj.priv;
65856
+ let topic_name = kw && kw.topic;
65857
+ if (!topic_name) {
65858
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} names no topic`);
65859
+ return -1;
65860
+ }
65861
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the arrangement of '${topic_name}' was not saved: the next Save writes it again`);
65862
+ forget_refused_graphs_write(priv._saved_graph_properties, topic_name);
65863
+ priv._graphs_writes_owed[topic_name] = true;
65864
+ if (priv.edit_mode) mark_graph_dirty(gobj);
65865
+ return 0;
65866
+ }
65867
+ /************************************************************
65791
65868
  * Node created, from subscription
65792
65869
  ************************************************************/
65793
65870
  function ac_node_created(gobj, event, kw, src) {
@@ -66855,6 +66932,11 @@ function create_gclass$4(gclass_name) {
66855
66932
  ac_node_deleted,
66856
66933
  null
66857
66934
  ],
66935
+ [
66936
+ "EV_GRAPHS_WRITE_REFUSED",
66937
+ ac_graphs_write_refused,
66938
+ null
66939
+ ],
66858
66940
  [
66859
66941
  "EV_NODE_CLICK",
66860
66942
  ac_node_click$2,
@@ -67108,6 +67190,7 @@ function create_gclass$4(gclass_name) {
67108
67190
  ["EV_NODE_CREATED", 0],
67109
67191
  ["EV_NODE_UPDATED", 0],
67110
67192
  ["EV_NODE_DELETED", 0],
67193
+ ["EV_GRAPHS_WRITE_REFUSED", 0],
67111
67194
  ["EV_NODE_CLICK", 0],
67112
67195
  ["EV_EDGE_CLICK", 0],
67113
67196
  ["EV_NODE_CONTEXT_MENU", 0],
@@ -68284,7 +68367,7 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options) {
68284
68367
  let priv = gobj.priv;
68285
68368
  if (!priv.gobj_remote_yuno) {
68286
68369
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: No gobj_remote_yuno defined`);
68287
- return;
68370
+ return -1;
68288
68371
  }
68289
68372
  let command = "update-node";
68290
68373
  let kw = {
@@ -68296,7 +68379,11 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options) {
68296
68379
  };
68297
68380
  kw.__md_command__ = { topic_name };
68298
68381
  let ret = (0, _yuneta_gobj_js.gobj_command)(priv.gobj_remote_yuno, command, kw, gobj);
68299
- if (ret) (0, _yuneta_gobj_js.log_error)(ret);
68382
+ if (ret) {
68383
+ (0, _yuneta_gobj_js.log_error)(ret);
68384
+ return -1;
68385
+ }
68386
+ return 0;
68300
68387
  }
68301
68388
  /************************************************************
68302
68389
  * Command to remote service
@@ -68569,6 +68656,7 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
68569
68656
  case "delete-node":
68570
68657
  case "link-nodes":
68571
68658
  case "unlink-nodes":
68659
+ if (result < 0 && command === "update-node") graphs_write_refused(gobj, (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0), (0, _yuneta_gobj_js.kw_get_dict)(gobj, kw_command, "record", {}, 0));
68572
68660
  if (result >= 0 && (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_RECORD_WRITTEN", {
68573
68661
  treedb_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "treedb_name", "", 0),
68574
68662
  topic_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0),
@@ -68812,12 +68900,40 @@ function ac_create_node(gobj, event, kw, src) {
68812
68900
  * Send to backend
68813
68901
  ********************************************/
68814
68902
  function ac_update_node(gobj, event, kw, src) {
68815
- let priv = gobj.priv;
68816
- if (refuse_if_readonly$1(gobj, event)) return -1;
68817
- let treedb_name = priv.treedb_name;
68903
+ let treedb_name = gobj.priv.treedb_name;
68818
68904
  let topic_name = kw.topic_name;
68819
68905
  let record = kw.record;
68820
- return treedb_update_node(gobj, treedb_name, topic_name, record, kw.options || {});
68906
+ let options = kw.options || {};
68907
+ if (refuse_if_readonly$1(gobj, event)) {
68908
+ graphs_write_refused(gobj, topic_name, record);
68909
+ return -1;
68910
+ }
68911
+ if (topic_name === "__graphs__" && !is_connected(gobj)) {
68912
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no session, the arrangement was not sent`);
68913
+ graphs_write_refused(gobj, topic_name, record);
68914
+ return -1;
68915
+ }
68916
+ let ret = treedb_update_node(gobj, treedb_name, topic_name, record, options);
68917
+ if (ret < 0) graphs_write_refused(gobj, topic_name, record);
68918
+ return ret;
68919
+ }
68920
+ /************************************************************
68921
+ * A write of `__graphs__` did not land. The engine
68922
+ * (C_G6_NODES_TREE) took it for granted when it left -- nothing
68923
+ * answers it when it lands -- so it is told, and its next Save
68924
+ * writes the topic again. Other topics are the operator's data:
68925
+ * the answer's error says what happened to them.
68926
+ ************************************************************/
68927
+ function graphs_write_refused(gobj, topic_name, record) {
68928
+ let priv = gobj.priv;
68929
+ if (topic_name !== "__graphs__") return;
68930
+ let topic = record && typeof record === "object" ? record.topic || record.id : "";
68931
+ if (!topic) {
68932
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a refused __graphs__ write names no topic`);
68933
+ return;
68934
+ }
68935
+ if (!priv.gobj_nodes_tree) return;
68936
+ (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_GRAPHS_WRITE_REFUSED", { topic }, gobj);
68821
68937
  }
68822
68938
  /********************************************
68823
68939
  * Message from G6_nodes_tree
@@ -70473,9 +70589,8 @@ function register_c_yui_treedb_schema() {
70473
70589
  * it, and that mark lived in the memory of the session only:
70474
70590
  * a reload of the page, a reconnect or a refresh of the model
70475
70591
  * lost the chip, the banner and the export warning while
70476
- * __system__ still differed from the file in use (N13 of the
70477
- * 2026-09-22 review). The host reads that difference from
70478
- * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70592
+ * __system__ still differed from the file in use. The host
70593
+ * reads that difference from C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70479
70594
  * hands it here; the editor applies it to its model, in place
70480
70595
  * of what the host said before.
70481
70596
  *
@@ -72143,11 +72258,10 @@ function write_options(op) {
72143
72258
  * AN EDIT IS A DRAFT. A write here moves no version and reaches no
72144
72259
  * treedb: the host SAVES it (C_TREEDB's save-schema raises the
72145
72260
  * topic_version of what changed and the schema_version, once) and
72146
- * APPLIES it (apply-schema + a restart of the owning yuno). Until
72147
- * the owner's design of M36 (2026-09-21 review) every write here
72148
- * raised both versions, so an edit half made was already the
72149
- * schema of the next start. The topic list marks what this session
72150
- * wrote and has not saved (EV_REFRESH forgets it: the view's own
72261
+ * APPLIES it (apply-schema + a restart of the owning yuno). If a
72262
+ * write here raised both versions, an edit half made would already
72263
+ * be the schema of the next start. The topic list marks what this
72264
+ * session wrote and has not saved (EV_REFRESH forgets it: the view's own
72151
72265
  * Refresh button sends it, and a host may after a save -- the
72152
72266
  * agent console's does not) and what the host says is not saved
72153
72267
  * (EV_DRAFTS, replaced whole each time it is sent).
@@ -72213,6 +72327,7 @@ var HOOK_TOPICS = "topics";
72213
72327
  var HOOK_COLS = "cols";
72214
72328
  var DIAGRAM_SEG = "diagram";
72215
72329
  var PURPOSE = "schema_editor";
72330
+ var DIALOGS_THAT_READ_NOTHING = ["SCHEMA_EXPORT_DIALOG", "SCHEMA_REPORT_DIALOG"];
72216
72331
  /***************************************************************
72217
72332
  * Attrs
72218
72333
  ***************************************************************/
@@ -72245,6 +72360,7 @@ var PRIVATE_DATA$1 = {
72245
72360
  diagram_gobj: null,
72246
72361
  dialog: null,
72247
72362
  dialog_gen: 0,
72363
+ dialog_kind: "",
72248
72364
  save_queue: null,
72249
72365
  save_return: "",
72250
72366
  save_wrote: false,
@@ -72254,6 +72370,8 @@ var PRIVATE_DATA$1 = {
72254
72370
  dropped_round: 0,
72255
72371
  dropped_write: 0,
72256
72372
  reload_after_write: false,
72373
+ refresh_after_write: false,
72374
+ reopen_form: null,
72257
72375
  pending_seg: null,
72258
72376
  import_plan: null,
72259
72377
  import_pane: null,
@@ -72413,9 +72531,9 @@ function refuse_if_readonly(gobj, event) {
72413
72531
  * dumped by the machine trace).
72414
72532
  *
72415
72533
  * Refused, and said. A column form sends every field it shows, so
72416
- * its Save after a reload wrote the OLD record over the newer one
72417
- * (fourth independent review, probe A3); a form whose column the
72418
- * reload removed answered "names no column" for ever. end_load()
72534
+ * its Save after a reload wrote the OLD record over the newer one;
72535
+ * a form whose column the reload removed answered "names no column"
72536
+ * for ever. end_load()
72419
72537
  * closes such a dialog when the load lands; this is the same rule
72420
72538
  * for whatever arrives anyway -- a stale screen, a confirmation
72421
72539
  * that was up, a keyboard.
@@ -72435,8 +72553,7 @@ function built_on_a_replaced_model(gobj, event, kw) {
72435
72553
  * the model shown is kept, and it may be older than the store.
72436
72554
  * The session is up, so no reconnect will ask for it again -- it
72437
72555
  * was owed for as long as the session lasted, and a form opened
72438
- * on that model wrote every field of it over the newer record
72439
- * (fifth independent review; A3 of the fourth, by another path).
72556
+ * on that model wrote every field of it over the newer record.
72440
72557
  *
72441
72558
  * So the operator's NEXT action asks it. An edit -- a form, a
72442
72559
  * write, a confirmation, what reads the model whole (check,
@@ -72460,14 +72577,61 @@ function owes_reload(gobj) {
72460
72577
  let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
72461
72578
  return gobj.priv.reload_on_open && transport_in_session(gobj) && state !== "ST_LOADING" && state !== "ST_SAVING";
72462
72579
  }
72463
- function reload_first(gobj, event) {
72580
+ function reload_first(gobj, event, reopen) {
72581
+ let priv = gobj.priv;
72464
72582
  if (!owes_reload(gobj)) return false;
72465
- gobj.priv.reload_on_open = false;
72583
+ priv.reload_on_open = false;
72584
+ if (reopen && priv.dialog) {
72585
+ priv.reopen_form = Object.assign((0, _yuneta_gobj_js.json_deep_copy)(reopen), {
72586
+ dialog: priv.dialog,
72587
+ treedb_id: priv.treedb_id
72588
+ });
72589
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} refused, the schemas shown may be out of date: they are read again first, and the form opens again on them with its changes`);
72590
+ yui_shell_show_error(yui_shell_of(gobj), "the schemas shown may be out of date: they are read again, and the form opens again on them with your changes", { t: i18next.t });
72591
+ request_model(gobj, true);
72592
+ return true;
72593
+ }
72466
72594
  (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} refused, the schemas shown may be out of date: they are read again first`);
72467
72595
  yui_shell_show_error(yui_shell_of(gobj), "the schemas shown may be out of date: they are read again, try again when they are in", { t: i18next.t });
72468
72596
  request_model(gobj, true);
72469
72597
  return true;
72470
72598
  }
72599
+ /***************************************************************
72600
+ * The form reload_first() took down, opened again on the model
72601
+ * the load just made, with the operator's changes on top.
72602
+ *
72603
+ * Only where it can mean the same thing: on the treedb it was
72604
+ * opened on, on the screen whose Save it has, and on a record
72605
+ * that is still there. Otherwise it is not opened, and that is
72606
+ * said -- a form opened on another treedb, or on nothing, would
72607
+ * save somewhere the operator never meant.
72608
+ ***************************************************************/
72609
+ function reopen_form(gobj, r) {
72610
+ let priv = gobj.priv;
72611
+ let state = (0, _yuneta_gobj_js.gobj_current_state)(gobj);
72612
+ let treedb = current_treedb(gobj);
72613
+ let topic = null;
72614
+ let col = null;
72615
+ let fits = false;
72616
+ if (treedb && r.treedb_id === priv.treedb_id) {
72617
+ if (r.form === "column" && state === "ST_COLUMNS") {
72618
+ topic = find_topic(priv.model, priv.treedb_id, r.topic);
72619
+ col = topic && !r.creating ? find_col(priv.model, priv.treedb_id, r.topic, r.col) : null;
72620
+ fits = !!topic && (r.creating || !!col);
72621
+ } else if (r.form === "topic" && state === "ST_TOPICS") {
72622
+ topic = r.creating ? null : find_topic(priv.model, priv.treedb_id, r.topic);
72623
+ fits = r.creating || !!topic;
72624
+ }
72625
+ }
72626
+ if (!fits) {
72627
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the ${r.form} form was not opened again: what it edited is not on the schemas the reload read (${r.treedb_id} ${r.topic}${r.col ? "." + r.col : ""}, now ${state})`);
72628
+ yui_shell_show_error(yui_shell_of(gobj), "the schemas were read again and what the form was editing is not there any more", { t: i18next.t });
72629
+ return -1;
72630
+ }
72631
+ if (r.form === "column") open_column_form(gobj, topic, col, null, r.changed);
72632
+ else open_topic_form(gobj, treedb, topic, r.changed);
72633
+ return 0;
72634
+ }
72471
72635
  function reload_after_move(gobj) {
72472
72636
  if (!owes_reload(gobj)) return 0;
72473
72637
  gobj.priv.reload_on_open = false;
@@ -72486,7 +72650,7 @@ function reload_after_move(gobj) {
72486
72650
  * none of the actions of a screen, so the screen it replaces goes
72487
72651
  * and the toolbar stops answering: left up, a click on a card, on
72488
72652
  * Back or on the drawing answered "Event NOT DEFINED in state
72489
- * ST_LOADING" and was lost (third independent review). A dialog
72653
+ * ST_LOADING" and was lost. A dialog
72490
72654
  * left open is answered in ST_LOADING instead (ac_wait_for_the_load),
72491
72655
  * and a load that LANDS closes it (end_load()): it was built on the
72492
72656
  * model the load replaced, and its Save sends every field it shows.
@@ -72613,7 +72777,7 @@ function build_model(gobj) {
72613
72777
  * measurement of "changed since" can be measured against.
72614
72778
  *
72615
72779
  * `written` is forgotten only by an EV_REFRESH (the view's own
72616
- * Refresh button, or a host that sends one). Since M36 a write moves no
72780
+ * Refresh button, or a host that sends one). Since 7.23.196 a write moves no
72617
72781
  * version, so `written` is the draft chip of what this session
72618
72782
  * wrote and no longer half of a version measurement: a reload of
72619
72783
  * the reconnect, or of a write that turned out done, forgot the
@@ -73706,6 +73870,7 @@ function close_dialog(gobj) {
73706
73870
  if (!priv.dialog) return;
73707
73871
  let dialog = priv.dialog;
73708
73872
  priv.dialog = null;
73873
+ priv.dialog_kind = "";
73709
73874
  priv.export_view = null;
73710
73875
  try {
73711
73876
  dialog.close();
@@ -73717,6 +73882,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
73717
73882
  let priv = gobj.priv;
73718
73883
  close_dialog(gobj);
73719
73884
  priv.dialog_gen = priv.model_gen;
73885
+ priv.dialog_kind = logical;
73720
73886
  priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
73721
73887
  dialog: true,
73722
73888
  title,
@@ -73725,6 +73891,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix) {
73725
73891
  t: i18next.t,
73726
73892
  on_close: () => {
73727
73893
  priv.dialog = null;
73894
+ priv.dialog_kind = "";
73728
73895
  priv.export_view = null;
73729
73896
  }
73730
73897
  });
@@ -73888,6 +74055,55 @@ function read_form($root) {
73888
74055
  return out;
73889
74056
  }
73890
74057
  /***************************************************************
74058
+ * A column form as its Save sends it: the fields and the flags.
74059
+ ***************************************************************/
74060
+ function column_form_state($form) {
74061
+ let values = read_form($form);
74062
+ values.flag = read_flags($form);
74063
+ return values;
74064
+ }
74065
+ /***************************************************************
74066
+ * The fields the operator CHANGED: those whose value is not the
74067
+ * one the form was opened with.
74068
+ ***************************************************************/
74069
+ function form_changes(initial, values) {
74070
+ let changed = {};
74071
+ for (let name of Object.keys(values)) if (JSON.stringify(values[name]) !== JSON.stringify(initial[name])) changed[name] = values[name];
74072
+ return changed;
74073
+ }
74074
+ /***************************************************************
74075
+ * Put changes back into a form just built (reopen_form()). The
74076
+ * flags are drawn again for the type the changes leave, holding
74077
+ * the flags they name, so none of them is hidden by the type.
74078
+ ***************************************************************/
74079
+ function apply_form_changes(gobj, $form, changes) {
74080
+ for (let name of Object.keys(changes)) {
74081
+ if (name === "flag") continue;
74082
+ let $control = $form.querySelector(`[data-name="${name}"]`);
74083
+ if (!$control) {
74084
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a change of '${name}' has no field in the form opened again: not put back`);
74085
+ continue;
74086
+ }
74087
+ if ($control.type === "checkbox") $control.checked = !!changes[name];
74088
+ else $control.value = changes[name];
74089
+ }
74090
+ let $type = $form.querySelector("[data-name=\"type\"]");
74091
+ if ($type && $form.querySelector(".SCHEMA_FLAGS")) {
74092
+ let flags = Array.isArray(changes.flag) ? changes.flag : read_flags($form);
74093
+ redraw_flags(gobj, $form, $type.value, flags);
74094
+ }
74095
+ }
74096
+ /***************************************************************
74097
+ * Draw the flags of a column form again, for `type`, with
74098
+ * `current` checked.
74099
+ ***************************************************************/
74100
+ function redraw_flags(gobj, $form, type, current) {
74101
+ let $holder = $form.querySelector(".SCHEMA_FLAGS");
74102
+ let $new = (0, _yuneta_gobj_js.createElement2)(flags_field(gobj, type, current));
74103
+ $holder.parentNode.replaceChild($new, $holder);
74104
+ (0, _yuneta_gobj_js.refresh_language)($new, i18next.t);
74105
+ }
74106
+ /***************************************************************
73891
74107
  * The flags of a column, as checkboxes that say what they do.
73892
74108
  *
73893
74109
  * Grouped the way they act, greyed when they are meaningless on
@@ -73961,7 +74177,7 @@ function read_flags($root) {
73961
74177
  * Offering a field that silently does something else is worse
73962
74178
  * than not offering it.
73963
74179
  ***************************************************************/
73964
- function open_column_form(gobj, topic, col, prefill) {
74180
+ function open_column_form(gobj, topic, col, prefill, changes) {
73965
74181
  let priv = gobj.priv;
73966
74182
  let record = prefill || col && col.record || {};
73967
74183
  let creating = !col;
@@ -74065,11 +74281,7 @@ function open_column_form(gobj, topic, col, prefill) {
74065
74281
  ]);
74066
74282
  let $type = $form.querySelector("[data-name=\"type\"]");
74067
74283
  $type.addEventListener("change", () => {
74068
- let $holder = $form.querySelector(".SCHEMA_FLAGS");
74069
- let current = read_flags($form);
74070
- let $new = (0, _yuneta_gobj_js.createElement2)(flags_field(gobj, $type.value, current));
74071
- $holder.parentNode.replaceChild($new, $holder);
74072
- (0, _yuneta_gobj_js.refresh_language)($new, i18next.t);
74284
+ redraw_flags(gobj, $form, $type.value, read_flags($form));
74073
74285
  });
74074
74286
  $form.addEventListener("change", (evt) => {
74075
74287
  let $box = evt.target.closest(".SCHEMA_FLAG_BOX");
@@ -74081,15 +74293,17 @@ function open_column_form(gobj, topic, col, prefill) {
74081
74293
  evt.stopPropagation();
74082
74294
  close_dialog(gobj);
74083
74295
  });
74296
+ let initial = column_form_state($form);
74297
+ if (changes) apply_form_changes(gobj, $form, changes);
74084
74298
  $form.querySelector(".SCHEMA_COL_FORM_SAVE").addEventListener("click", (evt) => {
74085
74299
  evt.stopPropagation();
74086
- let values = read_form($form);
74087
- values.flag = read_flags($form);
74300
+ let values = column_form_state($form);
74088
74301
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SAVE_COLUMN", {
74089
74302
  topic: topic.name,
74090
74303
  col: creating ? "" : col.name,
74091
74304
  creating,
74092
74305
  values,
74306
+ changed: creating ? values : form_changes(initial, values),
74093
74307
  model_gen
74094
74308
  }, gobj);
74095
74309
  });
@@ -74109,7 +74323,7 @@ function stringify_field(value) {
74109
74323
  * actually has: a pkey naming no column is a topic that does
74110
74324
  * not open, and it is the first thing the validator looks for.
74111
74325
  ***************************************************************/
74112
- function open_topic_form(gobj, treedb, topic) {
74326
+ function open_topic_form(gobj, treedb, topic, changes) {
74113
74327
  let record = topic && topic.record || {};
74114
74328
  let creating = !topic;
74115
74329
  let col_names = topic ? topic.cols.map((c) => c.name) : [];
@@ -74204,12 +74418,16 @@ function open_topic_form(gobj, treedb, topic) {
74204
74418
  evt.stopPropagation();
74205
74419
  close_dialog(gobj);
74206
74420
  });
74421
+ let initial = read_form($form);
74422
+ if (changes) apply_form_changes(gobj, $form, changes);
74207
74423
  $form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
74208
74424
  evt.stopPropagation();
74425
+ let values = read_form($form);
74209
74426
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SAVE_TOPIC", {
74210
74427
  topic: creating ? "" : topic.name,
74211
74428
  creating,
74212
- values: read_form($form),
74429
+ values,
74430
+ changed: creating ? values : form_changes(initial, values),
74213
74431
  model_gen
74214
74432
  }, gobj);
74215
74433
  });
@@ -74919,11 +75137,11 @@ function transport_in_session(gobj) {
74919
75137
  * The state is part of the question, and that is only true
74920
75138
  * because NOTHING moves the editor out of ST_LOADING or ST_SAVING
74921
75139
  * but the end of what is in flight: a position the host sends
74922
- * meanwhile waits in `pending_seg` (ac_show()). 7.25.6 let EV_SHOW
74923
- * move a RELOAD out of ST_LOADING, and every answer of it was then
74924
- * "for a load that is over": the records it had emptied stayed
74925
- * empty, and the next write patched them into a model with no
74926
- * treedb at all.
75140
+ * meanwhile waits in `pending_seg` (ac_show()). If EV_SHOW moved a
75141
+ * RELOAD out of ST_LOADING, every answer of it would be "for a load
75142
+ * that is over": the records it had emptied would stay empty, and
75143
+ * the next write would patch them into a model with no treedb at
75144
+ * all.
74927
75145
  ***************************************************************/
74928
75146
  function is_current_load(gobj, md) {
74929
75147
  return (0, _yuneta_gobj_js.gobj_current_state)(gobj) === "ST_LOADING" && (0, _yuneta_gobj_js.kw_get_int)(gobj, md, "round", 0, 0) === gobj.priv.load_round;
@@ -74979,20 +75197,28 @@ function transport_dropped(gobj) {
74979
75197
  function end_load(gobj) {
74980
75198
  let priv = gobj.priv;
74981
75199
  let seg = priv.pending_seg;
75200
+ let reopen = priv.reopen_form;
74982
75201
  priv.pending_seg = null;
75202
+ priv.reopen_form = null;
74983
75203
  if (priv.dialog && priv.dialog_gen !== priv.model_gen) {
74984
- (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a dialog was open on the schemas the load replaced: closed`);
75204
+ if (!reopen || reopen.dialog !== priv.dialog) {
75205
+ reopen = null;
75206
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a dialog was open on the schemas the load replaced: closed`);
75207
+ yui_shell_show_error(yui_shell_of(gobj), "the schemas were read again: open the dialog again", { t: i18next.t });
75208
+ }
74985
75209
  close_dialog(gobj);
74986
- yui_shell_show_error(yui_shell_of(gobj), "the schemas were read again: open the dialog again", { t: i18next.t });
74987
- }
75210
+ } else reopen = null;
75211
+ let ret = 0;
74988
75212
  if (priv.model) {
74989
- if (seg !== null) return apply_seg(gobj, seg);
74990
- return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
75213
+ if (seg !== null) ret = apply_seg(gobj, seg);
75214
+ else ret = go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
75215
+ } else {
75216
+ if (seg !== null) adopt_seg(gobj, seg);
75217
+ (0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_IDLE");
75218
+ render(gobj);
74991
75219
  }
74992
- if (seg !== null) adopt_seg(gobj, seg);
74993
- (0, _yuneta_gobj_js.gobj_change_state)(gobj, "ST_IDLE");
74994
- render(gobj);
74995
- return 0;
75220
+ if (reopen) reopen_form(gobj, reopen);
75221
+ return ret;
74996
75222
  }
74997
75223
  /***************************************************************
74998
75224
  * The queue is done, or gave up. Either way the model is what
@@ -75017,7 +75243,11 @@ function end_writes(gobj, error) {
75017
75243
  render(gobj);
75018
75244
  publish_check(gobj);
75019
75245
  }
75020
- reload_if_owed(gobj);
75246
+ if (priv.refresh_after_write) {
75247
+ priv.refresh_after_write = false;
75248
+ priv.reload_after_write = false;
75249
+ request_model(gobj, false);
75250
+ } else reload_if_owed(gobj);
75021
75251
  return error ? -1 : 0;
75022
75252
  }
75023
75253
  /***************************************************************
@@ -75129,10 +75359,59 @@ function position_seg(gobj) {
75129
75359
  * ...and the same in reverse. A tail naming a treedb or a topic
75130
75360
  * the store does not have lands one level up rather than on an
75131
75361
  * empty screen: a shared link outlives the schema it points at.
75362
+ *
75363
+ * Every position the HOST sends comes through here, at once
75364
+ * (ac_show()) or when what was in flight ends (end_load(),
75365
+ * end_writes()). A move to another position closes the dialog of
75366
+ * the screen the editor left (close_dialog_left_behind()).
75132
75367
  ***************************************************************/
75133
75368
  function apply_seg(gobj, seg) {
75369
+ let before = position_seg(gobj);
75134
75370
  let pos = seg_position(seg);
75135
- return go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
75371
+ let ret = go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
75372
+ if (position_seg(gobj) !== before) close_dialog_left_behind(gobj);
75373
+ return ret;
75374
+ }
75375
+ /***************************************************************
75376
+ * The host moved the editor while a dialog was up. C_YUI_SHELL
75377
+ * keeps the overlays open on a move that changes only the subpath
75378
+ * (a url typed in, a link), so the dialog outlives its screen.
75379
+ *
75380
+ * A form, the import and the orphans send events that only the
75381
+ * screen they were opened from declares, and they name their
75382
+ * topic under the treedb open at the time. Left up on another
75383
+ * screen, their Save answered "Event NOT DEFINED" and the edit was
75384
+ * lost with no word. So the dialog is closed and the operator told.
75385
+ * The import plan goes with its dialog: it was computed for the
75386
+ * treedb the editor left.
75387
+ *
75388
+ * The export and the check stay: they show the texts they were
75389
+ * opened with, and what they send is legal in every state.
75390
+ ***************************************************************/
75391
+ function close_dialog_left_behind(gobj) {
75392
+ let priv = gobj.priv;
75393
+ if (!priv.dialog || DIALOGS_THAT_READ_NOTHING.includes(priv.dialog_kind)) return;
75394
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the host moved the view while the dialog ${priv.dialog_kind} was open: closed`);
75395
+ close_dialog(gobj);
75396
+ forget_import_plan(gobj);
75397
+ yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", { t: i18next.t });
75398
+ }
75399
+ /***************************************************************
75400
+ * A confirmation answered on another screen than the one it was
75401
+ * asked from. A confirmation is a shell modal, so a move sent by
75402
+ * the host does not close it. It names its topic and column, and
75403
+ * the actions look them up under the treedb open NOW: a Yes after
75404
+ * a move to another treedb deleted the topic or column of the same
75405
+ * name THERE.
75406
+ *
75407
+ * Refused, and said. A kw with no `seg` stamp is not a
75408
+ * confirmation's and is not asked.
75409
+ ***************************************************************/
75410
+ function asked_on_another_screen(gobj, event, kw) {
75411
+ if (!kw || kw.seg === void 0 || kw.seg === position_seg(gobj)) return false;
75412
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} refused, it was asked on '${kw.seg}' and the view is on '${position_seg(gobj)}'`);
75413
+ yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", { t: i18next.t });
75414
+ return true;
75136
75415
  }
75137
75416
  function seg_position(seg) {
75138
75417
  let parts = String(seg || "").split("/").filter((x) => x.length > 0);
@@ -75249,7 +75528,9 @@ function ac_mt_command_answer(gobj, event, kw, src) {
75249
75528
  priv.save_return = "";
75250
75529
  priv.save_wrote = false;
75251
75530
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_RECORD_WRITTEN", { treedb_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name") });
75252
- request_model(gobj, true);
75531
+ let refresh = priv.refresh_after_write;
75532
+ priv.refresh_after_write = false;
75533
+ request_model(gobj, !refresh);
75253
75534
  return 0;
75254
75535
  }
75255
75536
  }
@@ -75308,8 +75589,7 @@ function late_write_done(gobj, command, topic_name, data) {
75308
75589
  * The fkey is read with parse_fkey_ref(), because the store answers
75309
75590
  * in list_dict -- `[{topic_name, id, hook_name}]` -- and not in the
75310
75591
  * `topics^<topic id>^cols` strings this view writes. Split as a
75311
- * string, every production answer marked nothing (fourth independent
75312
- * review, probe C).
75592
+ * string, every production answer marked nothing.
75313
75593
  ***************************************************************/
75314
75594
  function mark_written_record(gobj, topic_name, record) {
75315
75595
  let priv = gobj.priv;
@@ -75366,7 +75646,7 @@ function ac_hide(gobj, event, kw, src) {
75366
75646
  *
75367
75647
  * Nothing else ends a load or a write the drop cut: their answers
75368
75648
  * will never come, and waiting in ST_LOADING skipped the reload
75369
- * itself (M-1 of the independent review of 7.25.4).
75649
+ * itself.
75370
75650
  ***************************************************************/
75371
75651
  function ac_transport_state(gobj, event, kw, src) {
75372
75652
  let priv = gobj.priv;
@@ -75397,6 +75677,21 @@ function ac_refresh$1(gobj, event, kw, src) {
75397
75677
  return request_model(gobj, false);
75398
75678
  }
75399
75679
  /***************************************************************
75680
+ * EV_REFRESH while writes are in flight: the view's own Refresh
75681
+ * is busy then, so it is a host's (after a Save, say).
75682
+ *
75683
+ * Run when the writes END (end_writes()), not now. Run now, the
75684
+ * load replaced ST_SAVING and what was still queued was never
75685
+ * sent, with no word: nothing but the end of what is in flight
75686
+ * moves this view out of ST_LOADING or ST_SAVING.
75687
+ ***************************************************************/
75688
+ function ac_refresh_after_the_writes(gobj, event, kw, src) {
75689
+ let priv = gobj.priv;
75690
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} while writes are in flight: the schemas are read again when the writes end`);
75691
+ priv.refresh_after_write = true;
75692
+ return 0;
75693
+ }
75694
+ /***************************************************************
75400
75695
  * What a DIALOG sends while the model is being read again: the
75401
75696
  * Save of a form left open when the reload started, a
75402
75697
  * confirmation, an import. The loading screen took the rest of
@@ -75405,12 +75700,11 @@ function ac_refresh$1(gobj, event, kw, src) {
75405
75700
  * a model the answers in the air are about to replace.
75406
75701
  *
75407
75702
  * Refused, and SAID. Undeclared, it answered "Event NOT DEFINED
75408
- * in state ST_LOADING" and the edit was gone with no word (third
75409
- * independent review).
75703
+ * in state ST_LOADING" and the edit was gone with no word.
75410
75704
  *
75411
75705
  * And NOT "try again": the same Save, once the load was in, sent
75412
75706
  * the form built on the old model -- every field of the old record
75413
- * over the newer one (fourth independent review). The load that
75707
+ * over the newer one. The load that
75414
75708
  * lands closes such a dialog (end_load()); one that fails leaves it
75415
75709
  * up, on the model it was built on.
75416
75710
  ***************************************************************/
@@ -75424,8 +75718,7 @@ function ac_wait_for_the_load(gobj, event, kw, src) {
75424
75718
  * reload landed on zero treedbs while it was up) or ST_IDLE (no
75425
75719
  * model). A confirmation is a shell modal, not this view's dialog,
75426
75720
  * so the load that lands does not close it, and its Yes arrives
75427
- * anyway. Undeclared, it answered "Event NOT DEFINED" (fifth
75428
- * independent review).
75721
+ * anyway. Undeclared, it answered "Event NOT DEFINED".
75429
75722
  *
75430
75723
  * Refused with the stamp's own words: whatever it was decided on
75431
75724
  * is not on screen any more.
@@ -75441,13 +75734,12 @@ function ac_confirmed_on_nothing(gobj, event, kw, src) {
75441
75734
  * topics hold a draft in __system__ that is not saved (C_TREEDB's
75442
75735
  * saved-schema, `draft_changed`). The mark of a write lived in this
75443
75736
  * session's memory only, so a reload of the page, a reconnect or a
75444
- * refresh showed no draft while __system__ still differed (N13 of
75445
- * the 2026-09-22 review).
75737
+ * refresh showed no draft while __system__ still differed.
75446
75738
  *
75447
75739
  * REPLACES what the host said before -- the host is the truth after
75448
75740
  * a reload, and a Save is exactly the host saying "none any more".
75449
75741
  * It only ever added, and a topic stayed a draft after the Save that
75450
- * published it (M1 of the 2026-09-23 review). What this session wrote
75742
+ * published it. What this session wrote
75451
75743
  * stays marked: it is kept apart (see topic_is_draft()).
75452
75744
  ***************************************************************/
75453
75745
  function ac_drafts(gobj, event, kw, src) {
@@ -75504,7 +75796,10 @@ function ac_show_orphans(gobj, event, kw, src) {
75504
75796
  * to feed.
75505
75797
  ***************************************************************/
75506
75798
  function confirm_then(gobj, message, detail, kw) {
75507
- kw = Object.assign({ model_gen: gobj.priv.model_gen }, kw);
75799
+ kw = Object.assign({
75800
+ model_gen: gobj.priv.model_gen,
75801
+ seg: position_seg(gobj)
75802
+ }, kw);
75508
75803
  yui_shell_confirm_danger(yui_shell_of(gobj), message, {
75509
75804
  t: i18next.t,
75510
75805
  detail,
@@ -75571,7 +75866,13 @@ function ac_save_column(gobj, event, kw, src) {
75571
75866
  let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
75572
75867
  let col = kw.creating ? null : find_col(priv.model, priv.treedb_id, kw.topic, kw.col);
75573
75868
  if (refuse_if_readonly(gobj, event)) return -1;
75574
- if (reload_first(gobj, event)) return -1;
75869
+ if (reload_first(gobj, event, {
75870
+ form: "column",
75871
+ topic: kw.topic,
75872
+ col: kw.col,
75873
+ creating: !!kw.creating,
75874
+ changed: kw.changed || {}
75875
+ })) return -1;
75575
75876
  if (built_on_a_replaced_model(gobj, event, kw)) return -1;
75576
75877
  if (!topic || !kw.creating && !col) {
75577
75878
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} names no column: ${kw.topic}.${kw.col}`);
@@ -75699,7 +76000,13 @@ function ac_save_topic(gobj, event, kw, src) {
75699
76000
  let treedb = current_treedb(gobj);
75700
76001
  let topic = kw.creating ? null : find_topic(priv.model, priv.treedb_id, kw.topic);
75701
76002
  if (refuse_if_readonly(gobj, event)) return -1;
75702
- if (reload_first(gobj, event)) return -1;
76003
+ if (reload_first(gobj, event, {
76004
+ form: "topic",
76005
+ topic: kw.topic,
76006
+ col: "",
76007
+ creating: !!kw.creating,
76008
+ changed: kw.changed || {}
76009
+ })) return -1;
75703
76010
  if (built_on_a_replaced_model(gobj, event, kw)) return -1;
75704
76011
  if (!treedb || !kw.creating && !topic) {
75705
76012
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} names no topic: ${kw.topic}`);
@@ -75890,6 +76197,7 @@ function ac_confirmed(gobj, event, kw, src) {
75890
76197
  if (refuse_if_readonly(gobj, event)) return -1;
75891
76198
  if (reload_first(gobj, event)) return -1;
75892
76199
  if (built_on_a_replaced_model(gobj, event, kw)) return -1;
76200
+ if (asked_on_another_screen(gobj, event, kw)) return -1;
75893
76201
  if (kw.what === "import") return run_import(gobj);
75894
76202
  if (kw.what === "topic") {
75895
76203
  let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
@@ -76161,7 +76469,11 @@ function create_gclass$1(gclass_name) {
76161
76469
  null
76162
76470
  ]
76163
76471
  ])],
76164
- ["ST_SAVING", COMMON.slice()]
76472
+ ["ST_SAVING", COMMON.filter((a) => a[0] !== "EV_REFRESH").concat([[
76473
+ "EV_REFRESH",
76474
+ ac_refresh_after_the_writes,
76475
+ null
76476
+ ]])]
76165
76477
  ];
76166
76478
  const event_types = [
76167
76479
  ["EV_MT_COMMAND_ANSWER", _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT],