@yuneta/gobj-ui 7.25.4 → 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);
@@ -29573,6 +29680,7 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
29573
29680
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
29574
29681
  record: strip_read_only_cols(gobj, record)
29575
29682
  };
29683
+ if (mode !== "create" && gobj.priv.form_instance) Object.assign(kw_write.record, gobj.priv.form_instance);
29576
29684
  if (form_write) kw_write.form_write = form_write;
29577
29685
  if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
29578
29686
  gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
@@ -70273,16 +70381,39 @@ function register_c_yui_treedb_schema() {
70273
70381
  * __system__ still differed from the file in use (N13 of the
70274
70382
  * 2026-09-22 review). The host reads that difference from
70275
70383
  * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70276
- * 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.
70277
70392
  *
70278
70393
  * Copyright (c) 2026, ArtGins.
70279
70394
  * All Rights Reserved.
70280
70395
  ***********************************************************************/
70281
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
+ /************************************************************
70282
70412
  * Mark in `written` the topics of `model` the host names as
70283
70413
  * drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
70284
70414
  * topic the model does not hold is ignored. Returns how many were
70285
- * 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()).
70286
70417
  ************************************************************/
70287
70418
  function mark_host_drafts(written, model, drafts) {
70288
70419
  let marked = 0;
@@ -71921,8 +72052,9 @@ function write_options(op) {
71921
72052
  * the owner's design of M36 (2026-09-21 review) every write here
71922
72053
  * raised both versions, so an edit half made was already the
71923
72054
  * schema of the next start. The topic list marks what this session
71924
- * wrote and has not saved; the host's EV_REFRESH after a save is
71925
- * 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).
71926
72058
  *
71927
72059
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
71928
72060
  *
@@ -72001,6 +72133,8 @@ var PRIVATE_DATA$1 = {
72001
72133
  diagram: false,
72002
72134
  baseline: null,
72003
72135
  written: null,
72136
+ host_drafts: null,
72137
+ host_ids: null,
72004
72138
  order_undo: null,
72005
72139
  diagram_gobj: null,
72006
72140
  dialog: null,
@@ -72025,6 +72159,7 @@ function mt_create$1(gobj) {
72025
72159
  priv.baseline = {};
72026
72160
  priv.written = {};
72027
72161
  priv.host_drafts = {};
72162
+ priv.host_ids = {};
72028
72163
  priv.save_queue = [];
72029
72164
  build_ui$1(gobj);
72030
72165
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
@@ -72264,25 +72399,33 @@ function start_measuring(gobj) {
72264
72399
  let priv = gobj.priv;
72265
72400
  priv.baseline = {};
72266
72401
  priv.written = {};
72402
+ priv.host_ids = {};
72267
72403
  if (!priv.model) return;
72268
72404
  for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
72269
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
72405
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
72270
72406
  }
72271
72407
  /***************************************************************
72272
- * "This topic was written in this session and is not saved."
72408
+ * "This topic holds an edit that is not saved."
72273
72409
  *
72274
72410
  * An edit is a DRAFT: it moves no version and reaches no treedb
72275
72411
  * until the host saves it (C_TREEDB's save-schema, which raises the
72276
- * versions of what changed) -- and the save makes the host send
72277
- * 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.
72278
72420
  *
72279
72421
  * ONE predicate, because it is asked in three places -- the topic
72280
72422
  * list draws a chip with it, the column view a banner, the export
72281
72423
  * a warning -- and copies of a rule are how they drift.
72282
72424
  ***************************************************************/
72283
- function topic_is_draft(written, topic) {
72284
- if (!topic || !written) return false;
72285
- 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;
72286
72429
  }
72287
72430
  /***************************************************************
72288
72431
  * Say what the whole store would be refused for.
@@ -72724,7 +72867,7 @@ function render_topics(gobj, $body) {
72724
72867
  let twice = repeated_names(treedb.topics);
72725
72868
  let $rows = [];
72726
72869
  for (let topic of treedb.topics) {
72727
- let pending = topic_is_draft(priv.written, topic);
72870
+ let pending = topic_is_draft(priv, topic);
72728
72871
  let ambiguous = !!twice[topic.name];
72729
72872
  let address = ambiguous ? topic.id : topic.name;
72730
72873
  let $actions = [];
@@ -73047,7 +73190,7 @@ function render_columns(gobj, $body) {
73047
73190
  ***************************************************************/
73048
73191
  function draft_banner(gobj, topic) {
73049
73192
  let priv = gobj.priv;
73050
- if (!topic_is_draft(priv.written, topic)) return null;
73193
+ if (!topic_is_draft(priv, topic)) return null;
73051
73194
  if (is_readonly(gobj)) return null;
73052
73195
  return createElement2([
73053
73196
  "div",
@@ -73937,7 +74080,7 @@ function open_report(gobj, findings, treedb_id) {
73937
74080
  ***************************************************************/
73938
74081
  function open_export(gobj, treedb) {
73939
74082
  let priv = gobj.priv;
73940
- let drafts = treedb.topics.some((topic) => topic_is_draft(priv.written, topic));
74083
+ let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
73941
74084
  let c_text = schema_to_c(treedb);
73942
74085
  let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
73943
74086
  let $content = createElement2([
@@ -74705,18 +74848,22 @@ function ac_refresh$1(gobj, event, kw, src) {
74705
74848
  }
74706
74849
  /***************************************************************
74707
74850
  * {drafts: {treedb_name: [topic names]}} -- the host says which
74708
- * topics hold a draft in __system__ that the file in use does not
74709
- * have (C_TREEDB's saved-schema, `draft_changed`). The mark of a
74710
- * write lived in this session's memory only, so a reload of the
74711
- * page, a reconnect or a refresh showed no draft while __system__
74712
- * still differed from the file (N13 of the 2026-09-22 review).
74713
- * Replaces what the host said before; what this session wrote
74714
- * 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()).
74715
74862
  ***************************************************************/
74716
74863
  function ac_drafts(gobj, event, kw, src) {
74717
74864
  let priv = gobj.priv;
74718
74865
  priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
74719
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
74866
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
74720
74867
  if (priv.model) render(gobj);
74721
74868
  return 0;
74722
74869
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.25.4",
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",
@@ -26,8 +26,9 @@
26
26
  * the owner's design of M36 (2026-09-21 review) every write here
27
27
  * raised both versions, so an edit half made was already the
28
28
  * schema of the next start. The topic list marks what this session
29
- * wrote and has not saved; the host's EV_REFRESH after a save is
30
- * what forgets it.
29
+ * wrote and has not saved (the host's EV_REFRESH after a save is
30
+ * what forgets it) and what the host says is not saved (EV_DRAFTS,
31
+ * replaced whole each time it is sent).
31
32
  *
32
33
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
33
34
  *
@@ -113,7 +114,7 @@ import {
113
114
  empty_string,
114
115
  } from "@yuneta/gobj-js";
115
116
  import {yui_tint} from "./bulma_tint.js";
116
- import {mark_host_drafts} from "./host_drafts.js";
117
+ import {host_draft_ids} from "./host_drafts.js";
117
118
 
118
119
  import "./c_yui_schema_editor.css";
119
120
 
@@ -207,6 +208,8 @@ let PRIVATE_DATA = {
207
208
  * version still has to move. */
208
209
  baseline: null, /* {topic id: topic_version} at load time */
209
210
  written: null, /* {topic id: true} */
211
+ host_drafts: null, /* {treedb: [topic names]}, the host's last EV_DRAFTS */
212
+ host_ids: null, /* host_drafts resolved on the model, {topic id: true} */
210
213
  order_undo: null, /* {topic id: [{id, order}]} before the first drag */
211
214
 
212
215
  /* Children and overlays */
@@ -250,6 +253,7 @@ function mt_create(gobj)
250
253
  priv.baseline = {};
251
254
  priv.written = {};
252
255
  priv.host_drafts = {}; /* what the host says is a draft, {treedb: [topics]} (EV_DRAFTS) */
256
+ priv.host_ids = {}; /* the same, as {topic id: true} of the model */
253
257
  priv.save_queue = [];
254
258
 
255
259
  build_ui(gobj);
@@ -564,6 +568,7 @@ function start_measuring(gobj)
564
568
 
565
569
  priv.baseline = {};
566
570
  priv.written = {};
571
+ priv.host_ids = {};
567
572
 
568
573
  if(!priv.model) {
569
574
  return;
@@ -573,29 +578,39 @@ function start_measuring(gobj)
573
578
  priv.baseline[topic.id] = topic.topic_version;
574
579
  }
575
580
  }
576
- /* A draft is a draft after a reload too: what the host said stays
577
- * marked (host_drafts.js) until the host says otherwise. */
578
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
581
+ /* A draft is a draft after a reload too: what the host said last
582
+ * is resolved against the new model (host_drafts.js), apart from
583
+ * what this session writes, until the host says otherwise. */
584
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
579
585
  }
580
586
 
581
587
  /***************************************************************
582
- * "This topic was written in this session and is not saved."
588
+ * "This topic holds an edit that is not saved."
583
589
  *
584
590
  * An edit is a DRAFT: it moves no version and reaches no treedb
585
591
  * until the host saves it (C_TREEDB's save-schema, which raises the
586
- * versions of what changed) -- and the save makes the host send
587
- * EV_REFRESH, whose reload is what forgets the drafts.
592
+ * versions of what changed). Two sources say a topic is one, and
593
+ * they are kept APART because they are forgotten differently:
594
+ * - `written`: what THIS session wrote since the model was loaded
595
+ * (a reload, the host's EV_REFRESH, forgets it -- the store says
596
+ * what is left);
597
+ * - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
598
+ * by the next EV_DRAFTS. Folded into `written`, a topic the host
599
+ * named once stayed a draft after the Save that published it.
588
600
  *
589
601
  * ONE predicate, because it is asked in three places -- the topic
590
602
  * list draws a chip with it, the column view a banner, the export
591
603
  * a warning -- and copies of a rule are how they drift.
592
604
  ***************************************************************/
593
- function topic_is_draft(written, topic)
605
+ function topic_is_draft(priv, topic)
594
606
  {
595
- if(!topic || !written) {
607
+ if(!topic || !priv) {
596
608
  return false;
597
609
  }
598
- return written[topic.id]? true: false;
610
+ if(priv.written && priv.written[topic.id]) {
611
+ return true;
612
+ }
613
+ return (priv.host_ids && priv.host_ids[topic.id])? true: false;
599
614
  }
600
615
 
601
616
  /***************************************************************
@@ -1050,7 +1065,7 @@ function render_topics(gobj, $body)
1050
1065
  let twice = repeated_names(treedb.topics);
1051
1066
  let $rows = [];
1052
1067
  for(let topic of treedb.topics) {
1053
- let pending = topic_is_draft(priv.written, topic);
1068
+ let pending = topic_is_draft(priv, topic);
1054
1069
  /* Two topics of one name: the id is what tells them apart, and
1055
1070
  * it is what the url has to carry — addressing by name would
1056
1071
  * open the first one whichever row was clicked. */
@@ -1256,7 +1271,7 @@ function draft_banner(gobj, topic)
1256
1271
  {
1257
1272
  let priv = gobj.priv;
1258
1273
 
1259
- if(!topic_is_draft(priv.written, topic)) {
1274
+ if(!topic_is_draft(priv, topic)) {
1260
1275
  return null;
1261
1276
  }
1262
1277
  if(is_readonly(gobj)) {
@@ -1997,7 +2012,7 @@ function open_report(gobj, findings, treedb_id)
1997
2012
  function open_export(gobj, treedb)
1998
2013
  {
1999
2014
  let priv = gobj.priv;
2000
- let drafts = treedb.topics.some((topic) => topic_is_draft(priv.written, topic));
2015
+ let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
2001
2016
  let c_text = schema_to_c(treedb);
2002
2017
  let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
2003
2018
 
@@ -2762,20 +2777,24 @@ function ac_refresh(gobj, event, kw, src)
2762
2777
 
2763
2778
  /***************************************************************
2764
2779
  * {drafts: {treedb_name: [topic names]}} -- the host says which
2765
- * topics hold a draft in __system__ that the file in use does not
2766
- * have (C_TREEDB's saved-schema, `draft_changed`). The mark of a
2767
- * write lived in this session's memory only, so a reload of the
2768
- * page, a reconnect or a refresh showed no draft while __system__
2769
- * still differed from the file (N13 of the 2026-09-22 review).
2770
- * Replaces what the host said before; what this session wrote
2771
- * stays marked.
2780
+ * topics hold a draft in __system__ that is not saved (C_TREEDB's
2781
+ * saved-schema, `draft_changed`). The mark of a write lived in this
2782
+ * session's memory only, so a reload of the page, a reconnect or a
2783
+ * refresh showed no draft while __system__ still differed (N13 of
2784
+ * the 2026-09-22 review).
2785
+ *
2786
+ * REPLACES what the host said before -- the host is the truth after
2787
+ * a reload, and a Save is exactly the host saying "none any more".
2788
+ * It only ever added, and a topic stayed a draft after the Save that
2789
+ * published it (M1 of the 2026-09-23 review). What this session wrote
2790
+ * stays marked: it is kept apart (see topic_is_draft()).
2772
2791
  ***************************************************************/
2773
2792
  function ac_drafts(gobj, event, kw, src)
2774
2793
  {
2775
2794
  let priv = gobj.priv;
2776
2795
 
2777
2796
  priv.host_drafts = (kw && kw.drafts && typeof kw.drafts === "object")? kw.drafts: {};
2778
- mark_host_drafts(priv.written, priv.model, priv.host_drafts);
2797
+ priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
2779
2798
  if(priv.model) {
2780
2799
  render(gobj);
2781
2800
  }