@yuneta/gobj-ui 7.23.192 → 7.23.194

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,24 @@ 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,
25243
+ create_only: true,
25231
25244
  autolink: true
25232
- });
25245
+ }, form_write) < 0) {
25246
+ answer_form_write(gobj, src, form_write, false);
25247
+ return -1;
25248
+ }
25249
+ return 0;
25233
25250
  }
25234
25251
  /********************************************
25235
25252
  * A topic table wants its records as json, WITH the metadata of
@@ -25379,14 +25396,37 @@ function ac_update_field(gobj, event, kw, src) {
25379
25396
  * }
25380
25397
  ********************************************/
25381
25398
  function ac_update_record(gobj, event, kw, src) {
25382
- if (refuse_if_readonly$3(gobj, event)) return -1;
25399
+ let form_write = kw && kw.form_write || 0;
25400
+ if (refuse_if_readonly$3(gobj, event)) {
25401
+ answer_form_write(gobj, src, form_write, false);
25402
+ return -1;
25403
+ }
25383
25404
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
25384
25405
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
25385
25406
  let record = kw.record;
25386
- return treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25407
+ if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25387
25408
  list_dict: true,
25388
25409
  autolink: true
25389
- });
25410
+ }, form_write) < 0) {
25411
+ answer_form_write(gobj, src, form_write, false);
25412
+ return -1;
25413
+ }
25414
+ return 0;
25415
+ }
25416
+ /************************************************************
25417
+ * Tell a topic form how its write ended: EV_WRITE_DONE closes
25418
+ * it, EV_WRITE_REFUSED leaves it open on what was typed. Every
25419
+ * way a form's write can end passes here -- refused before it
25420
+ * left (read-only, no session) or answered by the backend -- so
25421
+ * a form never waits for an answer that will not come.
25422
+ ************************************************************/
25423
+ function answer_form_write(gobj, gobj_topic_form, form_write, ok) {
25424
+ if (!form_write) return;
25425
+ if (!gobj_topic_form) {
25426
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no topic form to answer write ${form_write}`);
25427
+ return;
25428
+ }
25429
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED", { form_write }, gobj);
25390
25430
  }
25391
25431
  /********************************************
25392
25432
  * Event from formtable
@@ -25757,6 +25797,46 @@ function yui_selected_rows(tabulator) {
25757
25797
  }
25758
25798
  }
25759
25799
  /***************************************************************
25800
+ * The IDENTITY of some rows: the value of the table's `index`
25801
+ * field (Tabulator's default is `id`). This is what crosses a
25802
+ * confirmation dialog -- never the rows' POSITIONS, which the
25803
+ * table regenerates with every addData / deleteRow while the
25804
+ * dialog is open, so a position names whatever row sits there
25805
+ * when the person answers.
25806
+ ***************************************************************/
25807
+ function yui_row_ids(tabulator, rows) {
25808
+ let field = tabulator && tabulator.options && tabulator.options.index || "id";
25809
+ let ids = [];
25810
+ for (let row of rows || []) {
25811
+ let id = row ? row[field] : void 0;
25812
+ if (id !== void 0 && id !== null && id !== "") ids.push(id);
25813
+ }
25814
+ return ids;
25815
+ }
25816
+ /***************************************************************
25817
+ * The rows that carry these ids NOW -> {rows, missing}.
25818
+ * `rows` are the rows' data; `missing` the ids no row carries
25819
+ * any more. A missing id is never replaced by another row.
25820
+ ***************************************************************/
25821
+ function yui_rows_by_ids(tabulator, ids) {
25822
+ let rows = [];
25823
+ let missing = [];
25824
+ for (let id of ids || []) {
25825
+ let row = null;
25826
+ if (tabulator && typeof tabulator.getRow === "function") try {
25827
+ row = tabulator.getRow(id) || null;
25828
+ } catch (e) {
25829
+ row = null;
25830
+ }
25831
+ if (row) rows.push(row.getData());
25832
+ else missing.push(id);
25833
+ }
25834
+ return {
25835
+ rows,
25836
+ missing
25837
+ };
25838
+ }
25839
+ /***************************************************************
25760
25840
  * Drop the selection (after acting on it, or after a
25761
25841
  * reload that leaves the ticked rows behind).
25762
25842
  ***************************************************************/
@@ -26184,10 +26264,18 @@ function yui_icon_is_defined(name) {
26184
26264
  ***********************************************************************/
26185
26265
  var MAX_DEPTH = 4;
26186
26266
  /***************************************************************
26187
- * An fkey, as `nodes` answers it with `list_dict`.
26267
+ * A REFERENCE to a node: an fkey as `nodes` answers it with
26268
+ * `list_dict` -- `{id, topic_name, hook_name}` -- or a hook's child
26269
+ * as the node events deliver it, `{id, topic_name}`. It carries
26270
+ * nothing but those keys; an object with more is data.
26188
26271
  ***************************************************************/
26272
+ var REF_KEYS = /* @__PURE__ */ new Set([
26273
+ "id",
26274
+ "topic_name",
26275
+ "hook_name"
26276
+ ]);
26189
26277
  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;
26278
+ 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
26279
  }
26192
26280
  /***************************************************************
26193
26281
  * A hook read with `hook_size`: the COUNT of its children,
@@ -26447,6 +26535,7 @@ var attrs_table$5 = [
26447
26535
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
26448
26536
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_copy_button", 0, true, "Button toolbar COPY"),
26449
26537
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_paste_button", 0, true, "Button toolbar PASTE"),
26538
+ (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
26539
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
26451
26540
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
26452
26541
  (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 +26711,8 @@ function popup_mount_layer(gobj) {
26622
26711
  function mt_create$5(gobj) {
26623
26712
  gobj.priv.reading_files = 0;
26624
26713
  gobj.priv.read_serial = 0;
26714
+ gobj.priv.awaiting_write = 0;
26715
+ gobj.priv.write_serial = 0;
26625
26716
  let name = (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj));
26626
26717
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "table_id", "table" + name);
26627
26718
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "toolbar_id", "toolbar" + name);
@@ -27140,14 +27231,11 @@ function render_selection_state(gobj) {
27140
27231
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
27141
27232
  let $delete = $container ? $container.querySelector(`.button-delete-record`) : null;
27142
27233
  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
- }
27234
+ 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");
27235
+ for (let $button of [$delete, $copy]) {
27236
+ if (!$button) continue;
27237
+ if (enable) $button.removeAttribute("disabled");
27238
+ else $button.setAttribute("disabled", true);
27151
27239
  }
27152
27240
  render_selection_bar(gobj);
27153
27241
  }
@@ -27455,7 +27543,7 @@ function create_tabulator(gobj) {
27455
27543
  hozAlign,
27456
27544
  formatter: colFormatter
27457
27545
  };
27458
- if (max_col_width > 0) colDef.maxWidth = max_col_width;
27546
+ if (max_col_width > 0) colDef.maxInitialWidth = max_col_width;
27459
27547
  if (with_header_filters) apply_header_filter(colDef, field_desc);
27460
27548
  if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
27461
27549
  if (vertAlign) colDef.vertAlign = vertAlign;
@@ -27493,10 +27581,7 @@ function create_tabulator(gobj) {
27493
27581
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_EDIT_RECORD", { index }, gobj);
27494
27582
  } else if (e.target.closest(".remove")) {
27495
27583
  e.stopPropagation();
27496
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_DELETE_ROWS", {
27497
- index,
27498
- row
27499
- }, gobj);
27584
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_DELETE_ROWS", { row }, gobj);
27500
27585
  }
27501
27586
  }
27502
27587
  });
@@ -28034,6 +28119,7 @@ function form_may_close(gobj) {
28034
28119
  function teardown_form_child(gobj) {
28035
28120
  let priv = gobj.priv;
28036
28121
  priv.reading_files = 0;
28122
+ priv.awaiting_write = 0;
28037
28123
  if (priv.form) {
28038
28124
  if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
28039
28125
  (0, _yuneta_gobj_js.gobj_destroy)(priv.form);
@@ -29043,25 +29129,31 @@ function ac_edition_mode(gobj, event, kw, src) {
29043
29129
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
29044
29130
  let $button_paste_record = $container.querySelector(`.button-paste-record`);
29045
29131
  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");
29132
+ if ($button_edit_record) $button_edit_record.classList.add("is-primary");
29133
+ if ($button_new_record) {
29134
+ $button_new_record.classList.add("is-info");
29135
+ $button_new_record.removeAttribute("disabled");
29136
+ }
29137
+ if ($button_delete_record) $button_delete_record.classList.add("is-danger");
29049
29138
  tabulator.showColumn("_operation");
29050
29139
  tabulator.showColumn("_check_box_state_");
29051
- $button_new_record.removeAttribute("disabled");
29052
29140
  if ($button_paste_record) $button_paste_record.removeAttribute("disabled");
29053
29141
  if (yui_selected_rows(tabulator).length) {
29054
- $button_delete_record.removeAttribute("disabled");
29142
+ if ($button_delete_record) $button_delete_record.removeAttribute("disabled");
29055
29143
  if ($button_copy_record) $button_copy_record.removeAttribute("disabled");
29056
29144
  }
29057
29145
  } 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");
29146
+ if ($button_edit_record) $button_edit_record.classList.remove("is-primary");
29147
+ if ($button_new_record) {
29148
+ $button_new_record.classList.remove("is-info");
29149
+ $button_new_record.setAttribute("disabled", true);
29150
+ }
29151
+ if ($button_delete_record) {
29152
+ $button_delete_record.classList.remove("is-danger");
29153
+ $button_delete_record.setAttribute("disabled", true);
29154
+ }
29061
29155
  tabulator.hideColumn("_operation");
29062
29156
  tabulator.hideColumn("_check_box_state_");
29063
- $button_new_record.setAttribute("disabled", true);
29064
- $button_delete_record.setAttribute("disabled", true);
29065
29157
  if ($button_copy_record) $button_copy_record.setAttribute("disabled", true);
29066
29158
  if ($button_paste_record) $button_paste_record.setAttribute("disabled", true);
29067
29159
  }
@@ -29103,7 +29195,12 @@ function ac_new_row(gobj, event, kw, src) {
29103
29195
  * kw {} empty
29104
29196
  * - From the column _operation delete icon inside a row
29105
29197
  * It will delete this one row
29106
- * kw {index:, row:}
29198
+ * kw {row:}
29199
+ *
29200
+ * What crosses the confirmation is the rows' IDENTITY, never their
29201
+ * position: the view applies EV_TREEDB_NODE_* of every writer while the
29202
+ * dialog is open, and a position names whatever row sits there when the
29203
+ * person answers -- with force:true (A6 of the 2026-09-21 review).
29107
29204
  ************************************************************/
29108
29205
  function ac_delete_rows(gobj, event, kw, src) {
29109
29206
  if (refuse_if_readonly$2(gobj, event)) return -1;
@@ -29117,17 +29214,20 @@ function ac_delete_rows(gobj, event, kw, src) {
29117
29214
  });
29118
29215
  return 0;
29119
29216
  }
29120
- confirm_then$1(gobj, build_delete_question(gobj, rows), { what: "delete_selection" });
29217
+ confirm_then$1(gobj, build_delete_question(gobj, rows), {
29218
+ what: "delete_selection",
29219
+ ids: yui_row_ids(tabulator, rows)
29220
+ });
29121
29221
  } else confirm_then$1(gobj, build_delete_question(gobj, kw.row), {
29122
29222
  what: "delete_row",
29123
- index: kw.index
29223
+ ids: yui_row_ids(tabulator, [kw.row])
29124
29224
  });
29125
29225
  return 0;
29126
29226
  }
29127
29227
  /************************************************************
29128
29228
  * {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.
29229
+ * asked. The rows are found again HERE by the ids the question
29230
+ * named: the kw is plain json, and a Tabulator row is not.
29131
29231
  ************************************************************/
29132
29232
  function ac_confirmed$2(gobj, event, kw, src) {
29133
29233
  let priv = gobj.priv;
@@ -29139,31 +29239,16 @@ function ac_confirmed$2(gobj, event, kw, src) {
29139
29239
  if (refuse_if_readonly$2(gobj, event)) return -1;
29140
29240
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
29141
29241
  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", {
29242
+ if (what === "delete_selection" || what === "delete_row") {
29243
+ let found = yui_rows_by_ids(tabulator, Array.isArray(kw.ids) ? kw.ids : []);
29244
+ 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(", ")}`);
29245
+ if (!found.rows.length) return -1;
29246
+ for (let row of found.rows) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_DELETE_RECORD", {
29149
29247
  topic_name,
29150
29248
  record: row
29151
29249
  });
29152
29250
  return 0;
29153
29251
  }
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
29252
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} of nothing this view asked about: ${what}`);
29168
29253
  return -1;
29169
29254
  }
@@ -29221,6 +29306,10 @@ function ac_form_save_record(gobj, event, kw, src) {
29221
29306
  if (answer && answer.result >= 0 && answer.data) picked = answer.data;
29222
29307
  const cols = Object.keys(picked);
29223
29308
  if (cols.length === 0) {
29309
+ if (wait_for_write_answer(gobj)) {
29310
+ publish_treedb_write(gobj, mode, kw, null, priv.awaiting_write);
29311
+ return 0;
29312
+ }
29224
29313
  publish_treedb_write(gobj, mode, kw);
29225
29314
  (0, _yuneta_gobj_js.gobj_post_event)(gobj, "EV_CLOSE_FORM", {}, gobj);
29226
29315
  return 0;
@@ -29291,11 +29380,12 @@ function read_is_current(gobj, kw) {
29291
29380
  /************************************************************
29292
29381
  * The one place a write of this view leaves for the app.
29293
29382
  ************************************************************/
29294
- function publish_treedb_write(gobj, mode, record, jn_files) {
29383
+ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29295
29384
  const kw_write = {
29296
29385
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
29297
29386
  record: strip_read_only_cols(gobj, record)
29298
29387
  };
29388
+ if (form_write) kw_write.form_write = form_write;
29299
29389
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29300
29390
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
29301
29391
  }
@@ -29314,11 +29404,53 @@ function ac_files_read(gobj, event, kw, src) {
29314
29404
  if (!read_is_current(gobj, kw)) return -1;
29315
29405
  gobj.priv.reading_files = 0;
29316
29406
  const { record, __files__ } = yui_files_manifest(kw.picks, kw.record);
29407
+ if (wait_for_write_answer(gobj)) {
29408
+ publish_treedb_write(gobj, kw.mode, record, __files__, gobj.priv.awaiting_write);
29409
+ return 0;
29410
+ }
29317
29411
  publish_treedb_write(gobj, kw.mode, record, __files__);
29318
29412
  close_form_dialog(gobj);
29319
29413
  return 0;
29320
29414
  }
29321
29415
  /************************************************************
29416
+ * Does the form wait for the answer of its write? Then it stays
29417
+ * open and BUSY, and the serial it waits for is taken here.
29418
+ *
29419
+ * A refused write used to close the form all the same and throw
29420
+ * away what was typed, although the README said it stayed open
29421
+ * (M25 of the 2026-09-21 review): the close was posted with the
29422
+ * publish, before the backend had answered anything.
29423
+ ************************************************************/
29424
+ function wait_for_write_answer(gobj) {
29425
+ let priv = gobj.priv;
29426
+ if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "form_waits_for_answer") || !priv.form) return false;
29427
+ priv.awaiting_write = ++priv.write_serial;
29428
+ set_form_busy(gobj, true);
29429
+ return true;
29430
+ }
29431
+ /************************************************************
29432
+ * {form_write} -- the host says the form's write was done
29433
+ ************************************************************/
29434
+ function ac_write_done(gobj, event, kw, src) {
29435
+ let priv = gobj.priv;
29436
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29437
+ priv.awaiting_write = 0;
29438
+ set_form_busy(gobj, false);
29439
+ close_form_dialog(gobj);
29440
+ return 0;
29441
+ }
29442
+ /************************************************************
29443
+ * {form_write} -- the host says the form's write was refused.
29444
+ * The form stays OPEN on what was typed; the host has shown why.
29445
+ ************************************************************/
29446
+ function ac_write_refused(gobj, event, kw, src) {
29447
+ let priv = gobj.priv;
29448
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29449
+ priv.awaiting_write = 0;
29450
+ set_form_busy(gobj, false);
29451
+ return 0;
29452
+ }
29453
+ /************************************************************
29322
29454
  * {reason} -- the browser could not read what was picked
29323
29455
  *
29324
29456
  * The form stays OPEN: the record is still there, and closing it would
@@ -30078,6 +30210,16 @@ function create_gclass$5(gclass_name) {
30078
30210
  ac_close_form,
30079
30211
  null
30080
30212
  ],
30213
+ [
30214
+ "EV_WRITE_DONE",
30215
+ ac_write_done,
30216
+ null
30217
+ ],
30218
+ [
30219
+ "EV_WRITE_REFUSED",
30220
+ ac_write_refused,
30221
+ null
30222
+ ],
30081
30223
  [
30082
30224
  "EV_FILES_READ",
30083
30225
  ac_files_read,
@@ -30273,6 +30415,8 @@ function create_gclass$5(gclass_name) {
30273
30415
  ["EV_EDITION_MODE", 0],
30274
30416
  ["EV_SAVE_RECORD", 0],
30275
30417
  ["EV_CLOSE_FORM", 0],
30418
+ ["EV_WRITE_DONE", 0],
30419
+ ["EV_WRITE_REFUSED", 0],
30276
30420
  ["EV_FILES_READ", 0],
30277
30421
  ["EV_FILES_FAILED", 0],
30278
30422
  ["EV_NEW_ROW", 0],
@@ -30414,6 +30558,38 @@ function plan_graph_saves(live, saved) {
30414
30558
  for (const [topic_name, properties] of Object.entries(live)) if (topic_arrangement_changed(properties, have[topic_name])) plan.push(topic_name);
30415
30559
  return plan;
30416
30560
  }
30561
+ /***************************************************************
30562
+ * An ECHO of one __graphs__ record arrived: the node event a save
30563
+ * of it sends back, from this browser or from another one.
30564
+ *
30565
+ * live {topic_name: properties} what the view shows
30566
+ * saved {topic_name: properties} what the backend has
30567
+ * records every __graphs__ record the view holds, `rec`
30568
+ * already in its place among them
30569
+ * rec the echoed record
30570
+ *
30571
+ * Only rec's TOPIC moves: it becomes the record, as shown and as
30572
+ * saved. The other topics are left alone -- rebuilding their saved
30573
+ * copies here took them from the live objects the view had already
30574
+ * rearranged in place, so their unsaved changes counted as saved.
30575
+ *
30576
+ * `live[topic]` IS the record's own object, as when the view loads:
30577
+ * the view arranges it in place. `saved[topic]` is a copy.
30578
+ ***************************************************************/
30579
+ function apply_graphs_echo(live, saved, records, rec) {
30580
+ if (!(0, _yuneta_gobj_js.is_object)(rec) || !rec.topic) return;
30581
+ if (rec.active && (0, _yuneta_gobj_js.is_object)(rec.properties)) {
30582
+ live[rec.topic] = rec.properties;
30583
+ saved[rec.topic] = (0, _yuneta_gobj_js.json_deep_copy)(rec.properties);
30584
+ return;
30585
+ }
30586
+ if (!(records || []).some((r) => {
30587
+ return r && r !== rec && r.topic === rec.topic && r.active && (0, _yuneta_gobj_js.is_object)(r.properties);
30588
+ })) {
30589
+ delete live[rec.topic];
30590
+ delete saved[rec.topic];
30591
+ }
30592
+ }
30417
30593
  //#endregion
30418
30594
  //#region src/treedb_node_label.js
30419
30595
  /***********************************************************************
@@ -65266,7 +65442,7 @@ function ac_node_updated(gobj, event, kw, src) {
65266
65442
  break;
65267
65443
  }
65268
65444
  if (!found) priv.__graphs__.push(node);
65269
- build_graph_properties(gobj);
65445
+ apply_graphs_echo(priv._graph_properties, priv._saved_graph_properties, priv.__graphs__, node);
65270
65446
  return 0;
65271
65447
  }
65272
65448
  let desc = priv.descs[topic_name];
@@ -65294,8 +65470,9 @@ function ac_node_updated(gobj, event, kw, src) {
65294
65470
  update_local_node(gobj, topic_name, node);
65295
65471
  let on_screen = false;
65296
65472
  try {
65297
- on_screen = !!graph.getNodeData(node_name);
65473
+ on_screen = !!priv.graph.getNodeData(node_name);
65298
65474
  } catch (e) {
65475
+ if (e instanceof ReferenceError) throw e;
65299
65476
  on_screen = false;
65300
65477
  }
65301
65478
  if (on_screen) update_topic_node(gobj, desc, node_name, node);