@yuneta/gobj-ui 7.23.192 → 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.
@@ -24544,7 +24544,8 @@ function process_treedb_descs$1(gobj) {
24544
24544
  desc,
24545
24545
  with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging"),
24546
24546
  page_size: gobj_read_integer_attr(gobj, "page_size"),
24547
- with_selection_bar: gobj_read_bool_attr(gobj, "with_selection_bar")
24547
+ with_selection_bar: gobj_read_bool_attr(gobj, "with_selection_bar"),
24548
+ form_waits_for_answer: true
24548
24549
  };
24549
24550
  let id = `${gobj_name(gobj)}?${desc.topic_name}`;
24550
24551
  let gobj_topic_form = gobj_create_service(id, "C_YUI_TREEDB_TOPIC_WITH_FORM", kw_topic_form, gobj);
@@ -24690,7 +24691,7 @@ function treedb_nodes$1(gobj, treedb_name, topic_name, options, page, purpose) {
24690
24691
  /************************************************************
24691
24692
  * Command to remote service
24692
24693
  ************************************************************/
24693
- function treedb_update_node$1(gobj, treedb_name, topic_name, record, options) {
24694
+ function treedb_update_node$1(gobj, treedb_name, topic_name, record, options, form_write) {
24694
24695
  let command = "update-node";
24695
24696
  let gobj_remote_yuno = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
24696
24697
  let kw = {
@@ -24701,8 +24702,13 @@ function treedb_update_node$1(gobj, treedb_name, topic_name, record, options) {
24701
24702
  options: options || {}
24702
24703
  };
24703
24704
  kw.__md_command__ = { topic_name };
24705
+ if (form_write) kw.__md_command__.form_write = form_write;
24704
24706
  let ret = gobj_command(gobj_remote_yuno, command, kw, gobj);
24705
- if (ret) log_error(ret);
24707
+ if (ret) {
24708
+ log_error(ret);
24709
+ return -1;
24710
+ }
24711
+ return 0;
24706
24712
  }
24707
24713
  /************************************************************
24708
24714
  * Command to remote service
@@ -24965,6 +24971,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
24965
24971
  if (command === "update-node" || command === "create-node") {
24966
24972
  let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
24967
24973
  if (failed_topic) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
24974
+ answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), kw_get_int(gobj, kw_command, "form_write", 0, 0), false);
24968
24975
  }
24969
24976
  }
24970
24977
  return 0;
@@ -25001,6 +25008,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25001
25008
  created: command === "create-node",
25002
25009
  command
25003
25010
  });
25011
+ answer_form_write(gobj, get_gobj_formtable(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0)), kw_get_int(gobj, kw_command, "form_write", 0, 0), true);
25004
25012
  break;
25005
25013
  case "delete-node": break;
25006
25014
  default: log_error(`${gobj_short_name(gobj)} Command unknown: ${command}`);
@@ -25216,15 +25224,23 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
25216
25224
  * }
25217
25225
  ********************************************/
25218
25226
  function ac_create_record(gobj, event, kw, src) {
25219
- if (refuse_if_readonly$3(gobj, event)) return -1;
25227
+ let form_write = kw && kw.form_write || 0;
25228
+ if (refuse_if_readonly$3(gobj, event)) {
25229
+ answer_form_write(gobj, src, form_write, false);
25230
+ return -1;
25231
+ }
25220
25232
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
25221
25233
  let topic_name = gobj_read_attr(src, "topic_name");
25222
25234
  let record = kw.record;
25223
- return treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25235
+ if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25224
25236
  list_dict: true,
25225
25237
  create: true,
25226
25238
  autolink: true
25227
- });
25239
+ }, form_write) < 0) {
25240
+ answer_form_write(gobj, src, form_write, false);
25241
+ return -1;
25242
+ }
25243
+ return 0;
25228
25244
  }
25229
25245
  /********************************************
25230
25246
  * A topic table wants its records as json, WITH the metadata of
@@ -25374,14 +25390,37 @@ function ac_update_field(gobj, event, kw, src) {
25374
25390
  * }
25375
25391
  ********************************************/
25376
25392
  function ac_update_record(gobj, event, kw, src) {
25377
- if (refuse_if_readonly$3(gobj, event)) return -1;
25393
+ let form_write = kw && kw.form_write || 0;
25394
+ if (refuse_if_readonly$3(gobj, event)) {
25395
+ answer_form_write(gobj, src, form_write, false);
25396
+ return -1;
25397
+ }
25378
25398
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
25379
25399
  let topic_name = gobj_read_attr(src, "topic_name");
25380
25400
  let record = kw.record;
25381
- return treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25401
+ if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25382
25402
  list_dict: true,
25383
25403
  autolink: true
25384
- });
25404
+ }, form_write) < 0) {
25405
+ answer_form_write(gobj, src, form_write, false);
25406
+ return -1;
25407
+ }
25408
+ return 0;
25409
+ }
25410
+ /************************************************************
25411
+ * Tell a topic form how its write ended: EV_WRITE_DONE closes
25412
+ * it, EV_WRITE_REFUSED leaves it open on what was typed. Every
25413
+ * way a form's write can end passes here -- refused before it
25414
+ * left (read-only, no session) or answered by the backend -- so
25415
+ * a form never waits for an answer that will not come.
25416
+ ************************************************************/
25417
+ function answer_form_write(gobj, gobj_topic_form, form_write, ok) {
25418
+ if (!form_write) return;
25419
+ if (!gobj_topic_form) {
25420
+ log_error(`${gobj_short_name(gobj)}: no topic form to answer write ${form_write}`);
25421
+ return;
25422
+ }
25423
+ gobj_send_event(gobj_topic_form, ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED", { form_write }, gobj);
25385
25424
  }
25386
25425
  /********************************************
25387
25426
  * Event from formtable
@@ -25752,6 +25791,46 @@ function yui_selected_rows(tabulator) {
25752
25791
  }
25753
25792
  }
25754
25793
  /***************************************************************
25794
+ * The IDENTITY of some rows: the value of the table's `index`
25795
+ * field (Tabulator's default is `id`). This is what crosses a
25796
+ * confirmation dialog -- never the rows' POSITIONS, which the
25797
+ * table regenerates with every addData / deleteRow while the
25798
+ * dialog is open, so a position names whatever row sits there
25799
+ * when the person answers.
25800
+ ***************************************************************/
25801
+ function yui_row_ids(tabulator, rows) {
25802
+ let field = tabulator && tabulator.options && tabulator.options.index || "id";
25803
+ let ids = [];
25804
+ for (let row of rows || []) {
25805
+ let id = row ? row[field] : void 0;
25806
+ if (id !== void 0 && id !== null && id !== "") ids.push(id);
25807
+ }
25808
+ return ids;
25809
+ }
25810
+ /***************************************************************
25811
+ * The rows that carry these ids NOW -> {rows, missing}.
25812
+ * `rows` are the rows' data; `missing` the ids no row carries
25813
+ * any more. A missing id is never replaced by another row.
25814
+ ***************************************************************/
25815
+ function yui_rows_by_ids(tabulator, ids) {
25816
+ let rows = [];
25817
+ let missing = [];
25818
+ for (let id of ids || []) {
25819
+ let row = null;
25820
+ if (tabulator && typeof tabulator.getRow === "function") try {
25821
+ row = tabulator.getRow(id) || null;
25822
+ } catch (e) {
25823
+ row = null;
25824
+ }
25825
+ if (row) rows.push(row.getData());
25826
+ else missing.push(id);
25827
+ }
25828
+ return {
25829
+ rows,
25830
+ missing
25831
+ };
25832
+ }
25833
+ /***************************************************************
25755
25834
  * Drop the selection (after acting on it, or after a
25756
25835
  * reload that leaves the ticked rows behind).
25757
25836
  ***************************************************************/
@@ -26179,10 +26258,18 @@ function yui_icon_is_defined(name) {
26179
26258
  ***********************************************************************/
26180
26259
  var MAX_DEPTH = 4;
26181
26260
  /***************************************************************
26182
- * An fkey, as `nodes` answers it with `list_dict`.
26261
+ * A REFERENCE to a node: an fkey as `nodes` answers it with
26262
+ * `list_dict` -- `{id, topic_name, hook_name}` -- or a hook's child
26263
+ * as the node events deliver it, `{id, topic_name}`. It carries
26264
+ * nothing but those keys; an object with more is data.
26183
26265
  ***************************************************************/
26266
+ var REF_KEYS = /* @__PURE__ */ new Set([
26267
+ "id",
26268
+ "topic_name",
26269
+ "hook_name"
26270
+ ]);
26184
26271
  function is_fkey_ref(value) {
26185
- return !!value && typeof value === "object" && !Array.isArray(value) && "id" in value && "topic_name" in value && "hook_name" in value;
26272
+ return !!value && typeof value === "object" && !Array.isArray(value) && "id" in value && "topic_name" in value && Object.keys(value).every((k) => REF_KEYS.has(k));
26186
26273
  }
26187
26274
  /***************************************************************
26188
26275
  * A hook read with `hook_size`: the COUNT of its children,
@@ -26442,6 +26529,7 @@ var attrs_table$5 = [
26442
26529
  SDATA(data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
26443
26530
  SDATA(data_type_t.DTP_BOOLEAN, "with_copy_button", 0, true, "Button toolbar COPY"),
26444
26531
  SDATA(data_type_t.DTP_BOOLEAN, "with_paste_button", 0, true, "Button toolbar PASTE"),
26532
+ SDATA(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"),
26445
26533
  SDATA(data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
26446
26534
  SDATA(data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
26447
26535
  SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
@@ -26617,6 +26705,8 @@ function popup_mount_layer(gobj) {
26617
26705
  function mt_create$5(gobj) {
26618
26706
  gobj.priv.reading_files = 0;
26619
26707
  gobj.priv.read_serial = 0;
26708
+ gobj.priv.awaiting_write = 0;
26709
+ gobj.priv.write_serial = 0;
26620
26710
  let name = clean_name(gobj_name(gobj));
26621
26711
  gobj_write_attr(gobj, "table_id", "table" + name);
26622
26712
  gobj_write_attr(gobj, "toolbar_id", "toolbar" + name);
@@ -27135,14 +27225,11 @@ function render_selection_state(gobj) {
27135
27225
  let $container = gobj_read_attr(gobj, "$container");
27136
27226
  let $delete = $container ? $container.querySelector(`.button-delete-record`) : null;
27137
27227
  let $copy = $container ? $container.querySelector(`.button-copy-record`) : null;
27138
- if ($delete && $copy) {
27139
- if (yui_selected_rows(gobj_read_attr(gobj, "tabulator")).length > 0 && gobj_read_bool_attr(gobj, "editable")) {
27140
- $delete.removeAttribute("disabled");
27141
- $copy.removeAttribute("disabled");
27142
- } else {
27143
- $delete.setAttribute("disabled", true);
27144
- $copy.setAttribute("disabled", true);
27145
- }
27228
+ let enable = yui_selected_rows(gobj_read_attr(gobj, "tabulator")).length > 0 && gobj_read_bool_attr(gobj, "editable");
27229
+ for (let $button of [$delete, $copy]) {
27230
+ if (!$button) continue;
27231
+ if (enable) $button.removeAttribute("disabled");
27232
+ else $button.setAttribute("disabled", true);
27146
27233
  }
27147
27234
  render_selection_bar(gobj);
27148
27235
  }
@@ -27450,7 +27537,7 @@ function create_tabulator(gobj) {
27450
27537
  hozAlign,
27451
27538
  formatter: colFormatter
27452
27539
  };
27453
- if (max_col_width > 0) colDef.maxWidth = max_col_width;
27540
+ if (max_col_width > 0) colDef.maxInitialWidth = max_col_width;
27454
27541
  if (with_header_filters) apply_header_filter(colDef, field_desc);
27455
27542
  if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
27456
27543
  if (vertAlign) colDef.vertAlign = vertAlign;
@@ -27488,10 +27575,7 @@ function create_tabulator(gobj) {
27488
27575
  gobj_send_event(gobj, "EV_EDIT_RECORD", { index }, gobj);
27489
27576
  } else if (e.target.closest(".remove")) {
27490
27577
  e.stopPropagation();
27491
- gobj_send_event(gobj, "EV_DELETE_ROWS", {
27492
- index,
27493
- row
27494
- }, gobj);
27578
+ gobj_send_event(gobj, "EV_DELETE_ROWS", { row }, gobj);
27495
27579
  }
27496
27580
  }
27497
27581
  });
@@ -28029,6 +28113,7 @@ function form_may_close(gobj) {
28029
28113
  function teardown_form_child(gobj) {
28030
28114
  let priv = gobj.priv;
28031
28115
  priv.reading_files = 0;
28116
+ priv.awaiting_write = 0;
28032
28117
  if (priv.form) {
28033
28118
  if (gobj_is_running(priv.form)) gobj_stop(priv.form);
28034
28119
  gobj_destroy(priv.form);
@@ -29038,25 +29123,31 @@ function ac_edition_mode(gobj, event, kw, src) {
29038
29123
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
29039
29124
  let $button_paste_record = $container.querySelector(`.button-paste-record`);
29040
29125
  if (editable) {
29041
- $button_edit_record.classList.add("is-primary");
29042
- $button_new_record.classList.add("is-info");
29043
- $button_delete_record.classList.add("is-danger");
29126
+ if ($button_edit_record) $button_edit_record.classList.add("is-primary");
29127
+ if ($button_new_record) {
29128
+ $button_new_record.classList.add("is-info");
29129
+ $button_new_record.removeAttribute("disabled");
29130
+ }
29131
+ if ($button_delete_record) $button_delete_record.classList.add("is-danger");
29044
29132
  tabulator.showColumn("_operation");
29045
29133
  tabulator.showColumn("_check_box_state_");
29046
- $button_new_record.removeAttribute("disabled");
29047
29134
  if ($button_paste_record) $button_paste_record.removeAttribute("disabled");
29048
29135
  if (yui_selected_rows(tabulator).length) {
29049
- $button_delete_record.removeAttribute("disabled");
29136
+ if ($button_delete_record) $button_delete_record.removeAttribute("disabled");
29050
29137
  if ($button_copy_record) $button_copy_record.removeAttribute("disabled");
29051
29138
  }
29052
29139
  } else {
29053
- $button_edit_record.classList.remove("is-primary");
29054
- $button_new_record.classList.remove("is-info");
29055
- $button_delete_record.classList.remove("is-danger");
29140
+ if ($button_edit_record) $button_edit_record.classList.remove("is-primary");
29141
+ if ($button_new_record) {
29142
+ $button_new_record.classList.remove("is-info");
29143
+ $button_new_record.setAttribute("disabled", true);
29144
+ }
29145
+ if ($button_delete_record) {
29146
+ $button_delete_record.classList.remove("is-danger");
29147
+ $button_delete_record.setAttribute("disabled", true);
29148
+ }
29056
29149
  tabulator.hideColumn("_operation");
29057
29150
  tabulator.hideColumn("_check_box_state_");
29058
- $button_new_record.setAttribute("disabled", true);
29059
- $button_delete_record.setAttribute("disabled", true);
29060
29151
  if ($button_copy_record) $button_copy_record.setAttribute("disabled", true);
29061
29152
  if ($button_paste_record) $button_paste_record.setAttribute("disabled", true);
29062
29153
  }
@@ -29098,7 +29189,12 @@ function ac_new_row(gobj, event, kw, src) {
29098
29189
  * kw {} empty
29099
29190
  * - From the column _operation delete icon inside a row
29100
29191
  * It will delete this one row
29101
- * kw {index:, row:}
29192
+ * kw {row:}
29193
+ *
29194
+ * What crosses the confirmation is the rows' IDENTITY, never their
29195
+ * position: the view applies EV_TREEDB_NODE_* of every writer while the
29196
+ * dialog is open, and a position names whatever row sits there when the
29197
+ * person answers -- with force:true (A6 of the 2026-09-21 review).
29102
29198
  ************************************************************/
29103
29199
  function ac_delete_rows(gobj, event, kw, src) {
29104
29200
  if (refuse_if_readonly$2(gobj, event)) return -1;
@@ -29112,17 +29208,20 @@ function ac_delete_rows(gobj, event, kw, src) {
29112
29208
  });
29113
29209
  return 0;
29114
29210
  }
29115
- confirm_then$1(gobj, build_delete_question(gobj, rows), { what: "delete_selection" });
29211
+ confirm_then$1(gobj, build_delete_question(gobj, rows), {
29212
+ what: "delete_selection",
29213
+ ids: yui_row_ids(tabulator, rows)
29214
+ });
29116
29215
  } else confirm_then$1(gobj, build_delete_question(gobj, kw.row), {
29117
29216
  what: "delete_row",
29118
- index: kw.index
29217
+ ids: yui_row_ids(tabulator, [kw.row])
29119
29218
  });
29120
29219
  return 0;
29121
29220
  }
29122
29221
  /************************************************************
29123
29222
  * {what, ...} -- the person said yes to what confirm_then()
29124
- * asked. The rows are read again HERE and not carried in the
29125
- * kw: the kw is plain json, and a Tabulator row is not.
29223
+ * asked. The rows are found again HERE by the ids the question
29224
+ * named: the kw is plain json, and a Tabulator row is not.
29126
29225
  ************************************************************/
29127
29226
  function ac_confirmed$2(gobj, event, kw, src) {
29128
29227
  let priv = gobj.priv;
@@ -29134,31 +29233,16 @@ function ac_confirmed$2(gobj, event, kw, src) {
29134
29233
  if (refuse_if_readonly$2(gobj, event)) return -1;
29135
29234
  let tabulator = gobj_read_attr(gobj, "tabulator");
29136
29235
  let topic_name = gobj_read_str_attr(gobj, "topic_name");
29137
- if (what === "delete_selection") {
29138
- let rows = yui_selected_rows(tabulator);
29139
- if (!rows.length) {
29140
- log_error(`${gobj_short_name(gobj)}: ${event} with the selection gone`);
29141
- return -1;
29142
- }
29143
- for (let row of rows) gobj_publish_event(gobj, "EV_DELETE_RECORD", {
29236
+ if (what === "delete_selection" || what === "delete_row") {
29237
+ let found = yui_rows_by_ids(tabulator, Array.isArray(kw.ids) ? kw.ids : []);
29238
+ if (found.missing.length) log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while confirming the delete, not deleted: ${found.missing.join(", ")}`);
29239
+ if (!found.rows.length) return -1;
29240
+ for (let row of found.rows) gobj_publish_event(gobj, "EV_DELETE_RECORD", {
29144
29241
  topic_name,
29145
29242
  record: row
29146
29243
  });
29147
29244
  return 0;
29148
29245
  }
29149
- if (what === "delete_row") {
29150
- let index = kw.index;
29151
- let row = tabulator && typeof index === "number" ? tabulator.getRowFromPosition(index) : null;
29152
- if (!row) {
29153
- log_error(`${gobj_short_name(gobj)}: ${event} names no row: ${index}`);
29154
- return -1;
29155
- }
29156
- gobj_publish_event(gobj, "EV_DELETE_RECORD", {
29157
- topic_name,
29158
- record: row.getData()
29159
- });
29160
- return 0;
29161
- }
29162
29246
  log_error(`${gobj_short_name(gobj)}: ${event} of nothing this view asked about: ${what}`);
29163
29247
  return -1;
29164
29248
  }
@@ -29216,6 +29300,10 @@ function ac_form_save_record(gobj, event, kw, src) {
29216
29300
  if (answer && answer.result >= 0 && answer.data) picked = answer.data;
29217
29301
  const cols = Object.keys(picked);
29218
29302
  if (cols.length === 0) {
29303
+ if (wait_for_write_answer(gobj)) {
29304
+ publish_treedb_write(gobj, mode, kw, null, priv.awaiting_write);
29305
+ return 0;
29306
+ }
29219
29307
  publish_treedb_write(gobj, mode, kw);
29220
29308
  gobj_post_event(gobj, "EV_CLOSE_FORM", {}, gobj);
29221
29309
  return 0;
@@ -29286,11 +29374,12 @@ function read_is_current(gobj, kw) {
29286
29374
  /************************************************************
29287
29375
  * The one place a write of this view leaves for the app.
29288
29376
  ************************************************************/
29289
- function publish_treedb_write(gobj, mode, record, jn_files) {
29377
+ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29290
29378
  const kw_write = {
29291
29379
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
29292
29380
  record: strip_read_only_cols(gobj, record)
29293
29381
  };
29382
+ if (form_write) kw_write.form_write = form_write;
29294
29383
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29295
29384
  gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
29296
29385
  }
@@ -29309,11 +29398,53 @@ function ac_files_read(gobj, event, kw, src) {
29309
29398
  if (!read_is_current(gobj, kw)) return -1;
29310
29399
  gobj.priv.reading_files = 0;
29311
29400
  const { record, __files__ } = yui_files_manifest(kw.picks, kw.record);
29401
+ if (wait_for_write_answer(gobj)) {
29402
+ publish_treedb_write(gobj, kw.mode, record, __files__, gobj.priv.awaiting_write);
29403
+ return 0;
29404
+ }
29312
29405
  publish_treedb_write(gobj, kw.mode, record, __files__);
29313
29406
  close_form_dialog(gobj);
29314
29407
  return 0;
29315
29408
  }
29316
29409
  /************************************************************
29410
+ * Does the form wait for the answer of its write? Then it stays
29411
+ * open and BUSY, and the serial it waits for is taken here.
29412
+ *
29413
+ * A refused write used to close the form all the same and throw
29414
+ * away what was typed, although the README said it stayed open
29415
+ * (M25 of the 2026-09-21 review): the close was posted with the
29416
+ * publish, before the backend had answered anything.
29417
+ ************************************************************/
29418
+ function wait_for_write_answer(gobj) {
29419
+ let priv = gobj.priv;
29420
+ if (!gobj_read_bool_attr(gobj, "form_waits_for_answer") || !priv.form) return false;
29421
+ priv.awaiting_write = ++priv.write_serial;
29422
+ set_form_busy(gobj, true);
29423
+ return true;
29424
+ }
29425
+ /************************************************************
29426
+ * {form_write} -- the host says the form's write was done
29427
+ ************************************************************/
29428
+ function ac_write_done(gobj, event, kw, src) {
29429
+ let priv = gobj.priv;
29430
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29431
+ priv.awaiting_write = 0;
29432
+ set_form_busy(gobj, false);
29433
+ close_form_dialog(gobj);
29434
+ return 0;
29435
+ }
29436
+ /************************************************************
29437
+ * {form_write} -- the host says the form's write was refused.
29438
+ * The form stays OPEN on what was typed; the host has shown why.
29439
+ ************************************************************/
29440
+ function ac_write_refused(gobj, event, kw, src) {
29441
+ let priv = gobj.priv;
29442
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29443
+ priv.awaiting_write = 0;
29444
+ set_form_busy(gobj, false);
29445
+ return 0;
29446
+ }
29447
+ /************************************************************
29317
29448
  * {reason} -- the browser could not read what was picked
29318
29449
  *
29319
29450
  * The form stays OPEN: the record is still there, and closing it would
@@ -30073,6 +30204,16 @@ function create_gclass$5(gclass_name) {
30073
30204
  ac_close_form,
30074
30205
  null
30075
30206
  ],
30207
+ [
30208
+ "EV_WRITE_DONE",
30209
+ ac_write_done,
30210
+ null
30211
+ ],
30212
+ [
30213
+ "EV_WRITE_REFUSED",
30214
+ ac_write_refused,
30215
+ null
30216
+ ],
30076
30217
  [
30077
30218
  "EV_FILES_READ",
30078
30219
  ac_files_read,
@@ -30268,6 +30409,8 @@ function create_gclass$5(gclass_name) {
30268
30409
  ["EV_EDITION_MODE", 0],
30269
30410
  ["EV_SAVE_RECORD", 0],
30270
30411
  ["EV_CLOSE_FORM", 0],
30412
+ ["EV_WRITE_DONE", 0],
30413
+ ["EV_WRITE_REFUSED", 0],
30271
30414
  ["EV_FILES_READ", 0],
30272
30415
  ["EV_FILES_FAILED", 0],
30273
30416
  ["EV_NEW_ROW", 0],
@@ -30409,6 +30552,38 @@ function plan_graph_saves(live, saved) {
30409
30552
  for (const [topic_name, properties] of Object.entries(live)) if (topic_arrangement_changed(properties, have[topic_name])) plan.push(topic_name);
30410
30553
  return plan;
30411
30554
  }
30555
+ /***************************************************************
30556
+ * An ECHO of one __graphs__ record arrived: the node event a save
30557
+ * of it sends back, from this browser or from another one.
30558
+ *
30559
+ * live {topic_name: properties} what the view shows
30560
+ * saved {topic_name: properties} what the backend has
30561
+ * records every __graphs__ record the view holds, `rec`
30562
+ * already in its place among them
30563
+ * rec the echoed record
30564
+ *
30565
+ * Only rec's TOPIC moves: it becomes the record, as shown and as
30566
+ * saved. The other topics are left alone -- rebuilding their saved
30567
+ * copies here took them from the live objects the view had already
30568
+ * rearranged in place, so their unsaved changes counted as saved.
30569
+ *
30570
+ * `live[topic]` IS the record's own object, as when the view loads:
30571
+ * the view arranges it in place. `saved[topic]` is a copy.
30572
+ ***************************************************************/
30573
+ function apply_graphs_echo(live, saved, records, rec) {
30574
+ if (!is_object(rec) || !rec.topic) return;
30575
+ if (rec.active && is_object(rec.properties)) {
30576
+ live[rec.topic] = rec.properties;
30577
+ saved[rec.topic] = json_deep_copy(rec.properties);
30578
+ return;
30579
+ }
30580
+ if (!(records || []).some((r) => {
30581
+ return r && r !== rec && r.topic === rec.topic && r.active && is_object(r.properties);
30582
+ })) {
30583
+ delete live[rec.topic];
30584
+ delete saved[rec.topic];
30585
+ }
30586
+ }
30412
30587
  //#endregion
30413
30588
  //#region src/treedb_node_label.js
30414
30589
  /***********************************************************************
@@ -65261,7 +65436,7 @@ function ac_node_updated(gobj, event, kw, src) {
65261
65436
  break;
65262
65437
  }
65263
65438
  if (!found) priv.__graphs__.push(node);
65264
- build_graph_properties(gobj);
65439
+ apply_graphs_echo(priv._graph_properties, priv._saved_graph_properties, priv.__graphs__, node);
65265
65440
  return 0;
65266
65441
  }
65267
65442
  let desc = priv.descs[topic_name];
@@ -65289,8 +65464,9 @@ function ac_node_updated(gobj, event, kw, src) {
65289
65464
  update_local_node(gobj, topic_name, node);
65290
65465
  let on_screen = false;
65291
65466
  try {
65292
- on_screen = !!graph.getNodeData(node_name);
65467
+ on_screen = !!priv.graph.getNodeData(node_name);
65293
65468
  } catch (e) {
65469
+ if (e instanceof ReferenceError) throw e;
65294
65470
  on_screen = false;
65295
65471
  }
65296
65472
  if (on_screen) update_topic_node(gobj, desc, node_name, node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.192",
3
+ "version": "7.23.193",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -93,6 +93,7 @@ import {
93
93
  } from "@yuneta/gobj-js";
94
94
 
95
95
  import {
96
+ apply_graphs_echo,
96
97
  plan_graph_saves,
97
98
  topic_arrangement_changed,
98
99
  } from "./graph_save_plan.js";
@@ -10579,7 +10580,12 @@ function ac_node_updated(gobj, event, kw, src)
10579
10580
  if(!found) {
10580
10581
  priv.__graphs__.push(node);
10581
10582
  }
10582
- build_graph_properties(gobj);
10583
+ /* Only THIS record's topic: a rebuild of every topic took their
10584
+ * saved copies from the live objects (M32 of the 2026-09-21
10585
+ * review). build_graph_properties() is for the load. */
10586
+ apply_graphs_echo(
10587
+ priv._graph_properties, priv._saved_graph_properties, priv.__graphs__, node
10588
+ );
10583
10589
  return 0;
10584
10590
  }
10585
10591
 
@@ -10628,10 +10634,17 @@ function ac_node_updated(gobj, event, kw, src)
10628
10634
  * repaints the count on whatever pill reaches it.
10629
10635
  */
10630
10636
  update_local_node(gobj, topic_name, node);
10637
+ /* `priv.graph`: this read an undeclared `graph`, the ReferenceError
10638
+ * fell into the catch, and a card on screen kept its old record after
10639
+ * every UPDATED (M33 of the 2026-09-21 review). The catch is for G6,
10640
+ * which throws on an id it does not hold -- a folded card. */
10631
10641
  let on_screen = false;
10632
10642
  try {
10633
- on_screen = !!graph.getNodeData(node_name);
10643
+ on_screen = !!priv.graph.getNodeData(node_name);
10634
10644
  } catch(e) {
10645
+ if(e instanceof ReferenceError) {
10646
+ throw e;
10647
+ }
10635
10648
  on_screen = false;
10636
10649
  }
10637
10650
  if(on_screen) {