@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.
@@ -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,24 @@ 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,
25238
+ create_only: true,
25226
25239
  autolink: true
25227
- });
25240
+ }, form_write) < 0) {
25241
+ answer_form_write(gobj, src, form_write, false);
25242
+ return -1;
25243
+ }
25244
+ return 0;
25228
25245
  }
25229
25246
  /********************************************
25230
25247
  * A topic table wants its records as json, WITH the metadata of
@@ -25374,14 +25391,37 @@ function ac_update_field(gobj, event, kw, src) {
25374
25391
  * }
25375
25392
  ********************************************/
25376
25393
  function ac_update_record(gobj, event, kw, src) {
25377
- if (refuse_if_readonly$3(gobj, event)) return -1;
25394
+ let form_write = kw && kw.form_write || 0;
25395
+ if (refuse_if_readonly$3(gobj, event)) {
25396
+ answer_form_write(gobj, src, form_write, false);
25397
+ return -1;
25398
+ }
25378
25399
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
25379
25400
  let topic_name = gobj_read_attr(src, "topic_name");
25380
25401
  let record = kw.record;
25381
- return treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25402
+ if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25382
25403
  list_dict: true,
25383
25404
  autolink: true
25384
- });
25405
+ }, form_write) < 0) {
25406
+ answer_form_write(gobj, src, form_write, false);
25407
+ return -1;
25408
+ }
25409
+ return 0;
25410
+ }
25411
+ /************************************************************
25412
+ * Tell a topic form how its write ended: EV_WRITE_DONE closes
25413
+ * it, EV_WRITE_REFUSED leaves it open on what was typed. Every
25414
+ * way a form's write can end passes here -- refused before it
25415
+ * left (read-only, no session) or answered by the backend -- so
25416
+ * a form never waits for an answer that will not come.
25417
+ ************************************************************/
25418
+ function answer_form_write(gobj, gobj_topic_form, form_write, ok) {
25419
+ if (!form_write) return;
25420
+ if (!gobj_topic_form) {
25421
+ log_error(`${gobj_short_name(gobj)}: no topic form to answer write ${form_write}`);
25422
+ return;
25423
+ }
25424
+ gobj_send_event(gobj_topic_form, ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED", { form_write }, gobj);
25385
25425
  }
25386
25426
  /********************************************
25387
25427
  * Event from formtable
@@ -25752,6 +25792,46 @@ function yui_selected_rows(tabulator) {
25752
25792
  }
25753
25793
  }
25754
25794
  /***************************************************************
25795
+ * The IDENTITY of some rows: the value of the table's `index`
25796
+ * field (Tabulator's default is `id`). This is what crosses a
25797
+ * confirmation dialog -- never the rows' POSITIONS, which the
25798
+ * table regenerates with every addData / deleteRow while the
25799
+ * dialog is open, so a position names whatever row sits there
25800
+ * when the person answers.
25801
+ ***************************************************************/
25802
+ function yui_row_ids(tabulator, rows) {
25803
+ let field = tabulator && tabulator.options && tabulator.options.index || "id";
25804
+ let ids = [];
25805
+ for (let row of rows || []) {
25806
+ let id = row ? row[field] : void 0;
25807
+ if (id !== void 0 && id !== null && id !== "") ids.push(id);
25808
+ }
25809
+ return ids;
25810
+ }
25811
+ /***************************************************************
25812
+ * The rows that carry these ids NOW -> {rows, missing}.
25813
+ * `rows` are the rows' data; `missing` the ids no row carries
25814
+ * any more. A missing id is never replaced by another row.
25815
+ ***************************************************************/
25816
+ function yui_rows_by_ids(tabulator, ids) {
25817
+ let rows = [];
25818
+ let missing = [];
25819
+ for (let id of ids || []) {
25820
+ let row = null;
25821
+ if (tabulator && typeof tabulator.getRow === "function") try {
25822
+ row = tabulator.getRow(id) || null;
25823
+ } catch (e) {
25824
+ row = null;
25825
+ }
25826
+ if (row) rows.push(row.getData());
25827
+ else missing.push(id);
25828
+ }
25829
+ return {
25830
+ rows,
25831
+ missing
25832
+ };
25833
+ }
25834
+ /***************************************************************
25755
25835
  * Drop the selection (after acting on it, or after a
25756
25836
  * reload that leaves the ticked rows behind).
25757
25837
  ***************************************************************/
@@ -26179,10 +26259,18 @@ function yui_icon_is_defined(name) {
26179
26259
  ***********************************************************************/
26180
26260
  var MAX_DEPTH = 4;
26181
26261
  /***************************************************************
26182
- * An fkey, as `nodes` answers it with `list_dict`.
26262
+ * A REFERENCE to a node: an fkey as `nodes` answers it with
26263
+ * `list_dict` -- `{id, topic_name, hook_name}` -- or a hook's child
26264
+ * as the node events deliver it, `{id, topic_name}`. It carries
26265
+ * nothing but those keys; an object with more is data.
26183
26266
  ***************************************************************/
26267
+ var REF_KEYS = /* @__PURE__ */ new Set([
26268
+ "id",
26269
+ "topic_name",
26270
+ "hook_name"
26271
+ ]);
26184
26272
  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;
26273
+ 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
26274
  }
26187
26275
  /***************************************************************
26188
26276
  * A hook read with `hook_size`: the COUNT of its children,
@@ -26442,6 +26530,7 @@ var attrs_table$5 = [
26442
26530
  SDATA(data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
26443
26531
  SDATA(data_type_t.DTP_BOOLEAN, "with_copy_button", 0, true, "Button toolbar COPY"),
26444
26532
  SDATA(data_type_t.DTP_BOOLEAN, "with_paste_button", 0, true, "Button toolbar PASTE"),
26533
+ 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
26534
  SDATA(data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
26446
26535
  SDATA(data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
26447
26536
  SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
@@ -26617,6 +26706,8 @@ function popup_mount_layer(gobj) {
26617
26706
  function mt_create$5(gobj) {
26618
26707
  gobj.priv.reading_files = 0;
26619
26708
  gobj.priv.read_serial = 0;
26709
+ gobj.priv.awaiting_write = 0;
26710
+ gobj.priv.write_serial = 0;
26620
26711
  let name = clean_name(gobj_name(gobj));
26621
26712
  gobj_write_attr(gobj, "table_id", "table" + name);
26622
26713
  gobj_write_attr(gobj, "toolbar_id", "toolbar" + name);
@@ -27135,14 +27226,11 @@ function render_selection_state(gobj) {
27135
27226
  let $container = gobj_read_attr(gobj, "$container");
27136
27227
  let $delete = $container ? $container.querySelector(`.button-delete-record`) : null;
27137
27228
  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
- }
27229
+ let enable = yui_selected_rows(gobj_read_attr(gobj, "tabulator")).length > 0 && gobj_read_bool_attr(gobj, "editable");
27230
+ for (let $button of [$delete, $copy]) {
27231
+ if (!$button) continue;
27232
+ if (enable) $button.removeAttribute("disabled");
27233
+ else $button.setAttribute("disabled", true);
27146
27234
  }
27147
27235
  render_selection_bar(gobj);
27148
27236
  }
@@ -27450,7 +27538,7 @@ function create_tabulator(gobj) {
27450
27538
  hozAlign,
27451
27539
  formatter: colFormatter
27452
27540
  };
27453
- if (max_col_width > 0) colDef.maxWidth = max_col_width;
27541
+ if (max_col_width > 0) colDef.maxInitialWidth = max_col_width;
27454
27542
  if (with_header_filters) apply_header_filter(colDef, field_desc);
27455
27543
  if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
27456
27544
  if (vertAlign) colDef.vertAlign = vertAlign;
@@ -27488,10 +27576,7 @@ function create_tabulator(gobj) {
27488
27576
  gobj_send_event(gobj, "EV_EDIT_RECORD", { index }, gobj);
27489
27577
  } else if (e.target.closest(".remove")) {
27490
27578
  e.stopPropagation();
27491
- gobj_send_event(gobj, "EV_DELETE_ROWS", {
27492
- index,
27493
- row
27494
- }, gobj);
27579
+ gobj_send_event(gobj, "EV_DELETE_ROWS", { row }, gobj);
27495
27580
  }
27496
27581
  }
27497
27582
  });
@@ -28029,6 +28114,7 @@ function form_may_close(gobj) {
28029
28114
  function teardown_form_child(gobj) {
28030
28115
  let priv = gobj.priv;
28031
28116
  priv.reading_files = 0;
28117
+ priv.awaiting_write = 0;
28032
28118
  if (priv.form) {
28033
28119
  if (gobj_is_running(priv.form)) gobj_stop(priv.form);
28034
28120
  gobj_destroy(priv.form);
@@ -29038,25 +29124,31 @@ function ac_edition_mode(gobj, event, kw, src) {
29038
29124
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
29039
29125
  let $button_paste_record = $container.querySelector(`.button-paste-record`);
29040
29126
  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");
29127
+ if ($button_edit_record) $button_edit_record.classList.add("is-primary");
29128
+ if ($button_new_record) {
29129
+ $button_new_record.classList.add("is-info");
29130
+ $button_new_record.removeAttribute("disabled");
29131
+ }
29132
+ if ($button_delete_record) $button_delete_record.classList.add("is-danger");
29044
29133
  tabulator.showColumn("_operation");
29045
29134
  tabulator.showColumn("_check_box_state_");
29046
- $button_new_record.removeAttribute("disabled");
29047
29135
  if ($button_paste_record) $button_paste_record.removeAttribute("disabled");
29048
29136
  if (yui_selected_rows(tabulator).length) {
29049
- $button_delete_record.removeAttribute("disabled");
29137
+ if ($button_delete_record) $button_delete_record.removeAttribute("disabled");
29050
29138
  if ($button_copy_record) $button_copy_record.removeAttribute("disabled");
29051
29139
  }
29052
29140
  } 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");
29141
+ if ($button_edit_record) $button_edit_record.classList.remove("is-primary");
29142
+ if ($button_new_record) {
29143
+ $button_new_record.classList.remove("is-info");
29144
+ $button_new_record.setAttribute("disabled", true);
29145
+ }
29146
+ if ($button_delete_record) {
29147
+ $button_delete_record.classList.remove("is-danger");
29148
+ $button_delete_record.setAttribute("disabled", true);
29149
+ }
29056
29150
  tabulator.hideColumn("_operation");
29057
29151
  tabulator.hideColumn("_check_box_state_");
29058
- $button_new_record.setAttribute("disabled", true);
29059
- $button_delete_record.setAttribute("disabled", true);
29060
29152
  if ($button_copy_record) $button_copy_record.setAttribute("disabled", true);
29061
29153
  if ($button_paste_record) $button_paste_record.setAttribute("disabled", true);
29062
29154
  }
@@ -29098,7 +29190,12 @@ function ac_new_row(gobj, event, kw, src) {
29098
29190
  * kw {} empty
29099
29191
  * - From the column _operation delete icon inside a row
29100
29192
  * It will delete this one row
29101
- * kw {index:, row:}
29193
+ * kw {row:}
29194
+ *
29195
+ * What crosses the confirmation is the rows' IDENTITY, never their
29196
+ * position: the view applies EV_TREEDB_NODE_* of every writer while the
29197
+ * dialog is open, and a position names whatever row sits there when the
29198
+ * person answers -- with force:true (A6 of the 2026-09-21 review).
29102
29199
  ************************************************************/
29103
29200
  function ac_delete_rows(gobj, event, kw, src) {
29104
29201
  if (refuse_if_readonly$2(gobj, event)) return -1;
@@ -29112,17 +29209,20 @@ function ac_delete_rows(gobj, event, kw, src) {
29112
29209
  });
29113
29210
  return 0;
29114
29211
  }
29115
- confirm_then$1(gobj, build_delete_question(gobj, rows), { what: "delete_selection" });
29212
+ confirm_then$1(gobj, build_delete_question(gobj, rows), {
29213
+ what: "delete_selection",
29214
+ ids: yui_row_ids(tabulator, rows)
29215
+ });
29116
29216
  } else confirm_then$1(gobj, build_delete_question(gobj, kw.row), {
29117
29217
  what: "delete_row",
29118
- index: kw.index
29218
+ ids: yui_row_ids(tabulator, [kw.row])
29119
29219
  });
29120
29220
  return 0;
29121
29221
  }
29122
29222
  /************************************************************
29123
29223
  * {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.
29224
+ * asked. The rows are found again HERE by the ids the question
29225
+ * named: the kw is plain json, and a Tabulator row is not.
29126
29226
  ************************************************************/
29127
29227
  function ac_confirmed$2(gobj, event, kw, src) {
29128
29228
  let priv = gobj.priv;
@@ -29134,31 +29234,16 @@ function ac_confirmed$2(gobj, event, kw, src) {
29134
29234
  if (refuse_if_readonly$2(gobj, event)) return -1;
29135
29235
  let tabulator = gobj_read_attr(gobj, "tabulator");
29136
29236
  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", {
29237
+ if (what === "delete_selection" || what === "delete_row") {
29238
+ let found = yui_rows_by_ids(tabulator, Array.isArray(kw.ids) ? kw.ids : []);
29239
+ if (found.missing.length) log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while confirming the delete, not deleted: ${found.missing.join(", ")}`);
29240
+ if (!found.rows.length) return -1;
29241
+ for (let row of found.rows) gobj_publish_event(gobj, "EV_DELETE_RECORD", {
29144
29242
  topic_name,
29145
29243
  record: row
29146
29244
  });
29147
29245
  return 0;
29148
29246
  }
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
29247
  log_error(`${gobj_short_name(gobj)}: ${event} of nothing this view asked about: ${what}`);
29163
29248
  return -1;
29164
29249
  }
@@ -29216,6 +29301,10 @@ function ac_form_save_record(gobj, event, kw, src) {
29216
29301
  if (answer && answer.result >= 0 && answer.data) picked = answer.data;
29217
29302
  const cols = Object.keys(picked);
29218
29303
  if (cols.length === 0) {
29304
+ if (wait_for_write_answer(gobj)) {
29305
+ publish_treedb_write(gobj, mode, kw, null, priv.awaiting_write);
29306
+ return 0;
29307
+ }
29219
29308
  publish_treedb_write(gobj, mode, kw);
29220
29309
  gobj_post_event(gobj, "EV_CLOSE_FORM", {}, gobj);
29221
29310
  return 0;
@@ -29286,11 +29375,12 @@ function read_is_current(gobj, kw) {
29286
29375
  /************************************************************
29287
29376
  * The one place a write of this view leaves for the app.
29288
29377
  ************************************************************/
29289
- function publish_treedb_write(gobj, mode, record, jn_files) {
29378
+ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29290
29379
  const kw_write = {
29291
29380
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
29292
29381
  record: strip_read_only_cols(gobj, record)
29293
29382
  };
29383
+ if (form_write) kw_write.form_write = form_write;
29294
29384
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29295
29385
  gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
29296
29386
  }
@@ -29309,11 +29399,53 @@ function ac_files_read(gobj, event, kw, src) {
29309
29399
  if (!read_is_current(gobj, kw)) return -1;
29310
29400
  gobj.priv.reading_files = 0;
29311
29401
  const { record, __files__ } = yui_files_manifest(kw.picks, kw.record);
29402
+ if (wait_for_write_answer(gobj)) {
29403
+ publish_treedb_write(gobj, kw.mode, record, __files__, gobj.priv.awaiting_write);
29404
+ return 0;
29405
+ }
29312
29406
  publish_treedb_write(gobj, kw.mode, record, __files__);
29313
29407
  close_form_dialog(gobj);
29314
29408
  return 0;
29315
29409
  }
29316
29410
  /************************************************************
29411
+ * Does the form wait for the answer of its write? Then it stays
29412
+ * open and BUSY, and the serial it waits for is taken here.
29413
+ *
29414
+ * A refused write used to close the form all the same and throw
29415
+ * away what was typed, although the README said it stayed open
29416
+ * (M25 of the 2026-09-21 review): the close was posted with the
29417
+ * publish, before the backend had answered anything.
29418
+ ************************************************************/
29419
+ function wait_for_write_answer(gobj) {
29420
+ let priv = gobj.priv;
29421
+ if (!gobj_read_bool_attr(gobj, "form_waits_for_answer") || !priv.form) return false;
29422
+ priv.awaiting_write = ++priv.write_serial;
29423
+ set_form_busy(gobj, true);
29424
+ return true;
29425
+ }
29426
+ /************************************************************
29427
+ * {form_write} -- the host says the form's write was done
29428
+ ************************************************************/
29429
+ function ac_write_done(gobj, event, kw, src) {
29430
+ let priv = gobj.priv;
29431
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29432
+ priv.awaiting_write = 0;
29433
+ set_form_busy(gobj, false);
29434
+ close_form_dialog(gobj);
29435
+ return 0;
29436
+ }
29437
+ /************************************************************
29438
+ * {form_write} -- the host says the form's write was refused.
29439
+ * The form stays OPEN on what was typed; the host has shown why.
29440
+ ************************************************************/
29441
+ function ac_write_refused(gobj, event, kw, src) {
29442
+ let priv = gobj.priv;
29443
+ if (!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) return 0;
29444
+ priv.awaiting_write = 0;
29445
+ set_form_busy(gobj, false);
29446
+ return 0;
29447
+ }
29448
+ /************************************************************
29317
29449
  * {reason} -- the browser could not read what was picked
29318
29450
  *
29319
29451
  * The form stays OPEN: the record is still there, and closing it would
@@ -30073,6 +30205,16 @@ function create_gclass$5(gclass_name) {
30073
30205
  ac_close_form,
30074
30206
  null
30075
30207
  ],
30208
+ [
30209
+ "EV_WRITE_DONE",
30210
+ ac_write_done,
30211
+ null
30212
+ ],
30213
+ [
30214
+ "EV_WRITE_REFUSED",
30215
+ ac_write_refused,
30216
+ null
30217
+ ],
30076
30218
  [
30077
30219
  "EV_FILES_READ",
30078
30220
  ac_files_read,
@@ -30268,6 +30410,8 @@ function create_gclass$5(gclass_name) {
30268
30410
  ["EV_EDITION_MODE", 0],
30269
30411
  ["EV_SAVE_RECORD", 0],
30270
30412
  ["EV_CLOSE_FORM", 0],
30413
+ ["EV_WRITE_DONE", 0],
30414
+ ["EV_WRITE_REFUSED", 0],
30271
30415
  ["EV_FILES_READ", 0],
30272
30416
  ["EV_FILES_FAILED", 0],
30273
30417
  ["EV_NEW_ROW", 0],
@@ -30409,6 +30553,38 @@ function plan_graph_saves(live, saved) {
30409
30553
  for (const [topic_name, properties] of Object.entries(live)) if (topic_arrangement_changed(properties, have[topic_name])) plan.push(topic_name);
30410
30554
  return plan;
30411
30555
  }
30556
+ /***************************************************************
30557
+ * An ECHO of one __graphs__ record arrived: the node event a save
30558
+ * of it sends back, from this browser or from another one.
30559
+ *
30560
+ * live {topic_name: properties} what the view shows
30561
+ * saved {topic_name: properties} what the backend has
30562
+ * records every __graphs__ record the view holds, `rec`
30563
+ * already in its place among them
30564
+ * rec the echoed record
30565
+ *
30566
+ * Only rec's TOPIC moves: it becomes the record, as shown and as
30567
+ * saved. The other topics are left alone -- rebuilding their saved
30568
+ * copies here took them from the live objects the view had already
30569
+ * rearranged in place, so their unsaved changes counted as saved.
30570
+ *
30571
+ * `live[topic]` IS the record's own object, as when the view loads:
30572
+ * the view arranges it in place. `saved[topic]` is a copy.
30573
+ ***************************************************************/
30574
+ function apply_graphs_echo(live, saved, records, rec) {
30575
+ if (!is_object(rec) || !rec.topic) return;
30576
+ if (rec.active && is_object(rec.properties)) {
30577
+ live[rec.topic] = rec.properties;
30578
+ saved[rec.topic] = json_deep_copy(rec.properties);
30579
+ return;
30580
+ }
30581
+ if (!(records || []).some((r) => {
30582
+ return r && r !== rec && r.topic === rec.topic && r.active && is_object(r.properties);
30583
+ })) {
30584
+ delete live[rec.topic];
30585
+ delete saved[rec.topic];
30586
+ }
30587
+ }
30412
30588
  //#endregion
30413
30589
  //#region src/treedb_node_label.js
30414
30590
  /***********************************************************************
@@ -65261,7 +65437,7 @@ function ac_node_updated(gobj, event, kw, src) {
65261
65437
  break;
65262
65438
  }
65263
65439
  if (!found) priv.__graphs__.push(node);
65264
- build_graph_properties(gobj);
65440
+ apply_graphs_echo(priv._graph_properties, priv._saved_graph_properties, priv.__graphs__, node);
65265
65441
  return 0;
65266
65442
  }
65267
65443
  let desc = priv.descs[topic_name];
@@ -65289,8 +65465,9 @@ function ac_node_updated(gobj, event, kw, src) {
65289
65465
  update_local_node(gobj, topic_name, node);
65290
65466
  let on_screen = false;
65291
65467
  try {
65292
- on_screen = !!graph.getNodeData(node_name);
65468
+ on_screen = !!priv.graph.getNodeData(node_name);
65293
65469
  } catch (e) {
65470
+ if (e instanceof ReferenceError) throw e;
65294
65471
  on_screen = false;
65295
65472
  }
65296
65473
  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.194",
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) {