@yuneta/gobj-ui 7.23.191 → 7.23.193

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.
package/README.md CHANGED
@@ -774,6 +774,30 @@ A refused write puts the topic back to what the treedb has. Leaving the typed
774
774
  value on screen is tolerable for a form, which stays open on the values it
775
775
  failed with; a cell edited in place would just look saved.
776
776
 
777
+ **The form stays open until the backend answers** (7.23.193). With
778
+ `form_waits_for_answer` set — `C_YUI_TREEDB_TOPICS` sets it — a Save leaves the
779
+ form open and busy, and `EV_CREATE_RECORD` / `EV_UPDATE_RECORD` carry a
780
+ `form_write` serial. The host echoes it back in `EV_WRITE_DONE` (the form
781
+ closes) or `EV_WRITE_REFUSED` (it stays open on what was typed), and it must
782
+ answer EVERY way the write can end: refused before it left (read-only, no
783
+ session) as well as answered by the backend, or the form waits forever. A host
784
+ that does not answer leaves the attribute off, and the form closes when the
785
+ write is published, as before 7.23.193 — whatever the backend says.
786
+
787
+ ```js
788
+ // A host of C_YUI_TREEDB_TOPIC_WITH_FORM that answers the form's writes
789
+ gobj_create_pure_child(name, "C_YUI_TREEDB_TOPIC_WITH_FORM",
790
+ {topic_name: "users", form_waits_for_answer: true /* ... */}, host);
791
+ // on the backend's answer to the write that carried kw.form_write:
792
+ gobj_send_event(topic_form, ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED",
793
+ {form_write: kw.form_write}, host);
794
+ ```
795
+
796
+ **A delete is resolved by IDENTITY, never by position** (7.23.193). The row's
797
+ `id` crosses the confirmation dialog; a Tabulator position is regenerated by
798
+ every row the node events add or remove while the dialog is open, and it used
799
+ to delete the row that sat there when the person answered.
800
+
777
801
  ### Reading a topic table: filters, columns, CSV
778
802
 
779
803
  `C_YUI_TREEDB_TOPIC_WITH_FORM` had one global search box over the loaded rows.
@@ -24549,7 +24549,8 @@ function process_treedb_descs$1(gobj) {
24549
24549
  desc,
24550
24550
  with_remote_paging: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging"),
24551
24551
  page_size: (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "page_size"),
24552
- with_selection_bar: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_selection_bar")
24552
+ with_selection_bar: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_selection_bar"),
24553
+ form_waits_for_answer: true
24553
24554
  };
24554
24555
  let id = `${(0, _yuneta_gobj_js.gobj_name)(gobj)}?${desc.topic_name}`;
24555
24556
  let gobj_topic_form = (0, _yuneta_gobj_js.gobj_create_service)(id, "C_YUI_TREEDB_TOPIC_WITH_FORM", kw_topic_form, gobj);
@@ -24695,7 +24696,7 @@ function treedb_nodes$1(gobj, treedb_name, topic_name, options, page, purpose) {
24695
24696
  /************************************************************
24696
24697
  * Command to remote service
24697
24698
  ************************************************************/
24698
- function treedb_update_node$1(gobj, treedb_name, topic_name, record, options) {
24699
+ function treedb_update_node$1(gobj, treedb_name, topic_name, record, options, form_write) {
24699
24700
  let command = "update-node";
24700
24701
  let gobj_remote_yuno = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "gobj_remote_yuno");
24701
24702
  let kw = {
@@ -24706,8 +24707,13 @@ function treedb_update_node$1(gobj, treedb_name, topic_name, record, options) {
24706
24707
  options: options || {}
24707
24708
  };
24708
24709
  kw.__md_command__ = { topic_name };
24710
+ if (form_write) kw.__md_command__.form_write = form_write;
24709
24711
  let ret = (0, _yuneta_gobj_js.gobj_command)(gobj_remote_yuno, command, kw, gobj);
24710
- if (ret) (0, _yuneta_gobj_js.log_error)(ret);
24712
+ if (ret) {
24713
+ (0, _yuneta_gobj_js.log_error)(ret);
24714
+ return -1;
24715
+ }
24716
+ return 0;
24711
24717
  }
24712
24718
  /************************************************************
24713
24719
  * Command to remote service
@@ -24970,6 +24976,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
24970
24976
  if (command === "update-node" || command === "create-node") {
24971
24977
  let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
24972
24978
  if (failed_topic) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
24979
+ answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), false);
24973
24980
  }
24974
24981
  }
24975
24982
  return 0;
@@ -25006,6 +25013,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25006
25013
  created: command === "create-node",
25007
25014
  command
25008
25015
  });
25016
+ answer_form_write(gobj, get_gobj_formtable(gobj, (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0)), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), true);
25009
25017
  break;
25010
25018
  case "delete-node": break;
25011
25019
  default: (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)} Command unknown: ${command}`);
@@ -25221,15 +25229,23 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
25221
25229
  * }
25222
25230
  ********************************************/
25223
25231
  function ac_create_record(gobj, event, kw, src) {
25224
- if (refuse_if_readonly$3(gobj, event)) return -1;
25232
+ let form_write = kw && kw.form_write || 0;
25233
+ if (refuse_if_readonly$3(gobj, event)) {
25234
+ answer_form_write(gobj, src, form_write, false);
25235
+ return -1;
25236
+ }
25225
25237
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
25226
25238
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
25227
25239
  let record = kw.record;
25228
- return treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25240
+ if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25229
25241
  list_dict: true,
25230
25242
  create: true,
25231
25243
  autolink: true
25232
- });
25244
+ }, form_write) < 0) {
25245
+ answer_form_write(gobj, src, form_write, false);
25246
+ return -1;
25247
+ }
25248
+ return 0;
25233
25249
  }
25234
25250
  /********************************************
25235
25251
  * A topic table wants its records as json, WITH the metadata of
@@ -25379,14 +25395,37 @@ function ac_update_field(gobj, event, kw, src) {
25379
25395
  * }
25380
25396
  ********************************************/
25381
25397
  function ac_update_record(gobj, event, kw, src) {
25382
- if (refuse_if_readonly$3(gobj, event)) return -1;
25398
+ let form_write = kw && kw.form_write || 0;
25399
+ if (refuse_if_readonly$3(gobj, event)) {
25400
+ answer_form_write(gobj, src, form_write, false);
25401
+ return -1;
25402
+ }
25383
25403
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
25384
25404
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
25385
25405
  let record = kw.record;
25386
- return treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25406
+ if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25387
25407
  list_dict: true,
25388
25408
  autolink: true
25389
- });
25409
+ }, form_write) < 0) {
25410
+ answer_form_write(gobj, src, form_write, false);
25411
+ return -1;
25412
+ }
25413
+ return 0;
25414
+ }
25415
+ /************************************************************
25416
+ * Tell a topic form how its write ended: EV_WRITE_DONE closes
25417
+ * it, EV_WRITE_REFUSED leaves it open on what was typed. Every
25418
+ * way a form's write can end passes here -- refused before it
25419
+ * left (read-only, no session) or answered by the backend -- so
25420
+ * a form never waits for an answer that will not come.
25421
+ ************************************************************/
25422
+ function answer_form_write(gobj, gobj_topic_form, form_write, ok) {
25423
+ if (!form_write) return;
25424
+ if (!gobj_topic_form) {
25425
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no topic form to answer write ${form_write}`);
25426
+ return;
25427
+ }
25428
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED", { form_write }, gobj);
25390
25429
  }
25391
25430
  /********************************************
25392
25431
  * Event from formtable
@@ -25757,6 +25796,46 @@ function yui_selected_rows(tabulator) {
25757
25796
  }
25758
25797
  }
25759
25798
  /***************************************************************
25799
+ * The IDENTITY of some rows: the value of the table's `index`
25800
+ * field (Tabulator's default is `id`). This is what crosses a
25801
+ * confirmation dialog -- never the rows' POSITIONS, which the
25802
+ * table regenerates with every addData / deleteRow while the
25803
+ * dialog is open, so a position names whatever row sits there
25804
+ * when the person answers.
25805
+ ***************************************************************/
25806
+ function yui_row_ids(tabulator, rows) {
25807
+ let field = tabulator && tabulator.options && tabulator.options.index || "id";
25808
+ let ids = [];
25809
+ for (let row of rows || []) {
25810
+ let id = row ? row[field] : void 0;
25811
+ if (id !== void 0 && id !== null && id !== "") ids.push(id);
25812
+ }
25813
+ return ids;
25814
+ }
25815
+ /***************************************************************
25816
+ * The rows that carry these ids NOW -> {rows, missing}.
25817
+ * `rows` are the rows' data; `missing` the ids no row carries
25818
+ * any more. A missing id is never replaced by another row.
25819
+ ***************************************************************/
25820
+ function yui_rows_by_ids(tabulator, ids) {
25821
+ let rows = [];
25822
+ let missing = [];
25823
+ for (let id of ids || []) {
25824
+ let row = null;
25825
+ if (tabulator && typeof tabulator.getRow === "function") try {
25826
+ row = tabulator.getRow(id) || null;
25827
+ } catch (e) {
25828
+ row = null;
25829
+ }
25830
+ if (row) rows.push(row.getData());
25831
+ else missing.push(id);
25832
+ }
25833
+ return {
25834
+ rows,
25835
+ missing
25836
+ };
25837
+ }
25838
+ /***************************************************************
25760
25839
  * Drop the selection (after acting on it, or after a
25761
25840
  * reload that leaves the ticked rows behind).
25762
25841
  ***************************************************************/
@@ -26184,10 +26263,18 @@ function yui_icon_is_defined(name) {
26184
26263
  ***********************************************************************/
26185
26264
  var MAX_DEPTH = 4;
26186
26265
  /***************************************************************
26187
- * An fkey, as `nodes` answers it with `list_dict`.
26266
+ * A REFERENCE to a node: an fkey as `nodes` answers it with
26267
+ * `list_dict` -- `{id, topic_name, hook_name}` -- or a hook's child
26268
+ * as the node events deliver it, `{id, topic_name}`. It carries
26269
+ * nothing but those keys; an object with more is data.
26188
26270
  ***************************************************************/
26271
+ var REF_KEYS = /* @__PURE__ */ new Set([
26272
+ "id",
26273
+ "topic_name",
26274
+ "hook_name"
26275
+ ]);
26189
26276
  function is_fkey_ref(value) {
26190
- return !!value && typeof value === "object" && !Array.isArray(value) && "id" in value && "topic_name" in value && "hook_name" in value;
26277
+ return !!value && typeof value === "object" && !Array.isArray(value) && "id" in value && "topic_name" in value && Object.keys(value).every((k) => REF_KEYS.has(k));
26191
26278
  }
26192
26279
  /***************************************************************
26193
26280
  * A hook read with `hook_size`: the COUNT of its children,
@@ -26447,6 +26534,7 @@ var attrs_table$5 = [
26447
26534
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
26448
26535
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_copy_button", 0, true, "Button toolbar COPY"),
26449
26536
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_paste_button", 0, true, "Button toolbar PASTE"),
26537
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "form_waits_for_answer", 0, false, "The form stays open (busy) after a Save until the host answers EV_WRITE_DONE (close) or EV_WRITE_REFUSED (stay open on what was typed). Off: the form closes when the write is published, whatever the backend says. The host must echo the `form_write` serial of EV_CREATE_RECORD / EV_UPDATE_RECORD; C_YUI_TREEDB_TOPICS does"),
26450
26538
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
26451
26539
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
26452
26540
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
@@ -26622,6 +26710,8 @@ function popup_mount_layer(gobj) {
26622
26710
  function mt_create$5(gobj) {
26623
26711
  gobj.priv.reading_files = 0;
26624
26712
  gobj.priv.read_serial = 0;
26713
+ gobj.priv.awaiting_write = 0;
26714
+ gobj.priv.write_serial = 0;
26625
26715
  let name = (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj));
26626
26716
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "table_id", "table" + name);
26627
26717
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "toolbar_id", "toolbar" + name);
@@ -27140,14 +27230,11 @@ function render_selection_state(gobj) {
27140
27230
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
27141
27231
  let $delete = $container ? $container.querySelector(`.button-delete-record`) : null;
27142
27232
  let $copy = $container ? $container.querySelector(`.button-copy-record`) : null;
27143
- if ($delete && $copy) {
27144
- if (yui_selected_rows((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator")).length > 0 && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable")) {
27145
- $delete.removeAttribute("disabled");
27146
- $copy.removeAttribute("disabled");
27147
- } else {
27148
- $delete.setAttribute("disabled", true);
27149
- $copy.setAttribute("disabled", true);
27150
- }
27233
+ let enable = yui_selected_rows((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator")).length > 0 && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable");
27234
+ for (let $button of [$delete, $copy]) {
27235
+ if (!$button) continue;
27236
+ if (enable) $button.removeAttribute("disabled");
27237
+ else $button.setAttribute("disabled", true);
27151
27238
  }
27152
27239
  render_selection_bar(gobj);
27153
27240
  }
@@ -27455,7 +27542,7 @@ function create_tabulator(gobj) {
27455
27542
  hozAlign,
27456
27543
  formatter: colFormatter
27457
27544
  };
27458
- if (max_col_width > 0) colDef.maxWidth = max_col_width;
27545
+ if (max_col_width > 0) colDef.maxInitialWidth = max_col_width;
27459
27546
  if (with_header_filters) apply_header_filter(colDef, field_desc);
27460
27547
  if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
27461
27548
  if (vertAlign) colDef.vertAlign = vertAlign;
@@ -27493,10 +27580,7 @@ function create_tabulator(gobj) {
27493
27580
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_EDIT_RECORD", { index }, gobj);
27494
27581
  } else if (e.target.closest(".remove")) {
27495
27582
  e.stopPropagation();
27496
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_DELETE_ROWS", {
27497
- index,
27498
- row
27499
- }, gobj);
27583
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_DELETE_ROWS", { row }, gobj);
27500
27584
  }
27501
27585
  }
27502
27586
  });
@@ -28034,6 +28118,7 @@ function form_may_close(gobj) {
28034
28118
  function teardown_form_child(gobj) {
28035
28119
  let priv = gobj.priv;
28036
28120
  priv.reading_files = 0;
28121
+ priv.awaiting_write = 0;
28037
28122
  if (priv.form) {
28038
28123
  if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
28039
28124
  (0, _yuneta_gobj_js.gobj_destroy)(priv.form);
@@ -29043,25 +29128,31 @@ function ac_edition_mode(gobj, event, kw, src) {
29043
29128
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
29044
29129
  let $button_paste_record = $container.querySelector(`.button-paste-record`);
29045
29130
  if (editable) {
29046
- $button_edit_record.classList.add("is-primary");
29047
- $button_new_record.classList.add("is-info");
29048
- $button_delete_record.classList.add("is-danger");
29131
+ if ($button_edit_record) $button_edit_record.classList.add("is-primary");
29132
+ if ($button_new_record) {
29133
+ $button_new_record.classList.add("is-info");
29134
+ $button_new_record.removeAttribute("disabled");
29135
+ }
29136
+ if ($button_delete_record) $button_delete_record.classList.add("is-danger");
29049
29137
  tabulator.showColumn("_operation");
29050
29138
  tabulator.showColumn("_check_box_state_");
29051
- $button_new_record.removeAttribute("disabled");
29052
29139
  if ($button_paste_record) $button_paste_record.removeAttribute("disabled");
29053
29140
  if (yui_selected_rows(tabulator).length) {
29054
- $button_delete_record.removeAttribute("disabled");
29141
+ if ($button_delete_record) $button_delete_record.removeAttribute("disabled");
29055
29142
  if ($button_copy_record) $button_copy_record.removeAttribute("disabled");
29056
29143
  }
29057
29144
  } else {
29058
- $button_edit_record.classList.remove("is-primary");
29059
- $button_new_record.classList.remove("is-info");
29060
- $button_delete_record.classList.remove("is-danger");
29145
+ if ($button_edit_record) $button_edit_record.classList.remove("is-primary");
29146
+ if ($button_new_record) {
29147
+ $button_new_record.classList.remove("is-info");
29148
+ $button_new_record.setAttribute("disabled", true);
29149
+ }
29150
+ if ($button_delete_record) {
29151
+ $button_delete_record.classList.remove("is-danger");
29152
+ $button_delete_record.setAttribute("disabled", true);
29153
+ }
29061
29154
  tabulator.hideColumn("_operation");
29062
29155
  tabulator.hideColumn("_check_box_state_");
29063
- $button_new_record.setAttribute("disabled", true);
29064
- $button_delete_record.setAttribute("disabled", true);
29065
29156
  if ($button_copy_record) $button_copy_record.setAttribute("disabled", true);
29066
29157
  if ($button_paste_record) $button_paste_record.setAttribute("disabled", true);
29067
29158
  }
@@ -29103,7 +29194,12 @@ function ac_new_row(gobj, event, kw, src) {
29103
29194
  * kw {} empty
29104
29195
  * - From the column _operation delete icon inside a row
29105
29196
  * It will delete this one row
29106
- * kw {index:, row:}
29197
+ * kw {row:}
29198
+ *
29199
+ * What crosses the confirmation is the rows' IDENTITY, never their
29200
+ * position: the view applies EV_TREEDB_NODE_* of every writer while the
29201
+ * dialog is open, and a position names whatever row sits there when the
29202
+ * person answers -- with force:true (A6 of the 2026-09-21 review).
29107
29203
  ************************************************************/
29108
29204
  function ac_delete_rows(gobj, event, kw, src) {
29109
29205
  if (refuse_if_readonly$2(gobj, event)) return -1;
@@ -29117,17 +29213,20 @@ function ac_delete_rows(gobj, event, kw, src) {
29117
29213
  });
29118
29214
  return 0;
29119
29215
  }
29120
- confirm_then$1(gobj, build_delete_question(gobj, rows), { what: "delete_selection" });
29216
+ confirm_then$1(gobj, build_delete_question(gobj, rows), {
29217
+ what: "delete_selection",
29218
+ ids: yui_row_ids(tabulator, rows)
29219
+ });
29121
29220
  } else confirm_then$1(gobj, build_delete_question(gobj, kw.row), {
29122
29221
  what: "delete_row",
29123
- index: kw.index
29222
+ ids: yui_row_ids(tabulator, [kw.row])
29124
29223
  });
29125
29224
  return 0;
29126
29225
  }
29127
29226
  /************************************************************
29128
29227
  * {what, ...} -- the person said yes to what confirm_then()
29129
- * asked. The rows are read again HERE and not carried in the
29130
- * kw: the kw is plain json, and a Tabulator row is not.
29228
+ * asked. The rows are found again HERE by the ids the question
29229
+ * named: the kw is plain json, and a Tabulator row is not.
29131
29230
  ************************************************************/
29132
29231
  function ac_confirmed$2(gobj, event, kw, src) {
29133
29232
  let priv = gobj.priv;
@@ -29139,31 +29238,16 @@ function ac_confirmed$2(gobj, event, kw, src) {
29139
29238
  if (refuse_if_readonly$2(gobj, event)) return -1;
29140
29239
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
29141
29240
  let topic_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name");
29142
- if (what === "delete_selection") {
29143
- let rows = yui_selected_rows(tabulator);
29144
- if (!rows.length) {
29145
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} with the selection gone`);
29146
- return -1;
29147
- }
29148
- for (let row of rows) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_DELETE_RECORD", {
29241
+ if (what === "delete_selection" || what === "delete_row") {
29242
+ let found = yui_rows_by_ids(tabulator, Array.isArray(kw.ids) ? kw.ids : []);
29243
+ if (found.missing.length) (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event}: rows gone while confirming the delete, not deleted: ${found.missing.join(", ")}`);
29244
+ if (!found.rows.length) return -1;
29245
+ for (let row of found.rows) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_DELETE_RECORD", {
29149
29246
  topic_name,
29150
29247
  record: row
29151
29248
  });
29152
29249
  return 0;
29153
29250
  }
29154
- if (what === "delete_row") {
29155
- let index = kw.index;
29156
- let row = tabulator && typeof index === "number" ? tabulator.getRowFromPosition(index) : null;
29157
- if (!row) {
29158
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} names no row: ${index}`);
29159
- return -1;
29160
- }
29161
- (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_DELETE_RECORD", {
29162
- topic_name,
29163
- record: row.getData()
29164
- });
29165
- return 0;
29166
- }
29167
29251
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} of nothing this view asked about: ${what}`);
29168
29252
  return -1;
29169
29253
  }
@@ -29221,6 +29305,10 @@ function ac_form_save_record(gobj, event, kw, src) {
29221
29305
  if (answer && answer.result >= 0 && answer.data) picked = answer.data;
29222
29306
  const cols = Object.keys(picked);
29223
29307
  if (cols.length === 0) {
29308
+ if (wait_for_write_answer(gobj)) {
29309
+ publish_treedb_write(gobj, mode, kw, null, priv.awaiting_write);
29310
+ return 0;
29311
+ }
29224
29312
  publish_treedb_write(gobj, mode, kw);
29225
29313
  (0, _yuneta_gobj_js.gobj_post_event)(gobj, "EV_CLOSE_FORM", {}, gobj);
29226
29314
  return 0;
@@ -29291,11 +29379,12 @@ function read_is_current(gobj, kw) {
29291
29379
  /************************************************************
29292
29380
  * The one place a write of this view leaves for the app.
29293
29381
  ************************************************************/
29294
- function publish_treedb_write(gobj, mode, record, jn_files) {
29382
+ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29295
29383
  const kw_write = {
29296
29384
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
29297
29385
  record: strip_read_only_cols(gobj, record)
29298
29386
  };
29387
+ if (form_write) kw_write.form_write = form_write;
29299
29388
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29300
29389
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
29301
29390
  }
@@ -29314,11 +29403,53 @@ function ac_files_read(gobj, event, kw, src) {
29314
29403
  if (!read_is_current(gobj, kw)) return -1;
29315
29404
  gobj.priv.reading_files = 0;
29316
29405
  const { record, __files__ } = yui_files_manifest(kw.picks, kw.record);
29406
+ if (wait_for_write_answer(gobj)) {
29407
+ publish_treedb_write(gobj, kw.mode, record, __files__, gobj.priv.awaiting_write);
29408
+ return 0;
29409
+ }
29317
29410
  publish_treedb_write(gobj, kw.mode, record, __files__);
29318
29411
  close_form_dialog(gobj);
29319
29412
  return 0;
29320
29413
  }
29321
29414
  /************************************************************
29415
+ * Does the form wait for the answer of its write? Then it stays
29416
+ * open and BUSY, and the serial it waits for is taken here.
29417
+ *
29418
+ * A refused write used to close the form all the same and throw
29419
+ * away what was typed, although the README said it stayed open
29420
+ * (M25 of the 2026-09-21 review): the close was posted with the
29421
+ * publish, before the backend had answered anything.
29422
+ ************************************************************/
29423
+ function wait_for_write_answer(gobj) {
29424
+ let priv = gobj.priv;
29425
+ if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "form_waits_for_answer") || !priv.form) return false;
29426
+ priv.awaiting_write = ++priv.write_serial;
29427
+ set_form_busy(gobj, true);
29428
+ return true;
29429
+ }
29430
+ /************************************************************
29431
+ * {form_write} -- the host says the form's write was done
29432
+ ************************************************************/
29433
+ function ac_write_done(gobj, event, kw, src) {
29434
+ let priv = gobj.priv;
29435
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29436
+ priv.awaiting_write = 0;
29437
+ set_form_busy(gobj, false);
29438
+ close_form_dialog(gobj);
29439
+ return 0;
29440
+ }
29441
+ /************************************************************
29442
+ * {form_write} -- the host says the form's write was refused.
29443
+ * The form stays OPEN on what was typed; the host has shown why.
29444
+ ************************************************************/
29445
+ function ac_write_refused(gobj, event, kw, src) {
29446
+ let priv = gobj.priv;
29447
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29448
+ priv.awaiting_write = 0;
29449
+ set_form_busy(gobj, false);
29450
+ return 0;
29451
+ }
29452
+ /************************************************************
29322
29453
  * {reason} -- the browser could not read what was picked
29323
29454
  *
29324
29455
  * The form stays OPEN: the record is still there, and closing it would
@@ -30078,6 +30209,16 @@ function create_gclass$5(gclass_name) {
30078
30209
  ac_close_form,
30079
30210
  null
30080
30211
  ],
30212
+ [
30213
+ "EV_WRITE_DONE",
30214
+ ac_write_done,
30215
+ null
30216
+ ],
30217
+ [
30218
+ "EV_WRITE_REFUSED",
30219
+ ac_write_refused,
30220
+ null
30221
+ ],
30081
30222
  [
30082
30223
  "EV_FILES_READ",
30083
30224
  ac_files_read,
@@ -30273,6 +30414,8 @@ function create_gclass$5(gclass_name) {
30273
30414
  ["EV_EDITION_MODE", 0],
30274
30415
  ["EV_SAVE_RECORD", 0],
30275
30416
  ["EV_CLOSE_FORM", 0],
30417
+ ["EV_WRITE_DONE", 0],
30418
+ ["EV_WRITE_REFUSED", 0],
30276
30419
  ["EV_FILES_READ", 0],
30277
30420
  ["EV_FILES_FAILED", 0],
30278
30421
  ["EV_NEW_ROW", 0],
@@ -30414,6 +30557,38 @@ function plan_graph_saves(live, saved) {
30414
30557
  for (const [topic_name, properties] of Object.entries(live)) if (topic_arrangement_changed(properties, have[topic_name])) plan.push(topic_name);
30415
30558
  return plan;
30416
30559
  }
30560
+ /***************************************************************
30561
+ * An ECHO of one __graphs__ record arrived: the node event a save
30562
+ * of it sends back, from this browser or from another one.
30563
+ *
30564
+ * live {topic_name: properties} what the view shows
30565
+ * saved {topic_name: properties} what the backend has
30566
+ * records every __graphs__ record the view holds, `rec`
30567
+ * already in its place among them
30568
+ * rec the echoed record
30569
+ *
30570
+ * Only rec's TOPIC moves: it becomes the record, as shown and as
30571
+ * saved. The other topics are left alone -- rebuilding their saved
30572
+ * copies here took them from the live objects the view had already
30573
+ * rearranged in place, so their unsaved changes counted as saved.
30574
+ *
30575
+ * `live[topic]` IS the record's own object, as when the view loads:
30576
+ * the view arranges it in place. `saved[topic]` is a copy.
30577
+ ***************************************************************/
30578
+ function apply_graphs_echo(live, saved, records, rec) {
30579
+ if (!(0, _yuneta_gobj_js.is_object)(rec) || !rec.topic) return;
30580
+ if (rec.active && (0, _yuneta_gobj_js.is_object)(rec.properties)) {
30581
+ live[rec.topic] = rec.properties;
30582
+ saved[rec.topic] = (0, _yuneta_gobj_js.json_deep_copy)(rec.properties);
30583
+ return;
30584
+ }
30585
+ if (!(records || []).some((r) => {
30586
+ return r && r !== rec && r.topic === rec.topic && r.active && (0, _yuneta_gobj_js.is_object)(r.properties);
30587
+ })) {
30588
+ delete live[rec.topic];
30589
+ delete saved[rec.topic];
30590
+ }
30591
+ }
30417
30592
  //#endregion
30418
30593
  //#region src/treedb_node_label.js
30419
30594
  /***********************************************************************
@@ -65266,7 +65441,7 @@ function ac_node_updated(gobj, event, kw, src) {
65266
65441
  break;
65267
65442
  }
65268
65443
  if (!found) priv.__graphs__.push(node);
65269
- build_graph_properties(gobj);
65444
+ apply_graphs_echo(priv._graph_properties, priv._saved_graph_properties, priv.__graphs__, node);
65270
65445
  return 0;
65271
65446
  }
65272
65447
  let desc = priv.descs[topic_name];
@@ -65294,8 +65469,9 @@ function ac_node_updated(gobj, event, kw, src) {
65294
65469
  update_local_node(gobj, topic_name, node);
65295
65470
  let on_screen = false;
65296
65471
  try {
65297
- on_screen = !!graph.getNodeData(node_name);
65472
+ on_screen = !!priv.graph.getNodeData(node_name);
65298
65473
  } catch (e) {
65474
+ if (e instanceof ReferenceError) throw e;
65299
65475
  on_screen = false;
65300
65476
  }
65301
65477
  if (on_screen) update_topic_node(gobj, desc, node_name, node);