@yuneta/gobj-ui 7.25.3 → 7.25.5

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.
@@ -1,4 +1,4 @@
1
- import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_get_gclass_trace_level2, gobj_global_trace_level, gobj_global_trace_no_level, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_is_identical, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_level_t, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
1
+ import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_get_gclass_trace_level2, gobj_global_trace_level, gobj_global_trace_no_level, gobj_has_event, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_is_identical, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_level_t, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
2
2
  import i18next, { t } from "i18next";
3
3
  import TomSelect from "tom-select";
4
4
  import { createJSONEditor } from "vanilla-jsoneditor";
@@ -919,6 +919,7 @@ var PRIVATE_DATA$22 = {
919
919
  avatar_nodes: [],
920
920
  translator: null,
921
921
  conn_nodes: [],
922
+ connected: null,
922
923
  active_dropdown: null
923
924
  };
924
925
  var __gclass__$22 = null;
@@ -2550,11 +2551,28 @@ function ac_nav_item_close(gobj, event, kw, src) {
2550
2551
  /***************************************************************
2551
2552
  * FSM
2552
2553
  ***************************************************************/
2554
+ /***************************************************************
2555
+ * EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
2556
+ * backend answer subscribes to it by name. An app that subscribed
2557
+ * to EVERY event of its shell with one attr (`subscriber`) did not
2558
+ * ask for it and does not declare it -- published to it anyway, the
2559
+ * event would be an FSM error in the app on every connection edge,
2560
+ * and adding it to the shell a breaking change for every such app.
2561
+ * So it reaches only the subscribers that declare it. Every other
2562
+ * event of the shell goes out as before.
2563
+ ***************************************************************/
2564
+ function mt_publication_pre_filter(gobj, subs, event, kw) {
2565
+ if (event !== "EV_CONNECTION_STATE") return 1;
2566
+ let subscriber = subs && subs.subscriber;
2567
+ if (!subscriber || !gobj_has_event(subscriber, event, 0)) return 0;
2568
+ return 1;
2569
+ }
2553
2570
  var gmt$22 = {
2554
2571
  mt_create: mt_create$22,
2555
2572
  mt_start: mt_start$20,
2556
2573
  mt_stop: mt_stop$20,
2557
- mt_destroy: mt_destroy$22
2574
+ mt_destroy: mt_destroy$22,
2575
+ mt_publication_pre_filter
2558
2576
  };
2559
2577
  function create_gclass$22(gclass_name) {
2560
2578
  if (__gclass__$22) {
@@ -2584,7 +2602,8 @@ function create_gclass$22(gclass_name) {
2584
2602
  ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2585
2603
  ["EV_ROUTE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2586
2604
  ["EV_ROUTE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2587
- ["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
2605
+ ["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2606
+ ["EV_CONNECTION_STATE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
2588
2607
  ];
2589
2608
  __gclass__$22 = gclass_create(gclass_name, event_types, states, gmt$22, 0, attrs_table$22, PRIVATE_DATA$22, 0, 0, 0, gclass_flag_t.gcflag_no_check_output_events);
2590
2609
  if (!__gclass__$22) return -1;
@@ -2867,13 +2886,17 @@ function yui_shell_translate(shell_gobj, $el) {
2867
2886
  * is-connected / is-disconnected classes; CSS owns the look.
2868
2887
  ************************************************************/
2869
2888
  function yui_shell_set_connection_state(shell_gobj, connected) {
2889
+ if (!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj)) return;
2870
2890
  let priv = shell_gobj.priv;
2871
- if (!priv || !is_array(priv.conn_nodes)) return;
2891
+ if (!priv) return;
2872
2892
  let on = !!connected;
2873
- for (let $n of priv.conn_nodes) {
2893
+ let changed = priv.connected !== on;
2894
+ priv.connected = on;
2895
+ if (is_array(priv.conn_nodes)) for (let $n of priv.conn_nodes) {
2874
2896
  $n.classList.toggle("is-connected", on);
2875
2897
  $n.classList.toggle("is-disconnected", !on);
2876
2898
  }
2899
+ if (changed) gobj_publish_event(shell_gobj, "EV_CONNECTION_STATE", { connected: on });
2877
2900
  }
2878
2901
  /************************************************************
2879
2902
  * Swap the icon of a toolbar item at runtime (host-driven,
@@ -7314,6 +7337,29 @@ function json_diff_rows(doc_a, doc_b) {
7314
7337
  * them, and they must be each other's inverse: a writable time
7315
7338
  * column travels back on EVERY save of the record.
7316
7339
  *
7340
+ * KNOWN LIMIT -- the change of hour. A `datetime-local` holds a
7341
+ * LOCAL WALL time and no offset, and a wall time is not always
7342
+ * one instant:
7343
+ *
7344
+ * - the night the clocks go back (autumn), one hour happens
7345
+ * twice. "02:30" names two instants an hour apart, and
7346
+ * `new Date("YYYY-MM-DDT02:30:00")` answers the EARLIER one
7347
+ * (ECMAScript's "compatible" disambiguation). An epoch in the
7348
+ * SECOND pass of that hour is shown as 02:30 and comes back
7349
+ * one hour earlier: saving the record -- any field of it --
7350
+ * moves a writable time column that holds it by -3600 s.
7351
+ * - the night the clocks go forward (spring), one hour does not
7352
+ * exist. No epoch is shown in it, so nothing read comes back
7353
+ * wrong; a wall time TYPED in it is moved forward by the
7354
+ * browser.
7355
+ *
7356
+ * Not fixable here without the offset, which the input cannot
7357
+ * carry. What is safe: a read-only time column never goes back
7358
+ * (treedb_write_plan.js, col_goes_back_to_treedb), and a time
7359
+ * pkey2 goes back as the record had it, never through this
7360
+ * widget (instance_keys_of). A writable time column edited or
7361
+ * merely saved inside the repeated hour is the exposed case.
7362
+ *
7317
7363
  * Copyright (c) 2026, ArtGins.
7318
7364
  * All Rights Reserved.
7319
7365
  ****************************************************************************/
@@ -23021,30 +23067,46 @@ function yui_shell_show_route_map(shell, opts) {
23021
23067
  * form_writes_in_flight.js
23022
23068
  *
23023
23069
  * The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
23024
- * and not yet answered, by their `form_write` serial. A form that
23025
- * waits for its answer (form_waits_for_answer) stays open and
23026
- * busy until the host answers, so the host has to answer EVERY
23027
- * way a write ends -- and the transport closing under a write in
23028
- * flight, or a command that never left because there was no
23029
- * session, are two of them that nobody answered: the form stayed
23030
- * busy for ever (N8 of the 2026-09-22 review).
23070
+ * and not yet answered. A form that waits for its answer
23071
+ * (form_waits_for_answer) stays open and busy until the host
23072
+ * answers, so the host has to answer EVERY way a write ends --
23073
+ * and the transport closing under a write in flight, or a
23074
+ * command that never left because there was no session, are two
23075
+ * of them that nobody answered: the form stayed busy for ever
23076
+ * (N8 of the 2026-09-22 review).
23077
+ *
23078
+ * A write is known by its TOPIC and its `form_write` serial,
23079
+ * never by the serial alone: each topic has its own form, and
23080
+ * each form counts its serials from 1. Keyed by the serial, two
23081
+ * forms with a write in flight at once shared the key "1", the
23082
+ * first answer settled both, and the transport closing then
23083
+ * answered only one of them.
23031
23084
  *
23032
23085
  * Copyright (c) 2026, ArtGins.
23033
23086
  * All Rights Reserved.
23034
23087
  ***********************************************************************/
23035
23088
  /************************************************************
23089
+ * The key of one write: `<topic>^<serial>`.
23090
+ ************************************************************/
23091
+ function form_write_key(topic_name, form_write) {
23092
+ return `${topic_name || ""}^${form_write}`;
23093
+ }
23094
+ /************************************************************
23036
23095
  * A write left: remember whose it is
23037
23096
  ************************************************************/
23038
23097
  function track_form_write(in_flight, form_write, topic_name) {
23039
23098
  if (!in_flight || !form_write) return;
23040
- in_flight[form_write] = topic_name || "";
23099
+ in_flight[form_write_key(topic_name, form_write)] = {
23100
+ form_write,
23101
+ topic_name: topic_name || ""
23102
+ };
23041
23103
  }
23042
23104
  /************************************************************
23043
23105
  * A write was answered, one way or the other
23044
23106
  ************************************************************/
23045
- function settle_form_write(in_flight, form_write) {
23107
+ function settle_form_write(in_flight, form_write, topic_name) {
23046
23108
  if (!in_flight || !form_write) return;
23047
- delete in_flight[form_write];
23109
+ delete in_flight[form_write_key(topic_name, form_write)];
23048
23110
  }
23049
23111
  /************************************************************
23050
23112
  * Nothing in flight can be answered any more: take them all,
@@ -23053,12 +23115,13 @@ function settle_form_write(in_flight, form_write) {
23053
23115
  function abandon_form_writes(in_flight) {
23054
23116
  const abandoned = [];
23055
23117
  if (!in_flight) return abandoned;
23056
- Object.keys(in_flight).forEach((serial) => {
23118
+ Object.keys(in_flight).forEach((key) => {
23119
+ const w = in_flight[key];
23057
23120
  abandoned.push({
23058
- form_write: Number(serial),
23059
- topic_name: in_flight[serial]
23121
+ form_write: Number(w.form_write),
23122
+ topic_name: w.topic_name
23060
23123
  });
23061
- delete in_flight[serial];
23124
+ delete in_flight[key];
23062
23125
  });
23063
23126
  return abandoned;
23064
23127
  }
@@ -23630,6 +23693,7 @@ var __gclass__$6 = null;
23630
23693
  ***************************************************************/
23631
23694
  function mt_create$6(gobj) {
23632
23695
  gobj.priv.form_writes = {};
23696
+ gobj.priv.conn_shell = null;
23633
23697
  build_ui$6(gobj);
23634
23698
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
23635
23699
  if (!subscriber) subscriber = gobj_parent(gobj);
@@ -23639,12 +23703,22 @@ function mt_create$6(gobj) {
23639
23703
  * Framework Method: Start
23640
23704
  ***************************************************************/
23641
23705
  function mt_start$6(gobj) {
23706
+ let shell = yui_shell_of(gobj);
23707
+ if (shell) {
23708
+ gobj_subscribe_event(shell, "EV_CONNECTION_STATE", {}, gobj);
23709
+ gobj.priv.conn_shell = shell;
23710
+ }
23642
23711
  request_treedb_descs$1(gobj);
23643
23712
  }
23644
23713
  /***************************************************************
23645
23714
  * Framework Method: Stop
23646
23715
  ***************************************************************/
23647
23716
  function mt_stop$6(gobj) {
23717
+ let priv = gobj.priv;
23718
+ if (priv.conn_shell) {
23719
+ if (!gobj_is_destroying(priv.conn_shell)) gobj_unsubscribe_event(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
23720
+ priv.conn_shell = null;
23721
+ }
23648
23722
  let shell = yui_shell_of(gobj);
23649
23723
  if (shell) yui_shell_set_sub_routes(shell, gobj_read_str_attr(gobj, "base_route"), null);
23650
23724
  close_json_viewer$1(gobj);
@@ -25050,7 +25124,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25050
25124
  if (command === "update-node" || command === "create-node") {
25051
25125
  let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
25052
25126
  if (failed_topic) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
25053
- settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0));
25127
+ settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0), failed_topic);
25054
25128
  answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), kw_get_int(gobj, kw_command, "form_write", 0, 0), false);
25055
25129
  }
25056
25130
  }
@@ -25088,7 +25162,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25088
25162
  created: command === "create-node",
25089
25163
  command
25090
25164
  });
25091
- settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0));
25165
+ settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0), kw_get_str(gobj, kw_command, "topic_name", "", 0));
25092
25166
  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);
25093
25167
  break;
25094
25168
  case "delete-node": break;
@@ -25232,11 +25306,19 @@ function ac_back_to_topics(gobj, event, kw, src) {
25232
25306
  return 0;
25233
25307
  }
25234
25308
  /************************************************************
25235
- * The host (C_TREEDB_VIEW) forwards the backend transport edges here so the
25236
- * toolbar can disable the JSON viewers the moment the session drops (and
25237
- * re-enable them on reconnect) — the library view must not subscribe to the
25238
- * C_IEVENT_CLI itself (that forwards the subscription upstream and breaks
25239
- * the session).
25309
+ * A backend transport edge, from either of two places:
25310
+ * - EV_TRANSPORT_STATE, forwarded by a host that keeps one
25311
+ * transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
25312
+ * C_AGENT_TREEDB_VIEW);
25313
+ * - EV_CONNECTION_STATE, published by the SHELL when the app tells
25314
+ * it its backend went up or down (yui_shell_set_connection_state),
25315
+ * which is what every single-backend app already does for the
25316
+ * toolbar dot. No host has to remember anything for this one.
25317
+ * Both can arrive for one edge; what is done here is idempotent.
25318
+ * The toolbar disables the JSON viewers the moment the session drops
25319
+ * (and re-enables them on reconnect). The library view must not
25320
+ * subscribe to the C_IEVENT_CLI itself: that forwards the subscription
25321
+ * upstream and breaks the session.
25240
25322
  ************************************************************/
25241
25323
  function ac_transport_state$2(gobj, event, kw, src) {
25242
25324
  let connected = !!(kw && kw.connected);
@@ -25768,6 +25850,11 @@ function create_gclass$6(gclass_name) {
25768
25850
  "EV_TRANSPORT_STATE",
25769
25851
  ac_transport_state$2,
25770
25852
  null
25853
+ ],
25854
+ [
25855
+ "EV_CONNECTION_STATE",
25856
+ ac_transport_state$2,
25857
+ null
25771
25858
  ]
25772
25859
  ]]];
25773
25860
  const event_types = [
@@ -25798,6 +25885,7 @@ function create_gclass$6(gclass_name) {
25798
25885
  ["EV_HIDE", 0],
25799
25886
  ["EV_BACK_TO_TOPICS", 0],
25800
25887
  ["EV_TRANSPORT_STATE", 0],
25888
+ ["EV_CONNECTION_STATE", 0],
25801
25889
  ["EV_TOPIC_SELECTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
25802
25890
  ];
25803
25891
  __gclass__$6 = gclass_create(gclass_name, event_types, states, gmt$6, 0, attrs_table$6, PRIVATE_DATA$6, 0, 0, 0, 0);
@@ -26515,6 +26603,23 @@ function col_goes_back_to_treedb(desc, col) {
26515
26603
  if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
26516
26604
  return topic_pkey2s(d).includes(col.id);
26517
26605
  }
26606
+ /************************************************************
26607
+ * The INSTANCE an update of `record` is for: its pkey2 values, as the
26608
+ * record held them when the form was opened, {pkey2: value}.
26609
+ *
26610
+ * They go back as they WERE, never as the form's widget hands them: a
26611
+ * `time` pkey2 is an epoch rendered in a `datetime-local`, and a value
26612
+ * that crossed the widget (seconds dropped, a local wall time read
26613
+ * back, an hour the autumn change names twice) names ANOTHER instance
26614
+ * -- or none, and C_NODE falls back to the primary. The pkey2 is an
26615
+ * address, not a field of the edit.
26616
+ ************************************************************/
26617
+ function instance_keys_of(record, desc) {
26618
+ const keys = {};
26619
+ if (!record || typeof record !== "object") return keys;
26620
+ for (const k of topic_pkey2s(desc || {})) if (Object.prototype.hasOwnProperty.call(record, k) && record[k] !== void 0 && record[k] !== null) keys[k] = record[k];
26621
+ return keys;
26622
+ }
26518
26623
  //#endregion
26519
26624
  //#region src/delete_impact.js
26520
26625
  /***********************************************************************
@@ -28246,6 +28351,7 @@ function open_form_dialog(gobj, mode, record) {
28246
28351
  },
28247
28352
  []
28248
28353
  ]);
28354
+ priv.form_instance = form_mode === "update" ? instance_keys_of(record, desc) : null;
28249
28355
  let form_plan = write_plan(gobj);
28250
28356
  let form = gobj_create_pure_child("form_" + clean_name(gobj_name(gobj)), "C_YUI_FORM", {
28251
28357
  template: build_form_template(gobj),
@@ -28314,6 +28420,7 @@ function teardown_form_child(gobj) {
28314
28420
  priv.reading_files = 0;
28315
28421
  priv.awaiting_write = 0;
28316
28422
  priv.form_busy = { busy: false };
28423
+ priv.form_instance = null;
28317
28424
  if (priv.form) {
28318
28425
  if (gobj_is_running(priv.form)) gobj_stop(priv.form);
28319
28426
  gobj_destroy(priv.form);
@@ -29103,6 +29210,7 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation) {
29103
29210
  switch (field_desc.real_type) {
29104
29211
  case "string":
29105
29212
  if (value && is_date(value)) value = value.toISOString();
29213
+ else if (typeof value === "number" && value > 0) value = (/* @__PURE__ */ new Date(value * 1e3)).toISOString();
29106
29214
  break;
29107
29215
  case "integer":
29108
29216
  if (value && is_date(value)) value = value.getTime() / 1e3;
@@ -29435,7 +29543,10 @@ function ac_confirmed$2(gobj, event, kw, src) {
29435
29543
  let topic_name = gobj_read_str_attr(gobj, "topic_name");
29436
29544
  if (what === "delete_selection" || what === "delete_row") {
29437
29545
  let found = yui_rows_by_ids(tabulator, Array.isArray(kw.ids) ? kw.ids : []);
29438
- if (found.missing.length) log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while confirming the delete, not deleted: ${found.missing.join(", ")}`);
29546
+ if (found.missing.length) {
29547
+ log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while confirming the delete, not deleted: ${found.missing.join(", ")}`);
29548
+ yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", { t });
29549
+ }
29439
29550
  if (!found.rows.length) return -1;
29440
29551
  for (let row of found.rows) gobj_publish_event(gobj, "EV_DELETE_RECORD", {
29441
29552
  topic_name,
@@ -29569,6 +29680,7 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29569
29680
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
29570
29681
  record: strip_read_only_cols(gobj, record)
29571
29682
  };
29683
+ if (mode !== "create" && gobj.priv.form_instance) Object.assign(kw_write.record, gobj.priv.form_instance);
29572
29684
  if (form_write) kw_write.form_write = form_write;
29573
29685
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29574
29686
  gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
@@ -66446,6 +66558,7 @@ function ac_confirmed$1(gobj, event, kw, src) {
66446
66558
  if (what === "delete_selection") {
66447
66559
  let ids = kw && Array.isArray(kw.ids) ? kw.ids : [];
66448
66560
  let nodes = [];
66561
+ let missing = [];
66449
66562
  for (let id of ids) {
66450
66563
  let nd = null;
66451
66564
  try {
@@ -66454,6 +66567,11 @@ function ac_confirmed$1(gobj, event, kw, src) {
66454
66567
  nd = null;
66455
66568
  }
66456
66569
  if (nd && nd.data && nd.data.desc) nodes.push(nd);
66570
+ else missing.push(id);
66571
+ }
66572
+ if (missing.length) {
66573
+ log_error(`${gobj_short_name(gobj)}: ${event}: cards gone while confirming the delete, not deleted: ${missing.join(", ")}`);
66574
+ yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", { t });
66457
66575
  }
66458
66576
  if (!nodes.length) {
66459
66577
  log_error(`${gobj_short_name(gobj)}: ${event} names no node any more`);
@@ -70263,16 +70381,39 @@ function register_c_yui_treedb_schema() {
70263
70381
  * __system__ still differed from the file in use (N13 of the
70264
70382
  * 2026-09-22 review). The host reads that difference from
70265
70383
  * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70266
- * hands it here; the editor applies it to its model.
70384
+ * hands it here; the editor applies it to its model, in place
70385
+ * of what the host said before.
70386
+ *
70387
+ * What `draft_changed` measures depends on the node: the draft
70388
+ * against the SAVED schema on a node newer than yunetas 7.25.3,
70389
+ * and against the file IN USE on a 7.25.3 node -- where a topic saved and
70390
+ * not yet applied is still named, and the editor, which cannot
70391
+ * tell the two apart, shows it as a draft until the Apply.
70267
70392
  *
70268
70393
  * Copyright (c) 2026, ArtGins.
70269
70394
  * All Rights Reserved.
70270
70395
  ***********************************************************************/
70271
70396
  /************************************************************
70397
+ * The topics of `model` the host names as drafts, as
70398
+ * {topic id: true}: `drafts` is {treedb_name: [topic names]}. A
70399
+ * treedb or a topic the model does not hold is ignored.
70400
+ *
70401
+ * What the editor keeps is THIS, apart from what its own session
70402
+ * wrote, and replaced whole on every EV_DRAFTS: the host is the
70403
+ * truth after a reload. Folded into the session's marks, a topic the
70404
+ * host once named stayed a draft after the save that published it.
70405
+ ************************************************************/
70406
+ function host_draft_ids(model, drafts) {
70407
+ const ids = {};
70408
+ mark_host_drafts(ids, model, drafts);
70409
+ return ids;
70410
+ }
70411
+ /************************************************************
70272
70412
  * Mark in `written` the topics of `model` the host names as
70273
70413
  * drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
70274
70414
  * topic the model does not hold is ignored. Returns how many were
70275
- * marked.
70415
+ * marked. It only ADDS: a caller that must forget what the host
70416
+ * no longer names starts from an empty map (host_draft_ids()).
70276
70417
  ************************************************************/
70277
70418
  function mark_host_drafts(written, model, drafts) {
70278
70419
  let marked = 0;
@@ -71911,8 +72052,9 @@ function write_options(op) {
71911
72052
  * the owner's design of M36 (2026-09-21 review) every write here
71912
72053
  * raised both versions, so an edit half made was already the
71913
72054
  * schema of the next start. The topic list marks what this session
71914
- * wrote and has not saved; the host's EV_REFRESH after a save is
71915
- * what forgets it.
72055
+ * wrote and has not saved (the host's EV_REFRESH after a save is
72056
+ * what forgets it) and what the host says is not saved (EV_DRAFTS,
72057
+ * replaced whole each time it is sent).
71916
72058
  *
71917
72059
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
71918
72060
  *
@@ -71991,6 +72133,8 @@ var PRIVATE_DATA$1 = {
71991
72133
  diagram: false,
71992
72134
  baseline: null,
71993
72135
  written: null,
72136
+ host_drafts: null,
72137
+ host_ids: null,
71994
72138
  order_undo: null,
71995
72139
  diagram_gobj: null,
71996
72140
  dialog: null,
@@ -72015,6 +72159,7 @@ function mt_create$1(gobj) {
72015
72159
  priv.baseline = {};
72016
72160
  priv.written = {};
72017
72161
  priv.host_drafts = {};
72162
+ priv.host_ids = {};
72018
72163
  priv.save_queue = [];
72019
72164
  build_ui$1(gobj);
72020
72165
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
@@ -72254,25 +72399,33 @@ function start_measuring(gobj) {
72254
72399
  let priv = gobj.priv;
72255
72400
  priv.baseline = {};
72256
72401
  priv.written = {};
72402
+ priv.host_ids = {};
72257
72403
  if (!priv.model) return;
72258
72404
  for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
72259
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
72405
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
72260
72406
  }
72261
72407
  /***************************************************************
72262
- * "This topic was written in this session and is not saved."
72408
+ * "This topic holds an edit that is not saved."
72263
72409
  *
72264
72410
  * An edit is a DRAFT: it moves no version and reaches no treedb
72265
72411
  * until the host saves it (C_TREEDB's save-schema, which raises the
72266
- * versions of what changed) -- and the save makes the host send
72267
- * EV_REFRESH, whose reload is what forgets the drafts.
72412
+ * versions of what changed). Two sources say a topic is one, and
72413
+ * they are kept APART because they are forgotten differently:
72414
+ * - `written`: what THIS session wrote since the model was loaded
72415
+ * (a reload, the host's EV_REFRESH, forgets it -- the store says
72416
+ * what is left);
72417
+ * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
72418
+ * by the next EV_DRAFTS. Folded into `written`, a topic the host
72419
+ * named once stayed a draft after the Save that published it.
72268
72420
  *
72269
72421
  * ONE predicate, because it is asked in three places -- the topic
72270
72422
  * list draws a chip with it, the column view a banner, the export
72271
72423
  * a warning -- and copies of a rule are how they drift.
72272
72424
  ***************************************************************/
72273
- function topic_is_draft(written, topic) {
72274
- if (!topic || !written) return false;
72275
- return written[topic.id] ? true : false;
72425
+ function topic_is_draft(priv, topic) {
72426
+ if (!topic || !priv) return false;
72427
+ if (priv.written && priv.written[topic.id]) return true;
72428
+ return priv.host_ids && priv.host_ids[topic.id] ? true : false;
72276
72429
  }
72277
72430
  /***************************************************************
72278
72431
  * Say what the whole store would be refused for.
@@ -72714,7 +72867,7 @@ function render_topics(gobj, $body) {
72714
72867
  let twice = repeated_names(treedb.topics);
72715
72868
  let $rows = [];
72716
72869
  for (let topic of treedb.topics) {
72717
- let pending = topic_is_draft(priv.written, topic);
72870
+ let pending = topic_is_draft(priv, topic);
72718
72871
  let ambiguous = !!twice[topic.name];
72719
72872
  let address = ambiguous ? topic.id : topic.name;
72720
72873
  let $actions = [];
@@ -73037,7 +73190,7 @@ function render_columns(gobj, $body) {
73037
73190
  ***************************************************************/
73038
73191
  function draft_banner(gobj, topic) {
73039
73192
  let priv = gobj.priv;
73040
- if (!topic_is_draft(priv.written, topic)) return null;
73193
+ if (!topic_is_draft(priv, topic)) return null;
73041
73194
  if (is_readonly(gobj)) return null;
73042
73195
  return createElement2([
73043
73196
  "div",
@@ -73927,7 +74080,7 @@ function open_report(gobj, findings, treedb_id) {
73927
74080
  ***************************************************************/
73928
74081
  function open_export(gobj, treedb) {
73929
74082
  let priv = gobj.priv;
73930
- let drafts = treedb.topics.some((topic) => topic_is_draft(priv.written, topic));
74083
+ let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
73931
74084
  let c_text = schema_to_c(treedb);
73932
74085
  let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
73933
74086
  let $content = createElement2([
@@ -74695,18 +74848,22 @@ function ac_refresh$1(gobj, event, kw, src) {
74695
74848
  }
74696
74849
  /***************************************************************
74697
74850
  * {drafts: {treedb_name: [topic names]}} -- the host says which
74698
- * topics hold a draft in __system__ that the file in use does not
74699
- * have (C_TREEDB's saved-schema, `draft_changed`). The mark of a
74700
- * write lived in this session's memory only, so a reload of the
74701
- * page, a reconnect or a refresh showed no draft while __system__
74702
- * still differed from the file (N13 of the 2026-09-22 review).
74703
- * Replaces what the host said before; what this session wrote
74704
- * stays marked.
74851
+ * topics hold a draft in __system__ that is not saved (C_TREEDB's
74852
+ * saved-schema, `draft_changed`). The mark of a write lived in this
74853
+ * session's memory only, so a reload of the page, a reconnect or a
74854
+ * refresh showed no draft while __system__ still differed (N13 of
74855
+ * the 2026-09-22 review).
74856
+ *
74857
+ * REPLACES what the host said before -- the host is the truth after
74858
+ * a reload, and a Save is exactly the host saying "none any more".
74859
+ * It only ever added, and a topic stayed a draft after the Save that
74860
+ * published it (M1 of the 2026-09-23 review). What this session wrote
74861
+ * stays marked: it is kept apart (see topic_is_draft()).
74705
74862
  ***************************************************************/
74706
74863
  function ac_drafts(gobj, event, kw, src) {
74707
74864
  let priv = gobj.priv;
74708
74865
  priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
74709
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
74866
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
74710
74867
  if (priv.model) render(gobj);
74711
74868
  return 0;
74712
74869
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.25.3",
3
+ "version": "7.25.5",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -174,6 +174,7 @@ import {
174
174
  } from "./g6_touch_gestures.js";
175
175
  import {yui_theme_now, yui_watch_theme} from "./yui_theme.js";
176
176
  import {yui_shell_of} from "./c_yui_shell.js";
177
+ import {yui_shell_show_error} from "./shell_modals.js";
177
178
 
178
179
  /***************************************************************
179
180
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
@@ -11761,6 +11762,7 @@ function ac_confirmed(gobj, event, kw, src)
11761
11762
  if(what === "delete_selection") {
11762
11763
  let ids = (kw && Array.isArray(kw.ids))? kw.ids : [];
11763
11764
  let nodes = [];
11765
+ let missing = [];
11764
11766
  for(let id of ids) {
11765
11767
  let nd = null;
11766
11768
  try {
@@ -11770,8 +11772,18 @@ function ac_confirmed(gobj, event, kw, src)
11770
11772
  }
11771
11773
  if(nd && nd.data && nd.data.desc) {
11772
11774
  nodes.push(nd);
11775
+ } else {
11776
+ missing.push(id);
11773
11777
  }
11774
11778
  }
11779
+ if(missing.length) {
11780
+ /* Gone while the question was open (another writer deleted
11781
+ * them): said, to the log and to the person (a low of the
11782
+ * 2026-09-22 review); the rest go on. */
11783
+ log_error(`${gobj_short_name(gobj)}: ${event}: cards gone while ` +
11784
+ `confirming the delete, not deleted: ${missing.join(", ")}`);
11785
+ yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", {t: t});
11786
+ }
11775
11787
  if(!nodes.length) {
11776
11788
  log_error(`${gobj_short_name(gobj)}: ${event} names no node any more`);
11777
11789
  return -1;