@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.
package/README.md CHANGED
@@ -790,6 +790,49 @@ writes in flight when the transport closes are answered refused on the
790
790
  transport edge (`form_writes_in_flight.js`), so the form comes back on what
791
791
  was typed instead of staying busy for ever.
792
792
 
793
+ **Where the transport edge comes from** (7.25.5). Until 7.25.4 only from the
794
+ host, as `EV_TRANSPORT_STATE` — and two hosts (wattyzer, yunovatios) never
795
+ forwarded it, so a websocket drop during a Save left Save and Cancel busy for
796
+ ever. The view now also hears it from its SHELL: `yui_shell_set_connection_state()`
797
+ — which every single-backend app already calls for the toolbar dot — publishes
798
+ `EV_CONNECTION_STATE {connected}` on each edge, and `C_YUI_TREEDB_TOPICS`
799
+ subscribes to it in `mt_start`. Nothing to add in such a host. A host with
800
+ one transport per view (gui_treedb's `C_TREEDB_VIEW`, gui_agent's
801
+ `C_AGENT_TREEDB_VIEW`) keeps forwarding `EV_TRANSPORT_STATE`; both paths can
802
+ fire for one edge, and the view answers each write once. The view never
803
+ subscribes to the transport itself: a subscription to a `C_IEVENT_CLI` is
804
+ forwarded to the backend.
805
+
806
+ `EV_CONNECTION_STATE` reaches only the subscribers that DECLARE it: an app
807
+ that subscribed to every event of its shell with the `subscriber` attr does
808
+ not get it (the shell's `mt_publication_pre_filter`), so adding it broke no
809
+ app.
810
+
811
+ ```js
812
+ // The app, from its transport handlers -- as it already did for the dot:
813
+ yui_shell_set_connection_state(shell, false); // EV_ON_CLOSE
814
+ // -> every C_YUI_TREEDB_TOPICS under that shell answers its forms'
815
+ // writes in flight EV_WRITE_REFUSED, and they come back on what was typed.
816
+ ```
817
+
818
+ A write is known by its topic AND its serial (`<topic>^<form_write>`): each
819
+ topic's form counts its serials from 1, and keyed by the serial alone two
820
+ forms saving at once shared one entry.
821
+
822
+ **The pkey2 of an update goes back as the record had it** (7.25.5). It names
823
+ the instance the write is for, and a `time` pkey2 that crossed the form's
824
+ `datetime-local` came back without its seconds — another instance, or none.
825
+ `instance_keys_of()` (`treedb_write_plan.js`) keeps it from the record the
826
+ form was opened on.
827
+
828
+ **Known limit — the change of hour** (`form_time_value.js`). A
829
+ `datetime-local` holds a local wall time and no offset. The night the clocks
830
+ go back, one hour happens twice: an epoch in its second pass is shown with the
831
+ wall time of the first and read back one hour earlier, so saving that record —
832
+ any field of it — moves a WRITABLE time column holding it by -3600 s. The
833
+ hour skipped in spring holds no epoch, so nothing read there comes back wrong.
834
+ Read-only time columns never go back, and a time pkey2 goes back as it was.
835
+
793
836
  ```js
794
837
  // A host of C_YUI_TREEDB_TOPIC_WITH_FORM that answers the form's writes
795
838
  gobj_create_pure_child(name, "C_YUI_TREEDB_TOPIC_WITH_FORM",
@@ -802,7 +845,24 @@ gobj_send_event(topic_form, ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED",
802
845
  **A delete is resolved by IDENTITY, never by position** (7.23.193). The row's
803
846
  `id` crosses the confirmation dialog; a Tabulator position is regenerated by
804
847
  every row the node events add or remove while the dialog is open, and it used
805
- to delete the row that sat there when the person answered.
848
+ to delete the row that sat there when the person answered. Rows (table) or
849
+ cards (`C_G6_NODES_TREE`) that another writer deleted while the question was
850
+ open are not deleted and the person is told (7.25.4); the rest go on.
851
+
852
+ Consumer i18n keys of the form's writes and deletes — define them in the app,
853
+ or they render as the key itself in every language:
854
+
855
+ | key | shown when |
856
+ |---|---|
857
+ | `no session` | a Save asked with no backend session: the write is not sent (7.25.2) |
858
+ | `some records were gone before the delete` | a confirmed delete named rows or cards that no longer exist (7.25.4). The text must not promise a refresh: nothing is reloaded, and in the graph there is no table |
859
+
860
+ ```js
861
+ // en.js / es.js of the app
862
+ "no session": "No session",
863
+ "some records were gone before the delete":
864
+ "Some records no longer existed when the delete was confirmed; they were not deleted",
865
+ ```
806
866
 
807
867
  ### Reading a topic table: filters, columns, CSV
808
868
 
@@ -2030,6 +2090,17 @@ host sends it as `EV_DRAFTS`; `drafts_of_saved_answer()` reads the unnamed
2030
2090
  answer into the shape the event takes. What the session wrote stays marked;
2031
2091
  what the host said is applied again after every reload of the model.
2032
2092
 
2093
+ **`EV_DRAFTS` REPLACES what the host said before** (7.25.5). It only added,
2094
+ and a topic the host had named before a Save stayed a draft after the Save
2095
+ that published it ("unsaved schema changes" again). The editor keeps the
2096
+ host's marks apart from its session's writes (`host_draft_ids()`) and swaps
2097
+ them whole on each `EV_DRAFTS`, so a host sends the COMPLETE set every time —
2098
+ an empty `{drafts: {}}` included, which is how "nothing is a draft any more"
2099
+ is said. What `draft_changed` measures is the node's: the draft against the
2100
+ SAVED schema on a node newer than yunetas 7.25.3, against the file in use on
2101
+ a 7.25.3 node (where a topic saved and not yet applied is still a draft until
2102
+ the Apply).
2103
+
2033
2104
  ```js
2034
2105
  // The host, when its saved-schema answers land (rows = the unnamed answer's data)
2035
2106
  import {drafts_of_saved_answer} from "@yuneta/gobj-ui/src/host_drafts.js";
@@ -924,6 +924,7 @@ var PRIVATE_DATA$22 = {
924
924
  avatar_nodes: [],
925
925
  translator: null,
926
926
  conn_nodes: [],
927
+ connected: null,
927
928
  active_dropdown: null
928
929
  };
929
930
  var __gclass__$22 = null;
@@ -2555,11 +2556,28 @@ function ac_nav_item_close(gobj, event, kw, src) {
2555
2556
  /***************************************************************
2556
2557
  * FSM
2557
2558
  ***************************************************************/
2559
+ /***************************************************************
2560
+ * EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
2561
+ * backend answer subscribes to it by name. An app that subscribed
2562
+ * to EVERY event of its shell with one attr (`subscriber`) did not
2563
+ * ask for it and does not declare it -- published to it anyway, the
2564
+ * event would be an FSM error in the app on every connection edge,
2565
+ * and adding it to the shell a breaking change for every such app.
2566
+ * So it reaches only the subscribers that declare it. Every other
2567
+ * event of the shell goes out as before.
2568
+ ***************************************************************/
2569
+ function mt_publication_pre_filter(gobj, subs, event, kw) {
2570
+ if (event !== "EV_CONNECTION_STATE") return 1;
2571
+ let subscriber = subs && subs.subscriber;
2572
+ if (!subscriber || !(0, _yuneta_gobj_js.gobj_has_event)(subscriber, event, 0)) return 0;
2573
+ return 1;
2574
+ }
2558
2575
  var gmt$22 = {
2559
2576
  mt_create: mt_create$22,
2560
2577
  mt_start: mt_start$20,
2561
2578
  mt_stop: mt_stop$20,
2562
- mt_destroy: mt_destroy$22
2579
+ mt_destroy: mt_destroy$22,
2580
+ mt_publication_pre_filter
2563
2581
  };
2564
2582
  function create_gclass$22(gclass_name) {
2565
2583
  if (__gclass__$22) {
@@ -2589,7 +2607,8 @@ function create_gclass$22(gclass_name) {
2589
2607
  ["EV_NAV_ITEM_CLOSE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2590
2608
  ["EV_ROUTE_REQUESTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2591
2609
  ["EV_ROUTE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2592
- ["EV_LANGUAGE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
2610
+ ["EV_LANGUAGE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
2611
+ ["EV_CONNECTION_STATE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
2593
2612
  ];
2594
2613
  __gclass__$22 = (0, _yuneta_gobj_js.gclass_create)(gclass_name, event_types, states, gmt$22, 0, attrs_table$22, PRIVATE_DATA$22, 0, 0, 0, _yuneta_gobj_js.gclass_flag_t.gcflag_no_check_output_events);
2595
2614
  if (!__gclass__$22) return -1;
@@ -2872,13 +2891,17 @@ function yui_shell_translate(shell_gobj, $el) {
2872
2891
  * is-connected / is-disconnected classes; CSS owns the look.
2873
2892
  ************************************************************/
2874
2893
  function yui_shell_set_connection_state(shell_gobj, connected) {
2894
+ if (!shell_gobj || !(0, _yuneta_gobj_js.is_gobj)(shell_gobj) || (0, _yuneta_gobj_js.gobj_is_destroying)(shell_gobj)) return;
2875
2895
  let priv = shell_gobj.priv;
2876
- if (!priv || !(0, _yuneta_gobj_js.is_array)(priv.conn_nodes)) return;
2896
+ if (!priv) return;
2877
2897
  let on = !!connected;
2878
- for (let $n of priv.conn_nodes) {
2898
+ let changed = priv.connected !== on;
2899
+ priv.connected = on;
2900
+ if ((0, _yuneta_gobj_js.is_array)(priv.conn_nodes)) for (let $n of priv.conn_nodes) {
2879
2901
  $n.classList.toggle("is-connected", on);
2880
2902
  $n.classList.toggle("is-disconnected", !on);
2881
2903
  }
2904
+ if (changed) (0, _yuneta_gobj_js.gobj_publish_event)(shell_gobj, "EV_CONNECTION_STATE", { connected: on });
2882
2905
  }
2883
2906
  /************************************************************
2884
2907
  * Swap the icon of a toolbar item at runtime (host-driven,
@@ -7319,6 +7342,29 @@ function json_diff_rows(doc_a, doc_b) {
7319
7342
  * them, and they must be each other's inverse: a writable time
7320
7343
  * column travels back on EVERY save of the record.
7321
7344
  *
7345
+ * KNOWN LIMIT -- the change of hour. A `datetime-local` holds a
7346
+ * LOCAL WALL time and no offset, and a wall time is not always
7347
+ * one instant:
7348
+ *
7349
+ * - the night the clocks go back (autumn), one hour happens
7350
+ * twice. "02:30" names two instants an hour apart, and
7351
+ * `new Date("YYYY-MM-DDT02:30:00")` answers the EARLIER one
7352
+ * (ECMAScript's "compatible" disambiguation). An epoch in the
7353
+ * SECOND pass of that hour is shown as 02:30 and comes back
7354
+ * one hour earlier: saving the record -- any field of it --
7355
+ * moves a writable time column that holds it by -3600 s.
7356
+ * - the night the clocks go forward (spring), one hour does not
7357
+ * exist. No epoch is shown in it, so nothing read comes back
7358
+ * wrong; a wall time TYPED in it is moved forward by the
7359
+ * browser.
7360
+ *
7361
+ * Not fixable here without the offset, which the input cannot
7362
+ * carry. What is safe: a read-only time column never goes back
7363
+ * (treedb_write_plan.js, col_goes_back_to_treedb), and a time
7364
+ * pkey2 goes back as the record had it, never through this
7365
+ * widget (instance_keys_of). A writable time column edited or
7366
+ * merely saved inside the repeated hour is the exposed case.
7367
+ *
7322
7368
  * Copyright (c) 2026, ArtGins.
7323
7369
  * All Rights Reserved.
7324
7370
  ****************************************************************************/
@@ -23026,30 +23072,46 @@ function yui_shell_show_route_map(shell, opts) {
23026
23072
  * form_writes_in_flight.js
23027
23073
  *
23028
23074
  * The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
23029
- * and not yet answered, by their `form_write` serial. A form that
23030
- * waits for its answer (form_waits_for_answer) stays open and
23031
- * busy until the host answers, so the host has to answer EVERY
23032
- * way a write ends -- and the transport closing under a write in
23033
- * flight, or a command that never left because there was no
23034
- * session, are two of them that nobody answered: the form stayed
23035
- * busy for ever (N8 of the 2026-09-22 review).
23075
+ * and not yet answered. A form that waits for its answer
23076
+ * (form_waits_for_answer) stays open and busy until the host
23077
+ * answers, so the host has to answer EVERY way a write ends --
23078
+ * and the transport closing under a write in flight, or a
23079
+ * command that never left because there was no session, are two
23080
+ * of them that nobody answered: the form stayed busy for ever
23081
+ * (N8 of the 2026-09-22 review).
23082
+ *
23083
+ * A write is known by its TOPIC and its `form_write` serial,
23084
+ * never by the serial alone: each topic has its own form, and
23085
+ * each form counts its serials from 1. Keyed by the serial, two
23086
+ * forms with a write in flight at once shared the key "1", the
23087
+ * first answer settled both, and the transport closing then
23088
+ * answered only one of them.
23036
23089
  *
23037
23090
  * Copyright (c) 2026, ArtGins.
23038
23091
  * All Rights Reserved.
23039
23092
  ***********************************************************************/
23040
23093
  /************************************************************
23094
+ * The key of one write: `<topic>^<serial>`.
23095
+ ************************************************************/
23096
+ function form_write_key(topic_name, form_write) {
23097
+ return `${topic_name || ""}^${form_write}`;
23098
+ }
23099
+ /************************************************************
23041
23100
  * A write left: remember whose it is
23042
23101
  ************************************************************/
23043
23102
  function track_form_write(in_flight, form_write, topic_name) {
23044
23103
  if (!in_flight || !form_write) return;
23045
- in_flight[form_write] = topic_name || "";
23104
+ in_flight[form_write_key(topic_name, form_write)] = {
23105
+ form_write,
23106
+ topic_name: topic_name || ""
23107
+ };
23046
23108
  }
23047
23109
  /************************************************************
23048
23110
  * A write was answered, one way or the other
23049
23111
  ************************************************************/
23050
- function settle_form_write(in_flight, form_write) {
23112
+ function settle_form_write(in_flight, form_write, topic_name) {
23051
23113
  if (!in_flight || !form_write) return;
23052
- delete in_flight[form_write];
23114
+ delete in_flight[form_write_key(topic_name, form_write)];
23053
23115
  }
23054
23116
  /************************************************************
23055
23117
  * Nothing in flight can be answered any more: take them all,
@@ -23058,12 +23120,13 @@ function settle_form_write(in_flight, form_write) {
23058
23120
  function abandon_form_writes(in_flight) {
23059
23121
  const abandoned = [];
23060
23122
  if (!in_flight) return abandoned;
23061
- Object.keys(in_flight).forEach((serial) => {
23123
+ Object.keys(in_flight).forEach((key) => {
23124
+ const w = in_flight[key];
23062
23125
  abandoned.push({
23063
- form_write: Number(serial),
23064
- topic_name: in_flight[serial]
23126
+ form_write: Number(w.form_write),
23127
+ topic_name: w.topic_name
23065
23128
  });
23066
- delete in_flight[serial];
23129
+ delete in_flight[key];
23067
23130
  });
23068
23131
  return abandoned;
23069
23132
  }
@@ -23635,6 +23698,7 @@ var __gclass__$6 = null;
23635
23698
  ***************************************************************/
23636
23699
  function mt_create$6(gobj) {
23637
23700
  gobj.priv.form_writes = {};
23701
+ gobj.priv.conn_shell = null;
23638
23702
  build_ui$6(gobj);
23639
23703
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
23640
23704
  if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
@@ -23644,12 +23708,22 @@ function mt_create$6(gobj) {
23644
23708
  * Framework Method: Start
23645
23709
  ***************************************************************/
23646
23710
  function mt_start$6(gobj) {
23711
+ let shell = yui_shell_of(gobj);
23712
+ if (shell) {
23713
+ (0, _yuneta_gobj_js.gobj_subscribe_event)(shell, "EV_CONNECTION_STATE", {}, gobj);
23714
+ gobj.priv.conn_shell = shell;
23715
+ }
23647
23716
  request_treedb_descs$1(gobj);
23648
23717
  }
23649
23718
  /***************************************************************
23650
23719
  * Framework Method: Stop
23651
23720
  ***************************************************************/
23652
23721
  function mt_stop$6(gobj) {
23722
+ let priv = gobj.priv;
23723
+ if (priv.conn_shell) {
23724
+ if (!(0, _yuneta_gobj_js.gobj_is_destroying)(priv.conn_shell)) (0, _yuneta_gobj_js.gobj_unsubscribe_event)(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
23725
+ priv.conn_shell = null;
23726
+ }
23653
23727
  let shell = yui_shell_of(gobj);
23654
23728
  if (shell) yui_shell_set_sub_routes(shell, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "base_route"), null);
23655
23729
  close_json_viewer$1(gobj);
@@ -25055,7 +25129,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25055
25129
  if (command === "update-node" || command === "create-node") {
25056
25130
  let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
25057
25131
  if (failed_topic) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
25058
- settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0));
25132
+ settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), failed_topic);
25059
25133
  answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), false);
25060
25134
  }
25061
25135
  }
@@ -25093,7 +25167,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25093
25167
  created: command === "create-node",
25094
25168
  command
25095
25169
  });
25096
- settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0));
25170
+ settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0));
25097
25171
  answer_form_write(gobj, get_gobj_formtable(gobj, (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0)), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), true);
25098
25172
  break;
25099
25173
  case "delete-node": break;
@@ -25237,11 +25311,19 @@ function ac_back_to_topics(gobj, event, kw, src) {
25237
25311
  return 0;
25238
25312
  }
25239
25313
  /************************************************************
25240
- * The host (C_TREEDB_VIEW) forwards the backend transport edges here so the
25241
- * toolbar can disable the JSON viewers the moment the session drops (and
25242
- * re-enable them on reconnect) — the library view must not subscribe to the
25243
- * C_IEVENT_CLI itself (that forwards the subscription upstream and breaks
25244
- * the session).
25314
+ * A backend transport edge, from either of two places:
25315
+ * - EV_TRANSPORT_STATE, forwarded by a host that keeps one
25316
+ * transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
25317
+ * C_AGENT_TREEDB_VIEW);
25318
+ * - EV_CONNECTION_STATE, published by the SHELL when the app tells
25319
+ * it its backend went up or down (yui_shell_set_connection_state),
25320
+ * which is what every single-backend app already does for the
25321
+ * toolbar dot. No host has to remember anything for this one.
25322
+ * Both can arrive for one edge; what is done here is idempotent.
25323
+ * The toolbar disables the JSON viewers the moment the session drops
25324
+ * (and re-enables them on reconnect). The library view must not
25325
+ * subscribe to the C_IEVENT_CLI itself: that forwards the subscription
25326
+ * upstream and breaks the session.
25245
25327
  ************************************************************/
25246
25328
  function ac_transport_state$2(gobj, event, kw, src) {
25247
25329
  let connected = !!(kw && kw.connected);
@@ -25773,6 +25855,11 @@ function create_gclass$6(gclass_name) {
25773
25855
  "EV_TRANSPORT_STATE",
25774
25856
  ac_transport_state$2,
25775
25857
  null
25858
+ ],
25859
+ [
25860
+ "EV_CONNECTION_STATE",
25861
+ ac_transport_state$2,
25862
+ null
25776
25863
  ]
25777
25864
  ]]];
25778
25865
  const event_types = [
@@ -25803,6 +25890,7 @@ function create_gclass$6(gclass_name) {
25803
25890
  ["EV_HIDE", 0],
25804
25891
  ["EV_BACK_TO_TOPICS", 0],
25805
25892
  ["EV_TRANSPORT_STATE", 0],
25893
+ ["EV_CONNECTION_STATE", 0],
25806
25894
  ["EV_TOPIC_SELECTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
25807
25895
  ];
25808
25896
  __gclass__$6 = (0, _yuneta_gobj_js.gclass_create)(gclass_name, event_types, states, gmt$6, 0, attrs_table$6, PRIVATE_DATA$6, 0, 0, 0, 0);
@@ -26520,6 +26608,23 @@ function col_goes_back_to_treedb(desc, col) {
26520
26608
  if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
26521
26609
  return topic_pkey2s(d).includes(col.id);
26522
26610
  }
26611
+ /************************************************************
26612
+ * The INSTANCE an update of `record` is for: its pkey2 values, as the
26613
+ * record held them when the form was opened, {pkey2: value}.
26614
+ *
26615
+ * They go back as they WERE, never as the form's widget hands them: a
26616
+ * `time` pkey2 is an epoch rendered in a `datetime-local`, and a value
26617
+ * that crossed the widget (seconds dropped, a local wall time read
26618
+ * back, an hour the autumn change names twice) names ANOTHER instance
26619
+ * -- or none, and C_NODE falls back to the primary. The pkey2 is an
26620
+ * address, not a field of the edit.
26621
+ ************************************************************/
26622
+ function instance_keys_of(record, desc) {
26623
+ const keys = {};
26624
+ if (!record || typeof record !== "object") return keys;
26625
+ 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];
26626
+ return keys;
26627
+ }
26523
26628
  //#endregion
26524
26629
  //#region src/delete_impact.js
26525
26630
  /***********************************************************************
@@ -28251,6 +28356,7 @@ function open_form_dialog(gobj, mode, record) {
28251
28356
  },
28252
28357
  []
28253
28358
  ]);
28359
+ priv.form_instance = form_mode === "update" ? instance_keys_of(record, desc) : null;
28254
28360
  let form_plan = write_plan(gobj);
28255
28361
  let form = (0, _yuneta_gobj_js.gobj_create_pure_child)("form_" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)), "C_YUI_FORM", {
28256
28362
  template: build_form_template(gobj),
@@ -28319,6 +28425,7 @@ function teardown_form_child(gobj) {
28319
28425
  priv.reading_files = 0;
28320
28426
  priv.awaiting_write = 0;
28321
28427
  priv.form_busy = { busy: false };
28428
+ priv.form_instance = null;
28322
28429
  if (priv.form) {
28323
28430
  if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
28324
28431
  (0, _yuneta_gobj_js.gobj_destroy)(priv.form);
@@ -29108,6 +29215,7 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation) {
29108
29215
  switch (field_desc.real_type) {
29109
29216
  case "string":
29110
29217
  if (value && (0, _yuneta_gobj_js.is_date)(value)) value = value.toISOString();
29218
+ else if (typeof value === "number" && value > 0) value = (/* @__PURE__ */ new Date(value * 1e3)).toISOString();
29111
29219
  break;
29112
29220
  case "integer":
29113
29221
  if (value && (0, _yuneta_gobj_js.is_date)(value)) value = value.getTime() / 1e3;
@@ -29440,7 +29548,10 @@ function ac_confirmed$2(gobj, event, kw, src) {
29440
29548
  let topic_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name");
29441
29549
  if (what === "delete_selection" || what === "delete_row") {
29442
29550
  let found = yui_rows_by_ids(tabulator, Array.isArray(kw.ids) ? kw.ids : []);
29443
- if (found.missing.length) (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event}: rows gone while confirming the delete, not deleted: ${found.missing.join(", ")}`);
29551
+ if (found.missing.length) {
29552
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event}: rows gone while confirming the delete, not deleted: ${found.missing.join(", ")}`);
29553
+ yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", { t: i18next.t });
29554
+ }
29444
29555
  if (!found.rows.length) return -1;
29445
29556
  for (let row of found.rows) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_DELETE_RECORD", {
29446
29557
  topic_name,
@@ -29574,6 +29685,7 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29574
29685
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
29575
29686
  record: strip_read_only_cols(gobj, record)
29576
29687
  };
29688
+ if (mode !== "create" && gobj.priv.form_instance) Object.assign(kw_write.record, gobj.priv.form_instance);
29577
29689
  if (form_write) kw_write.form_write = form_write;
29578
29690
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29579
29691
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
@@ -66451,6 +66563,7 @@ function ac_confirmed$1(gobj, event, kw, src) {
66451
66563
  if (what === "delete_selection") {
66452
66564
  let ids = kw && Array.isArray(kw.ids) ? kw.ids : [];
66453
66565
  let nodes = [];
66566
+ let missing = [];
66454
66567
  for (let id of ids) {
66455
66568
  let nd = null;
66456
66569
  try {
@@ -66459,6 +66572,11 @@ function ac_confirmed$1(gobj, event, kw, src) {
66459
66572
  nd = null;
66460
66573
  }
66461
66574
  if (nd && nd.data && nd.data.desc) nodes.push(nd);
66575
+ else missing.push(id);
66576
+ }
66577
+ if (missing.length) {
66578
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event}: cards gone while confirming the delete, not deleted: ${missing.join(", ")}`);
66579
+ yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", { t: i18next.t });
66462
66580
  }
66463
66581
  if (!nodes.length) {
66464
66582
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} names no node any more`);
@@ -70268,16 +70386,39 @@ function register_c_yui_treedb_schema() {
70268
70386
  * __system__ still differed from the file in use (N13 of the
70269
70387
  * 2026-09-22 review). The host reads that difference from
70270
70388
  * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70271
- * hands it here; the editor applies it to its model.
70389
+ * hands it here; the editor applies it to its model, in place
70390
+ * of what the host said before.
70391
+ *
70392
+ * What `draft_changed` measures depends on the node: the draft
70393
+ * against the SAVED schema on a node newer than yunetas 7.25.3,
70394
+ * and against the file IN USE on a 7.25.3 node -- where a topic saved and
70395
+ * not yet applied is still named, and the editor, which cannot
70396
+ * tell the two apart, shows it as a draft until the Apply.
70272
70397
  *
70273
70398
  * Copyright (c) 2026, ArtGins.
70274
70399
  * All Rights Reserved.
70275
70400
  ***********************************************************************/
70276
70401
  /************************************************************
70402
+ * The topics of `model` the host names as drafts, as
70403
+ * {topic id: true}: `drafts` is {treedb_name: [topic names]}. A
70404
+ * treedb or a topic the model does not hold is ignored.
70405
+ *
70406
+ * What the editor keeps is THIS, apart from what its own session
70407
+ * wrote, and replaced whole on every EV_DRAFTS: the host is the
70408
+ * truth after a reload. Folded into the session's marks, a topic the
70409
+ * host once named stayed a draft after the save that published it.
70410
+ ************************************************************/
70411
+ function host_draft_ids(model, drafts) {
70412
+ const ids = {};
70413
+ mark_host_drafts(ids, model, drafts);
70414
+ return ids;
70415
+ }
70416
+ /************************************************************
70277
70417
  * Mark in `written` the topics of `model` the host names as
70278
70418
  * drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
70279
70419
  * topic the model does not hold is ignored. Returns how many were
70280
- * marked.
70420
+ * marked. It only ADDS: a caller that must forget what the host
70421
+ * no longer names starts from an empty map (host_draft_ids()).
70281
70422
  ************************************************************/
70282
70423
  function mark_host_drafts(written, model, drafts) {
70283
70424
  let marked = 0;
@@ -71916,8 +72057,9 @@ function write_options(op) {
71916
72057
  * the owner's design of M36 (2026-09-21 review) every write here
71917
72058
  * raised both versions, so an edit half made was already the
71918
72059
  * schema of the next start. The topic list marks what this session
71919
- * wrote and has not saved; the host's EV_REFRESH after a save is
71920
- * what forgets it.
72060
+ * wrote and has not saved (the host's EV_REFRESH after a save is
72061
+ * what forgets it) and what the host says is not saved (EV_DRAFTS,
72062
+ * replaced whole each time it is sent).
71921
72063
  *
71922
72064
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
71923
72065
  *
@@ -71996,6 +72138,8 @@ var PRIVATE_DATA$1 = {
71996
72138
  diagram: false,
71997
72139
  baseline: null,
71998
72140
  written: null,
72141
+ host_drafts: null,
72142
+ host_ids: null,
71999
72143
  order_undo: null,
72000
72144
  diagram_gobj: null,
72001
72145
  dialog: null,
@@ -72020,6 +72164,7 @@ function mt_create$1(gobj) {
72020
72164
  priv.baseline = {};
72021
72165
  priv.written = {};
72022
72166
  priv.host_drafts = {};
72167
+ priv.host_ids = {};
72023
72168
  priv.save_queue = [];
72024
72169
  build_ui$1(gobj);
72025
72170
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
@@ -72259,25 +72404,33 @@ function start_measuring(gobj) {
72259
72404
  let priv = gobj.priv;
72260
72405
  priv.baseline = {};
72261
72406
  priv.written = {};
72407
+ priv.host_ids = {};
72262
72408
  if (!priv.model) return;
72263
72409
  for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
72264
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
72410
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
72265
72411
  }
72266
72412
  /***************************************************************
72267
- * "This topic was written in this session and is not saved."
72413
+ * "This topic holds an edit that is not saved."
72268
72414
  *
72269
72415
  * An edit is a DRAFT: it moves no version and reaches no treedb
72270
72416
  * until the host saves it (C_TREEDB's save-schema, which raises the
72271
- * versions of what changed) -- and the save makes the host send
72272
- * EV_REFRESH, whose reload is what forgets the drafts.
72417
+ * versions of what changed). Two sources say a topic is one, and
72418
+ * they are kept APART because they are forgotten differently:
72419
+ * - `written`: what THIS session wrote since the model was loaded
72420
+ * (a reload, the host's EV_REFRESH, forgets it -- the store says
72421
+ * what is left);
72422
+ * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
72423
+ * by the next EV_DRAFTS. Folded into `written`, a topic the host
72424
+ * named once stayed a draft after the Save that published it.
72273
72425
  *
72274
72426
  * ONE predicate, because it is asked in three places -- the topic
72275
72427
  * list draws a chip with it, the column view a banner, the export
72276
72428
  * a warning -- and copies of a rule are how they drift.
72277
72429
  ***************************************************************/
72278
- function topic_is_draft(written, topic) {
72279
- if (!topic || !written) return false;
72280
- return written[topic.id] ? true : false;
72430
+ function topic_is_draft(priv, topic) {
72431
+ if (!topic || !priv) return false;
72432
+ if (priv.written && priv.written[topic.id]) return true;
72433
+ return priv.host_ids && priv.host_ids[topic.id] ? true : false;
72281
72434
  }
72282
72435
  /***************************************************************
72283
72436
  * Say what the whole store would be refused for.
@@ -72719,7 +72872,7 @@ function render_topics(gobj, $body) {
72719
72872
  let twice = repeated_names(treedb.topics);
72720
72873
  let $rows = [];
72721
72874
  for (let topic of treedb.topics) {
72722
- let pending = topic_is_draft(priv.written, topic);
72875
+ let pending = topic_is_draft(priv, topic);
72723
72876
  let ambiguous = !!twice[topic.name];
72724
72877
  let address = ambiguous ? topic.id : topic.name;
72725
72878
  let $actions = [];
@@ -73042,7 +73195,7 @@ function render_columns(gobj, $body) {
73042
73195
  ***************************************************************/
73043
73196
  function draft_banner(gobj, topic) {
73044
73197
  let priv = gobj.priv;
73045
- if (!topic_is_draft(priv.written, topic)) return null;
73198
+ if (!topic_is_draft(priv, topic)) return null;
73046
73199
  if (is_readonly(gobj)) return null;
73047
73200
  return (0, _yuneta_gobj_js.createElement2)([
73048
73201
  "div",
@@ -73932,7 +74085,7 @@ function open_report(gobj, findings, treedb_id) {
73932
74085
  ***************************************************************/
73933
74086
  function open_export(gobj, treedb) {
73934
74087
  let priv = gobj.priv;
73935
- let drafts = treedb.topics.some((topic) => topic_is_draft(priv.written, topic));
74088
+ let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
73936
74089
  let c_text = schema_to_c(treedb);
73937
74090
  let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
73938
74091
  let $content = (0, _yuneta_gobj_js.createElement2)([
@@ -74700,18 +74853,22 @@ function ac_refresh$1(gobj, event, kw, src) {
74700
74853
  }
74701
74854
  /***************************************************************
74702
74855
  * {drafts: {treedb_name: [topic names]}} -- the host says which
74703
- * topics hold a draft in __system__ that the file in use does not
74704
- * have (C_TREEDB's saved-schema, `draft_changed`). The mark of a
74705
- * write lived in this session's memory only, so a reload of the
74706
- * page, a reconnect or a refresh showed no draft while __system__
74707
- * still differed from the file (N13 of the 2026-09-22 review).
74708
- * Replaces what the host said before; what this session wrote
74709
- * stays marked.
74856
+ * topics hold a draft in __system__ that is not saved (C_TREEDB's
74857
+ * saved-schema, `draft_changed`). The mark of a write lived in this
74858
+ * session's memory only, so a reload of the page, a reconnect or a
74859
+ * refresh showed no draft while __system__ still differed (N13 of
74860
+ * the 2026-09-22 review).
74861
+ *
74862
+ * REPLACES what the host said before -- the host is the truth after
74863
+ * a reload, and a Save is exactly the host saying "none any more".
74864
+ * It only ever added, and a topic stayed a draft after the Save that
74865
+ * published it (M1 of the 2026-09-23 review). What this session wrote
74866
+ * stays marked: it is kept apart (see topic_is_draft()).
74710
74867
  ***************************************************************/
74711
74868
  function ac_drafts(gobj, event, kw, src) {
74712
74869
  let priv = gobj.priv;
74713
74870
  priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
74714
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
74871
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
74715
74872
  if (priv.model) render(gobj);
74716
74873
  return 0;
74717
74874
  }