@yuneta/gobj-ui 7.25.1 → 7.25.3

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
@@ -783,6 +783,12 @@ answer EVERY way the write can end: refused before it left (read-only, no
783
783
  session) as well as answered by the backend, or the form waits forever. A host
784
784
  that does not answer leaves the attribute off, and the form closes when the
785
785
  write is published, as before 7.23.193 — whatever the backend says.
786
+ `C_YUI_TREEDB_TOPICS` answers them all since 7.25.2: a write sent with no
787
+ session is refused before it is sent (the command would never leave, and
788
+ `C_IEVENT_CLI` answers `null` for that as for a command that went), and the
789
+ writes in flight when the transport closes are answered refused on the
790
+ transport edge (`form_writes_in_flight.js`), so the form comes back on what
791
+ was typed instead of staying busy for ever.
786
792
 
787
793
  ```js
788
794
  // A host of C_YUI_TREEDB_TOPIC_WITH_FORM that answers the form's writes
@@ -2015,6 +2021,22 @@ and the column screen says so in a banner; the host sends `EV_REFRESH` after a
2015
2021
  save, and the reload forgets the drafts. The export (C literal and JSON) warns
2016
2022
  while there are drafts: the literal carries the versions a save publishes.
2017
2023
 
2024
+ **A draft survives a reload** (7.25.3). The mark of a write lived in the
2025
+ session's memory only, so a reload of the page, a reconnect or a refresh of
2026
+ the model showed no draft while `__system__` still differed from the file in
2027
+ use. The host tells the editor what IS a draft, from data: `C_TREEDB`'s
2028
+ `saved-schema` answers `draft_changed` per treedb (yunetas 7.25.3+), and the
2029
+ host sends it as `EV_DRAFTS`; `drafts_of_saved_answer()` reads the unnamed
2030
+ answer into the shape the event takes. What the session wrote stays marked;
2031
+ what the host said is applied again after every reload of the model.
2032
+
2033
+ ```js
2034
+ // The host, when its saved-schema answers land (rows = the unnamed answer's data)
2035
+ import {drafts_of_saved_answer} from "@yuneta/gobj-ui/src/host_drafts.js";
2036
+ gobj_send_event(editor, "EV_DRAFTS", {drafts: drafts_of_saved_answer(rows)}, gobj);
2037
+ // -> {drafts: {treedb_authzs: ["users"]}}
2038
+ ```
2039
+
2018
2040
  What the screens offer:
2019
2041
 
2020
2042
  | Screen | What it is for |
@@ -23021,6 +23021,53 @@ function yui_shell_show_route_map(shell, opts) {
23021
23021
  return modal_ref.modal;
23022
23022
  }
23023
23023
  //#endregion
23024
+ //#region src/form_writes_in_flight.js
23025
+ /***********************************************************************
23026
+ * form_writes_in_flight.js
23027
+ *
23028
+ * 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).
23036
+ *
23037
+ * Copyright (c) 2026, ArtGins.
23038
+ * All Rights Reserved.
23039
+ ***********************************************************************/
23040
+ /************************************************************
23041
+ * A write left: remember whose it is
23042
+ ************************************************************/
23043
+ function track_form_write(in_flight, form_write, topic_name) {
23044
+ if (!in_flight || !form_write) return;
23045
+ in_flight[form_write] = topic_name || "";
23046
+ }
23047
+ /************************************************************
23048
+ * A write was answered, one way or the other
23049
+ ************************************************************/
23050
+ function settle_form_write(in_flight, form_write) {
23051
+ if (!in_flight || !form_write) return;
23052
+ delete in_flight[form_write];
23053
+ }
23054
+ /************************************************************
23055
+ * Nothing in flight can be answered any more: take them all,
23056
+ * as [{form_write, topic_name}], and leave the map empty.
23057
+ ************************************************************/
23058
+ function abandon_form_writes(in_flight) {
23059
+ const abandoned = [];
23060
+ if (!in_flight) return abandoned;
23061
+ Object.keys(in_flight).forEach((serial) => {
23062
+ abandoned.push({
23063
+ form_write: Number(serial),
23064
+ topic_name: in_flight[serial]
23065
+ });
23066
+ delete in_flight[serial];
23067
+ });
23068
+ return abandoned;
23069
+ }
23070
+ //#endregion
23024
23071
  //#region src/nodes_answer.js
23025
23072
  /***********************************************************************
23026
23073
  * nodes_answer.js
@@ -23587,6 +23634,7 @@ var __gclass__$6 = null;
23587
23634
  * Framework Method: Create
23588
23635
  ***************************************************************/
23589
23636
  function mt_create$6(gobj) {
23637
+ gobj.priv.form_writes = {};
23590
23638
  build_ui$6(gobj);
23591
23639
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
23592
23640
  if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
@@ -24743,6 +24791,7 @@ function treedb_update_node$1(gobj, treedb_name, topic_name, record, options, fo
24743
24791
  (0, _yuneta_gobj_js.log_error)(ret);
24744
24792
  return -1;
24745
24793
  }
24794
+ track_form_write(gobj.priv.form_writes, form_write, topic_name);
24746
24795
  return 0;
24747
24796
  }
24748
24797
  /************************************************************
@@ -25006,6 +25055,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25006
25055
  if (command === "update-node" || command === "create-node") {
25007
25056
  let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
25008
25057
  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));
25009
25059
  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);
25010
25060
  }
25011
25061
  }
@@ -25043,6 +25093,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
25043
25093
  created: command === "create-node",
25044
25094
  command
25045
25095
  });
25096
+ settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0));
25046
25097
  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);
25047
25098
  break;
25048
25099
  case "delete-node": break;
@@ -25193,7 +25244,12 @@ function ac_back_to_topics(gobj, event, kw, src) {
25193
25244
  * the session).
25194
25245
  ************************************************************/
25195
25246
  function ac_transport_state$2(gobj, event, kw, src) {
25196
- refresh_toolbar_buttons(gobj, !!(kw && kw.connected));
25247
+ let connected = !!(kw && kw.connected);
25248
+ refresh_toolbar_buttons(gobj, connected);
25249
+ if (!connected) abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
25250
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
25251
+ answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
25252
+ });
25197
25253
  return 0;
25198
25254
  }
25199
25255
  /************************************************************
@@ -25267,6 +25323,10 @@ function ac_create_record(gobj, event, kw, src) {
25267
25323
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
25268
25324
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
25269
25325
  let record = kw.record;
25326
+ if (refuse_without_session(gobj, topic_name)) {
25327
+ answer_form_write(gobj, src, form_write, false);
25328
+ return -1;
25329
+ }
25270
25330
  if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25271
25331
  list_dict: true,
25272
25332
  create: true,
@@ -25434,6 +25494,10 @@ function ac_update_record(gobj, event, kw, src) {
25434
25494
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
25435
25495
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
25436
25496
  let record = kw.record;
25497
+ if (refuse_without_session(gobj, topic_name)) {
25498
+ answer_form_write(gobj, src, form_write, false);
25499
+ return -1;
25500
+ }
25437
25501
  if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
25438
25502
  list_dict: true,
25439
25503
  autolink: true
@@ -25444,6 +25508,18 @@ function ac_update_record(gobj, event, kw, src) {
25444
25508
  return 0;
25445
25509
  }
25446
25510
  /************************************************************
25511
+ * A write sent with no session never leaves: C_IEVENT_CLI logs
25512
+ * "Not in session" and answers null, as it does when the command
25513
+ * went -- so a form waiting for the answer waited for ever (N8 of
25514
+ * the 2026-09-22 review). Refused before it is sent, aloud.
25515
+ ************************************************************/
25516
+ function refuse_without_session(gobj, topic_name) {
25517
+ if (is_connected$1(gobj)) return false;
25518
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no session, the write of '${topic_name}' was not sent`);
25519
+ yui_shell_show_error(yui_shell_of(gobj), "no session", { t: i18next.t });
25520
+ return true;
25521
+ }
25522
+ /************************************************************
25447
25523
  * Tell a topic form how its write ended: EV_WRITE_DONE closes
25448
25524
  * it, EV_WRITE_REFUSED leaves it open on what was typed. Every
25449
25525
  * way a form's write can end passes here -- refused before it
@@ -26511,6 +26587,49 @@ function delete_impact(desc, records) {
26511
26587
  return out;
26512
26588
  }
26513
26589
  //#endregion
26590
+ //#region src/form_busy.js
26591
+ /***********************************************************************
26592
+ * form_busy.js
26593
+ *
26594
+ * The toolbar of a topic form while a write is in flight: every
26595
+ * button disabled, the save button spinning. Bulma's `is-loading`
26596
+ * is the spinner; `disabled` is what stops the second click.
26597
+ *
26598
+ * A transition, not a setting: each button remembers whether it
26599
+ * was disabled BEFORE the toolbar went busy, and busy(false) puts
26600
+ * that back. So a second busy(true) while already busy must not
26601
+ * run at all -- it read the buttons it had just disabled as
26602
+ * "disabled on their own", and the busy(false) of a refused write
26603
+ * left Save and Cancel dead (N9 of the 2026-09-22 review: a record
26604
+ * with a picked file went busy for the read, then busy again for
26605
+ * the write). The caller owns `state` ({busy}) for the form's life.
26606
+ *
26607
+ * Copyright (c) 2026, ArtGins.
26608
+ * All Rights Reserved.
26609
+ ***********************************************************************/
26610
+ /************************************************************
26611
+ * Move the toolbar to `busy`. Returns true when it moved, false when
26612
+ * it was already there (nothing touched).
26613
+ ************************************************************/
26614
+ function set_toolbar_busy($container, state, busy) {
26615
+ busy = !!busy;
26616
+ if (!$container || !state) return false;
26617
+ if (!!state.busy === busy) return false;
26618
+ state.busy = busy;
26619
+ $container.querySelectorAll(".yui-toolbar-form button").forEach(($b) => {
26620
+ if (busy) {
26621
+ $b.dataset.was_disabled = $b.disabled ? "1" : "0";
26622
+ $b.disabled = true;
26623
+ } else if ($b.dataset.was_disabled !== void 0) {
26624
+ $b.disabled = $b.dataset.was_disabled === "1";
26625
+ delete $b.dataset.was_disabled;
26626
+ }
26627
+ });
26628
+ const $save = $container.querySelector(".yui-toolbar-form .button-save");
26629
+ if ($save) $save.classList.toggle("is-loading", busy);
26630
+ return true;
26631
+ }
26632
+ //#endregion
26514
26633
  //#region src/table_cell_text.js
26515
26634
  /****************************************************************************
26516
26635
  * table_cell_text.js
@@ -26795,6 +26914,7 @@ function mt_create$5(gobj) {
26795
26914
  gobj.priv.read_serial = 0;
26796
26915
  gobj.priv.awaiting_write = 0;
26797
26916
  gobj.priv.write_serial = 0;
26917
+ gobj.priv.form_busy = { busy: false };
26798
26918
  let name = (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj));
26799
26919
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "table_id", "table" + name);
26800
26920
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "toolbar_id", "toolbar" + name);
@@ -28198,6 +28318,7 @@ function teardown_form_child(gobj) {
28198
28318
  let priv = gobj.priv;
28199
28319
  priv.reading_files = 0;
28200
28320
  priv.awaiting_write = 0;
28321
+ priv.form_busy = { busy: false };
28201
28322
  if (priv.form) {
28202
28323
  if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
28203
28324
  (0, _yuneta_gobj_js.gobj_destroy)(priv.form);
@@ -29422,26 +29543,16 @@ function ac_form_save_record(gobj, event, kw, src) {
29422
29543
  return 0;
29423
29544
  }
29424
29545
  /************************************************************
29425
- * The form's toolbar while a read is in flight: every button
29426
- * disabled, the save button spinning. Bulma's `is-loading` is the
29427
- * spinner; `disabled` is what stops the second click.
29546
+ * The form's toolbar while a read or a write is in flight: every
29547
+ * button disabled, the save button spinning (set_toolbar_busy). A
29548
+ * transition: busy(true) while busy touches nothing, or a record
29549
+ * with a picked file -- busy for the read, busy again for the
29550
+ * write -- came back from a refusal with Save and Cancel dead.
29428
29551
  ************************************************************/
29429
29552
  function set_form_busy(gobj, busy) {
29430
29553
  let priv = gobj.priv;
29431
29554
  if (!priv.form) return;
29432
- const $container = (0, _yuneta_gobj_js.gobj_read_attr)(priv.form, "$container");
29433
- if (!$container) return;
29434
- $container.querySelectorAll(".yui-toolbar-form button").forEach(($b) => {
29435
- if (busy) {
29436
- $b.dataset.was_disabled = $b.disabled ? "1" : "0";
29437
- $b.disabled = true;
29438
- } else if ($b.dataset.was_disabled !== void 0) {
29439
- $b.disabled = $b.dataset.was_disabled === "1";
29440
- delete $b.dataset.was_disabled;
29441
- }
29442
- });
29443
- const $save = $container.querySelector(".yui-toolbar-form .button-save");
29444
- if ($save) $save.classList.toggle("is-loading", !!busy);
29555
+ set_toolbar_busy((0, _yuneta_gobj_js.gobj_read_attr)(priv.form, "$container"), priv.form_busy, busy);
29445
29556
  }
29446
29557
  /************************************************************
29447
29558
  * Is this the read the view is waiting for? A read whose form was
@@ -70144,6 +70255,44 @@ function register_c_yui_treedb_schema() {
70144
70255
  return create_gclass$2(GCLASS_NAME$2);
70145
70256
  }
70146
70257
  //#endregion
70258
+ //#region src/host_drafts.js
70259
+ /***********************************************************************
70260
+ * host_drafts.js
70261
+ *
70262
+ * The topics a schema editor shows as DRAFTS (edited in
70263
+ * __system__, not yet saved), told by the host from data. The
70264
+ * editor marks a topic when a write of this session lands on
70265
+ * it, and that mark lived in the memory of the session only:
70266
+ * a reload of the page, a reconnect or a refresh of the model
70267
+ * lost the chip, the banner and the export warning while
70268
+ * __system__ still differed from the file in use (N13 of the
70269
+ * 2026-09-22 review). The host reads that difference from
70270
+ * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
70271
+ * hands it here; the editor applies it to its model.
70272
+ *
70273
+ * Copyright (c) 2026, ArtGins.
70274
+ * All Rights Reserved.
70275
+ ***********************************************************************/
70276
+ /************************************************************
70277
+ * Mark in `written` the topics of `model` the host names as
70278
+ * drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
70279
+ * topic the model does not hold is ignored. Returns how many were
70280
+ * marked.
70281
+ ************************************************************/
70282
+ function mark_host_drafts(written, model, drafts) {
70283
+ let marked = 0;
70284
+ if (!written || !model || !Array.isArray(model.treedbs) || !drafts) return marked;
70285
+ for (let treedb of model.treedbs) {
70286
+ const names = drafts[treedb.id];
70287
+ if (!Array.isArray(names) || names.length === 0) continue;
70288
+ for (let topic of treedb.topics || []) if (names.includes(topic.name)) {
70289
+ written[topic.id] = true;
70290
+ marked++;
70291
+ }
70292
+ }
70293
+ return marked;
70294
+ }
70295
+ //#endregion
70147
70296
  //#region src/schema_validate.js
70148
70297
  /***********************************************************************
70149
70298
  * schema_validate.js
@@ -71870,6 +72019,7 @@ function mt_create$1(gobj) {
71870
72019
  let priv = gobj.priv;
71871
72020
  priv.baseline = {};
71872
72021
  priv.written = {};
72022
+ priv.host_drafts = {};
71873
72023
  priv.save_queue = [];
71874
72024
  build_ui$1(gobj);
71875
72025
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
@@ -72111,6 +72261,7 @@ function start_measuring(gobj) {
72111
72261
  priv.written = {};
72112
72262
  if (!priv.model) return;
72113
72263
  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);
72114
72265
  }
72115
72266
  /***************************************************************
72116
72267
  * "This topic was written in this session and is not saved."
@@ -74548,6 +74699,23 @@ function ac_refresh$1(gobj, event, kw, src) {
74548
74699
  return request_model(gobj);
74549
74700
  }
74550
74701
  /***************************************************************
74702
+ * {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.
74710
+ ***************************************************************/
74711
+ function ac_drafts(gobj, event, kw, src) {
74712
+ let priv = gobj.priv;
74713
+ priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
74714
+ mark_host_drafts(priv.written, priv.model, priv.host_drafts);
74715
+ if (priv.model) render(gobj);
74716
+ return 0;
74717
+ }
74718
+ /***************************************************************
74551
74719
  * Moving about.
74552
74720
  ***************************************************************/
74553
74721
  function ac_select_treedb(gobj, event, kw, src) {
@@ -75021,6 +75189,11 @@ function create_gclass$1(gclass_name) {
75021
75189
  "EV_REFRESH",
75022
75190
  ac_refresh$1,
75023
75191
  null
75192
+ ],
75193
+ [
75194
+ "EV_DRAFTS",
75195
+ ac_drafts,
75196
+ null
75024
75197
  ]
75025
75198
  ];
75026
75199
  const TREEDB_WIDE = [
@@ -75169,6 +75342,7 @@ function create_gclass$1(gclass_name) {
75169
75342
  ["EV_TRANSPORT_STATE", 0],
75170
75343
  ["EV_LANGUAGE_CHANGED", 0],
75171
75344
  ["EV_REFRESH", 0],
75345
+ ["EV_DRAFTS", 0],
75172
75346
  ["EV_SELECT_TREEDB", 0],
75173
75347
  ["EV_SELECT_TOPIC", 0],
75174
75348
  ["EV_BACK", 0],